dsh-model-everything 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.en-US.md +12 -0
  2. package/README.md +4 -0
  3. package/lib/client.js +2829 -224
  4. package/lib/render/channels.js +243 -0
  5. package/lib/render/collapse.js +12 -20
  6. package/lib/render/components.js +99 -0
  7. package/lib/render/coords.js +288 -0
  8. package/lib/render/cycle-break.js +147 -0
  9. package/lib/render/dummy.js +43 -0
  10. package/lib/render/from-mermaid.js +34 -10
  11. package/lib/render/from-plantuml.js +318 -46
  12. package/lib/render/layer.js +91 -0
  13. package/lib/render/layout.js +27 -11
  14. package/lib/render/network-simplex.js +272 -0
  15. package/lib/render/order.js +122 -0
  16. package/lib/render/paint.js +1227 -69
  17. package/lib/render/place.js +393 -121
  18. package/lib/render/route.js +225 -0
  19. package/lib/render/svg.js +146 -21
  20. package/lib/render/symbol.js +108 -0
  21. package/lib/render/text.js +74 -0
  22. package/lib/render/trunk.js +213 -0
  23. package/lib/render/wrap.js +87 -0
  24. package/lib/types/render/channels.d.ts +63 -0
  25. package/lib/types/render/components.d.ts +57 -0
  26. package/lib/types/render/coords.d.ts +64 -0
  27. package/lib/types/render/cycle-break.d.ts +45 -0
  28. package/lib/types/render/dummy.d.ts +32 -0
  29. package/lib/types/render/from-plantuml.d.ts +12 -6
  30. package/lib/types/render/layer.d.ts +28 -0
  31. package/lib/types/render/layout.d.ts +43 -11
  32. package/lib/types/render/model.d.ts +17 -0
  33. package/lib/types/render/network-simplex.d.ts +54 -0
  34. package/lib/types/render/order.d.ts +27 -0
  35. package/lib/types/render/paint.d.ts +157 -10
  36. package/lib/types/render/place.d.ts +40 -19
  37. package/lib/types/render/route.d.ts +77 -0
  38. package/lib/types/render/svg.d.ts +1 -1
  39. package/lib/types/render/symbol.d.ts +73 -0
  40. package/lib/types/render/text.d.ts +38 -0
  41. package/lib/types/render/trunk.d.ts +104 -0
  42. package/lib/types/render/wrap.d.ts +27 -0
  43. package/package.json +148 -148
  44. package/skills/model-everything-diagram/SKILL.md +85 -82
  45. package/src/render/channels.ts +309 -0
  46. package/src/render/collapse.ts +150 -155
  47. package/src/render/components.ts +126 -0
  48. package/src/render/coords.ts +328 -0
  49. package/src/render/cycle-break.ts +164 -0
  50. package/src/render/dummy.ts +65 -0
  51. package/src/render/from-mermaid.ts +261 -236
  52. package/src/render/from-plantuml.ts +469 -178
  53. package/src/render/layer.ts +98 -0
  54. package/src/render/layout.ts +101 -62
  55. package/src/render/model.ts +86 -67
  56. package/src/render/network-simplex.ts +290 -0
  57. package/src/render/order.ts +139 -0
  58. package/src/render/paint.ts +1570 -290
  59. package/src/render/place.ts +532 -234
  60. package/src/render/route.ts +269 -0
  61. package/src/render/svg.ts +274 -127
  62. package/src/render/symbol.ts +137 -0
  63. package/src/render/text.ts +86 -0
  64. package/src/render/trunk.ts +275 -0
  65. package/src/render/wrap.ts +103 -0
@@ -1,22 +1,54 @@
1
1
  /**
2
- * Ranks for a machine, and the order of states within each rank.
2
+ * The graph the layout stages agree on, and the order they start from.
3
3
  *
4
- * Breadth-first from the initial state decides the row: the layout a reader expects, and the one a layered
5
- * drawing asks for - edges mostly flow down, and a state's row is how far it is from the start. A state
6
- * nothing reaches goes in a block of its own rather than being dropped.
4
+ * A layout stage has no business knowing what a diagram is: it sees boxes with a size and edges
5
+ * between them. That is what `LayoutGraph` is, and it is the whole vocabulary the pipeline shares -
6
+ * cycle breaking reads it, layering writes layers, ordering and coordinate assignment read those.
7
7
  *
8
- * Within a row, states keep the order breadth-first found them in: a row is filled by walking the row above
9
- * left to right, so a state already sits under the parent that discovered it. A barycentre pass was written
10
- * and removed - with breadth-first insertion it could never move a state, so it was complexity without an
11
- * observable effect.
8
+ * `rankStates` stays: it is the breadth-first order a source's own reading suggests, and it seeds
9
+ * the ordering stage with the discovery order instead of whatever order the parser happened to
10
+ * collect. It no longer decides which row a state is in - `layer.ts` does that now - so the rows it
11
+ * returns are only a first guess at the left-to-right order.
12
12
  *
13
- * Pure, and asserted on the rows themselves.
13
+ * Pure, and asserted on the answer itself.
14
14
  */
