dsh-model-everything 0.4.1 → 0.5.1

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 (114) hide show
  1. package/README.en-US.md +75 -19
  2. package/README.md +377 -345
  3. package/cordis.patch.yml +9 -0
  4. package/diagram/package.json +5 -0
  5. package/lib/client.js +3494 -0
  6. package/lib/diagram-diff-tool.js +90 -0
  7. package/lib/diagram-render-tool.js +102 -0
  8. package/lib/diagram-review-tool.js +99 -0
  9. package/lib/diagram.js +43 -0
  10. package/lib/gate.js +14 -12
  11. package/lib/presentation.js +10 -0
  12. package/lib/render/budget.js +99 -0
  13. package/lib/render/channels.js +243 -0
  14. package/lib/render/collapse.js +155 -0
  15. package/lib/render/components.js +99 -0
  16. package/lib/render/coords.js +288 -0
  17. package/lib/render/cycle-break.js +147 -0
  18. package/lib/render/diff.js +138 -0
  19. package/lib/render/dummy.js +43 -0
  20. package/lib/render/from-mermaid.js +235 -0
  21. package/lib/render/from-plantuml.js +430 -0
  22. package/lib/render/index.js +42 -0
  23. package/lib/render/layer.js +91 -0
  24. package/lib/render/layout.js +68 -0
  25. package/lib/render/model.js +17 -0
  26. package/lib/render/network-simplex.js +272 -0
  27. package/lib/render/order.js +122 -0
  28. package/lib/render/paint.js +891 -0
  29. package/lib/render/palette.js +127 -0
  30. package/lib/render/place.js +482 -0
  31. package/lib/render/route.js +225 -0
  32. package/lib/render/svg.js +229 -0
  33. package/lib/render/symbol.js +108 -0
  34. package/lib/render/text.js +74 -0
  35. package/lib/render/viewmodel.js +126 -0
  36. package/lib/render/wrap.js +87 -0
  37. package/lib/render/write.js +14 -0
  38. package/lib/scan.js +248 -8
  39. package/lib/structure-tool.js +2 -2
  40. package/lib/types/client.d.ts +83 -0
  41. package/lib/types/diagram-diff-tool.d.ts +8 -0
  42. package/lib/types/diagram-render-tool.d.ts +8 -0
  43. package/lib/types/diagram-review-tool.d.ts +8 -0
  44. package/lib/types/diagram.d.ts +34 -0
  45. package/lib/types/presentation.d.ts +7 -0
  46. package/lib/types/render/budget.d.ts +55 -0
  47. package/lib/types/render/channels.d.ts +63 -0
  48. package/lib/types/render/collapse.d.ts +23 -0
  49. package/lib/types/render/components.d.ts +57 -0
  50. package/lib/types/render/coords.d.ts +64 -0
  51. package/lib/types/render/cycle-break.d.ts +45 -0
  52. package/lib/types/render/diff.d.ts +44 -0
  53. package/lib/types/render/dummy.d.ts +32 -0
  54. package/lib/types/render/from-mermaid.d.ts +27 -0
  55. package/lib/types/render/from-plantuml.d.ts +33 -0
  56. package/lib/types/render/index.d.ts +38 -0
  57. package/lib/types/render/layer.d.ts +28 -0
  58. package/lib/types/render/layout.d.ts +54 -0
  59. package/lib/types/render/model.d.ts +73 -0
  60. package/lib/types/render/network-simplex.d.ts +54 -0
  61. package/lib/types/render/order.d.ts +27 -0
  62. package/lib/types/render/paint.d.ts +127 -0
  63. package/lib/types/render/palette.d.ts +41 -0
  64. package/lib/types/render/place.d.ts +55 -0
  65. package/lib/types/render/route.d.ts +77 -0
  66. package/lib/types/render/svg.d.ts +19 -0
  67. package/lib/types/render/symbol.d.ts +73 -0
  68. package/lib/types/render/text.d.ts +38 -0
  69. package/lib/types/render/viewmodel.d.ts +20 -0
  70. package/lib/types/render/wrap.d.ts +27 -0
  71. package/lib/types/render/write.d.ts +14 -0
  72. package/lib/types/scan.d.ts +10 -2
  73. package/lib/types/structure.d.ts +1 -0
  74. package/package.json +148 -132
  75. package/skills/model-everything/SKILL.md +454 -454
  76. package/skills/model-everything/references/architecture-domains.md +137 -0
  77. package/skills/model-everything/references/architecture-views.md +119 -0
  78. package/skills/model-everything-diagram/SKILL.md +84 -0
  79. package/src/client.ts +307 -0
  80. package/src/diagram-diff-tool.ts +94 -0
  81. package/src/diagram-render-tool.ts +108 -0
  82. package/src/diagram-review-tool.ts +109 -0
  83. package/src/diagram.ts +61 -0
  84. package/src/gate.ts +14 -12
  85. package/src/presentation.ts +12 -0
  86. package/src/render/budget.ts +135 -0
  87. package/src/render/channels.ts +309 -0
  88. package/src/render/collapse.ts +150 -0
  89. package/src/render/components.ts +126 -0
  90. package/src/render/coords.ts +328 -0
  91. package/src/render/cycle-break.ts +164 -0
  92. package/src/render/diff.ts +162 -0
  93. package/src/render/dummy.ts +65 -0
  94. package/src/render/from-mermaid.ts +261 -0
  95. package/src/render/from-plantuml.ts +469 -0
  96. package/src/render/index.ts +73 -0
  97. package/src/render/layer.ts +98 -0
  98. package/src/render/layout.ts +101 -0
  99. package/src/render/model.ts +86 -0
  100. package/src/render/network-simplex.ts +290 -0
  101. package/src/render/order.ts +139 -0
  102. package/src/render/paint.ts +1006 -0
  103. package/src/render/palette.ts +164 -0
  104. package/src/render/place.ts +527 -0
  105. package/src/render/route.ts +269 -0
  106. package/src/render/svg.ts +270 -0
  107. package/src/render/symbol.ts +137 -0
  108. package/src/render/text.ts +86 -0
  109. package/src/render/viewmodel.ts +136 -0
  110. package/src/render/wrap.ts +103 -0
  111. package/src/render/write.ts +17 -0
  112. package/src/scan.ts +807 -564
  113. package/src/structure-tool.ts +2 -2
  114. package/src/structure.ts +1 -1
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Verdict to colour, in the two surfaces the page can be in, and in the scheme a reader prefers.
3
+ *
4
+ * A drawing is read before it is parsed, so the first thing it says is the answer. The roles are named rather
5
+ * than coloured: a scheme maps a role to a value, and every scheme is chosen so the drawing survives both a
6
+ * light and a dark page - the reader never has to fix a palette to read a result.
7
+ *
8
+ * The mode is read at render time from the page's own background (see `client.ts`). The host publishes its
9
+ * palette as CSS variables whose names are private to it, and a drawing that hard-coded them would break on
10
+ * the next theme; the luminance of the surface is the one fact that cannot drift.
11
+ *
12
+ * Pure, and tested: a wrong role is a wrong verdict shown as a shape.
13
+ */
14
+ /** Ours: a quiet neutral drawing, with a green/amber/red answer. */
15
+ const NEUTRAL_LIGHT = {
16
+ pass: '#2f8f5b',
17
+ findings: '#b7791f',
18
+ fail: '#c0392b',
19
+ undecided: '#6b7280',
20
+ muted: '#9aa3ad',
21
+ text: '#1f2933',
22
+ edge: '#6b7280',
23
+ surface: 'transparent',
24
+ background: '#ffffff',
25
+ };
26
+ const NEUTRAL_DARK = {
27
+ pass: '#4ac486',
28
+ findings: '#e0b050',
29
+ fail: '#f2705f',
30
+ undecided: '#9aa3ad',
31
+ muted: '#7d8794',
32
+ text: '#e6e8eb',
33
+ edge: '#8b949e',
34
+ surface: 'transparent',
35
+ background: '#0d1117',
36
+ };
37
+ /**
38
+ * GitHub's own palette, light: the colours a reader already knows from the place they keep the diagram.
39
+ * Values are Primer's foreground and canvas tokens, not invented for this plugin.
40
+ */
41
+ const GITHUB_LIGHT = {
42
+ pass: '#1a7f37',
43
+ findings: '#9a6700',
44
+ fail: '#cf222e',
45
+ undecided: '#656d76',
46
+ muted: '#8c959f',
47
+ text: '#1f2328',
48
+ edge: '#656d76',
49
+ surface: 'transparent',
50
+ background: '#ffffff',
51
+ };
52
+ /** GitHub dark default: canvas #0d1117, foreground #e6edf3. */
53
+ const GITHUB_DARK = {
54
+ pass: '#3fb950',
55
+ findings: '#d29922',
56
+ fail: '#f85149',
57
+ undecided: '#7d8590',
58
+ muted: '#6e7681',
59
+ text: '#e6edf3',
60
+ edge: '#7d8590',
61
+ surface: 'transparent',
62
+ background: '#0d1117',
63
+ };
64
+ /** Black on white, for a projector or a poor print. The shape carries the meaning, not the hue. */
65
+ const HIGH_CONTRAST = {
66
+ pass: '#0a5c35',
67
+ findings: '#7a4300',
68
+ fail: '#a30000',
69
+ undecided: '#333333',
70
+ muted: '#444444',
71
+ text: '#000000',
72
+ edge: '#000000',
73
+ surface: 'transparent',
74
+ background: '#ffffff',
75
+ };
76
+ /** Greyscale throughout: a drawing that will be printed on a black-and-white printer. */
77
+ const PRINT = {
78
+ pass: '#333333',
79
+ findings: '#555555',
80
+ fail: '#000000',
81
+ undecided: '#666666',
82
+ muted: '#777777',
83
+ text: '#000000',
84
+ edge: '#333333',
85
+ surface: 'transparent',
86
+ background: '#ffffff',
87
+ };
88
+ const SCHEMES = {
89
+ default: { light: NEUTRAL_LIGHT, dark: NEUTRAL_DARK },
90
+ github: { light: GITHUB_LIGHT, dark: GITHUB_DARK },
91
+ 'high-contrast': { light: HIGH_CONTRAST },
92
+ print: { light: PRINT },
93
+ };
94
+ /**
95
+ * Resolve a palette.
96
+ *
97
+ * `overrides` win over everything, so a caller can retheme one role without inventing a scheme. A scheme with
98
+ * a dark variant follows the surface; one without stays as declared - high-contrast and print are what they
99
+ * say they are, on purpose. An unknown name falls back to the default palette rather than throwing, because
100
+ * the value can arrive from a DOM control.
101
+ */
102
+ export function palette(overrides = {}, options = {}) {
103
+ const scheme = options.scheme ?? 'default';
104
+ const mode = options.mode ?? 'light';
105
+ const chosen = SCHEMES[scheme] ?? SCHEMES.default;
106
+ const base = mode === 'dark' ? (chosen.dark ?? chosen.light) : chosen.light;
107
+ return { ...base, ...overrides };
108
+ }
109
+ /**
110
+ * The role a verdict earns.
111
+ *
112
+ * `pass_with_findings` is deliberately not `pass`: it reads as the middle band, because a card that paints a
113
+ * report with warnings the same colour as a clean one has stopped carrying the answer.
114
+ */
115
+ export function roleForVerdict(verdict) {
116
+ if (verdict === 'pass')
117
+ return 'pass';
118
+ if (verdict === 'pass_with_findings')
119
+ return 'findings';
120
+ if (verdict === 'fail')
121
+ return 'fail';
122
+ return 'undecided';
123
+ }
124
+ /** Every scheme by name, so a picker does not hard-code the list. */
125
+ export function schemeNames() {
126
+ return Object.keys(SCHEMES);
127
+ }
@@ -0,0 +1,482 @@
1
+ /**
2
+ * Where the boxes go.
3
+ *
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.
8
+ *
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.
13
+ *
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.
16
+ */
17
+ import { aspectOf, packRectangles, weakComponents } from './components.js';
18
+ import { assignChannels } from './channels.js';
19
+ import { assignCoordinates } from './coords.js';
20
+ import { breakCycles } from './cycle-break.js';
21
+ import { addDummies } from './dummy.js';
22
+ import { layer } from './layer.js';
23
+ import { seedOrder } from './layout.js';
24
+ import { order } from './order.js';
25
+ import { measure, MIN_NODE_WIDTH } from './text.js';
26
+ import { planBands } from './wrap.js';
27
+ /** Geometry constants in one place: a container with different proportions changes these, not the rules. */
28
+ export const MARGIN = 20;
29
+ /** The strip between two layers: room for a route and, when the level has labels, for one to sit in. */
30
+ const LAYER_GAP = 44;
31
+ const LABELLED_LAYER_GAP = 54;
32
+ /** The space between two boxes in one layer. */
33
+ const NODE_GAP = 30;
34
+ /** How far a lane keeps from a box in a row it crosses, and the least two lanes in one gap may share. */
35
+ const LANE_CLEARANCE = 12;
36
+ const LANE_SPACING = 14;
37
+ /** Two components are two drawings on one page, so they get a wider gap than two boxes. */
38
+ const COMPONENT_GAP = 30;
39
+ /** A boundary's inner padding and the strip its name sits in. */
40
+ const GROUP_PADDING = 10;
41
+ const GROUP_HEADER = 16;
42
+ /** The text a node will draw, measured from its label - the id is what the source called it, not what it says. */
43
+ export function textOf(node) {
44
+ return measure(node.label !== undefined && node.label !== '' ? node.label : node.id);
45
+ }
46
+ /**
47
+ * Read membership and containment off the model, which is data and can be wrong.
48
+ *
49
+ * Membership is read from `members`; containment from `parent`, because only the parser knows where a boundary was
50
+ * written - a hand-written model may omit it, and then the tightest member-set that contains this one is the best
51
+ * guess available. Guards, because a model is data: a parent that does not exist is ignored, a group that contains
52
+ * itself or closes a cycle is treated as a root, and a member claimed by two groups belongs to the tightest one.
53
+ */
54
+ function groupTree(groups) {
55
+ const byId = new Map(groups.map((group) => [group.id, group]));
56
+ const owner = new Map();
57
+ const bySize = [...groups].sort((left, right) => left.members.length - right.members.length);
58
+ for (const group of bySize) {
59
+ for (const member of group.members)
60
+ if (!owner.has(member))
61
+ owner.set(member, group.id);
62
+ }
63
+ const declared = (group) => {
64
+ const parent = group.parent;
65
+ if (parent === undefined || parent === group.id)
66
+ return undefined;
67
+ return byId.has(parent) ? parent : undefined;
68
+ };
69
+ const members = new Map(groups.map((group) => [group.id, new Set(group.members)]));
70
+ const infer = (group) => {
71
+ let best;
72
+ for (const other of groups) {
73
+ if (other.id === group.id)
74
+ continue;
75
+ const outer = members.get(other.id);
76
+ const inner = members.get(group.id);
77
+ if (inner.size === 0 || inner.size >= outer.size)
78
+ continue;
79
+ let contained = true;
80
+ for (const member of inner) {
81
+ if (!outer.has(member)) {
82
+ contained = false;
83
+ break;
84
+ }
85
+ }
86
+ if (contained && (best === undefined || outer.size < best.size))
87
+ best = { id: other.id, size: outer.size };
88
+ }
89
+ return best?.id;
90
+ };
91
+ const parentOf = new Map();
92
+ for (const group of groups) {
93
+ const parent = declared(group) ?? infer(group);
94
+ if (parent !== undefined)
95
+ parentOf.set(group.id, parent);
96
+ }
97
+ // A cycle would recurse forever: walk up from each group and drop the link that closes the loop.
98
+ for (const group of groups) {
99
+ const seen = new Set([group.id]);
100
+ let cursor = parentOf.get(group.id);
101
+ while (cursor !== undefined) {
102
+ if (seen.has(cursor)) {
103
+ parentOf.delete(cursor);
104
+ break;
105
+ }
106
+ seen.add(cursor);
107
+ cursor = parentOf.get(cursor);
108
+ }
109
+ }
110
+ const children = new Map();
111
+ const roots = [];
112
+ for (const group of groups) {
113
+ const parent = parentOf.get(group.id);
114
+ if (parent === undefined)
115
+ roots.push(group.id);
116
+ else
117
+ children.set(parent, [...(children.get(parent) ?? []), group.id]);
118
+ }
119
+ return { owner, parentOf, children, roots };
120
+ }
121
+ export function place(diagram, top) {
122
+ const groups = diagram.groups ?? [];
123
+ const nodes = [];
124
+ const seenNode = new Set();
125
+ for (const node of diagram.nodes) {
126
+ if (seenNode.has(node.id))
127
+ continue;
128
+ seenNode.add(node.id);
129
+ nodes.push(node);
130
+ }
131
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
132
+ const groupById = new Map(groups.map((group) => [group.id, group]));
133
+ const tree = groupTree(groups);
134
+ // The order the source wrote things in: the fallback order wherever connectivity says nothing.
135
+ const appearance = new Map();
136
+ for (const node of nodes)
137
+ if (!appearance.has(node.id))
138
+ appearance.set(node.id, appearance.size);
139
+ for (const group of groups)
140
+ if (!appearance.has(group.id))
141
+ appearance.set(group.id, appearance.size);
142
+ const visible = new Set();
143
+ const asked = new Map();
144
+ const isVisible = (id) => {
145
+ const known = asked.get(id);
146
+ if (known !== undefined)
147
+ return known;
148
+ const group = groupById.get(id);
149
+ let found = false;
150
+ for (const member of group.members) {
151
+ if (tree.owner.get(member) === id && nodeById.has(member)) {
152
+ found = true;
153
+ break;
154
+ }
155
+ }
156
+ if (!found) {
157
+ for (const child of tree.children.get(id) ?? []) {
158
+ if (isVisible(child)) {
159
+ found = true;
160
+ break;
161
+ }
162
+ }
163
+ }
164
+ asked.set(id, found);
165
+ if (found)
166
+ visible.add(id);
167
+ return found;
168
+ };
169
+ // Every group is asked, not only the roots: a nested group's visibility is what tells its parent it has contents.
170
+ for (const group of groups)
171
+ isVisible(group.id);
172
+ const directNodes = (scope) => nodes.filter((node) => tree.owner.get(node.id) === scope).map((node) => node.id);
173
+ const directGroups = (scope) => (tree.children.get(scope) ?? []).filter((child) => visible.has(child));
174
+ const firstIndexCache = new Map();
175
+ const firstIndexOf = (id) => {
176
+ const cached = firstIndexCache.get(id);
177
+ if (cached !== undefined)
178
+ return cached;
179
+ if (nodeById.has(id))
180
+ return appearance.get(id);
181
+ const members = groupById.get(id).members
182
+ .filter((member) => tree.owner.get(member) === id && nodeById.has(member))
183
+ .map((member) => appearance.get(member));
184
+ const children = directGroups(id).map(firstIndexOf);
185
+ const all = [...members, ...children];
186
+ const found = Math.min(...all);
187
+ firstIndexCache.set(id, found);
188
+ return found;
189
+ };
190
+ /** Which group or node a diagram node is drawn as, at one level; `undefined` when it is outside that level. */
191
+ const entityAt = (scope, id) => {
192
+ // An edge may name a boundary instead of a box: it attaches to the boundary's own rectangle. A group is
193
+ // an entity at the level it is drawn in, and a nested group belongs to its outermost drawn ancestor there.
194
+ const boundary = groupById.get(id);
195
+ if (boundary !== undefined) {
196
+ if (!visible.has(id))
197
+ return undefined;
198
+ if (scope === undefined) {
199
+ let cursor = id;
200
+ while (tree.parentOf.has(cursor))
201
+ cursor = tree.parentOf.get(cursor);
202
+ return cursor;
203
+ }
204
+ return tree.parentOf.get(id) === scope ? id : undefined;
205
+ }
206
+ if (!nodeById.has(id))
207
+ return undefined;
208
+ const group = tree.owner.get(id);
209
+ if (scope === undefined) {
210
+ if (group === undefined || !visible.has(group))
211
+ return id;
212
+ let cursor = group;
213
+ while (tree.parentOf.has(cursor))
214
+ cursor = tree.parentOf.get(cursor);
215
+ return cursor;
216
+ }
217
+ if (group === scope)
218
+ return id;
219
+ if (group === undefined)
220
+ return undefined;
221
+ return tree.parentOf.get(group) === scope && visible.has(group) ? group : undefined;
222
+ };
223
+ const groupLayouts = new Map();
224
+ const measureGroup = (id) => {
225
+ const inside = buildLevel(id);
226
+ const size = {
227
+ width: Math.max(MIN_NODE_WIDTH, inside.width) + GROUP_PADDING * 2,
228
+ height: inside.height + GROUP_HEADER + GROUP_PADDING * 2,
229
+ };
230
+ groupLayouts.set(id, inside);
231
+ return size;
232
+ };
233
+ const entityOf = (id) => {
234
+ const node = nodeById.get(id);
235
+ if (node !== undefined) {
236
+ const text = textOf(node);
237
+ return { id, group: false, width: text.width, height: text.height };
238
+ }
239
+ const size = measureGroup(id);
240
+ return { id, group: true, width: size.width, height: size.height };
241
+ };
242
+ const entitiesIn = (scope) => {
243
+ const found = [];
244
+ if (scope === undefined) {
245
+ for (const node of nodes) {
246
+ const group = tree.owner.get(node.id);
247
+ if (group === undefined || !visible.has(group))
248
+ found.push(entityOf(node.id));
249
+ }
250
+ for (const root of tree.roots)
251
+ if (visible.has(root))
252
+ found.push(entityOf(root));
253
+ }
254
+ else {
255
+ for (const id of directNodes(scope))
256
+ found.push(entityOf(id));
257
+ for (const id of directGroups(scope))
258
+ found.push(entityOf(id));
259
+ }
260
+ return found.sort((left, right) => firstIndexOf(left.id) - firstIndexOf(right.id));
261
+ };
262
+ const edgesIn = (scope) => {
263
+ const found = [];
264
+ diagram.edges.forEach((edge, index) => {
265
+ if (edge.from === edge.to)
266
+ return;
267
+ const from = entityAt(scope, edge.from);
268
+ const to = entityAt(scope, edge.to);
269
+ if (from === undefined || to === undefined || from === to)
270
+ return;
271
+ found.push({ index, from, to });
272
+ });
273
+ return found;
274
+ };
275
+ /** The entity that holds the initial state at this level, which is where layer zero is pinned. */
276
+ const pinnedAt = (scope) => {
277
+ if (diagram.init === undefined)
278
+ return undefined;
279
+ const entity = entityAt(scope, diagram.init);
280
+ return entity === diagram.init || (entity !== undefined && groupById.has(entity)) ? entity : undefined;
281
+ };
282
+ /**
283
+ * Lay out one weakly connected component: the pipeline, unshortened.
284
+ *
285
+ * A component has its own layers, its own ordering and its own coordinates. Nothing outside it can move it.
286
+ */
287
+ const layoutPiece = (entities, edges, pinned, seed) => {
288
+ const byId = new Map(entities.map((entity) => [entity.id, entity]));
289
+ const graph = {
290
+ nodes: entities.map((entity) => ({ id: entity.id, width: entity.width, height: entity.height })),
291
+ edges,
292
+ ...(pinned === undefined ? {} : { init: pinned }),
293
+ };
294
+ const broken = breakCycles(graph);
295
+ const layered = layer(graph, broken.edges);
296
+ const augmented = addDummies(layered, broken.edges);
297
+ const ordered = order(augmented.layers, augmented.chains, seed);
298
+ const gap = edges.some((edge) => diagram.edges[edge.index].label !== '') ? LABELLED_LAYER_GAP : LAYER_GAP;
299
+ const coordinates = assignCoordinates({
300
+ layers: ordered.layers,
301
+ chains: augmented.chains,
302
+ sizeOf: (id) => {
303
+ const entity = byId.get(id);
304
+ return { width: entity.width, height: entity.height };
305
+ },
306
+ chainOf: augmented.chainOf,
307
+ }, { gap: NODE_GAP, layerGap: gap });
308
+ const bands = planBands(coordinates.layerWidth, coordinates.layerHeight, gap);
309
+ const bandOfLayer = new Array(coordinates.layerHeight.length).fill(0);
310
+ bands.bands.forEach((band, index) => {
311
+ for (let depth = band.first; depth <= band.last; depth += 1)
312
+ bandOfLayer[depth] = index;
313
+ });
314
+ const layerTop = [];
315
+ for (const band of bands.bands) {
316
+ let y = 0;
317
+ for (let depth = band.first; depth <= band.last; depth += 1) {
318
+ layerTop[depth] = y;
319
+ y += coordinates.layerHeight[depth] + gap;
320
+ }
321
+ }
322
+ const items = new Map();
323
+ ordered.layers.forEach((row, depth) => {
324
+ const shift = bands.bands[bandOfLayer[depth]].offsetX;
325
+ const top = layerTop[depth];
326
+ const rowHeight = coordinates.layerHeight[depth];
327
+ for (const id of row) {
328
+ const entity = byId.get(id);
329
+ if (entity === undefined)
330
+ continue;
331
+ const centre = coordinates.x.get(id);
332
+ items.set(id, {
333
+ box: {
334
+ x: centre + shift - entity.width / 2,
335
+ y: top + (rowHeight - entity.height) / 2,
336
+ width: entity.width,
337
+ height: entity.height,
338
+ },
339
+ group: entity.group,
340
+ });
341
+ }
342
+ });
343
+ // An edge whose ends sit in two different columns is read across the page, not down it: the painter draws it
344
+ // out of the side of one box and into the side of the other, which is where a wrapped reading order turns.
345
+ const across = new Set();
346
+ for (const edge of edges) {
347
+ const from = layered.layerOf.get(edge.from);
348
+ const to = layered.layerOf.get(edge.to);
349
+ if (bandOfLayer[from] !== bandOfLayer[to])
350
+ across.add(edge.index);
351
+ }
352
+ // The channels: where each long edge's lane runs and where the runs between two rows sit. A lane is only worth
353
+ // reserving when both ends are boxes - a lane that ends on a boundary has no box to arrive at - and one that
354
+ // crosses a band boundary is two lanes with different column offsets. Those edges get the router instead.
355
+ const laneProposal = new Map();
356
+ for (const chain of augmented.chains) {
357
+ if (chain.path.length <= 2)
358
+ continue;
359
+ const lane = augmented.chainOf.get(chain.path[1]);
360
+ const fromLayer = layered.layerOf.get(chain.path[0]);
361
+ const proposed = coordinates.x.get(lane);
362
+ laneProposal.set(lane, proposed + bands.bands[bandOfLayer[fromLayer]].offsetX);
363
+ }
364
+ const itemBoxes = new Map();
365
+ for (const [id, item] of items)
366
+ itemBoxes.set(id, item.box);
367
+ const channels = assignChannels({
368
+ layers: ordered.layers,
369
+ chains: augmented.chains,
370
+ chainOf: augmented.chainOf,
371
+ boxes: itemBoxes,
372
+ layerY: layerTop,
373
+ layerHeight: coordinates.layerHeight,
374
+ laneX: laneProposal,
375
+ }, { clearance: LANE_CLEARANCE, spacing: LANE_SPACING });
376
+ const lanes = new Map();
377
+ for (const chain of augmented.chains) {
378
+ if (chain.path.length <= 2)
379
+ continue;
380
+ const start = byId.get(chain.path[0]);
381
+ const end = byId.get(chain.path[chain.path.length - 1]);
382
+ if (start.group || end.group)
383
+ continue;
384
+ const fromLayer = layered.layerOf.get(chain.path[0]);
385
+ const toLayer = layered.layerOf.get(chain.path[chain.path.length - 1]);
386
+ if (bandOfLayer[fromLayer] !== bandOfLayer[toLayer])
387
+ continue;
388
+ const channel = channels.lanes.get(chain.index);
389
+ lanes.set(chain.index, {
390
+ points: channel.y.map((y) => ({ x: channel.x, y })),
391
+ reversed: broken.reversed.has(chain.index),
392
+ });
393
+ }
394
+ const runs = new Map();
395
+ for (const [edgeIndex, y] of channels.runs)
396
+ if (!across.has(edgeIndex))
397
+ runs.set(edgeIndex, y);
398
+ let right = 0;
399
+ let bottom = 0;
400
+ for (const entry of items.values()) {
401
+ right = Math.max(right, entry.box.x + entry.box.width);
402
+ bottom = Math.max(bottom, entry.box.y + entry.box.height);
403
+ }
404
+ return { width: right, height: bottom, items, lanes, runs, across };
405
+ };
406
+ function buildLevel(scope) {
407
+ const entities = entitiesIn(scope);
408
+ if (entities.length === 0) {
409
+ return { width: 0, height: 0, items: new Map(), lanes: new Map(), runs: new Map(), across: new Set() };
410
+ }
411
+ const edges = edgesIn(scope);
412
+ const pinned = pinnedAt(scope);
413
+ const seed = seedOrder({ nodes: entities.map((entity) => ({ id: entity.id, width: entity.width, height: entity.height })), edges, ...(pinned === undefined ? {} : { init: pinned }) });
414
+ const components = weakComponents(entities, edges);
415
+ const pieces = [];
416
+ for (const members of components) {
417
+ const inside = new Set(members);
418
+ const pieceEntities = entities.filter((entity) => inside.has(entity.id));
419
+ const pieceEdges = edges.filter((edge) => inside.has(edge.from) && inside.has(edge.to));
420
+ const pin = pinned !== undefined && inside.has(pinned) ? pinned : undefined;
421
+ pieces.push(layoutPiece(pieceEntities, pieceEdges, pin, seed));
422
+ }
423
+ const packed = packRectangles(pieces.map((piece) => ({ width: piece.width, height: piece.height })), { gap: COMPONENT_GAP });
424
+ const layout = { width: packed.width, height: packed.height, items: new Map(), lanes: new Map(), runs: new Map(), across: new Set() };
425
+ pieces.forEach((piece, index) => {
426
+ for (const edgeIndex of piece.across)
427
+ layout.across.add(edgeIndex);
428
+ const at = packed.placements[index];
429
+ for (const [id, item] of piece.items) {
430
+ layout.items.set(id, { box: { x: item.box.x + at.x, y: item.box.y + at.y, width: item.box.width, height: item.box.height }, group: item.group });
431
+ }
432
+ for (const [edgeIndex, lane] of piece.lanes) {
433
+ layout.lanes.set(edgeIndex, { points: lane.points.map((point) => ({ x: point.x + at.x, y: point.y + at.y })), reversed: lane.reversed });
434
+ }
435
+ for (const [edgeIndex, y] of piece.runs)
436
+ layout.runs.set(edgeIndex, y + at.y);
437
+ });
438
+ return layout;
439
+ }
440
+ const root = buildLevel(undefined);
441
+ const boxes = new Map();
442
+ const groupRects = new Map();
443
+ const lanes = new Map();
444
+ const runs = new Map();
445
+ const across = new Set();
446
+ const placeLevel = (layout, originX, originY) => {
447
+ for (const edgeIndex of layout.across)
448
+ across.add(edgeIndex);
449
+ for (const [id, item] of layout.items) {
450
+ const box = { x: originX + item.box.x, y: originY + item.box.y, width: item.box.width, height: item.box.height };
451
+ if (item.group) {
452
+ groupRects.set(id, box);
453
+ const inside = groupLayouts.get(id);
454
+ placeLevel(inside, box.x + (box.width - inside.width) / 2, box.y + GROUP_HEADER + GROUP_PADDING);
455
+ continue;
456
+ }
457
+ boxes.set(id, box);
458
+ }
459
+ for (const [edgeIndex, lane] of layout.lanes) {
460
+ lanes.set(edgeIndex, { points: lane.points.map((point) => ({ x: point.x + originX, y: point.y + originY })), reversed: lane.reversed });
461
+ }
462
+ for (const [edgeIndex, y] of layout.runs)
463
+ runs.set(edgeIndex, y + originY);
464
+ };
465
+ placeLevel(root, MARGIN, top);
466
+ let right = MARGIN;
467
+ for (const box of boxes.values())
468
+ right = Math.max(right, box.x + box.width);
469
+ for (const box of groupRects.values())
470
+ right = Math.max(right, box.x + box.width);
471
+ return { boxes, groupRects, width: right + MARGIN, lanes, runs, across };
472
+ }
473
+ /** The aspect ratio of a placement, for the tests that check a drawing is a page and not a strip. */
474
+ export function placementAspect(placement) {
475
+ let right = 0;
476
+ let bottom = 0;
477
+ for (const box of [...placement.boxes.values(), ...placement.groupRects.values()]) {
478
+ right = Math.max(right, box.x + box.width);
479
+ bottom = Math.max(bottom, box.y + box.height);
480
+ }
481
+ return aspectOf(right, bottom);
482
+ }