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.
- package/README.en-US.md +12 -0
- package/README.md +4 -0
- package/lib/client.js +2829 -224
- package/lib/render/channels.js +243 -0
- package/lib/render/collapse.js +12 -20
- package/lib/render/components.js +99 -0
- package/lib/render/coords.js +288 -0
- package/lib/render/cycle-break.js +147 -0
- package/lib/render/dummy.js +43 -0
- package/lib/render/from-mermaid.js +34 -10
- package/lib/render/from-plantuml.js +318 -46
- package/lib/render/layer.js +91 -0
- package/lib/render/layout.js +27 -11
- package/lib/render/network-simplex.js +272 -0
- package/lib/render/order.js +122 -0
- package/lib/render/paint.js +1227 -69
- package/lib/render/place.js +393 -121
- package/lib/render/route.js +225 -0
- package/lib/render/svg.js +146 -21
- package/lib/render/symbol.js +108 -0
- package/lib/render/text.js +74 -0
- package/lib/render/trunk.js +213 -0
- package/lib/render/wrap.js +87 -0
- package/lib/types/render/channels.d.ts +63 -0
- package/lib/types/render/components.d.ts +57 -0
- package/lib/types/render/coords.d.ts +64 -0
- package/lib/types/render/cycle-break.d.ts +45 -0
- package/lib/types/render/dummy.d.ts +32 -0
- package/lib/types/render/from-plantuml.d.ts +12 -6
- package/lib/types/render/layer.d.ts +28 -0
- package/lib/types/render/layout.d.ts +43 -11
- package/lib/types/render/model.d.ts +17 -0
- package/lib/types/render/network-simplex.d.ts +54 -0
- package/lib/types/render/order.d.ts +27 -0
- package/lib/types/render/paint.d.ts +157 -10
- package/lib/types/render/place.d.ts +40 -19
- package/lib/types/render/route.d.ts +77 -0
- package/lib/types/render/svg.d.ts +1 -1
- package/lib/types/render/symbol.d.ts +73 -0
- package/lib/types/render/text.d.ts +38 -0
- package/lib/types/render/trunk.d.ts +104 -0
- package/lib/types/render/wrap.d.ts +27 -0
- package/package.json +148 -148
- package/skills/model-everything-diagram/SKILL.md +85 -82
- package/src/render/channels.ts +309 -0
- package/src/render/collapse.ts +150 -155
- package/src/render/components.ts +126 -0
- package/src/render/coords.ts +328 -0
- package/src/render/cycle-break.ts +164 -0
- package/src/render/dummy.ts +65 -0
- package/src/render/from-mermaid.ts +261 -236
- package/src/render/from-plantuml.ts +469 -178
- package/src/render/layer.ts +98 -0
- package/src/render/layout.ts +101 -62
- package/src/render/model.ts +86 -67
- package/src/render/network-simplex.ts +290 -0
- package/src/render/order.ts +139 -0
- package/src/render/paint.ts +1570 -290
- package/src/render/place.ts +532 -234
- package/src/render/route.ts +269 -0
- package/src/render/svg.ts +274 -127
- package/src/render/symbol.ts +137 -0
- package/src/render/text.ts +86 -0
- package/src/render/trunk.ts +275 -0
- package/src/render/wrap.ts +103 -0
package/lib/render/place.js
CHANGED
|
@@ -1,68 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Where the boxes go.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
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 {
|
|
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. */
|
|
17
28
|
export const MARGIN = 20;
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
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;
|
|
24
39
|
/** A boundary's inner padding and the strip its name sits in. */
|
|
25
40
|
const GROUP_PADDING = 10;
|
|
26
41
|
const GROUP_HEADER = 16;
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
export const nodeWidth = (label) => Math.min(MAX_NODE_WIDTH, Math.max(MIN_NODE_WIDTH, label.length * CHAR_WIDTH + 26));
|
|
31
|
-
/** The layered placement: breadth-first rows, centred, no containment. */
|
|
32
|
-
function placeByRanks(diagram, nodes, top) {
|
|
33
|
-
const ranked = rankStates(diagram.nodes, diagram.edges, diagram.init);
|
|
34
|
-
const rows = [...ranked.rows];
|
|
35
|
-
if (ranked.unreachable.length > 0)
|
|
36
|
-
rows.push(ranked.unreachable);
|
|
37
|
-
const boxes = new Map();
|
|
38
|
-
let widest = 0;
|
|
39
|
-
rows.forEach((row, index) => {
|
|
40
|
-
const widths = row.map((id) => nodeWidth(id));
|
|
41
|
-
widest = Math.max(widest, widths.reduce((total, width) => total + width, 0) + Math.max(0, row.length - 1) * COLUMN_GAP);
|
|
42
|
-
let x = 0;
|
|
43
|
-
row.forEach((id, position) => {
|
|
44
|
-
boxes.set(id, { x, y: top + index * (NODE_HEIGHT + ROW_GAP), width: widths[position], height: NODE_HEIGHT });
|
|
45
|
-
x += widths[position] + COLUMN_GAP;
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
rows.forEach((row) => {
|
|
49
|
-
const rowWidth = row.reduce((total, id) => total + boxes.get(id).width, 0) + Math.max(0, row.length - 1) * COLUMN_GAP;
|
|
50
|
-
const offset = (widest - rowWidth) / 2;
|
|
51
|
-
for (const id of row)
|
|
52
|
-
boxes.get(id).x += offset + MARGIN;
|
|
53
|
-
});
|
|
54
|
-
return { boxes, groupRects: new Map(), width: widest + MARGIN * 2 };
|
|
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);
|
|
55
45
|
}
|
|
56
46
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* Membership is read from `members`; containment is read from `parent`, because only the parser knows where a
|
|
60
|
-
* boundary was written. A model assembled by hand may omit `parent`, and then containment is inferred from
|
|
61
|
-
* member sets - the tightest superset wins - which is the best such a model can offer.
|
|
47
|
+
* Read membership and containment off the model, which is data and can be wrong.
|
|
62
48
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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.
|
|
66
53
|
*/
|
|
67
54
|
function groupTree(groups) {
|
|
68
55
|
const byId = new Map(groups.map((group) => [group.id, group]));
|
|
@@ -129,82 +116,367 @@ function groupTree(groups) {
|
|
|
129
116
|
else
|
|
130
117
|
children.set(parent, [...(children.get(parent) ?? []), group.id]);
|
|
131
118
|
}
|
|
132
|
-
return { owner, children, roots };
|
|
119
|
+
return { owner, parentOf, children, roots };
|
|
133
120
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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;
|
|
148
168
|
};
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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;
|
|
160
189
|
};
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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;
|
|
177
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);
|
|
178
231
|
return size;
|
|
179
232
|
};
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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;
|
|
189
281
|
};
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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;
|
|
198
439
|
}
|
|
199
|
-
|
|
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 };
|
|
200
472
|
}
|
|
201
|
-
/**
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
return
|
|
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);
|
|
210
482
|
}
|