Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

How layout works

drumlin encodes the habits of a careful draftsman as rules.

Layout pipeline Parser Compound Layout Slot Allocation Router Nudging Label Placement SVG Writer L over Z model rectangles terminals polylines polylines polylines

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:

CountSequenceActivity
Crossings and overlapping runsbetween messagesbetween flows and note links
Bendsnot counted: a self message always has twoevery flow
Lines through shapesmessages through heads, notes, refs, tabsflows through nodes and notes
Shape overlapstabs or notes over execution bars, shapes on shapes, shapes across frame edgesnodes and notes on each other or across partition dividers (fork bars may span)
Labels on linesmessage labels and guards within 2px of a message, lifeline, frame edge, arrowhead or dotguards within 2px of a flow, link, divider or arrowhead
Labels on shapes and labelsagainst heads, bars, notes, refs, tabsagainst nodes, notes, lane heads
Text overflownames in heads, note and ref text, operators in tabsnames in nodes, note text, lane names
Uneven gapsrows of consecutive one-line messages with nothing betweenstraight 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.