Deep-Symmetry/bytefield-svg

Long boxes

lorrden opened this issue · 11 comments

When working with autogenerated diagrams of network packets (bit level), we sometimes end up with fields crossing multiple lines and being in general unhelpfully unaligned.

Due to the unalignedness, it is hard to define a row length that will fit all values, in some cases this causes boxes to spill over into the next row, which generates an exception.

For example, a protocol has a double, aligned at a byte boundary, we then use draw-box with a span of 8.

Can we get a function like: draw-long-box or something like that that handles overflow into next line automatically? This of-course raises the issue as to what to draw in the lines (e.g. repeating the label, continuation symbol of some sort, label at the longest box sequence (i.e. the second row if the sequence is 1, 7)).

This seems more specialized than will admit of a general purpose solution. I’d encourage you to write something that works for you, and if you feel it is general enough after working with it for a while, you could submit it as a pull request.

What I have done in the few situations where my diagrams needed something like this was to draw boxes individually on the rows that needed them, controlling the borders appropriately to make it look the way I want.

My goal here was to make all diagrams possible, and really common cases easy.

A good starting point for inspiration might be the code that implements draw-related-boxes which does similar kinds of multi-line border management:

(defn- related?
"Checks whether the specified `border` direction from the cell at the
specified `address` is with a cell that is part of a group of boxes
being drawn. The group ranges from address `start` until just before
`end`."
[address start end border]
(let [width @('boxes-per-row @*globals*)
column (mod address width)
neighbor (case border
:left (dec address)
:right (inc address)
:top (- address width)
:bottom (+ address width))
edge (boolean (or (and (= border :left) (zero? column))
(and (= border :right) (= column (dec width)))))]
(and (<= start neighbor (dec end))
(not edge))))
(defn draw-related-boxes
"Draws multiple boxes with the same attributes for each. Borders
between boxes that are both generated by this invocation will be
styled as `:border-related`, while borders with other boxes or the
outside of the table will be styled as `:border-unrelated`."
([labels]
(draw-related-boxes labels nil))
([labels attr-spec]
(let [attrs (eval-attribute-spec attr-spec)
{:keys [:address :column]} @@('diagram-state @*globals*)
start (+ address column)
span (:span attrs 1)
end (+ start (* span (count labels)))]
(doseq [[i label] (map-indexed (fn [i label] [i label]) labels)]
(let [borders (into {} (map (fn [border]
[border (if (related? (+ start (* span i)) start end border)
:border-related
:border-unrelated)])
[:left :right :top :bottom]))]
(draw-box label (assoc attrs :borders borders)))))))

Here’s an example page with a diagram where I had to create long boxes: https://djl-analysis.deepsymmetry.org/djl-analysis/beats.html

My pages use the shared function draw-packet-header to draw this, and it’s an example of one of the approaches I took. There were variations in different include files for different packet types; each place which required thought about where to draw the label, and so on. https://github.com/Deep-Symmetry/dysentery/blob/38d961e6e54ec4dfdfee6475d57fd10e64fc4b56/doc/modules/ROOT/examples/status_shared.edn#L12-L21

But this reminds me that I do provide some predefined named attributes to make the border management easier, like :box-above and :box-below.

Those are very good examples, thanks. I suppose what is missing is a style that only draws its left/right border, that would let us define a long box spanning more than two rows.

Good idea. I didn’t run into any of those in my own diagrams that were not also variable-length, so I used draw-gap for them, but I will sure add one as part of the mini release I am working up to.

In the mean time you can just set those borders individually by looking at how the predefined attributes are set up, and set up your own predefined attribute if you want.

@lorrden in case you haven’t found them yet, here’s where the predefined attributes you are using are defined:

:box-above {:borders #{:left :right :top}} ; Style for box open to row below.
:box-above-related {:borders {:left :border-related ; Stle for box open to row below, related to previous box.
:right :border-unrelated
:top :border-unrelated}}
:box-below {:borders #{:left :right :bottom}}}) ; Style for box open to row above.

My biggest problem is that I can’t think of a good keyword to use to name one that is open on both the top on the bottom. :box-middle would be ambiguous with :box-first and :box-last in the :box-related group, unfortunately. Also, it’s not clear to me that it is worth setting one up as a named attribute, since you can use {:borders #{:left :right}} to draw such a box; that isn’t too much to type.

Maybe :box-open-row? Nothing feels completely right.

I guess :box-left-right would be the least ambiguous. But again, that doesn’t save many characters compared to just spelling out the borders explicitly, as {:borders #{:left :right}}, and I worry that never doing that gets people stuck thinking they need to use my special predefined attributes to control their borders, rather than realizing they are simply convenience shorthand.

As the above commit suggests, I have decided the best way to deal with this is by improving the documentation to make my intentions here more easy to discover. If you look at bottom of the master version of the predefined attributes guide I have added a tip there about how to approach this. Please let me know if you think this is inadequate.

I will hold off on my next release in case you come up with other ideas that merit including in it, since you seem to be actively working on things. 😄

Everything is possible, I see that now, so docs improvements is all that is needed in the end.

Again, an absolutely awesome tool :)