@zukhruf/diagram 0.3.32 → 0.3.34
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 +98 -52
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/compiler/diagram-compile.d.ts +3 -4
- package/dist/lib/compiler/diagram-compile.d.ts.map +1 -1
- package/dist/lib/compiler/diagram-compile.js +85 -370
- package/dist/lib/compiler/diagram-compile.js.map +1 -1
- package/dist/lib/layouts/graph/attach.d.ts +25 -0
- package/dist/lib/layouts/graph/attach.d.ts.map +1 -0
- package/dist/lib/layouts/graph/attach.js +272 -0
- package/dist/lib/layouts/graph/attach.js.map +1 -0
- package/dist/lib/layouts/graph/connections.d.ts +45 -0
- package/dist/lib/layouts/graph/connections.d.ts.map +1 -0
- package/dist/lib/layouts/graph/connections.js +138 -0
- package/dist/lib/layouts/graph/connections.js.map +1 -0
- package/dist/lib/layouts/graph/graph-layout-result.d.ts +5 -3
- package/dist/lib/layouts/graph/graph-layout-result.d.ts.map +1 -1
- package/dist/lib/layouts/graph/graph-layout-result.js.map +1 -1
- package/dist/lib/layouts/graph/gutters.js +41 -22
- package/dist/lib/layouts/graph/gutters.js.map +1 -1
- package/dist/lib/layouts/graph/labels.d.ts +23 -0
- package/dist/lib/layouts/graph/labels.d.ts.map +1 -0
- package/dist/lib/layouts/graph/labels.js +120 -0
- package/dist/lib/layouts/graph/labels.js.map +1 -0
- package/dist/lib/layouts/graph/layout.d.ts +1 -1
- package/dist/lib/layouts/graph/layout.d.ts.map +1 -1
- package/dist/lib/layouts/graph/layout.js +37 -12
- package/dist/lib/layouts/graph/layout.js.map +1 -1
- package/dist/lib/layouts/graph/measure.d.ts +15 -11
- package/dist/lib/layouts/graph/measure.d.ts.map +1 -1
- package/dist/lib/layouts/graph/measure.js +82 -38
- package/dist/lib/layouts/graph/measure.js.map +1 -1
- package/dist/lib/layouts/graph/min-heap.d.ts +8 -0
- package/dist/lib/layouts/graph/min-heap.d.ts.map +1 -0
- package/dist/lib/layouts/graph/min-heap.js +56 -0
- package/dist/lib/layouts/graph/min-heap.js.map +1 -0
- package/dist/lib/layouts/graph/node-content.d.ts +2 -3
- package/dist/lib/layouts/graph/node-content.d.ts.map +1 -1
- package/dist/lib/layouts/graph/node-content.js +67 -47
- package/dist/lib/layouts/graph/node-content.js.map +1 -1
- package/dist/lib/layouts/graph/path.d.ts +6 -0
- package/dist/lib/layouts/graph/path.d.ts.map +1 -0
- package/dist/lib/layouts/graph/path.js +53 -0
- package/dist/lib/layouts/graph/path.js.map +1 -0
- package/dist/lib/layouts/graph/round.d.ts +11 -0
- package/dist/lib/layouts/graph/round.d.ts.map +1 -0
- package/dist/lib/layouts/graph/round.js +181 -0
- package/dist/lib/layouts/graph/round.js.map +1 -0
- package/dist/lib/layouts/graph/route-search.d.ts +34 -0
- package/dist/lib/layouts/graph/route-search.d.ts.map +1 -0
- package/dist/lib/layouts/graph/route-search.js +208 -0
- package/dist/lib/layouts/graph/route-search.js.map +1 -0
- package/dist/lib/layouts/graph/route.d.ts +20 -0
- package/dist/lib/layouts/graph/route.d.ts.map +1 -0
- package/dist/lib/layouts/graph/route.js +276 -0
- package/dist/lib/layouts/graph/route.js.map +1 -0
- package/dist/lib/layouts/graph/sides.d.ts +28 -0
- package/dist/lib/layouts/graph/sides.d.ts.map +1 -0
- package/dist/lib/layouts/graph/sides.js +98 -0
- package/dist/lib/layouts/graph/sides.js.map +1 -0
- package/dist/lib/layouts/timeline/timeline-layout.d.ts +2 -1
- package/dist/lib/layouts/timeline/timeline-layout.d.ts.map +1 -1
- package/dist/lib/layouts/timeline/timeline-layout.js +9 -4
- package/dist/lib/layouts/timeline/timeline-layout.js.map +1 -1
- package/dist/lib/model/diagram-ir.d.ts +19 -156
- package/dist/lib/model/diagram-ir.d.ts.map +1 -1
- package/dist/lib/model/diagram-ir.js.map +1 -1
- package/dist/lib/model/validate-graph.d.ts +7 -0
- package/dist/lib/model/validate-graph.d.ts.map +1 -0
- package/dist/lib/model/validate-graph.js +54 -0
- package/dist/lib/model/validate-graph.js.map +1 -0
- package/dist/lib/react/Diagram.d.ts +3 -9
- package/dist/lib/react/Diagram.d.ts.map +1 -1
- package/dist/lib/react/Diagram.js +1 -13
- package/dist/lib/react/Diagram.js.map +1 -1
- package/dist/lib/react/diagram-types.d.ts +17 -46
- package/dist/lib/react/diagram-types.d.ts.map +1 -1
- package/dist/lib/react/diagram-types.js.map +1 -1
- package/dist/lib/renderers/canvas/graph-canvas.d.ts +2 -5
- package/dist/lib/renderers/canvas/graph-canvas.d.ts.map +1 -1
- package/dist/lib/renderers/canvas/graph-canvas.js +68 -43
- package/dist/lib/renderers/canvas/graph-canvas.js.map +1 -1
- package/dist/lib/renderers/html/graph-html.d.ts +2 -3
- package/dist/lib/renderers/html/graph-html.d.ts.map +1 -1
- package/dist/lib/renderers/html/graph-html.js +298 -289
- package/dist/lib/renderers/html/graph-html.js.map +1 -1
- package/dist/lib/renderers/shared/graph-parts.d.ts +3 -1
- package/dist/lib/renderers/shared/graph-parts.d.ts.map +1 -1
- package/dist/lib/renderers/shared/graph-parts.js +350 -418
- package/dist/lib/renderers/shared/graph-parts.js.map +1 -1
- package/dist/lib/renderers/shared/node-parts.d.ts +5 -6
- package/dist/lib/renderers/shared/node-parts.d.ts.map +1 -1
- package/dist/lib/renderers/shared/node-parts.js +112 -190
- package/dist/lib/renderers/shared/node-parts.js.map +1 -1
- package/dist/lib/renderers/shared/run-spans.js +2 -2
- package/dist/lib/renderers/shared/run-spans.js.map +1 -1
- package/dist/lib/renderers/shared/text-runs.d.ts +0 -6
- package/dist/lib/renderers/shared/text-runs.d.ts.map +1 -1
- package/dist/lib/renderers/shared/text-runs.js +0 -12
- package/dist/lib/renderers/shared/text-runs.js.map +1 -1
- package/dist/lib/renderers/svg/graph-svg.d.ts +1 -3
- package/dist/lib/renderers/svg/graph-svg.d.ts.map +1 -1
- package/dist/lib/renderers/svg/graph-svg.js +66 -80
- package/dist/lib/renderers/svg/graph-svg.js.map +1 -1
- package/dist/lib/renderers/svg/timeline-svg.d.ts.map +1 -1
- package/dist/lib/renderers/svg/timeline-svg.js +7 -5
- package/dist/lib/renderers/svg/timeline-svg.js.map +1 -1
- package/package.json +1 -1
- package/dist/lib/layouts/graph/graph-layout.d.ts +0 -154
- package/dist/lib/layouts/graph/graph-layout.d.ts.map +0 -1
- package/dist/lib/layouts/graph/graph-layout.js +0 -2340
- package/dist/lib/layouts/graph/graph-layout.js.map +0 -1
- package/dist/lib/layouts/graph/legacy-graph-layout.d.ts +0 -6
- package/dist/lib/layouts/graph/legacy-graph-layout.d.ts.map +0 -1
- package/dist/lib/layouts/graph/legacy-graph-layout.js +0 -542
- package/dist/lib/layouts/graph/legacy-graph-layout.js.map +0 -1
- package/dist/lib/layouts/text-metrics.d.ts +0 -7
- package/dist/lib/layouts/text-metrics.d.ts.map +0 -1
- package/dist/lib/layouts/text-metrics.js +0 -40
- package/dist/lib/layouts/text-metrics.js.map +0 -1
|
@@ -1,2340 +0,0 @@
|
|
|
1
|
-
import { parseEndpointRef } from "../../model/endpoint-ref.js";
|
|
2
|
-
import { centeredBaselineOffset, textAscent, textLineBox, textWidth, widestLine } from "../text-metrics.js";
|
|
3
|
-
export const COL_W = 240;
|
|
4
|
-
export const ROW_H = 128;
|
|
5
|
-
export const NODE_W = 176;
|
|
6
|
-
export const NODE_H = 64;
|
|
7
|
-
export const NODE_RX = 16;
|
|
8
|
-
export const NODE_PAD = 16;
|
|
9
|
-
export const GROUP_PAD = 24;
|
|
10
|
-
export const GROUP_LABEL_H = 28;
|
|
11
|
-
export const GROUP_TITLE_GAP = 8;
|
|
12
|
-
export const TITLE_TOP_PAD = 32;
|
|
13
|
-
export const SVG_PAD_X = 60;
|
|
14
|
-
export const SVG_PAD_Y = 32;
|
|
15
|
-
export const ROUTE_CELL = 4;
|
|
16
|
-
export const TURN_PENALTY = 14;
|
|
17
|
-
export const STUB = NODE_PAD + ROUTE_CELL;
|
|
18
|
-
export const ENTRY_GAP = 10;
|
|
19
|
-
export const CONTENT_VIEWBOX_PAD = 40;
|
|
20
|
-
export const PARALLEL_SPACING = 28;
|
|
21
|
-
const SELF_LOOP_OFFSET = 28;
|
|
22
|
-
const SELF_LOOP_DEPTH = 48;
|
|
23
|
-
const ROUTE_RESERVATION_CELLS = 0;
|
|
24
|
-
const EDGE_LABEL_FONT_SIZE = 10;
|
|
25
|
-
const EDGE_BADGE_FONT_SIZE = 10;
|
|
26
|
-
export const EDGE_BADGE_ICON = 22;
|
|
27
|
-
const EDGE_LABEL_LINE_H = 12;
|
|
28
|
-
const EDGE_LABEL_LINE_GAP = 2;
|
|
29
|
-
const EDGE_LABEL_BOX_PAD = 4;
|
|
30
|
-
const EDGE_LABEL_GAP = 4;
|
|
31
|
-
const GROUP_LABEL_LINE_H = 14;
|
|
32
|
-
const NODE_CAPTION_FONT_SIZE = 11;
|
|
33
|
-
const NODE_CAPTION_LINE_H = 14;
|
|
34
|
-
const NODE_CAPTION_OFFSET = 12;
|
|
35
|
-
const NODE_CAPTION_FRAME_PAD_X_EM = 0.9;
|
|
36
|
-
const NODE_CAPTION_FRAME_PAD_Y_EM = 0.55;
|
|
37
|
-
const ANNOTATION_LINE_GAP = 4;
|
|
38
|
-
export function nodeFrameRadius(node) {
|
|
39
|
-
return node.rx ?? (node.frame === 'flat' ? 4 : NODE_RX);
|
|
40
|
-
}
|
|
41
|
-
function graphLayout(layout) {
|
|
42
|
-
return {
|
|
43
|
-
colWidth: layout?.colWidth ?? COL_W,
|
|
44
|
-
rowHeight: layout?.rowHeight ?? ROW_H,
|
|
45
|
-
columns: layout?.columns ?? [],
|
|
46
|
-
rows: layout?.rows ?? [],
|
|
47
|
-
padX: layout?.padX ?? SVG_PAD_X,
|
|
48
|
-
padY: layout?.padY ?? SVG_PAD_Y,
|
|
49
|
-
titleTopPad: layout?.titleTopPad ?? TITLE_TOP_PAD
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
function trackSize(axis, index, layout) {
|
|
53
|
-
const resolved = graphLayout(layout);
|
|
54
|
-
const tracks = axis === 'col' ? resolved.columns : resolved.rows;
|
|
55
|
-
const fallback = axis === 'col' ? resolved.colWidth : resolved.rowHeight;
|
|
56
|
-
return tracks[index] ?? fallback;
|
|
57
|
-
}
|
|
58
|
-
function assertTrackStart(axis, value) {
|
|
59
|
-
if (Math.floor(value) < 0) {
|
|
60
|
-
throw new Error(`Diagram ${axis === 'col' ? 'column' : 'row'} ${value} resolves before the first track.`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
function trackPosition(axis, value, layout) {
|
|
64
|
-
assertTrackStart(axis, value);
|
|
65
|
-
const index = Math.floor(value);
|
|
66
|
-
const fraction = value - index;
|
|
67
|
-
let position = 0;
|
|
68
|
-
for (let i = 0; i < index; i++) {
|
|
69
|
-
position += trackSize(axis, i, layout);
|
|
70
|
-
}
|
|
71
|
-
if (fraction > 0) position += trackSize(axis, index, layout) * fraction;
|
|
72
|
-
return position;
|
|
73
|
-
}
|
|
74
|
-
function trackSpan(axis, start, end, layout) {
|
|
75
|
-
return trackPosition(axis, end, layout) - trackPosition(axis, start, layout);
|
|
76
|
-
}
|
|
77
|
-
function graphPadX(layout) {
|
|
78
|
-
return graphLayout(layout).padX;
|
|
79
|
-
}
|
|
80
|
-
function graphPadY(layout) {
|
|
81
|
-
const resolved = graphLayout(layout);
|
|
82
|
-
return resolved.padY + resolved.titleTopPad;
|
|
83
|
-
}
|
|
84
|
-
export function nodeBox(node, layout) {
|
|
85
|
-
const row = positionedValue(node, 'row');
|
|
86
|
-
const col = positionedValue(node, 'col');
|
|
87
|
-
const colSpan = node.colSpan ?? 1;
|
|
88
|
-
const innerWidth = trackSpan('col', col, col + colSpan, layout);
|
|
89
|
-
const defaultWidth = NODE_W + (colSpan - 1) * graphLayout(layout).colWidth;
|
|
90
|
-
const w = node.width ?? Math.min(defaultWidth, Math.max(NODE_W, innerWidth - 24));
|
|
91
|
-
const h = node.height ?? NODE_H;
|
|
92
|
-
const x = graphPadX(layout) + trackPosition('col', col, layout) + (innerWidth - w) / 2;
|
|
93
|
-
const rowHeight = trackSize('row', Math.floor(row), layout);
|
|
94
|
-
const y = graphPadY(layout) + trackPosition('row', row, layout) + (rowHeight - h) / 2;
|
|
95
|
-
return {
|
|
96
|
-
x,
|
|
97
|
-
y,
|
|
98
|
-
w,
|
|
99
|
-
h
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
export function nodeStackCount(node) {
|
|
103
|
-
return Math.max(1, Math.floor(node.stack ?? 1));
|
|
104
|
-
}
|
|
105
|
-
export function nodeStackOffset(node) {
|
|
106
|
-
const stackOffset = node.stackOffset ?? 12;
|
|
107
|
-
if (stackOffset < 0) {
|
|
108
|
-
throw new Error(`Diagram node "${node.id}" stackOffset must be non-negative.`);
|
|
109
|
-
}
|
|
110
|
-
return stackOffset;
|
|
111
|
-
}
|
|
112
|
-
export function nodeStackVector(node) {
|
|
113
|
-
const offset = nodeStackOffset(node);
|
|
114
|
-
const dx = node.stackDx ?? offset;
|
|
115
|
-
const dy = node.stackDy ?? offset;
|
|
116
|
-
if (dx < 0) {
|
|
117
|
-
throw new Error(`Diagram node "${node.id}" stackDx must be non-negative.`);
|
|
118
|
-
}
|
|
119
|
-
if (dy < 0) {
|
|
120
|
-
throw new Error(`Diagram node "${node.id}" stackDy must be non-negative.`);
|
|
121
|
-
}
|
|
122
|
-
const direction = node.stackDirection ?? 'up-left';
|
|
123
|
-
const xSign = direction.endsWith('left') ? -1 : 1;
|
|
124
|
-
const ySign = direction.startsWith('up') ? -1 : 1;
|
|
125
|
-
return {
|
|
126
|
-
x: xSign * dx,
|
|
127
|
-
y: ySign * dy
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
export function nodeStackWidthStep(node) {
|
|
131
|
-
const step = node.stackWidthStep ?? 0;
|
|
132
|
-
if (step < 0) {
|
|
133
|
-
throw new Error(`Diagram node "${node.id}" stackWidthStep must be non-negative.`);
|
|
134
|
-
}
|
|
135
|
-
return step;
|
|
136
|
-
}
|
|
137
|
-
export function nodeStackHeightStep(node) {
|
|
138
|
-
const step = node.stackHeightStep ?? 0;
|
|
139
|
-
if (step < 0) {
|
|
140
|
-
throw new Error(`Diagram node "${node.id}" stackHeightStep must be non-negative.`);
|
|
141
|
-
}
|
|
142
|
-
return step;
|
|
143
|
-
}
|
|
144
|
-
export function nodeStackFrameBox(node, box, level) {
|
|
145
|
-
const stackVector = nodeStackVector(node);
|
|
146
|
-
return {
|
|
147
|
-
x: box.x + level * stackVector.x,
|
|
148
|
-
y: box.y + level * stackVector.y,
|
|
149
|
-
w: box.w + level * nodeStackWidthStep(node),
|
|
150
|
-
h: box.h + level * nodeStackHeightStep(node)
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
export function nodeVisualBox(node, box = nodeBox(node)) {
|
|
154
|
-
const stackCount = nodeStackCount(node);
|
|
155
|
-
if (stackCount <= 1 || node.hidden || node.frame === 'none') return box;
|
|
156
|
-
const boxes = [box, ...Array.from({
|
|
157
|
-
length: stackCount - 1
|
|
158
|
-
}, (_, i) => nodeStackFrameBox(node, box, i + 1))];
|
|
159
|
-
const minX = Math.min(...boxes.map(candidate => candidate.x));
|
|
160
|
-
const minY = Math.min(...boxes.map(candidate => candidate.y));
|
|
161
|
-
const maxX = Math.max(...boxes.map(candidate => candidate.x + candidate.w));
|
|
162
|
-
const maxY = Math.max(...boxes.map(candidate => candidate.y + candidate.h));
|
|
163
|
-
return {
|
|
164
|
-
x: minX,
|
|
165
|
-
y: minY,
|
|
166
|
-
w: maxX - minX,
|
|
167
|
-
h: maxY - minY
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
export function nodeCaptionLines(node) {
|
|
171
|
-
if (node.caption === undefined) return [];
|
|
172
|
-
return (Array.isArray(node.caption) ? node.caption : [node.caption]).map(line => line.trim()).filter(Boolean);
|
|
173
|
-
}
|
|
174
|
-
export function nodeCaptionPlacement(node, box = nodeBox(node)) {
|
|
175
|
-
const lines = nodeCaptionLines(node);
|
|
176
|
-
if (node.hidden || lines.length === 0) return null;
|
|
177
|
-
const side = node.captionSide ?? 'bottom';
|
|
178
|
-
const offset = node.captionOffset ?? (node.captionFrame ? 0 : NODE_CAPTION_OFFSET);
|
|
179
|
-
const fontSize = node.captionFontSize ?? NODE_CAPTION_FONT_SIZE;
|
|
180
|
-
const lineHeight = Math.max(fontSize + 2, NODE_CAPTION_LINE_H);
|
|
181
|
-
const centerX = box.x + box.w / 2;
|
|
182
|
-
const centerY = box.y + box.h / 2;
|
|
183
|
-
if (node.captionFrame) {
|
|
184
|
-
const center = side === 'top' ? {
|
|
185
|
-
x: centerX,
|
|
186
|
-
y: box.y - offset
|
|
187
|
-
} : side === 'bottom' ? {
|
|
188
|
-
x: centerX,
|
|
189
|
-
y: box.y + box.h + offset
|
|
190
|
-
} : side === 'left' ? {
|
|
191
|
-
x: box.x - offset,
|
|
192
|
-
y: centerY
|
|
193
|
-
} : {
|
|
194
|
-
x: box.x + box.w + offset,
|
|
195
|
-
y: centerY
|
|
196
|
-
};
|
|
197
|
-
return {
|
|
198
|
-
lines,
|
|
199
|
-
x: center.x,
|
|
200
|
-
y: center.y - captionBlockHeight(node, fontSize, lines.length, lineHeight) / 2 + captionBlockAscent(node, fontSize),
|
|
201
|
-
align: 'middle',
|
|
202
|
-
fontSize,
|
|
203
|
-
lineHeight
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
if (side === 'top') {
|
|
207
|
-
return {
|
|
208
|
-
lines,
|
|
209
|
-
x: centerX,
|
|
210
|
-
y: box.y - offset - (lines.length - 1) * lineHeight,
|
|
211
|
-
align: 'middle',
|
|
212
|
-
fontSize,
|
|
213
|
-
lineHeight
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
if (side === 'left') {
|
|
217
|
-
return {
|
|
218
|
-
lines,
|
|
219
|
-
x: box.x - offset,
|
|
220
|
-
y: centerY - (lines.length - 1) * lineHeight / 2 + fontSize / 3,
|
|
221
|
-
align: 'end',
|
|
222
|
-
fontSize,
|
|
223
|
-
lineHeight
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
if (side === 'right') {
|
|
227
|
-
return {
|
|
228
|
-
lines,
|
|
229
|
-
x: box.x + box.w + offset,
|
|
230
|
-
y: centerY - (lines.length - 1) * lineHeight / 2 + fontSize / 3,
|
|
231
|
-
align: 'start',
|
|
232
|
-
fontSize,
|
|
233
|
-
lineHeight
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
return {
|
|
237
|
-
lines,
|
|
238
|
-
x: centerX,
|
|
239
|
-
y: box.y + box.h + offset + fontSize,
|
|
240
|
-
align: 'middle',
|
|
241
|
-
fontSize,
|
|
242
|
-
lineHeight
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
function captionFramePad(node, fontSize) {
|
|
246
|
-
return node.captionFrame ? fontSize * NODE_CAPTION_FRAME_PAD_Y_EM : 0;
|
|
247
|
-
}
|
|
248
|
-
function captionBlockAscent(node, fontSize) {
|
|
249
|
-
return textAscent(fontSize, 'sans') + captionFramePad(node, fontSize);
|
|
250
|
-
}
|
|
251
|
-
function captionBlockHeight(node, fontSize, lineCount, lineHeight) {
|
|
252
|
-
return textLineBox(fontSize, 'sans') + (lineCount - 1) * lineHeight + captionFramePad(node, fontSize) * 2;
|
|
253
|
-
}
|
|
254
|
-
export function nodeCaptionBounds(node, box = nodeBox(node)) {
|
|
255
|
-
const placement = nodeCaptionPlacement(node, box);
|
|
256
|
-
if (!placement) return null;
|
|
257
|
-
const w = widestLine(placement.lines, placement.fontSize, 'sans') + (node.captionFrame ? placement.fontSize * NODE_CAPTION_FRAME_PAD_X_EM * 2 : 0);
|
|
258
|
-
const h = captionBlockHeight(node, placement.fontSize, placement.lines.length, placement.lineHeight);
|
|
259
|
-
const x = placement.align === 'middle' ? placement.x - w / 2 : placement.align === 'end' ? placement.x - w : placement.x;
|
|
260
|
-
return {
|
|
261
|
-
x,
|
|
262
|
-
y: placement.y - captionBlockAscent(node, placement.fontSize),
|
|
263
|
-
w,
|
|
264
|
-
h
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
function positionedValue(node, axis) {
|
|
268
|
-
const value = node[axis];
|
|
269
|
-
if (value === undefined) {
|
|
270
|
-
throw new Error(`Diagram node "${node.id}" needs a ${axis}; resolve automatic graph layout before measuring nodes.`);
|
|
271
|
-
}
|
|
272
|
-
return value;
|
|
273
|
-
}
|
|
274
|
-
export function resolveGraphLayout(spec) {
|
|
275
|
-
const needsAutoLayout = spec.nodes.some(node => node.row === undefined || node.col === undefined);
|
|
276
|
-
let nodes = spec.nodes;
|
|
277
|
-
if (needsAutoLayout) {
|
|
278
|
-
for (const node of spec.nodes) {
|
|
279
|
-
const hasRow = node.row !== undefined;
|
|
280
|
-
const hasCol = node.col !== undefined;
|
|
281
|
-
if (hasRow !== hasCol) {
|
|
282
|
-
throw new Error(`Diagram node "${node.id}" needs both row and col, or neither for automatic graph layout.`);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
const ids = spec.nodes.map(node => node.id);
|
|
286
|
-
const nodeIds = new Set(ids);
|
|
287
|
-
const adjacency = new Map();
|
|
288
|
-
const indegree = new Map();
|
|
289
|
-
for (const id of ids) {
|
|
290
|
-
adjacency.set(id, []);
|
|
291
|
-
indegree.set(id, 0);
|
|
292
|
-
}
|
|
293
|
-
for (const edge of spec.edges) {
|
|
294
|
-
const from = parseEndpointRef(edge.from).nodeId;
|
|
295
|
-
const to = parseEndpointRef(edge.to).nodeId;
|
|
296
|
-
if (!nodeIds.has(from) || !nodeIds.has(to) || from === to) continue;
|
|
297
|
-
adjacency.get(from)?.push(to);
|
|
298
|
-
indegree.set(to, (indegree.get(to) ?? 0) + 1);
|
|
299
|
-
}
|
|
300
|
-
const queue = ids.filter(id => (indegree.get(id) ?? 0) === 0);
|
|
301
|
-
const rank = new Map(ids.map(id => [id, 0]));
|
|
302
|
-
const order = [];
|
|
303
|
-
// The loop visits the ids that it pushes onto the queue.
|
|
304
|
-
for (const id of queue) {
|
|
305
|
-
order.push(id);
|
|
306
|
-
for (const to of adjacency.get(id) ?? []) {
|
|
307
|
-
rank.set(to, Math.max(rank.get(to) ?? 0, (rank.get(id) ?? 0) + 1));
|
|
308
|
-
indegree.set(to, (indegree.get(to) ?? 0) - 1);
|
|
309
|
-
if (indegree.get(to) === 0) queue.push(to);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
if (order.length !== ids.length) {
|
|
313
|
-
throw new Error('Diagram automatic graph layout requires an acyclic graph. Set row and col manually for cycles.');
|
|
314
|
-
}
|
|
315
|
-
const usedRowsByCol = new Map();
|
|
316
|
-
for (const node of spec.nodes) {
|
|
317
|
-
if (node.row === undefined || node.col === undefined) continue;
|
|
318
|
-
const rows = usedRowsByCol.get(node.col) ?? new Set();
|
|
319
|
-
rows.add(node.row);
|
|
320
|
-
usedRowsByCol.set(node.col, rows);
|
|
321
|
-
}
|
|
322
|
-
const nextOpenRow = col => {
|
|
323
|
-
const rows = usedRowsByCol.get(col) ?? new Set();
|
|
324
|
-
let row = 0;
|
|
325
|
-
while (rows.has(row)) row += 1;
|
|
326
|
-
rows.add(row);
|
|
327
|
-
usedRowsByCol.set(col, rows);
|
|
328
|
-
return row;
|
|
329
|
-
};
|
|
330
|
-
nodes = spec.nodes.map(node => {
|
|
331
|
-
if (node.row !== undefined && node.col !== undefined) return node;
|
|
332
|
-
const col = rank.get(node.id) ?? 0;
|
|
333
|
-
return {
|
|
334
|
-
...node,
|
|
335
|
-
col,
|
|
336
|
-
row: nextOpenRow(col)
|
|
337
|
-
};
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
const groups = resolveGraphGroupBounds(spec.groups, nodes);
|
|
341
|
-
if (nodes === spec.nodes && groups === spec.groups) return spec;
|
|
342
|
-
return {
|
|
343
|
-
...spec,
|
|
344
|
-
nodes,
|
|
345
|
-
groups
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
function resolveGraphGroupBounds(groups, nodes) {
|
|
349
|
-
if (!groups?.length) return groups;
|
|
350
|
-
let changed = false;
|
|
351
|
-
const resolved = new Map();
|
|
352
|
-
const resolving = new Set();
|
|
353
|
-
const resolve = group => {
|
|
354
|
-
const cached = resolved.get(group.id);
|
|
355
|
-
if (cached) return cached;
|
|
356
|
-
if (resolving.has(group.id)) {
|
|
357
|
-
throw new Error(`Diagram.Group "${group.id}" has a cyclic parent relationship.`);
|
|
358
|
-
}
|
|
359
|
-
resolving.add(group.id);
|
|
360
|
-
const childGroups = groups.filter(candidate => candidate.parent === group.id).map(resolve);
|
|
361
|
-
let next = group;
|
|
362
|
-
if (!group.bounds) {
|
|
363
|
-
changed = true;
|
|
364
|
-
next = {
|
|
365
|
-
...group,
|
|
366
|
-
bounds: inferResolvedGroupBounds(group, nodes, childGroups)
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
resolving.delete(group.id);
|
|
370
|
-
resolved.set(group.id, next);
|
|
371
|
-
return next;
|
|
372
|
-
};
|
|
373
|
-
const nextGroups = groups.map(resolve);
|
|
374
|
-
return changed ? nextGroups : groups;
|
|
375
|
-
}
|
|
376
|
-
function inferResolvedGroupBounds(group, nodes, childGroups = []) {
|
|
377
|
-
const grouped = nodes.filter(node => node.group === group.id);
|
|
378
|
-
if (grouped.length === 0 && childGroups.length === 0) {
|
|
379
|
-
throw new Error(`Diagram.Group "${group.id}" needs bounds or direct node/group children so bounds can be inferred.`);
|
|
380
|
-
}
|
|
381
|
-
const bounds = emptyBounds();
|
|
382
|
-
for (const node of grouped) {
|
|
383
|
-
const row = positionedValue(node, 'row');
|
|
384
|
-
const col = positionedValue(node, 'col');
|
|
385
|
-
includeBounds(bounds, {
|
|
386
|
-
rowStart: row,
|
|
387
|
-
rowEnd: row,
|
|
388
|
-
colStart: col,
|
|
389
|
-
colEnd: col + (node.colSpan ?? 1) - 1
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
for (const childGroup of childGroups) {
|
|
393
|
-
includeBounds(bounds, requireGroupBounds(childGroup));
|
|
394
|
-
}
|
|
395
|
-
return bounds;
|
|
396
|
-
}
|
|
397
|
-
function emptyBounds() {
|
|
398
|
-
return {
|
|
399
|
-
rowStart: Number.POSITIVE_INFINITY,
|
|
400
|
-
rowEnd: Number.NEGATIVE_INFINITY,
|
|
401
|
-
colStart: Number.POSITIVE_INFINITY,
|
|
402
|
-
colEnd: Number.NEGATIVE_INFINITY
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
function includeBounds(target, source) {
|
|
406
|
-
target.rowStart = Math.min(target.rowStart, source.rowStart);
|
|
407
|
-
target.rowEnd = Math.max(target.rowEnd, source.rowEnd);
|
|
408
|
-
target.colStart = Math.min(target.colStart, source.colStart);
|
|
409
|
-
target.colEnd = Math.max(target.colEnd, source.colEnd);
|
|
410
|
-
}
|
|
411
|
-
function groupCornerInset(group) {
|
|
412
|
-
const frame = group.frame ?? 'solid';
|
|
413
|
-
if (frame === 'none') return 0;
|
|
414
|
-
return group.rx ?? 12;
|
|
415
|
-
}
|
|
416
|
-
export function groupBox(group, nodes, layout, groups) {
|
|
417
|
-
const bounds = requireGroupBounds(group);
|
|
418
|
-
const padding = groupPadding(group);
|
|
419
|
-
const contained = nodes?.filter(n => n.group === group.id) ?? [];
|
|
420
|
-
const childGroups = groups?.filter(candidate => candidate.parent === group.id) ?? [];
|
|
421
|
-
const childBoxes = nodes ? [...contained.map(node => nodeVisualBox(node, nodeBox(node, layout))), ...childGroups.map(childGroup => groupBox(childGroup, nodes, layout, groups))] : [];
|
|
422
|
-
const tightX = group.tightX && childBoxes.length > 0;
|
|
423
|
-
const tightY = (group.tight || group.tightY) && childBoxes.length > 0;
|
|
424
|
-
const minChildX = childBoxes.length > 0 ? Math.min(...childBoxes.map(b => b.x)) : 0;
|
|
425
|
-
const maxChildX = childBoxes.length > 0 ? Math.max(...childBoxes.map(b => b.x + b.w)) : 0;
|
|
426
|
-
const x = tightX ? minChildX - padding.left : graphPadX(layout) + trackPosition('col', bounds.colStart, layout) - padding.left;
|
|
427
|
-
const w = tightX ? maxChildX - minChildX + padding.left + padding.right : trackSpan('col', bounds.colStart, bounds.colEnd + 1, layout) + padding.left + padding.right;
|
|
428
|
-
if (tightY && nodes) {
|
|
429
|
-
const boxes = childBoxes;
|
|
430
|
-
if (boxes.length > 0) {
|
|
431
|
-
const minY = Math.min(...boxes.map(b => b.y));
|
|
432
|
-
const maxY = Math.max(...boxes.map(b => b.y + b.h));
|
|
433
|
-
const topPad = groupTopPadding(group, padding, true, bounds);
|
|
434
|
-
const y = minY - topPad;
|
|
435
|
-
const h = maxY - minY + topPad + padding.bottom;
|
|
436
|
-
return {
|
|
437
|
-
x,
|
|
438
|
-
y,
|
|
439
|
-
w,
|
|
440
|
-
h
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
const y = graphPadY(layout) + trackPosition('row', bounds.rowStart, layout) - groupTopPadding(group, padding, false, bounds, nodes, layout, groups);
|
|
445
|
-
const h = trackSpan('row', bounds.rowStart, bounds.rowEnd + 1, layout) + groupTopPadding(group, padding, false, bounds, nodes, layout, groups) + padding.bottom;
|
|
446
|
-
return {
|
|
447
|
-
x,
|
|
448
|
-
y,
|
|
449
|
-
w,
|
|
450
|
-
h
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
function groupTopPadding(group, padding, reserveTightLabel, bounds, nodes, layout, groups) {
|
|
454
|
-
const titleGutterHeight = groupTitleGutterHeight(group);
|
|
455
|
-
const childOverhang = titleGutterHeight > 0 && nodes ? groupChildTopOverhang(group, bounds, nodes, layout, groups) : 0;
|
|
456
|
-
const tightLabelReserve = reserveTightLabel && groupLabelHeight(group) > 0 ? groupLabelHeight(group) + GROUP_TITLE_GAP : 0;
|
|
457
|
-
return Math.max(padding.top, titleGutterHeight + childOverhang, tightLabelReserve);
|
|
458
|
-
}
|
|
459
|
-
function groupChildTopOverhang(group, bounds, nodes, layout, groups) {
|
|
460
|
-
const rowTop = graphPadY(layout) + trackPosition('row', bounds.rowStart, layout);
|
|
461
|
-
const containedNodes = nodes.filter(node => node.group === group.id);
|
|
462
|
-
const childGroups = groups?.filter(candidate => candidate.parent === group.id) ?? [];
|
|
463
|
-
const childBoxes = [...containedNodes.map(node => nodeVisualBox(node, nodeBox(node, layout))), ...childGroups.map(childGroup => groupBox(childGroup, nodes, layout, groups))];
|
|
464
|
-
if (childBoxes.length === 0) return 0;
|
|
465
|
-
return Math.max(0, ...childBoxes.map(box => rowTop - box.y));
|
|
466
|
-
}
|
|
467
|
-
function groupPadding(group) {
|
|
468
|
-
const padding = group.padding ?? GROUP_PAD;
|
|
469
|
-
if (typeof padding === 'number') {
|
|
470
|
-
return {
|
|
471
|
-
top: padding,
|
|
472
|
-
right: padding,
|
|
473
|
-
bottom: padding,
|
|
474
|
-
left: padding
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
return {
|
|
478
|
-
top: padding.top ?? GROUP_PAD,
|
|
479
|
-
right: padding.right ?? GROUP_PAD,
|
|
480
|
-
bottom: padding.bottom ?? GROUP_PAD,
|
|
481
|
-
left: padding.left ?? GROUP_PAD
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
export function groupLabelLines(group) {
|
|
485
|
-
if (Array.isArray(group.label)) return group.label;
|
|
486
|
-
return group.label ? [group.label] : [];
|
|
487
|
-
}
|
|
488
|
-
export function groupLabelHeight(group) {
|
|
489
|
-
const lines = groupLabelLines(group);
|
|
490
|
-
if (lines.length === 0) return 0;
|
|
491
|
-
return GROUP_LABEL_H + (lines.length - 1) * GROUP_LABEL_LINE_H;
|
|
492
|
-
}
|
|
493
|
-
export function groupTitleGutterHeight(group) {
|
|
494
|
-
if (group.titleGap !== undefined && group.titleGap < 0) {
|
|
495
|
-
throw new Error(`Diagram.Group "${group.id}" titleGap must be non-negative.`);
|
|
496
|
-
}
|
|
497
|
-
const labelHeight = groupLabelHeight(group);
|
|
498
|
-
if (labelHeight === 0) return 0;
|
|
499
|
-
if (!group.titleBand && group.titleGap === undefined) return 0;
|
|
500
|
-
return labelHeight + (group.titleGap ?? GROUP_TITLE_GAP);
|
|
501
|
-
}
|
|
502
|
-
function requireGroupBounds(group) {
|
|
503
|
-
if (!group.bounds) {
|
|
504
|
-
throw new Error(`Diagram.Group "${group.id}" needs bounds; resolve automatic graph layout before measuring groups.`);
|
|
505
|
-
}
|
|
506
|
-
return group.bounds;
|
|
507
|
-
}
|
|
508
|
-
export function groupDividerLines(group, nodes, layout, groups) {
|
|
509
|
-
const bounds = requireGroupBounds(group);
|
|
510
|
-
const box = groupBox(group, nodes, layout, groups);
|
|
511
|
-
const lines = [];
|
|
512
|
-
for (const row of group.dividers?.rows ?? []) {
|
|
513
|
-
if (row <= bounds.rowStart || row > bounds.rowEnd) {
|
|
514
|
-
throw new Error(`Diagram.Group "${group.id}" row divider ${row} is outside group bounds.`);
|
|
515
|
-
}
|
|
516
|
-
const y = graphPadY(layout) + trackPosition('row', row, layout);
|
|
517
|
-
lines.push({
|
|
518
|
-
axis: 'row',
|
|
519
|
-
value: row,
|
|
520
|
-
x1: box.x,
|
|
521
|
-
y1: y,
|
|
522
|
-
x2: box.x + box.w,
|
|
523
|
-
y2: y,
|
|
524
|
-
accent: group.accent ?? false
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
for (const col of group.dividers?.cols ?? []) {
|
|
528
|
-
if (col <= bounds.colStart || col > bounds.colEnd) {
|
|
529
|
-
throw new Error(`Diagram.Group "${group.id}" column divider ${col} is outside group bounds.`);
|
|
530
|
-
}
|
|
531
|
-
const x = graphPadX(layout) + trackPosition('col', col, layout);
|
|
532
|
-
lines.push({
|
|
533
|
-
axis: 'col',
|
|
534
|
-
value: col,
|
|
535
|
-
x1: x,
|
|
536
|
-
y1: box.y,
|
|
537
|
-
x2: x,
|
|
538
|
-
y2: box.y + box.h,
|
|
539
|
-
accent: group.accent ?? false
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
return lines;
|
|
543
|
-
}
|
|
544
|
-
export function svgDimensions(spec) {
|
|
545
|
-
spec = resolveGraphLayout(spec);
|
|
546
|
-
const layout = spec.layout;
|
|
547
|
-
let maxRow = 0;
|
|
548
|
-
let maxCol = 0;
|
|
549
|
-
let maxX = 0;
|
|
550
|
-
let maxY = 0;
|
|
551
|
-
for (const n of spec.nodes) {
|
|
552
|
-
const row = positionedValue(n, 'row');
|
|
553
|
-
const col = positionedValue(n, 'col');
|
|
554
|
-
if (row > maxRow) maxRow = row;
|
|
555
|
-
const lastCol = col + (n.colSpan ?? 1) - 1;
|
|
556
|
-
if (lastCol > maxCol) maxCol = lastCol;
|
|
557
|
-
}
|
|
558
|
-
for (const g of spec.groups ?? []) {
|
|
559
|
-
const bounds = requireGroupBounds(g);
|
|
560
|
-
if (bounds.rowEnd > maxRow) maxRow = bounds.rowEnd;
|
|
561
|
-
if (bounds.colEnd > maxCol) maxCol = bounds.colEnd;
|
|
562
|
-
}
|
|
563
|
-
for (const edge of spec.edges) {
|
|
564
|
-
for (const point of resolveEdgeWaypoints(edge.waypoints, spec)) {
|
|
565
|
-
if (point.x > maxX) maxX = point.x;
|
|
566
|
-
if (point.y > maxY) maxY = point.y;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
for (const annotation of resolveGraphAnnotations(spec)) {
|
|
570
|
-
const bounds = annotationTextBounds(annotation);
|
|
571
|
-
if (bounds.x + bounds.w > maxX) maxX = bounds.x + bounds.w;
|
|
572
|
-
if (bounds.y + bounds.h > maxY) maxY = bounds.y + bounds.h;
|
|
573
|
-
for (const point of annotationLeaderBoundaryPoints(annotation)) {
|
|
574
|
-
if (point.x > maxX) maxX = point.x;
|
|
575
|
-
if (point.y > maxY) maxY = point.y;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
const resolved = graphLayout(layout);
|
|
579
|
-
const gridW = resolved.padX * 2 + trackPosition('col', maxCol + 1, layout);
|
|
580
|
-
const gridH = resolved.padY * 2 + resolved.titleTopPad + trackPosition('row', maxRow + 1, layout) + GROUP_PAD;
|
|
581
|
-
const w = Math.max(gridW, maxX + resolved.padX);
|
|
582
|
-
const h = Math.max(gridH, maxY + resolved.padY);
|
|
583
|
-
return {
|
|
584
|
-
w,
|
|
585
|
-
h
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
export function graphContentViewBox(spec, routedEdges, padding = CONTENT_VIEWBOX_PAD) {
|
|
589
|
-
spec = resolveGraphLayout(spec);
|
|
590
|
-
const layout = spec.layout;
|
|
591
|
-
let minX = Number.POSITIVE_INFINITY;
|
|
592
|
-
let minY = Number.POSITIVE_INFINITY;
|
|
593
|
-
let maxX = Number.NEGATIVE_INFINITY;
|
|
594
|
-
let maxY = Number.NEGATIVE_INFINITY;
|
|
595
|
-
const includePoint = point => {
|
|
596
|
-
minX = Math.min(minX, point.x);
|
|
597
|
-
minY = Math.min(minY, point.y);
|
|
598
|
-
maxX = Math.max(maxX, point.x);
|
|
599
|
-
maxY = Math.max(maxY, point.y);
|
|
600
|
-
};
|
|
601
|
-
const includeBox = box => {
|
|
602
|
-
includePoint({
|
|
603
|
-
x: box.x,
|
|
604
|
-
y: box.y
|
|
605
|
-
});
|
|
606
|
-
includePoint({
|
|
607
|
-
x: box.x + box.w,
|
|
608
|
-
y: box.y + box.h
|
|
609
|
-
});
|
|
610
|
-
};
|
|
611
|
-
for (const node of spec.nodes) {
|
|
612
|
-
if (!node.hidden) {
|
|
613
|
-
const box = nodeBox(node, layout);
|
|
614
|
-
includeBox(nodeVisualBox(node, box));
|
|
615
|
-
const captionBounds = nodeCaptionBounds(node, box);
|
|
616
|
-
if (captionBounds) includeBox(captionBounds);
|
|
617
|
-
for (const port of nodePortPlacements(node, box)) {
|
|
618
|
-
includePoint({
|
|
619
|
-
x: port.x,
|
|
620
|
-
y: port.y
|
|
621
|
-
});
|
|
622
|
-
includePoint({
|
|
623
|
-
x: port.labelX,
|
|
624
|
-
y: port.labelY
|
|
625
|
-
});
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
for (const group of spec.groups ?? []) {
|
|
630
|
-
const box = groupBox(group, spec.nodes, layout, spec.groups);
|
|
631
|
-
includeBox(box);
|
|
632
|
-
for (const port of groupPortPlacements(group, box)) {
|
|
633
|
-
includePoint({
|
|
634
|
-
x: port.x,
|
|
635
|
-
y: port.y
|
|
636
|
-
});
|
|
637
|
-
includePoint({
|
|
638
|
-
x: port.labelX,
|
|
639
|
-
y: port.labelY
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
for (const line of groupDividerLines(group, spec.nodes, layout, spec.groups)) {
|
|
643
|
-
includePoint({
|
|
644
|
-
x: line.x1,
|
|
645
|
-
y: line.y1
|
|
646
|
-
});
|
|
647
|
-
includePoint({
|
|
648
|
-
x: line.x2,
|
|
649
|
-
y: line.y2
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
for (const routedEdge of routedEdges) {
|
|
654
|
-
for (const point of routedEdge.points) includePoint(point);
|
|
655
|
-
if (routedEdge.labelPlacement) {
|
|
656
|
-
includePoint({
|
|
657
|
-
x: routedEdge.labelPlacement.x,
|
|
658
|
-
y: routedEdge.labelPlacement.y
|
|
659
|
-
});
|
|
660
|
-
if (routedEdge.edge.label) {
|
|
661
|
-
includeBox(edgeLabelFrameBox(routedEdge.edge, routedEdge.labelPlacement));
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
if (routedEdge.badgePlacement) {
|
|
665
|
-
includeBox(edgeBadgeBounds(routedEdge.edge, routedEdge.badgePlacement));
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
for (const annotation of resolveGraphAnnotations(spec)) {
|
|
669
|
-
includeBox(annotationTextBounds(annotation));
|
|
670
|
-
for (const point of annotationLeaderBoundaryPoints(annotation)) {
|
|
671
|
-
includePoint(point);
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
if (!Number.isFinite(minX) || !Number.isFinite(minY) || !Number.isFinite(maxX) || !Number.isFinite(maxY)) {
|
|
675
|
-
const {
|
|
676
|
-
w,
|
|
677
|
-
h
|
|
678
|
-
} = svgDimensions(spec);
|
|
679
|
-
return {
|
|
680
|
-
x: 0,
|
|
681
|
-
y: 0,
|
|
682
|
-
w,
|
|
683
|
-
h
|
|
684
|
-
};
|
|
685
|
-
}
|
|
686
|
-
const x = Math.floor(minX - padding);
|
|
687
|
-
const y = Math.floor(minY - padding);
|
|
688
|
-
const w = Math.ceil(maxX - minX + padding * 2);
|
|
689
|
-
const h = Math.ceil(maxY - minY + padding * 2);
|
|
690
|
-
return {
|
|
691
|
-
x,
|
|
692
|
-
y,
|
|
693
|
-
w,
|
|
694
|
-
h
|
|
695
|
-
};
|
|
696
|
-
}
|
|
697
|
-
function portPlacements(ownerId, ports, box) {
|
|
698
|
-
return (ports ?? []).map(port => {
|
|
699
|
-
const point = pointOnBoxSide(box, port.side, port.offset ?? 0);
|
|
700
|
-
const labelGap = 10;
|
|
701
|
-
if (port.side === 'left') {
|
|
702
|
-
return {
|
|
703
|
-
...port,
|
|
704
|
-
ownerId,
|
|
705
|
-
x: point.x,
|
|
706
|
-
y: point.y,
|
|
707
|
-
labelX: point.x - labelGap,
|
|
708
|
-
labelY: point.y + 4,
|
|
709
|
-
labelAlign: 'end'
|
|
710
|
-
};
|
|
711
|
-
}
|
|
712
|
-
if (port.side === 'right') {
|
|
713
|
-
return {
|
|
714
|
-
...port,
|
|
715
|
-
ownerId,
|
|
716
|
-
x: point.x,
|
|
717
|
-
y: point.y,
|
|
718
|
-
labelX: point.x + labelGap,
|
|
719
|
-
labelY: point.y + 4,
|
|
720
|
-
labelAlign: 'start'
|
|
721
|
-
};
|
|
722
|
-
}
|
|
723
|
-
if (port.side === 'top') {
|
|
724
|
-
return {
|
|
725
|
-
...port,
|
|
726
|
-
ownerId,
|
|
727
|
-
x: point.x,
|
|
728
|
-
y: point.y,
|
|
729
|
-
labelX: point.x,
|
|
730
|
-
labelY: point.y - 8,
|
|
731
|
-
labelAlign: 'middle'
|
|
732
|
-
};
|
|
733
|
-
}
|
|
734
|
-
return {
|
|
735
|
-
...port,
|
|
736
|
-
ownerId,
|
|
737
|
-
x: point.x,
|
|
738
|
-
y: point.y,
|
|
739
|
-
labelX: point.x,
|
|
740
|
-
labelY: point.y + 18,
|
|
741
|
-
labelAlign: 'middle'
|
|
742
|
-
};
|
|
743
|
-
});
|
|
744
|
-
}
|
|
745
|
-
export function nodePortPlacements(node, box = nodeBox(node)) {
|
|
746
|
-
return portPlacements(node.id, node.ports, box);
|
|
747
|
-
}
|
|
748
|
-
export function groupPortPlacements(group, box = groupBox(group)) {
|
|
749
|
-
return portPlacements(group.id, group.ports, box);
|
|
750
|
-
}
|
|
751
|
-
export function resolveGraphAnnotations(spec) {
|
|
752
|
-
return (spec.annotations ?? []).map(annotation => resolveGraphAnnotation(annotation, spec));
|
|
753
|
-
}
|
|
754
|
-
export function resolveGraphAnnotation(annotation, spec) {
|
|
755
|
-
if (annotation.target && annotation.targets !== undefined) {
|
|
756
|
-
throw new Error('Diagram.Text accepts either target or targets, not both.');
|
|
757
|
-
}
|
|
758
|
-
if (annotation.targets !== undefined && annotation.targets.length === 0) {
|
|
759
|
-
throw new Error('Diagram.Text targets must include at least one id.');
|
|
760
|
-
}
|
|
761
|
-
if (annotation.guide && !annotation.target && !annotation.targets?.length) {
|
|
762
|
-
throw new Error(`Diagram.Text guide "${annotation.guide}" needs a target node/group id to place a lane callout.`);
|
|
763
|
-
}
|
|
764
|
-
if (annotation.targets?.length) {
|
|
765
|
-
const targetBoxes = annotation.targets.map(target => ({
|
|
766
|
-
target,
|
|
767
|
-
box: targetBoxForAnnotation(target, spec)
|
|
768
|
-
}));
|
|
769
|
-
const targetBox = unionBoxes(targetBoxes.map(({
|
|
770
|
-
box
|
|
771
|
-
}) => box));
|
|
772
|
-
const anchor = annotation.anchor ?? 'top';
|
|
773
|
-
const point = annotation.guide ? annotationGuidePoint(annotation.guide, targetBox, anchor, spec) : anchorPoint(targetBox, anchor, annotation.offset ?? 16);
|
|
774
|
-
const resolvedPoint = {
|
|
775
|
-
x: point.x + (annotation.dx ?? 0),
|
|
776
|
-
y: point.y + (annotation.dy ?? 0)
|
|
777
|
-
};
|
|
778
|
-
const leaders = annotation.leader ? targetBoxes.map(({
|
|
779
|
-
target,
|
|
780
|
-
box
|
|
781
|
-
}) => {
|
|
782
|
-
const leader = annotationLeaderPoints(annotation, box, anchor, resolvedPoint);
|
|
783
|
-
return {
|
|
784
|
-
target,
|
|
785
|
-
anchor,
|
|
786
|
-
...leader
|
|
787
|
-
};
|
|
788
|
-
}) : undefined;
|
|
789
|
-
return {
|
|
790
|
-
...annotation,
|
|
791
|
-
x: resolvedPoint.x,
|
|
792
|
-
y: resolvedPoint.y,
|
|
793
|
-
align: annotation.align ?? defaultAnnotationAlign(anchor),
|
|
794
|
-
leaders
|
|
795
|
-
};
|
|
796
|
-
}
|
|
797
|
-
if (annotation.target) {
|
|
798
|
-
const targetBox = targetBoxForAnnotation(annotation.target, spec);
|
|
799
|
-
const anchor = annotation.anchor ?? 'top';
|
|
800
|
-
const point = annotation.guide ? annotationGuidePoint(annotation.guide, targetBox, anchor, spec) : anchorPoint(targetBox, anchor, annotation.offset ?? 16);
|
|
801
|
-
const resolvedPoint = {
|
|
802
|
-
x: point.x + (annotation.dx ?? 0),
|
|
803
|
-
y: point.y + (annotation.dy ?? 0)
|
|
804
|
-
};
|
|
805
|
-
const leader = annotation.leader ? annotationLeaderPoints(annotation, targetBox, anchor, resolvedPoint) : null;
|
|
806
|
-
return {
|
|
807
|
-
...annotation,
|
|
808
|
-
x: resolvedPoint.x,
|
|
809
|
-
y: resolvedPoint.y,
|
|
810
|
-
align: annotation.align ?? defaultAnnotationAlign(anchor),
|
|
811
|
-
leaderFrom: leader?.from,
|
|
812
|
-
leaderTo: leader?.to,
|
|
813
|
-
leaders: leader ? [{
|
|
814
|
-
target: annotation.target,
|
|
815
|
-
anchor,
|
|
816
|
-
...leader
|
|
817
|
-
}] : undefined
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
|
-
if (annotation.x === undefined || annotation.y === undefined) {
|
|
821
|
-
throw new Error('Diagram.Text needs x/y coordinates or a target node/group id.');
|
|
822
|
-
}
|
|
823
|
-
return {
|
|
824
|
-
...annotation,
|
|
825
|
-
x: annotation.x,
|
|
826
|
-
y: annotation.y
|
|
827
|
-
};
|
|
828
|
-
}
|
|
829
|
-
function annotationGuidePoint(guideId, targetBox, anchor, spec) {
|
|
830
|
-
const guide = guideForId(guideId, spec, 'Diagram.Text');
|
|
831
|
-
const line = resolveGuideLine(guide, spec, 'Diagram.Text');
|
|
832
|
-
const targetPoint = anchorPoint(targetBox, anchor, 0);
|
|
833
|
-
if (line.orientation === 'vertical') {
|
|
834
|
-
return {
|
|
835
|
-
x: line.x,
|
|
836
|
-
y: targetPoint.y
|
|
837
|
-
};
|
|
838
|
-
}
|
|
839
|
-
return {
|
|
840
|
-
x: targetPoint.x,
|
|
841
|
-
y: line.y
|
|
842
|
-
};
|
|
843
|
-
}
|
|
844
|
-
function targetBoxForAnnotation(target, spec) {
|
|
845
|
-
const node = spec.nodes.find(candidate => candidate.id === target);
|
|
846
|
-
if (node) return nodeBox(node, spec.layout);
|
|
847
|
-
const group = spec.groups?.find(candidate => candidate.id === target);
|
|
848
|
-
if (group) return groupBox(group, spec.nodes, spec.layout, spec.groups);
|
|
849
|
-
throw new Error(`Diagram.Text target "${target}" does not match a node or group.`);
|
|
850
|
-
}
|
|
851
|
-
function unionBoxes(boxes) {
|
|
852
|
-
const minX = Math.min(...boxes.map(box => box.x));
|
|
853
|
-
const minY = Math.min(...boxes.map(box => box.y));
|
|
854
|
-
const maxX = Math.max(...boxes.map(box => box.x + box.w));
|
|
855
|
-
const maxY = Math.max(...boxes.map(box => box.y + box.h));
|
|
856
|
-
return {
|
|
857
|
-
x: minX,
|
|
858
|
-
y: minY,
|
|
859
|
-
w: maxX - minX,
|
|
860
|
-
h: maxY - minY
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
|
-
function anchorPoint(box, anchor, offset) {
|
|
864
|
-
const centerX = box.x + box.w / 2;
|
|
865
|
-
const centerY = box.y + box.h / 2;
|
|
866
|
-
if (anchor === 'top') return {
|
|
867
|
-
x: centerX,
|
|
868
|
-
y: box.y - offset
|
|
869
|
-
};
|
|
870
|
-
if (anchor === 'bottom') return {
|
|
871
|
-
x: centerX,
|
|
872
|
-
y: box.y + box.h + offset
|
|
873
|
-
};
|
|
874
|
-
if (anchor === 'left') return {
|
|
875
|
-
x: box.x - offset,
|
|
876
|
-
y: centerY
|
|
877
|
-
};
|
|
878
|
-
if (anchor === 'right') return {
|
|
879
|
-
x: box.x + box.w + offset,
|
|
880
|
-
y: centerY
|
|
881
|
-
};
|
|
882
|
-
return {
|
|
883
|
-
x: centerX,
|
|
884
|
-
y: centerY
|
|
885
|
-
};
|
|
886
|
-
}
|
|
887
|
-
function annotationLeaderPoints(annotation, targetBox, anchor, textPoint) {
|
|
888
|
-
const from = anchorPoint(targetBox, anchor, 0);
|
|
889
|
-
const gap = annotation.leaderGap ?? 6;
|
|
890
|
-
const fontSize = annotation.fontSize ?? 11;
|
|
891
|
-
const route = annotation.leaderRoute ?? 'straight';
|
|
892
|
-
const finish = to => ({
|
|
893
|
-
route,
|
|
894
|
-
from,
|
|
895
|
-
to,
|
|
896
|
-
points: annotationLeaderRoutePoints(route, anchor, from, to)
|
|
897
|
-
});
|
|
898
|
-
if (anchor === 'right') {
|
|
899
|
-
return finish({
|
|
900
|
-
x: textPoint.x - gap,
|
|
901
|
-
y: textPoint.y
|
|
902
|
-
});
|
|
903
|
-
}
|
|
904
|
-
if (anchor === 'left') {
|
|
905
|
-
return finish({
|
|
906
|
-
x: textPoint.x + gap,
|
|
907
|
-
y: textPoint.y
|
|
908
|
-
});
|
|
909
|
-
}
|
|
910
|
-
if (anchor === 'top') {
|
|
911
|
-
return finish({
|
|
912
|
-
x: textPoint.x,
|
|
913
|
-
y: textPoint.y + fontSize + gap
|
|
914
|
-
});
|
|
915
|
-
}
|
|
916
|
-
if (anchor === 'bottom') {
|
|
917
|
-
return finish({
|
|
918
|
-
x: textPoint.x,
|
|
919
|
-
y: textPoint.y - fontSize - gap
|
|
920
|
-
});
|
|
921
|
-
}
|
|
922
|
-
return finish(textPoint);
|
|
923
|
-
}
|
|
924
|
-
function annotationLeaderRoutePoints(route, anchor, from, to) {
|
|
925
|
-
if (route === 'straight') return [from, to];
|
|
926
|
-
if (anchor === 'center') return [from, to];
|
|
927
|
-
return compactLeaderPoints([from, {
|
|
928
|
-
x: from.x,
|
|
929
|
-
y: to.y
|
|
930
|
-
}, to]);
|
|
931
|
-
}
|
|
932
|
-
function compactLeaderPoints(points) {
|
|
933
|
-
return points.filter((point, index) => {
|
|
934
|
-
const previous = points[index - 1];
|
|
935
|
-
return !previous || previous.x !== point.x || previous.y !== point.y;
|
|
936
|
-
});
|
|
937
|
-
}
|
|
938
|
-
function defaultAnnotationAlign(anchor) {
|
|
939
|
-
if (anchor === 'left') return 'end';
|
|
940
|
-
if (anchor === 'right') return 'start';
|
|
941
|
-
return 'middle';
|
|
942
|
-
}
|
|
943
|
-
function annotationTextBounds(annotation) {
|
|
944
|
-
const fontSize = annotation.fontSize ?? 11;
|
|
945
|
-
const lineHeight = fontSize + (annotation.lineGap ?? ANNOTATION_LINE_GAP);
|
|
946
|
-
const width = widestLine(annotation.text, fontSize, 'sans');
|
|
947
|
-
const height = textLineBox(fontSize, 'sans') + Math.max(0, annotation.text.length - 1) * lineHeight;
|
|
948
|
-
const align = annotation.align ?? 'middle';
|
|
949
|
-
let x = annotation.x - width / 2;
|
|
950
|
-
if (align === 'start') x = annotation.x;
|
|
951
|
-
if (align === 'end') x = annotation.x - width;
|
|
952
|
-
return {
|
|
953
|
-
x,
|
|
954
|
-
y: annotation.y - fontSize,
|
|
955
|
-
w: width,
|
|
956
|
-
h: height
|
|
957
|
-
};
|
|
958
|
-
}
|
|
959
|
-
function annotationLeaderBoundaryPoints(annotation) {
|
|
960
|
-
if (annotation.leaders?.length) {
|
|
961
|
-
return annotation.leaders.flatMap(leader => leader.points);
|
|
962
|
-
}
|
|
963
|
-
return [annotation.leaderFrom, annotation.leaderTo].filter(point => point !== undefined);
|
|
964
|
-
}
|
|
965
|
-
class MinHeap {
|
|
966
|
-
data = [];
|
|
967
|
-
push(item, priority) {
|
|
968
|
-
this.data.push({
|
|
969
|
-
item,
|
|
970
|
-
priority
|
|
971
|
-
});
|
|
972
|
-
this.bubbleUp(this.data.length - 1);
|
|
973
|
-
}
|
|
974
|
-
pop() {
|
|
975
|
-
const top = this.data[0];
|
|
976
|
-
if (!top) return undefined;
|
|
977
|
-
const last = this.data.pop();
|
|
978
|
-
if (last && this.data.length > 0) {
|
|
979
|
-
this.data[0] = last;
|
|
980
|
-
this.bubbleDown(0);
|
|
981
|
-
}
|
|
982
|
-
return top.item;
|
|
983
|
-
}
|
|
984
|
-
empty() {
|
|
985
|
-
return this.data.length === 0;
|
|
986
|
-
}
|
|
987
|
-
bubbleUp(i) {
|
|
988
|
-
while (i > 0) {
|
|
989
|
-
const parent = i - 1 >> 1;
|
|
990
|
-
const entry = this.data[i];
|
|
991
|
-
const parentEntry = this.data[parent];
|
|
992
|
-
if (!entry || !parentEntry || entry.priority >= parentEntry.priority) break;
|
|
993
|
-
this.data[i] = parentEntry;
|
|
994
|
-
this.data[parent] = entry;
|
|
995
|
-
i = parent;
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
bubbleDown(i) {
|
|
999
|
-
while (true) {
|
|
1000
|
-
const entry = this.data[i];
|
|
1001
|
-
if (!entry) break;
|
|
1002
|
-
let smallest = i;
|
|
1003
|
-
let smallestEntry = entry;
|
|
1004
|
-
const left = this.data[2 * i + 1];
|
|
1005
|
-
if (left && left.priority < smallestEntry.priority) {
|
|
1006
|
-
smallest = 2 * i + 1;
|
|
1007
|
-
smallestEntry = left;
|
|
1008
|
-
}
|
|
1009
|
-
const right = this.data[2 * i + 2];
|
|
1010
|
-
if (right && right.priority < smallestEntry.priority) {
|
|
1011
|
-
smallest = 2 * i + 2;
|
|
1012
|
-
smallestEntry = right;
|
|
1013
|
-
}
|
|
1014
|
-
if (smallest === i) break;
|
|
1015
|
-
this.data[i] = smallestEntry;
|
|
1016
|
-
this.data[smallest] = entry;
|
|
1017
|
-
i = smallest;
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
function manhattan(a, b) {
|
|
1022
|
-
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
|
1023
|
-
}
|
|
1024
|
-
function dirCode(from, to) {
|
|
1025
|
-
if (to.x > from.x) return 0;
|
|
1026
|
-
if (to.x < from.x) return 1;
|
|
1027
|
-
if (to.y > from.y) return 2;
|
|
1028
|
-
return 3;
|
|
1029
|
-
}
|
|
1030
|
-
export function snapToGrid(v) {
|
|
1031
|
-
return Math.round(v / ROUTE_CELL) * ROUTE_CELL;
|
|
1032
|
-
}
|
|
1033
|
-
export function buildObstacles(nodes, gridW, gridH, layout) {
|
|
1034
|
-
const blocked = new Set();
|
|
1035
|
-
for (const n of nodes) {
|
|
1036
|
-
if (n.hidden) continue;
|
|
1037
|
-
const b = nodeVisualBox(n, nodeBox(n, layout));
|
|
1038
|
-
const x0 = Math.floor((b.x - NODE_PAD) / ROUTE_CELL);
|
|
1039
|
-
const y0 = Math.floor((b.y - NODE_PAD) / ROUTE_CELL);
|
|
1040
|
-
const x1 = Math.ceil((b.x + b.w + NODE_PAD) / ROUTE_CELL);
|
|
1041
|
-
const y1 = Math.ceil((b.y + b.h + NODE_PAD) / ROUTE_CELL);
|
|
1042
|
-
for (let y = y0; y <= y1; y++) {
|
|
1043
|
-
for (let x = x0; x <= x1; x++) {
|
|
1044
|
-
if (x < 0 || x >= gridW || y < 0 || y >= gridH) continue;
|
|
1045
|
-
blocked.add(y * gridW + x);
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
return blocked;
|
|
1050
|
-
}
|
|
1051
|
-
function pickDiagramSidesByGap(source, target) {
|
|
1052
|
-
const tIsRight = target.x >= source.x + source.w;
|
|
1053
|
-
const tIsLeft = target.x + target.w <= source.x;
|
|
1054
|
-
const tIsBelow = target.y >= source.y + source.h;
|
|
1055
|
-
const tIsAbove = target.y + target.h <= source.y;
|
|
1056
|
-
const horizontalGap = tIsRight ? target.x - (source.x + source.w) : tIsLeft ? source.x - (target.x + target.w) : null;
|
|
1057
|
-
const verticalGap = tIsBelow ? target.y - (source.y + source.h) : tIsAbove ? source.y - (target.y + target.h) : null;
|
|
1058
|
-
const useVertical = verticalGap !== null && (horizontalGap === null || verticalGap <= horizontalGap);
|
|
1059
|
-
if (useVertical) {
|
|
1060
|
-
return tIsBelow ? {
|
|
1061
|
-
exitSide: 'bottom',
|
|
1062
|
-
entrySide: 'top'
|
|
1063
|
-
} : {
|
|
1064
|
-
exitSide: 'top',
|
|
1065
|
-
entrySide: 'bottom'
|
|
1066
|
-
};
|
|
1067
|
-
}
|
|
1068
|
-
return tIsRight ? {
|
|
1069
|
-
exitSide: 'right',
|
|
1070
|
-
entrySide: 'left'
|
|
1071
|
-
} : {
|
|
1072
|
-
exitSide: 'left',
|
|
1073
|
-
entrySide: 'right'
|
|
1074
|
-
};
|
|
1075
|
-
}
|
|
1076
|
-
export function endpointsForDiagramSides(source, target, exitSide, entrySide, exitOffset = 0, entryOffset = 0, entryGap = ENTRY_GAP) {
|
|
1077
|
-
const sCenter = {
|
|
1078
|
-
x: source.x + source.w / 2,
|
|
1079
|
-
y: source.y + source.h / 2
|
|
1080
|
-
};
|
|
1081
|
-
const tCenter = {
|
|
1082
|
-
x: target.x + target.w / 2,
|
|
1083
|
-
y: target.y + target.h / 2
|
|
1084
|
-
};
|
|
1085
|
-
let exit;
|
|
1086
|
-
if (exitSide === 'top') {
|
|
1087
|
-
exit = {
|
|
1088
|
-
x: snapToGrid(sCenter.x + exitOffset),
|
|
1089
|
-
y: source.y
|
|
1090
|
-
};
|
|
1091
|
-
} else if (exitSide === 'bottom') {
|
|
1092
|
-
exit = {
|
|
1093
|
-
x: snapToGrid(sCenter.x + exitOffset),
|
|
1094
|
-
y: source.y + source.h
|
|
1095
|
-
};
|
|
1096
|
-
} else if (exitSide === 'left') {
|
|
1097
|
-
exit = {
|
|
1098
|
-
x: source.x,
|
|
1099
|
-
y: snapToGrid(sCenter.y + exitOffset)
|
|
1100
|
-
};
|
|
1101
|
-
} else {
|
|
1102
|
-
exit = {
|
|
1103
|
-
x: source.x + source.w,
|
|
1104
|
-
y: snapToGrid(sCenter.y + exitOffset)
|
|
1105
|
-
};
|
|
1106
|
-
}
|
|
1107
|
-
let entry;
|
|
1108
|
-
if (entrySide === 'top') {
|
|
1109
|
-
entry = {
|
|
1110
|
-
x: snapToGrid(tCenter.x + entryOffset),
|
|
1111
|
-
y: target.y - entryGap
|
|
1112
|
-
};
|
|
1113
|
-
} else if (entrySide === 'bottom') {
|
|
1114
|
-
entry = {
|
|
1115
|
-
x: snapToGrid(tCenter.x + entryOffset),
|
|
1116
|
-
y: target.y + target.h + entryGap
|
|
1117
|
-
};
|
|
1118
|
-
} else if (entrySide === 'left') {
|
|
1119
|
-
entry = {
|
|
1120
|
-
x: target.x - entryGap,
|
|
1121
|
-
y: snapToGrid(tCenter.y + entryOffset)
|
|
1122
|
-
};
|
|
1123
|
-
} else {
|
|
1124
|
-
entry = {
|
|
1125
|
-
x: target.x + target.w + entryGap,
|
|
1126
|
-
y: snapToGrid(tCenter.y + entryOffset)
|
|
1127
|
-
};
|
|
1128
|
-
}
|
|
1129
|
-
return {
|
|
1130
|
-
exit,
|
|
1131
|
-
entry,
|
|
1132
|
-
exitSide,
|
|
1133
|
-
entrySide
|
|
1134
|
-
};
|
|
1135
|
-
}
|
|
1136
|
-
export function pickEndpoints(source, target, hint, exitOffset = 0, entryOffset = 0, entryGap = ENTRY_GAP) {
|
|
1137
|
-
const sides = hint?.exitSide && hint?.entrySide ? {
|
|
1138
|
-
exitSide: hint.exitSide,
|
|
1139
|
-
entrySide: hint.entrySide
|
|
1140
|
-
} : pickDiagramSidesByGap(source, target);
|
|
1141
|
-
return endpointsForDiagramSides(source, target, sides.exitSide, sides.entrySide, exitOffset, entryOffset, entryGap);
|
|
1142
|
-
}
|
|
1143
|
-
export function computeStub(point, side, distance = STUB) {
|
|
1144
|
-
if (side === 'top') return {
|
|
1145
|
-
x: point.x,
|
|
1146
|
-
y: point.y - distance
|
|
1147
|
-
};
|
|
1148
|
-
if (side === 'bottom') return {
|
|
1149
|
-
x: point.x,
|
|
1150
|
-
y: point.y + distance
|
|
1151
|
-
};
|
|
1152
|
-
if (side === 'left') return {
|
|
1153
|
-
x: point.x - distance,
|
|
1154
|
-
y: point.y
|
|
1155
|
-
};
|
|
1156
|
-
return {
|
|
1157
|
-
x: point.x + distance,
|
|
1158
|
-
y: point.y
|
|
1159
|
-
};
|
|
1160
|
-
}
|
|
1161
|
-
function tryStraightLine(from, to, blocked, gridW) {
|
|
1162
|
-
if (from.x === to.x) {
|
|
1163
|
-
const x = Math.round(from.x / ROUTE_CELL);
|
|
1164
|
-
const y0 = Math.min(from.y, to.y);
|
|
1165
|
-
const y1 = Math.max(from.y, to.y);
|
|
1166
|
-
for (let y = y0; y <= y1; y += ROUTE_CELL) {
|
|
1167
|
-
const gy = Math.round(y / ROUTE_CELL);
|
|
1168
|
-
if (blocked.has(gy * gridW + x)) return false;
|
|
1169
|
-
}
|
|
1170
|
-
return true;
|
|
1171
|
-
}
|
|
1172
|
-
if (from.y === to.y) {
|
|
1173
|
-
const y = Math.round(from.y / ROUTE_CELL);
|
|
1174
|
-
const x0 = Math.min(from.x, to.x);
|
|
1175
|
-
const x1 = Math.max(from.x, to.x);
|
|
1176
|
-
for (let x = x0; x <= x1; x += ROUTE_CELL) {
|
|
1177
|
-
const gx = Math.round(x / ROUTE_CELL);
|
|
1178
|
-
if (blocked.has(y * gridW + gx)) return false;
|
|
1179
|
-
}
|
|
1180
|
-
return true;
|
|
1181
|
-
}
|
|
1182
|
-
return false;
|
|
1183
|
-
}
|
|
1184
|
-
function reserveRoutedEdge(blocked, points, gridW, gridH) {
|
|
1185
|
-
if (points.length < 2) return;
|
|
1186
|
-
for (let i = 0; i < points.length - 1; i++) {
|
|
1187
|
-
const isEndpointStub = points.length > 2 && (i === 0 || i === points.length - 2);
|
|
1188
|
-
const from = points[i];
|
|
1189
|
-
const to = points[i + 1];
|
|
1190
|
-
if (isEndpointStub || !from || !to) continue;
|
|
1191
|
-
reserveSegment(blocked, from, to, gridW, gridH);
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
function reserveSegment(blocked, from, to, gridW, gridH) {
|
|
1195
|
-
if (from.x !== to.x && from.y !== to.y) return;
|
|
1196
|
-
const x0 = Math.round(Math.min(from.x, to.x) / ROUTE_CELL);
|
|
1197
|
-
const x1 = Math.round(Math.max(from.x, to.x) / ROUTE_CELL);
|
|
1198
|
-
const y0 = Math.round(Math.min(from.y, to.y) / ROUTE_CELL);
|
|
1199
|
-
const y1 = Math.round(Math.max(from.y, to.y) / ROUTE_CELL);
|
|
1200
|
-
for (let y = y0; y <= y1; y++) {
|
|
1201
|
-
for (let x = x0; x <= x1; x++) {
|
|
1202
|
-
for (let dy = -ROUTE_RESERVATION_CELLS; dy <= ROUTE_RESERVATION_CELLS; dy++) {
|
|
1203
|
-
for (let dx = -ROUTE_RESERVATION_CELLS; dx <= ROUTE_RESERVATION_CELLS; dx++) {
|
|
1204
|
-
const gx = x + dx;
|
|
1205
|
-
const gy = y + dy;
|
|
1206
|
-
if (gx < 0 || gx >= gridW || gy < 0 || gy >= gridH) continue;
|
|
1207
|
-
blocked.add(gy * gridW + gx);
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
function aStar(start, goal, blocked, gridW, gridH) {
|
|
1214
|
-
const startKey = start.y * gridW + start.x;
|
|
1215
|
-
const goalKey = goal.y * gridW + goal.x;
|
|
1216
|
-
if (startKey === goalKey) return [start];
|
|
1217
|
-
const open = new MinHeap();
|
|
1218
|
-
const gScore = new Map();
|
|
1219
|
-
const cameFrom = new Map();
|
|
1220
|
-
gScore.set(startKey, 0);
|
|
1221
|
-
open.push({
|
|
1222
|
-
pt: start,
|
|
1223
|
-
dir: null
|
|
1224
|
-
}, manhattan(start, goal));
|
|
1225
|
-
const DIRS = [[1, 0], [-1, 0], [0, 1], [0, -1]];
|
|
1226
|
-
while (!open.empty()) {
|
|
1227
|
-
const current = open.pop();
|
|
1228
|
-
if (!current) break;
|
|
1229
|
-
const cur = current.pt;
|
|
1230
|
-
const curKey = cur.y * gridW + cur.x;
|
|
1231
|
-
if (curKey === goalKey) {
|
|
1232
|
-
const path = [cur];
|
|
1233
|
-
let k = curKey;
|
|
1234
|
-
while (cameFrom.has(k)) {
|
|
1235
|
-
const prev = cameFrom.get(k);
|
|
1236
|
-
if (!prev) break;
|
|
1237
|
-
path.unshift(prev);
|
|
1238
|
-
k = prev.y * gridW + prev.x;
|
|
1239
|
-
}
|
|
1240
|
-
return path;
|
|
1241
|
-
}
|
|
1242
|
-
const lastDir = current.dir;
|
|
1243
|
-
for (const [dx, dy] of DIRS) {
|
|
1244
|
-
const nx = cur.x + dx;
|
|
1245
|
-
const ny = cur.y + dy;
|
|
1246
|
-
if (nx < 0 || nx >= gridW || ny < 0 || ny >= gridH) continue;
|
|
1247
|
-
const nKey = ny * gridW + nx;
|
|
1248
|
-
if (blocked.has(nKey) && nKey !== goalKey) continue;
|
|
1249
|
-
const newDir = dirCode(cur, {
|
|
1250
|
-
x: nx,
|
|
1251
|
-
y: ny
|
|
1252
|
-
});
|
|
1253
|
-
const turnCost = lastDir !== null && newDir !== lastDir ? TURN_PENALTY : 0;
|
|
1254
|
-
const tentativeG = (gScore.get(curKey) ?? 0) + 1 + turnCost;
|
|
1255
|
-
const existingG = gScore.get(nKey);
|
|
1256
|
-
if (existingG !== undefined && tentativeG >= existingG) continue;
|
|
1257
|
-
gScore.set(nKey, tentativeG);
|
|
1258
|
-
cameFrom.set(nKey, cur);
|
|
1259
|
-
const f = tentativeG + manhattan({
|
|
1260
|
-
x: nx,
|
|
1261
|
-
y: ny
|
|
1262
|
-
}, goal);
|
|
1263
|
-
open.push({
|
|
1264
|
-
pt: {
|
|
1265
|
-
x: nx,
|
|
1266
|
-
y: ny
|
|
1267
|
-
},
|
|
1268
|
-
dir: newDir
|
|
1269
|
-
}, f);
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
return null;
|
|
1273
|
-
}
|
|
1274
|
-
export function simplifyPath(path) {
|
|
1275
|
-
const first = path[0];
|
|
1276
|
-
const last = path.at(-1);
|
|
1277
|
-
if (path.length < 3 || !first || !last) return path;
|
|
1278
|
-
const result = [first];
|
|
1279
|
-
let prev = first;
|
|
1280
|
-
for (let i = 1; i < path.length - 1; i++) {
|
|
1281
|
-
const cur = path[i];
|
|
1282
|
-
const next = path[i + 1];
|
|
1283
|
-
if (!cur || !next) continue;
|
|
1284
|
-
const cross = (cur.x - prev.x) * (next.y - cur.y) - (cur.y - prev.y) * (next.x - cur.x);
|
|
1285
|
-
if (cross === 0) continue;
|
|
1286
|
-
result.push(cur);
|
|
1287
|
-
prev = cur;
|
|
1288
|
-
}
|
|
1289
|
-
result.push(last);
|
|
1290
|
-
return result;
|
|
1291
|
-
}
|
|
1292
|
-
export function pathToSvg(points, radius = 8) {
|
|
1293
|
-
const [first, second] = points;
|
|
1294
|
-
const last = points.at(-1);
|
|
1295
|
-
if (!first || !last) return '';
|
|
1296
|
-
if (!second) return `M ${first.x} ${first.y}`;
|
|
1297
|
-
if (points.length === 2) return `M ${first.x} ${first.y} L ${second.x} ${second.y}`;
|
|
1298
|
-
const parts = [`M ${first.x} ${first.y}`];
|
|
1299
|
-
for (let i = 1; i < points.length - 1; i++) {
|
|
1300
|
-
const prev = points[i - 1];
|
|
1301
|
-
const cur = points[i];
|
|
1302
|
-
const next = points[i + 1];
|
|
1303
|
-
if (!prev || !cur || !next) continue;
|
|
1304
|
-
const dpx = cur.x - prev.x;
|
|
1305
|
-
const dpy = cur.y - prev.y;
|
|
1306
|
-
const dnx = next.x - cur.x;
|
|
1307
|
-
const dny = next.y - cur.y;
|
|
1308
|
-
const lp = Math.hypot(dpx, dpy);
|
|
1309
|
-
const ln = Math.hypot(dnx, dny);
|
|
1310
|
-
const r = Math.min(radius, lp / 2, ln / 2);
|
|
1311
|
-
const cs = {
|
|
1312
|
-
x: cur.x - dpx / lp * r,
|
|
1313
|
-
y: cur.y - dpy / lp * r
|
|
1314
|
-
};
|
|
1315
|
-
const ce = {
|
|
1316
|
-
x: cur.x + dnx / ln * r,
|
|
1317
|
-
y: cur.y + dny / ln * r
|
|
1318
|
-
};
|
|
1319
|
-
parts.push(`L ${cs.x} ${cs.y}`);
|
|
1320
|
-
parts.push(`Q ${cur.x} ${cur.y} ${ce.x} ${ce.y}`);
|
|
1321
|
-
}
|
|
1322
|
-
parts.push(`L ${last.x} ${last.y}`);
|
|
1323
|
-
return parts.join(' ');
|
|
1324
|
-
}
|
|
1325
|
-
function computeLabelPlacement(points, edge) {
|
|
1326
|
-
if (points.length < 2) return null;
|
|
1327
|
-
if (edge?.labelSegment !== undefined) {
|
|
1328
|
-
const segmentIndex = resolveLabelSegmentIndex(edge.labelSegment, points);
|
|
1329
|
-
const a = points[segmentIndex];
|
|
1330
|
-
const b = points[segmentIndex + 1];
|
|
1331
|
-
if (!a || !b) return null;
|
|
1332
|
-
return {
|
|
1333
|
-
...edgeLabelPlacementForSegment(a, b, edge),
|
|
1334
|
-
segmentIndex
|
|
1335
|
-
};
|
|
1336
|
-
}
|
|
1337
|
-
if (edge?.labelT !== undefined) {
|
|
1338
|
-
const anchor = pathAnchorAtFraction(points, edge.labelT);
|
|
1339
|
-
if (!anchor) return null;
|
|
1340
|
-
if (edge.labelSide && edge.labelSide !== 'auto') {
|
|
1341
|
-
return edgeLabelPlacementFromHint(anchor.point.x, anchor.point.y, edge);
|
|
1342
|
-
}
|
|
1343
|
-
const isHorizontal = anchor.from.y === anchor.to.y;
|
|
1344
|
-
if (isHorizontal) {
|
|
1345
|
-
return {
|
|
1346
|
-
x: anchor.point.x,
|
|
1347
|
-
y: anchor.point.y - 10,
|
|
1348
|
-
align: 'middle',
|
|
1349
|
-
side: 'above'
|
|
1350
|
-
};
|
|
1351
|
-
}
|
|
1352
|
-
return {
|
|
1353
|
-
x: anchor.point.x + 10,
|
|
1354
|
-
y: anchor.point.y,
|
|
1355
|
-
align: 'start',
|
|
1356
|
-
side: 'right'
|
|
1357
|
-
};
|
|
1358
|
-
}
|
|
1359
|
-
let bestI = 0;
|
|
1360
|
-
let bestLen = -1;
|
|
1361
|
-
for (let i = 0; i < points.length - 1; i++) {
|
|
1362
|
-
const a = points[i];
|
|
1363
|
-
const b = points[i + 1];
|
|
1364
|
-
if (!a || !b) continue;
|
|
1365
|
-
const len = Math.hypot(b.x - a.x, b.y - a.y);
|
|
1366
|
-
if (len > bestLen) {
|
|
1367
|
-
bestLen = len;
|
|
1368
|
-
bestI = i;
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
const a = points[bestI];
|
|
1372
|
-
const b = points[bestI + 1];
|
|
1373
|
-
if (!a || !b) return null;
|
|
1374
|
-
return {
|
|
1375
|
-
...edgeLabelPlacementForSegment(a, b, edge),
|
|
1376
|
-
segmentIndex: bestI
|
|
1377
|
-
};
|
|
1378
|
-
}
|
|
1379
|
-
function resolveLabelSegmentIndex(segment, points) {
|
|
1380
|
-
if (!Number.isInteger(segment) || segment < 0) {
|
|
1381
|
-
throw new Error(`Diagram edge labelSegment must be a non-negative integer, got ${segment}.`);
|
|
1382
|
-
}
|
|
1383
|
-
const maxSegment = points.length - 2;
|
|
1384
|
-
if (segment > maxSegment) {
|
|
1385
|
-
throw new Error(`Diagram edge labelSegment ${segment} is outside routed path segments 0-${maxSegment}.`);
|
|
1386
|
-
}
|
|
1387
|
-
return segment;
|
|
1388
|
-
}
|
|
1389
|
-
function edgeLabelPlacementForSegment(a, b, edge) {
|
|
1390
|
-
const midX = (a.x + b.x) / 2;
|
|
1391
|
-
const midY = (a.y + b.y) / 2;
|
|
1392
|
-
if (edge?.labelSide && edge.labelSide !== 'auto') {
|
|
1393
|
-
return edgeLabelPlacementFromHint(midX, midY, edge);
|
|
1394
|
-
}
|
|
1395
|
-
const isHorizontal = a.y === b.y;
|
|
1396
|
-
if (isHorizontal) {
|
|
1397
|
-
return {
|
|
1398
|
-
x: midX,
|
|
1399
|
-
y: midY - 10,
|
|
1400
|
-
align: 'middle',
|
|
1401
|
-
side: 'above'
|
|
1402
|
-
};
|
|
1403
|
-
}
|
|
1404
|
-
return {
|
|
1405
|
-
x: midX + 10,
|
|
1406
|
-
y: midY,
|
|
1407
|
-
align: 'start',
|
|
1408
|
-
side: 'right'
|
|
1409
|
-
};
|
|
1410
|
-
}
|
|
1411
|
-
function computeBadgePlacement(points, edge) {
|
|
1412
|
-
if (!edge.badgeIcon && !edge.badgeLabel?.length) return null;
|
|
1413
|
-
const anchor = pointAtPathFraction(points, edge.badgeT ?? 0.5);
|
|
1414
|
-
if (!anchor) return null;
|
|
1415
|
-
const side = edge.badgeSide && edge.badgeSide !== 'auto' ? edge.badgeSide : 'above';
|
|
1416
|
-
const offset = edge.badgeOffset ?? 22;
|
|
1417
|
-
const dx = edge.badgeDx ?? 0;
|
|
1418
|
-
const dy = edge.badgeDy ?? 0;
|
|
1419
|
-
if (side === 'above') {
|
|
1420
|
-
return {
|
|
1421
|
-
x: anchor.x + dx,
|
|
1422
|
-
y: anchor.y - offset + dy,
|
|
1423
|
-
side
|
|
1424
|
-
};
|
|
1425
|
-
}
|
|
1426
|
-
if (side === 'below') {
|
|
1427
|
-
return {
|
|
1428
|
-
x: anchor.x + dx,
|
|
1429
|
-
y: anchor.y + offset + dy,
|
|
1430
|
-
side
|
|
1431
|
-
};
|
|
1432
|
-
}
|
|
1433
|
-
if (side === 'left') {
|
|
1434
|
-
return {
|
|
1435
|
-
x: anchor.x - offset + dx,
|
|
1436
|
-
y: anchor.y + dy,
|
|
1437
|
-
side
|
|
1438
|
-
};
|
|
1439
|
-
}
|
|
1440
|
-
return {
|
|
1441
|
-
x: anchor.x + offset + dx,
|
|
1442
|
-
y: anchor.y + dy,
|
|
1443
|
-
side
|
|
1444
|
-
};
|
|
1445
|
-
}
|
|
1446
|
-
function pointAtPathFraction(points, fraction) {
|
|
1447
|
-
return pathAnchorAtFraction(points, fraction)?.point ?? null;
|
|
1448
|
-
}
|
|
1449
|
-
function pathAnchorAtFraction(points, fraction) {
|
|
1450
|
-
const first = points[0];
|
|
1451
|
-
const last = points.at(-1);
|
|
1452
|
-
const beforeLast = points.at(-2);
|
|
1453
|
-
if (!first || !last) return null;
|
|
1454
|
-
if (!beforeLast) {
|
|
1455
|
-
return {
|
|
1456
|
-
point: first,
|
|
1457
|
-
from: first,
|
|
1458
|
-
to: first
|
|
1459
|
-
};
|
|
1460
|
-
}
|
|
1461
|
-
const clamped = Math.max(0, Math.min(1, fraction));
|
|
1462
|
-
const segments = [];
|
|
1463
|
-
let from = first;
|
|
1464
|
-
for (const to of points.slice(1)) {
|
|
1465
|
-
segments.push({
|
|
1466
|
-
from,
|
|
1467
|
-
to,
|
|
1468
|
-
length: Math.hypot(to.x - from.x, to.y - from.y)
|
|
1469
|
-
});
|
|
1470
|
-
from = to;
|
|
1471
|
-
}
|
|
1472
|
-
const total = segments.reduce((sum, segment) => sum + segment.length, 0);
|
|
1473
|
-
if (total === 0) {
|
|
1474
|
-
return {
|
|
1475
|
-
point: first,
|
|
1476
|
-
from: first,
|
|
1477
|
-
to: first
|
|
1478
|
-
};
|
|
1479
|
-
}
|
|
1480
|
-
let target = total * clamped;
|
|
1481
|
-
for (const segment of segments) {
|
|
1482
|
-
if (target > segment.length) {
|
|
1483
|
-
target -= segment.length;
|
|
1484
|
-
continue;
|
|
1485
|
-
}
|
|
1486
|
-
const ratio = segment.length === 0 ? 0 : target / segment.length;
|
|
1487
|
-
return {
|
|
1488
|
-
point: {
|
|
1489
|
-
x: segment.from.x + (segment.to.x - segment.from.x) * ratio,
|
|
1490
|
-
y: segment.from.y + (segment.to.y - segment.from.y) * ratio
|
|
1491
|
-
},
|
|
1492
|
-
from: segment.from,
|
|
1493
|
-
to: segment.to
|
|
1494
|
-
};
|
|
1495
|
-
}
|
|
1496
|
-
return {
|
|
1497
|
-
point: last,
|
|
1498
|
-
from: beforeLast,
|
|
1499
|
-
to: last
|
|
1500
|
-
};
|
|
1501
|
-
}
|
|
1502
|
-
function edgeLabelPlacementFromHint(midX, midY, edge) {
|
|
1503
|
-
const side = edge.labelSide;
|
|
1504
|
-
if (!side || side === 'auto') {
|
|
1505
|
-
throw new Error('Diagram edge label side must be resolved before use.');
|
|
1506
|
-
}
|
|
1507
|
-
const offset = edge.labelOffset ?? 10;
|
|
1508
|
-
const dx = edge.labelDx ?? 0;
|
|
1509
|
-
const dy = edge.labelDy ?? 0;
|
|
1510
|
-
const align = edge.labelAlign ?? (side === 'left' ? 'end' : side === 'right' ? 'start' : 'middle');
|
|
1511
|
-
if (side === 'above') {
|
|
1512
|
-
return {
|
|
1513
|
-
x: midX + dx,
|
|
1514
|
-
y: midY - offset + dy,
|
|
1515
|
-
align,
|
|
1516
|
-
side
|
|
1517
|
-
};
|
|
1518
|
-
}
|
|
1519
|
-
if (side === 'below') {
|
|
1520
|
-
return {
|
|
1521
|
-
x: midX + dx,
|
|
1522
|
-
y: midY + offset + dy,
|
|
1523
|
-
align,
|
|
1524
|
-
side
|
|
1525
|
-
};
|
|
1526
|
-
}
|
|
1527
|
-
if (side === 'left') {
|
|
1528
|
-
return {
|
|
1529
|
-
x: midX - offset + dx,
|
|
1530
|
-
y: midY + dy,
|
|
1531
|
-
align,
|
|
1532
|
-
side
|
|
1533
|
-
};
|
|
1534
|
-
}
|
|
1535
|
-
return {
|
|
1536
|
-
x: midX + offset + dx,
|
|
1537
|
-
y: midY + dy,
|
|
1538
|
-
align,
|
|
1539
|
-
side
|
|
1540
|
-
};
|
|
1541
|
-
}
|
|
1542
|
-
function deconflictLabelPlacements(routedEdges) {
|
|
1543
|
-
const occupied = [];
|
|
1544
|
-
return routedEdges.map(routedEdge => {
|
|
1545
|
-
const placement = routedEdge.labelPlacement;
|
|
1546
|
-
const label = routedEdge.edge.label;
|
|
1547
|
-
if (!placement || !label?.length) return routedEdge;
|
|
1548
|
-
const nextPlacement = placeLabel(placement, routedEdge.edge, occupied);
|
|
1549
|
-
occupied.push(edgeLabelBounds(routedEdge.edge, nextPlacement));
|
|
1550
|
-
if (nextPlacement === placement) return routedEdge;
|
|
1551
|
-
return {
|
|
1552
|
-
...routedEdge,
|
|
1553
|
-
labelPlacement: nextPlacement
|
|
1554
|
-
};
|
|
1555
|
-
});
|
|
1556
|
-
}
|
|
1557
|
-
function placeLabel(placement, edge, occupied) {
|
|
1558
|
-
const maxAttempts = Math.max(12, occupied.length * 2 + 8);
|
|
1559
|
-
for (const offset of labelOffsetCandidates(placement, edge, maxAttempts)) {
|
|
1560
|
-
const candidate = offsetLabelPlacement(placement, offset);
|
|
1561
|
-
const bounds = edgeLabelBounds(edge, candidate);
|
|
1562
|
-
if (!occupied.some(other => labelBoundsOverlap(bounds, other))) {
|
|
1563
|
-
return candidate;
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
throw new Error('Diagram edge labels could not be placed without overlap. Add waypoints or split the diagram.');
|
|
1567
|
-
}
|
|
1568
|
-
function labelOffsetCandidates(placement, edge, maxAttempts) {
|
|
1569
|
-
const size = edgeLabelSize(edge);
|
|
1570
|
-
const step = placement.side === 'above' || placement.side === 'below' ? size.h + EDGE_LABEL_GAP : size.w + EDGE_LABEL_GAP;
|
|
1571
|
-
const candidates = [{
|
|
1572
|
-
x: 0,
|
|
1573
|
-
y: 0
|
|
1574
|
-
}];
|
|
1575
|
-
for (let i = 1; candidates.length < maxAttempts; i++) {
|
|
1576
|
-
const primary = i * step;
|
|
1577
|
-
if (placement.side === 'above') {
|
|
1578
|
-
candidates.push({
|
|
1579
|
-
x: 0,
|
|
1580
|
-
y: -primary
|
|
1581
|
-
}, {
|
|
1582
|
-
x: 0,
|
|
1583
|
-
y: primary
|
|
1584
|
-
});
|
|
1585
|
-
} else if (placement.side === 'below') {
|
|
1586
|
-
candidates.push({
|
|
1587
|
-
x: 0,
|
|
1588
|
-
y: primary
|
|
1589
|
-
}, {
|
|
1590
|
-
x: 0,
|
|
1591
|
-
y: -primary
|
|
1592
|
-
});
|
|
1593
|
-
} else if (placement.side === 'left') {
|
|
1594
|
-
candidates.push({
|
|
1595
|
-
x: -primary,
|
|
1596
|
-
y: 0
|
|
1597
|
-
}, {
|
|
1598
|
-
x: primary,
|
|
1599
|
-
y: 0
|
|
1600
|
-
});
|
|
1601
|
-
} else {
|
|
1602
|
-
candidates.push({
|
|
1603
|
-
x: primary,
|
|
1604
|
-
y: 0
|
|
1605
|
-
}, {
|
|
1606
|
-
x: -primary,
|
|
1607
|
-
y: 0
|
|
1608
|
-
});
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
return candidates.slice(0, maxAttempts);
|
|
1612
|
-
}
|
|
1613
|
-
function offsetLabelPlacement(placement, offset) {
|
|
1614
|
-
if (offset.x === 0 && offset.y === 0) return placement;
|
|
1615
|
-
return {
|
|
1616
|
-
...placement,
|
|
1617
|
-
x: placement.x + offset.x,
|
|
1618
|
-
y: placement.y + offset.y
|
|
1619
|
-
};
|
|
1620
|
-
}
|
|
1621
|
-
export function edgeLabelFontSize(edge) {
|
|
1622
|
-
return edge.labelFontSize ?? EDGE_LABEL_FONT_SIZE;
|
|
1623
|
-
}
|
|
1624
|
-
export function edgeLabelFramePad(edge) {
|
|
1625
|
-
return edge.labelFramePadding ?? EDGE_LABEL_BOX_PAD;
|
|
1626
|
-
}
|
|
1627
|
-
export function edgeLabelSize(edge) {
|
|
1628
|
-
const label = edge.label ?? [];
|
|
1629
|
-
const pad = edgeLabelFramePad(edge);
|
|
1630
|
-
return {
|
|
1631
|
-
w: widestLine(label, edgeLabelFontSize(edge), 'mono') + pad * 2,
|
|
1632
|
-
h: textLineBox(edgeLabelFontSize(edge), 'mono') + Math.max(0, label.length - 1) * edgeLabelLineHeight(edge) + pad * 2
|
|
1633
|
-
};
|
|
1634
|
-
}
|
|
1635
|
-
export function edgeLabelAscent(edge) {
|
|
1636
|
-
return textAscent(edgeLabelFontSize(edge), 'mono') + edgeLabelFramePad(edge);
|
|
1637
|
-
}
|
|
1638
|
-
export function edgeLabelLineHeight(edge) {
|
|
1639
|
-
return edgeLabelFontSize(edge) + (edge.labelLineGap ?? EDGE_LABEL_LINE_GAP);
|
|
1640
|
-
}
|
|
1641
|
-
export function edgeBadgeSize(edge) {
|
|
1642
|
-
const label = edge.badgeLabel ?? [];
|
|
1643
|
-
const labelWidth = widestLine(label, EDGE_BADGE_FONT_SIZE, 'sans');
|
|
1644
|
-
const iconWidth = edge.badgeIcon ? EDGE_BADGE_ICON : 0;
|
|
1645
|
-
const gap = edge.badgeIcon && label.length > 0 ? 8 : 0;
|
|
1646
|
-
return {
|
|
1647
|
-
w: Math.max(24, iconWidth + gap + labelWidth) + 16,
|
|
1648
|
-
h: Math.max(24, label.length * EDGE_LABEL_LINE_H) + 12
|
|
1649
|
-
};
|
|
1650
|
-
}
|
|
1651
|
-
function edgeBadgeBounds(edge, placement) {
|
|
1652
|
-
const {
|
|
1653
|
-
w,
|
|
1654
|
-
h
|
|
1655
|
-
} = edgeBadgeSize(edge);
|
|
1656
|
-
return {
|
|
1657
|
-
x: placement.x - w / 2,
|
|
1658
|
-
y: placement.y - h / 2,
|
|
1659
|
-
w,
|
|
1660
|
-
h
|
|
1661
|
-
};
|
|
1662
|
-
}
|
|
1663
|
-
function edgeLabelBounds(edge, placement) {
|
|
1664
|
-
const box = edgeLabelFrameBox(edge, placement);
|
|
1665
|
-
return {
|
|
1666
|
-
x1: box.x,
|
|
1667
|
-
y1: box.y,
|
|
1668
|
-
x2: box.x + box.w,
|
|
1669
|
-
y2: box.y + box.h
|
|
1670
|
-
};
|
|
1671
|
-
}
|
|
1672
|
-
export function edgeLabelFrameBox(edge, placement) {
|
|
1673
|
-
const {
|
|
1674
|
-
w,
|
|
1675
|
-
h
|
|
1676
|
-
} = edgeLabelSize(edge);
|
|
1677
|
-
const y = placement.y - edgeLabelAscent(edge);
|
|
1678
|
-
if (placement.align === 'middle') return {
|
|
1679
|
-
x: placement.x - w / 2,
|
|
1680
|
-
y,
|
|
1681
|
-
w,
|
|
1682
|
-
h
|
|
1683
|
-
};
|
|
1684
|
-
if (placement.align === 'end') return {
|
|
1685
|
-
x: placement.x - w,
|
|
1686
|
-
y,
|
|
1687
|
-
w,
|
|
1688
|
-
h
|
|
1689
|
-
};
|
|
1690
|
-
return {
|
|
1691
|
-
x: placement.x,
|
|
1692
|
-
y,
|
|
1693
|
-
w,
|
|
1694
|
-
h
|
|
1695
|
-
};
|
|
1696
|
-
}
|
|
1697
|
-
function labelBoundsOverlap(a, b) {
|
|
1698
|
-
return a.x1 < b.x2 && a.x2 > b.x1 && a.y1 < b.y2 && a.y2 > b.y1;
|
|
1699
|
-
}
|
|
1700
|
-
function resolveEdgeWaypoints(waypoints, spec) {
|
|
1701
|
-
const layout = spec.layout;
|
|
1702
|
-
return (waypoints ?? []).map(waypoint => {
|
|
1703
|
-
if ('guide' in waypoint) return resolveGuideWaypoint(waypoint, spec);
|
|
1704
|
-
if ('x' in waypoint && 'y' in waypoint) return waypoint;
|
|
1705
|
-
if ('target' in waypoint) {
|
|
1706
|
-
const point = targetAnchorPoint(waypoint.target, waypoint.anchor ?? 'center', waypoint.gap ?? 0, spec);
|
|
1707
|
-
return {
|
|
1708
|
-
x: snapToGrid(point.x + (waypoint.dx ?? 0)),
|
|
1709
|
-
y: snapToGrid(point.y + (waypoint.dy ?? 0))
|
|
1710
|
-
};
|
|
1711
|
-
}
|
|
1712
|
-
return {
|
|
1713
|
-
x: snapToGrid(graphPadX(layout) + trackPosition('col', waypoint.col, layout) + (waypoint.dx ?? 0)),
|
|
1714
|
-
y: snapToGrid(graphPadY(layout) + trackPosition('row', waypoint.row, layout) + (waypoint.dy ?? 0))
|
|
1715
|
-
};
|
|
1716
|
-
});
|
|
1717
|
-
}
|
|
1718
|
-
function resolveGuideWaypoint(waypoint, spec) {
|
|
1719
|
-
const guide = guideForId(waypoint.guide, spec, 'Diagram.Waypoint');
|
|
1720
|
-
const line = resolveGuideLine(guide, spec, 'Diagram.Waypoint');
|
|
1721
|
-
if (line.orientation === 'vertical') {
|
|
1722
|
-
return {
|
|
1723
|
-
x: snapToGrid(line.x + (waypoint.dx ?? 0)),
|
|
1724
|
-
y: snapToGrid(guideWaypointCrossAxis('y', waypoint, spec) + (waypoint.dy ?? 0))
|
|
1725
|
-
};
|
|
1726
|
-
}
|
|
1727
|
-
return {
|
|
1728
|
-
x: snapToGrid(guideWaypointCrossAxis('x', waypoint, spec) + (waypoint.dx ?? 0)),
|
|
1729
|
-
y: snapToGrid(line.y + (waypoint.dy ?? 0))
|
|
1730
|
-
};
|
|
1731
|
-
}
|
|
1732
|
-
function guideForId(guideId, spec, consumer) {
|
|
1733
|
-
const matches = (spec.guides ?? []).filter(candidate => candidate.id === guideId);
|
|
1734
|
-
const [match, ...duplicates] = matches;
|
|
1735
|
-
if (!match) {
|
|
1736
|
-
throw new Error(`${consumer} guide "${guideId}" does not match a Diagram.Guide id.`);
|
|
1737
|
-
}
|
|
1738
|
-
if (duplicates.length > 0) {
|
|
1739
|
-
throw new Error(`Duplicate Diagram guide id "${guideId}".`);
|
|
1740
|
-
}
|
|
1741
|
-
return match;
|
|
1742
|
-
}
|
|
1743
|
-
function resolveGuideLine(guide, spec, consumer) {
|
|
1744
|
-
const orientation = guide.orientation;
|
|
1745
|
-
if (!orientation) {
|
|
1746
|
-
throw new Error(`Diagram.Guide "${guide.id}" needs an orientation before ${consumer} can use it.`);
|
|
1747
|
-
}
|
|
1748
|
-
if (orientation === 'vertical') {
|
|
1749
|
-
return {
|
|
1750
|
-
orientation,
|
|
1751
|
-
x: resolveGuideMainAxis(guide, orientation, spec)
|
|
1752
|
-
};
|
|
1753
|
-
}
|
|
1754
|
-
return {
|
|
1755
|
-
orientation,
|
|
1756
|
-
y: resolveGuideMainAxis(guide, orientation, spec)
|
|
1757
|
-
};
|
|
1758
|
-
}
|
|
1759
|
-
function resolveGuideMainAxis(guide, orientation, spec) {
|
|
1760
|
-
if (orientation === 'vertical') {
|
|
1761
|
-
if (guide.x !== undefined) return snapToGrid(guide.x + (guide.dx ?? 0));
|
|
1762
|
-
if (guide.col !== undefined) {
|
|
1763
|
-
return snapToGrid(graphPadX(spec.layout) + trackPosition('col', guide.col, spec.layout) + (guide.dx ?? 0));
|
|
1764
|
-
}
|
|
1765
|
-
if (guide.target) {
|
|
1766
|
-
const point = targetAnchorPoint(guide.target, guide.anchor ?? 'center', guide.gap ?? 0, spec);
|
|
1767
|
-
return snapToGrid(point.x + (guide.dx ?? 0));
|
|
1768
|
-
}
|
|
1769
|
-
throw new Error(`Diagram.Guide "${guide.id}" needs x, col, or target to define a vertical corridor.`);
|
|
1770
|
-
}
|
|
1771
|
-
if (guide.y !== undefined) return snapToGrid(guide.y + (guide.dy ?? 0));
|
|
1772
|
-
if (guide.row !== undefined) {
|
|
1773
|
-
return snapToGrid(graphPadY(spec.layout) + trackPosition('row', guide.row, spec.layout) + (guide.dy ?? 0));
|
|
1774
|
-
}
|
|
1775
|
-
if (guide.target) {
|
|
1776
|
-
const point = targetAnchorPoint(guide.target, guide.anchor ?? 'center', guide.gap ?? 0, spec);
|
|
1777
|
-
return snapToGrid(point.y + (guide.dy ?? 0));
|
|
1778
|
-
}
|
|
1779
|
-
throw new Error(`Diagram.Guide "${guide.id}" needs y, row, or target to define a horizontal corridor.`);
|
|
1780
|
-
}
|
|
1781
|
-
function guideWaypointCrossAxis(axis, waypoint, spec) {
|
|
1782
|
-
if (axis === 'x') {
|
|
1783
|
-
if (waypoint.x !== undefined) return waypoint.x;
|
|
1784
|
-
if (waypoint.col !== undefined) {
|
|
1785
|
-
return graphPadX(spec.layout) + trackPosition('col', waypoint.col, spec.layout);
|
|
1786
|
-
}
|
|
1787
|
-
if (waypoint.target) {
|
|
1788
|
-
return targetAnchorPoint(waypoint.target, waypoint.anchor ?? 'center', waypoint.gap ?? 0, spec).x;
|
|
1789
|
-
}
|
|
1790
|
-
throw new Error(`Diagram.Waypoint guide "${waypoint.guide}" needs target, x, or col to place a point on a horizontal guide.`);
|
|
1791
|
-
}
|
|
1792
|
-
if (waypoint.y !== undefined) return waypoint.y;
|
|
1793
|
-
if (waypoint.row !== undefined) {
|
|
1794
|
-
return graphPadY(spec.layout) + trackPosition('row', waypoint.row, spec.layout);
|
|
1795
|
-
}
|
|
1796
|
-
if (waypoint.target) {
|
|
1797
|
-
return targetAnchorPoint(waypoint.target, waypoint.anchor ?? 'center', waypoint.gap ?? 0, spec).y;
|
|
1798
|
-
}
|
|
1799
|
-
throw new Error(`Diagram.Waypoint guide "${waypoint.guide}" needs target, y, or row to place a point on a vertical guide.`);
|
|
1800
|
-
}
|
|
1801
|
-
function targetAnchorPoint(target, anchor, gap, spec) {
|
|
1802
|
-
return anchorPoint(targetBoxForWaypoint(target, spec), anchor, gap);
|
|
1803
|
-
}
|
|
1804
|
-
function targetBoxForWaypoint(target, spec) {
|
|
1805
|
-
const node = spec.nodes.find(candidate => candidate.id === target);
|
|
1806
|
-
if (node) return nodeBox(node, spec.layout);
|
|
1807
|
-
const group = spec.groups?.find(candidate => candidate.id === target);
|
|
1808
|
-
if (group) return groupBox(group, spec.nodes, spec.layout, spec.groups);
|
|
1809
|
-
throw new Error(`Diagram.Waypoint target "${target}" does not match a node or group.`);
|
|
1810
|
-
}
|
|
1811
|
-
export function routeEdge(edge, source, target, blocked, gridW, gridH, hint, exitOffset = 0, entryOffset = 0, waypoints, entryGap = ENTRY_GAP, routeOffset = 0) {
|
|
1812
|
-
const {
|
|
1813
|
-
exit,
|
|
1814
|
-
entry,
|
|
1815
|
-
exitSide,
|
|
1816
|
-
entrySide
|
|
1817
|
-
} = pickEndpoints(source, target, hint, exitOffset, entryOffset, entryGap);
|
|
1818
|
-
const stubDistance = (edge.stubDistance ?? STUB) + routeOffset;
|
|
1819
|
-
const stubExit = computeStub(exit, exitSide, stubDistance);
|
|
1820
|
-
const stubEntry = computeStub(entry, entrySide, stubDistance);
|
|
1821
|
-
const staggerLabel = placement => {
|
|
1822
|
-
if (!placement) return null;
|
|
1823
|
-
if ((exitSide === 'top' || exitSide === 'bottom') && (placement.side === 'left' || placement.side === 'right')) {
|
|
1824
|
-
return {
|
|
1825
|
-
...placement,
|
|
1826
|
-
y: placement.y + exitOffset
|
|
1827
|
-
};
|
|
1828
|
-
}
|
|
1829
|
-
return placement;
|
|
1830
|
-
};
|
|
1831
|
-
if (edge.route === 'straight') {
|
|
1832
|
-
const points = [exit, entry];
|
|
1833
|
-
return {
|
|
1834
|
-
d: pathToSvg(points),
|
|
1835
|
-
entry,
|
|
1836
|
-
points,
|
|
1837
|
-
labelPlacement: staggerLabel(computeLabelPlacement(points, edge)),
|
|
1838
|
-
badgePlacement: computeBadgePlacement(points, edge)
|
|
1839
|
-
};
|
|
1840
|
-
}
|
|
1841
|
-
if (waypoints?.length) {
|
|
1842
|
-
const points = simplifyPath([exit, stubExit, ...waypoints, stubEntry, entry]);
|
|
1843
|
-
return {
|
|
1844
|
-
d: pathToSvg(points),
|
|
1845
|
-
entry,
|
|
1846
|
-
points,
|
|
1847
|
-
labelPlacement: staggerLabel(computeLabelPlacement(points, edge)),
|
|
1848
|
-
badgePlacement: computeBadgePlacement(points, edge)
|
|
1849
|
-
};
|
|
1850
|
-
}
|
|
1851
|
-
if (tryStraightLine(stubExit, stubEntry, blocked, gridW)) {
|
|
1852
|
-
const points = simplifyPath([exit, stubExit, stubEntry, entry]);
|
|
1853
|
-
return {
|
|
1854
|
-
d: pathToSvg(points),
|
|
1855
|
-
entry,
|
|
1856
|
-
points,
|
|
1857
|
-
labelPlacement: staggerLabel(computeLabelPlacement(points, edge)),
|
|
1858
|
-
badgePlacement: computeBadgePlacement(points, edge)
|
|
1859
|
-
};
|
|
1860
|
-
}
|
|
1861
|
-
const startG = {
|
|
1862
|
-
x: Math.round(stubExit.x / ROUTE_CELL),
|
|
1863
|
-
y: Math.round(stubExit.y / ROUTE_CELL)
|
|
1864
|
-
};
|
|
1865
|
-
const goalG = {
|
|
1866
|
-
x: Math.round(stubEntry.x / ROUTE_CELL),
|
|
1867
|
-
y: Math.round(stubEntry.y / ROUTE_CELL)
|
|
1868
|
-
};
|
|
1869
|
-
const path = aStar(startG, goalG, blocked, gridW, gridH);
|
|
1870
|
-
if (!path) {
|
|
1871
|
-
const fallback = [exit, entry];
|
|
1872
|
-
return {
|
|
1873
|
-
d: pathToSvg(fallback),
|
|
1874
|
-
entry,
|
|
1875
|
-
points: fallback,
|
|
1876
|
-
labelPlacement: staggerLabel(computeLabelPlacement(fallback, edge)),
|
|
1877
|
-
badgePlacement: computeBadgePlacement(fallback, edge)
|
|
1878
|
-
};
|
|
1879
|
-
}
|
|
1880
|
-
const pixelPath = path.map(p => ({
|
|
1881
|
-
x: p.x * ROUTE_CELL,
|
|
1882
|
-
y: p.y * ROUTE_CELL
|
|
1883
|
-
}));
|
|
1884
|
-
const fullPath = [exit, ...pixelPath, entry];
|
|
1885
|
-
const simplified = simplifyPath(fullPath);
|
|
1886
|
-
return {
|
|
1887
|
-
d: pathToSvg(simplified),
|
|
1888
|
-
entry,
|
|
1889
|
-
points: simplified,
|
|
1890
|
-
labelPlacement: staggerLabel(computeLabelPlacement(simplified, edge)),
|
|
1891
|
-
badgePlacement: computeBadgePlacement(simplified, edge)
|
|
1892
|
-
};
|
|
1893
|
-
}
|
|
1894
|
-
export function pointOnBoxSide(box, side, offset = 0) {
|
|
1895
|
-
const center = {
|
|
1896
|
-
x: box.x + box.w / 2,
|
|
1897
|
-
y: box.y + box.h / 2
|
|
1898
|
-
};
|
|
1899
|
-
if (side === 'top') return {
|
|
1900
|
-
x: snapToGrid(center.x + offset),
|
|
1901
|
-
y: box.y
|
|
1902
|
-
};
|
|
1903
|
-
if (side === 'bottom') {
|
|
1904
|
-
return {
|
|
1905
|
-
x: snapToGrid(center.x + offset),
|
|
1906
|
-
y: box.y + box.h
|
|
1907
|
-
};
|
|
1908
|
-
}
|
|
1909
|
-
if (side === 'left') return {
|
|
1910
|
-
x: box.x,
|
|
1911
|
-
y: snapToGrid(center.y + offset)
|
|
1912
|
-
};
|
|
1913
|
-
return {
|
|
1914
|
-
x: box.x + box.w,
|
|
1915
|
-
y: snapToGrid(center.y + offset)
|
|
1916
|
-
};
|
|
1917
|
-
}
|
|
1918
|
-
function pointOutsideBoxSide(box, side, offset = 0, gap = ENTRY_GAP) {
|
|
1919
|
-
const point = pointOnBoxSide(box, side, offset);
|
|
1920
|
-
if (side === 'top') return {
|
|
1921
|
-
...point,
|
|
1922
|
-
y: box.y - gap
|
|
1923
|
-
};
|
|
1924
|
-
if (side === 'bottom') return {
|
|
1925
|
-
...point,
|
|
1926
|
-
y: box.y + box.h + gap
|
|
1927
|
-
};
|
|
1928
|
-
if (side === 'left') return {
|
|
1929
|
-
...point,
|
|
1930
|
-
x: box.x - gap
|
|
1931
|
-
};
|
|
1932
|
-
return {
|
|
1933
|
-
...point,
|
|
1934
|
-
x: box.x + box.w + gap
|
|
1935
|
-
};
|
|
1936
|
-
}
|
|
1937
|
-
function routeSelfLoop(edge, source, side, waypoints, entryGap = ENTRY_GAP) {
|
|
1938
|
-
const axisOffset = side === 'top' || side === 'bottom' ? SELF_LOOP_OFFSET : -SELF_LOOP_OFFSET;
|
|
1939
|
-
const entryAxisOffset = -axisOffset;
|
|
1940
|
-
const exit = pointOnBoxSide(source, side, axisOffset);
|
|
1941
|
-
const entry = pointOutsideBoxSide(source, side, entryAxisOffset, entryGap);
|
|
1942
|
-
let loopPoints;
|
|
1943
|
-
if (waypoints?.length) {
|
|
1944
|
-
loopPoints = [exit, computeStub(exit, side), ...waypoints, entry];
|
|
1945
|
-
} else if (side === 'top') {
|
|
1946
|
-
const y = source.y - SELF_LOOP_DEPTH;
|
|
1947
|
-
loopPoints = [exit, {
|
|
1948
|
-
x: exit.x,
|
|
1949
|
-
y
|
|
1950
|
-
}, {
|
|
1951
|
-
x: entry.x,
|
|
1952
|
-
y
|
|
1953
|
-
}, entry];
|
|
1954
|
-
} else if (side === 'bottom') {
|
|
1955
|
-
const y = source.y + source.h + SELF_LOOP_DEPTH;
|
|
1956
|
-
loopPoints = [exit, {
|
|
1957
|
-
x: exit.x,
|
|
1958
|
-
y
|
|
1959
|
-
}, {
|
|
1960
|
-
x: entry.x,
|
|
1961
|
-
y
|
|
1962
|
-
}, entry];
|
|
1963
|
-
} else if (side === 'left') {
|
|
1964
|
-
const x = source.x - SELF_LOOP_DEPTH;
|
|
1965
|
-
loopPoints = [exit, {
|
|
1966
|
-
x,
|
|
1967
|
-
y: exit.y
|
|
1968
|
-
}, {
|
|
1969
|
-
x,
|
|
1970
|
-
y: entry.y
|
|
1971
|
-
}, entry];
|
|
1972
|
-
} else {
|
|
1973
|
-
const x = source.x + source.w + SELF_LOOP_DEPTH;
|
|
1974
|
-
loopPoints = [exit, {
|
|
1975
|
-
x,
|
|
1976
|
-
y: exit.y
|
|
1977
|
-
}, {
|
|
1978
|
-
x,
|
|
1979
|
-
y: entry.y
|
|
1980
|
-
}, entry];
|
|
1981
|
-
}
|
|
1982
|
-
const points = simplifyPath(loopPoints);
|
|
1983
|
-
return {
|
|
1984
|
-
d: pathToSvg(points),
|
|
1985
|
-
entry,
|
|
1986
|
-
points,
|
|
1987
|
-
labelPlacement: computeLabelPlacement(points, edge),
|
|
1988
|
-
badgePlacement: computeBadgePlacement(points, edge)
|
|
1989
|
-
};
|
|
1990
|
-
}
|
|
1991
|
-
function resolveDiagramSides(edge, source, target, hasReciprocal = false) {
|
|
1992
|
-
if (edge.exitSide && edge.entrySide) {
|
|
1993
|
-
return {
|
|
1994
|
-
exitSide: edge.exitSide,
|
|
1995
|
-
entrySide: edge.entrySide
|
|
1996
|
-
};
|
|
1997
|
-
}
|
|
1998
|
-
const edgeIsUnhinted = !edge.exitSide && !edge.entrySide && !edge.waypoints?.length;
|
|
1999
|
-
if (edgeIsUnhinted && hasReciprocal) {
|
|
2000
|
-
const sourceCenter = {
|
|
2001
|
-
x: source.x + source.w / 2,
|
|
2002
|
-
y: source.y + source.h / 2
|
|
2003
|
-
};
|
|
2004
|
-
const targetCenter = {
|
|
2005
|
-
x: target.x + target.w / 2,
|
|
2006
|
-
y: target.y + target.h / 2
|
|
2007
|
-
};
|
|
2008
|
-
const horizontalDelta = targetCenter.x - sourceCenter.x;
|
|
2009
|
-
const verticalDelta = targetCenter.y - sourceCenter.y;
|
|
2010
|
-
if (Math.abs(horizontalDelta) >= Math.abs(verticalDelta)) {
|
|
2011
|
-
return horizontalDelta < 0 ? {
|
|
2012
|
-
exitSide: 'bottom',
|
|
2013
|
-
entrySide: 'bottom'
|
|
2014
|
-
} : {
|
|
2015
|
-
exitSide: 'right',
|
|
2016
|
-
entrySide: 'left'
|
|
2017
|
-
};
|
|
2018
|
-
}
|
|
2019
|
-
return verticalDelta < 0 ? {
|
|
2020
|
-
exitSide: 'right',
|
|
2021
|
-
entrySide: 'right'
|
|
2022
|
-
} : {
|
|
2023
|
-
exitSide: 'bottom',
|
|
2024
|
-
entrySide: 'top'
|
|
2025
|
-
};
|
|
2026
|
-
}
|
|
2027
|
-
if (edgeIsUnhinted) {
|
|
2028
|
-
const sourceCenter = {
|
|
2029
|
-
x: source.x + source.w / 2,
|
|
2030
|
-
y: source.y + source.h / 2
|
|
2031
|
-
};
|
|
2032
|
-
const targetCenter = {
|
|
2033
|
-
x: target.x + target.w / 2,
|
|
2034
|
-
y: target.y + target.h / 2
|
|
2035
|
-
};
|
|
2036
|
-
const horizontalDelta = targetCenter.x - sourceCenter.x;
|
|
2037
|
-
const verticalDelta = targetCenter.y - sourceCenter.y;
|
|
2038
|
-
if (horizontalDelta < 0 && verticalDelta === 0) {
|
|
2039
|
-
return {
|
|
2040
|
-
exitSide: 'bottom',
|
|
2041
|
-
entrySide: 'bottom'
|
|
2042
|
-
};
|
|
2043
|
-
}
|
|
2044
|
-
if (horizontalDelta > 0 && horizontalDelta >= Math.abs(verticalDelta)) {
|
|
2045
|
-
return {
|
|
2046
|
-
exitSide: 'right',
|
|
2047
|
-
entrySide: 'left'
|
|
2048
|
-
};
|
|
2049
|
-
}
|
|
2050
|
-
}
|
|
2051
|
-
return pickDiagramSidesByGap(source, target);
|
|
2052
|
-
}
|
|
2053
|
-
function hasReciprocalEdge(spec, edge) {
|
|
2054
|
-
if (edge.from === edge.to) return false;
|
|
2055
|
-
return spec.edges.some(candidate => {
|
|
2056
|
-
return parseEndpointRef(candidate.from).nodeId === parseEndpointRef(edge.to).nodeId && parseEndpointRef(candidate.to).nodeId === parseEndpointRef(edge.from).nodeId;
|
|
2057
|
-
});
|
|
2058
|
-
}
|
|
2059
|
-
function computeParallelOffsets(spec, resolvedSides) {
|
|
2060
|
-
const exitOffsets = new Array(spec.edges.length).fill(0);
|
|
2061
|
-
const entryOffsets = new Array(spec.edges.length).fill(0);
|
|
2062
|
-
const routeOffsets = new Array(spec.edges.length).fill(0);
|
|
2063
|
-
const nodesById = new Map(spec.nodes.map(node => [node.id, node]));
|
|
2064
|
-
const nodeBoxes = new Map(spec.nodes.map(node => [node.id, nodeBox(node, spec.layout)]));
|
|
2065
|
-
const groupsById = new Map((spec.groups ?? []).map(group => [group.id, group]));
|
|
2066
|
-
const groupBoxes = new Map((spec.groups ?? []).map(group => [group.id, groupBox(group, spec.nodes, spec.layout, spec.groups)]));
|
|
2067
|
-
const lookup = {
|
|
2068
|
-
nodesById,
|
|
2069
|
-
nodeBoxes,
|
|
2070
|
-
groupsById,
|
|
2071
|
-
groupBoxes
|
|
2072
|
-
};
|
|
2073
|
-
const exitGroups = new Map();
|
|
2074
|
-
const entryGroups = new Map();
|
|
2075
|
-
const sameSideRouteGroups = new Map();
|
|
2076
|
-
const fanOutRouteGroups = new Map();
|
|
2077
|
-
spec.edges.forEach((edge, i) => {
|
|
2078
|
-
const sides = resolvedSides[i];
|
|
2079
|
-
const sourceEndpoint = resolveEndpoint(edge.from, lookup);
|
|
2080
|
-
const targetEndpoint = resolveEndpoint(edge.to, lookup);
|
|
2081
|
-
if (!sides || !sourceEndpoint || !targetEndpoint) return;
|
|
2082
|
-
const exitKey = `${sourceEndpoint.ref}|${sides.exitSide}`;
|
|
2083
|
-
const entryKey = `${targetEndpoint.ref}|${sides.entrySide}`;
|
|
2084
|
-
const exitArr = exitGroups.get(exitKey) ?? [];
|
|
2085
|
-
exitArr.push(i);
|
|
2086
|
-
exitGroups.set(exitKey, exitArr);
|
|
2087
|
-
const entryArr = entryGroups.get(entryKey) ?? [];
|
|
2088
|
-
entryArr.push(i);
|
|
2089
|
-
entryGroups.set(entryKey, entryArr);
|
|
2090
|
-
if (edge.from !== edge.to && !edge.waypoints?.length) {
|
|
2091
|
-
if (sides.exitSide === sides.entrySide) {
|
|
2092
|
-
const routeKey = `${sourceEndpoint.id}|${sides.exitSide}`;
|
|
2093
|
-
const routeArr = sameSideRouteGroups.get(routeKey) ?? [];
|
|
2094
|
-
routeArr.push(i);
|
|
2095
|
-
sameSideRouteGroups.set(routeKey, routeArr);
|
|
2096
|
-
} else {
|
|
2097
|
-
const routeKey = `${sourceEndpoint.id}|${sides.exitSide}|${sides.entrySide}`;
|
|
2098
|
-
const routeArr = fanOutRouteGroups.get(routeKey) ?? [];
|
|
2099
|
-
routeArr.push(i);
|
|
2100
|
-
fanOutRouteGroups.set(routeKey, routeArr);
|
|
2101
|
-
}
|
|
2102
|
-
}
|
|
2103
|
-
});
|
|
2104
|
-
for (const indices of exitGroups.values()) {
|
|
2105
|
-
const n = indices.length;
|
|
2106
|
-
const first = indices[0];
|
|
2107
|
-
if (n <= 1 || first === undefined) continue;
|
|
2108
|
-
const edge = spec.edges[first];
|
|
2109
|
-
const sides = resolvedSides[first];
|
|
2110
|
-
if (!edge || !sides) continue;
|
|
2111
|
-
const source = resolveEndpoint(edge.from, lookup);
|
|
2112
|
-
const offsets = computeSideOffsets(n, source?.box, sides.exitSide, source?.cornerInset);
|
|
2113
|
-
indices.forEach((idx, i) => {
|
|
2114
|
-
exitOffsets[idx] = offsets[i];
|
|
2115
|
-
});
|
|
2116
|
-
}
|
|
2117
|
-
for (const indices of entryGroups.values()) {
|
|
2118
|
-
const n = indices.length;
|
|
2119
|
-
const first = indices[0];
|
|
2120
|
-
if (n <= 1 || first === undefined) continue;
|
|
2121
|
-
const edge = spec.edges[first];
|
|
2122
|
-
const sides = resolvedSides[first];
|
|
2123
|
-
if (!edge || !sides) continue;
|
|
2124
|
-
const target = resolveEndpoint(edge.to, lookup);
|
|
2125
|
-
const offsets = computeSideOffsets(n, target?.box, sides.entrySide, target?.cornerInset);
|
|
2126
|
-
indices.forEach((idx, i) => {
|
|
2127
|
-
entryOffsets[idx] = offsets[i];
|
|
2128
|
-
});
|
|
2129
|
-
}
|
|
2130
|
-
for (const indices of sameSideRouteGroups.values()) {
|
|
2131
|
-
const n = indices.length;
|
|
2132
|
-
if (n <= 1) continue;
|
|
2133
|
-
const sorted = indices.flatMap(idx => {
|
|
2134
|
-
const edge = spec.edges[idx];
|
|
2135
|
-
return edge ? [{
|
|
2136
|
-
idx,
|
|
2137
|
-
span: edgeGridSpan(edge, nodesById)
|
|
2138
|
-
}] : [];
|
|
2139
|
-
}).sort((a, b) => a.span - b.span).map(({
|
|
2140
|
-
idx
|
|
2141
|
-
}) => idx);
|
|
2142
|
-
sorted.forEach((idx, i) => {
|
|
2143
|
-
routeOffsets[idx] = i * PARALLEL_SPACING;
|
|
2144
|
-
});
|
|
2145
|
-
}
|
|
2146
|
-
for (const indices of fanOutRouteGroups.values()) {
|
|
2147
|
-
const routed = indices.flatMap(idx => {
|
|
2148
|
-
const edge = spec.edges[idx];
|
|
2149
|
-
const sides = resolvedSides[idx];
|
|
2150
|
-
return edge && sides ? [{
|
|
2151
|
-
idx,
|
|
2152
|
-
edge,
|
|
2153
|
-
sides
|
|
2154
|
-
}] : [];
|
|
2155
|
-
});
|
|
2156
|
-
const targetIds = new Set(routed.map(({
|
|
2157
|
-
edge
|
|
2158
|
-
}) => edge.to));
|
|
2159
|
-
const n = targetIds.size;
|
|
2160
|
-
if (n <= 1) continue;
|
|
2161
|
-
const sorted = routed.map(({
|
|
2162
|
-
idx,
|
|
2163
|
-
edge,
|
|
2164
|
-
sides
|
|
2165
|
-
}) => ({
|
|
2166
|
-
idx,
|
|
2167
|
-
axis: edgeTargetAxisPosition(edge, lookup, sides.exitSide)
|
|
2168
|
-
})).sort((a, b) => a.axis - b.axis).map(({
|
|
2169
|
-
idx
|
|
2170
|
-
}) => idx);
|
|
2171
|
-
sorted.forEach((idx, i) => {
|
|
2172
|
-
routeOffsets[idx] = i * PARALLEL_SPACING;
|
|
2173
|
-
});
|
|
2174
|
-
}
|
|
2175
|
-
return {
|
|
2176
|
-
exitOffsets,
|
|
2177
|
-
entryOffsets,
|
|
2178
|
-
routeOffsets
|
|
2179
|
-
};
|
|
2180
|
-
}
|
|
2181
|
-
function computeSideOffsets(count, box, side, cornerInset = 0) {
|
|
2182
|
-
const maxIndexOffset = (count - 1) / 2;
|
|
2183
|
-
const sideSpan = side === 'top' || side === 'bottom' ? box?.w : box?.h;
|
|
2184
|
-
const maxOffset = sideSpan === undefined ? maxIndexOffset * PARALLEL_SPACING : Math.max(0, sideSpan / 2 - cornerInset - ROUTE_CELL);
|
|
2185
|
-
const spacing = maxIndexOffset === 0 ? 0 : Math.min(PARALLEL_SPACING, maxOffset / maxIndexOffset);
|
|
2186
|
-
return Array.from({
|
|
2187
|
-
length: count
|
|
2188
|
-
}, (_, i) => {
|
|
2189
|
-
return (i - maxIndexOffset) * spacing;
|
|
2190
|
-
});
|
|
2191
|
-
}
|
|
2192
|
-
function nodeCornerInset(node) {
|
|
2193
|
-
if (node.frame === 'none') return 0;
|
|
2194
|
-
return nodeFrameRadius(node);
|
|
2195
|
-
}
|
|
2196
|
-
function edgeGridSpan(edge, nodesById) {
|
|
2197
|
-
const source = nodesById.get(parseEndpointRef(edge.from).nodeId);
|
|
2198
|
-
const target = nodesById.get(parseEndpointRef(edge.to).nodeId);
|
|
2199
|
-
if (!source || !target) {
|
|
2200
|
-
return 0;
|
|
2201
|
-
}
|
|
2202
|
-
return Math.abs(positionedValue(source, 'col') - positionedValue(target, 'col')) + Math.abs(positionedValue(source, 'row') - positionedValue(target, 'row'));
|
|
2203
|
-
}
|
|
2204
|
-
function edgeTargetAxisPosition(edge, lookup, exitSide) {
|
|
2205
|
-
const target = resolveEndpoint(edge.to, lookup);
|
|
2206
|
-
if (!target) return 0;
|
|
2207
|
-
const box = target.box;
|
|
2208
|
-
if (exitSide === 'top' || exitSide === 'bottom') {
|
|
2209
|
-
return box.x + box.w / 2;
|
|
2210
|
-
}
|
|
2211
|
-
return box.y + box.h / 2;
|
|
2212
|
-
}
|
|
2213
|
-
export function computePaths(spec) {
|
|
2214
|
-
spec = resolveGraphLayout(spec);
|
|
2215
|
-
const {
|
|
2216
|
-
w: svgW,
|
|
2217
|
-
h: svgH
|
|
2218
|
-
} = svgDimensions(spec);
|
|
2219
|
-
const gridW = Math.ceil(svgW / ROUTE_CELL) + 2;
|
|
2220
|
-
const gridH = Math.ceil(svgH / ROUTE_CELL) + 2;
|
|
2221
|
-
const nodesById = new Map();
|
|
2222
|
-
const nodeBoxes = new Map();
|
|
2223
|
-
for (const n of spec.nodes) {
|
|
2224
|
-
nodesById.set(n.id, n);
|
|
2225
|
-
nodeBoxes.set(n.id, nodeBox(n, spec.layout));
|
|
2226
|
-
}
|
|
2227
|
-
const groupsById = new Map((spec.groups ?? []).map(group => [group.id, group]));
|
|
2228
|
-
const groupBoxes = new Map((spec.groups ?? []).map(group => [group.id, groupBox(group, spec.nodes, spec.layout, spec.groups)]));
|
|
2229
|
-
const lookup = {
|
|
2230
|
-
nodesById,
|
|
2231
|
-
nodeBoxes,
|
|
2232
|
-
groupsById,
|
|
2233
|
-
groupBoxes
|
|
2234
|
-
};
|
|
2235
|
-
const blocked = buildObstacles(spec.nodes, gridW, gridH, spec.layout);
|
|
2236
|
-
const resolvedSides = spec.edges.map(edge => {
|
|
2237
|
-
const sourceEndpoint = resolveEndpoint(edge.from, lookup);
|
|
2238
|
-
const targetEndpoint = resolveEndpoint(edge.to, lookup);
|
|
2239
|
-
if (!sourceEndpoint || !targetEndpoint) {
|
|
2240
|
-
return {
|
|
2241
|
-
exitSide: 'right',
|
|
2242
|
-
entrySide: 'left'
|
|
2243
|
-
};
|
|
2244
|
-
}
|
|
2245
|
-
assertConnectableEndpoint(edge, sourceEndpoint, 'source');
|
|
2246
|
-
assertConnectableEndpoint(edge, targetEndpoint, 'target');
|
|
2247
|
-
const sides = resolveDiagramSides(edge, sourceEndpoint.box, targetEndpoint.box, hasReciprocalEdge(spec, edge));
|
|
2248
|
-
return {
|
|
2249
|
-
exitSide: sourceEndpoint?.port?.side ?? sides.exitSide,
|
|
2250
|
-
entrySide: targetEndpoint?.port?.side ?? sides.entrySide
|
|
2251
|
-
};
|
|
2252
|
-
});
|
|
2253
|
-
const {
|
|
2254
|
-
exitOffsets,
|
|
2255
|
-
entryOffsets,
|
|
2256
|
-
routeOffsets
|
|
2257
|
-
} = computeParallelOffsets(spec, resolvedSides);
|
|
2258
|
-
const routedEdges = spec.edges.map((edge, i) => {
|
|
2259
|
-
const sourceEndpoint = resolveEndpoint(edge.from, lookup);
|
|
2260
|
-
const targetEndpoint = resolveEndpoint(edge.to, lookup);
|
|
2261
|
-
const waypoints = resolveEdgeWaypoints(edge.waypoints, spec);
|
|
2262
|
-
const sides = resolvedSides[i];
|
|
2263
|
-
const exitOffset = exitOffsets[i];
|
|
2264
|
-
const entryOffset = entryOffsets[i];
|
|
2265
|
-
if (!sourceEndpoint || !targetEndpoint || !sides || exitOffset === undefined || entryOffset === undefined) {
|
|
2266
|
-
return {
|
|
2267
|
-
edge,
|
|
2268
|
-
exitSide: sides?.exitSide ?? 'right',
|
|
2269
|
-
entrySide: sides?.entrySide ?? 'left',
|
|
2270
|
-
d: '',
|
|
2271
|
-
entry: {
|
|
2272
|
-
x: 0,
|
|
2273
|
-
y: 0
|
|
2274
|
-
},
|
|
2275
|
-
points: [],
|
|
2276
|
-
labelPlacement: null,
|
|
2277
|
-
badgePlacement: null
|
|
2278
|
-
};
|
|
2279
|
-
}
|
|
2280
|
-
assertConnectableEndpoint(edge, sourceEndpoint, 'source');
|
|
2281
|
-
assertConnectableEndpoint(edge, targetEndpoint, 'target');
|
|
2282
|
-
if (edge.from === edge.to) {
|
|
2283
|
-
const side = edge.exitSide ?? edge.entrySide ?? 'top';
|
|
2284
|
-
const result = routeSelfLoop(edge, sourceEndpoint.box, side, waypoints, edge.entryGap);
|
|
2285
|
-
reserveRoutedEdge(blocked, result.points, gridW, gridH);
|
|
2286
|
-
return {
|
|
2287
|
-
edge,
|
|
2288
|
-
exitSide: side,
|
|
2289
|
-
entrySide: side,
|
|
2290
|
-
...result
|
|
2291
|
-
};
|
|
2292
|
-
}
|
|
2293
|
-
const result = routeEdge(edge, sourceEndpoint.box, targetEndpoint.box, blocked, gridW, gridH, {
|
|
2294
|
-
exitSide: sides.exitSide,
|
|
2295
|
-
entrySide: sides.entrySide
|
|
2296
|
-
}, (sourceEndpoint?.port?.offset ?? 0) + (edge.exitOffset ?? exitOffset), (targetEndpoint?.port?.offset ?? 0) + (edge.entryOffset ?? entryOffset), waypoints, edge.entryGap, routeOffsets[i]);
|
|
2297
|
-
reserveRoutedEdge(blocked, result.points, gridW, gridH);
|
|
2298
|
-
return {
|
|
2299
|
-
edge,
|
|
2300
|
-
...sides,
|
|
2301
|
-
...result
|
|
2302
|
-
};
|
|
2303
|
-
});
|
|
2304
|
-
return deconflictLabelPlacements(routedEdges);
|
|
2305
|
-
}
|
|
2306
|
-
function resolveEndpoint(ref, lookup) {
|
|
2307
|
-
const {
|
|
2308
|
-
nodeId: id,
|
|
2309
|
-
portId
|
|
2310
|
-
} = parseEndpointRef(ref);
|
|
2311
|
-
const node = lookup.nodesById.get(id);
|
|
2312
|
-
const group = lookup.groupsById.get(id);
|
|
2313
|
-
const box = node ? lookup.nodeBoxes.get(id) : lookup.groupBoxes.get(id);
|
|
2314
|
-
const cornerInset = node ? nodeCornerInset(node) : group && groupCornerInset(group);
|
|
2315
|
-
if (cornerInset === undefined || !box) {
|
|
2316
|
-
return null;
|
|
2317
|
-
}
|
|
2318
|
-
const ports = node?.ports ?? group?.ports;
|
|
2319
|
-
const port = portId ? ports?.find(candidate => candidate.id === portId) : undefined;
|
|
2320
|
-
if (portId && !port) {
|
|
2321
|
-
throw new Error(`Diagram endpoint "${ref}" references missing port "${portId}" on "${id}".`);
|
|
2322
|
-
}
|
|
2323
|
-
return {
|
|
2324
|
-
ref: port ? `${id}:${port.id}` : id,
|
|
2325
|
-
id,
|
|
2326
|
-
kind: node ? 'node' : 'group',
|
|
2327
|
-
box,
|
|
2328
|
-
cornerInset,
|
|
2329
|
-
connectable: node ? node.connectable !== false : true,
|
|
2330
|
-
...(port ? {
|
|
2331
|
-
port
|
|
2332
|
-
} : {})
|
|
2333
|
-
};
|
|
2334
|
-
}
|
|
2335
|
-
function assertConnectableEndpoint(edge, endpoint, role) {
|
|
2336
|
-
if (endpoint.connectable) return;
|
|
2337
|
-
const relation = role === 'source' ? 'start from' : 'end at';
|
|
2338
|
-
throw new Error(`Diagram edge ${edge.from}->${edge.to} cannot ${relation} non-connectable node "${endpoint.id}". Use Diagram.Label, Diagram.Port, Diagram.Box, or Diagram.Phantom for routed endpoints; Diagram.Float is display-only.`);
|
|
2339
|
-
}
|
|
2340
|
-
//# sourceMappingURL=graph-layout.js.map
|