@yalehwang/plantuml-js 0.2.0 → 0.3.0

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 (64) hide show
  1. package/README.md +20 -0
  2. package/dist/core.d.ts +102 -9
  3. package/dist/core.js +91 -51
  4. package/dist/foundation/layers.d.ts +25 -0
  5. package/dist/foundation/layers.js +101 -0
  6. package/dist/foundation/layout.d.ts +116 -0
  7. package/dist/foundation/layout.js +298 -0
  8. package/dist/{model.d.ts → foundation/model.d.ts} +20 -0
  9. package/dist/foundation/relation-semantics.d.ts +49 -0
  10. package/dist/foundation/relation-semantics.js +112 -0
  11. package/dist/{text-measure.d.ts → foundation/text-measure.d.ts} +2 -0
  12. package/dist/{text-measure.js → foundation/text-measure.js} +20 -1
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.js +1 -1
  15. package/dist/layout/compound.d.ts +59 -0
  16. package/dist/layout/compound.js +1178 -0
  17. package/dist/layout/elk-graph.d.ts +36 -0
  18. package/dist/layout/elk-graph.js +220 -0
  19. package/dist/layout/elk-result.d.ts +18 -0
  20. package/dist/layout/elk-result.js +127 -0
  21. package/dist/layout/elk.d.ts +63 -33
  22. package/dist/layout/elk.js +215 -321
  23. package/dist/layout/fanout-trunk.d.ts +18 -0
  24. package/dist/layout/fanout-trunk.js +380 -0
  25. package/dist/layout/grid-router.d.ts +69 -0
  26. package/dist/layout/grid-router.js +615 -0
  27. package/dist/layout/lane-spread.d.ts +11 -0
  28. package/dist/layout/lane-spread.js +302 -0
  29. package/dist/layout/layout-metrics.d.ts +183 -0
  30. package/dist/layout/layout-metrics.js +766 -0
  31. package/dist/layout/package-endpoints.d.ts +10 -0
  32. package/dist/layout/package-endpoints.js +54 -0
  33. package/dist/layout/placement-strategy.d.ts +98 -0
  34. package/dist/layout/placement-strategy.js +26 -0
  35. package/dist/layout/strategy.d.ts +27 -0
  36. package/dist/layout/strategy.js +19 -0
  37. package/dist/layout/trunk-lanes.d.ts +29 -0
  38. package/dist/layout/trunk-lanes.js +308 -0
  39. package/dist/layout/viz.d.ts +3 -3
  40. package/dist/layout/viz.js +13 -17
  41. package/dist/parser.d.ts +27 -1
  42. package/dist/parser.js +111 -5
  43. package/dist/render/labels.d.ts +44 -0
  44. package/dist/render/labels.js +231 -0
  45. package/dist/render/legend.d.ts +15 -0
  46. package/dist/render/legend.js +42 -0
  47. package/dist/render/svg.d.ts +60 -3
  48. package/dist/render/svg.js +326 -98
  49. package/dist/render/theme.d.ts +109 -0
  50. package/dist/render/theme.js +99 -0
  51. package/dist/serialize.d.ts +32 -0
  52. package/dist/serialize.js +218 -0
  53. package/dist/transform.d.ts +55 -1
  54. package/dist/transform.js +277 -29
  55. package/package.json +9 -3
  56. package/dist/layout-metrics.d.ts +0 -42
  57. package/dist/layout-metrics.js +0 -241
  58. package/dist/layout.d.ts +0 -75
  59. package/dist/layout.js +0 -51
  60. /package/dist/{edge-router.d.ts → foundation/edge-router.d.ts} +0 -0
  61. /package/dist/{edge-router.js → foundation/edge-router.js} +0 -0
  62. /package/dist/{model.js → foundation/model.js} +0 -0
  63. /package/dist/{render-dot.d.ts → foundation/render-dot.d.ts} +0 -0
  64. /package/dist/{render-dot.js → foundation/render-dot.js} +0 -0
package/dist/transform.js CHANGED
@@ -9,6 +9,11 @@
9
9
  * input and produces the same output for the same input, so a transformed
10
10
  * render stays byte-for-byte reproducible.
11
11
  */