15
- import type { DiagramEdge, DiagramNode } from './model.js';
15
+ /** A box to place: an id, and the space its text needs. */
16
+ export interface LayoutNode {
17
+ id: string;
18
+ width: number;
19
+ height: number;
20
+ }
21
+ /** An edge to draw: which two boxes it joins, and which diagram edge it came from. */
22
+ export interface LayoutEdge {
23
+ /** The index in the diagram's own edge list, so the drawing can report a lane back for it. */
24
+ index: number;
25
+ from: string;
26
+ to: string;
27
+ }
28
+ /** One level of a nested drawing: what is placed together in a single pass of the pipeline. */
29
+ export interface LayoutGraph {
30
+ nodes: LayoutNode[];
31
+ edges: LayoutEdge[];
32
+ /** The box that must stay on layer zero, when the source named one. */
33
+ init?: string;
34
+ }
16
35
  export interface Ranked {
17
36
  /** The state ids of each row, top to bottom, already ordered left to right. */
18
37
  rows: string[][];
19
38
  /** States no edge reaches from the initial one. */
20
39
  unreachable: string[];
21
40
  }
22
- export declare function rankStates(nodes: DiagramNode[], edges: DiagramEdge[], init: string | undefined): Ranked;
41
+ export declare function rankStates(nodes: ReadonlyArray<{
42
+ id: string;
43
+ }>, edges: ReadonlyArray<{
44
+ from: string;
45
+ to: string;
46
+ }>, init: string | undefined): Ranked;
47
+ /**
48
+ * The left-to-right seed the ordering stage starts from.
49
+ *
50
+ * Breadth-first discovery is what a reader expects a drawing to follow, so it is a better first
51
+ * guess than the order the parser collected. A state nothing reaches has no discovery order and
52
+ * keeps the order it was written in, after everything that was reached.
53
+ */
54
+ export declare function seedOrder(graph: LayoutGraph): Map<string, number>;
@@ -9,18 +9,35 @@
9
9
  */
10
10
  /** What a node is, as far as colour is concerned. */
11
11
  export type NodeRole = 'normal' | 'error' | 'warning' | 'muted';
12
+ /** The symbol a node is drawn as. UML and Mermaid both distinguish these; the notation decides which one applies. */
13
+ export type NodeShape = 'rect' | 'diamond' | 'cylinder' | 'stadium' | 'circle' | 'subroutine' | 'hexagon';
12
14
  export interface DiagramNode {
13
15
  id: string;
14
16
  /** The text drawn inside the box. Defaults to the id when a notation labelled it. */
15
17
  label?: string;
16
18
  role: NodeRole;
19
+ /** Defaults to a rectangle: a node the source gave no shape has no shape to draw. */
20
+ shape?: NodeShape;
17
21
  }
22
+ /**
23
+ * What an edge's head means, when the notation distinguished it.
24
+ *
25
+ * The painter draws the meaning: a plain arrow for an association, a hollow triangle for a generalisation or a
26
+ * realisation, a filled diamond at the source for a composition, a hollow one for an aggregation, and a dashed
27
+ * line for the two dependency-flavoured kinds.
28
+ */
29
+ export type EdgeArrow = 'association' | 'dependency' | 'generalization' | 'realization' | 'composition' | 'aggregation';
18
30
  export interface DiagramEdge {
19
31
  from: string;
20
32
  to: string;
21
33
  label: string;
22
34
  /** A dashed edge, for notations that distinguish one. */
23
35
  dashed?: boolean;
36
+ /**
37
+ * The arrow the notation stated. Absent means the plain arrow every edge has been drawn with: a Mermaid edge
38
+ * states no arrow type, so it carries no field and is drawn exactly as before.
39
+ */
40
+ arrow?: EdgeArrow;
24
41
  }
