How layout works
drumlin encodes the habits of a careful draftsman as rules.
Component, package and state diagrams share the layered layout and router described below. Sequence diagrams use their own timeline layout: steps resolve execution depth, columns are spaced from labels and frames, and rows follow the page. Activity diagrams use a structured-flow layout driven by the block tree, one measure stage and one place stage.
Steps
Each step enforces some of the drawing principles. The principles say what a drawing should look like; the steps below are how drumlin gets there.
Compound layout
Each container is laid out bottom-up as a layered graph of its direct children. An inner layout becomes one large item in its parent. See Group what belongs together.
Ranking
Connectors and hints assign each item a rank. Long edges get invisible dummy items that hold a lane through the ranks between. See Flow one way.
Crossing reduction
Weighted median sweeps reorder each rank, alternating down and up, and a transpose pass swaps neighbours while that removes crossings. The sweeps run twice, from the input order and from a depth-first order, so a tree starts without crossings, and the best ordering seen wins. Hints and notes are applied inside every sweep, so the ordering is scored as it will be drawn. See Order ranks to avoid crossings.
Placement
An order-preserving median solve aligns connected items and snaps everything to an 8px grid. The dummies of each long edge then line up where both ends can take the line, so it runs straight, or where one end can, so it bends only once. Edges between the same two items move as a block. Pendant items, such as an initial state above its first state or a final state below its last, move in line with what they hang from. State transition labels get a reserved place in the gap between ranks. See Keep long edges straight and Reserve room for labels.
Box growth
Where a connector jogs because its end boxes miss each other by a step or two, core::grow grows
one end box by up to 16px on a side toward the other end, into free space, and routes the diagram
again. A growth is kept only when that connector loses a bend, no crossing, overlap or bend total
rises, and no two connectors come to leave one point together. Growth across the flow is tried
first, and only growth that lets the two boxes share a span is routed. See
Grow a box rather than bend a line.
Slots
Attachment points slide along box sides toward their peers. This is what makes most connectors straight. They are spread 16px from corners, and nudging may slide an end to 8px from a square corner. A package without visible contents also takes ends on top of its blank tab. See Spread and order attachment points, Keep ends off corners and Let arrows end on a package’s tab.
Routing
A* runs over a sparse grid with costs for length, bends, crossings, shared lanes and travel along container boundaries. Three rip-up rounds settle conflicts. See Draw lines orthogonally and Avoid crossings.
Nudging
Each segment then moves along its own axis to where its line overlaps, crosses, crowds and bends least, in that order, and never through a box, a reserved label or a container boundary. Two lines tangled in one channel move together. Jogs fold away where a straighter path is clear, and an end may slide along its side, between its neighbours, to straighten its line. Finally, the middle runs of Z-shaped lines that share a channel spread evenly across it, and a lone one sits in the middle of its gap. See Never lay lines on top of each other and Space parallel lines evenly.
L over Z
After nudging, a line shaped like a Z, whose two end runs are parallel, becomes an L when one of its
ends may move to the side of its box that faces the line: the first run continues past the box’s
corner and turns into that side. Adapters say which ends may move by giving a terminal its box
(router::Body). The L is kept only when it meets no obstacle or strip more than the Z did, its new end
keeps SLIDE_GAP from other ends on that side and 16px from the corners, it gains no overlap,
crossing or close run, and no two lines come to leave one point together. Among the Ls that pass, the
best standing wins, then the end nearest the middle of its side. Because it is part of router::route,
box growth and the label hybrid see the L. See
Prefer an L to a Z when the L is clear.
Labels after routing
State diagrams (diagrams/state/hybrid.rs): after box growth, the diagram is routed with every label
reserved. Each labelled line that still bends is then freed: routed without its label’s waypoint, with
its label no longer an obstacle. Each freed label is placed beside its new line: five positions along
every segment, on both sides, ink 8px off the line, on a segment that reaches 8px past the label. A
place must touch no line, arrowhead, shape or label (4px air, 8px from other labels, 6px from outlines
and region dividers), stay inside the diagram, sit within 12px of its own line, keep the rest of its own
line as far, and keep every other line 12px further. The best place prefers the flowing side (right of
a vertical run when the flow is down, above a horizontal run when it is right), the middle of long runs,
and room from other lines. A label with no place, or whose line gains no straightness or gains a
crossing, is pinned back to its spot and the rest are routed again, up to four rounds. The result
replaces the reserved drawing only when it has fewer bends and no more crossings, overlaps, merged
ends, failed routes or label flaws (metrics::read_labels). Each round costs one route.
Labels
Component and package labels, and state labels with no reserved room, take the position with the fewest collisions. See Labels.
Measuring quality
The quality test in src/diagrams/quality.rs counts crossings, overlapping runs,
bends and merged ends (two connectors leaving one point together) over every
component, package and state golden file, and fails if a change raises a total. Run cargo test --lib quality -- --nocapture for the table.
The same test reads every connector label in those files (core::metrics::read_labels): labels within
2px of a line, outline or arrowhead, labels on shapes or other labels, labels with another line at least
as near as their own (ambiguous), labels more than 12px from their own line, and the summed distance
from each label to its own line. Each kind builds this as a Scene with owners
(src/diagrams/connectors.rs). Interface names on assembly connectors read with their ball and count
only as text to keep clear of. Set DRUMLIN_LABELS=state/<name> and run the explain_labels test to
list the flaws in one golden file.
Sequence and activity diagrams have no router, so the same test measures their whole drawing instead.
Each kind describes its output as a Scene (src/diagrams/<kind>/scene.rs): connectors, opaque shapes,
shapes drawn under them, strokes text must avoid, free text, contained text and gaps meant to be equal.
core::metrics::assess then counts, per kind, only what layout decides:
| Count | Sequence | Activity |
|---|---|---|
| Crossings and overlapping runs | between messages | between flows and note links |
| Bends | not counted: a self message always has two | every flow |
| Lines through shapes | messages through heads, notes, refs, tabs | flows through nodes and notes |
| Shape overlaps | tabs or notes over execution bars, shapes on shapes, shapes across frame edges | nodes and notes on each other or across partition dividers (fork bars may span) |
| Labels on lines | message labels and guards within 2px of a message, lifeline, frame edge, arrowhead or dot | guards within 2px of a flow, link, divider or arrowhead |
| Labels on shapes and labels | against heads, bars, notes, refs, tabs | against nodes, notes, lane heads |
| Text overflow | names in heads, note and ref text, operators in tabs | names in nodes, note text, lane names |
| Uneven gaps | rows of consecutive one-line messages with nothing between | straight drops from node to node, except into a merge or bar |
Messages crossing lifelines and bars are the notation, so they are not counted. Diamonds and circles
count as the largest square inside them, because flows end on their outline. A gap is uneven when it
differs from the most common gap by more than half a grid step. Set DRUMLIN_SCENE=activity/<name> and
run the explain_scene test to list what was counted in one golden file.
Style rules
These are stated with their reasons in Shapes and spacing.
- Every box and line coordinate sits on an 8px grid.
- Lines are orthogonal with small rounded corners.
- All shapes use one line weight, one ink colour and a white fill.
- Text widths come from embedded Helvetica metrics, so layout needs no font files and is identical on every platform.
The measured effect of further changes (label freedom, names giving way, larger box growth) is in Layout levers. Box growth, the 8px corner inset and tab ends are in place since stage 8; L over Z and state labels after routing since stage 10.