@oml/markdown 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/out/index.d.ts +2 -0
- package/out/index.js +4 -0
- package/out/index.js.map +1 -0
- package/out/md/index.d.ts +6 -0
- package/out/md/index.js +8 -0
- package/out/md/index.js.map +1 -0
- package/out/md/md-execution.d.ts +33 -0
- package/out/md/md-execution.js +3 -0
- package/out/md/md-execution.js.map +1 -0
- package/out/md/md-executor.d.ts +21 -0
- package/out/md/md-executor.js +498 -0
- package/out/md/md-executor.js.map +1 -0
- package/out/md/md-frontmatter.d.ts +4 -0
- package/out/md/md-frontmatter.js +48 -0
- package/out/md/md-frontmatter.js.map +1 -0
- package/out/md/md-registry.d.ts +7 -0
- package/out/md/md-registry.js +19 -0
- package/out/md/md-registry.js.map +1 -0
- package/out/md/md-runtime.d.ts +10 -0
- package/out/md/md-runtime.js +166 -0
- package/out/md/md-runtime.js.map +1 -0
- package/out/md/md-types.d.ts +40 -0
- package/out/md/md-types.js +3 -0
- package/out/md/md-types.js.map +1 -0
- package/out/md/md-yaml.d.ts +1 -0
- package/out/md/md-yaml.js +15 -0
- package/out/md/md-yaml.js.map +1 -0
- package/out/renderers/chart-renderer.d.ts +6 -0
- package/out/renderers/chart-renderer.js +392 -0
- package/out/renderers/chart-renderer.js.map +1 -0
- package/out/renderers/diagram-renderer.d.ts +7 -0
- package/out/renderers/diagram-renderer.js +2354 -0
- package/out/renderers/diagram-renderer.js.map +1 -0
- package/out/renderers/graph-renderer.d.ts +6 -0
- package/out/renderers/graph-renderer.js +1384 -0
- package/out/renderers/graph-renderer.js.map +1 -0
- package/out/renderers/index.d.ts +14 -0
- package/out/renderers/index.js +16 -0
- package/out/renderers/index.js.map +1 -0
- package/out/renderers/list-renderer.d.ts +6 -0
- package/out/renderers/list-renderer.js +252 -0
- package/out/renderers/list-renderer.js.map +1 -0
- package/out/renderers/matrix-renderer.d.ts +14 -0
- package/out/renderers/matrix-renderer.js +498 -0
- package/out/renderers/matrix-renderer.js.map +1 -0
- package/out/renderers/message-renderer.d.ts +6 -0
- package/out/renderers/message-renderer.js +14 -0
- package/out/renderers/message-renderer.js.map +1 -0
- package/out/renderers/registry.d.ts +9 -0
- package/out/renderers/registry.js +41 -0
- package/out/renderers/registry.js.map +1 -0
- package/out/renderers/renderer.d.ts +28 -0
- package/out/renderers/renderer.js +61 -0
- package/out/renderers/renderer.js.map +1 -0
- package/out/renderers/table-editor-renderer.d.ts +4 -0
- package/out/renderers/table-editor-renderer.js +9 -0
- package/out/renderers/table-editor-renderer.js.map +1 -0
- package/out/renderers/table-renderer.d.ts +95 -0
- package/out/renderers/table-renderer.js +1571 -0
- package/out/renderers/table-renderer.js.map +1 -0
- package/out/renderers/text-renderer.d.ts +7 -0
- package/out/renderers/text-renderer.js +219 -0
- package/out/renderers/text-renderer.js.map +1 -0
- package/out/renderers/tree-renderer.d.ts +4 -0
- package/out/renderers/tree-renderer.js +9 -0
- package/out/renderers/tree-renderer.js.map +1 -0
- package/out/renderers/types.d.ts +18 -0
- package/out/renderers/types.js +3 -0
- package/out/renderers/types.js.map +1 -0
- package/out/renderers/wikilink-utils.d.ts +6 -0
- package/out/renderers/wikilink-utils.js +100 -0
- package/out/renderers/wikilink-utils.js.map +1 -0
- package/out/static/browser-runtime.bundle.js +74155 -0
- package/out/static/browser-runtime.bundle.js.map +7 -0
- package/out/static/browser-runtime.d.ts +1 -0
- package/out/static/browser-runtime.js +218 -0
- package/out/static/browser-runtime.js.map +1 -0
- package/out/static/index.d.ts +1 -0
- package/out/static/index.js +3 -0
- package/out/static/index.js.map +1 -0
- package/out/static/runtime-assets.d.ts +2 -0
- package/out/static/runtime-assets.js +174 -0
- package/out/static/runtime-assets.js.map +1 -0
- package/package.json +74 -0
- package/src/index.ts +4 -0
- package/src/md/index.ts +8 -0
- package/src/md/md-execution.ts +51 -0
- package/src/md/md-executor.ts +598 -0
- package/src/md/md-frontmatter.ts +53 -0
- package/src/md/md-registry.ts +22 -0
- package/src/md/md-runtime.ts +191 -0
- package/src/md/md-types.ts +48 -0
- package/src/md/md-yaml.ts +17 -0
- package/src/renderers/chart-renderer.ts +473 -0
- package/src/renderers/diagram-renderer.ts +2520 -0
- package/src/renderers/graph-renderer.ts +1653 -0
- package/src/renderers/index.ts +16 -0
- package/src/renderers/list-renderer.ts +289 -0
- package/src/renderers/matrix-renderer.ts +616 -0
- package/src/renderers/message-renderer.ts +18 -0
- package/src/renderers/registry.ts +45 -0
- package/src/renderers/renderer.ts +84 -0
- package/src/renderers/table-editor-renderer.ts +8 -0
- package/src/renderers/table-renderer.ts +1868 -0
- package/src/renderers/text-renderer.ts +252 -0
- package/src/renderers/tree-renderer.ts +7 -0
- package/src/renderers/types.ts +22 -0
- package/src/renderers/wikilink-utils.ts +108 -0
- package/src/static/browser-runtime.ts +249 -0
- package/src/static/index.ts +3 -0
- package/src/static/runtime-assets.ts +175 -0
|
@@ -0,0 +1,2354 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
import 'reflect-metadata';
|
|
3
|
+
import { CanvasMarkdownBlockRenderer } from './renderer.js';
|
|
4
|
+
const D = 'http://opencaesar.io/oml/diagram#';
|
|
5
|
+
const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
6
|
+
const TYPE_IRIS = {
|
|
7
|
+
Node: `${D}Node`,
|
|
8
|
+
Edge: `${D}Edge`,
|
|
9
|
+
Port: `${D}Port`,
|
|
10
|
+
Compartment: `${D}Compartment`,
|
|
11
|
+
};
|
|
12
|
+
const LIST_ITEM_IRI = `${D}ListItem`;
|
|
13
|
+
const CSS_EDITOR_FOREGROUND = 'var(--vscode-editor-foreground, var(--oml-static-foreground, #24292f))';
|
|
14
|
+
const CSS_EDITOR_BACKGROUND = 'var(--vscode-editor-background, transparent)';
|
|
15
|
+
const CSS_CANVAS_BACKGROUND = 'var(--vscode-editor-background, var(--oml-static-background, #ffffff))';
|
|
16
|
+
const CSS_FOCUS_BORDER = 'var(--vscode-focusBorder, var(--oml-static-link, #0969da))';
|
|
17
|
+
let diagramCanvasSeq = 0;
|
|
18
|
+
let x6GraphCtor;
|
|
19
|
+
let dagreLib;
|
|
20
|
+
export class DiagramMarkdownBlockRenderer extends CanvasMarkdownBlockRenderer {
|
|
21
|
+
constructor() {
|
|
22
|
+
super(...arguments);
|
|
23
|
+
this.canvasKinds = ['diagram'];
|
|
24
|
+
}
|
|
25
|
+
render(result) {
|
|
26
|
+
const container = this.createResultContainer(result.status);
|
|
27
|
+
const payload = result.payload;
|
|
28
|
+
if (!payload) {
|
|
29
|
+
container.appendChild(this.createMessageContainer('Diagram renderer requires tabular payload.'));
|
|
30
|
+
return container;
|
|
31
|
+
}
|
|
32
|
+
const subjectIndex = payload.columns.indexOf('subject');
|
|
33
|
+
const predicateIndex = payload.columns.indexOf('predicate');
|
|
34
|
+
const objectIndex = payload.columns.indexOf('object');
|
|
35
|
+
if (subjectIndex < 0 || predicateIndex < 0 || objectIndex < 0) {
|
|
36
|
+
container.appendChild(this.createMessageContainer("Diagram renderer requires columns named 'subject', 'predicate', and 'object'."));
|
|
37
|
+
return container;
|
|
38
|
+
}
|
|
39
|
+
const rows = payload.rows.map((row) => ({
|
|
40
|
+
s: row[subjectIndex] ?? '',
|
|
41
|
+
p: row[predicateIndex] ?? '',
|
|
42
|
+
o: row[objectIndex] ?? '',
|
|
43
|
+
})).filter((row) => row.s && row.p && row.o);
|
|
44
|
+
if (rows.length === 0) {
|
|
45
|
+
container.appendChild(this.createMessageContainer('No diagram triples returned.'));
|
|
46
|
+
return container;
|
|
47
|
+
}
|
|
48
|
+
const canvas = document.createElement('div');
|
|
49
|
+
canvas.className = 'graph-canvas-root diagram-canvas-root';
|
|
50
|
+
canvas.style.setProperty('--oml-diagram-height', resolveCanvasHeight(result.options));
|
|
51
|
+
canvas.style.setProperty('--oml-diagram-min-height', resolveCanvasMinHeight(result.options));
|
|
52
|
+
const baseId = `md-diagram-${diagramCanvasSeq++}`;
|
|
53
|
+
canvas.id = baseId;
|
|
54
|
+
container.appendChild(canvas);
|
|
55
|
+
const tripleIndex = indexTriples(rows);
|
|
56
|
+
const stylesheet = parseDiagramStylesheet(result.options);
|
|
57
|
+
const compiled = compileDiagramGraph(tripleIndex, stylesheet);
|
|
58
|
+
const layoutOptions = resolveDagreLayoutOptions(result.options);
|
|
59
|
+
if (compiled.nodes.length === 0) {
|
|
60
|
+
container.appendChild(this.createMessageContainer('No diagram nodes were inferred from the diagram namespace triples.'));
|
|
61
|
+
return container;
|
|
62
|
+
}
|
|
63
|
+
void renderWithX6(canvas, baseId, compiled, layoutOptions, {
|
|
64
|
+
downloadSvg: (content) => this.requestTextFileDownload(content, 'diagram', 'svg'),
|
|
65
|
+
}).catch((error) => {
|
|
66
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
67
|
+
container.appendChild(this.createMessageContainer(`Diagram rendering failed: ${detail}`));
|
|
68
|
+
});
|
|
69
|
+
return container;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
73
|
+
await waitForCanvasReady(canvas);
|
|
74
|
+
const liveCanvas = getLiveCanvas(baseId);
|
|
75
|
+
if (!liveCanvas) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const GraphCtor = await loadX6GraphCtor();
|
|
79
|
+
const layout = await layoutGraphDagre(graph, layoutOptions);
|
|
80
|
+
const minHeight = asFiniteNumber(parseCssPixels(liveCanvas.style.getPropertyValue('--oml-diagram-min-height')), numericCanvasMinHeight(undefined));
|
|
81
|
+
const desiredHeight = Math.max(minHeight, Math.ceil(layout.contentHeight + 12));
|
|
82
|
+
setLockedCanvasHeight(liveCanvas, desiredHeight, minHeight);
|
|
83
|
+
let activePortDrag;
|
|
84
|
+
let isResizingCanvas = false;
|
|
85
|
+
const isPortPointerTarget = (event) => {
|
|
86
|
+
const target = event?.target;
|
|
87
|
+
if (target?.closest('.x6-port, .x6-port-body, .oml-port-body')) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
const path = typeof event?.composedPath === 'function' ? event.composedPath() : [];
|
|
91
|
+
return path.some((entry) => (entry instanceof Element
|
|
92
|
+
&& (entry.classList.contains('x6-port')
|
|
93
|
+
|| entry.classList.contains('x6-port-body')
|
|
94
|
+
|| entry.classList.contains('oml-port-body'))));
|
|
95
|
+
};
|
|
96
|
+
const findPortIdFromTarget = (target) => {
|
|
97
|
+
if (!(target instanceof Element))
|
|
98
|
+
return undefined;
|
|
99
|
+
const portHost = target.closest('.x6-port');
|
|
100
|
+
if (!portHost)
|
|
101
|
+
return undefined;
|
|
102
|
+
return portHost.getAttribute('port')
|
|
103
|
+
?? portHost.getAttribute('data-port-id')
|
|
104
|
+
?? undefined;
|
|
105
|
+
};
|
|
106
|
+
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
107
|
+
const isCompartmentNode = (node) => {
|
|
108
|
+
const nodeId = String(node?.id ?? '');
|
|
109
|
+
return nodeById.get(nodeId)?.kind === 'Compartment';
|
|
110
|
+
};
|
|
111
|
+
const graphView = new GraphCtor({
|
|
112
|
+
container: liveCanvas,
|
|
113
|
+
autoResize: false,
|
|
114
|
+
grid: false,
|
|
115
|
+
panning: true,
|
|
116
|
+
mousewheel: {
|
|
117
|
+
enabled: true,
|
|
118
|
+
minScale: 0.4,
|
|
119
|
+
maxScale: 2.5,
|
|
120
|
+
factor: 1.1,
|
|
121
|
+
},
|
|
122
|
+
connecting: {
|
|
123
|
+
router: 'normal',
|
|
124
|
+
connector: 'rounded',
|
|
125
|
+
allowBlank: false,
|
|
126
|
+
allowNode: false,
|
|
127
|
+
allowPort: false,
|
|
128
|
+
allowEdge: false,
|
|
129
|
+
allowLoop: false,
|
|
130
|
+
},
|
|
131
|
+
guard: (event) => isPortPointerTarget(event) || isResizingCanvas,
|
|
132
|
+
interacting(cellView, event) {
|
|
133
|
+
const isPortTarget = isPortPointerTarget(event);
|
|
134
|
+
const isCompartment = isCompartmentNode(cellView?.cell);
|
|
135
|
+
return {
|
|
136
|
+
nodeMovable: !isCompartment && !isPortTarget && !activePortDrag,
|
|
137
|
+
edgeMovable: false,
|
|
138
|
+
vertexMovable: false,
|
|
139
|
+
arrowheadMovable: false,
|
|
140
|
+
labelMovable: false,
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
background: {
|
|
144
|
+
color: CSS_CANVAS_BACKGROUND,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
const toPlainRect = (value) => {
|
|
148
|
+
if (!value)
|
|
149
|
+
return undefined;
|
|
150
|
+
const x = Number(value.x);
|
|
151
|
+
const y = Number(value.y);
|
|
152
|
+
const width = Number(value.width);
|
|
153
|
+
const height = Number(value.height);
|
|
154
|
+
if (![x, y, width, height].every(Number.isFinite)) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
return { x, y, width, height };
|
|
158
|
+
};
|
|
159
|
+
const toPlainPoint = (value) => {
|
|
160
|
+
if (!value)
|
|
161
|
+
return undefined;
|
|
162
|
+
const x = Number(value.x);
|
|
163
|
+
const y = Number(value.y);
|
|
164
|
+
if (![x, y].every(Number.isFinite)) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
return { x, y };
|
|
168
|
+
};
|
|
169
|
+
const logResize = (_phase, _details) => { };
|
|
170
|
+
const portsByOwner = new Map();
|
|
171
|
+
for (const node of graph.nodes) {
|
|
172
|
+
if (node.kind !== 'Port' || !node.parentId)
|
|
173
|
+
continue;
|
|
174
|
+
const list = portsByOwner.get(node.parentId) ?? [];
|
|
175
|
+
list.push(node);
|
|
176
|
+
portsByOwner.set(node.parentId, list);
|
|
177
|
+
}
|
|
178
|
+
for (const list of portsByOwner.values()) {
|
|
179
|
+
list.sort((a, b) => a.id.localeCompare(b.id));
|
|
180
|
+
}
|
|
181
|
+
const ownerByPortId = new Map();
|
|
182
|
+
for (const [ownerId, ports] of portsByOwner.entries()) {
|
|
183
|
+
for (const port of ports) {
|
|
184
|
+
ownerByPortId.set(port.id, ownerId);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const ordered = [...graph.nodes]
|
|
188
|
+
.filter((node) => node.kind !== 'Port')
|
|
189
|
+
.sort((a, b) => nodeDepth(a.id, nodeById) - nodeDepth(b.id, nodeById));
|
|
190
|
+
for (const node of ordered) {
|
|
191
|
+
const box = layout.boxes.get(node.id);
|
|
192
|
+
if (!box)
|
|
193
|
+
continue;
|
|
194
|
+
const labelText = node.labels.length > 0 ? node.labels.join('\n') : localName(node.id);
|
|
195
|
+
const resolvedStyle = node.style;
|
|
196
|
+
const resolvedShape = resolveRenderNodeShape(resolvedStyle);
|
|
197
|
+
const x = box.x;
|
|
198
|
+
const y = box.y;
|
|
199
|
+
const ownerPorts = portsByOwner.get(node.id) ?? [];
|
|
200
|
+
const portItems = ownerPorts.map((port, index) => ({
|
|
201
|
+
id: port.id,
|
|
202
|
+
group: 'boundary',
|
|
203
|
+
args: {
|
|
204
|
+
x: box.width,
|
|
205
|
+
y: ((index + 1) * box.height) / (ownerPorts.length + 1),
|
|
206
|
+
},
|
|
207
|
+
attrs: resolvePortAttrs(port.style, port.classes, port.labels[0] ?? 'Port'),
|
|
208
|
+
}));
|
|
209
|
+
const { bodyAttrs, labelAttrs, iconSvgAttrs, iconPathAttrs, imageAttrs } = resolveNodeAttrs(resolvedStyle);
|
|
210
|
+
const iconPathSelectors = iconPathAttrs.map((_, index) => `iconPath${index}`);
|
|
211
|
+
graphView.addNode({
|
|
212
|
+
id: node.id,
|
|
213
|
+
shape: resolvedShape.graphShape,
|
|
214
|
+
markup: [
|
|
215
|
+
{ tagName: resolvedShape.bodyTag, selector: 'body' },
|
|
216
|
+
{
|
|
217
|
+
tagName: 'svg',
|
|
218
|
+
selector: 'iconSvg',
|
|
219
|
+
children: iconPathSelectors.map((selector) => ({
|
|
220
|
+
tagName: 'path',
|
|
221
|
+
selector,
|
|
222
|
+
})),
|
|
223
|
+
},
|
|
224
|
+
{ tagName: 'image', selector: 'icon' },
|
|
225
|
+
{ tagName: 'text', selector: 'label' },
|
|
226
|
+
],
|
|
227
|
+
x,
|
|
228
|
+
y,
|
|
229
|
+
width: box.width,
|
|
230
|
+
height: box.height,
|
|
231
|
+
attrs: {
|
|
232
|
+
body: {
|
|
233
|
+
...resolvedShape.bodyDefaults,
|
|
234
|
+
...bodyAttrs,
|
|
235
|
+
...(resolvedShape.bodyTag === 'rect' ? { rx: 2, ry: 2 } : {}),
|
|
236
|
+
class: node.classes.length > 0 ? node.classes.join(' ') : undefined,
|
|
237
|
+
},
|
|
238
|
+
iconSvg: iconSvgAttrs,
|
|
239
|
+
icon: imageAttrs,
|
|
240
|
+
...Object.fromEntries(iconPathSelectors.map((selector, index) => [selector, iconPathAttrs[index]])),
|
|
241
|
+
label: {
|
|
242
|
+
text: labelText,
|
|
243
|
+
fontFamily: 'var(--vscode-editor-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)',
|
|
244
|
+
fontSize: 12,
|
|
245
|
+
textAnchor: 'middle',
|
|
246
|
+
textVerticalAnchor: node.children.length > 0 ? 'top' : 'middle',
|
|
247
|
+
refX: '50%',
|
|
248
|
+
refX2: 0,
|
|
249
|
+
refY: node.children.length > 0 ? 0 : '50%',
|
|
250
|
+
...labelAttrs,
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
ports: ownerPorts.length > 0 ? {
|
|
254
|
+
groups: {
|
|
255
|
+
boundary: {
|
|
256
|
+
position: { name: 'absolute' },
|
|
257
|
+
markup: [
|
|
258
|
+
{
|
|
259
|
+
tagName: 'rect',
|
|
260
|
+
selector: 'body',
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
tagName: 'image',
|
|
264
|
+
selector: 'icon',
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
tagName: 'text',
|
|
268
|
+
selector: 'label',
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
attrs: {
|
|
272
|
+
body: {
|
|
273
|
+
width: 12,
|
|
274
|
+
height: 12,
|
|
275
|
+
x: -6,
|
|
276
|
+
y: -6,
|
|
277
|
+
class: 'oml-port-body',
|
|
278
|
+
magnet: false,
|
|
279
|
+
stroke: CSS_FOCUS_BORDER,
|
|
280
|
+
strokeWidth: 1,
|
|
281
|
+
fill: CSS_EDITOR_BACKGROUND,
|
|
282
|
+
},
|
|
283
|
+
icon: {
|
|
284
|
+
width: 12,
|
|
285
|
+
height: 12,
|
|
286
|
+
x: -6,
|
|
287
|
+
y: -6,
|
|
288
|
+
preserveAspectRatio: 'xMidYMid meet',
|
|
289
|
+
display: 'none',
|
|
290
|
+
},
|
|
291
|
+
label: {
|
|
292
|
+
fill: CSS_EDITOR_FOREGROUND,
|
|
293
|
+
fontFamily: 'var(--vscode-editor-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)',
|
|
294
|
+
fontSize: 12,
|
|
295
|
+
textAnchor: 'start',
|
|
296
|
+
x: 10,
|
|
297
|
+
dy: '0.9em',
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
label: {
|
|
301
|
+
position: {
|
|
302
|
+
name: 'right',
|
|
303
|
+
args: {
|
|
304
|
+
dx: 4,
|
|
305
|
+
dy: 8,
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
items: portItems,
|
|
312
|
+
} : undefined,
|
|
313
|
+
zIndex: 10,
|
|
314
|
+
data: {
|
|
315
|
+
kind: node.kind,
|
|
316
|
+
ownerId: node.parentId,
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
// Establish explicit embedding after all nodes exist so nested nodes move with parents.
|
|
321
|
+
for (const node of ordered) {
|
|
322
|
+
if (!node.parentId)
|
|
323
|
+
continue;
|
|
324
|
+
const childCell = graphView.getCellById(node.id);
|
|
325
|
+
const parentCell = graphView.getCellById(node.parentId);
|
|
326
|
+
if (!childCell || !parentCell || typeof parentCell.addChild !== 'function')
|
|
327
|
+
continue;
|
|
328
|
+
parentCell.addChild(childCell);
|
|
329
|
+
}
|
|
330
|
+
let isAutoResizingContainers = false;
|
|
331
|
+
const collectModelDescendants = (rootId) => {
|
|
332
|
+
const out = [];
|
|
333
|
+
const stack = [...(nodeById.get(rootId)?.children ?? [])];
|
|
334
|
+
const seen = new Set();
|
|
335
|
+
while (stack.length > 0) {
|
|
336
|
+
const id = stack.pop();
|
|
337
|
+
if (!id || seen.has(id))
|
|
338
|
+
continue;
|
|
339
|
+
seen.add(id);
|
|
340
|
+
const spec = nodeById.get(id);
|
|
341
|
+
if (!spec || spec.kind === 'Port')
|
|
342
|
+
continue;
|
|
343
|
+
out.push(id);
|
|
344
|
+
for (const childId of spec.children) {
|
|
345
|
+
stack.push(childId);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
349
|
+
};
|
|
350
|
+
const toRectBounds = (value) => {
|
|
351
|
+
if (!value)
|
|
352
|
+
return undefined;
|
|
353
|
+
const left = Number(value.x);
|
|
354
|
+
const top = Number(value.y);
|
|
355
|
+
const width = Number(value.width);
|
|
356
|
+
const height = Number(value.height);
|
|
357
|
+
if (![left, top, width, height].every(Number.isFinite)) {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
return { left, top, right: left + width, bottom: top + height };
|
|
361
|
+
};
|
|
362
|
+
const rectsIntersect = (a, b) => {
|
|
363
|
+
const ra = toRectBounds(a);
|
|
364
|
+
const rb = toRectBounds(b);
|
|
365
|
+
if (!ra || !rb)
|
|
366
|
+
return false;
|
|
367
|
+
return ra.left <= rb.right
|
|
368
|
+
&& ra.right >= rb.left
|
|
369
|
+
&& ra.top <= rb.bottom
|
|
370
|
+
&& ra.bottom >= rb.top;
|
|
371
|
+
};
|
|
372
|
+
const findContainingCompartments = (nodeId) => {
|
|
373
|
+
const movedCell = graphView.getCellById(nodeId);
|
|
374
|
+
if (!movedCell || typeof movedCell.getBBox !== 'function')
|
|
375
|
+
return [];
|
|
376
|
+
const movedBBox = movedCell.getBBox();
|
|
377
|
+
const containing = [];
|
|
378
|
+
for (const spec of nodeById.values()) {
|
|
379
|
+
if (spec.kind !== 'Compartment')
|
|
380
|
+
continue;
|
|
381
|
+
const compartment = graphView.getCellById(spec.id);
|
|
382
|
+
if (!compartment || typeof compartment.getBBox !== 'function')
|
|
383
|
+
continue;
|
|
384
|
+
const box = compartment.getBBox();
|
|
385
|
+
if (!rectsIntersect(movedBBox, box))
|
|
386
|
+
continue;
|
|
387
|
+
const area = box.width * box.height;
|
|
388
|
+
containing.push({ id: spec.id, area });
|
|
389
|
+
}
|
|
390
|
+
containing.sort((left, right) => left.area - right.area || left.id.localeCompare(right.id));
|
|
391
|
+
return containing.map((entry) => entry.id);
|
|
392
|
+
};
|
|
393
|
+
const collectVisualChildrenInCompartment = (containerId, containerBBox) => {
|
|
394
|
+
const spec = nodeById.get(containerId);
|
|
395
|
+
if (!spec || spec.kind !== 'Compartment') {
|
|
396
|
+
return [];
|
|
397
|
+
}
|
|
398
|
+
const parentId = spec.parentId;
|
|
399
|
+
if (!parentId) {
|
|
400
|
+
return [];
|
|
401
|
+
}
|
|
402
|
+
const out = [];
|
|
403
|
+
if (!toRectBounds(containerBBox)) {
|
|
404
|
+
return [];
|
|
405
|
+
}
|
|
406
|
+
for (const candidate of nodeById.values()) {
|
|
407
|
+
if (candidate.id === containerId || candidate.kind !== 'Node' || candidate.parentId !== parentId) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const cell = graphView.getCellById(candidate.id);
|
|
411
|
+
if (!cell || typeof cell.getBBox !== 'function')
|
|
412
|
+
continue;
|
|
413
|
+
const bbox = cell.getBBox();
|
|
414
|
+
if (rectsIntersect(bbox, containerBBox)) {
|
|
415
|
+
out.push(candidate.id);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return out;
|
|
419
|
+
};
|
|
420
|
+
const ensureContainerContainsChildren = (containerId, movedNodeId) => {
|
|
421
|
+
const container = graphView.getCellById(containerId);
|
|
422
|
+
const containerSpec = nodeById.get(containerId);
|
|
423
|
+
if (!container || !containerSpec || typeof container.getBBox !== 'function' || typeof container.size !== 'function') {
|
|
424
|
+
logResize('container-skip', {
|
|
425
|
+
containerId,
|
|
426
|
+
hasContainer: !!container,
|
|
427
|
+
hasSpec: !!containerSpec,
|
|
428
|
+
hasGetBBox: typeof container?.getBBox === 'function',
|
|
429
|
+
hasSize: typeof container?.size === 'function',
|
|
430
|
+
});
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const modelChildIds = collectModelDescendants(containerId);
|
|
434
|
+
const runtimeChildren = typeof container.getChildren === 'function'
|
|
435
|
+
? container.getChildren().map((child) => String(child?.id ?? '')).filter((id) => id.length > 0)
|
|
436
|
+
: [];
|
|
437
|
+
const containerBBox = container.getBBox();
|
|
438
|
+
let childIds = [...new Set([...modelChildIds, ...runtimeChildren])]
|
|
439
|
+
.filter((childId) => nodeById.get(childId)?.kind !== 'Port');
|
|
440
|
+
if (movedNodeId && nodeById.get(movedNodeId)?.kind !== 'Port' && !childIds.includes(movedNodeId)) {
|
|
441
|
+
childIds.push(movedNodeId);
|
|
442
|
+
}
|
|
443
|
+
const visualChildren = (childIds.length === 0 && containerSpec.kind === 'Compartment')
|
|
444
|
+
? collectVisualChildrenInCompartment(containerId, containerBBox)
|
|
445
|
+
: [];
|
|
446
|
+
if (visualChildren.length > 0) {
|
|
447
|
+
childIds = [...new Set([...childIds, ...visualChildren])];
|
|
448
|
+
}
|
|
449
|
+
if (childIds.length === 0) {
|
|
450
|
+
logResize('container-no-children', { containerId, modelChildIds, runtimeChildren, visualChildren });
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const containerSize = container.size();
|
|
454
|
+
let minLeft = Number.POSITIVE_INFINITY;
|
|
455
|
+
let minTop = Number.POSITIVE_INFINITY;
|
|
456
|
+
let maxRight = Number.NEGATIVE_INFINITY;
|
|
457
|
+
let maxBottom = Number.NEGATIVE_INFINITY;
|
|
458
|
+
const childCells = [];
|
|
459
|
+
const childDebug = [];
|
|
460
|
+
for (const childId of childIds) {
|
|
461
|
+
const child = graphView.getCellById(childId);
|
|
462
|
+
if (!child || typeof child.getBBox !== 'function')
|
|
463
|
+
continue;
|
|
464
|
+
const absBBox = child.getBBox();
|
|
465
|
+
const relLeft = absBBox.x - containerBBox.x;
|
|
466
|
+
const relTop = absBBox.y - containerBBox.y;
|
|
467
|
+
minLeft = Math.min(minLeft, relLeft);
|
|
468
|
+
minTop = Math.min(minTop, relTop);
|
|
469
|
+
maxRight = Math.max(maxRight, relLeft + absBBox.width);
|
|
470
|
+
maxBottom = Math.max(maxBottom, relTop + absBBox.height);
|
|
471
|
+
childDebug.push({
|
|
472
|
+
id: childId,
|
|
473
|
+
relPos: toPlainPoint({ x: relLeft, y: relTop }),
|
|
474
|
+
childSize: toPlainRect({ x: 0, y: 0, width: absBBox.width, height: absBBox.height }),
|
|
475
|
+
absBBox: toPlainRect(absBBox),
|
|
476
|
+
});
|
|
477
|
+
childCells.push(child);
|
|
478
|
+
}
|
|
479
|
+
if (childCells.length === 0 || !Number.isFinite(minLeft) || !Number.isFinite(minTop) || !Number.isFinite(maxRight) || !Number.isFinite(maxBottom)) {
|
|
480
|
+
logResize('container-no-bounds', {
|
|
481
|
+
containerId,
|
|
482
|
+
childIds,
|
|
483
|
+
childCellCount: childCells.length,
|
|
484
|
+
minLeft,
|
|
485
|
+
minTop,
|
|
486
|
+
maxRight,
|
|
487
|
+
maxBottom,
|
|
488
|
+
childDebug,
|
|
489
|
+
});
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
// When a child overflows into or past the inset (padding) area, shift the
|
|
493
|
+
// container origin (without moving its embedded children, which keep their
|
|
494
|
+
// absolute positions) and expand the size to compensate so the opposite edge
|
|
495
|
+
// is preserved.
|
|
496
|
+
// topPadding is the reserved header/label area; children must stay below it.
|
|
497
|
+
const topPadding = containerSpec.contentTopPadding;
|
|
498
|
+
const shiftX = minLeft < 0 ? minLeft : 0;
|
|
499
|
+
const shiftY = minTop < topPadding ? minTop - topPadding : 0;
|
|
500
|
+
// Dimensions needed relative to the (possibly shifted) new origin.
|
|
501
|
+
const baseWidth = containerSize.width - shiftX;
|
|
502
|
+
const baseHeight = containerSize.height - shiftY;
|
|
503
|
+
const neededWidth = Math.ceil(Math.max(baseWidth, maxRight - shiftX));
|
|
504
|
+
const neededHeight = Math.ceil(Math.max(baseHeight, maxBottom - shiftY));
|
|
505
|
+
logResize('container-eval', {
|
|
506
|
+
containerId,
|
|
507
|
+
modelChildIds,
|
|
508
|
+
runtimeChildren,
|
|
509
|
+
childIds,
|
|
510
|
+
containerBBox: toPlainRect(containerBBox),
|
|
511
|
+
containerSize: toPlainRect({ x: 0, y: 0, ...containerSize }),
|
|
512
|
+
topPadding,
|
|
513
|
+
bounds: { minLeft, minTop, maxRight, maxBottom },
|
|
514
|
+
shift: { shiftX, shiftY },
|
|
515
|
+
childDebug,
|
|
516
|
+
needed: { neededWidth, neededHeight },
|
|
517
|
+
});
|
|
518
|
+
if (shiftX < 0 || shiftY < 0) {
|
|
519
|
+
// Move only the container box (not children) so it encompasses the overflow.
|
|
520
|
+
container.setPosition(containerBBox.x + shiftX, containerBBox.y + shiftY);
|
|
521
|
+
}
|
|
522
|
+
if (neededWidth !== containerSize.width || neededHeight !== containerSize.height) {
|
|
523
|
+
logResize('container-resize', {
|
|
524
|
+
containerId,
|
|
525
|
+
from: containerSize,
|
|
526
|
+
to: { width: neededWidth, height: neededHeight },
|
|
527
|
+
});
|
|
528
|
+
container.resize(neededWidth, neededHeight);
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
logResize('container-no-resize', { containerId });
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
const growAncestorContainers = (node) => {
|
|
535
|
+
if (isAutoResizingContainers) {
|
|
536
|
+
logResize('grow-skip-reentrant', { nodeId: String(node?.id ?? '') });
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const nodeId = String(node?.id ?? '');
|
|
540
|
+
const movedSpec = nodeById.get(nodeId);
|
|
541
|
+
let parentId = movedSpec?.parentId;
|
|
542
|
+
if (!parentId && typeof node?.getParent === 'function') {
|
|
543
|
+
const runtimeParent = node.getParent();
|
|
544
|
+
parentId = runtimeParent ? String(runtimeParent.id ?? '') : undefined;
|
|
545
|
+
}
|
|
546
|
+
if (!parentId) {
|
|
547
|
+
logResize('grow-no-parent', { nodeId, specParentId: movedSpec?.parentId });
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const visualCompartmentIds = findContainingCompartments(nodeId)
|
|
551
|
+
.filter((id) => id !== nodeId);
|
|
552
|
+
logResize('grow-start', { nodeId, parentId, visualCompartmentIds });
|
|
553
|
+
// Set the flag before any ensureContainerContainsChildren call so that
|
|
554
|
+
// position/size changes we apply do not re-enter growAncestorContainers.
|
|
555
|
+
isAutoResizingContainers = true;
|
|
556
|
+
try {
|
|
557
|
+
for (const visualCompartmentId of visualCompartmentIds) {
|
|
558
|
+
logResize('grow-visual-compartment', { nodeId, parentId, visualCompartmentId });
|
|
559
|
+
ensureContainerContainsChildren(visualCompartmentId, nodeId);
|
|
560
|
+
}
|
|
561
|
+
while (parentId) {
|
|
562
|
+
ensureContainerContainsChildren(parentId, nodeId);
|
|
563
|
+
const parentCell = graphView.getCellById(parentId);
|
|
564
|
+
if (parentCell && typeof parentCell.getParent === 'function') {
|
|
565
|
+
const runtimeParent = parentCell.getParent();
|
|
566
|
+
logResize('grow-step', {
|
|
567
|
+
nodeId,
|
|
568
|
+
currentParentId: parentId,
|
|
569
|
+
nextParentId: runtimeParent ? String(runtimeParent.id ?? '') : undefined,
|
|
570
|
+
});
|
|
571
|
+
parentId = runtimeParent ? String(runtimeParent.id ?? '') : undefined;
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
const modelParentId = nodeById.get(parentId)?.parentId;
|
|
575
|
+
logResize('grow-step-fallback', { nodeId, currentParentId: parentId, modelParentId });
|
|
576
|
+
parentId = modelParentId;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
finally {
|
|
581
|
+
isAutoResizingContainers = false;
|
|
582
|
+
logResize('grow-end', { nodeId });
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
const isPrimaryMover = (node, options) => {
|
|
586
|
+
// Reject events fired because an ancestor was translated (co-translated children).
|
|
587
|
+
// options.translateBy holds the id of the original mover; for co-translated
|
|
588
|
+
// children it will differ from node.id.
|
|
589
|
+
if (!options?.translateBy)
|
|
590
|
+
return true;
|
|
591
|
+
return String(options.translateBy) === String(node?.id ?? '');
|
|
592
|
+
};
|
|
593
|
+
graphView.on('node:change:position', ({ node, options }) => {
|
|
594
|
+
logResize('event-change-position', { nodeId: String(node?.id ?? ''), silent: !!options?.silent, translateBy: options?.translateBy });
|
|
595
|
+
if (options?.silent)
|
|
596
|
+
return;
|
|
597
|
+
if (!isPrimaryMover(node, options))
|
|
598
|
+
return;
|
|
599
|
+
growAncestorContainers(node);
|
|
600
|
+
});
|
|
601
|
+
graphView.on('node:moving', ({ node }) => {
|
|
602
|
+
logResize('event-moving', { nodeId: String(node?.id ?? '') });
|
|
603
|
+
growAncestorContainers(node);
|
|
604
|
+
});
|
|
605
|
+
graphView.on('node:moved', ({ node, options }) => {
|
|
606
|
+
logResize('event-moved', { nodeId: String(node?.id ?? ''), silent: !!options?.silent, translateBy: options?.translateBy });
|
|
607
|
+
if (options?.silent)
|
|
608
|
+
return;
|
|
609
|
+
if (!isPrimaryMover(node, options))
|
|
610
|
+
return;
|
|
611
|
+
growAncestorContainers(node);
|
|
612
|
+
});
|
|
613
|
+
const edgeLabelPosition = (placement) => {
|
|
614
|
+
if (placement === 'begin')
|
|
615
|
+
return 0.15;
|
|
616
|
+
if (placement === 'end')
|
|
617
|
+
return 0.85;
|
|
618
|
+
return 0.5;
|
|
619
|
+
};
|
|
620
|
+
const endpointOwnerId = (nodeId) => {
|
|
621
|
+
const node = nodeById.get(nodeId);
|
|
622
|
+
if (!node)
|
|
623
|
+
return undefined;
|
|
624
|
+
return node.kind === 'Port' ? node.parentId : node.id;
|
|
625
|
+
};
|
|
626
|
+
const undirectedPairKey = (a, b) => (a < b ? `${a}<->${b}` : `${b}<->${a}`);
|
|
627
|
+
const edgeById = new Map(graph.edges.map((edge) => [edge.id, edge]));
|
|
628
|
+
const undirectedEdgeIdsByPair = new Map();
|
|
629
|
+
for (const edge of graph.edges) {
|
|
630
|
+
const sourceOwner = endpointOwnerId(edge.sourceId);
|
|
631
|
+
const targetOwner = endpointOwnerId(edge.targetId);
|
|
632
|
+
if (!sourceOwner || !targetOwner || sourceOwner === targetOwner)
|
|
633
|
+
continue;
|
|
634
|
+
const key = undirectedPairKey(sourceOwner, targetOwner);
|
|
635
|
+
const ids = undirectedEdgeIdsByPair.get(key) ?? [];
|
|
636
|
+
ids.push(edge.id);
|
|
637
|
+
undirectedEdgeIdsByPair.set(key, ids);
|
|
638
|
+
}
|
|
639
|
+
const fanningVertexForEdge = (edge) => {
|
|
640
|
+
const sourceOwner = endpointOwnerId(edge.sourceId);
|
|
641
|
+
const targetOwner = endpointOwnerId(edge.targetId);
|
|
642
|
+
if (!sourceOwner || !targetOwner || sourceOwner === targetOwner) {
|
|
643
|
+
return undefined;
|
|
644
|
+
}
|
|
645
|
+
const edgeIds = undirectedEdgeIdsByPair.get(undirectedPairKey(sourceOwner, targetOwner)) ?? [];
|
|
646
|
+
if (edgeIds.length <= 1) {
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
649
|
+
const orderedEdgeIds = [...edgeIds].sort((leftId, rightId) => {
|
|
650
|
+
const leftEdge = edgeById.get(leftId);
|
|
651
|
+
const rightEdge = edgeById.get(rightId);
|
|
652
|
+
if (!leftEdge || !rightEdge)
|
|
653
|
+
return leftId.localeCompare(rightId);
|
|
654
|
+
const leftSource = endpointOwnerId(leftEdge.sourceId);
|
|
655
|
+
const leftTarget = endpointOwnerId(leftEdge.targetId);
|
|
656
|
+
const rightSource = endpointOwnerId(rightEdge.sourceId);
|
|
657
|
+
const rightTarget = endpointOwnerId(rightEdge.targetId);
|
|
658
|
+
const leftForward = leftSource && leftTarget ? (leftSource < leftTarget ? 0 : 1) : 0;
|
|
659
|
+
const rightForward = rightSource && rightTarget ? (rightSource < rightTarget ? 0 : 1) : 0;
|
|
660
|
+
if (leftForward !== rightForward)
|
|
661
|
+
return leftForward - rightForward;
|
|
662
|
+
return leftId.localeCompare(rightId);
|
|
663
|
+
});
|
|
664
|
+
const [pairA, pairB] = sourceOwner < targetOwner
|
|
665
|
+
? [sourceOwner, targetOwner]
|
|
666
|
+
: [targetOwner, sourceOwner];
|
|
667
|
+
const forwardIds = [];
|
|
668
|
+
const reverseIds = [];
|
|
669
|
+
for (const edgeId of orderedEdgeIds) {
|
|
670
|
+
const pairEdge = edgeById.get(edgeId);
|
|
671
|
+
if (!pairEdge)
|
|
672
|
+
continue;
|
|
673
|
+
const pairSource = endpointOwnerId(pairEdge.sourceId);
|
|
674
|
+
const pairTarget = endpointOwnerId(pairEdge.targetId);
|
|
675
|
+
if (pairSource === pairA && pairTarget === pairB) {
|
|
676
|
+
forwardIds.push(edgeId);
|
|
677
|
+
}
|
|
678
|
+
else if (pairSource === pairB && pairTarget === pairA) {
|
|
679
|
+
reverseIds.push(edgeId);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
// Only fan when both directions exist for the same endpoint pair.
|
|
683
|
+
if (forwardIds.length === 0 || reverseIds.length === 0) {
|
|
684
|
+
return undefined;
|
|
685
|
+
}
|
|
686
|
+
let directionSign = 0;
|
|
687
|
+
let laneIndex = 0;
|
|
688
|
+
let laneCount = 0;
|
|
689
|
+
if (sourceOwner === pairA && targetOwner === pairB) {
|
|
690
|
+
directionSign = 1;
|
|
691
|
+
laneIndex = forwardIds.indexOf(edge.id);
|
|
692
|
+
laneCount = forwardIds.length;
|
|
693
|
+
}
|
|
694
|
+
else if (sourceOwner === pairB && targetOwner === pairA) {
|
|
695
|
+
directionSign = -1;
|
|
696
|
+
laneIndex = reverseIds.indexOf(edge.id);
|
|
697
|
+
laneCount = reverseIds.length;
|
|
698
|
+
}
|
|
699
|
+
if (directionSign === 0 || laneIndex < 0 || laneCount <= 0) {
|
|
700
|
+
return undefined;
|
|
701
|
+
}
|
|
702
|
+
const pairABox = layout.boxes.get(pairA);
|
|
703
|
+
const pairBBox = layout.boxes.get(pairB);
|
|
704
|
+
if (!pairABox || !pairBBox) {
|
|
705
|
+
return undefined;
|
|
706
|
+
}
|
|
707
|
+
const sx = pairABox.x + (pairABox.width / 2);
|
|
708
|
+
const sy = pairABox.y + (pairABox.height / 2);
|
|
709
|
+
const tx = pairBBox.x + (pairBBox.width / 2);
|
|
710
|
+
const ty = pairBBox.y + (pairBBox.height / 2);
|
|
711
|
+
const dx = tx - sx;
|
|
712
|
+
const dy = ty - sy;
|
|
713
|
+
const len = Math.hypot(dx, dy);
|
|
714
|
+
if (!Number.isFinite(len) || len < 1) {
|
|
715
|
+
return undefined;
|
|
716
|
+
}
|
|
717
|
+
const laneOffset = (laneIndex - ((laneCount - 1) / 2));
|
|
718
|
+
const offset = (directionSign * 16) + (laneOffset * 10);
|
|
719
|
+
if (Math.abs(offset) < 0.01) {
|
|
720
|
+
return undefined;
|
|
721
|
+
}
|
|
722
|
+
const px = -dy / len;
|
|
723
|
+
const py = dx / len;
|
|
724
|
+
const midx = sx + (dx * 0.5);
|
|
725
|
+
const midy = sy + (dy * 0.5);
|
|
726
|
+
return [
|
|
727
|
+
{ x: midx + (px * offset), y: midy + (py * offset) },
|
|
728
|
+
];
|
|
729
|
+
};
|
|
730
|
+
const resolveEndpoint = (nodeId) => {
|
|
731
|
+
const node = nodeById.get(nodeId);
|
|
732
|
+
if (node?.kind === 'Port' && node.parentId) {
|
|
733
|
+
return {
|
|
734
|
+
cell: node.parentId,
|
|
735
|
+
port: node.id,
|
|
736
|
+
anchor: { name: 'center' },
|
|
737
|
+
connectionPoint: { name: 'boundary' },
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
return {
|
|
741
|
+
cell: nodeId,
|
|
742
|
+
anchor: { name: 'center' },
|
|
743
|
+
connectionPoint: { name: 'boundary' },
|
|
744
|
+
};
|
|
745
|
+
};
|
|
746
|
+
for (const edge of graph.edges) {
|
|
747
|
+
const resolvedStyle = edge.style;
|
|
748
|
+
const lineAttrs = resolveEdgeLineAttrs(resolvedStyle);
|
|
749
|
+
graphView.addEdge({
|
|
750
|
+
id: edge.id,
|
|
751
|
+
source: resolveEndpoint(edge.sourceId),
|
|
752
|
+
target: resolveEndpoint(edge.targetId),
|
|
753
|
+
router: { name: 'normal' },
|
|
754
|
+
connector: { name: 'rounded' },
|
|
755
|
+
attrs: {
|
|
756
|
+
line: lineAttrs,
|
|
757
|
+
},
|
|
758
|
+
vertices: fanningVertexForEdge(edge),
|
|
759
|
+
labels: edge.labels.map((label, index) => ({
|
|
760
|
+
id: `${edge.id}:label:${index}`,
|
|
761
|
+
position: {
|
|
762
|
+
distance: edgeLabelPosition(label.placement),
|
|
763
|
+
offset: 8,
|
|
764
|
+
},
|
|
765
|
+
attrs: {
|
|
766
|
+
label: resolveEdgeLabelAttrs(resolvedStyle, label.placement, label.text),
|
|
767
|
+
body: resolveEdgeLabelBodyAttrs(resolvedStyle, label.placement),
|
|
768
|
+
labelBody: resolveEdgeLabelBodyAttrs(resolvedStyle, label.placement),
|
|
769
|
+
},
|
|
770
|
+
})),
|
|
771
|
+
zIndex: 5,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
// Compartments are structural containers: do not drag them; select parent instead.
|
|
775
|
+
const selectCompartmentParent = (node) => {
|
|
776
|
+
if (!isCompartmentNode(node)) {
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
const parent = typeof node.getParent === 'function' ? node.getParent() : undefined;
|
|
780
|
+
if (!parent) {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if (typeof graphView.cleanSelection === 'function') {
|
|
784
|
+
graphView.cleanSelection();
|
|
785
|
+
}
|
|
786
|
+
if (typeof graphView.select === 'function') {
|
|
787
|
+
graphView.select(parent);
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
graphView.on('node:mousedown', ({ node, e }) => {
|
|
791
|
+
if (!isCompartmentNode(node)) {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
selectCompartmentParent(node);
|
|
795
|
+
e.preventDefault();
|
|
796
|
+
e.stopPropagation();
|
|
797
|
+
if (typeof e.stopImmediatePropagation === 'function') {
|
|
798
|
+
e.stopImmediatePropagation();
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
graphView.on('node:click', ({ node, e }) => {
|
|
802
|
+
if (!isCompartmentNode(node)) {
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
selectCompartmentParent(node);
|
|
806
|
+
e.preventDefault();
|
|
807
|
+
e.stopPropagation();
|
|
808
|
+
});
|
|
809
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
810
|
+
const onPointerMove = (event) => {
|
|
811
|
+
if (!activePortDrag)
|
|
812
|
+
return;
|
|
813
|
+
const node = graphView.getCellById(activePortDrag.nodeId);
|
|
814
|
+
if (!node || typeof node.size !== 'function' || typeof node.getPort !== 'function' || typeof node.setPortProp !== 'function')
|
|
815
|
+
return;
|
|
816
|
+
const size = node.size();
|
|
817
|
+
const deltaY = event.clientY - activePortDrag.startClientY;
|
|
818
|
+
const nextY = clamp(activePortDrag.startPortY + deltaY, 4, Math.max(4, size.height - 4));
|
|
819
|
+
node.setPortProp(activePortDrag.portId, 'args/y', nextY);
|
|
820
|
+
};
|
|
821
|
+
const onPointerUp = () => {
|
|
822
|
+
activePortDrag = undefined;
|
|
823
|
+
};
|
|
824
|
+
window.addEventListener('pointermove', onPointerMove);
|
|
825
|
+
window.addEventListener('pointerup', onPointerUp);
|
|
826
|
+
const startPortDrag = (clientY, node, portId) => {
|
|
827
|
+
if (!node || typeof node.getPort !== 'function')
|
|
828
|
+
return;
|
|
829
|
+
const existing = node.getPort(portId);
|
|
830
|
+
const startPortY = typeof existing?.args?.y === 'number' ? existing.args.y : (node.size().height / 2);
|
|
831
|
+
activePortDrag = {
|
|
832
|
+
nodeId: String(node.id),
|
|
833
|
+
portId: String(portId),
|
|
834
|
+
startClientY: clientY,
|
|
835
|
+
startPortY,
|
|
836
|
+
};
|
|
837
|
+
};
|
|
838
|
+
const onCanvasPointerDown = (event) => {
|
|
839
|
+
if (!isPortPointerTarget(event))
|
|
840
|
+
return;
|
|
841
|
+
const portId = findPortIdFromTarget(event.target);
|
|
842
|
+
if (!portId)
|
|
843
|
+
return;
|
|
844
|
+
const ownerId = ownerByPortId.get(portId);
|
|
845
|
+
if (!ownerId)
|
|
846
|
+
return;
|
|
847
|
+
const node = graphView.getCellById(ownerId);
|
|
848
|
+
startPortDrag(event.clientY, node, portId);
|
|
849
|
+
event.preventDefault();
|
|
850
|
+
event.stopPropagation();
|
|
851
|
+
if (typeof event.stopImmediatePropagation === 'function')
|
|
852
|
+
event.stopImmediatePropagation();
|
|
853
|
+
};
|
|
854
|
+
liveCanvas.addEventListener('pointerdown', onCanvasPointerDown, true);
|
|
855
|
+
liveCanvas.addEventListener('DOMNodeRemovedFromDocument', () => {
|
|
856
|
+
window.removeEventListener('pointermove', onPointerMove);
|
|
857
|
+
window.removeEventListener('pointerup', onPointerUp);
|
|
858
|
+
liveCanvas.removeEventListener('pointerdown', onCanvasPointerDown, true);
|
|
859
|
+
}, { once: true });
|
|
860
|
+
const resultContainer = liveCanvas.closest('.oml-md-result');
|
|
861
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
862
|
+
if (!liveCanvas.isConnected) {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
const width = liveCanvas.clientWidth;
|
|
866
|
+
const height = liveCanvas.clientHeight;
|
|
867
|
+
graphView.resize(width, height);
|
|
868
|
+
});
|
|
869
|
+
resizeObserver.observe(liveCanvas);
|
|
870
|
+
let resultResizeObserver;
|
|
871
|
+
if (resultContainer instanceof HTMLElement) {
|
|
872
|
+
resultResizeObserver = new ResizeObserver(() => {
|
|
873
|
+
const nextWidth = Math.max(0, Math.floor(resultContainer.clientWidth));
|
|
874
|
+
liveCanvas.style.width = `${nextWidth}px`;
|
|
875
|
+
graphView.resize(liveCanvas.clientWidth, liveCanvas.clientHeight);
|
|
876
|
+
});
|
|
877
|
+
resultResizeObserver.observe(resultContainer);
|
|
878
|
+
}
|
|
879
|
+
liveCanvas.addEventListener('DOMNodeRemovedFromDocument', () => {
|
|
880
|
+
resizeObserver.disconnect();
|
|
881
|
+
resultResizeObserver?.disconnect();
|
|
882
|
+
}, { once: true });
|
|
883
|
+
const resizeHandle = document.createElement('div');
|
|
884
|
+
resizeHandle.className = 'canvas-resize-handle';
|
|
885
|
+
liveCanvas.appendChild(resizeHandle);
|
|
886
|
+
let canvasResize;
|
|
887
|
+
const onResizePointerDown = (event) => {
|
|
888
|
+
event.preventDefault();
|
|
889
|
+
event.stopPropagation();
|
|
890
|
+
if (typeof event.stopImmediatePropagation === 'function')
|
|
891
|
+
event.stopImmediatePropagation();
|
|
892
|
+
canvasResize = { pointerId: event.pointerId, startY: event.clientY, startHeight: liveCanvas.clientHeight };
|
|
893
|
+
isResizingCanvas = true;
|
|
894
|
+
resizeHandle.setPointerCapture(event.pointerId);
|
|
895
|
+
};
|
|
896
|
+
const onResizePointerMove = (event) => {
|
|
897
|
+
if (!canvasResize || event.pointerId !== canvasResize.pointerId)
|
|
898
|
+
return;
|
|
899
|
+
const delta = event.clientY - canvasResize.startY;
|
|
900
|
+
const newHeight = Math.max(minHeight, canvasResize.startHeight + delta);
|
|
901
|
+
liveCanvas.style.height = `${Math.ceil(newHeight)}px`;
|
|
902
|
+
};
|
|
903
|
+
const onResizePointerEnd = (event) => {
|
|
904
|
+
if (!canvasResize || event.pointerId !== canvasResize.pointerId)
|
|
905
|
+
return;
|
|
906
|
+
canvasResize = undefined;
|
|
907
|
+
isResizingCanvas = false;
|
|
908
|
+
};
|
|
909
|
+
resizeHandle.addEventListener('pointerdown', onResizePointerDown);
|
|
910
|
+
resizeHandle.addEventListener('pointermove', onResizePointerMove);
|
|
911
|
+
resizeHandle.addEventListener('pointerup', onResizePointerEnd);
|
|
912
|
+
resizeHandle.addEventListener('pointercancel', onResizePointerEnd);
|
|
913
|
+
installDiagramToolbar(liveCanvas, graphView, graph, actions);
|
|
914
|
+
}
|
|
915
|
+
function installDiagramToolbar(graphRoot, graphView, graph, actions) {
|
|
916
|
+
const hotspot = document.createElement('div');
|
|
917
|
+
hotspot.className = 'graph-corner-hotspot';
|
|
918
|
+
graphRoot.appendChild(hotspot);
|
|
919
|
+
const toolbar = document.createElement('div');
|
|
920
|
+
toolbar.className = 'graph-corner-toolbar';
|
|
921
|
+
const filterInput = document.createElement('input');
|
|
922
|
+
filterInput.type = 'search';
|
|
923
|
+
filterInput.className = 'tree-filter graph-filter';
|
|
924
|
+
filterInput.placeholder = 'Filter...';
|
|
925
|
+
filterInput.addEventListener('input', () => {
|
|
926
|
+
applyDiagramFilter(graphView, graph, filterInput.value);
|
|
927
|
+
});
|
|
928
|
+
toolbar.appendChild(filterInput);
|
|
929
|
+
const downloadButton = createToolbarDownloadButton('Download SVG');
|
|
930
|
+
downloadButton.addEventListener('click', () => {
|
|
931
|
+
const svg = graphRoot.querySelector('.x6-graph-svg, .x6-svg, svg');
|
|
932
|
+
if (!svg) {
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
const exported = createStandaloneSvg(svg);
|
|
936
|
+
const serialized = new XMLSerializer().serializeToString(exported);
|
|
937
|
+
const content = serialized.startsWith('<?xml')
|
|
938
|
+
? serialized
|
|
939
|
+
: `<?xml version="1.0" encoding="UTF-8"?>\n${serialized}`;
|
|
940
|
+
actions.downloadSvg(content);
|
|
941
|
+
});
|
|
942
|
+
toolbar.appendChild(downloadButton);
|
|
943
|
+
graphRoot.appendChild(toolbar);
|
|
944
|
+
let hideTimer = 0;
|
|
945
|
+
const showToolbar = () => {
|
|
946
|
+
if (hideTimer) {
|
|
947
|
+
window.clearTimeout(hideTimer);
|
|
948
|
+
hideTimer = 0;
|
|
949
|
+
}
|
|
950
|
+
graphRoot.classList.add('graph-toolbar-visible');
|
|
951
|
+
};
|
|
952
|
+
const scheduleHideToolbar = () => {
|
|
953
|
+
if (document.activeElement === filterInput) {
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
hideTimer = window.setTimeout(() => {
|
|
957
|
+
graphRoot.classList.remove('graph-toolbar-visible');
|
|
958
|
+
hideTimer = 0;
|
|
959
|
+
}, 120);
|
|
960
|
+
};
|
|
961
|
+
hotspot.addEventListener('mouseenter', showToolbar);
|
|
962
|
+
toolbar.addEventListener('mouseenter', showToolbar);
|
|
963
|
+
hotspot.addEventListener('mouseleave', scheduleHideToolbar);
|
|
964
|
+
toolbar.addEventListener('mouseleave', scheduleHideToolbar);
|
|
965
|
+
filterInput.addEventListener('focus', showToolbar);
|
|
966
|
+
filterInput.addEventListener('blur', scheduleHideToolbar);
|
|
967
|
+
const swallowPointer = (event) => {
|
|
968
|
+
event.stopPropagation();
|
|
969
|
+
};
|
|
970
|
+
toolbar.addEventListener('pointerdown', swallowPointer, true);
|
|
971
|
+
toolbar.addEventListener('mousedown', swallowPointer, true);
|
|
972
|
+
filterInput.addEventListener('pointerdown', swallowPointer, true);
|
|
973
|
+
filterInput.addEventListener('mousedown', swallowPointer, true);
|
|
974
|
+
downloadButton.addEventListener('pointerdown', swallowPointer, true);
|
|
975
|
+
downloadButton.addEventListener('mousedown', swallowPointer, true);
|
|
976
|
+
}
|
|
977
|
+
function applyDiagramFilter(graphView, graph, rawQuery) {
|
|
978
|
+
const query = rawQuery.trim().toLowerCase();
|
|
979
|
+
const allNodes = graphView.getNodes();
|
|
980
|
+
const allEdges = graphView.getEdges();
|
|
981
|
+
if (!query) {
|
|
982
|
+
for (const node of allNodes)
|
|
983
|
+
node.show();
|
|
984
|
+
for (const edge of allEdges)
|
|
985
|
+
edge.show();
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
989
|
+
const visibleNodeIds = new Set(allNodes.map((node) => String(node.id)));
|
|
990
|
+
const endpointOwner = (id) => {
|
|
991
|
+
const node = nodeById.get(id);
|
|
992
|
+
if (!node)
|
|
993
|
+
return undefined;
|
|
994
|
+
return node.kind === 'Port' ? node.parentId : node.id;
|
|
995
|
+
};
|
|
996
|
+
const childrenByParent = new Map();
|
|
997
|
+
for (const node of graph.nodes) {
|
|
998
|
+
if (!node.parentId || !visibleNodeIds.has(node.id))
|
|
999
|
+
continue;
|
|
1000
|
+
const list = childrenByParent.get(node.parentId) ?? [];
|
|
1001
|
+
list.push(node.id);
|
|
1002
|
+
childrenByParent.set(node.parentId, list);
|
|
1003
|
+
}
|
|
1004
|
+
const edgeById = new Map(graph.edges.map((edge) => [edge.id, edge]));
|
|
1005
|
+
const edgesByNode = new Map();
|
|
1006
|
+
const addEdgeToNode = (nodeId, edgeId) => {
|
|
1007
|
+
const list = edgesByNode.get(nodeId) ?? [];
|
|
1008
|
+
list.push(edgeId);
|
|
1009
|
+
edgesByNode.set(nodeId, list);
|
|
1010
|
+
};
|
|
1011
|
+
for (const edge of graph.edges) {
|
|
1012
|
+
const source = endpointOwner(edge.sourceId);
|
|
1013
|
+
const target = endpointOwner(edge.targetId);
|
|
1014
|
+
if (!source || !target)
|
|
1015
|
+
continue;
|
|
1016
|
+
if (!visibleNodeIds.has(source) || !visibleNodeIds.has(target))
|
|
1017
|
+
continue;
|
|
1018
|
+
addEdgeToNode(source, edge.id);
|
|
1019
|
+
addEdgeToNode(target, edge.id);
|
|
1020
|
+
}
|
|
1021
|
+
const addAncestors = (nodeId, keepNodeIds) => {
|
|
1022
|
+
let cursor = nodeById.get(nodeId)?.parentId;
|
|
1023
|
+
while (cursor) {
|
|
1024
|
+
if (visibleNodeIds.has(cursor)) {
|
|
1025
|
+
keepNodeIds.add(cursor);
|
|
1026
|
+
}
|
|
1027
|
+
cursor = nodeById.get(cursor)?.parentId;
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
const addDescendants = (nodeId, keepNodeIds) => {
|
|
1031
|
+
const stack = [...(childrenByParent.get(nodeId) ?? [])];
|
|
1032
|
+
while (stack.length > 0) {
|
|
1033
|
+
const next = stack.pop();
|
|
1034
|
+
if (!next)
|
|
1035
|
+
continue;
|
|
1036
|
+
keepNodeIds.add(next);
|
|
1037
|
+
stack.push(...(childrenByParent.get(next) ?? []));
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
const matchedNodeIds = new Set();
|
|
1041
|
+
for (const nodeId of visibleNodeIds) {
|
|
1042
|
+
const spec = nodeById.get(nodeId);
|
|
1043
|
+
if (!spec)
|
|
1044
|
+
continue;
|
|
1045
|
+
const labels = spec.labels.join(' ').toLowerCase();
|
|
1046
|
+
const idValue = spec.id.toLowerCase();
|
|
1047
|
+
const kind = spec.kind.toLowerCase();
|
|
1048
|
+
if (labels.includes(query) || idValue.includes(query) || kind.includes(query)) {
|
|
1049
|
+
matchedNodeIds.add(nodeId);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const matchedEdgeIds = new Set();
|
|
1053
|
+
for (const edge of graph.edges) {
|
|
1054
|
+
const edgeText = [
|
|
1055
|
+
edge.id,
|
|
1056
|
+
edge.labels.map((label) => label.text).join(' ')
|
|
1057
|
+
].join(' ').toLowerCase();
|
|
1058
|
+
if (edgeText.includes(query)) {
|
|
1059
|
+
matchedEdgeIds.add(edge.id);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
const keepNodeIds = new Set();
|
|
1063
|
+
const keepEdgeIds = new Set();
|
|
1064
|
+
for (const nodeId of matchedNodeIds) {
|
|
1065
|
+
keepNodeIds.add(nodeId);
|
|
1066
|
+
addAncestors(nodeId, keepNodeIds);
|
|
1067
|
+
addDescendants(nodeId, keepNodeIds);
|
|
1068
|
+
for (const edgeId of edgesByNode.get(nodeId) ?? []) {
|
|
1069
|
+
const edge = edgeById.get(edgeId);
|
|
1070
|
+
if (!edge)
|
|
1071
|
+
continue;
|
|
1072
|
+
const source = endpointOwner(edge.sourceId);
|
|
1073
|
+
const target = endpointOwner(edge.targetId);
|
|
1074
|
+
if (!source || !target)
|
|
1075
|
+
continue;
|
|
1076
|
+
keepEdgeIds.add(edgeId);
|
|
1077
|
+
keepNodeIds.add(source);
|
|
1078
|
+
keepNodeIds.add(target);
|
|
1079
|
+
addAncestors(source, keepNodeIds);
|
|
1080
|
+
addAncestors(target, keepNodeIds);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
for (const edgeId of matchedEdgeIds) {
|
|
1084
|
+
const edge = edgeById.get(edgeId);
|
|
1085
|
+
if (!edge)
|
|
1086
|
+
continue;
|
|
1087
|
+
const source = endpointOwner(edge.sourceId);
|
|
1088
|
+
const target = endpointOwner(edge.targetId);
|
|
1089
|
+
if (!source || !target)
|
|
1090
|
+
continue;
|
|
1091
|
+
keepEdgeIds.add(edgeId);
|
|
1092
|
+
keepNodeIds.add(source);
|
|
1093
|
+
keepNodeIds.add(target);
|
|
1094
|
+
addAncestors(source, keepNodeIds);
|
|
1095
|
+
addAncestors(target, keepNodeIds);
|
|
1096
|
+
}
|
|
1097
|
+
for (const edge of graph.edges) {
|
|
1098
|
+
const source = endpointOwner(edge.sourceId);
|
|
1099
|
+
const target = endpointOwner(edge.targetId);
|
|
1100
|
+
if (!source || !target)
|
|
1101
|
+
continue;
|
|
1102
|
+
if (keepNodeIds.has(source) && keepNodeIds.has(target)) {
|
|
1103
|
+
keepEdgeIds.add(edge.id);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
for (const node of allNodes) {
|
|
1107
|
+
if (keepNodeIds.has(String(node.id))) {
|
|
1108
|
+
node.show();
|
|
1109
|
+
}
|
|
1110
|
+
else {
|
|
1111
|
+
node.hide();
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
for (const edge of allEdges) {
|
|
1115
|
+
if (keepEdgeIds.has(String(edge.id))) {
|
|
1116
|
+
edge.show();
|
|
1117
|
+
}
|
|
1118
|
+
else {
|
|
1119
|
+
edge.hide();
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
function createToolbarDownloadButton(title) {
|
|
1124
|
+
const downloadButton = document.createElement('button');
|
|
1125
|
+
downloadButton.className = 'tree-download-btn';
|
|
1126
|
+
downloadButton.title = title;
|
|
1127
|
+
downloadButton.setAttribute('aria-label', title);
|
|
1128
|
+
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
1129
|
+
iconSvg.setAttribute('viewBox', '0 0 24 24');
|
|
1130
|
+
iconSvg.setAttribute('width', '20');
|
|
1131
|
+
iconSvg.setAttribute('height', '20');
|
|
1132
|
+
iconSvg.setAttribute('aria-hidden', 'true');
|
|
1133
|
+
iconSvg.setAttribute('focusable', 'false');
|
|
1134
|
+
iconSvg.style.fill = 'currentColor';
|
|
1135
|
+
const iconPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1136
|
+
iconPath.setAttribute('d', 'M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z');
|
|
1137
|
+
iconSvg.appendChild(iconPath);
|
|
1138
|
+
downloadButton.appendChild(iconSvg);
|
|
1139
|
+
return downloadButton;
|
|
1140
|
+
}
|
|
1141
|
+
function createStandaloneSvg(sourceSvg) {
|
|
1142
|
+
const clone = sourceSvg.cloneNode(true);
|
|
1143
|
+
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
1144
|
+
clone.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
|
|
1145
|
+
const bbox = getSafeSvgBounds(sourceSvg);
|
|
1146
|
+
if (bbox) {
|
|
1147
|
+
clone.setAttribute('viewBox', `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`);
|
|
1148
|
+
clone.setAttribute('width', `${Math.ceil(bbox.width)}`);
|
|
1149
|
+
clone.setAttribute('height', `${Math.ceil(bbox.height)}`);
|
|
1150
|
+
}
|
|
1151
|
+
else {
|
|
1152
|
+
const fallbackWidth = Math.max(1, Math.ceil(sourceSvg.clientWidth || 1));
|
|
1153
|
+
const fallbackHeight = Math.max(1, Math.ceil(sourceSvg.clientHeight || 1));
|
|
1154
|
+
clone.setAttribute('viewBox', `0 0 ${fallbackWidth} ${fallbackHeight}`);
|
|
1155
|
+
clone.setAttribute('width', `${fallbackWidth}`);
|
|
1156
|
+
clone.setAttribute('height', `${fallbackHeight}`);
|
|
1157
|
+
}
|
|
1158
|
+
inlineComputedSvgStyles(sourceSvg, clone);
|
|
1159
|
+
return clone;
|
|
1160
|
+
}
|
|
1161
|
+
function getSafeSvgBounds(svg) {
|
|
1162
|
+
try {
|
|
1163
|
+
const box = svg.getBBox();
|
|
1164
|
+
if (Number.isFinite(box.width) && Number.isFinite(box.height) && box.width > 0 && box.height > 0) {
|
|
1165
|
+
return { x: box.x, y: box.y, width: box.width, height: box.height };
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
catch {
|
|
1169
|
+
// ignore and use fallback below
|
|
1170
|
+
}
|
|
1171
|
+
return undefined;
|
|
1172
|
+
}
|
|
1173
|
+
function inlineComputedSvgStyles(sourceRoot, cloneRoot) {
|
|
1174
|
+
const properties = [
|
|
1175
|
+
'fill',
|
|
1176
|
+
'stroke',
|
|
1177
|
+
'stroke-width',
|
|
1178
|
+
'stroke-linecap',
|
|
1179
|
+
'stroke-linejoin',
|
|
1180
|
+
'stroke-dasharray',
|
|
1181
|
+
'stroke-dashoffset',
|
|
1182
|
+
'opacity',
|
|
1183
|
+
'fill-opacity',
|
|
1184
|
+
'stroke-opacity',
|
|
1185
|
+
'vector-effect',
|
|
1186
|
+
'paint-order',
|
|
1187
|
+
'font-family',
|
|
1188
|
+
'font-size',
|
|
1189
|
+
'font-weight',
|
|
1190
|
+
'font-style',
|
|
1191
|
+
'line-height',
|
|
1192
|
+
'text-anchor',
|
|
1193
|
+
'dominant-baseline',
|
|
1194
|
+
'letter-spacing',
|
|
1195
|
+
'word-spacing',
|
|
1196
|
+
'color',
|
|
1197
|
+
'marker-start',
|
|
1198
|
+
'marker-mid',
|
|
1199
|
+
'marker-end',
|
|
1200
|
+
];
|
|
1201
|
+
const sourceNodes = sourceRoot.querySelectorAll('*');
|
|
1202
|
+
const cloneNodes = cloneRoot.querySelectorAll('*');
|
|
1203
|
+
const count = Math.min(sourceNodes.length, cloneNodes.length);
|
|
1204
|
+
for (let i = 0; i < count; i += 1) {
|
|
1205
|
+
const source = sourceNodes[i];
|
|
1206
|
+
const target = cloneNodes[i];
|
|
1207
|
+
const computed = window.getComputedStyle(source);
|
|
1208
|
+
const declarations = [];
|
|
1209
|
+
for (const property of properties) {
|
|
1210
|
+
const value = computed.getPropertyValue(property).trim();
|
|
1211
|
+
if (!value) {
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
declarations.push(`${property}:${value}`);
|
|
1215
|
+
}
|
|
1216
|
+
if (declarations.length > 0) {
|
|
1217
|
+
target.setAttribute('style', declarations.join(';'));
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
async function loadX6GraphCtor() {
|
|
1222
|
+
if (x6GraphCtor) {
|
|
1223
|
+
return x6GraphCtor;
|
|
1224
|
+
}
|
|
1225
|
+
const mod = await import('@antv/x6');
|
|
1226
|
+
const ctor = mod.Graph;
|
|
1227
|
+
if (typeof ctor !== 'function') {
|
|
1228
|
+
throw new Error('X6 Graph constructor is unavailable in @antv/x6');
|
|
1229
|
+
}
|
|
1230
|
+
x6GraphCtor = ctor;
|
|
1231
|
+
return x6GraphCtor;
|
|
1232
|
+
}
|
|
1233
|
+
async function loadDagreLib() {
|
|
1234
|
+
if (dagreLib) {
|
|
1235
|
+
return dagreLib;
|
|
1236
|
+
}
|
|
1237
|
+
const mod = await import('@dagrejs/dagre');
|
|
1238
|
+
dagreLib = mod.default ?? mod;
|
|
1239
|
+
return dagreLib;
|
|
1240
|
+
}
|
|
1241
|
+
async function layoutGraphDagre(graph, options) {
|
|
1242
|
+
const dagre = await loadDagreLib();
|
|
1243
|
+
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
1244
|
+
const layoutNodes = graph.nodes.filter((node) => node.kind !== 'Port');
|
|
1245
|
+
const childrenByParent = new Map();
|
|
1246
|
+
for (const node of layoutNodes) {
|
|
1247
|
+
const list = childrenByParent.get(node.parentId) ?? [];
|
|
1248
|
+
list.push(node.id);
|
|
1249
|
+
childrenByParent.set(node.parentId, list);
|
|
1250
|
+
}
|
|
1251
|
+
const endpointOwner = (id) => {
|
|
1252
|
+
const node = nodeById.get(id);
|
|
1253
|
+
if (!node)
|
|
1254
|
+
return undefined;
|
|
1255
|
+
if (node.kind === 'Port')
|
|
1256
|
+
return node.parentId;
|
|
1257
|
+
return node.id;
|
|
1258
|
+
};
|
|
1259
|
+
const layoutOptionsForParent = (parentId) => {
|
|
1260
|
+
if (!parentId) {
|
|
1261
|
+
return { type: 'dagre', dagre: options };
|
|
1262
|
+
}
|
|
1263
|
+
const parent = nodeById.get(parentId);
|
|
1264
|
+
if (!parent) {
|
|
1265
|
+
return { type: 'dagre', dagre: options };
|
|
1266
|
+
}
|
|
1267
|
+
const styleLayout = asRecord(parent.style.layout);
|
|
1268
|
+
if (!styleLayout) {
|
|
1269
|
+
return { type: 'dagre', dagre: options };
|
|
1270
|
+
}
|
|
1271
|
+
const type = typeof styleLayout.type === 'string' ? styleLayout.type.trim().toLowerCase() : '';
|
|
1272
|
+
if (type === 'stack') {
|
|
1273
|
+
return {
|
|
1274
|
+
type: 'stack',
|
|
1275
|
+
stack: {
|
|
1276
|
+
direction: styleLayout.direction === 'horizontal' ? 'horizontal' : 'vertical',
|
|
1277
|
+
gap: clampLayoutNumber(styleLayout.gap, 0, 400, 0),
|
|
1278
|
+
marginx: clampLayoutNumber(styleLayout.marginx, 0, 200, 0),
|
|
1279
|
+
marginy: clampLayoutNumber(styleLayout.marginy, 0, 200, 0),
|
|
1280
|
+
stretch: typeof styleLayout.stretch === 'boolean' ? styleLayout.stretch : true,
|
|
1281
|
+
},
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
if (type !== '' && type !== 'dagre') {
|
|
1285
|
+
return { type: 'dagre', dagre: options };
|
|
1286
|
+
}
|
|
1287
|
+
const rankdirRaw = typeof styleLayout.rankdir === 'string'
|
|
1288
|
+
? styleLayout.rankdir.toUpperCase()
|
|
1289
|
+
: options.rankdir;
|
|
1290
|
+
const rankdir = (rankdirRaw === 'LR' || rankdirRaw === 'RL' || rankdirRaw === 'TB' || rankdirRaw === 'BT')
|
|
1291
|
+
? rankdirRaw
|
|
1292
|
+
: options.rankdir;
|
|
1293
|
+
return {
|
|
1294
|
+
type: 'dagre',
|
|
1295
|
+
dagre: {
|
|
1296
|
+
rankdir,
|
|
1297
|
+
nodesep: clampLayoutNumber(styleLayout.nodesep, 0, 400, options.nodesep),
|
|
1298
|
+
ranksep: clampLayoutNumber(styleLayout.ranksep, 0, 500, options.ranksep),
|
|
1299
|
+
marginx: clampLayoutNumber(styleLayout.marginx, 0, 200, options.marginx),
|
|
1300
|
+
marginy: clampLayoutNumber(styleLayout.marginy, 0, 200, options.marginy),
|
|
1301
|
+
},
|
|
1302
|
+
};
|
|
1303
|
+
};
|
|
1304
|
+
const childPositionById = new Map();
|
|
1305
|
+
const edgesBetweenChildren = (parentId, childSet) => {
|
|
1306
|
+
const seen = new Set();
|
|
1307
|
+
const links = [];
|
|
1308
|
+
for (const edge of graph.edges) {
|
|
1309
|
+
const sourceId = endpointOwner(edge.sourceId);
|
|
1310
|
+
const targetId = endpointOwner(edge.targetId);
|
|
1311
|
+
if (!sourceId || !targetId || sourceId === targetId)
|
|
1312
|
+
continue;
|
|
1313
|
+
if (!childSet.has(sourceId) || !childSet.has(targetId))
|
|
1314
|
+
continue;
|
|
1315
|
+
const sourceNode = nodeById.get(sourceId);
|
|
1316
|
+
const targetNode = nodeById.get(targetId);
|
|
1317
|
+
if (!sourceNode || !targetNode)
|
|
1318
|
+
continue;
|
|
1319
|
+
if (sourceNode.parentId !== parentId || targetNode.parentId !== parentId)
|
|
1320
|
+
continue;
|
|
1321
|
+
const key = `${sourceId}=>${targetId}`;
|
|
1322
|
+
if (seen.has(key))
|
|
1323
|
+
continue;
|
|
1324
|
+
seen.add(key);
|
|
1325
|
+
links.push({ source: sourceId, target: targetId });
|
|
1326
|
+
}
|
|
1327
|
+
return links;
|
|
1328
|
+
};
|
|
1329
|
+
const layoutChildren = (parentId) => {
|
|
1330
|
+
const childIds = childrenByParent.get(parentId) ?? [];
|
|
1331
|
+
for (const childId of childIds) {
|
|
1332
|
+
layoutChildren(childId);
|
|
1333
|
+
}
|
|
1334
|
+
if (childIds.length === 0) {
|
|
1335
|
+
return { width: 0, height: 0 };
|
|
1336
|
+
}
|
|
1337
|
+
const parent = parentId ? nodeById.get(parentId) : undefined;
|
|
1338
|
+
const topPadding = parent?.contentTopPadding ?? 0;
|
|
1339
|
+
const localLayout = layoutOptionsForParent(parentId);
|
|
1340
|
+
let totalWidth = 0;
|
|
1341
|
+
let totalHeight = 0;
|
|
1342
|
+
if (localLayout.type === 'stack') {
|
|
1343
|
+
const local = localLayout.stack;
|
|
1344
|
+
const childNodes = childIds.map((childId) => nodeById.get(childId)).filter((node) => !!node);
|
|
1345
|
+
const maxChildWidth = childNodes.reduce((max, child) => Math.max(max, child.width), 0);
|
|
1346
|
+
const maxChildHeight = childNodes.reduce((max, child) => Math.max(max, child.height), 0);
|
|
1347
|
+
const parentContentWidth = Math.max(0, (parent?.width ?? 0) - (local.marginx * 2));
|
|
1348
|
+
const parentContentHeight = Math.max(0, (parent?.height ?? 0) - topPadding - (local.marginy * 2));
|
|
1349
|
+
const availableWidth = Math.max(maxChildWidth, parentContentWidth);
|
|
1350
|
+
const availableHeight = Math.max(maxChildHeight, parentContentHeight);
|
|
1351
|
+
const isSingleChild = childNodes.length === 1;
|
|
1352
|
+
let cursorX = local.marginx;
|
|
1353
|
+
let cursorY = topPadding + local.marginy;
|
|
1354
|
+
for (const child of childNodes) {
|
|
1355
|
+
if (local.stretch && isSingleChild) {
|
|
1356
|
+
child.width = Math.max(0, availableWidth);
|
|
1357
|
+
child.height = Math.max(0, availableHeight);
|
|
1358
|
+
}
|
|
1359
|
+
else if (local.direction === 'vertical' && local.stretch) {
|
|
1360
|
+
child.width = Math.max(0, availableWidth);
|
|
1361
|
+
}
|
|
1362
|
+
else if (local.direction === 'horizontal' && local.stretch) {
|
|
1363
|
+
child.height = Math.max(0, availableHeight);
|
|
1364
|
+
}
|
|
1365
|
+
childPositionById.set(child.id, { x: cursorX, y: cursorY });
|
|
1366
|
+
if (local.direction === 'vertical') {
|
|
1367
|
+
cursorY += child.height + local.gap;
|
|
1368
|
+
}
|
|
1369
|
+
else {
|
|
1370
|
+
cursorX += child.width + local.gap;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
if (local.direction === 'vertical') {
|
|
1374
|
+
const contentHeight = Math.max(0, childNodes.reduce((sum, child) => sum + child.height, 0) + (Math.max(0, childNodes.length - 1) * local.gap));
|
|
1375
|
+
const contentWidth = Math.max(0, childNodes.reduce((max, child) => Math.max(max, child.width), 0));
|
|
1376
|
+
totalWidth = (local.marginx * 2) + contentWidth;
|
|
1377
|
+
totalHeight = topPadding + (local.marginy * 2) + contentHeight;
|
|
1378
|
+
}
|
|
1379
|
+
else {
|
|
1380
|
+
const contentWidth = Math.max(0, childNodes.reduce((sum, child) => sum + child.width, 0) + (Math.max(0, childNodes.length - 1) * local.gap));
|
|
1381
|
+
const contentHeight = Math.max(0, childNodes.reduce((max, child) => Math.max(max, child.height), 0));
|
|
1382
|
+
totalWidth = (local.marginx * 2) + contentWidth;
|
|
1383
|
+
totalHeight = topPadding + (local.marginy * 2) + contentHeight;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
else {
|
|
1387
|
+
const localOptions = localLayout.dagre;
|
|
1388
|
+
const g = new dagre.graphlib.Graph({ multigraph: true, compound: false });
|
|
1389
|
+
g.setGraph({
|
|
1390
|
+
rankdir: localOptions.rankdir,
|
|
1391
|
+
nodesep: localOptions.nodesep,
|
|
1392
|
+
ranksep: localOptions.ranksep,
|
|
1393
|
+
marginx: 0,
|
|
1394
|
+
marginy: 0,
|
|
1395
|
+
});
|
|
1396
|
+
g.setDefaultEdgeLabel(() => ({}));
|
|
1397
|
+
const childSet = new Set(childIds);
|
|
1398
|
+
for (const childId of childIds) {
|
|
1399
|
+
const child = nodeById.get(childId);
|
|
1400
|
+
if (!child)
|
|
1401
|
+
continue;
|
|
1402
|
+
g.setNode(child.id, { width: child.width, height: child.height });
|
|
1403
|
+
}
|
|
1404
|
+
let edgeSeq = 0;
|
|
1405
|
+
for (const link of edgesBetweenChildren(parentId, childSet)) {
|
|
1406
|
+
g.setEdge(link.source, link.target, {}, `e${edgeSeq++}`);
|
|
1407
|
+
}
|
|
1408
|
+
dagre.layout(g);
|
|
1409
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
1410
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
1411
|
+
let maxX = 0;
|
|
1412
|
+
let maxY = 0;
|
|
1413
|
+
for (const childId of childIds) {
|
|
1414
|
+
const child = nodeById.get(childId);
|
|
1415
|
+
const laid = child ? g.node(childId) : undefined;
|
|
1416
|
+
if (!child || !laid || typeof laid.x !== 'number' || typeof laid.y !== 'number')
|
|
1417
|
+
continue;
|
|
1418
|
+
const width = asFiniteNumber(laid.width, child.width);
|
|
1419
|
+
const height = asFiniteNumber(laid.height, child.height);
|
|
1420
|
+
const left = laid.x - (width / 2);
|
|
1421
|
+
const top = laid.y - (height / 2);
|
|
1422
|
+
minX = Math.min(minX, left);
|
|
1423
|
+
minY = Math.min(minY, top);
|
|
1424
|
+
maxX = Math.max(maxX, left + width);
|
|
1425
|
+
maxY = Math.max(maxY, top + height);
|
|
1426
|
+
}
|
|
1427
|
+
if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
|
|
1428
|
+
minX = 0;
|
|
1429
|
+
minY = 0;
|
|
1430
|
+
}
|
|
1431
|
+
for (const childId of childIds) {
|
|
1432
|
+
const child = nodeById.get(childId);
|
|
1433
|
+
const laid = child ? g.node(childId) : undefined;
|
|
1434
|
+
if (!child || !laid || typeof laid.x !== 'number' || typeof laid.y !== 'number')
|
|
1435
|
+
continue;
|
|
1436
|
+
const width = asFiniteNumber(laid.width, child.width);
|
|
1437
|
+
const height = asFiniteNumber(laid.height, child.height);
|
|
1438
|
+
const left = laid.x - (width / 2);
|
|
1439
|
+
const top = laid.y - (height / 2);
|
|
1440
|
+
childPositionById.set(childId, {
|
|
1441
|
+
x: localOptions.marginx + (left - minX),
|
|
1442
|
+
y: topPadding + localOptions.marginy + (top - minY),
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
const contentWidth = Math.max(0, maxX - minX);
|
|
1446
|
+
const contentHeight = Math.max(0, maxY - minY);
|
|
1447
|
+
totalWidth = (localOptions.marginx * 2) + contentWidth;
|
|
1448
|
+
totalHeight = topPadding + (localOptions.marginy * 2) + contentHeight;
|
|
1449
|
+
}
|
|
1450
|
+
if (parent) {
|
|
1451
|
+
if (!parent.hasExplicitWidth) {
|
|
1452
|
+
parent.width = Math.max(parent.width, totalWidth);
|
|
1453
|
+
}
|
|
1454
|
+
if (!parent.hasExplicitHeight) {
|
|
1455
|
+
parent.height = Math.max(parent.height, totalHeight);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
return { width: totalWidth, height: totalHeight };
|
|
1459
|
+
};
|
|
1460
|
+
layoutChildren(undefined);
|
|
1461
|
+
const normalizeStackChildren = (parentId) => {
|
|
1462
|
+
const parent = nodeById.get(parentId);
|
|
1463
|
+
if (!parent) {
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
const childIds = childrenByParent.get(parentId) ?? [];
|
|
1467
|
+
if (childIds.length === 0) {
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
const localLayout = layoutOptionsForParent(parentId);
|
|
1471
|
+
if (localLayout.type === 'stack') {
|
|
1472
|
+
const local = localLayout.stack;
|
|
1473
|
+
const childNodes = childIds.map((childId) => nodeById.get(childId)).filter((node) => !!node);
|
|
1474
|
+
const topPadding = parent.contentTopPadding;
|
|
1475
|
+
const contentWidth = Math.max(0, parent.width - (local.marginx * 2));
|
|
1476
|
+
const contentHeight = Math.max(0, parent.height - topPadding - (local.marginy * 2));
|
|
1477
|
+
const isSingleChild = childNodes.length === 1;
|
|
1478
|
+
if (local.stretch) {
|
|
1479
|
+
for (const child of childNodes) {
|
|
1480
|
+
if (isSingleChild) {
|
|
1481
|
+
child.width = contentWidth;
|
|
1482
|
+
child.height = contentHeight;
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
if (local.direction === 'vertical') {
|
|
1486
|
+
child.width = contentWidth;
|
|
1487
|
+
}
|
|
1488
|
+
else {
|
|
1489
|
+
child.height = contentHeight;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
let cursorX = local.marginx;
|
|
1494
|
+
let cursorY = topPadding + local.marginy;
|
|
1495
|
+
for (const child of childNodes) {
|
|
1496
|
+
childPositionById.set(child.id, { x: cursorX, y: cursorY });
|
|
1497
|
+
if (local.direction === 'vertical') {
|
|
1498
|
+
cursorY += child.height + local.gap;
|
|
1499
|
+
}
|
|
1500
|
+
else {
|
|
1501
|
+
cursorX += child.width + local.gap;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
for (const childId of childIds) {
|
|
1506
|
+
normalizeStackChildren(childId);
|
|
1507
|
+
}
|
|
1508
|
+
};
|
|
1509
|
+
for (const rootId of graph.roots) {
|
|
1510
|
+
normalizeStackChildren(rootId);
|
|
1511
|
+
}
|
|
1512
|
+
const boxes = new Map();
|
|
1513
|
+
let maxRight = 0;
|
|
1514
|
+
let maxBottom = 0;
|
|
1515
|
+
const assignAbsolute = (parentId, baseX, baseY) => {
|
|
1516
|
+
const childIds = childrenByParent.get(parentId) ?? [];
|
|
1517
|
+
for (const childId of childIds) {
|
|
1518
|
+
const node = nodeById.get(childId);
|
|
1519
|
+
const rel = childPositionById.get(childId);
|
|
1520
|
+
if (!node || !rel)
|
|
1521
|
+
continue;
|
|
1522
|
+
const x = baseX + rel.x;
|
|
1523
|
+
const y = baseY + rel.y;
|
|
1524
|
+
boxes.set(node.id, { x, y, width: node.width, height: node.height });
|
|
1525
|
+
maxRight = Math.max(maxRight, x + node.width);
|
|
1526
|
+
maxBottom = Math.max(maxBottom, y + node.height);
|
|
1527
|
+
assignAbsolute(node.id, x, y);
|
|
1528
|
+
}
|
|
1529
|
+
};
|
|
1530
|
+
assignAbsolute(undefined, 0, 0);
|
|
1531
|
+
for (const node of layoutNodes) {
|
|
1532
|
+
if (boxes.has(node.id))
|
|
1533
|
+
continue;
|
|
1534
|
+
boxes.set(node.id, { x: 0, y: 0, width: node.width, height: node.height });
|
|
1535
|
+
maxRight = Math.max(maxRight, node.width);
|
|
1536
|
+
maxBottom = Math.max(maxBottom, node.height);
|
|
1537
|
+
}
|
|
1538
|
+
return {
|
|
1539
|
+
boxes,
|
|
1540
|
+
contentWidth: Math.max(280, Math.ceil(maxRight + 16)),
|
|
1541
|
+
contentHeight: Math.max(160, Math.ceil(maxBottom + 16)),
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
function nodeDepth(id, nodeById) {
|
|
1545
|
+
let depth = 0;
|
|
1546
|
+
let cursor = nodeById.get(id);
|
|
1547
|
+
while (cursor?.parentId) {
|
|
1548
|
+
depth += 1;
|
|
1549
|
+
cursor = nodeById.get(cursor.parentId);
|
|
1550
|
+
if (depth > 32)
|
|
1551
|
+
break;
|
|
1552
|
+
}
|
|
1553
|
+
return depth;
|
|
1554
|
+
}
|
|
1555
|
+
function asFiniteNumber(value, fallback) {
|
|
1556
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
1557
|
+
return value;
|
|
1558
|
+
}
|
|
1559
|
+
return fallback;
|
|
1560
|
+
}
|
|
1561
|
+
function getLiveCanvas(baseId) {
|
|
1562
|
+
const element = document.getElementById(baseId);
|
|
1563
|
+
return element instanceof HTMLElement && element.isConnected ? element : null;
|
|
1564
|
+
}
|
|
1565
|
+
function setLockedCanvasHeight(canvas, height, minHeight) {
|
|
1566
|
+
canvas.style.setProperty('--oml-diagram-height', `${Math.ceil(height)}px`);
|
|
1567
|
+
canvas.style.setProperty('--oml-diagram-min-height', `${Math.ceil(minHeight)}px`);
|
|
1568
|
+
}
|
|
1569
|
+
function parseCssPixels(value) {
|
|
1570
|
+
if (!value) {
|
|
1571
|
+
return undefined;
|
|
1572
|
+
}
|
|
1573
|
+
const match = /^\s*(\d+(?:\.\d+)?)px\s*$/.exec(value);
|
|
1574
|
+
if (!match) {
|
|
1575
|
+
return undefined;
|
|
1576
|
+
}
|
|
1577
|
+
const parsed = Number.parseFloat(match[1]);
|
|
1578
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1579
|
+
}
|
|
1580
|
+
function compileDiagramGraph(index, stylesheet) {
|
|
1581
|
+
const typeMap = new Map();
|
|
1582
|
+
const parentMap = new Map();
|
|
1583
|
+
const sourceMap = new Map();
|
|
1584
|
+
const targetMap = new Map();
|
|
1585
|
+
const textMap = new Map();
|
|
1586
|
+
const beginTextMap = new Map();
|
|
1587
|
+
const endTextMap = new Map();
|
|
1588
|
+
const classMap = new Map();
|
|
1589
|
+
const propertyRowsBySubject = new Map();
|
|
1590
|
+
for (const row of index.rows) {
|
|
1591
|
+
if (row.p !== RDF_TYPE) {
|
|
1592
|
+
const properties = propertyRowsBySubject.get(row.s) ?? [];
|
|
1593
|
+
properties.push(row);
|
|
1594
|
+
propertyRowsBySubject.set(row.s, properties);
|
|
1595
|
+
}
|
|
1596
|
+
if (row.p === RDF_TYPE) {
|
|
1597
|
+
const types = typeMap.get(row.s) ?? new Set();
|
|
1598
|
+
types.add(row.o);
|
|
1599
|
+
typeMap.set(row.s, types);
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1602
|
+
if (row.p === `${D}parent`) {
|
|
1603
|
+
parentMap.set(row.s, row.o);
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
if (row.p === `${D}source`) {
|
|
1607
|
+
sourceMap.set(row.s, row.o);
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
if (row.p === `${D}target`) {
|
|
1611
|
+
targetMap.set(row.s, row.o);
|
|
1612
|
+
continue;
|
|
1613
|
+
}
|
|
1614
|
+
if (row.p === `${D}text`) {
|
|
1615
|
+
textMap.set(row.s, row.o);
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
if (row.p === `${D}beginText`) {
|
|
1619
|
+
beginTextMap.set(row.s, row.o);
|
|
1620
|
+
continue;
|
|
1621
|
+
}
|
|
1622
|
+
if (row.p === `${D}endText`) {
|
|
1623
|
+
endTextMap.set(row.s, row.o);
|
|
1624
|
+
continue;
|
|
1625
|
+
}
|
|
1626
|
+
if (row.p === `${D}class`) {
|
|
1627
|
+
const classes = classMap.get(row.s) ?? [];
|
|
1628
|
+
for (const token of row.o.split(/\s+/).map((value) => value.trim()).filter((value) => value.length > 0)) {
|
|
1629
|
+
if (!classes.includes(token)) {
|
|
1630
|
+
classes.push(token);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
classMap.set(row.s, classes);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
const propertiesFor = (subject) => {
|
|
1637
|
+
const rows = propertyRowsBySubject.get(subject) ?? [];
|
|
1638
|
+
const props = new Map();
|
|
1639
|
+
for (const row of rows) {
|
|
1640
|
+
const fullIri = row.p;
|
|
1641
|
+
const shortName = localName(row.p);
|
|
1642
|
+
const add = (key) => {
|
|
1643
|
+
if (!key)
|
|
1644
|
+
return;
|
|
1645
|
+
const list = props.get(key) ?? [];
|
|
1646
|
+
if (!list.includes(row.o)) {
|
|
1647
|
+
list.push(row.o);
|
|
1648
|
+
props.set(key, list);
|
|
1649
|
+
}
|
|
1650
|
+
};
|
|
1651
|
+
add(fullIri);
|
|
1652
|
+
add(shortName);
|
|
1653
|
+
add(toIdentifier(shortName));
|
|
1654
|
+
}
|
|
1655
|
+
return Object.fromEntries(props);
|
|
1656
|
+
};
|
|
1657
|
+
const classSetFor = (subject) => {
|
|
1658
|
+
const classes = classMap.get(subject) ?? [];
|
|
1659
|
+
return [...classes];
|
|
1660
|
+
};
|
|
1661
|
+
const styleFor = (elementKind, subject, classes, properties) => resolveElementStyle(elementKind, subject, classes, properties, stylesheet);
|
|
1662
|
+
const kindOf = (id) => {
|
|
1663
|
+
const types = typeMap.get(id) ?? new Set();
|
|
1664
|
+
if (types.has(TYPE_IRIS.Node))
|
|
1665
|
+
return 'Node';
|
|
1666
|
+
// Keep backward compatibility: treat legacy :ListItem as a regular node.
|
|
1667
|
+
if (types.has(LIST_ITEM_IRI))
|
|
1668
|
+
return 'Node';
|
|
1669
|
+
if (types.has(TYPE_IRIS.Edge))
|
|
1670
|
+
return 'Edge';
|
|
1671
|
+
if (types.has(TYPE_IRIS.Port))
|
|
1672
|
+
return 'Port';
|
|
1673
|
+
if (types.has(TYPE_IRIS.Compartment))
|
|
1674
|
+
return 'Compartment';
|
|
1675
|
+
return undefined;
|
|
1676
|
+
};
|
|
1677
|
+
const nodeCandidates = [...typeMap.keys()].filter((id) => {
|
|
1678
|
+
const kind = kindOf(id);
|
|
1679
|
+
return kind === 'Node' || kind === 'Port' || kind === 'Compartment';
|
|
1680
|
+
});
|
|
1681
|
+
const edgeCandidates = [...typeMap.keys()].filter((id) => kindOf(id) === 'Edge');
|
|
1682
|
+
const nodeMap = new Map();
|
|
1683
|
+
for (const id of nodeCandidates) {
|
|
1684
|
+
const kind = kindOf(id);
|
|
1685
|
+
nodeMap.set(id, {
|
|
1686
|
+
id,
|
|
1687
|
+
kind,
|
|
1688
|
+
labels: [],
|
|
1689
|
+
classes: classSetFor(id),
|
|
1690
|
+
properties: propertiesFor(id),
|
|
1691
|
+
style: {},
|
|
1692
|
+
children: [],
|
|
1693
|
+
width: 160,
|
|
1694
|
+
height: 70,
|
|
1695
|
+
hasExplicitWidth: false,
|
|
1696
|
+
hasExplicitHeight: false,
|
|
1697
|
+
contentTopPadding: 0,
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
const validParent = (child, parent) => {
|
|
1701
|
+
if (!parent)
|
|
1702
|
+
return false;
|
|
1703
|
+
if (child === 'Node')
|
|
1704
|
+
return parent === 'Node' || parent === 'Compartment';
|
|
1705
|
+
if (child === 'Compartment')
|
|
1706
|
+
return parent === 'Node';
|
|
1707
|
+
if (child === 'Port')
|
|
1708
|
+
return parent === 'Node';
|
|
1709
|
+
return false;
|
|
1710
|
+
};
|
|
1711
|
+
for (const node of nodeMap.values()) {
|
|
1712
|
+
const parentId = parentMap.get(node.id);
|
|
1713
|
+
if (!parentId)
|
|
1714
|
+
continue;
|
|
1715
|
+
const parent = nodeMap.get(parentId);
|
|
1716
|
+
if (!parent)
|
|
1717
|
+
continue;
|
|
1718
|
+
if (!validParent(node.kind, parent.kind))
|
|
1719
|
+
continue;
|
|
1720
|
+
node.parentId = parent.id;
|
|
1721
|
+
parent.children.push(node.id);
|
|
1722
|
+
}
|
|
1723
|
+
const attachNodeLabel = (parentId, text) => {
|
|
1724
|
+
const node = nodeMap.get(parentId);
|
|
1725
|
+
if (!node)
|
|
1726
|
+
return;
|
|
1727
|
+
if (text.trim().length === 0)
|
|
1728
|
+
return;
|
|
1729
|
+
if (!node.labels.includes(text)) {
|
|
1730
|
+
node.labels.push(text);
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
for (const row of index.rows) {
|
|
1734
|
+
if (row.p === `${D}text` && nodeMap.has(row.s)) {
|
|
1735
|
+
attachNodeLabel(row.s, row.o);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
const edges = [];
|
|
1739
|
+
for (const id of edgeCandidates) {
|
|
1740
|
+
const sourceId = sourceMap.get(id);
|
|
1741
|
+
const targetId = targetMap.get(id);
|
|
1742
|
+
if (!sourceId || !targetId)
|
|
1743
|
+
continue;
|
|
1744
|
+
if (!nodeMap.has(sourceId) || !nodeMap.has(targetId))
|
|
1745
|
+
continue;
|
|
1746
|
+
const edge = {
|
|
1747
|
+
id,
|
|
1748
|
+
sourceId,
|
|
1749
|
+
targetId,
|
|
1750
|
+
classes: classSetFor(id),
|
|
1751
|
+
properties: propertiesFor(id),
|
|
1752
|
+
style: {},
|
|
1753
|
+
labels: [],
|
|
1754
|
+
};
|
|
1755
|
+
edges.push(edge);
|
|
1756
|
+
}
|
|
1757
|
+
for (const edge of edges) {
|
|
1758
|
+
const beginText = beginTextMap.get(edge.id);
|
|
1759
|
+
if (beginText && beginText.trim().length > 0) {
|
|
1760
|
+
edge.labels.push({ text: beginText, placement: 'begin' });
|
|
1761
|
+
}
|
|
1762
|
+
const centerText = textMap.get(edge.id);
|
|
1763
|
+
if (centerText && centerText.trim().length > 0) {
|
|
1764
|
+
edge.labels.push({ text: centerText, placement: 'center' });
|
|
1765
|
+
}
|
|
1766
|
+
const endText = endTextMap.get(edge.id);
|
|
1767
|
+
if (endText && endText.trim().length > 0) {
|
|
1768
|
+
edge.labels.push({ text: endText, placement: 'end' });
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
for (const node of nodeMap.values()) {
|
|
1772
|
+
const baseLabel = node.labels[0] ?? localName(node.id);
|
|
1773
|
+
const selectorKind = node.kind === 'Compartment'
|
|
1774
|
+
? 'compartment'
|
|
1775
|
+
: (node.kind === 'Port' ? 'port' : 'node');
|
|
1776
|
+
node.style = styleFor(selectorKind, node.id, node.classes, node.properties);
|
|
1777
|
+
const estimated = estimateSize(node.kind, baseLabel, node.labels.length, node.style);
|
|
1778
|
+
node.width = estimated.width;
|
|
1779
|
+
node.height = estimated.height;
|
|
1780
|
+
node.hasExplicitWidth = estimated.hasExplicitWidth;
|
|
1781
|
+
node.hasExplicitHeight = estimated.hasExplicitHeight;
|
|
1782
|
+
node.contentTopPadding = resolveContainerTopPadding(node, baseLabel);
|
|
1783
|
+
}
|
|
1784
|
+
for (const edge of edges) {
|
|
1785
|
+
edge.style = styleFor('edge', edge.id, edge.classes, edge.properties);
|
|
1786
|
+
}
|
|
1787
|
+
const roots = [...nodeMap.values()]
|
|
1788
|
+
.filter((node) => node.kind === 'Node' && !node.parentId)
|
|
1789
|
+
.map((node) => node.id);
|
|
1790
|
+
const reachable = new Set();
|
|
1791
|
+
const visit = (id) => {
|
|
1792
|
+
if (reachable.has(id))
|
|
1793
|
+
return;
|
|
1794
|
+
reachable.add(id);
|
|
1795
|
+
const node = nodeMap.get(id);
|
|
1796
|
+
if (!node)
|
|
1797
|
+
return;
|
|
1798
|
+
for (const child of node.children)
|
|
1799
|
+
visit(child);
|
|
1800
|
+
};
|
|
1801
|
+
for (const root of roots)
|
|
1802
|
+
visit(root);
|
|
1803
|
+
const filteredNodes = [...nodeMap.values()].filter((node) => reachable.has(node.id));
|
|
1804
|
+
const filteredEdges = edges.filter((edge) => reachable.has(edge.sourceId) && reachable.has(edge.targetId));
|
|
1805
|
+
return { nodes: filteredNodes, edges: filteredEdges, roots };
|
|
1806
|
+
}
|
|
1807
|
+
function estimateSize(kind, label, labelCount, style) {
|
|
1808
|
+
const styledWidth = toPositiveNumber(style.width);
|
|
1809
|
+
const styledHeight = toPositiveNumber(style.height);
|
|
1810
|
+
if (styledWidth && styledHeight) {
|
|
1811
|
+
return { width: styledWidth, height: styledHeight, hasExplicitWidth: true, hasExplicitHeight: true };
|
|
1812
|
+
}
|
|
1813
|
+
if (kind === 'Port') {
|
|
1814
|
+
return {
|
|
1815
|
+
width: styledWidth ?? 14,
|
|
1816
|
+
height: styledHeight ?? 14,
|
|
1817
|
+
hasExplicitWidth: styledWidth !== undefined,
|
|
1818
|
+
hasExplicitHeight: styledHeight !== undefined,
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
const textWidth = Math.max(24, label.length * 7);
|
|
1822
|
+
const baseWidth = Math.max(72, Math.min(280, textWidth + 26));
|
|
1823
|
+
if (kind === 'Compartment') {
|
|
1824
|
+
const size = { width: baseWidth, height: Math.max(44, 24 + labelCount * 16) };
|
|
1825
|
+
return {
|
|
1826
|
+
width: styledWidth ?? size.width,
|
|
1827
|
+
height: styledHeight ?? size.height,
|
|
1828
|
+
hasExplicitWidth: styledWidth !== undefined,
|
|
1829
|
+
hasExplicitHeight: styledHeight !== undefined,
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
const size = { width: baseWidth, height: Math.max(36, 20 + labelCount * 16) };
|
|
1833
|
+
return {
|
|
1834
|
+
width: styledWidth ?? size.width,
|
|
1835
|
+
height: styledHeight ?? size.height,
|
|
1836
|
+
hasExplicitWidth: styledWidth !== undefined,
|
|
1837
|
+
hasExplicitHeight: styledHeight !== undefined,
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
function resolveContainerTopPadding(node, labelText) {
|
|
1841
|
+
if (node.children.length === 0) {
|
|
1842
|
+
return 0;
|
|
1843
|
+
}
|
|
1844
|
+
const explicitPadding = toNonNegativeNumber(node.style.paddingTop);
|
|
1845
|
+
if (explicitPadding !== undefined) {
|
|
1846
|
+
return Math.ceil(explicitPadding);
|
|
1847
|
+
}
|
|
1848
|
+
const attrs = extractStyleAttrs(node.style);
|
|
1849
|
+
const label = asRecord(attrs.label);
|
|
1850
|
+
if (label?.display === 'none') {
|
|
1851
|
+
return 0;
|
|
1852
|
+
}
|
|
1853
|
+
const labelOpacity = toNonNegativeNumber(label?.opacity);
|
|
1854
|
+
if (labelOpacity !== undefined && labelOpacity <= 0) {
|
|
1855
|
+
return 0;
|
|
1856
|
+
}
|
|
1857
|
+
const fontSize = toPositiveNumber(label?.fontSize) ?? 12;
|
|
1858
|
+
const lineCount = Math.max(1, labelText.split('\n').filter((line) => line.trim().length > 0).length);
|
|
1859
|
+
return Math.ceil((fontSize * 1.2) * lineCount);
|
|
1860
|
+
}
|
|
1861
|
+
function localName(value) {
|
|
1862
|
+
const hash = value.lastIndexOf('#');
|
|
1863
|
+
if (hash >= 0 && hash < value.length - 1)
|
|
1864
|
+
return value.slice(hash + 1);
|
|
1865
|
+
const slash = value.lastIndexOf('/');
|
|
1866
|
+
if (slash >= 0 && slash < value.length - 1)
|
|
1867
|
+
return value.slice(slash + 1);
|
|
1868
|
+
return value;
|
|
1869
|
+
}
|
|
1870
|
+
function indexTriples(rows) {
|
|
1871
|
+
const bySubject = new Map();
|
|
1872
|
+
for (const row of rows) {
|
|
1873
|
+
const list = bySubject.get(row.s) ?? [];
|
|
1874
|
+
list.push(row);
|
|
1875
|
+
bySubject.set(row.s, list);
|
|
1876
|
+
}
|
|
1877
|
+
return { bySubject, rows };
|
|
1878
|
+
}
|
|
1879
|
+
function resolveCanvasHeight(options) {
|
|
1880
|
+
const canvas = isRecord(options?.canvas) ? options.canvas : undefined;
|
|
1881
|
+
const raw = canvas?.height ?? options?.height;
|
|
1882
|
+
const minHeight = numericCanvasMinHeight(options);
|
|
1883
|
+
if (typeof raw === 'number' && Number.isFinite(raw) && raw >= minHeight) {
|
|
1884
|
+
return `${raw}px`;
|
|
1885
|
+
}
|
|
1886
|
+
if (typeof raw === 'string') {
|
|
1887
|
+
const trimmed = raw.trim();
|
|
1888
|
+
if (/^\d+(\.\d+)?(px|vh|%)$/.test(trimmed)) {
|
|
1889
|
+
return trimmed;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
return '920px';
|
|
1893
|
+
}
|
|
1894
|
+
function resolveCanvasMinHeight(options) {
|
|
1895
|
+
return `${numericCanvasMinHeight(options)}px`;
|
|
1896
|
+
}
|
|
1897
|
+
function numericCanvasMinHeight(options) {
|
|
1898
|
+
const canvas = isRecord(options?.canvas) ? options.canvas : undefined;
|
|
1899
|
+
const raw = canvas?.minHeight ?? options?.minHeight;
|
|
1900
|
+
if (typeof raw === 'number' && Number.isFinite(raw)) {
|
|
1901
|
+
return Math.max(120, Math.round(raw));
|
|
1902
|
+
}
|
|
1903
|
+
return 220;
|
|
1904
|
+
}
|
|
1905
|
+
function isRecord(value) {
|
|
1906
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1907
|
+
}
|
|
1908
|
+
function resolveDagreLayoutOptions(options) {
|
|
1909
|
+
const layout = isRecord(options?.layout) ? options.layout : {};
|
|
1910
|
+
const rankdirRaw = typeof layout.rankdir === 'string' ? layout.rankdir.toUpperCase() : 'LR';
|
|
1911
|
+
const rankdir = (rankdirRaw === 'LR' || rankdirRaw === 'RL' || rankdirRaw === 'TB' || rankdirRaw === 'BT')
|
|
1912
|
+
? rankdirRaw
|
|
1913
|
+
: 'LR';
|
|
1914
|
+
const nodesep = clampLayoutNumber(layout.nodesep, 0, 400, 28);
|
|
1915
|
+
const ranksep = clampLayoutNumber(layout.ranksep, 0, 500, 64);
|
|
1916
|
+
const marginx = clampLayoutNumber(layout.marginx, 0, 200, 16);
|
|
1917
|
+
const marginy = clampLayoutNumber(layout.marginy, 0, 200, 16);
|
|
1918
|
+
return { rankdir, nodesep, ranksep, marginx, marginy };
|
|
1919
|
+
}
|
|
1920
|
+
function clampLayoutNumber(value, min, max, fallback) {
|
|
1921
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
1922
|
+
return fallback;
|
|
1923
|
+
}
|
|
1924
|
+
return Math.max(min, Math.min(max, Math.round(value)));
|
|
1925
|
+
}
|
|
1926
|
+
function parseDiagramStylesheet(options) {
|
|
1927
|
+
const stylesheet = options?.stylesheet;
|
|
1928
|
+
if (!Array.isArray(stylesheet)) {
|
|
1929
|
+
return [];
|
|
1930
|
+
}
|
|
1931
|
+
const rules = [];
|
|
1932
|
+
for (const entry of stylesheet) {
|
|
1933
|
+
if (!isRecord(entry))
|
|
1934
|
+
continue;
|
|
1935
|
+
const selector = typeof entry.selector === 'string' ? entry.selector.trim() : '';
|
|
1936
|
+
const style = isRecord(entry.style) ? entry.style : undefined;
|
|
1937
|
+
if (!selector || !style)
|
|
1938
|
+
continue;
|
|
1939
|
+
const parsed = parseStyleSelector(selector);
|
|
1940
|
+
if (!parsed)
|
|
1941
|
+
continue;
|
|
1942
|
+
rules.push({
|
|
1943
|
+
elementKind: parsed.elementKind,
|
|
1944
|
+
className: parsed.className,
|
|
1945
|
+
condition: parsed.condition,
|
|
1946
|
+
style: normalizeDiagramStyle(parsed.elementKind, style),
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
return rules;
|
|
1950
|
+
}
|
|
1951
|
+
function parseStyleSelector(selector) {
|
|
1952
|
+
const match = /^\s*(node|compartment|port|edge)(?:\.([A-Za-z0-9_-]+))?(?:\s*\[(.+)\]\s*)?$/i.exec(selector);
|
|
1953
|
+
if (!match)
|
|
1954
|
+
return undefined;
|
|
1955
|
+
return {
|
|
1956
|
+
elementKind: match[1].toLowerCase(),
|
|
1957
|
+
className: match[2]?.trim() || undefined,
|
|
1958
|
+
condition: match[3]?.trim() || undefined,
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
// OML node-level properties read directly from style (not from attrs).
|
|
1962
|
+
// These pass through as top-level keys so callers can read style.layout, style.width, etc.
|
|
1963
|
+
const OML_PASSTHROUGH_STYLE_KEYS = new Set([
|
|
1964
|
+
'layout', 'shape', 'width', 'height',
|
|
1965
|
+
'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
|
|
1966
|
+
]);
|
|
1967
|
+
// Record-valued top-level style keys that map directly into the attrs sub-tree under the same name.
|
|
1968
|
+
// 'label' and 'icon' cover nodes, compartments, ports, and edges.
|
|
1969
|
+
// The *-label and *-label-body variants are edge-specific (begin/center/end label text and background).
|
|
1970
|
+
const ATTRS_RECORD_STYLE_KEYS = new Set([
|
|
1971
|
+
'label', 'icon',
|
|
1972
|
+
'label-body',
|
|
1973
|
+
'begin-label', 'center-label', 'end-label',
|
|
1974
|
+
'begin-label-body', 'center-label-body', 'end-label-body',
|
|
1975
|
+
]);
|
|
1976
|
+
function normalizeDiagramStyle(elementKind, raw) {
|
|
1977
|
+
const isEdge = elementKind === 'edge';
|
|
1978
|
+
const shapeKey = isEdge ? 'line' : 'body';
|
|
1979
|
+
const shapeOverrides = {};
|
|
1980
|
+
const labelOverrides = {};
|
|
1981
|
+
const attrsRecords = {};
|
|
1982
|
+
const passthrough = {};
|
|
1983
|
+
for (const [rawKey, value] of Object.entries(raw)) {
|
|
1984
|
+
const key = rawKey.trim();
|
|
1985
|
+
if (!key || value === undefined || value === null)
|
|
1986
|
+
continue;
|
|
1987
|
+
// OML node-level keys: pass through to the top level of the normalized style object
|
|
1988
|
+
if (OML_PASSTHROUGH_STYLE_KEYS.has(key)) {
|
|
1989
|
+
passthrough[key] = value;
|
|
1990
|
+
continue;
|
|
1991
|
+
}
|
|
1992
|
+
// Record-valued keys: routed directly into attrs.X (e.g. label → attrs.label)
|
|
1993
|
+
if (ATTRS_RECORD_STYLE_KEYS.has(key)) {
|
|
1994
|
+
if (isRecord(value)) {
|
|
1995
|
+
attrsRecords[key] = value;
|
|
1996
|
+
}
|
|
1997
|
+
continue;
|
|
1998
|
+
}
|
|
1999
|
+
// Typographic flat keys: routed to attrs.label; X6-specific label props (from 'label:') take priority
|
|
2000
|
+
if (key === 'color') {
|
|
2001
|
+
labelOverrides.fill = value;
|
|
2002
|
+
continue;
|
|
2003
|
+
}
|
|
2004
|
+
if (key === 'font-size' || key === 'fontSize') {
|
|
2005
|
+
labelOverrides.fontSize = value;
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
if (key === 'font-family' || key === 'fontFamily') {
|
|
2009
|
+
labelOverrides.fontFamily = value;
|
|
2010
|
+
continue;
|
|
2011
|
+
}
|
|
2012
|
+
if (key === 'font-weight' || key === 'fontWeight') {
|
|
2013
|
+
labelOverrides.fontWeight = value;
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
if (key === 'font-style' || key === 'fontStyle') {
|
|
2017
|
+
labelOverrides.fontStyle = value;
|
|
2018
|
+
continue;
|
|
2019
|
+
}
|
|
2020
|
+
if (key === 'font-variant' || key === 'fontVariant') {
|
|
2021
|
+
labelOverrides.fontVariant = value;
|
|
2022
|
+
continue;
|
|
2023
|
+
}
|
|
2024
|
+
// All remaining flat keys are SVG/CSS presentation attributes routed to attrs.body (shapes) or
|
|
2025
|
+
// attrs.line (edges), camelCased. Any SVG attribute works without needing a new mapping —
|
|
2026
|
+
// stroke-dasharray, rx, ry, fill-opacity, stroke-linecap, opacity, etc.
|
|
2027
|
+
const camel = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
2028
|
+
shapeOverrides[camel] = value;
|
|
2029
|
+
// For edges, auto-propagate stroke colour to the arrowhead targetMarker
|
|
2030
|
+
if (isEdge && key === 'stroke') {
|
|
2031
|
+
shapeOverrides.targetMarker = { stroke: value };
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
const mergedAttrs = { ...attrsRecords };
|
|
2035
|
+
if (Object.keys(shapeOverrides).length > 0) {
|
|
2036
|
+
mergedAttrs[shapeKey] = shapeOverrides;
|
|
2037
|
+
}
|
|
2038
|
+
// Merge typographic flat keys (base) with explicit label: sub-object (takes priority)
|
|
2039
|
+
const explicitLabel = isRecord(attrsRecords.label) ? attrsRecords.label : {};
|
|
2040
|
+
const mergedLabel = { ...labelOverrides, ...explicitLabel };
|
|
2041
|
+
if (Object.keys(mergedLabel).length > 0) {
|
|
2042
|
+
mergedAttrs.label = mergedLabel;
|
|
2043
|
+
}
|
|
2044
|
+
const result = { ...passthrough };
|
|
2045
|
+
if (Object.keys(mergedAttrs).length > 0) {
|
|
2046
|
+
result.attrs = mergedAttrs;
|
|
2047
|
+
}
|
|
2048
|
+
return result;
|
|
2049
|
+
}
|
|
2050
|
+
function resolveElementStyle(elementKind, value, classes, properties, stylesheet) {
|
|
2051
|
+
if (stylesheet.length === 0) {
|
|
2052
|
+
return {};
|
|
2053
|
+
}
|
|
2054
|
+
const merged = {};
|
|
2055
|
+
for (const rule of stylesheet) {
|
|
2056
|
+
if (rule.elementKind !== elementKind)
|
|
2057
|
+
continue;
|
|
2058
|
+
if (rule.className && !classes.includes(rule.className))
|
|
2059
|
+
continue;
|
|
2060
|
+
if (rule.condition && !evaluateStyleCondition(rule.condition, value, properties))
|
|
2061
|
+
continue;
|
|
2062
|
+
mergeStyleRecord(merged, rule.style);
|
|
2063
|
+
}
|
|
2064
|
+
return merged;
|
|
2065
|
+
}
|
|
2066
|
+
function mergeStyleRecord(target, source) {
|
|
2067
|
+
for (const [key, value] of Object.entries(source)) {
|
|
2068
|
+
if (key !== 'attrs') {
|
|
2069
|
+
target[key] = value;
|
|
2070
|
+
continue;
|
|
2071
|
+
}
|
|
2072
|
+
const sourceAttrs = asRecord(value);
|
|
2073
|
+
if (!sourceAttrs) {
|
|
2074
|
+
target[key] = value;
|
|
2075
|
+
continue;
|
|
2076
|
+
}
|
|
2077
|
+
const targetAttrs = asRecord(target.attrs) ?? {};
|
|
2078
|
+
const mergedAttrs = { ...targetAttrs };
|
|
2079
|
+
for (const [part, partAttrsValue] of Object.entries(sourceAttrs)) {
|
|
2080
|
+
const sourcePartAttrs = asRecord(partAttrsValue);
|
|
2081
|
+
if (!sourcePartAttrs) {
|
|
2082
|
+
mergedAttrs[part] = partAttrsValue;
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
const targetPartAttrs = asRecord(mergedAttrs[part]) ?? {};
|
|
2086
|
+
mergedAttrs[part] = { ...targetPartAttrs, ...sourcePartAttrs };
|
|
2087
|
+
}
|
|
2088
|
+
target.attrs = mergedAttrs;
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
function evaluateStyleCondition(condition, value, properties) {
|
|
2092
|
+
const context = { value, properties };
|
|
2093
|
+
for (const [key, values] of Object.entries(properties)) {
|
|
2094
|
+
context[key] = values.length === 1 ? values[0] : values;
|
|
2095
|
+
}
|
|
2096
|
+
try {
|
|
2097
|
+
// eslint-disable-next-line no-new-func
|
|
2098
|
+
const fn = new Function('value', 'properties', 'ctx', `with (ctx) { return !!(${condition}); }`);
|
|
2099
|
+
return fn(value, properties, context);
|
|
2100
|
+
}
|
|
2101
|
+
catch {
|
|
2102
|
+
return false;
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
function extractStyleAttrs(style) {
|
|
2106
|
+
return asRecord(style.attrs) ?? {};
|
|
2107
|
+
}
|
|
2108
|
+
function asRecord(value) {
|
|
2109
|
+
return isRecord(value) ? value : undefined;
|
|
2110
|
+
}
|
|
2111
|
+
function extractImageHrefFromIcon(iconAttrs) {
|
|
2112
|
+
if (!iconAttrs)
|
|
2113
|
+
return undefined;
|
|
2114
|
+
const href = iconAttrs.href ?? iconAttrs.xlinkHref ?? iconAttrs['xlink:href'];
|
|
2115
|
+
if (typeof href !== 'string') {
|
|
2116
|
+
return undefined;
|
|
2117
|
+
}
|
|
2118
|
+
const trimmed = href.trim();
|
|
2119
|
+
if (!trimmed) {
|
|
2120
|
+
return undefined;
|
|
2121
|
+
}
|
|
2122
|
+
return normalizeImageHref(trimmed);
|
|
2123
|
+
}
|
|
2124
|
+
function normalizeImageHref(href) {
|
|
2125
|
+
if (!href || href.startsWith('#') || href.startsWith('data:') || /^[a-z][a-z0-9+.-]*:/i.test(href)) {
|
|
2126
|
+
return href;
|
|
2127
|
+
}
|
|
2128
|
+
try {
|
|
2129
|
+
return new URL(href, window.location.href).toString();
|
|
2130
|
+
}
|
|
2131
|
+
catch {
|
|
2132
|
+
return href;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
function readIconPaths(iconAttrs) {
|
|
2136
|
+
if (!iconAttrs) {
|
|
2137
|
+
return [];
|
|
2138
|
+
}
|
|
2139
|
+
const paths = Array.isArray(iconAttrs.paths) ? iconAttrs.paths : undefined;
|
|
2140
|
+
if (!paths || paths.length === 0) {
|
|
2141
|
+
return [];
|
|
2142
|
+
}
|
|
2143
|
+
const normalized = [];
|
|
2144
|
+
for (const entry of paths) {
|
|
2145
|
+
if (!isRecord(entry)) {
|
|
2146
|
+
continue;
|
|
2147
|
+
}
|
|
2148
|
+
const d = typeof entry.d === 'string' ? entry.d.trim() : '';
|
|
2149
|
+
if (!d) {
|
|
2150
|
+
continue;
|
|
2151
|
+
}
|
|
2152
|
+
const attrs = { d };
|
|
2153
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
2154
|
+
if (key === 'd' || value === undefined || value === null) {
|
|
2155
|
+
continue;
|
|
2156
|
+
}
|
|
2157
|
+
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
|
|
2158
|
+
continue;
|
|
2159
|
+
}
|
|
2160
|
+
attrs[key] = value;
|
|
2161
|
+
}
|
|
2162
|
+
normalized.push(attrs);
|
|
2163
|
+
}
|
|
2164
|
+
return normalized;
|
|
2165
|
+
}
|
|
2166
|
+
function resolveRenderNodeShape(style) {
|
|
2167
|
+
const raw = typeof style.shape === 'string' ? style.shape.trim().toLowerCase() : '';
|
|
2168
|
+
if (raw === 'ellipse' || raw === 'circle') {
|
|
2169
|
+
return {
|
|
2170
|
+
graphShape: 'ellipse',
|
|
2171
|
+
bodyTag: 'ellipse',
|
|
2172
|
+
bodyDefaults: {
|
|
2173
|
+
refCx: '50%',
|
|
2174
|
+
refCy: '50%',
|
|
2175
|
+
refRx: '50%',
|
|
2176
|
+
refRy: '50%',
|
|
2177
|
+
},
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
return {
|
|
2181
|
+
graphShape: 'rect',
|
|
2182
|
+
bodyTag: 'rect',
|
|
2183
|
+
bodyDefaults: {},
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
function resolveNodeAttrs(style) {
|
|
2187
|
+
const attrs = extractStyleAttrs(style);
|
|
2188
|
+
const body = asRecord(attrs.body);
|
|
2189
|
+
const label = asRecord(attrs.label);
|
|
2190
|
+
const icon = asRecord(attrs.icon);
|
|
2191
|
+
const imageUrl = extractImageHrefFromIcon(icon);
|
|
2192
|
+
const iconPaths = readIconPaths(icon);
|
|
2193
|
+
const usesImage = !!imageUrl;
|
|
2194
|
+
const bodyAttrs = {
|
|
2195
|
+
fill: CSS_EDITOR_BACKGROUND,
|
|
2196
|
+
stroke: CSS_EDITOR_FOREGROUND,
|
|
2197
|
+
strokeWidth: 1,
|
|
2198
|
+
...(body ?? {}),
|
|
2199
|
+
};
|
|
2200
|
+
const labelAttrs = {
|
|
2201
|
+
fill: CSS_EDITOR_FOREGROUND,
|
|
2202
|
+
...(label ?? {}),
|
|
2203
|
+
};
|
|
2204
|
+
const iconSvgAttrs = {
|
|
2205
|
+
refX: 0,
|
|
2206
|
+
refY: 0,
|
|
2207
|
+
refWidth: '100%',
|
|
2208
|
+
refHeight: '100%',
|
|
2209
|
+
viewBox: (typeof icon?.viewBox === 'string' && icon.viewBox.trim().length > 0)
|
|
2210
|
+
? icon.viewBox.trim()
|
|
2211
|
+
: '0 0 24 24',
|
|
2212
|
+
preserveAspectRatio: icon?.preserveAspectRatio ?? 'xMidYMid meet',
|
|
2213
|
+
display: iconPaths.length > 0 ? undefined : 'none',
|
|
2214
|
+
};
|
|
2215
|
+
const imageAttrs = {
|
|
2216
|
+
refX: 0,
|
|
2217
|
+
refY: 0,
|
|
2218
|
+
refWidth: '100%',
|
|
2219
|
+
refHeight: '100%',
|
|
2220
|
+
opacity: 1,
|
|
2221
|
+
display: usesImage ? undefined : 'none',
|
|
2222
|
+
...(icon ?? {}),
|
|
2223
|
+
};
|
|
2224
|
+
delete imageAttrs.paths;
|
|
2225
|
+
delete imageAttrs.viewBox;
|
|
2226
|
+
if (usesImage) {
|
|
2227
|
+
imageAttrs.href = imageUrl;
|
|
2228
|
+
imageAttrs.xlinkHref = imageUrl;
|
|
2229
|
+
imageAttrs['xlink:href'] = imageUrl;
|
|
2230
|
+
}
|
|
2231
|
+
return { bodyAttrs, labelAttrs, iconSvgAttrs, iconPathAttrs: iconPaths, imageAttrs };
|
|
2232
|
+
}
|
|
2233
|
+
function toNonNegativeNumber(value) {
|
|
2234
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
|
|
2235
|
+
return value;
|
|
2236
|
+
}
|
|
2237
|
+
if (typeof value === 'string') {
|
|
2238
|
+
const parsed = Number.parseFloat(value);
|
|
2239
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
2240
|
+
return parsed;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
return undefined;
|
|
2244
|
+
}
|
|
2245
|
+
function resolveEdgeLineAttrs(style) {
|
|
2246
|
+
const attrs = extractStyleAttrs(style);
|
|
2247
|
+
const line = asRecord(attrs.line);
|
|
2248
|
+
return {
|
|
2249
|
+
fill: 'none',
|
|
2250
|
+
stroke: CSS_EDITOR_FOREGROUND,
|
|
2251
|
+
strokeOpacity: 0.85,
|
|
2252
|
+
strokeWidth: 1.2,
|
|
2253
|
+
targetMarker: {
|
|
2254
|
+
name: 'block',
|
|
2255
|
+
open: true,
|
|
2256
|
+
size: 10,
|
|
2257
|
+
stroke: CSS_EDITOR_FOREGROUND,
|
|
2258
|
+
fill: 'none',
|
|
2259
|
+
},
|
|
2260
|
+
...(line ?? {}),
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
2263
|
+
function resolveEdgeLabelAttrs(style, placement, text) {
|
|
2264
|
+
const attrs = extractStyleAttrs(style);
|
|
2265
|
+
const base = asRecord(attrs.label);
|
|
2266
|
+
const specific = asRecord(attrs[`${placement}-label`]);
|
|
2267
|
+
return {
|
|
2268
|
+
text,
|
|
2269
|
+
fill: CSS_EDITOR_FOREGROUND,
|
|
2270
|
+
fontFamily: 'var(--vscode-editor-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)',
|
|
2271
|
+
fontSize: 12,
|
|
2272
|
+
...(base ?? {}),
|
|
2273
|
+
...(specific ?? {}),
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
function resolveEdgeLabelBodyAttrs(style, placement) {
|
|
2277
|
+
const attrs = extractStyleAttrs(style);
|
|
2278
|
+
const base = asRecord(attrs['label-body']);
|
|
2279
|
+
const specific = asRecord(attrs[`${placement}-label-body`]);
|
|
2280
|
+
return {
|
|
2281
|
+
fill: CSS_EDITOR_BACKGROUND,
|
|
2282
|
+
fillOpacity: 0.9,
|
|
2283
|
+
stroke: 'none',
|
|
2284
|
+
strokeWidth: 0,
|
|
2285
|
+
...(base ?? {}),
|
|
2286
|
+
...(specific ?? {}),
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
function resolvePortAttrs(style, classes, text) {
|
|
2290
|
+
const attrs = extractStyleAttrs(style);
|
|
2291
|
+
const body = asRecord(attrs.body);
|
|
2292
|
+
const icon = asRecord(attrs.icon);
|
|
2293
|
+
const label = asRecord(attrs.label);
|
|
2294
|
+
const imageUrl = extractImageHrefFromIcon(icon);
|
|
2295
|
+
return {
|
|
2296
|
+
body: {
|
|
2297
|
+
width: 12,
|
|
2298
|
+
height: 12,
|
|
2299
|
+
x: -6,
|
|
2300
|
+
y: -6,
|
|
2301
|
+
class: ['oml-port-body', ...classes].join(' '),
|
|
2302
|
+
magnet: false,
|
|
2303
|
+
stroke: CSS_FOCUS_BORDER,
|
|
2304
|
+
strokeWidth: 1,
|
|
2305
|
+
fill: CSS_EDITOR_BACKGROUND,
|
|
2306
|
+
...(body ?? {}),
|
|
2307
|
+
},
|
|
2308
|
+
icon: {
|
|
2309
|
+
width: 12,
|
|
2310
|
+
height: 12,
|
|
2311
|
+
x: -6,
|
|
2312
|
+
y: -6,
|
|
2313
|
+
preserveAspectRatio: 'xMidYMid meet',
|
|
2314
|
+
display: imageUrl ? undefined : 'none',
|
|
2315
|
+
...(icon ?? {}),
|
|
2316
|
+
...(imageUrl ? { href: imageUrl, xlinkHref: imageUrl, 'xlink:href': imageUrl } : {}),
|
|
2317
|
+
},
|
|
2318
|
+
label: {
|
|
2319
|
+
text,
|
|
2320
|
+
fill: CSS_EDITOR_FOREGROUND,
|
|
2321
|
+
fontFamily: 'var(--vscode-editor-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)',
|
|
2322
|
+
fontSize: 12,
|
|
2323
|
+
textAnchor: 'start',
|
|
2324
|
+
x: 10,
|
|
2325
|
+
dy: '0.9em',
|
|
2326
|
+
...(label ?? {}),
|
|
2327
|
+
},
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
function toPositiveNumber(value) {
|
|
2331
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|
|
2332
|
+
return value;
|
|
2333
|
+
}
|
|
2334
|
+
if (typeof value === 'string') {
|
|
2335
|
+
const parsed = Number.parseFloat(value);
|
|
2336
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
2337
|
+
return parsed;
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
return undefined;
|
|
2341
|
+
}
|
|
2342
|
+
function toIdentifier(value) {
|
|
2343
|
+
return value.replace(/[^A-Za-z0-9_$]/g, '_');
|
|
2344
|
+
}
|
|
2345
|
+
async function waitForCanvasReady(canvas) {
|
|
2346
|
+
const maxAttempts = 24;
|
|
2347
|
+
for (let i = 0; i < maxAttempts; i += 1) {
|
|
2348
|
+
if (canvas.isConnected && canvas.clientWidth > 0 && canvas.clientHeight > 0) {
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
await new Promise((resolve) => requestAnimationFrame(() => resolve(undefined)));
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
//# sourceMappingURL=diagram-renderer.js.map
|