25
42
  export interface DiagramGroup {
26
43
  id: string;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Minimum-length ranking: the network simplex of Gansner, Koutsofios, North and Vo (1993).
3
+ *
4
+ * The ranking problem: give every node a layer such that each edge spans at least its minimum length, and make the
5
+ * total of all the slack - the layers every edge spans beyond its minimum - as small as it can be. Slack is what a
6
+ * reader pays for: an edge that spans three layers has to be drawn through two of them.
7
+ *
8
+ * Longest paths are one feasible answer and they are not the best one. They put every node as high as it can
9
+ * possibly go, which leaves a node whose edges leave it with nothing to do above the boxes it points at. The best
10
+ * answer is the solution of a linear program, and network simplex is how the mature layering implementations solve
11
+ * it: hold a *feasible tree* - a spanning tree every edge of which is tight - and a ranking that tightens it, attach
12
+ * a *cut value* to each tree edge (the weight flowing one way across that cut minus the weight flowing the other),
13
+ * and while some tree edge has a negative cut value, take it out, let the cheapest edge across its cut in, and move
14
+ * the ranks so the new edge is tight. The cut value is the rate at which the total length falls as the tree edge is
15
+ * left out, so every exchange shortens the drawing, and a tree with no negative cut value is the optimum: no single
16
+ * exchange is left that would shorten it, and for this program that is the definition of optimal.
17
+ *
18
+ * Three things are done differently from the paper, each because this graph is not the paper's graph:
19
+ *
20
+ * - **Components are solved one at a time.** The ranking is translation invariant, so two components that share no
21
+ * edge can be laid out apart from each other; each is solved on its own and its top row is then numbered zero.
22
+ * - **A node may be pinned to the top.** A state machine has an initial state and a reader expects it first, so the
23
+ * stage can name one node that must be on the topmost layer: a zero-length, zero-weight constraint edge runs from
24
+ * it to every other node of its component (`rank(v) >= rank(pinned)`), and the ranking is minimised subject to that.
25
+ * The constraint costs length when the free optimum would have put the pinned node below another source, and that
26
+ * price is what "the initial state is first" means.
27
+ * - **The iteration is capped.** A degenerate exchange (one that moves nothing) is legal and in theory can go round
28
+ * in circles. The cap turns that into a ranking that is feasible and no worse than where it started.
29
+ *
30
+ * Pure, and generic in the ranking program: no ids, no sizes, no drawing.
31
+ */
32
+ /** One edge of the ranking program: two layers, how far apart they may be, and what one layer of slack costs. */
33
+ export interface RankEdge {
34
+ from: number;
35
+ to: number;
36
+ /** The least number of layers this edge may span. One for an edge that is drawn, zero for a constraint. */
37
+ minLength: number;
38
+ /** What one extra layer on this edge costs. Zero for a constraint, which must hold but is not worth shortening. */
39
+ weight: number;
40
+ }
41
+ export interface RankProblem {
42
+ count: number;
43
+ edges: RankEdge[];
44
+ /** A feasible ranking to start from: it already gives every edge at least its minimum length. */
45
+ start: number[];
46
+ /**
47
+ * The node that has to sit on the topmost layer, when the source named one.
48
+ *
49
+ * It has to be a node nothing points at, or no ranking can hold it at the top - a predecessor forces it down. The
50
+ * constraint is dropped when the start ranking cannot satisfy it.
51
+ */
52
+ pinned?: number;
53
+ }
54
+ export declare function minimumLength(problem: RankProblem): number[];
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Ordering: the left-to-right order of each layer.
3
+ *
4
+ * The layer a box is in is fixed by now; the only freedom left is the order inside each layer, and that order decides
5
+ * how many edges cross. A crossing is counted on a pair of adjacent layers: two edges between the same two layers
6
+ * cross when their end points are in opposite order on the top and the bottom.
7
+ *
8
+ * The method is the one every layered layout uses. A median sweep takes each layer in turn and sorts it by the median
9
+ * position of its neighbours in the layer just visited - down the drawing, then up. A down sweep alone settles each
10
+ * layer against the one above it and can be undone by the next; alternating is what makes the passes converge.
11
+ *
12
+ * The median is a good guess, not a search. Greedy switch is the search: try swapping two neighbours in a layer, keep
13
+ * the swap if the total crossing count falls, and stop when a whole pass finds nothing. That is where a crossing the
14
+ * median cannot see gets removed.
15
+ *
16
+ * Pure: layers of ids in, layers of ids out, plus the crossing count that justified them.
17
+ */
18
+ import type { Chain } from './dummy.js';
19
+ export interface Ordered {
20
+ /** The same layers, each in its final order. */
21
+ layers: string[][];
22
+ /** The number of crossings between adjacent layers left by the ordering. */
23
+ crossings: number;
24
+ }
25
+ /** How many pairs of edges between adjacent layers cross. */
26
+ export declare function countCrossings(layers: string[][], chains: Chain[]): number;
27
+ export declare function order(seedLayers: string[][], chains: Chain[], seed?: Map<string, number>): Ordered;
@@ -5,21 +5,39 @@
5
5
  * sidebar tab - so nothing here knows about pixel size. It emits a content-sized canvas and lets the
6
6
  * container scale it: the same numbers serve a 360px card and a 1200px whiteboard.
7
7
  *
8
- * Two placements, chosen by the source:
8
+ * The drawing is placed by the layered pipeline (`place.ts`), which also says which lane a long edge reserved.
9
+ * Edges are drawn from where the boxes ended up, in two ways only: a long edge follows its lane as one polyline,
10
+ * and everything else goes through the orthogonal router. Which side of a box an edge leaves by is read off the
11
+ * route, not picked by comparing the boxes' centres: a route between two rows arrives vertically, so the endpoint
12
+ * is snapped onto the top or bottom perimeter in the direction the route approaches. Where along that side it sits
13
+ * follows the symbol: a rectangle spreads its ports along the side, a cylinder's top port sits on its cap curve, and a
14
+ * diamond or a circle has four legal points and no side to spread along at all. A side that carries one port is
15
+ * centred on it, and a side that carries several spreads them symmetrically about the middle; the bend budget comes
16
+ * first, so a port that is the end of a straight run or of a lane keeps the coordinate its route asked for.
9
17
  *
10
- * - **layered** (no groups): breadth-first rows, forward edges dropping straight down, back edges on a side
11
- * channel clear of every box, self loops as small loops. The shape a state machine wants.
12
- * - **tree** (groups present): a group is a block, its members stack inside it, a nested group is a block
13
- * inside its parent, and the top level stacks. The shape an architecture diagram wants, because a package
14
- * whose members float somewhere else on the page is not showing containment at all.
18
+ * A symbol with discrete points gives each of its edges a point of its own: the point whose outward normal faces the
19
+ * box at the other end is the one that edge takes, and an edge only shares a point when all four are spoken for. A
20
+ * node with a run on its side keeps doing what it always did, because there the ports are places on the run and not
21
+ * points at all. Sharing is the fallback, not the rule: two labelled branches of a decision leave by two vertices.
15
22
  *
16
- * Edges are placed from where the boxes ended up, so neither placement needs to tell the edge code which one
17
- * ran: "below" is a straight line, "beside" is horizontal, "above" routes around.
23
+ * An edge's name is drawn on the edge, not beside it: it is anchored at the midpoint of one of the edge's own straight
24
+ * runs, over a rectangle in the page colour that masks the line behind the text, and it slides along that run when
25
+ * something is in the way. It tries every run - the longest first - because on a dense drawing the longest one can be
26
+ * crowded for its whole length while a shorter run of the same edge is clear. A label that can clear nothing still
27
+ * stays on its edge, takes the place with the fewest collisions, and records what it could not avoid. Labels are
28
+ * placed after every edge is drawn, because what a label has to avoid is the whole picture.
29
+ *
30
+ * Every route is checked before it is drawn, and the assembled drawing is checked once more: a segment may touch no
31
+ * box except the two the edge terminates at. A route is a proposal - a channel run, a lane, a trunk branch, a wrapped
32
+ * column's sideways read - and any proposal that would cross a box is handed to the router instead. When even the
33
+ * router cannot clear a box, the failure is reported on the canvas rather than hidden: a drawing that says which edge
34
+ * could not be routed is worth more than one that quietly draws it through a box.
18
35
  *
19
36
  * Pure: a diagram in, shapes out, every decision assertable.
20
37
  */
38
+ import { type Box } from './place.js';
21
39
  import { type ColourRole, type Palette, type PaletteOptions } from './palette.js';
22
- import { type Diagram, type DiagramNode } from './model.js';
40
+ import { type Diagram, type DiagramNode, type EdgeArrow, type NodeShape } from './model.js';
23
41
  export interface Point {
24
42
  x: number;
25
43
  y: number;
@@ -30,10 +48,30 @@ export interface BoxShape {
30
48
  y: number;
31
49
  width: number;
32
50
  height: number;
51
+ /** The lines to draw, already wrapped; `label` keeps the whole text for anything that reads the shape. */
33
52
  label: string;
53
+ lines: string[];
54
+ /** The symbol to draw. Undefined means a rectangle. */
55
+ shape?: NodeShape;
34
56
  colour: ColourRole;
35
57
  initial: boolean;
36
58
  }
59
+ /**
60
+ * Why a label sits where it does.
61
+ *
62
+ * A clear label records `collisions: 0` and the run it chose. A label that could clear nothing records the collisions
63
+ * it could not avoid, so a reader of the drawing (or a test) can tell a crowded drawing from a placement bug. The
64
+ * label never leaves its own edge to earn any of these numbers.
65
+ */
66
+ export interface LabelPlacement {
67
+ /** The straight run it sits on, 0 being the edge's longest. */
68
+ run: number;
69
+ /** Everything its mask overlaps: the other labels, boxes and edges it would have had to move off its line to clear. */
70
+ collisions: number;
71
+ boxes: number;
72
+ labels: number;
73
+ edges: number;
74
+ }
37
75
  export interface LinkShape {
38
76
  kind: 'link';
39
77
  x1: number;
@@ -41,17 +79,43 @@ export interface LinkShape {
41
79
  x2: number;
42
80
  y2: number;
43
81
  label: string;
82
+ /** The centre the label is drawn on, and therefore a point on the edge itself. */
44
83
  labelX: number;
45
84
  labelY: number;
46
85
  colour: ColourRole;
86
+ /** A dashed line, when the notation distinguished one. */
87
+ dashed?: boolean;
88
+ /** The arrow the source edge stated. Undefined means the plain arrow every edge has been drawn with. */
89
+ arrow?: EdgeArrow;
90
+ /** Why the label sits where it does. Undefined on an edge with no label. */
91
+ labelPlacement?: LabelPlacement;
47
92
  }
48
93
  export interface PathShape {
49
94
  kind: 'path';
50
95
  points: Point[];
51
96
  label: string;
97
+ /** The centre the label is drawn on, and therefore a point on the edge itself. */
52
98
  labelX: number;
53
99
  labelY: number;
54
100
  colour: ColourRole;
101
+ dashed?: boolean;
102
+ arrow?: EdgeArrow;
103
+ /** Why the label sits where it does. Undefined on an edge with no label. */
104
+ labelPlacement?: LabelPlacement;
105
+ }
106
+ /**
107
+ * A junction dot: the place one branch of a merged bundle meets the bundle's trunk.
108
+ *
109
+ * The dot is not decoration. Two lines that meet at a right angle read as a crossing unless one of them is marked as
110
+ * joining the other, and a bundle whose branches are not marked reads as a set of broken lines rather than a merge.
111
+ */
112
+ export interface JunctionShape {
113
+ kind: 'junction';
114
+ x: number;
115
+ y: number;
116
+ colour: ColourRole;
117
+ /** The trunk the dot belongs to: one dot per branch, one trunk id per bundle. */
118
+ trunk: number;
55
119
  }
56
120
  /** A boundary around several states: a package, a layer, a subgraph. */
57
121
  export interface GroupShape {
@@ -79,16 +143,99 @@ export interface TitleShape {
79
143
  text: string;
80
144
  colour: ColourRole;
81
145
  }
82
- export type Shape = GroupShape | BoxShape | LinkShape | PathShape | CaptionShape | TitleShape;
146
+ export type Shape = GroupShape | BoxShape | LinkShape | PathShape | JunctionShape | CaptionShape | TitleShape;
147
+ /** One segment of a drawn route that could not be kept out of a box it does not terminate at. */
148
+ export interface RoutingFailure {
149
+ /** The edge's index in the diagram. */
150
+ edge: number;
151
+ from: string;
152
+ to: string;
153
+ /** The box the segment passes through. */
154
+ box: string;
155
+ x1: number;
156
+ y1: number;
157
+ x2: number;
158
+ y2: number;
159
+ }
83
160
  export interface Canvas {
84
161
  width: number;
85
162
  height: number;
86
163
  shapes: Shape[];
164
+ /**
165
+ * Every place a drawn segment still passes through a box it does not terminate at, after every route was checked.
166
+ *
167
+ * A drawing should carry none. The field exists for the case where no stage and no router could find a way around:
168
+ * a route that cannot be drawn honestly is reported here rather than silently drawn through a box.
169
+ */
170
+ routingFailures: RoutingFailure[];
171
+ /** How many edge labels could not be placed clear of every box, label and other edge. */
172
+ labelCollisions: number;
87
173
  }
174
+ /**
175
+ * How far apart two edges that join the same pair are drawn.
176
+ *
177
+ * Wider than the least two ports may sit: two statements about one pair have to read as two lines, and a gap the
178
+ * port packer would allow is a gap a reader sees as one thick line. Exported so a test can name the pattern.
179
+ */
180
+ export declare const MULTI_EDGE_SPACING = 20;
88
181
  /** Which colour a box earns from its role. One mapping, used by every container. */
89
182
  export declare function roleOfNode(node: DiagramNode): ColourRole;
183
+ /**
184
+ * How tall the mask an edge label is drawn over is.
185
+ *
186
+ * Exported because two modules must agree on it: the painter reserves the rectangle when it looks for a clear
187
+ * place, and the writer draws the rectangle that was reserved. A label with two sizes is a label half masked.
188
+ */
189
+ export declare const LABEL_HEIGHT = 12;
190
+ /** A straight piece of an edge, as the collision test needs it. */
191
+ interface Segment {
192
+ x1: number;
193
+ y1: number;
194
+ x2: number;
195
+ y2: number;
196
+ }
197
+ /** Where a label sits: the point it is centred on, and the rectangle its mask covers. */
198
+ interface LabelSpot {
199
+ x: number;
200
+ y: number;
201
+ width: number;
202
+ height: number;
203
+ }
204
+ /**
205
+ * The point an edge's label is drawn at.
206
+ *
207
+ * A label is not furniture parked beside the line: it names the transition, so it sits on the line - anchored on one
208
+ * of its own straight runs - and the writer masks the stretch of line it covers. Text beside a line can be read as
209
+ * belonging to whatever it happens to sit next to; text that interrupts its own line reads as part of it.
210
+ *
211
+ * The anchor then has to be earned. A mask over a box hides a border, over another label it makes both unreadable,
212
+ * and over a third edge's line it hides that line. So the label walks along a run in small steps, in both directions
213
+ * and bounded so its rectangle stays on that run, and takes the first place that is clear. It tries the longest run
214
+ * first and then the shorter ones: on a dense drawing the longest run can be crowded for its whole length while a
215
+ * shorter run of the same edge is empty, and a label that gives up after one run leaves the drawing with a collision
216
+ * it did not have to accept.
217
+ *
218
+ * When no run offers a clear place, the label takes the place with the fewest collisions - boxes and other labels
219
+ * first, because text over a border hides the border and text over text is unreadable, then other edges, which a mask
220
+ * merely interrupts. It never leaves its own edge: a label moved onto a neighbouring edge is a label that names the
221
+ * wrong thing. The choice is returned with the label, so a crowded drawing can be told from a placement bug.
222
+ *
223
+ * Exported because the choice is pure and worth asserting without a layout: a test can hand it one polyline, one box
224
+ * and one other route, and read which run it took and what it could not clear.
225
+ */
226
+ export declare function placeLabel(label: string, points: Point[], boxes: Iterable<Box>, placed: LabelSpot[], otherRoutes: Segment[][]): LabelPlacement & LabelSpot;
227
+ /**
228
+ * The boxes a polyline passes through other than the two it terminates at.
229
+ *
230
+ * This is the drawing's one clearance rule, and it is deliberately written once: a channel run, a lane, a trunk
231
+ * branch, a wrapped column's sideways step and the router's fallback all have to obey the same test. A box is skipped
232
+ * only when it is one of the edge's own two ends, so a port that sits inside its own symbol (a cylinder's cap apex)
233
+ * is allowed and a segment that ends inside somebody else's box is not.
234
+ */
235
+ export declare function boxesCrossedBy(points: Point[], from: Box, to: Box, boxes: Iterable<Box>): Box[];
90
236
  export declare function paintWith(diagram: Diagram, overrides?: Partial<Palette>, options?: PaletteOptions): {
91
237
  canvas: Canvas;
92
238
  colours: Palette;
93
239
  };
94
240
  export declare function paint(diagram: Diagram, overrides?: Partial<Palette>): Canvas;
241
+ export {};
@@ -1,18 +1,21 @@
1
1
  /**
2
2
  * Where the boxes go.
3
3
  *
4
- * Two placements, chosen by the source:
4
+ * The drawing is placed by the layered pipeline, in the order the mature implementations run it: cycle breaking turns
5
+ * the graph into a DAG, layering gives every box a row, dummy nodes give every long edge a lane, ordering reduces the
6
+ * crossings, and coordinate assignment decides the x of each lane and box. This module composes those stages; it does
7
+ * not decide any of them itself. Each stage is a pure module next door, so it can be asserted without a canvas.
5
8
  *
6
- * - **layered** (no groups): breadth-first rows, centred, no containment - the shape a state machine wants.
7
- * - **tree** (groups present): a group is a block, its members stack inside it, a nested group is a block inside
8
- * its parent, and the top level stacks - the shape an architecture diagram wants, because a package whose
9
- * members float somewhere else on the page is not showing containment at all.
9
+ * Two decompositions sit around the pipeline. A level - the top of the drawing, or the inside of a boundary - is
10
+ * split into weakly connected components, each laid out on its own and then packed onto the page, so a wide
11
+ * subgraph cannot squeeze an unrelated one. A boundary is a node in its parent's layer whose size is whatever its
12
+ * contents need, and its inside is laid out by the same pipeline; that is how nesting is drawn.
10
13
  *
11
- * Split out of the painter so the two questions stay separate: this module decides positions, `paint.ts` decides
12
- * what to draw between them. A change to edge routing cannot move a box, and a change to nesting cannot move a
13
- * line.
14
+ * What comes out is geometry only: a box per node, a rectangle per boundary, and - for a long edge - the lane its
15
+ * dummies reserved, so the painter can draw one polyline through it instead of searching around the boxes.
14
16
  */
15
- import type { Diagram } from './model.js';
17
+ import { type Measured as TextBox } from './text.js';
18
+ import type { Diagram, DiagramNode } from './model.js';
16
19
  /** Geometry of one placed box, before anything is drawn. */
17
20
  export type Box = {
18
21
  x: number;
@@ -20,20 +23,38 @@ export type Box = {
20
23
  width: number;
21
24
  height: number;
22
25
  };
23
- /** What a placement returns: every box, every drawn boundary, and how wide the content is. */
26
+ /** A point in the drawing's own coordinates. */
27
+ export interface Point {
28
+ x: number;
29
+ y: number;
30
+ }
31
+ /** The lane a long edge's dummies reserved, in layout order: from the layout source down to the layout target. */
32
+ export interface Lane {
33
+ /** One point per gap the edge crosses, at the lane's own x: it keeps its own order, and the painter walks it. */
34
+ points: Point[];
35
+ /** True when the layout runs opposite to the source's arrow, so the painter walks the lane backwards. */
36
+ reversed: boolean;
37
+ }
38
+ /** What a placement returns: every box, every drawn boundary, every reserved lane, and how wide the content is. */
24
39
  export interface Placement {
25
40
  boxes: Map<string, Box>;
26
41
  groupRects: Map<string, Box>;
27
42
  width: number;
43
+ lanes: Map<number, Lane>;
44
+ /**
45
+ * The height of the one horizontal run an edge between two adjacent rows needs.
46
+ *
47
+ * A run is measured between two rows of the level that produced it. When a row is a boundary, that strip is the space
48
+ * outside the boundary while the painter draws the boxes inside it, so the run is a proposal rather than a fact: the
49
+ * painter clears it against every box and hands the edge to the router when a leg would cross one.
50
+ */
51
+ runs: Map<number, number>; /** Edges whose two ends ended up in different wrapped columns: they are read across, not down. */
52
+ across: Set<number>;
28
53
  }
29
- /** Geometry constants shared by both placements. */
54
+ /** Geometry constants in one place: a container with different proportions changes these, not the rules. */
30
55
  export declare const MARGIN = 20;
31
- /** A box wide enough for its label, clamped so a long id cannot dominate the page. */
32
- export declare const nodeWidth: (label: string) => number;
33
- /**
34
- * Place every box of a diagram.
35
- *
36
- * The choice of placement is a property of the source, not of the caller: a drawing that declares boundaries is
37
- * read as containers and gets the tree; one that does not is read as a machine and gets the ranks.
38
- */
56
+ /** The text a node will draw, measured from its label - the id is what the source called it, not what it says. */
57
+ export declare function textOf(node: DiagramNode): TextBox;
39
58
  export declare function place(diagram: Diagram, top: number): Placement;
59
+ /** The aspect ratio of a placement, for the tests that check a drawing is a page and not a strip. */
60
+ export declare function placementAspect(placement: Placement): number;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Orthogonal edge routing around boxes.
3
+ *
4
+ * A straight line between two boxes is right until a third box sits between them; then it is a line drawn through a
5
+ * component, which reads as a connection that is not there. Graphviz solves this by routing along a visibility
6
+ * graph and then fitting splines; ELK, yFiles and mxGraph build a *sparse* grid from the boxes' own edges and run
7
+ * A* over it with a bend penalty. Sparse is the whole trick: a pixel grid is O(pixels²), while the candidate lines
8
+ * are the coordinate of every box edge plus a margin - O(boxes) per axis.
9
+ *
10
+ * So this module does exactly that, in the two steps those routers use:
11
+ *
12
+ * 1. **Candidate lines.** Every box contributes four: one lane just outside each side. Together with the anchor
13
+ * coordinates they form the grid a route may travel on.
14
+ * 2. **A* with bends.** Moving along a candidate line costs its length; changing direction costs extra, because a
15
+ * route that zig-zags to save three points is harder to follow than one that goes around.
16
+ *
17
+ * A route is only computed for an edge that needs it: the cheap straight segment is tried first, and routing is the
18
+ * fallback. That keeps the common case - a box directly above another - at two points and no search.
19
+ *
20
+ * The layered painter mostly does not ask: an edge between two rows and an edge with a lane are both built from the
21
+ * channel stage's answer, because a route searched on its own cannot know which lanes the other edges already hold.
22
+ * What is left for this router is the drawing the layers do not describe - an edge across two wrapped columns, an
23
+ * edge one of whose ends is a boundary - and the fallback when a channel route would meet a box.
24
+ */
25
+ import type { Point } from './paint.js';
26
+ /** A rectangle an edge may not cross. */
27
+ export interface Obstacle {
28
+ x: number;
29
+ y: number;
30
+ width: number;
31
+ height: number;
32
+ }
33
+ /** The four sides a route may leave a box from. */
34
+ export type Side = 'top' | 'bottom' | 'left' | 'right';
35
+ /** The length of the straight run an edge takes before it is allowed to turn. */
36
+ export declare const ROUTE_STUB = 16;
37
+ /** The outward normal of a side: the direction an edge leaves along. */
38
+ export declare function normalOf(side: Side): Point;
39
+ /** A segment crossing a box at all, with a little slack so a route does not shave a corner. */
40
+ export declare function crossesBox(x1: number, y1: number, x2: number, y2: number, box: Obstacle): boolean;
41
+ /** The point on a side of a box, in the middle of that side. */
42
+ export declare function anchor(box: Obstacle, side: Side): Point;
43
+ /**
44
+ * Which side of `from` an edge leaves by, and which side of `to` it arrives at.
45
+ *
46
+ * The side is a property of the drawing, not of the distance between two centres. A layered drawing puts its boxes
47
+ * in horizontal rows, and an edge between two rows approaches vertically: out of the bottom, into the top, and the
48
+ * other way round for an edge drawn against the flow. Only two boxes in the same row are joined sideways, and then
49
+ * the direction of travel decides which side.
50
+ *
51
+ * This replaces the old rule, which compared the two centres' dx and dy and picked the larger axis - so an edge
52
+ * between two rows left through a box's side as soon as the target was far enough sideways, which is the hook a
53
+ * top-down drawing is read as a defect.
54
+ */
55
+ export declare function approachSides(from: Obstacle, to: Obstacle): {
56
+ exit: Side;
57
+ entry: Side;
58
+ };
59
+ /**
60
+ * Route from one box to another, going around any box in the way.
61
+ *
62
+ * Returns the polyline to draw. When nothing blocks the straight segment, or when every candidate is blocked (a
63
+ * single box that fills the drawing), the straight line is returned and the caller keeps a two-point shape - an
64
+ * unroutable edge is still an edge, and drawing it straight is more honest than dropping it.
65
+ */
66
+ export interface Ports {
67
+ exit: Point;
68
+ entry: Point;
69
+ exitSide: Side;
70
+ entrySide: Side;
71
+ }
72
+ export declare function routeEdge(from: Obstacle, to: Obstacle, obstacles: Obstacle[], anchors?: Ports): {
73
+ points: Point[];
74
+ routed: boolean;
75
+ };
76
+ /** Drop the points in the middle of a straight run: three collinear points are one segment. */
77
+ export declare function simplify(points: Point[]): Point[];
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * Pure, and the output is asserted as text: escaping, geometry, arrowheads, wrapped captions.
13
13
  */
14
- import type { Canvas } from './paint.js';
14
+ import { type Canvas } from './paint.js';
15
15
  import type { Palette } from './palette.js';
16
16
  export declare function toSvg(canvas: Canvas, colours: Palette, label?: string): {
17
17
  markup: string;