@rse/gradia 1.2.0 → 1.2.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.md +17 -4
- package/dst/gradia-api-render-node.d.ts +1 -0
- package/dst/gradia-api-render-node.js +8 -1
- package/dst/gradia-api-type-graph.js +110 -55
- package/dst/gradia-api-type-grid.js +6 -3
- package/dst/gradia-api.umd.js +2 -2
- package/dst/gradia-mcp.js +13 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -197,7 +197,7 @@ separated by `|`.
|
|
|
197
197
|
- The `<edge-name>` is rendered as the label of the edge and the
|
|
198
198
|
`<edge-arity>` as its cardinality label.
|
|
199
199
|
|
|
200
|
-
-
|
|
200
|
+
- Seven attribute keys are reserved and consumed by the renderer instead
|
|
201
201
|
of being displayed as a regular `<key>: <val>` line:
|
|
202
202
|
|
|
203
203
|
- `url: <url>` renders the node box as a hyperlink. Only relative URLs
|
|
@@ -213,7 +213,18 @@ separated by `|`.
|
|
|
213
213
|
node which is implicitly declared if never referenced otherwise),
|
|
214
214
|
- `container: <type>` marks the node as a container whose members
|
|
215
215
|
are laid out with the diagram type `<type>` (`graph`, `hub`, or
|
|
216
|
-
`grid`), even if it has no members at all
|
|
216
|
+
`grid`), even if it has no members at all,
|
|
217
|
+
- `order: <number>` places the node explicitly (see below).
|
|
218
|
+
|
|
219
|
+
- A `graph` (a `graph` container level, too) with at least one node
|
|
220
|
+
carrying an `order` attribute is laid out in that explicit order
|
|
221
|
+
instead of by the layered algorithm: the distinct order values become
|
|
222
|
+
the rows, top-down in ascending order, the nodes of one value fill
|
|
223
|
+
their row left-to-right in declaration order (wrapped into further
|
|
224
|
+
rows beyond `graph-columns-max` nodes), and the nodes without an order
|
|
225
|
+
follow in the trailing rows. A `grid` places its tiles in the order of
|
|
226
|
+
the attribute, too, the tiles without one trailing. An invalid value
|
|
227
|
+
is ignored.
|
|
217
228
|
|
|
218
229
|
- A node cannot be a member of more than one group. As soon as at least
|
|
219
230
|
one node carries a `group` attribute, all nodes without one implicitly
|
|
@@ -236,8 +247,10 @@ separated by `|`.
|
|
|
236
247
|
except a container with one of its own members. An edge crossing the
|
|
237
248
|
boundary of a container laid out as `graph` is routed through the box
|
|
238
249
|
border to the inner node (entering on the west and leaving on the east
|
|
239
|
-
side
|
|
240
|
-
|
|
250
|
+
side, and where other nodes lie between the inner node and the border,
|
|
251
|
+
around them through the gutter below, passing the border at the
|
|
252
|
+
height of that gutter), while an edge crossing the boundary of a
|
|
253
|
+
container laid out as `hub` or `grid` ends at the box border instead.
|
|
241
254
|
|
|
242
255
|
- All remaining attributes are displayed as `<key>: <val>` lines inside
|
|
243
256
|
the node box.
|
|
@@ -9,6 +9,7 @@ export declare const containerTypeOf: (node: Node) => string | undefined;
|
|
|
9
9
|
export declare const HEAD_H = 34;
|
|
10
10
|
export declare const HEAD_T = 20;
|
|
11
11
|
export declare const containerHead: (node: Node) => number;
|
|
12
|
+
export declare const orderOf: (node: Node) => number | undefined;
|
|
12
13
|
export declare const attrsOfNode: (node: Node) => Attr[];
|
|
13
14
|
export declare const urlOf: (node: Node) => string | undefined;
|
|
14
15
|
export declare const PAD_W = 18;
|
|
@@ -28,9 +28,16 @@ export const containerTypeOf = (node) => node.attrs.findLast((attr) => attr.key
|
|
|
28
28
|
export const HEAD_H = 34; /* height of the container box head */
|
|
29
29
|
export const HEAD_T = 20; /* extra head height of a type line */
|
|
30
30
|
export const containerHead = (node) => HEAD_H + (typeOf(node) !== undefined ? HEAD_T : 0);
|
|
31
|
+
/* the special "order" annotation (the explicit top-down placement
|
|
32
|
+
order of the node, a finite number, an invalid value being ignored) */
|
|
33
|
+
export const orderOf = (node) => {
|
|
34
|
+
const val = node.attrs.findLast((attr) => attr.key === "order")?.val;
|
|
35
|
+
const num = val !== undefined && val.trim() !== "" ? Number(val) : NaN;
|
|
36
|
+
return Number.isFinite(num) ? num : undefined;
|
|
37
|
+
};
|
|
31
38
|
/* the special attributes consumed by the renderers instead of being
|
|
32
39
|
rendered as node box text lines */
|
|
33
|
-
const ATTRS_SPECIAL = new Set(["url", "type", "primary", "group", "parent", "container"]);
|
|
40
|
+
const ATTRS_SPECIAL = new Set(["url", "type", "primary", "group", "parent", "container", "order"]);
|
|
34
41
|
/* split the attributes rendered as text lines from the special ones */
|
|
35
42
|
export const attrsOfNode = (node) => node.attrs.filter((attr) => !ATTRS_SPECIAL.has(attr.key));
|
|
36
43
|
/* the special "url" attribute (rendered as a hyperlink on the whole node
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
/* external dependencies */
|
|
7
7
|
import { DagreLayout } from "@antv/layout";
|
|
8
8
|
import { FS_EDGE, FS_ARITY, ARITY_OFF, textWidth } from "./gradia-api-render-base.js";
|
|
9
|
-
import { measureNodes } from "./gradia-api-render-node.js";
|
|
9
|
+
import { measureNodes, orderOf } from "./gradia-api-render-node.js";
|
|
10
10
|
import { simplifyPoly, assignPorts, assignTracks, computeHops } from "./gradia-api-render-edge.js";
|
|
11
11
|
/* rendering geometry constants */
|
|
12
12
|
const CLUSTX = 26; /* max X distance within a grid column */
|
|
@@ -243,13 +243,17 @@ const placeGates = (gates, edges, col, row, ncols, nrows) => {
|
|
|
243
243
|
place(outs, ncols - 1);
|
|
244
244
|
return { ncols, nrows };
|
|
245
245
|
};
|
|
246
|
+
/* the channels an edge runs a vertical track in (the gate-side
|
|
247
|
+
channel of a gutter-ending edge is merely crossed horizontally) */
|
|
248
|
+
const trackChans = (plan) => plan.gate === "t" ? [plan.chans[0]] : plan.gate === "s" ? [plan.chans[1]] : plan.chans;
|
|
246
249
|
/* plan the coarse route of every edge: which inter-column channels
|
|
247
250
|
and inter-row gutters it occupies (based on the grid indices and
|
|
248
251
|
the attachment sides only): the source stub reaches the channel
|
|
249
252
|
beside its attachment side, the target stub the channel beside
|
|
250
253
|
its one, and, if those differ, the gutter next to the target row
|
|
251
|
-
connects them
|
|
252
|
-
|
|
254
|
+
connects them (an edge whose gate end lies beyond that gutter ends
|
|
255
|
+
in the gutter itself, as the gate merely marks the border passage) */
|
|
256
|
+
const planRoutes = (edges, col, row, sides, gates) => {
|
|
253
257
|
const flag = (from, to) => ({ mirror: from === "r", hairpin: from === to });
|
|
254
258
|
const plans = edges.map((edge, i) => {
|
|
255
259
|
/* a self-loop detours as a hairpin through the channel beside
|
|
@@ -267,10 +271,12 @@ const planRoutes = (edges, col, row, sides) => {
|
|
|
267
271
|
if (sChan === tChan)
|
|
268
272
|
return { chans: [sChan], guts: [], flags: [flag(sFrom, tTo)] };
|
|
269
273
|
const dir = tChan > sChan ? "r" : "l";
|
|
274
|
+
const gate = gates.has(edge.target) ? "t" : gates.has(edge.source) ? "s" : undefined;
|
|
270
275
|
return {
|
|
271
276
|
chans: [sChan, tChan],
|
|
272
277
|
guts: [tr > sr ? tr - 1 : tr],
|
|
273
|
-
flags: [flag(sFrom, dir), flag(dir === "r" ? "l" : "r", tTo)]
|
|
278
|
+
flags: [flag(sFrom, dir), flag(dir === "r" ? "l" : "r", tTo)],
|
|
279
|
+
gate
|
|
274
280
|
};
|
|
275
281
|
});
|
|
276
282
|
/* count the edges occupying each channel and gutter and determine
|
|
@@ -286,7 +292,8 @@ const planRoutes = (edges, col, row, sides) => {
|
|
|
286
292
|
const gutLbl = new Map();
|
|
287
293
|
const chanNeed = (c, width) => chanLbl.set(c, Math.max(chanLbl.get(c) ?? 0, width));
|
|
288
294
|
plans.forEach((plan, i) => {
|
|
289
|
-
|
|
295
|
+
const chans = trackChans(plan);
|
|
296
|
+
for (const c of chans)
|
|
290
297
|
chanCnt.set(c, (chanCnt.get(c) ?? 0) + 1);
|
|
291
298
|
for (const g of plan.guts)
|
|
292
299
|
gutCnt.set(g, (gutCnt.get(g) ?? 0) + 1);
|
|
@@ -297,8 +304,8 @@ const planRoutes = (edges, col, row, sides) => {
|
|
|
297
304
|
chanNeed(plan.chans[0], textWidth(name, FS_EDGE) + 2 * CHAN_LBL);
|
|
298
305
|
else if (name !== undefined && plan.guts.length > 0)
|
|
299
306
|
gutLbl.set(plan.guts[0], (gutLbl.get(plan.guts[0]) ?? 0) + 1);
|
|
300
|
-
if (arity !== undefined &&
|
|
301
|
-
chanNeed(
|
|
307
|
+
if (arity !== undefined && chans.length > 0)
|
|
308
|
+
chanNeed(chans[chans.length - 1], ARITY_OFF + textWidth(arity, FS_ARITY) + CHAN_LBL);
|
|
302
309
|
});
|
|
303
310
|
/* count the self-loops of every node, as their detours stack up
|
|
304
311
|
above its box and have to fit into the gutter above its row
|
|
@@ -363,10 +370,11 @@ const assignTrackCoords = (edges, plans, portPos, grid, config) => {
|
|
|
363
370
|
const { colWidth, rowHeight, chanW, gutH, colCX, rowCY } = grid;
|
|
364
371
|
const gutBase = (g) => rowCY[g] + rowHeight[g] / 2 + gutH[g] / 2;
|
|
365
372
|
/* assign the vertical tracks within the inter-column channels (the
|
|
366
|
-
leg of a self-loop spans up to its detour above the box
|
|
373
|
+
leg of a self-loop spans up to its detour above the box, and a
|
|
374
|
+
gutter-ending edge takes no track in its gate-side channel) */
|
|
367
375
|
const chanUsers = new Map();
|
|
368
376
|
edges.forEach((edge, i) => {
|
|
369
|
-
const { chans, guts, flags } = plans[i];
|
|
377
|
+
const { chans, guts, flags, gate } = plans[i];
|
|
370
378
|
if (chans.length === 0)
|
|
371
379
|
return;
|
|
372
380
|
const sp = portPos.get(`${i}:s`);
|
|
@@ -377,8 +385,10 @@ const assignTrackCoords = (edges, plans, portPos, grid, config) => {
|
|
|
377
385
|
pushTo(chanUsers, chans[0], { edge: i, posIn: sp.y, posOut: tp.y, ...flags[0] });
|
|
378
386
|
else {
|
|
379
387
|
const gy = gutBase(guts[0]);
|
|
380
|
-
|
|
381
|
-
|
|
388
|
+
if (gate !== "s")
|
|
389
|
+
pushTo(chanUsers, chans[0], { edge: i, posIn: sp.y, posOut: gy, ...flags[0] });
|
|
390
|
+
if (gate !== "t")
|
|
391
|
+
pushTo(chanUsers, chans[1], { edge: i, posIn: gy, posOut: tp.y, ...flags[1] });
|
|
382
392
|
}
|
|
383
393
|
});
|
|
384
394
|
const chanOff = new Map();
|
|
@@ -387,17 +397,20 @@ const assignTrackCoords = (edges, plans, portPos, grid, config) => {
|
|
|
387
397
|
chanOff.set(`${c}:${edge}`, off);
|
|
388
398
|
const chanX = (c, edge) => colCX[c] + colWidth[c] / 2 + chanW[c] / 2 +
|
|
389
399
|
chanOff.get(`${c}:${edge}`);
|
|
390
|
-
/* assign the horizontal tracks within the inter-row gutters
|
|
400
|
+
/* assign the horizontal tracks within the inter-row gutters (the
|
|
401
|
+
run of a gutter-ending edge extends up to its gate, where its
|
|
402
|
+
stub continues straight and hence crosses no other track) */
|
|
391
403
|
const gutUsers = new Map();
|
|
392
404
|
edges.forEach((_, i) => {
|
|
393
|
-
const { chans, guts } = plans[i];
|
|
405
|
+
const { chans, guts, gate } = plans[i];
|
|
394
406
|
if (guts.length === 0)
|
|
395
407
|
return;
|
|
396
408
|
const sp = portPos.get(`${i}:s`);
|
|
409
|
+
const tp = portPos.get(`${i}:t`);
|
|
397
410
|
pushTo(gutUsers, guts[0], {
|
|
398
411
|
edge: i,
|
|
399
|
-
posIn: chanX(chans[0], i),
|
|
400
|
-
posOut: chanX(chans[1], i),
|
|
412
|
+
posIn: gate === "s" ? sp.x : chanX(chans[0], i),
|
|
413
|
+
posOut: gate === "t" ? tp.x : chanX(chans[1], i),
|
|
401
414
|
mirror: sp.y > gutBase(guts[0])
|
|
402
415
|
});
|
|
403
416
|
});
|
|
@@ -442,10 +455,17 @@ const routePolys = (edges, plans, portPos, boxH, cy, chanX, gutY) => {
|
|
|
442
455
|
pts = [[sp.x, sp.y], [ch, sp.y], [ch, tp.y], [tp.x, tp.y]];
|
|
443
456
|
}
|
|
444
457
|
else {
|
|
458
|
+
/* a gutter-ending edge leaves the gutter straight into its
|
|
459
|
+
gate instead of turning into the gate-side channel */
|
|
445
460
|
const ch1 = chanX(chans[0], i);
|
|
446
461
|
const ch2 = chanX(chans[1], i);
|
|
447
462
|
const gy = gutY(guts[0], i);
|
|
448
|
-
|
|
463
|
+
if (plans[i].gate === "t")
|
|
464
|
+
pts = [[sp.x, sp.y], [ch1, sp.y], [ch1, gy], [tp.x, gy]];
|
|
465
|
+
else if (plans[i].gate === "s")
|
|
466
|
+
pts = [[sp.x, gy], [ch2, gy], [ch2, tp.y], [tp.x, tp.y]];
|
|
467
|
+
else
|
|
468
|
+
pts = [[sp.x, sp.y], [ch1, sp.y], [ch1, gy], [ch2, gy], [ch2, tp.y], [tp.x, tp.y]];
|
|
449
469
|
}
|
|
450
470
|
return simplifyPoly(pts);
|
|
451
471
|
});
|
|
@@ -480,7 +500,8 @@ const routeEdges = (edges, plans, sides, boxW, boxH, cx, cy, grid, config, level
|
|
|
480
500
|
gutter-routed edges keep approaching it from their gutter side */
|
|
481
501
|
const nudgeNodes = (nodes, edges, plans, col, row, boxH, grid, nrows, dy, route) => {
|
|
482
502
|
const gutterFree = (g, c) => !plans.some((plan) => plan.guts[0] === g
|
|
483
|
-
&& Math.min(...plan.chans) < c
|
|
503
|
+
&& Math.min(...plan.chans) < c
|
|
504
|
+
&& c <= Math.max(...plan.chans) + (plan.gate === "t" ? 1 : 0))
|
|
484
505
|
&& !edges.some((edge) => edge.source === edge.target
|
|
485
506
|
&& row.get(edge.source) === g + 1 && col.get(edge.source) === c);
|
|
486
507
|
const shiftRange = (id) => {
|
|
@@ -596,56 +617,90 @@ export const render = async (graph, config, level = {}) => {
|
|
|
596
617
|
(all boxes at half height scale, grown by their edge
|
|
597
618
|
attachment needs once the attachment sides are known) */
|
|
598
619
|
const { boxW, boxH, contentH } = measureNodes(nodes, config, () => config["size-node-height-scale"] / 2, level.fixedSize);
|
|
599
|
-
/* determine raw node positions with the AntV Dagre layout */
|
|
600
|
-
const rawX = new Map();
|
|
601
|
-
const rawY = new Map();
|
|
602
|
-
if (real.length > 0) {
|
|
603
|
-
const layout = new DagreLayout({
|
|
604
|
-
rankdir: "LR",
|
|
605
|
-
nodesep: config["graph-node-separation"],
|
|
606
|
-
ranksep: config["graph-rank-separation"],
|
|
607
|
-
nodeSize: (node) => node.size
|
|
608
|
-
});
|
|
609
|
-
await layout.execute({
|
|
610
|
-
nodes: real.map((node) => ({ id: node.id, size: [boxW.get(node.id), boxH.get(node.id)] })),
|
|
611
|
-
edges: realEdges.map((edge, i) => ({ id: `e${i}`, source: edge.source, target: edge.target }))
|
|
612
|
-
});
|
|
613
|
-
layout.forEachNode((node) => {
|
|
614
|
-
rawX.set(String(node.id), node.x);
|
|
615
|
-
rawY.set(String(node.id), node.y);
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
/* snap the raw positions onto the discrete column/row grid */
|
|
619
|
-
const { col, row, ncols: gridCols, nrows: gridRows } = snapToGrid(real, rawX, rawY);
|
|
620
|
-
/* refine the row assignment by pulling every node vertically
|
|
621
|
-
toward the median row of its direct neighbors */
|
|
622
|
-
refineRows(real, realEdges, col, row);
|
|
623
|
-
/* fold the grid columns: constrain the diagram width to at most
|
|
624
|
-
the configured maximum of side-by-side nodes by wrapping excess
|
|
625
|
-
columns into additional row bands below, so wide graphs grow in
|
|
626
|
-
height */
|
|
627
620
|
const maxCols = Math.max(Math.floor(config["graph-columns-max"]), 1);
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
621
|
+
const col = new Map();
|
|
622
|
+
const row = new Map();
|
|
623
|
+
let ncols;
|
|
624
|
+
let nrows;
|
|
625
|
+
if (real.some((node) => orderOf(node) !== undefined)) {
|
|
626
|
+
/* an explicitly ordered graph (at least one node carrying an
|
|
627
|
+
"order" attribute) bypasses the layered layout: the distinct
|
|
628
|
+
order values become the rows, top-down in ascending order,
|
|
629
|
+
the nodes of one value fill their row left-to-right in
|
|
630
|
+
declaration order (wrapped into further rows beyond the
|
|
631
|
+
configured maximum of side-by-side nodes), and the nodes
|
|
632
|
+
without an order follow in the trailing rows */
|
|
633
|
+
const orderOrLast = (node) => orderOf(node) ?? Infinity;
|
|
634
|
+
const values = Array.from(new Set(real.map(orderOrLast))).sort((a, b) => a - b);
|
|
635
|
+
ncols = 1;
|
|
636
|
+
nrows = 0;
|
|
637
|
+
for (const value of values) {
|
|
638
|
+
const members = real.filter((node) => orderOrLast(node) === value);
|
|
639
|
+
members.forEach((node, i) => {
|
|
640
|
+
col.set(node.id, i % maxCols);
|
|
641
|
+
row.set(node.id, nrows + Math.floor(i / maxCols));
|
|
642
|
+
});
|
|
643
|
+
ncols = Math.max(ncols, Math.min(members.length, maxCols));
|
|
644
|
+
nrows += Math.ceil(members.length / maxCols);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
/* determine raw node positions with the AntV Dagre layout */
|
|
649
|
+
const rawX = new Map();
|
|
650
|
+
const rawY = new Map();
|
|
651
|
+
if (real.length > 0) {
|
|
652
|
+
const layout = new DagreLayout({
|
|
653
|
+
rankdir: "LR",
|
|
654
|
+
nodesep: config["graph-node-separation"],
|
|
655
|
+
ranksep: config["graph-rank-separation"],
|
|
656
|
+
nodeSize: (node) => node.size
|
|
657
|
+
});
|
|
658
|
+
await layout.execute({
|
|
659
|
+
nodes: real.map((node) => ({ id: node.id, size: [boxW.get(node.id), boxH.get(node.id)] })),
|
|
660
|
+
edges: realEdges.map((edge, i) => ({ id: `e${i}`, source: edge.source, target: edge.target }))
|
|
661
|
+
});
|
|
662
|
+
layout.forEachNode((node) => {
|
|
663
|
+
rawX.set(String(node.id), node.x);
|
|
664
|
+
rawY.set(String(node.id), node.y);
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
/* snap the raw positions onto the discrete column/row grid */
|
|
668
|
+
const snapped = snapToGrid(real, rawX, rawY);
|
|
669
|
+
for (const [id, c] of snapped.col)
|
|
670
|
+
col.set(id, c);
|
|
671
|
+
for (const [id, r] of snapped.row)
|
|
672
|
+
row.set(id, r);
|
|
673
|
+
/* refine the row assignment by pulling every node vertically
|
|
674
|
+
toward the median row of its direct neighbors */
|
|
675
|
+
refineRows(real, realEdges, col, row);
|
|
676
|
+
/* fold the grid columns: constrain the diagram width to at most
|
|
677
|
+
the configured maximum of side-by-side nodes by wrapping excess
|
|
678
|
+
columns into additional row bands below, so wide graphs grow in
|
|
679
|
+
height */
|
|
680
|
+
if (snapped.ncols > maxCols) {
|
|
681
|
+
for (const node of real) {
|
|
682
|
+
const c = col.get(node.id);
|
|
683
|
+
col.set(node.id, c % maxCols);
|
|
684
|
+
row.set(node.id, row.get(node.id) + Math.floor(c / maxCols) * snapped.nrows);
|
|
685
|
+
}
|
|
633
686
|
}
|
|
687
|
+
/* compact the sparsely occupied grid rows into fewer, denser ones */
|
|
688
|
+
ncols = Math.min(snapped.ncols, maxCols);
|
|
689
|
+
nrows = compactRows(real, realEdges, col, row);
|
|
634
690
|
}
|
|
635
|
-
/*
|
|
636
|
-
|
|
637
|
-
let { ncols, nrows } = placeGates(gates, edges, col, row, Math.min(gridCols, maxCols), compactRows(real, realEdges, col, row));
|
|
691
|
+
/* place the gate nodes into their own boundary columns */
|
|
692
|
+
({ ncols, nrows } = placeGates(gates, edges, col, row, ncols, nrows));
|
|
638
693
|
/* determine the attachment sides of every edge */
|
|
639
694
|
const sides = attachSides(edges, col, level);
|
|
640
695
|
/* plan the coarse channel/gutter route of every edge (a west side
|
|
641
696
|
attachment in the first column needs a channel left of it, so
|
|
642
697
|
the whole grid is shifted one column to the right then) */
|
|
643
|
-
let routes = planRoutes(edges, col, row, sides);
|
|
698
|
+
let routes = planRoutes(edges, col, row, sides, gates);
|
|
644
699
|
if (routes.plans.some((plan) => plan.chans.some((c) => c < 0))) {
|
|
645
700
|
for (const node of nodes)
|
|
646
701
|
col.set(node.id, col.get(node.id) + 1);
|
|
647
702
|
ncols++;
|
|
648
|
-
routes = planRoutes(edges, col, row, sides);
|
|
703
|
+
routes = planRoutes(edges, col, row, sides, gates);
|
|
649
704
|
}
|
|
650
705
|
const { plans, chanCnt, gutCnt, chanLbl, gutLbl, gutLoop } = routes;
|
|
651
706
|
/* grow the boxes by their edge attachment needs */
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Distributed under MIT license <https://spdx.org/licenses/MIT.html>
|
|
5
5
|
*/
|
|
6
|
-
import { measureNodes } from "./gradia-api-render-node.js";
|
|
6
|
+
import { measureNodes, orderOf } from "./gradia-api-render-node.js";
|
|
7
7
|
/* lay out an edge-less graph model as a compact grid of tiles (for a
|
|
8
8
|
containment level: with the container placeholders at their fixed
|
|
9
|
-
sizes, which take part in the tile size unification)
|
|
9
|
+
sizes, which take part in the tile size unification), the tiles in
|
|
10
|
+
declaration order, or in the order of their "order" attributes
|
|
11
|
+
(the tiles without one trailing) */
|
|
10
12
|
export const render = async (graph, config, level = {}) => {
|
|
11
|
-
const nodes = Array.from(graph.nodes.values())
|
|
13
|
+
const nodes = Array.from(graph.nodes.values())
|
|
14
|
+
.sort((a, b) => (orderOf(a) ?? Infinity) - (orderOf(b) ?? Infinity) || 0);
|
|
12
15
|
/* resolve the configurable rendering geometry */
|
|
13
16
|
const margin = config["size-canvas-margin"];
|
|
14
17
|
const gapH = config["grid-gap-horizontal"];
|