@yalehwang/plantuml-js 0.2.2 → 0.3.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 (65) hide show
  1. package/README.md +20 -0
  2. package/dist/core.d.ts +106 -13
  3. package/dist/core.js +92 -52
  4. package/dist/foundation/labels.d.ts +44 -0
  5. package/dist/foundation/labels.js +231 -0
  6. package/dist/foundation/layers.d.ts +25 -0
  7. package/dist/foundation/layers.js +101 -0
  8. package/dist/foundation/layout.d.ts +116 -0
  9. package/dist/foundation/layout.js +298 -0
  10. package/dist/{model.d.ts → foundation/model.d.ts} +20 -0
  11. package/dist/foundation/relation-semantics.d.ts +49 -0
  12. package/dist/foundation/relation-semantics.js +112 -0
  13. package/dist/{text-measure.d.ts → foundation/text-measure.d.ts} +2 -0
  14. package/dist/{text-measure.js → foundation/text-measure.js} +20 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.js +1 -1
  17. package/dist/layout/compound.d.ts +74 -0
  18. package/dist/layout/compound.js +1204 -0
  19. package/dist/layout/elk-graph.d.ts +36 -0
  20. package/dist/layout/elk-graph.js +220 -0
  21. package/dist/layout/elk-result.d.ts +18 -0
  22. package/dist/layout/elk-result.js +127 -0
  23. package/dist/layout/elk.d.ts +80 -33
  24. package/dist/layout/elk.js +229 -320
  25. package/dist/layout/fanout-trunk.d.ts +18 -0
  26. package/dist/layout/fanout-trunk.js +380 -0
  27. package/dist/layout/grid-router.d.ts +69 -0
  28. package/dist/layout/grid-router.js +615 -0
  29. package/dist/layout/lane-spread.d.ts +11 -0
  30. package/dist/layout/lane-spread.js +302 -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/metrics/layout-metrics.d.ts +183 -0
  42. package/dist/metrics/layout-metrics.js +766 -0
  43. package/dist/parse/parser.d.ts +43 -0
  44. package/dist/{parser.js → parse/parser.js} +111 -5
  45. package/dist/parse/serialize.d.ts +32 -0
  46. package/dist/parse/serialize.js +218 -0
  47. package/dist/render/legend.d.ts +15 -0
  48. package/dist/render/legend.js +42 -0
  49. package/dist/render/svg.d.ts +60 -3
  50. package/dist/render/svg.js +326 -98
  51. package/dist/render/theme.d.ts +109 -0
  52. package/dist/render/theme.js +99 -0
  53. package/dist/{transform.d.ts → transform/transform.d.ts} +55 -1
  54. package/dist/{transform.js → transform/transform.js} +277 -29
  55. package/package.json +4 -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/parser.d.ts +0 -17
  61. /package/dist/{edge-router.d.ts → foundation/edge-router.d.ts} +0 -0
  62. /package/dist/{edge-router.js → foundation/edge-router.js} +0 -0
  63. /package/dist/{model.js → foundation/model.js} +0 -0
  64. /package/dist/{render-dot.d.ts → layout/render-dot.d.ts} +0 -0
  65. /package/dist/{render-dot.js → layout/render-dot.js} +0 -0
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Edge-label and hub-badge geometry shared by the SVG renderer and the layout
3
+ * metrics, so a drawn label and a measured label are the same rectangle.
4
+ *
5
+ * Weight labels (`×N`) are placed on the edge itself over a white backing box.
6
+ * Candidate anchors run along the edge's segments, longest first; the first
7
+ * candidate that collides with no node and no previously placed label wins, so
8
+ * labels of neighbouring edges do not pile onto each other.
9
+ */
10
+ import { METRICS, displayName } from './layout.js';
11
+ import { nodeRect } from './edge-router.js';
12
+ import { measureTextWidth } from './text-measure.js';
13
+ export const EDGE_LABEL_FONT_SIZE = 11;
14
+ const EDGE_LABEL_PAD_X = 3;
15
+ const EDGE_LABEL_HEIGHT = 14;
16
+ /** Fractions along a segment tried, in order, for a label anchor. */
17
+ const ANCHOR_FRACTIONS = [0.5, 0.3, 0.7, 0.15, 0.85];
18
+ export const HUB_BADGE_RADIUS = 9;
19
+ /** Hub badge circle: centred on the node's top-right corner, half outside the box. */
20
+ export function hubBadgeCircle(node) {
21
+ return { cx: node.x + node.width, cy: node.y, r: HUB_BADGE_RADIUS };
22
+ }
23
+ export function hubBadgeRect(node) {
24
+ const { cx, cy, r } = hubBadgeCircle(node);
25
+ return { x: cx - r, y: cy - r, width: 2 * r, height: 2 * r };
26
+ }
27
+ /** Bounding boxes of the text lines drawn inside a node (title + members). */
28
+ export function entityTextRects(entity, node) {
29
+ const rects = [];
30
+ const title = measureTextWidth(displayName(entity), METRICS.fontSize);
31
+ rects.push({
32
+ x: node.x + (node.width - title) / 2,
33
+ y: node.y + METRICS.paddingY,
34
+ width: title,
35
+ height: METRICS.lineHeight,
36
+ });
37
+ entity.members.forEach((member, i) => {
38
+ rects.push({
39
+ x: node.x + METRICS.paddingX,
40
+ y: node.y + METRICS.paddingY + METRICS.lineHeight * (i + 1),
41
+ width: measureTextWidth(member.raw, METRICS.fontSize),
42
+ height: METRICS.lineHeight,
43
+ });
44
+ });
45
+ return rects;
46
+ }
47
+ export function rectsIntersect(a, b) {
48
+ return (a.x < b.x + b.width &&
49
+ b.x < a.x + a.width &&
50
+ a.y < b.y + b.height &&
51
+ b.y < a.y + a.height);
52
+ }
53
+ /** Same-text labels sharing a source (or target) and the other end's package collapse to one. */
54
+ export const LABEL_DEDUPE_MIN = 3;
55
+ /** A collapsed label standing for at least this many edges carries a `×N` count. */
56
+ export const LABEL_COUNT_BADGE_MIN = 4;
57
+ const PLAIN_LABEL_FONT_SIZE = 12;
58
+ const PLAIN_LABEL_HEIGHT = 14;
59
+ /** Gap between a beside-the-line label and the line. */
60
+ const LABEL_SIDE_GAP = 2;
61
+ /** True when the segment ab touches the rectangle (Liang–Barsky clip). */
62
+ export function segmentIntersectsRect(a, b, r) {
63
+ let t0 = 0;
64
+ let t1 = 1;
65
+ const dx = b.x - a.x;
66
+ const dy = b.y - a.y;
67
+ const clip = (p, q) => {
68
+ if (p === 0)
69
+ return q >= 0;
70
+ const t = q / p;
71
+ if (p < 0) {
72
+ if (t > t1)
73
+ return false;
74
+ if (t > t0)
75
+ t0 = t;
76
+ }
77
+ else {
78
+ if (t < t0)
79
+ return false;
80
+ if (t < t1)
81
+ t1 = t;
82
+ }
83
+ return true;
84
+ };
85
+ return (clip(-dx, a.x - r.x) &&
86
+ clip(dx, r.x + r.width - a.x) &&
87
+ clip(-dy, a.y - r.y) &&
88
+ clip(dy, r.y + r.height - a.y));
89
+ }
90
+ /** Edge ids (`rel-<i>`) of every segment of every edge other than `ownId` that crosses `rect`. */
91
+ export function rectHitsOtherEdge(rect, edges, ownId) {
92
+ for (const e of edges) {
93
+ if (e.id === ownId)
94
+ continue;
95
+ for (let i = 0; i + 1 < e.points.length; i++) {
96
+ if (segmentIntersectsRect(e.points[i], e.points[i + 1], rect))
97
+ return true;
98
+ }
99
+ }
100
+ return false;
101
+ }
102
+ /**
103
+ * Plain labels that repeat along one bundle (same text, same source — or same
104
+ * target — and the same package at the other end) show on a single
105
+ * representative edge; the rest keep a `<title>` tooltip. Returns the
106
+ * representative index → group size, and the set of suppressed indexes.
107
+ */
108
+ function dedupePlainLabels(model) {
109
+ const pkgOf = new Map(model.entities.map((e) => [e.id, e.packageId ?? e.id]));
110
+ const reps = new Map();
111
+ const suppressed = new Set();
112
+ for (const end of ['source', 'target']) {
113
+ const other = end === 'source' ? 'target' : 'source';
114
+ const groups = new Map();
115
+ model.relations.forEach((rel, i) => {
116
+ if (!rel.label || suppressed.has(i))
117
+ return;
118
+ const key = `${rel[end]}\u0000${rel.label}\u0000${pkgOf.get(rel[other]) ?? rel[other]}`;
119
+ const list = groups.get(key);
120
+ if (list)
121
+ list.push(i);
122
+ else
123
+ groups.set(key, [i]);
124
+ });
125
+ for (const g of groups.values()) {
126
+ if (g.length < LABEL_DEDUPE_MIN)
127
+ continue;
128
+ const total = g.reduce((n, i) => n + (reps.get(i) ?? 1), 0);
129
+ reps.set(g[0], total);
130
+ for (const i of g.slice(1)) {
131
+ suppressed.add(i);
132
+ reps.delete(i);
133
+ }
134
+ }
135
+ }
136
+ return { reps, suppressed };
137
+ }
138
+ /**
139
+ * Candidate rectangles for a plain label: beside each segment (longest first),
140
+ * offset to either side so the text never sits on its own line.
141
+ */
142
+ function plainLabelCandidates(points, width, height) {
143
+ const segs = [];
144
+ for (let i = 0; i + 1 < points.length; i++) {
145
+ const len = Math.hypot(points[i + 1].x - points[i].x, points[i + 1].y - points[i].y);
146
+ if (len > 0)
147
+ segs.push({ a: points[i], b: points[i + 1], len });
148
+ }
149
+ segs.sort((p, q) => q.len - p.len);
150
+ const rects = [];
151
+ for (const { a, b } of segs) {
152
+ const horizontal = Math.abs(b.x - a.x) >= Math.abs(b.y - a.y);
153
+ for (const t of ANCHOR_FRACTIONS) {
154
+ const cx = a.x + (b.x - a.x) * t;
155
+ const cy = a.y + (b.y - a.y) * t;
156
+ for (const side of [-1, 1]) {
157
+ rects.push(horizontal
158
+ ? { x: cx - width / 2, y: side < 0 ? cy - LABEL_SIDE_GAP - height : cy + LABEL_SIDE_GAP, width, height }
159
+ : { x: side < 0 ? cx - LABEL_SIDE_GAP - width : cx + LABEL_SIDE_GAP, y: cy - height / 2, width, height });
160
+ }
161
+ }
162
+ }
163
+ return rects;
164
+ }
165
+ /** Place every weight label and collect every plain edge label, deterministically. */
166
+ export function placeEdgeLabels(model, layout) {
167
+ const edges = layout.edges ?? [];
168
+ const edgesById = new Map(edges.map((e) => [e.id, e]));
169
+ const nodeRects = layout.nodes.map(nodeRect);
170
+ const out = [];
171
+ const placed = [];
172
+ const { reps, suppressed } = dedupePlainLabels(model);
173
+ // A plain label sits beside its own edge, clear of every node, every other
174
+ // edge and every earlier label. A label that fits nowhere is left out (the
175
+ // renderer keeps it as a `<title>` tooltip).
176
+ model.relations.forEach((rel, edge) => {
177
+ const points = edgesById.get(`rel-${edge}`)?.points;
178
+ if (!points || points.length < 2 || !rel.label || suppressed.has(edge))
179
+ return;
180
+ const count = reps.get(edge) ?? 1;
181
+ const text = count >= LABEL_COUNT_BADGE_MIN ? `${rel.label} ×${count}` : rel.label;
182
+ const width = measureTextWidth(text, PLAIN_LABEL_FONT_SIZE);
183
+ const free = plainLabelCandidates(points, width, PLAIN_LABEL_HEIGHT).find((rect) => !nodeRects.some((n) => rectsIntersect(rect, n)) &&
184
+ !placed.some((p) => rectsIntersect(rect, p)) &&
185
+ !rectHitsOtherEdge(rect, edges, `rel-${edge}`) &&
186
+ !ownEdgeTouches(rect, points));
187
+ if (!free)
188
+ return;
189
+ placed.push(free);
190
+ out.push({ edge, kind: 'label', text, rect: free });
191
+ });
192
+ model.relations.forEach((rel, edge) => {
193
+ const points = edgesById.get(`rel-${edge}`)?.points;
194
+ if (!points || points.length < 2 || !rel.weight || rel.weight <= 1)
195
+ return;
196
+ const text = `×${rel.weight}`;
197
+ const width = measureTextWidth(text, EDGE_LABEL_FONT_SIZE) + EDGE_LABEL_PAD_X * 2;
198
+ const candidates = labelCandidates(points, width, EDGE_LABEL_HEIGHT);
199
+ const clear = (rect) => !nodeRects.some((n) => rectsIntersect(rect, n)) && !placed.some((p) => rectsIntersect(rect, p));
200
+ const free = candidates.find((rect) => clear(rect) && !rectHitsOtherEdge(rect, edges, `rel-${edge}`)) ??
201
+ candidates.find(clear);
202
+ const rect = free ?? candidates[0];
203
+ placed.push(rect);
204
+ out.push({ edge, kind: 'weight', text, rect });
205
+ });
206
+ return out;
207
+ }
208
+ function ownEdgeTouches(rect, points) {
209
+ for (let i = 0; i + 1 < points.length; i++) {
210
+ if (segmentIntersectsRect(points[i], points[i + 1], rect))
211
+ return true;
212
+ }
213
+ return false;
214
+ }
215
+ function labelCandidates(points, width, height) {
216
+ const segs = [];
217
+ for (let i = 0; i + 1 < points.length; i++) {
218
+ const len = Math.hypot(points[i + 1].x - points[i].x, points[i + 1].y - points[i].y);
219
+ segs.push({ a: points[i], b: points[i + 1], len });
220
+ }
221
+ segs.sort((p, q) => q.len - p.len);
222
+ const rects = [];
223
+ for (const { a, b } of segs) {
224
+ for (const t of ANCHOR_FRACTIONS) {
225
+ const cx = a.x + (b.x - a.x) * t;
226
+ const cy = a.y + (b.y - a.y) * t;
227
+ rects.push({ x: cx - width / 2, y: cy - height / 2, width, height });
228
+ }
229
+ }
230
+ return rects;
231
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Semantic layer resolution for the ELK engine: which layer a package belongs
3
+ * to, how layers are ordered, and which ELK partition each package lands in.
4
+ * Pure functions over the model; no ELK types involved.
5
+ */
6
+ import { PlantUmlModel, PlantUmlPackage } from './model.js';
7
+ /** Resolve a package's semantic layer from its explicit field or its name. */
8
+ export declare function packageLayer(pkg: PlantUmlPackage): string | undefined;
9
+ /**
10
+ * Order semantic layers by dependency topology instead of puml declaration
11
+ * order: a layer that depends on another (`A ..> B` across layers) sits *above*
12
+ * its dependency, so entry layers (Tests, Examples) stack over the base they
13
+ * consume (Src) and independent layers (Templates) fall out of the sandwich.
14
+ *
15
+ * A Kahn topological sort places in-degree-zero layers (nothing depends on
16
+ * them) first, breaking ties in declaration order for determinism. Layers left
17
+ * over because of a dependency cycle are appended in declaration order, so a
18
+ * cyclic graph still yields a stable (if not strictly topological) order; the
19
+ * renderer marks the offending back-edges as feedback arcs.
20
+ */
21
+ export declare function topologicalLayerOrder(model: PlantUmlModel): string[];
22
+ /** Ordered, de-duplicated list of semantic layers (dependency topology by default). */
23
+ export declare function resolveLayerOrder(model: PlantUmlModel, layerOrder?: string[]): string[];
24
+ /** Map each package to its partition index (layers first, unlayered last). */
25
+ export declare function assignPartitions(model: PlantUmlModel, layers: string[]): Map<string, number>;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Semantic layer resolution for the ELK engine: which layer a package belongs
3
+ * to, how layers are ordered, and which ELK partition each package lands in.
4
+ * Pure functions over the model; no ELK types involved.
5
+ */
6
+ /** Name convention: `"X Layer"` declares a semantic layer `X`. */
7
+ const LAYER_SUFFIX = /^(.*)\s+Layer$/i;
8
+ /** Resolve a package's semantic layer from its explicit field or its name. */
9
+ export function packageLayer(pkg) {
10
+ if (pkg.layer !== undefined)
11
+ return pkg.layer;
12
+ const m = LAYER_SUFFIX.exec(pkg.name);
13
+ if (!m)
14
+ return undefined;
15
+ const core = m[1].trim();
16
+ return core === '' ? undefined : core;
17
+ }
18
+ /**
19
+ * Order semantic layers by dependency topology instead of puml declaration
20
+ * order: a layer that depends on another (`A ..> B` across layers) sits *above*
21
+ * its dependency, so entry layers (Tests, Examples) stack over the base they
22
+ * consume (Src) and independent layers (Templates) fall out of the sandwich.
23
+ *
24
+ * A Kahn topological sort places in-degree-zero layers (nothing depends on
25
+ * them) first, breaking ties in declaration order for determinism. Layers left
26
+ * over because of a dependency cycle are appended in declaration order, so a
27
+ * cyclic graph still yields a stable (if not strictly topological) order; the
28
+ * renderer marks the offending back-edges as feedback arcs.
29
+ */
30
+ export function topologicalLayerOrder(model) {
31
+ const declared = [];
32
+ for (const pkg of model.packages) {
33
+ const layer = packageLayer(pkg);
34
+ if (layer !== undefined && !declared.includes(layer))
35
+ declared.push(layer);
36
+ }
37
+ if (declared.length <= 1)
38
+ return declared;
39
+ const pkgById = new Map(model.packages.map((p) => [p.id, p]));
40
+ const layerOf = new Map();
41
+ for (const entity of model.entities) {
42
+ const pkg = entity.packageId ? pkgById.get(entity.packageId) : undefined;
43
+ layerOf.set(entity.id, pkg ? packageLayer(pkg) : undefined);
44
+ }
45
+ const dependsOn = new Map();
46
+ const dependents = new Map();
47
+ for (const layer of declared) {
48
+ dependsOn.set(layer, new Set());
49
+ dependents.set(layer, 0);
50
+ }
51
+ for (const relation of model.relations) {
52
+ const source = layerOf.get(relation.source);
53
+ const target = layerOf.get(relation.target);
54
+ if (source === undefined || target === undefined || source === target)
55
+ continue;
56
+ const set = dependsOn.get(source);
57
+ if (!set.has(target)) {
58
+ set.add(target);
59
+ dependents.set(target, (dependents.get(target) ?? 0) + 1);
60
+ }
61
+ }
62
+ const order = [];
63
+ const done = new Set();
64
+ const queue = declared.filter((layer) => (dependents.get(layer) ?? 0) === 0);
65
+ for (let i = 0; i < queue.length; i++) {
66
+ const layer = queue[i];
67
+ order.push(layer);
68
+ done.add(layer);
69
+ for (const dep of dependsOn.get(layer) ?? []) {
70
+ const remaining = (dependents.get(dep) ?? 0) - 1;
71
+ dependents.set(dep, remaining);
72
+ if (remaining === 0 && !done.has(dep))
73
+ queue.push(dep);
74
+ }
75
+ }
76
+ // Cyclic remainder: keep declaration order so the output is stable.
77
+ for (const layer of declared) {
78
+ if (!done.has(layer))
79
+ order.push(layer);
80
+ }
81
+ return order;
82
+ }
83
+ /** Ordered, de-duplicated list of semantic layers (dependency topology by default). */
84
+ export function resolveLayerOrder(model, layerOrder) {
85
+ if (layerOrder) {
86
+ return layerOrder.filter((layer, i, all) => all.indexOf(layer) === i);
87
+ }
88
+ return topologicalLayerOrder(model);
89
+ }
90
+ /** Map each package to its partition index (layers first, unlayered last). */
91
+ export function assignPartitions(model, layers) {
92
+ const partitions = new Map();
93
+ if (layers.length === 0)
94
+ return partitions;
95
+ for (const pkg of model.packages) {
96
+ const layer = packageLayer(pkg);
97
+ const index = layer !== undefined ? layers.indexOf(layer) : -1;
98
+ partitions.set(pkg.id, index >= 0 ? index : layers.length);
99
+ }
100
+ return partitions;
101
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * LayoutEngine — the pluggable layout contract. A LayoutEngine is a pure
3
+ * function from PlantUmlModel to concrete positions; the parser never talks
4
+ * to a layout engine and the renderer never talks to the parser, so swapping
5
+ * engines (ELK, Graphviz, a custom tuner) is an injection, not a rewrite.
6
+ */
7
+ import { PlantUmlModel, PlantUmlEntity } from './model.js';
8
+ export interface Point {
9
+ x: number;
10
+ y: number;
11
+ }
12
+ export interface NodeLayout {
13
+ id: string;
14
+ x: number;
15
+ y: number;
16
+ width: number;
17
+ height: number;
18
+ }
19
+ export interface EdgeLayout {
20
+ id: string;
21
+ source: string;
22
+ target: string;
23
+ points: Point[];
24
+ }
25
+ export interface PackageLayout {
26
+ id: string;
27
+ x: number;
28
+ y: number;
29
+ width: number;
30
+ height: number;
31
+ }
32
+ export interface LayoutResult {
33
+ nodes: NodeLayout[];
34
+ edges: EdgeLayout[];
35
+ /** Package frames; engines without container support may omit this. */
36
+ packages?: PackageLayout[];
37
+ width: number;
38
+ height: number;
39
+ }
40
+ export interface LayoutEngine {
41
+ layout(model: PlantUmlModel): Promise<LayoutResult>;
42
+ }
43
+ /**
44
+ * Shared drawing metrics. The renderer reads these so entity sizing and the
45
+ * emitted SVG stay in lockstep: the font family and size are bound to the same
46
+ * monospace advance [[../text-measure]] measures with.
47
+ */
48
+ export declare const METRICS: {
49
+ readonly fontFamily: "monospace";
50
+ readonly fontSize: 13;
51
+ readonly lineHeight: 18;
52
+ readonly paddingX: 10;
53
+ readonly paddingY: 8;
54
+ readonly minWidth: 60;
55
+ };
56
+ /**
57
+ * Package label-tab geometry, shared by the renderer and the layout-metrics
58
+ * overlap counter so a drawn tab and a measured tab are the same rectangle.
59
+ */
60
+ export declare const PACKAGE_LABEL: {
61
+ readonly tabHeight: 20;
62
+ readonly fontSize: 12;
63
+ readonly tabPadding: 8;
64
+ };
65
+ /** Spacing grid (px). Entity box sizes snap up to a multiple of it. */
66
+ export declare const GRID = 8;
67
+ export declare function snapUp(n: number, grid?: number): number;
68
+ /** Reconstruct the name drawn in the title compartment, generics expanded. */
69
+ export declare function displayName(entity: PlantUmlEntity): string;
70
+ /**
71
+ * Approximate the on-canvas size of an entity box. The title plus every member
72
+ * line contributes one row; the width is the longest rendered line measured
73
+ * through the text table plus padding, never below MIN_WIDTH.
74
+ */
75
+ export declare function estimateEntitySize(entity: PlantUmlEntity): {
76
+ width: number;
77
+ height: number;
78
+ };
79
+ /**
80
+ * Level of member detail drawn per entity: every member (`full`), the first
81
+ * few plus a count (`compact`), or just the class name (`names`).
82
+ */
83
+ export type DetailLevel = 'full' | 'compact' | 'names';
84
+ /** Entity count up to which `full` is chosen automatically. */
85
+ export declare const DETAIL_FULL_MAX_ENTITIES = 40;
86
+ /** Entity count up to which `compact` is chosen automatically (beyond: `names`). */
87
+ export declare const DETAIL_COMPACT_MAX_ENTITIES = 150;
88
+ /** Member rows kept per entity in `compact` mode. */
89
+ export declare const COMPACT_MEMBER_ROWS = 4;
90
+ export declare function autoDetail(entityCount: number): DetailLevel;
91
+ /**
92
+ * Reduce member rows to the requested detail level *before* layout, so boxes
93
+ * are sized for what is actually drawn. `undefined` picks by entity count.
94
+ */
95
+ export declare function applyDetail(model: PlantUmlModel, detail?: DetailLevel): PlantUmlModel;
96
+ /**
97
+ * Group nodes into rows: within one package (or the top level), nodes whose
98
+ * vertical extents overlap belong to the same layered-layout rank. Returns
99
+ * arrays of node ids, each sorted top-to-bottom.
100
+ */
101
+ export declare function layoutRows(model: PlantUmlModel, layout: LayoutResult): NodeLayout[][];
102
+ /**
103
+ * Top-align the nodes of every row (never moving a node down or out of its
104
+ * package) and keep edges attached: an endpoint on a moved node shifts with it,
105
+ * dragging along the next bend when the first segment is horizontal, so
106
+ * orthogonal routes stay orthogonal. Mutates and returns `layout`.
107
+ */
108
+ export declare function alignRows(model: PlantUmlModel, layout: LayoutResult): LayoutResult;
109
+ /**
110
+ * Top-align an isolated package frame (no edge touches any entity inside it)
111
+ * with the topmost side-by-side neighbour frame, so a lone package sits level
112
+ * with its neighbour instead of floating at its own offset. Only moves up, and
113
+ * only when the frame (with its contents) stays clear of everything else.
114
+ * Mutates and returns `layout`.
115
+ */
116
+ export declare function alignIsolatedFrames(model: PlantUmlModel, layout: LayoutResult): LayoutResult;