12
+ import { relationSemantics } from './foundation/relation-semantics.js';
13
+ /** Entity count above which inter-package edges aggregate into corridors by default. */
14
+ export const INTER_PACKAGE_AGGREGATE_ENTITY_THRESHOLD = 150;
15
+ /** Stub count above which a single-package view groups stubs by name prefix. */
16
+ export const DEFAULT_STUB_GROUP_THRESHOLD = 8;
12
17
  const ROOT = '__root__';
13
18
  function cloneModel(model) {
14
19
  return {
@@ -24,23 +29,89 @@ function cloneModel(model) {
24
29
  * summarization, then cycle marking on the final edge set.
25
30
  */
26
31
  export function transformModel(model, options = {}) {
27
- const { aggregate = true, transitiveReduction = false, hubThreshold, cycles = false } = options;
32
+ return transformModelDetailed(model, options).model;
33
+ }
34
+ /** [[transformModel]] plus the number of edges the transitive reduction hid. */
35
+ export function transformModelDetailed(model, options = {}) {
36
+ const { aggregate = true, transitiveReduction = false, hubThreshold, cycles = false, interPackage = 'raw' } = options;
28
37
  let out = cloneModel(model);
38
+ out = summarizeExternals(out, EXTERNAL_SUMMARY_MIN_IN_EDGES);
29
39
  if (aggregate)
30
40
  out = aggregateRelations(out);
31
- if (transitiveReduction)
41
+ let hiddenTransitive = 0;
42
+ if (transitiveReduction) {
43
+ const before = out.relations.length;
32
44
  out = transitiveReduce(out);
45
+ hiddenTransitive = before - out.relations.length;
46
+ }
33
47
  if (hubThreshold !== undefined)
34
48
  out = summarizeHubs(out, hubThreshold);
49
+ if (interPackage === 'aggregate')
50
+ out = aggregateInterPackage(out);
35
51
  if (cycles)
36
52
  out = markCycles(out);
37
- return out;
53
+ return { model: groupExternals(out), hiddenTransitive };
54
+ }
55
+ /** Edge count ÷ node count above which `edgeReduction: 'auto'` reduces a pure dependency graph. */
56
+ export const AUTO_REDUCTION_EDGE_NODE_RATIO = 3;
57
+ /**
58
+ * Whether `edgeReduction: 'auto'` applies: every relation is bundlable
59
+ * (dependency/association) and there are more than
60
+ * [[AUTO_REDUCTION_EDGE_NODE_RATIO]] times as many edges as nodes.
61
+ */
62
+ export function shouldAutoReduce(model) {
63
+ if (model.relations.length === 0)
64
+ return false;
65
+ if (!model.relations.every((r) => relationSemantics(r.type).isBundlable()))
66
+ return false;
67
+ const nodes = model.entities.length;
68
+ const distinct = new Set(model.relations.map((r) => `${r.source}\u0000${r.target}`)).size;
69
+ return distinct > AUTO_REDUCTION_EDGE_NODE_RATIO * nodes;
70
+ }
71
+ /** Incoming edges at which an external type's edges collapse into a count badge. */
72
+ export const EXTERNAL_SUMMARY_MIN_IN_EDGES = 3;
73
+ /** Id of the synthetic package that gathers implicit/external types. */
74
+ export const EXTERNAL_PACKAGE_ID = '__external__';
75
+ /**
76
+ * Implicit/external types (`Error`, `EventEmitter`, …) receive many parallel
77
+ * edges that would fly across the whole canvas. Once one has `minEdges` or more
78
+ * incoming relations they are dropped and replaced by a `usageCount` badge that
79
+ * carries the original incoming-edge count.
80
+ */
81
+ export function summarizeExternals(model, minEdges) {
82
+ const incoming = new Map();
83
+ for (const r of model.relations) {
84
+ const e = model.entities.find((x) => x.id === r.target);
85
+ if (e?.external)
86
+ incoming.set(r.target, (incoming.get(r.target) ?? 0) + (r.weight ?? 1));
87
+ }
88
+ const summarized = new Set([...incoming].filter(([, n]) => n >= minEdges).map(([id]) => id));
89
+ if (summarized.size === 0)
90
+ return model;
91
+ return {
92
+ ...model,
93
+ entities: model.entities.map((e) => summarized.has(e.id) ? { ...e, usageCount: incoming.get(e.id) } : e),
94
+ relations: model.relations.filter((r) => !summarized.has(r.target)),
95
+ };
96
+ }
97
+ /** Gather top-level external entities into one synthetic External package. */
98
+ export function groupExternals(model) {
99
+ const externals = model.entities.filter((e) => e.external && e.packageId === undefined);
100
+ if (externals.length < 2 || model.packages.some((p) => p.id === EXTERNAL_PACKAGE_ID)) {
101
+ return model;
102
+ }
103
+ const ids = new Set(externals.map((e) => e.id));
104
+ return {
105
+ ...model,
106
+ entities: model.entities.map((e) => (ids.has(e.id) ? { ...e, packageId: EXTERNAL_PACKAGE_ID } : e)),
107
+ packages: [...model.packages, { id: EXTERNAL_PACKAGE_ID, name: 'External' }],
108
+ };
38
109
  }
39
110
  /** Merge relations sharing (source, target) into one edge with `weight`. */
40
111
  export function aggregateRelations(model) {
41
112
  const byKey = new Map();
42
113
  for (const relation of model.relations) {
43
- const key = `${relation.source}\u0000${relation.target}`;
114
+ const key = `${relation.source}\u0000${relation.target}\u0000${relation.sourcePackageId ?? ''}\u0000${relation.targetPackageId ?? ''}`;
44
115
  const existing = byKey.get(key);
45
116
  if (existing) {
46
117
  existing.weight = (existing.weight ?? 1) + 1;
@@ -51,6 +122,89 @@ export function aggregateRelations(model) {
51
122
  }
52
123
  return { ...model, relations: [...byKey.values()] };
53
124
  }
125
+ /**
126
+ * Fold every relation between two different packages into one corridor edge
127
+ * per ordered package pair: `weight` = number of originals, `descriptions`
128
+ * keeps each original, and the endpoints are the entities carrying most of the
129
+ * traffic with `sourcePackageId`/`targetPackageId` set so the layout clips the
130
+ * edge to the frame boundaries. Intra-package edges and edges touching
131
+ * package-less entities, or between nested packages, are left as they are.
132
+ */
133
+ export function aggregateInterPackage(model) {
134
+ const pkgOf = new Map(model.entities.map((e) => [e.id, e.packageId]));
135
+ const parent = new Map(model.packages.map((p) => [p.id, p.parentId]));
136
+ const nested = (a, b) => {
137
+ for (const [x, y] of [[a, b], [b, a]]) {
138
+ for (let p = parent.get(x); p !== undefined; p = parent.get(p))
139
+ if (p === y)
140
+ return true;
141
+ }
142
+ return false;
143
+ };
144
+ const groups = new Map();
145
+ const relations = [];
146
+ for (const rel of model.relations) {
147
+ const a = pkgOf.get(rel.source);
148
+ const b = pkgOf.get(rel.target);
149
+ if (!a || !b || a === b || nested(a, b) || rel.sourcePackageId || rel.targetPackageId) {
150
+ relations.push(rel);
151
+ continue;
152
+ }
153
+ const key = `${a}\u0000${b}`;
154
+ let g = groups.get(key);
155
+ if (!g) {
156
+ g = { src: a, dst: b, items: [], srcCount: new Map(), dstCount: new Map(), at: relations.length };
157
+ groups.set(key, g);
158
+ relations.push(g);
159
+ }
160
+ g.items.push(rel);
161
+ g.srcCount.set(rel.source, (g.srcCount.get(rel.source) ?? 0) + 1);
162
+ g.dstCount.set(rel.target, (g.dstCount.get(rel.target) ?? 0) + 1);
163
+ }
164
+ const top = (m) => {
165
+ let best = '';
166
+ let n = -1;
167
+ for (const [k, v] of m)
168
+ if (v > n) {
169
+ best = k;
170
+ n = v;
171
+ }
172
+ return best;
173
+ };
174
+ const out = relations.map((r) => {
175
+ if (!('items' in r))
176
+ return r;
177
+ const types = new Set(r.items.map((i) => i.type));
178
+ const only = r.items.length === 1 ? r.items[0].type : [...types][0];
179
+ const type = types.size === 1 && relationSemantics(only).isBundlable() ? only : 'dependency';
180
+ return {
181
+ source: top(r.srcCount),
182
+ target: top(r.dstCount),
183
+ type,
184
+ weight: r.items.reduce((sum, i) => sum + (i.weight ?? 1), 0),
185
+ feedback: r.items.every((i) => i.feedback) || undefined,
186
+ sourcePackageId: r.src,
187
+ targetPackageId: r.dst,
188
+ descriptions: r.items.map((i) => `${i.source} ${ARROWS[i.type]} ${i.target}${i.label ? ` : ${i.label}` : ''}`),
189
+ };
190
+ });
191
+ return { ...model, relations: out.map(dropUndefined) };
192
+ }
193
+ const ARROWS = {
194
+ inheritance: '--|>',
195
+ implementation: '..|>',
196
+ dependency: '..>',
197
+ association: '-->',
198
+ aggregation: 'o--',
199
+ composition: '*--',
200
+ };
201
+ function dropUndefined(r) {
202
+ const copy = { ...r };
203
+ for (const k of Object.keys(copy))
204
+ if (copy[k] === undefined)
205
+ delete copy[k];
206
+ return copy;
207
+ }
54
208
  /**
55
209
  * Remove a relation A→B when A still reaches B through another path (an edge
56
210
  * derivable by transitivity). Only the redundant direct edge is dropped; the
@@ -58,37 +212,52 @@ export function aggregateRelations(model) {
58
212
  */
59
213
  export function transitiveReduce(model) {
60
214
  const targets = new Map();
61
- for (const relation of model.relations) {
62
- const list = targets.get(relation.source);
63
- if (list)
64
- list.push(relation.target);
65
- else
66
- targets.set(relation.source, [relation.target]);
67
- }
68
- const reachable = (start, goal, skipTarget) => {
69
- const seen = new Set();
70
- const queue = [start];
71
- while (queue.length > 0) {
72
- const u = queue.shift();
73
- for (const v of targets.get(u) ?? []) {
74
- if (u === start && v === skipTarget)
75
- continue; // ignore the direct edge under test
215
+ const addEdge = (u, v) => {
216
+ const m = targets.get(u) ?? new Map();
217
+ m.set(v, (m.get(v) ?? 0) + 1);
218
+ targets.set(u, m);
219
+ };
220
+ for (const relation of model.relations)
221
+ addEdge(relation.source, relation.target);
222
+ const reachable = (start, goal) => {
223
+ const seen = new Set([start]);
224
+ const stack = [start];
225
+ while (stack.length > 0) {
226
+ const u = stack.pop();
227
+ for (const v of targets.get(u)?.keys() ?? []) {
76
228
  if (v === goal)
77
229
  return true;
78
230
  if (!seen.has(v)) {
79
231
  seen.add(v);
80
- queue.push(v);
232
+ stack.push(v);
81
233
  }
82
234
  }
83
235
  }
84
236
  return false;
85
237
  };
86
- const relations = model.relations.filter((relation) => {
87
- // A self-loop is its own shortest witness; keep it. A single-hop edge with
88
- // no alternate path is necessary; keep it.
89
- return !reachable(relation.source, relation.target, relation.target);
238
+ // Greedy in authored order: an edge is dropped when its end points stay
239
+ // connected through the edges still present, so reachability is preserved
240
+ // exactly (also inside cycles, where dropping every "redundant" edge at once
241
+ // would disconnect the cycle). A self-loop is its own witness and is kept.
242
+ const removed = new Set();
243
+ model.relations.forEach((relation, i) => {
244
+ if (relation.source === relation.target)
245
+ return;
246
+ const out = targets.get(relation.source);
247
+ const count = out.get(relation.target);
248
+ if (count > 1) {
249
+ // A parallel duplicate of the same pair is always redundant.
250
+ out.set(relation.target, count - 1);
251
+ removed.add(i);
252
+ return;
253
+ }
254
+ out.delete(relation.target);
255
+ if (reachable(relation.source, relation.target))
256
+ removed.add(i);
257
+ else
258
+ out.set(relation.target, 1);
90
259
  });
91
- return { ...model, relations };
260
+ return { ...model, relations: model.relations.filter((_, i) => !removed.has(i)) };
92
261
  }
93
262
  /**
94
263
  * Nodes with an in-degree above `threshold` have their incoming edges dropped
@@ -170,7 +339,7 @@ export function applyView(model, view) {
170
339
  }
171
340
  return overviewView(model);
172
341
  case 'package':
173
- return packageView(model, view.id);
342
+ return packageView(model, view.id, view.stubGroupThreshold ?? DEFAULT_STUB_GROUP_THRESHOLD);
174
343
  case 'neighborhood':
175
344
  return neighborhoodView(model, view.entity, view.hops);
176
345
  }
@@ -276,7 +445,7 @@ export function groupByPathPrefix(model, depth) {
276
445
  * merged by aggregation). Entities and relations entirely outside the package
277
446
  * are dropped.
278
447
  */
279
- function packageView(model, id) {
448
+ function packageView(model, id, stubGroupThreshold) {
280
449
  const pkgById = new Map(model.packages.map((p) => [p.id, p]));
281
450
  const selected = pkgById.get(id);
282
451
  if (!selected) {
@@ -332,13 +501,92 @@ function packageView(model, id) {
332
501
  const stub = stubFor(externalKey);
333
502
  relations.push(sourceIn ? { ...relation, target: stub.id } : { ...relation, source: stub.id });
334
503
  }
504
+ const grouped = groupStubs({ entities, relations }, stubGroupThreshold);
335
505
  return {
336
506
  diagramType: 'class',
337
- entities,
338
- relations,
507
+ entities: grouped.entities,
508
+ relations: grouped.relations,
339
509
  packages: [{ ...selected, parentId: undefined }],
340
510
  };
341
511
  }
512
+ /** Name with its last word dropped (`Src Cli Utils` → `Src Cli`); a one-word name stays as is. */
513
+ function namePrefix(name) {
514
+ const words = name.trim().split(/\s+/);
515
+ return words.length > 1 ? words.slice(0, -1).join(' ') : name;
516
+ }
517
+ /**
518
+ * Past `threshold` stubs, merge stubs sharing a name prefix into one group stub
519
+ * (`Src Cli … (×6)`), dropping trailing words until the group count fits (or nothing shortens any
520
+ * more). The edges to the members of a group collapse onto
521
+ * the group stub per (entity, direction): `weight` counts the originals and
522
+ * `descriptions` keeps each one for the edge's `<title>`.
523
+ */
524
+ export function groupStubs(model, threshold) {
525
+ const stubs = model.entities.filter((e) => e.stub);
526
+ if (stubs.length <= threshold)
527
+ return model;
528
+ let keyOf = new Map(stubs.map((e) => [e.id, e.name]));
529
+ for (;;) {
530
+ if (new Set(keyOf.values()).size <= threshold)
531
+ break;
532
+ const next = new Map([...keyOf].map(([id, k]) => [id, namePrefix(k)]));
533
+ if ([...next].every(([id, k]) => k === keyOf.get(id)))
534
+ break;
535
+ keyOf = next;
536
+ }
537
+ const members = new Map();
538
+ for (const stub of stubs) {
539
+ const key = keyOf.get(stub.id);
540
+ const list = members.get(key);
541
+ if (list)
542
+ list.push(stub);
543
+ else
544
+ members.set(key, [stub]);
545
+ }
546
+ const groupId = new Map();
547
+ const entities = [];
548
+ for (const e of model.entities) {
549
+ if (!e.stub) {
550
+ entities.push(e);
551
+ continue;
552
+ }
553
+ const key = keyOf.get(e.id);
554
+ const list = members.get(key);
555
+ if (list.length === 1) {
556
+ groupId.set(e.id, e.id);
557
+ entities.push(e);
558
+ }
559
+ else if (list[0] === e) {
560
+ const id = `stub::group::${key}`;
561
+ for (const m of list)
562
+ groupId.set(m.id, id);
563
+ entities.push({ ...e, id, name: `${list.length} other ${key} packages`, groupMembers: list.map((m) => m.name) });
564
+ }
565
+ }
566
+ const merged = new Map();
567
+ const out = [];
568
+ const label = (r) => `${r.source} ${r.type} ${r.target}`;
569
+ for (const r of model.relations) {
570
+ const source = groupId.get(r.source) ?? r.source;
571
+ const target = groupId.get(r.target) ?? r.target;
572
+ if (source === r.source && target === r.target) {
573
+ out.push(r);
574
+ continue;
575
+ }
576
+ const key = `${source}\u0000${target}`;
577
+ const existing = merged.get(key);
578
+ if (existing) {
579
+ existing.weight = (existing.weight ?? 1) + (r.weight ?? 1);
580
+ existing.descriptions.push(...(r.descriptions ?? [label(r)]));
581
+ }
582
+ else {
583
+ const rel = { ...r, source, target, weight: r.weight ?? 1, descriptions: [...(r.descriptions ?? [label(r)])] };
584
+ merged.set(key, rel);
585
+ out.push(rel);
586
+ }
587
+ }
588
+ return { entities, relations: out };
589
+ }
342
590
  /**
343
591
  * Expand one entity plus every entity within `hops` undirected hops, together
344
592
  * with the package frames that contain the kept entities.
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@yalehwang/plantuml-js",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Pure-TypeScript PlantUML class diagram renderer (MIT)",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/yaleh/plantuml-js.git"
10
+ },
7
11
  "main": "./dist/index.js",
8
12
  "types": "./dist/index.d.ts",
9
13
  "exports": {
@@ -30,11 +34,13 @@
30
34
  "node": ">=18"
31
35
  },
32
36
  "scripts": {
33
- "build": "tsc -p tsconfig.json",
37
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
38
+ "build": "npm run clean && tsc -p tsconfig.json",
34
39
  "typecheck": "tsc -p tsconfig.test.json",
35
40
  "test": "vitest run",
36
41
  "demo:build": "node scripts/build-demo.mjs",
37
- "prepublishOnly": "npm run typecheck && npm run build"
42
+ "release": "node scripts/release.mjs",
43
+ "prepublishOnly": "npm run typecheck && npm run build && node scripts/check-dist-clean.mjs"
38
44
  },
39
45
  "dependencies": {
40
46
  "@viz-js/viz": "^3.31.0",
@@ -1,42 +0,0 @@
1
- /**
2
- * Layout quality metrics. Pure functions from (model, layout) to numbers, so a
3
- * layout strategy change can be judged by a diff in figures rather than by eye.
4
- *
5
- * Edge polylines are taken at face value: for the viz engine they are bezier
6
- * control points, which is a coarse but consistent approximation. Every metric
7
- * is deterministic — the same (model, layout) always yields the same numbers —
8
- * so a run can be committed as `rendered/metrics.baseline.json` and diffed
9
- * against a later run to prove an improvement or a regression.
10
- */
11
- import { PlantUmlModel } from './model.js';
12
- import { LayoutResult } from './layout.js';
13
- export interface LayoutMetrics {
14
- /** Proper crossings between segments of two different edges. */
15
- edgeCrossings: number;
16
- /** Edge segments passing through the interior of an unrelated node. */
17
- edgeNodeIntersections: number;
18
- /** Summed polyline length of every edge, in canvas pixels. */
19
- totalEdgeLength: number;
20
- /** Sum of node areas divided by canvas area (0..1). */
21
- areaUtilization: number;
22
- /** Canvas width / height. */
23
- aspectRatio: number;
24
- /** Entity text lines wider than the box they are drawn in. */
25
- textOverflowCount: number;
26
- /** Package label tabs that overlap content or are narrower than their text. */
27
- labelOverlapCount: number;
28
- /**
29
- * Summed length of collinear, overlapping segments from *different* edges —
30
- * the orthogonal router's "parallel-line wall" is invisible to
31
- * [[edgeCrossings]] (segments that merely overlap never *cross*), so this
32
- * counts that hidden overlap in canvas pixels.
33
- */
34
- overlappingEdgeLength: number;
35
- /**
36
- * Incoming arrowheads stacked beyond a threshold on the same side of the
37
- * same node — the "everything points at this hub" pile-up that
38
- * [[edgeCrossings]] cannot see either.
39
- */
40
- hubArrowStack: number;
41
- }
42
- export declare function computeLayoutMetrics(model: PlantUmlModel, layout: LayoutResult): LayoutMetrics;