@oml/markdown 0.10.0 → 0.12.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/out/md/md-execution.d.ts +16 -0
- package/out/md/md-executor.d.ts +1 -0
- package/out/md/md-executor.js +219 -35
- package/out/md/md-executor.js.map +1 -1
- package/out/renderers/chart-renderer.js +72 -4
- package/out/renderers/chart-renderer.js.map +1 -1
- package/out/renderers/diagram-renderer.js +896 -245
- package/out/renderers/diagram-renderer.js.map +1 -1
- package/out/renderers/graph-renderer.js +452 -18
- package/out/renderers/graph-renderer.js.map +1 -1
- package/out/renderers/matrix-renderer.d.ts +0 -2
- package/out/renderers/matrix-renderer.js +45 -40
- package/out/renderers/matrix-renderer.js.map +1 -1
- package/out/renderers/renderer.d.ts +4 -1
- package/out/renderers/renderer.js +98 -0
- package/out/renderers/renderer.js.map +1 -1
- package/out/renderers/table-renderer.d.ts +12 -2
- package/out/renderers/table-renderer.js +126 -39
- package/out/renderers/table-renderer.js.map +1 -1
- package/out/renderers/types.d.ts +16 -0
- package/out/renderers/wikilink-utils.d.ts +1 -0
- package/out/renderers/wikilink-utils.js +60 -32
- package/out/renderers/wikilink-utils.js.map +1 -1
- package/out/static/browser-runtime.bundle.js +8011 -1292
- package/out/static/browser-runtime.bundle.js.map +4 -4
- package/out/static/browser-runtime.js +15 -2
- package/out/static/browser-runtime.js.map +1 -1
- package/package.json +2 -2
- package/src/md/md-execution.ts +20 -0
- package/src/md/md-executor.ts +268 -40
- package/src/renderers/chart-renderer.ts +93 -2
- package/src/renderers/diagram-renderer.ts +964 -253
- package/src/renderers/graph-renderer.ts +512 -12
- package/src/renderers/matrix-renderer.ts +57 -44
- package/src/renderers/renderer.ts +105 -1
- package/src/renderers/table-renderer.ts +190 -41
- package/src/renderers/types.ts +20 -0
- package/src/renderers/wikilink-utils.ts +66 -31
- package/src/static/browser-runtime.ts +20 -2
- package/src/static/markdown-webview.css +44 -15
|
@@ -11,7 +11,7 @@ const TYPE_IRIS = {
|
|
|
11
11
|
};
|
|
12
12
|
const LIST_ITEM_IRI = `${D}ListItem`;
|
|
13
13
|
const CSS_EDITOR_FOREGROUND = 'var(--vscode-editor-foreground, var(--oml-static-foreground, #24292f))';
|
|
14
|
-
const CSS_EDITOR_BACKGROUND = 'var(--vscode-editor-background,
|
|
14
|
+
const CSS_EDITOR_BACKGROUND = 'var(--vscode-editor-background, var(--oml-static-background, #ffffff))';
|
|
15
15
|
const CSS_CANVAS_BACKGROUND = 'var(--vscode-editor-background, var(--oml-static-background, #ffffff))';
|
|
16
16
|
const CSS_FOCUS_BORDER = 'var(--vscode-focusBorder, var(--oml-static-link, #0969da))';
|
|
17
17
|
let diagramCanvasSeq = 0;
|
|
@@ -55,12 +55,15 @@ export class DiagramMarkdownBlockRenderer extends CanvasMarkdownBlockRenderer {
|
|
|
55
55
|
const tripleIndex = indexTriples(rows);
|
|
56
56
|
const stylesheet = parseDiagramStylesheet(result.options);
|
|
57
57
|
const compiled = compileDiagramGraph(tripleIndex, stylesheet);
|
|
58
|
-
const
|
|
58
|
+
const diagramStyle = resolveElementStyle('diagram', 'diagram', [], {}, stylesheet);
|
|
59
|
+
const layoutOptions = resolveDagreLayoutOptions(diagramStyle);
|
|
60
|
+
const rootSpacing = resolveRootSpacing(diagramStyle);
|
|
59
61
|
if (compiled.nodes.length === 0) {
|
|
60
62
|
container.appendChild(this.createMessageContainer('No diagram nodes were inferred from the diagram namespace triples.'));
|
|
61
63
|
return container;
|
|
62
64
|
}
|
|
63
65
|
void renderWithX6(canvas, baseId, compiled, layoutOptions, {
|
|
66
|
+
rootSpacing,
|
|
64
67
|
downloadSvg: (content) => this.requestTextFileDownload(content, 'diagram', 'svg'),
|
|
65
68
|
}).catch((error) => {
|
|
66
69
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -76,7 +79,7 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
76
79
|
return;
|
|
77
80
|
}
|
|
78
81
|
const GraphCtor = await loadX6GraphCtor();
|
|
79
|
-
const layout = await layoutGraphDagre(graph, layoutOptions);
|
|
82
|
+
const layout = await layoutGraphDagre(graph, layoutOptions, actions.rootSpacing);
|
|
80
83
|
const minHeight = asFiniteNumber(parseCssPixels(liveCanvas.style.getPropertyValue('--oml-diagram-min-height')), numericCanvasMinHeight(undefined));
|
|
81
84
|
const desiredHeight = Math.max(minHeight, Math.ceil(layout.contentHeight + 12));
|
|
82
85
|
setLockedCanvasHeight(liveCanvas, desiredHeight, minHeight);
|
|
@@ -84,30 +87,23 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
84
87
|
let isResizingCanvas = false;
|
|
85
88
|
const isPortPointerTarget = (event) => {
|
|
86
89
|
const target = event?.target;
|
|
87
|
-
if (target?.closest('.x6-port, .x6-port-body, .oml-port-body')) {
|
|
90
|
+
if (target?.closest('.x6-port, .x6-port-body, .oml-port-body, [port]')) {
|
|
88
91
|
return true;
|
|
89
92
|
}
|
|
90
93
|
const path = typeof event?.composedPath === 'function' ? event.composedPath() : [];
|
|
91
94
|
return path.some((entry) => (entry instanceof Element
|
|
92
95
|
&& (entry.classList.contains('x6-port')
|
|
93
96
|
|| 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;
|
|
97
|
+
|| entry.classList.contains('oml-port-body')
|
|
98
|
+
|| entry.hasAttribute('port'))));
|
|
105
99
|
};
|
|
106
100
|
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
107
101
|
const isCompartmentNode = (node) => {
|
|
108
102
|
const nodeId = String(node?.id ?? '');
|
|
109
103
|
return nodeById.get(nodeId)?.kind === 'Compartment';
|
|
110
104
|
};
|
|
105
|
+
const boundPortContainers = new WeakSet();
|
|
106
|
+
let nodeTransform;
|
|
111
107
|
const graphView = new GraphCtor({
|
|
112
108
|
container: liveCanvas,
|
|
113
109
|
autoResize: false,
|
|
@@ -118,10 +114,14 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
118
114
|
minScale: 0.4,
|
|
119
115
|
maxScale: 2.5,
|
|
120
116
|
factor: 1.1,
|
|
117
|
+
modifiers: ['meta', 'ctrl'],
|
|
121
118
|
},
|
|
122
119
|
connecting: {
|
|
123
120
|
router: 'normal',
|
|
124
|
-
connector:
|
|
121
|
+
connector: {
|
|
122
|
+
name: 'jumpover',
|
|
123
|
+
args: { size: 5 },
|
|
124
|
+
},
|
|
125
125
|
allowBlank: false,
|
|
126
126
|
allowNode: false,
|
|
127
127
|
allowPort: false,
|
|
@@ -137,13 +137,131 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
137
137
|
edgeMovable: false,
|
|
138
138
|
vertexMovable: false,
|
|
139
139
|
arrowheadMovable: false,
|
|
140
|
-
labelMovable:
|
|
140
|
+
labelMovable: true,
|
|
141
141
|
};
|
|
142
142
|
},
|
|
143
143
|
background: {
|
|
144
144
|
color: CSS_CANVAS_BACKGROUND,
|
|
145
145
|
},
|
|
146
|
+
onPortRendered: ({ port, node, container }) => {
|
|
147
|
+
if (boundPortContainers.has(container)) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
boundPortContainers.add(container);
|
|
151
|
+
const portIri = String(port.id ?? '').trim();
|
|
152
|
+
const applyPortNativeTitle = () => {
|
|
153
|
+
if (!portIri) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
container.setAttribute('title', portIri);
|
|
157
|
+
for (const element of Array.from(container.querySelectorAll('*'))) {
|
|
158
|
+
element.setAttribute('title', portIri);
|
|
159
|
+
if (element instanceof SVGElement) {
|
|
160
|
+
let titleNode = null;
|
|
161
|
+
for (const child of Array.from(element.children)) {
|
|
162
|
+
if (child instanceof SVGTitleElement) {
|
|
163
|
+
titleNode = child;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!titleNode) {
|
|
168
|
+
titleNode = document.createElementNS('http://www.w3.org/2000/svg', 'title');
|
|
169
|
+
element.insertBefore(titleNode, element.firstChild);
|
|
170
|
+
}
|
|
171
|
+
titleNode.textContent = portIri;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
container.setAttribute('data-port-id', String(port.id));
|
|
176
|
+
applyPortNativeTitle();
|
|
177
|
+
container.style.cursor = 'grab';
|
|
178
|
+
container.style.touchAction = 'none';
|
|
179
|
+
container.addEventListener('mouseenter', (event) => {
|
|
180
|
+
const iri = portIri;
|
|
181
|
+
if (!iri) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
applyPortNativeTitle();
|
|
185
|
+
const rect = container.getBoundingClientRect();
|
|
186
|
+
liveCanvas.dispatchEvent(new CustomEvent('md-show-iri-hover', {
|
|
187
|
+
bubbles: true,
|
|
188
|
+
detail: {
|
|
189
|
+
iri,
|
|
190
|
+
previewEnabled: /^Mac/i.test(navigator.platform) ? event.metaKey : event.ctrlKey,
|
|
191
|
+
anchorRect: {
|
|
192
|
+
left: rect.left,
|
|
193
|
+
right: rect.right,
|
|
194
|
+
top: rect.top,
|
|
195
|
+
bottom: rect.bottom,
|
|
196
|
+
width: rect.width,
|
|
197
|
+
height: rect.height,
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
}));
|
|
201
|
+
});
|
|
202
|
+
container.addEventListener('mouseleave', () => {
|
|
203
|
+
liveCanvas.dispatchEvent(new CustomEvent('md-hide-iri-hover', { bubbles: true }));
|
|
204
|
+
});
|
|
205
|
+
container.addEventListener('dblclick', (event) => {
|
|
206
|
+
const iri = String(port.id ?? '').trim();
|
|
207
|
+
if (!iri) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
event.preventDefault();
|
|
211
|
+
event.stopPropagation();
|
|
212
|
+
if (typeof event.stopImmediatePropagation === 'function') {
|
|
213
|
+
event.stopImmediatePropagation();
|
|
214
|
+
}
|
|
215
|
+
liveCanvas.dispatchEvent(new CustomEvent('md-navigate-iri', {
|
|
216
|
+
bubbles: true,
|
|
217
|
+
detail: { iri },
|
|
218
|
+
}));
|
|
219
|
+
});
|
|
220
|
+
container.addEventListener('pointerdown', (event) => {
|
|
221
|
+
if (typeof graphView.cleanSelection === 'function') {
|
|
222
|
+
graphView.cleanSelection();
|
|
223
|
+
}
|
|
224
|
+
nodeTransform.clearWidgets();
|
|
225
|
+
const owner = graphView.getCellById(String(node.id));
|
|
226
|
+
if (!owner) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
startPortDrag(event.clientX, event.clientY, owner, String(port.id), event.pointerId, container);
|
|
230
|
+
event.preventDefault();
|
|
231
|
+
event.stopPropagation();
|
|
232
|
+
if (typeof event.stopImmediatePropagation === 'function') {
|
|
233
|
+
event.stopImmediatePropagation();
|
|
234
|
+
}
|
|
235
|
+
if (typeof container.setPointerCapture === 'function') {
|
|
236
|
+
try {
|
|
237
|
+
container.setPointerCapture(event.pointerId);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// Ignore capture failures; window listeners still handle the drag.
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
const x6Mod = await import('@antv/x6');
|
|
247
|
+
const TransformCtor = x6Mod.Transform;
|
|
248
|
+
if (typeof TransformCtor !== 'function') {
|
|
249
|
+
throw new Error('X6 Transform plugin is unavailable in @antv/x6');
|
|
250
|
+
}
|
|
251
|
+
nodeTransform = new TransformCtor({
|
|
252
|
+
resizing: {
|
|
253
|
+
enabled: (node) => node?.getData?.()?.kind === 'Node',
|
|
254
|
+
minWidth: 48,
|
|
255
|
+
minHeight: 32,
|
|
256
|
+
orthogonal: true,
|
|
257
|
+
restrict: false,
|
|
258
|
+
autoScroll: false,
|
|
259
|
+
preserveAspectRatio: false,
|
|
260
|
+
allowReverse: false,
|
|
261
|
+
},
|
|
262
|
+
rotating: false,
|
|
146
263
|
});
|
|
264
|
+
graphView.use(nodeTransform);
|
|
147
265
|
const toPlainRect = (value) => {
|
|
148
266
|
if (!value)
|
|
149
267
|
return undefined;
|
|
@@ -178,12 +296,7 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
178
296
|
for (const list of portsByOwner.values()) {
|
|
179
297
|
list.sort((a, b) => a.id.localeCompare(b.id));
|
|
180
298
|
}
|
|
181
|
-
const
|
|
182
|
-
for (const [ownerId, ports] of portsByOwner.entries()) {
|
|
183
|
-
for (const port of ports) {
|
|
184
|
-
ownerByPortId.set(port.id, ownerId);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
299
|
+
const portPlacementById = computeDefaultPortPlacements(graph, nodeById, portsByOwner, layout.boxes);
|
|
187
300
|
const ordered = [...graph.nodes]
|
|
188
301
|
.filter((node) => node.kind !== 'Port')
|
|
189
302
|
.sort((a, b) => nodeDepth(a.id, nodeById) - nodeDepth(b.id, nodeById));
|
|
@@ -194,19 +307,35 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
194
307
|
const labelText = node.labels.length > 0 ? node.labels.join('\n') : localName(node.id);
|
|
195
308
|
const resolvedStyle = node.style;
|
|
196
309
|
const resolvedShape = resolveRenderNodeShape(resolvedStyle);
|
|
310
|
+
const { bodyAttrs, labelAttrs, iconSvgAttrs, iconPathAttrs, imageAttrs } = resolveNodeAttrs(resolvedStyle);
|
|
197
311
|
const x = box.x;
|
|
198
312
|
const y = box.y;
|
|
199
313
|
const ownerPorts = portsByOwner.get(node.id) ?? [];
|
|
200
|
-
const portItems = ownerPorts.map((port
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
314
|
+
const portItems = ownerPorts.map((port) => {
|
|
315
|
+
const placement = portPlacementById.get(port.id) ?? { side: 'right', ratio: 0.5 };
|
|
316
|
+
const ratio = clamp(placement.ratio, 0.05, 0.95);
|
|
317
|
+
const position = placement.side === 'left' || placement.side === 'right'
|
|
318
|
+
? {
|
|
319
|
+
x: placement.side === 'left' ? 0 : box.width,
|
|
320
|
+
y: ratio * box.height,
|
|
321
|
+
}
|
|
322
|
+
: {
|
|
323
|
+
x: ratio * box.width,
|
|
324
|
+
y: placement.side === 'top' ? 0 : box.height,
|
|
325
|
+
};
|
|
326
|
+
const portText = port.labels[0];
|
|
327
|
+
return {
|
|
328
|
+
id: port.id,
|
|
329
|
+
group: 'boundary',
|
|
330
|
+
args: {
|
|
331
|
+
x: position.x,
|
|
332
|
+
y: position.y,
|
|
333
|
+
side: placement.side,
|
|
334
|
+
ratio,
|
|
335
|
+
},
|
|
336
|
+
attrs: resolvePortAttrs(port.style, port.classes, portText, placement.side, typeof bodyAttrs.stroke === 'string' ? bodyAttrs.stroke : undefined),
|
|
337
|
+
};
|
|
338
|
+
});
|
|
210
339
|
const iconPathSelectors = iconPathAttrs.map((_, index) => `iconPath${index}`);
|
|
211
340
|
graphView.addNode({
|
|
212
341
|
id: node.id,
|
|
@@ -310,7 +439,7 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
310
439
|
},
|
|
311
440
|
items: portItems,
|
|
312
441
|
} : undefined,
|
|
313
|
-
zIndex:
|
|
442
|
+
zIndex: 50,
|
|
314
443
|
data: {
|
|
315
444
|
kind: node.kind,
|
|
316
445
|
ownerId: node.parentId,
|
|
@@ -457,11 +586,32 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
457
586
|
let maxBottom = Number.NEGATIVE_INFINITY;
|
|
458
587
|
const childCells = [];
|
|
459
588
|
const childDebug = [];
|
|
589
|
+
const childGeometryBounds = (child) => {
|
|
590
|
+
if (!child || typeof child.size !== 'function') {
|
|
591
|
+
return undefined;
|
|
592
|
+
}
|
|
593
|
+
const size = child.size();
|
|
594
|
+
const width = Number(size?.width);
|
|
595
|
+
const height = Number(size?.height);
|
|
596
|
+
let position;
|
|
597
|
+
if (typeof child.getPosition === 'function') {
|
|
598
|
+
position = child.getPosition();
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
position = child.position;
|
|
602
|
+
}
|
|
603
|
+
const x = Number(position?.x);
|
|
604
|
+
const y = Number(position?.y);
|
|
605
|
+
if (![x, y, width, height].every(Number.isFinite)) {
|
|
606
|
+
return undefined;
|
|
607
|
+
}
|
|
608
|
+
return { x, y, width, height };
|
|
609
|
+
};
|
|
460
610
|
for (const childId of childIds) {
|
|
461
611
|
const child = graphView.getCellById(childId);
|
|
462
|
-
|
|
612
|
+
const absBBox = childGeometryBounds(child);
|
|
613
|
+
if (!absBBox)
|
|
463
614
|
continue;
|
|
464
|
-
const absBBox = child.getBBox();
|
|
465
615
|
const relLeft = absBBox.x - containerBBox.x;
|
|
466
616
|
const relTop = absBBox.y - containerBBox.y;
|
|
467
617
|
minLeft = Math.min(minLeft, relLeft);
|
|
@@ -493,10 +643,11 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
493
643
|
// container origin (without moving its embedded children, which keep their
|
|
494
644
|
// absolute positions) and expand the size to compensate so the opposite edge
|
|
495
645
|
// is preserved.
|
|
496
|
-
|
|
497
|
-
const
|
|
498
|
-
const
|
|
499
|
-
const
|
|
646
|
+
const spacing = resolveBoxSpacing(containerSpec.style, 0);
|
|
647
|
+
const minInsetX = spacing.marginLeft + spacing.paddingLeft;
|
|
648
|
+
const minInsetY = spacing.marginTop + spacing.paddingTop;
|
|
649
|
+
const shiftX = minLeft < minInsetX ? minLeft - minInsetX : 0;
|
|
650
|
+
const shiftY = minTop < minInsetY ? minTop - minInsetY : 0;
|
|
500
651
|
// Dimensions needed relative to the (possibly shifted) new origin.
|
|
501
652
|
const baseWidth = containerSize.width - shiftX;
|
|
502
653
|
const baseHeight = containerSize.height - shiftY;
|
|
@@ -509,7 +660,8 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
509
660
|
childIds,
|
|
510
661
|
containerBBox: toPlainRect(containerBBox),
|
|
511
662
|
containerSize: toPlainRect({ x: 0, y: 0, ...containerSize }),
|
|
512
|
-
|
|
663
|
+
minInsetX,
|
|
664
|
+
minInsetY,
|
|
513
665
|
bounds: { minLeft, minTop, maxRight, maxBottom },
|
|
514
666
|
shift: { shiftX, shiftY },
|
|
515
667
|
childDebug,
|
|
@@ -526,6 +678,7 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
526
678
|
to: { width: neededWidth, height: neededHeight },
|
|
527
679
|
});
|
|
528
680
|
container.resize(neededWidth, neededHeight);
|
|
681
|
+
syncOwnedPortPositions(containerId);
|
|
529
682
|
}
|
|
530
683
|
else {
|
|
531
684
|
logResize('container-no-resize', { containerId });
|
|
@@ -610,6 +763,48 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
610
763
|
return;
|
|
611
764
|
growAncestorContainers(node);
|
|
612
765
|
});
|
|
766
|
+
const syncOwnedPortPositions = (ownerId) => {
|
|
767
|
+
const owner = graphView.getCellById(ownerId);
|
|
768
|
+
if (!owner || typeof owner.size !== 'function' || typeof owner.getPorts !== 'function' || typeof owner.setPortProp !== 'function') {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const size = owner.size();
|
|
772
|
+
const portIds = owner.getPorts()
|
|
773
|
+
.map((port) => String(port?.id ?? ''))
|
|
774
|
+
.filter((portId) => portId.length > 0);
|
|
775
|
+
for (const portId of portIds) {
|
|
776
|
+
const placement = portPlacementById.get(portId) ?? { side: 'right', ratio: 0.5 };
|
|
777
|
+
const ratio = clamp(placement.ratio, 0.05, 0.95);
|
|
778
|
+
const nextArgs = placement.side === 'left' || placement.side === 'right'
|
|
779
|
+
? {
|
|
780
|
+
x: placement.side === 'left' ? 0 : size.width,
|
|
781
|
+
y: ratio * size.height,
|
|
782
|
+
side: placement.side,
|
|
783
|
+
ratio,
|
|
784
|
+
}
|
|
785
|
+
: {
|
|
786
|
+
x: ratio * size.width,
|
|
787
|
+
y: placement.side === 'top' ? 0 : size.height,
|
|
788
|
+
side: placement.side,
|
|
789
|
+
ratio,
|
|
790
|
+
};
|
|
791
|
+
owner.setPortProp(portId, {
|
|
792
|
+
args: nextArgs,
|
|
793
|
+
label: {
|
|
794
|
+
position: resolveBorderPortLabelPosition(placement.side),
|
|
795
|
+
},
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
graphView.on('node:resizing', ({ node }) => {
|
|
800
|
+
syncOwnedPortPositions(String(node?.id ?? ''));
|
|
801
|
+
});
|
|
802
|
+
graphView.on('node:resized', ({ node, options }) => {
|
|
803
|
+
if (options?.silent)
|
|
804
|
+
return;
|
|
805
|
+
syncOwnedPortPositions(String(node?.id ?? ''));
|
|
806
|
+
growAncestorContainers(node);
|
|
807
|
+
});
|
|
613
808
|
const edgeLabelPosition = (placement) => {
|
|
614
809
|
if (placement === 'begin')
|
|
615
810
|
return 0.15;
|
|
@@ -617,105 +812,104 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
617
812
|
return 0.85;
|
|
618
813
|
return 0.5;
|
|
619
814
|
};
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
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]));
|
|
815
|
+
const directedPairKey = (sourceId, targetId) => `${sourceId}=>${targetId}`;
|
|
816
|
+
const undirectedPairKey = (leftId, rightId) => (leftId < rightId ? `${leftId}<=>${rightId}` : `${rightId}<=>${leftId}`);
|
|
817
|
+
const edgeIdsByPair = new Map();
|
|
628
818
|
const undirectedEdgeIdsByPair = new Map();
|
|
629
819
|
for (const edge of graph.edges) {
|
|
630
|
-
|
|
631
|
-
const targetOwner = endpointOwnerId(edge.targetId);
|
|
632
|
-
if (!sourceOwner || !targetOwner || sourceOwner === targetOwner)
|
|
820
|
+
if (!edge.sourceId || !edge.targetId)
|
|
633
821
|
continue;
|
|
634
|
-
const key =
|
|
635
|
-
const ids =
|
|
822
|
+
const key = directedPairKey(edge.sourceId, edge.targetId);
|
|
823
|
+
const ids = edgeIdsByPair.get(key) ?? [];
|
|
636
824
|
ids.push(edge.id);
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
if (edgeIds.length <= 1) {
|
|
825
|
+
edgeIdsByPair.set(key, ids);
|
|
826
|
+
const undirectedKey = undirectedPairKey(edge.sourceId, edge.targetId);
|
|
827
|
+
const pairIds = undirectedEdgeIdsByPair.get(undirectedKey) ?? [];
|
|
828
|
+
pairIds.push(edge.id);
|
|
829
|
+
undirectedEdgeIdsByPair.set(undirectedKey, pairIds);
|
|
830
|
+
}
|
|
831
|
+
const edgePointForEndpoint = (endpointId) => {
|
|
832
|
+
const endpoint = nodeById.get(endpointId);
|
|
833
|
+
if (!endpoint) {
|
|
647
834
|
return undefined;
|
|
648
835
|
}
|
|
649
|
-
|
|
650
|
-
const
|
|
651
|
-
const
|
|
652
|
-
if (!
|
|
653
|
-
return
|
|
654
|
-
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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);
|
|
836
|
+
if (endpoint.kind === 'Port' && endpoint.parentId) {
|
|
837
|
+
const ownerBox = layout.boxes.get(endpoint.parentId);
|
|
838
|
+
const placement = portPlacementById.get(endpoint.id);
|
|
839
|
+
if (!ownerBox || !placement) {
|
|
840
|
+
return undefined;
|
|
841
|
+
}
|
|
842
|
+
const ratio = clamp(placement.ratio, 0.05, 0.95);
|
|
843
|
+
if (placement.side === 'left') {
|
|
844
|
+
return { x: ownerBox.x, y: ownerBox.y + (ownerBox.height * ratio) };
|
|
845
|
+
}
|
|
846
|
+
if (placement.side === 'right') {
|
|
847
|
+
return { x: ownerBox.x + ownerBox.width, y: ownerBox.y + (ownerBox.height * ratio) };
|
|
677
848
|
}
|
|
678
|
-
|
|
679
|
-
|
|
849
|
+
if (placement.side === 'top') {
|
|
850
|
+
return { x: ownerBox.x + (ownerBox.width * ratio), y: ownerBox.y };
|
|
680
851
|
}
|
|
852
|
+
return { x: ownerBox.x + (ownerBox.width * ratio), y: ownerBox.y + ownerBox.height };
|
|
681
853
|
}
|
|
682
|
-
|
|
683
|
-
if (
|
|
854
|
+
const box = layout.boxes.get(endpointId);
|
|
855
|
+
if (!box) {
|
|
684
856
|
return undefined;
|
|
685
857
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (
|
|
690
|
-
|
|
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;
|
|
858
|
+
return { x: box.x + (box.width / 2), y: box.y + (box.height / 2) };
|
|
859
|
+
};
|
|
860
|
+
const fanningVertexForEdge = (edge) => {
|
|
861
|
+
if (!edge.sourceId || !edge.targetId) {
|
|
862
|
+
return undefined;
|
|
698
863
|
}
|
|
699
|
-
|
|
864
|
+
const undirectedIds = undirectedEdgeIdsByPair.get(undirectedPairKey(edge.sourceId, edge.targetId)) ?? [];
|
|
865
|
+
if (undirectedIds.length <= 1) {
|
|
700
866
|
return undefined;
|
|
701
867
|
}
|
|
702
|
-
const
|
|
703
|
-
const
|
|
704
|
-
if (!
|
|
868
|
+
const sourcePoint = edgePointForEndpoint(edge.sourceId);
|
|
869
|
+
const targetPoint = edgePointForEndpoint(edge.targetId);
|
|
870
|
+
if (!sourcePoint || !targetPoint) {
|
|
705
871
|
return undefined;
|
|
706
872
|
}
|
|
707
|
-
const sx =
|
|
708
|
-
const sy =
|
|
709
|
-
const tx =
|
|
710
|
-
const ty =
|
|
873
|
+
const sx = sourcePoint.x;
|
|
874
|
+
const sy = sourcePoint.y;
|
|
875
|
+
const tx = targetPoint.x;
|
|
876
|
+
const ty = targetPoint.y;
|
|
711
877
|
const dx = tx - sx;
|
|
712
878
|
const dy = ty - sy;
|
|
713
879
|
const len = Math.hypot(dx, dy);
|
|
714
880
|
if (!Number.isFinite(len) || len < 1) {
|
|
715
881
|
return undefined;
|
|
716
882
|
}
|
|
717
|
-
const
|
|
718
|
-
|
|
883
|
+
const forwardIds = (edgeIdsByPair.get(directedPairKey(edge.sourceId, edge.targetId)) ?? [])
|
|
884
|
+
.slice()
|
|
885
|
+
.sort((left, right) => left.localeCompare(right));
|
|
886
|
+
const reverseIds = (edgeIdsByPair.get(directedPairKey(edge.targetId, edge.sourceId)) ?? [])
|
|
887
|
+
.slice()
|
|
888
|
+
.sort((left, right) => left.localeCompare(right));
|
|
889
|
+
let offset = 0;
|
|
890
|
+
if (forwardIds.length > 0 && reverseIds.length > 0) {
|
|
891
|
+
const forwardIndex = forwardIds.indexOf(edge.id);
|
|
892
|
+
const reverseIndex = reverseIds.indexOf(edge.id);
|
|
893
|
+
if (forwardIndex >= 0) {
|
|
894
|
+
const laneOffset = forwardIndex - ((forwardIds.length - 1) / 2);
|
|
895
|
+
offset = 16 + (laneOffset * 10);
|
|
896
|
+
}
|
|
897
|
+
else if (reverseIndex >= 0) {
|
|
898
|
+
const laneOffset = reverseIndex - ((reverseIds.length - 1) / 2);
|
|
899
|
+
offset = -16 + (laneOffset * 10);
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
return undefined;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
else {
|
|
906
|
+
const laneIndex = forwardIds.indexOf(edge.id);
|
|
907
|
+
if (laneIndex < 0 || forwardIds.length <= 1) {
|
|
908
|
+
return undefined;
|
|
909
|
+
}
|
|
910
|
+
const laneOffset = laneIndex - ((forwardIds.length - 1) / 2);
|
|
911
|
+
offset = laneOffset * 16;
|
|
912
|
+
}
|
|
719
913
|
if (Math.abs(offset) < 0.01) {
|
|
720
914
|
return undefined;
|
|
721
915
|
}
|
|
@@ -750,8 +944,8 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
750
944
|
id: edge.id,
|
|
751
945
|
source: resolveEndpoint(edge.sourceId),
|
|
752
946
|
target: resolveEndpoint(edge.targetId),
|
|
753
|
-
router:
|
|
754
|
-
connector:
|
|
947
|
+
router: resolveEdgeRouter(resolvedStyle),
|
|
948
|
+
connector: resolveEdgeConnector(resolvedStyle),
|
|
755
949
|
attrs: {
|
|
756
950
|
line: lineAttrs,
|
|
757
951
|
},
|
|
@@ -768,7 +962,7 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
768
962
|
labelBody: resolveEdgeLabelBodyAttrs(resolvedStyle, label.placement),
|
|
769
963
|
},
|
|
770
964
|
})),
|
|
771
|
-
zIndex:
|
|
965
|
+
zIndex: 50,
|
|
772
966
|
});
|
|
773
967
|
}
|
|
774
968
|
// Compartments are structural containers: do not drag them; select parent instead.
|
|
@@ -806,56 +1000,120 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
806
1000
|
e.preventDefault();
|
|
807
1001
|
e.stopPropagation();
|
|
808
1002
|
});
|
|
809
|
-
const
|
|
1003
|
+
const sidePriority = (side) => {
|
|
1004
|
+
switch (side) {
|
|
1005
|
+
case 'left':
|
|
1006
|
+
return 0;
|
|
1007
|
+
case 'top':
|
|
1008
|
+
return 1;
|
|
1009
|
+
case 'right':
|
|
1010
|
+
return 2;
|
|
1011
|
+
case 'bottom':
|
|
1012
|
+
return 3;
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
const projectPortPlacement = (node, clientX, clientY, preferredSide) => {
|
|
1016
|
+
if (!node || typeof node.size !== 'function')
|
|
1017
|
+
return undefined;
|
|
1018
|
+
const size = node.size();
|
|
1019
|
+
const position = typeof node.getPosition === 'function'
|
|
1020
|
+
? node.getPosition()
|
|
1021
|
+
: { x: Number(node?.position?.x ?? 0), y: Number(node?.position?.y ?? 0) };
|
|
1022
|
+
const localPoint = typeof graphView.clientToGraph === 'function'
|
|
1023
|
+
? graphView.clientToGraph(clientX, clientY)
|
|
1024
|
+
: { x: clientX, y: clientY };
|
|
1025
|
+
const localX = clamp(localPoint.x - position.x, 0, size.width);
|
|
1026
|
+
const localY = clamp(localPoint.y - position.y, 0, size.height);
|
|
1027
|
+
const candidates = [
|
|
1028
|
+
{ side: 'left', distance: localX },
|
|
1029
|
+
{ side: 'right', distance: size.width - localX },
|
|
1030
|
+
{ side: 'top', distance: localY },
|
|
1031
|
+
{ side: 'bottom', distance: size.height - localY },
|
|
1032
|
+
];
|
|
1033
|
+
candidates.sort((left, right) => left.distance - right.distance || sidePriority(left.side) - sidePriority(right.side));
|
|
1034
|
+
const closest = candidates[0];
|
|
1035
|
+
const preferredCandidate = preferredSide
|
|
1036
|
+
? candidates.find((candidate) => candidate.side === preferredSide)
|
|
1037
|
+
: undefined;
|
|
1038
|
+
const hysteresis = 8;
|
|
1039
|
+
const side = preferredCandidate && preferredSide && (preferredCandidate.distance - closest.distance) <= hysteresis
|
|
1040
|
+
? preferredSide
|
|
1041
|
+
: closest.side;
|
|
1042
|
+
const ratio = side === 'left' || side === 'right'
|
|
1043
|
+
? (size.height > 0 ? localY / size.height : 0.5)
|
|
1044
|
+
: (size.width > 0 ? localX / size.width : 0.5);
|
|
1045
|
+
return {
|
|
1046
|
+
side,
|
|
1047
|
+
ratio: Math.max(0.05, Math.min(0.95, ratio)),
|
|
1048
|
+
};
|
|
1049
|
+
};
|
|
810
1050
|
const onPointerMove = (event) => {
|
|
811
|
-
if (!activePortDrag)
|
|
1051
|
+
if (!activePortDrag || event.pointerId !== activePortDrag.pointerId)
|
|
812
1052
|
return;
|
|
813
1053
|
const node = graphView.getCellById(activePortDrag.nodeId);
|
|
814
1054
|
if (!node || typeof node.size !== 'function' || typeof node.getPort !== 'function' || typeof node.setPortProp !== 'function')
|
|
815
1055
|
return;
|
|
1056
|
+
const placement = projectPortPlacement(node, event.clientX, event.clientY, activePortDrag.side);
|
|
1057
|
+
if (!placement)
|
|
1058
|
+
return;
|
|
1059
|
+
if (placement.side === activePortDrag.side && Math.abs(placement.ratio - activePortDrag.ratio) < 0.005) {
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
816
1062
|
const size = node.size();
|
|
817
|
-
const
|
|
818
|
-
|
|
1063
|
+
const nextX = placement.side === 'left'
|
|
1064
|
+
? 0
|
|
1065
|
+
: placement.side === 'right'
|
|
1066
|
+
? size.width
|
|
1067
|
+
: placement.ratio * size.width;
|
|
1068
|
+
const nextY = placement.side === 'top'
|
|
1069
|
+
? 0
|
|
1070
|
+
: placement.side === 'bottom'
|
|
1071
|
+
? size.height
|
|
1072
|
+
: placement.ratio * size.height;
|
|
1073
|
+
node.setPortProp(activePortDrag.portId, 'args/x', nextX);
|
|
819
1074
|
node.setPortProp(activePortDrag.portId, 'args/y', nextY);
|
|
1075
|
+
node.setPortProp(activePortDrag.portId, 'args/side', placement.side);
|
|
1076
|
+
node.setPortProp(activePortDrag.portId, 'args/ratio', placement.ratio);
|
|
1077
|
+
if (placement.side !== activePortDrag.side) {
|
|
1078
|
+
node.setPortProp(activePortDrag.portId, 'label/position', resolveBorderPortLabelPosition(placement.side));
|
|
1079
|
+
}
|
|
1080
|
+
activePortDrag.side = placement.side;
|
|
1081
|
+
activePortDrag.ratio = placement.ratio;
|
|
1082
|
+
portPlacementById.set(activePortDrag.portId, placement);
|
|
820
1083
|
};
|
|
821
|
-
const onPointerUp = () => {
|
|
1084
|
+
const onPointerUp = (event) => {
|
|
1085
|
+
if (!activePortDrag || event.pointerId !== activePortDrag.pointerId)
|
|
1086
|
+
return;
|
|
1087
|
+
if (typeof activePortDrag.container.releasePointerCapture === 'function') {
|
|
1088
|
+
try {
|
|
1089
|
+
activePortDrag.container.releasePointerCapture(activePortDrag.pointerId);
|
|
1090
|
+
}
|
|
1091
|
+
catch {
|
|
1092
|
+
// Ignore capture release failures.
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
822
1095
|
activePortDrag = undefined;
|
|
823
1096
|
};
|
|
824
1097
|
window.addEventListener('pointermove', onPointerMove);
|
|
825
1098
|
window.addEventListener('pointerup', onPointerUp);
|
|
826
|
-
const startPortDrag = (clientY, node, portId) => {
|
|
1099
|
+
const startPortDrag = (clientX, clientY, node, portId, pointerId, container) => {
|
|
827
1100
|
if (!node || typeof node.getPort !== 'function')
|
|
828
1101
|
return;
|
|
829
|
-
const
|
|
830
|
-
|
|
1102
|
+
const placement = projectPortPlacement(node, clientX, clientY, portPlacementById.get(portId)?.side ?? 'right')
|
|
1103
|
+
?? portPlacementById.get(portId)
|
|
1104
|
+
?? { side: 'right', ratio: 0.5 };
|
|
831
1105
|
activePortDrag = {
|
|
832
1106
|
nodeId: String(node.id),
|
|
833
1107
|
portId: String(portId),
|
|
834
|
-
|
|
835
|
-
|
|
1108
|
+
pointerId,
|
|
1109
|
+
side: placement.side,
|
|
1110
|
+
ratio: placement.ratio,
|
|
1111
|
+
container,
|
|
836
1112
|
};
|
|
837
1113
|
};
|
|
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
1114
|
liveCanvas.addEventListener('DOMNodeRemovedFromDocument', () => {
|
|
856
1115
|
window.removeEventListener('pointermove', onPointerMove);
|
|
857
1116
|
window.removeEventListener('pointerup', onPointerUp);
|
|
858
|
-
liveCanvas.removeEventListener('pointerdown', onCanvasPointerDown, true);
|
|
859
1117
|
}, { once: true });
|
|
860
1118
|
const resultContainer = liveCanvas.closest('.oml-md-result');
|
|
861
1119
|
const resizeObserver = new ResizeObserver(() => {
|
|
@@ -910,8 +1168,100 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
910
1168
|
resizeHandle.addEventListener('pointermove', onResizePointerMove);
|
|
911
1169
|
resizeHandle.addEventListener('pointerup', onResizePointerEnd);
|
|
912
1170
|
resizeHandle.addEventListener('pointercancel', onResizePointerEnd);
|
|
1171
|
+
installDiagramNodeInteractions(liveCanvas, graphView);
|
|
913
1172
|
installDiagramToolbar(liveCanvas, graphView, graph, actions);
|
|
914
1173
|
}
|
|
1174
|
+
function installDiagramNodeInteractions(canvas, graphView) {
|
|
1175
|
+
const ensureSvgNativeTitle = (element, iri) => {
|
|
1176
|
+
let titleNode = null;
|
|
1177
|
+
for (const child of Array.from(element.children)) {
|
|
1178
|
+
if (child instanceof SVGTitleElement) {
|
|
1179
|
+
titleNode = child;
|
|
1180
|
+
break;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
if (!titleNode) {
|
|
1184
|
+
titleNode = document.createElementNS('http://www.w3.org/2000/svg', 'title');
|
|
1185
|
+
element.insertBefore(titleNode, element.firstChild);
|
|
1186
|
+
}
|
|
1187
|
+
titleNode.textContent = iri;
|
|
1188
|
+
};
|
|
1189
|
+
const applyNativeTooltipTitle = (container, iri, eventTarget) => {
|
|
1190
|
+
if (!iri) {
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
if (container instanceof HTMLElement || container instanceof SVGElement) {
|
|
1194
|
+
container.setAttribute('title', iri);
|
|
1195
|
+
}
|
|
1196
|
+
if (container instanceof Element) {
|
|
1197
|
+
for (const element of Array.from(container.querySelectorAll('*'))) {
|
|
1198
|
+
element.setAttribute('title', iri);
|
|
1199
|
+
if (element instanceof SVGElement) {
|
|
1200
|
+
ensureSvgNativeTitle(element, iri);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
if (container instanceof SVGElement) {
|
|
1204
|
+
ensureSvgNativeTitle(container, iri);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
if (eventTarget instanceof HTMLElement || eventTarget instanceof SVGElement) {
|
|
1208
|
+
eventTarget.setAttribute('title', iri);
|
|
1209
|
+
if (eventTarget instanceof SVGElement) {
|
|
1210
|
+
ensureSvgNativeTitle(eventTarget, iri);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
const clearHover = () => {
|
|
1215
|
+
canvas.dispatchEvent(new CustomEvent('md-hide-iri-hover', { bubbles: true }));
|
|
1216
|
+
};
|
|
1217
|
+
graphView.on('node:mouseenter', ({ node, e }) => {
|
|
1218
|
+
const iri = String(node?.id ?? '');
|
|
1219
|
+
if (iri) {
|
|
1220
|
+
const view = graphView.findViewByCell?.(node);
|
|
1221
|
+
const bbox = view?.container?.getBoundingClientRect?.();
|
|
1222
|
+
applyNativeTooltipTitle(view?.container, iri, e?.target ?? null);
|
|
1223
|
+
if (!bbox) {
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
canvas.dispatchEvent(new CustomEvent('md-show-iri-hover', {
|
|
1227
|
+
bubbles: true,
|
|
1228
|
+
detail: {
|
|
1229
|
+
iri,
|
|
1230
|
+
previewEnabled: !!e && (/^Mac/i.test(navigator.platform) ? e.metaKey : e.ctrlKey),
|
|
1231
|
+
anchorRect: {
|
|
1232
|
+
left: bbox.left,
|
|
1233
|
+
right: bbox.right,
|
|
1234
|
+
top: bbox.top,
|
|
1235
|
+
bottom: bbox.bottom,
|
|
1236
|
+
width: bbox.width,
|
|
1237
|
+
height: bbox.height,
|
|
1238
|
+
},
|
|
1239
|
+
},
|
|
1240
|
+
}));
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
graphView.on('node:mouseleave', () => {
|
|
1244
|
+
clearHover();
|
|
1245
|
+
});
|
|
1246
|
+
graphView.on('blank:mousemove', () => {
|
|
1247
|
+
clearHover();
|
|
1248
|
+
});
|
|
1249
|
+
graphView.on('node:dblclick', ({ node, e }) => {
|
|
1250
|
+
const iri = String(node?.id ?? '');
|
|
1251
|
+
if (!iri) {
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
e.preventDefault();
|
|
1255
|
+
e.stopPropagation();
|
|
1256
|
+
if (typeof e.stopImmediatePropagation === 'function') {
|
|
1257
|
+
e.stopImmediatePropagation();
|
|
1258
|
+
}
|
|
1259
|
+
canvas.dispatchEvent(new CustomEvent('md-navigate-iri', {
|
|
1260
|
+
bubbles: true,
|
|
1261
|
+
detail: { iri },
|
|
1262
|
+
}));
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
915
1265
|
function installDiagramToolbar(graphRoot, graphView, graph, actions) {
|
|
916
1266
|
const hotspot = document.createElement('div');
|
|
917
1267
|
hotspot.className = 'graph-corner-hotspot';
|
|
@@ -1238,7 +1588,7 @@ async function loadDagreLib() {
|
|
|
1238
1588
|
dagreLib = mod.default ?? mod;
|
|
1239
1589
|
return dagreLib;
|
|
1240
1590
|
}
|
|
1241
|
-
async function layoutGraphDagre(graph, options) {
|
|
1591
|
+
async function layoutGraphDagre(graph, options, rootSpacing) {
|
|
1242
1592
|
const dagre = await loadDagreLib();
|
|
1243
1593
|
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
1244
1594
|
const layoutNodes = graph.nodes.filter((node) => node.kind !== 'Port');
|
|
@@ -1275,8 +1625,6 @@ async function layoutGraphDagre(graph, options) {
|
|
|
1275
1625
|
stack: {
|
|
1276
1626
|
direction: styleLayout.direction === 'horizontal' ? 'horizontal' : 'vertical',
|
|
1277
1627
|
gap: clampLayoutNumber(styleLayout.gap, 0, 400, 0),
|
|
1278
|
-
marginx: clampLayoutNumber(styleLayout.marginx, 0, 200, 0),
|
|
1279
|
-
marginy: clampLayoutNumber(styleLayout.marginy, 0, 200, 0),
|
|
1280
1628
|
stretch: typeof styleLayout.stretch === 'boolean' ? styleLayout.stretch : true,
|
|
1281
1629
|
},
|
|
1282
1630
|
};
|
|
@@ -1296,33 +1644,43 @@ async function layoutGraphDagre(graph, options) {
|
|
|
1296
1644
|
rankdir,
|
|
1297
1645
|
nodesep: clampLayoutNumber(styleLayout.nodesep, 0, 400, options.nodesep),
|
|
1298
1646
|
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
1647
|
},
|
|
1302
1648
|
};
|
|
1303
1649
|
};
|
|
1304
1650
|
const childPositionById = new Map();
|
|
1651
|
+
const branchChildForParent = (parentId, nodeId) => {
|
|
1652
|
+
let cursorId = nodeId;
|
|
1653
|
+
while (cursorId) {
|
|
1654
|
+
const node = nodeById.get(cursorId);
|
|
1655
|
+
if (!node) {
|
|
1656
|
+
return undefined;
|
|
1657
|
+
}
|
|
1658
|
+
if (node.parentId === parentId) {
|
|
1659
|
+
return node.id;
|
|
1660
|
+
}
|
|
1661
|
+
cursorId = node.parentId;
|
|
1662
|
+
}
|
|
1663
|
+
return undefined;
|
|
1664
|
+
};
|
|
1305
1665
|
const edgesBetweenChildren = (parentId, childSet) => {
|
|
1306
1666
|
const seen = new Set();
|
|
1307
1667
|
const links = [];
|
|
1308
1668
|
for (const edge of graph.edges) {
|
|
1309
1669
|
const sourceId = endpointOwner(edge.sourceId);
|
|
1310
1670
|
const targetId = endpointOwner(edge.targetId);
|
|
1311
|
-
if (!sourceId || !targetId
|
|
1312
|
-
continue;
|
|
1313
|
-
if (!childSet.has(sourceId) || !childSet.has(targetId))
|
|
1671
|
+
if (!sourceId || !targetId)
|
|
1314
1672
|
continue;
|
|
1315
|
-
const
|
|
1316
|
-
const
|
|
1317
|
-
if (!
|
|
1673
|
+
const sourceBranchId = branchChildForParent(parentId, sourceId);
|
|
1674
|
+
const targetBranchId = branchChildForParent(parentId, targetId);
|
|
1675
|
+
if (!sourceBranchId || !targetBranchId || sourceBranchId === targetBranchId)
|
|
1318
1676
|
continue;
|
|
1319
|
-
if (
|
|
1677
|
+
if (!childSet.has(sourceBranchId) || !childSet.has(targetBranchId))
|
|
1320
1678
|
continue;
|
|
1321
|
-
const key = `${
|
|
1679
|
+
const key = `${sourceBranchId}=>${targetBranchId}`;
|
|
1322
1680
|
if (seen.has(key))
|
|
1323
1681
|
continue;
|
|
1324
1682
|
seen.add(key);
|
|
1325
|
-
links.push({ source:
|
|
1683
|
+
links.push({ source: sourceBranchId, target: targetBranchId });
|
|
1326
1684
|
}
|
|
1327
1685
|
return links;
|
|
1328
1686
|
};
|
|
@@ -1335,8 +1693,8 @@ async function layoutGraphDagre(graph, options) {
|
|
|
1335
1693
|
return { width: 0, height: 0 };
|
|
1336
1694
|
}
|
|
1337
1695
|
const parent = parentId ? nodeById.get(parentId) : undefined;
|
|
1338
|
-
const topPadding = parent?.contentTopPadding ?? 0;
|
|
1339
1696
|
const localLayout = layoutOptionsForParent(parentId);
|
|
1697
|
+
const spacing = parent ? resolveBoxSpacing(parent.style, 0) : rootSpacing;
|
|
1340
1698
|
let totalWidth = 0;
|
|
1341
1699
|
let totalHeight = 0;
|
|
1342
1700
|
if (localLayout.type === 'stack') {
|
|
@@ -1344,13 +1702,13 @@ async function layoutGraphDagre(graph, options) {
|
|
|
1344
1702
|
const childNodes = childIds.map((childId) => nodeById.get(childId)).filter((node) => !!node);
|
|
1345
1703
|
const maxChildWidth = childNodes.reduce((max, child) => Math.max(max, child.width), 0);
|
|
1346
1704
|
const maxChildHeight = childNodes.reduce((max, child) => Math.max(max, child.height), 0);
|
|
1347
|
-
const parentContentWidth = Math.max(0, (parent?.width ?? 0) -
|
|
1348
|
-
const parentContentHeight = Math.max(0, (parent?.height ?? 0) -
|
|
1705
|
+
const parentContentWidth = Math.max(0, (parent?.width ?? 0) - spacing.marginLeft - spacing.marginRight - spacing.paddingLeft - spacing.paddingRight);
|
|
1706
|
+
const parentContentHeight = Math.max(0, (parent?.height ?? 0) - spacing.marginTop - spacing.marginBottom - spacing.paddingTop - spacing.paddingBottom);
|
|
1349
1707
|
const availableWidth = Math.max(maxChildWidth, parentContentWidth);
|
|
1350
1708
|
const availableHeight = Math.max(maxChildHeight, parentContentHeight);
|
|
1351
1709
|
const isSingleChild = childNodes.length === 1;
|
|
1352
|
-
let cursorX =
|
|
1353
|
-
let cursorY =
|
|
1710
|
+
let cursorX = spacing.marginLeft + spacing.paddingLeft;
|
|
1711
|
+
let cursorY = spacing.marginTop + spacing.paddingTop;
|
|
1354
1712
|
for (const child of childNodes) {
|
|
1355
1713
|
if (local.stretch && isSingleChild) {
|
|
1356
1714
|
child.width = Math.max(0, availableWidth);
|
|
@@ -1373,14 +1731,14 @@ async function layoutGraphDagre(graph, options) {
|
|
|
1373
1731
|
if (local.direction === 'vertical') {
|
|
1374
1732
|
const contentHeight = Math.max(0, childNodes.reduce((sum, child) => sum + child.height, 0) + (Math.max(0, childNodes.length - 1) * local.gap));
|
|
1375
1733
|
const contentWidth = Math.max(0, childNodes.reduce((max, child) => Math.max(max, child.width), 0));
|
|
1376
|
-
totalWidth =
|
|
1377
|
-
totalHeight =
|
|
1734
|
+
totalWidth = spacing.marginLeft + spacing.paddingLeft + contentWidth + spacing.paddingRight + spacing.marginRight;
|
|
1735
|
+
totalHeight = spacing.marginTop + spacing.paddingTop + contentHeight + spacing.paddingBottom + spacing.marginBottom;
|
|
1378
1736
|
}
|
|
1379
1737
|
else {
|
|
1380
1738
|
const contentWidth = Math.max(0, childNodes.reduce((sum, child) => sum + child.width, 0) + (Math.max(0, childNodes.length - 1) * local.gap));
|
|
1381
1739
|
const contentHeight = Math.max(0, childNodes.reduce((max, child) => Math.max(max, child.height), 0));
|
|
1382
|
-
totalWidth =
|
|
1383
|
-
totalHeight =
|
|
1740
|
+
totalWidth = spacing.marginLeft + spacing.paddingLeft + contentWidth + spacing.paddingRight + spacing.marginRight;
|
|
1741
|
+
totalHeight = spacing.marginTop + spacing.paddingTop + contentHeight + spacing.paddingBottom + spacing.marginBottom;
|
|
1384
1742
|
}
|
|
1385
1743
|
}
|
|
1386
1744
|
else {
|
|
@@ -1438,22 +1796,18 @@ async function layoutGraphDagre(graph, options) {
|
|
|
1438
1796
|
const left = laid.x - (width / 2);
|
|
1439
1797
|
const top = laid.y - (height / 2);
|
|
1440
1798
|
childPositionById.set(childId, {
|
|
1441
|
-
x:
|
|
1442
|
-
y:
|
|
1799
|
+
x: spacing.marginLeft + spacing.paddingLeft + (left - minX),
|
|
1800
|
+
y: spacing.marginTop + spacing.paddingTop + (top - minY),
|
|
1443
1801
|
});
|
|
1444
1802
|
}
|
|
1445
1803
|
const contentWidth = Math.max(0, maxX - minX);
|
|
1446
1804
|
const contentHeight = Math.max(0, maxY - minY);
|
|
1447
|
-
totalWidth =
|
|
1448
|
-
totalHeight =
|
|
1805
|
+
totalWidth = spacing.marginLeft + spacing.paddingLeft + contentWidth + spacing.paddingRight + spacing.marginRight;
|
|
1806
|
+
totalHeight = spacing.marginTop + spacing.paddingTop + contentHeight + spacing.paddingBottom + spacing.marginBottom;
|
|
1449
1807
|
}
|
|
1450
1808
|
if (parent) {
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
}
|
|
1454
|
-
if (!parent.hasExplicitHeight) {
|
|
1455
|
-
parent.height = Math.max(parent.height, totalHeight);
|
|
1456
|
-
}
|
|
1809
|
+
parent.width = Math.max(parent.width, totalWidth);
|
|
1810
|
+
parent.height = Math.max(parent.height, totalHeight);
|
|
1457
1811
|
}
|
|
1458
1812
|
return { width: totalWidth, height: totalHeight };
|
|
1459
1813
|
};
|
|
@@ -1471,9 +1825,9 @@ async function layoutGraphDagre(graph, options) {
|
|
|
1471
1825
|
if (localLayout.type === 'stack') {
|
|
1472
1826
|
const local = localLayout.stack;
|
|
1473
1827
|
const childNodes = childIds.map((childId) => nodeById.get(childId)).filter((node) => !!node);
|
|
1474
|
-
const
|
|
1475
|
-
const contentWidth = Math.max(0, parent.width -
|
|
1476
|
-
const contentHeight = Math.max(0, parent.height -
|
|
1828
|
+
const spacing = resolveBoxSpacing(parent.style, 0);
|
|
1829
|
+
const contentWidth = Math.max(0, parent.width - spacing.marginLeft - spacing.marginRight - spacing.paddingLeft - spacing.paddingRight);
|
|
1830
|
+
const contentHeight = Math.max(0, parent.height - spacing.marginTop - spacing.marginBottom - spacing.paddingTop - spacing.paddingBottom);
|
|
1477
1831
|
const isSingleChild = childNodes.length === 1;
|
|
1478
1832
|
if (local.stretch) {
|
|
1479
1833
|
for (const child of childNodes) {
|
|
@@ -1490,8 +1844,8 @@ async function layoutGraphDagre(graph, options) {
|
|
|
1490
1844
|
}
|
|
1491
1845
|
}
|
|
1492
1846
|
}
|
|
1493
|
-
let cursorX =
|
|
1494
|
-
let cursorY =
|
|
1847
|
+
let cursorX = spacing.marginLeft + spacing.paddingLeft;
|
|
1848
|
+
let cursorY = spacing.marginTop + spacing.paddingTop;
|
|
1495
1849
|
for (const child of childNodes) {
|
|
1496
1850
|
childPositionById.set(child.id, { x: cursorX, y: cursorY });
|
|
1497
1851
|
if (local.direction === 'vertical') {
|
|
@@ -1692,9 +2046,6 @@ function compileDiagramGraph(index, stylesheet) {
|
|
|
1692
2046
|
children: [],
|
|
1693
2047
|
width: 160,
|
|
1694
2048
|
height: 70,
|
|
1695
|
-
hasExplicitWidth: false,
|
|
1696
|
-
hasExplicitHeight: false,
|
|
1697
|
-
contentTopPadding: 0,
|
|
1698
2049
|
});
|
|
1699
2050
|
}
|
|
1700
2051
|
const validParent = (child, parent) => {
|
|
@@ -1777,9 +2128,6 @@ function compileDiagramGraph(index, stylesheet) {
|
|
|
1777
2128
|
const estimated = estimateSize(node.kind, baseLabel, node.labels.length, node.style);
|
|
1778
2129
|
node.width = estimated.width;
|
|
1779
2130
|
node.height = estimated.height;
|
|
1780
|
-
node.hasExplicitWidth = estimated.hasExplicitWidth;
|
|
1781
|
-
node.hasExplicitHeight = estimated.hasExplicitHeight;
|
|
1782
|
-
node.contentTopPadding = resolveContainerTopPadding(node, baseLabel);
|
|
1783
2131
|
}
|
|
1784
2132
|
for (const edge of edges) {
|
|
1785
2133
|
edge.style = styleFor('edge', edge.id, edge.classes, edge.properties);
|
|
@@ -1807,57 +2155,40 @@ function compileDiagramGraph(index, stylesheet) {
|
|
|
1807
2155
|
function estimateSize(kind, label, labelCount, style) {
|
|
1808
2156
|
const styledWidth = toPositiveNumber(style.width);
|
|
1809
2157
|
const styledHeight = toPositiveNumber(style.height);
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
2158
|
+
const attrs = extractStyleAttrs(style);
|
|
2159
|
+
const labelAttrs = asRecord(attrs.label);
|
|
2160
|
+
const labelDisplay = typeof labelAttrs?.display === 'string' ? labelAttrs.display.trim().toLowerCase() : '';
|
|
2161
|
+
const labelOpacity = toNonNegativeNumber(labelAttrs?.opacity);
|
|
2162
|
+
const labelVisible = labelDisplay !== 'none' && (labelOpacity === undefined || labelOpacity > 0);
|
|
2163
|
+
const effectiveLabel = labelVisible ? label : '';
|
|
2164
|
+
const effectiveLabelCount = labelVisible ? labelCount : 0;
|
|
1813
2165
|
if (kind === 'Port') {
|
|
1814
2166
|
return {
|
|
1815
|
-
width: styledWidth ??
|
|
1816
|
-
height: styledHeight ??
|
|
1817
|
-
hasExplicitWidth: styledWidth !== undefined,
|
|
1818
|
-
hasExplicitHeight: styledHeight !== undefined,
|
|
2167
|
+
width: Math.max(14, styledWidth ?? 0),
|
|
2168
|
+
height: Math.max(14, styledHeight ?? 0),
|
|
1819
2169
|
};
|
|
1820
2170
|
}
|
|
1821
|
-
const textWidth = Math.max(24,
|
|
1822
|
-
const baseWidth = Math.max(72, Math.min(280, textWidth + 26));
|
|
2171
|
+
const textWidth = effectiveLabel.length > 0 ? Math.max(24, effectiveLabel.length * 7) : 0;
|
|
2172
|
+
const baseWidth = effectiveLabel.length > 0 ? Math.max(72, Math.min(280, textWidth + 26)) : 0;
|
|
1823
2173
|
if (kind === 'Compartment') {
|
|
1824
|
-
const size = {
|
|
2174
|
+
const size = {
|
|
2175
|
+
width: baseWidth,
|
|
2176
|
+
height: effectiveLabelCount > 0 ? Math.max(44, 24 + effectiveLabelCount * 16) : 0,
|
|
2177
|
+
};
|
|
1825
2178
|
return {
|
|
1826
|
-
width: styledWidth ??
|
|
1827
|
-
height: styledHeight ??
|
|
1828
|
-
hasExplicitWidth: styledWidth !== undefined,
|
|
1829
|
-
hasExplicitHeight: styledHeight !== undefined,
|
|
2179
|
+
width: Math.max(size.width, styledWidth ?? 0),
|
|
2180
|
+
height: Math.max(size.height, styledHeight ?? 0),
|
|
1830
2181
|
};
|
|
1831
2182
|
}
|
|
1832
|
-
const size = {
|
|
2183
|
+
const size = {
|
|
2184
|
+
width: baseWidth,
|
|
2185
|
+
height: effectiveLabelCount > 0 ? Math.max(36, 20 + effectiveLabelCount * 16) : 0,
|
|
2186
|
+
};
|
|
1833
2187
|
return {
|
|
1834
|
-
width: styledWidth ??
|
|
1835
|
-
height: styledHeight ??
|
|
1836
|
-
hasExplicitWidth: styledWidth !== undefined,
|
|
1837
|
-
hasExplicitHeight: styledHeight !== undefined,
|
|
2188
|
+
width: Math.max(size.width, styledWidth ?? 0),
|
|
2189
|
+
height: Math.max(size.height, styledHeight ?? 0),
|
|
1838
2190
|
};
|
|
1839
2191
|
}
|
|
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
2192
|
function localName(value) {
|
|
1862
2193
|
return displayLabelFromIri(value);
|
|
1863
2194
|
}
|
|
@@ -1907,9 +2238,7 @@ function resolveDagreLayoutOptions(options) {
|
|
|
1907
2238
|
: 'LR';
|
|
1908
2239
|
const nodesep = clampLayoutNumber(layout.nodesep, 0, 400, 28);
|
|
1909
2240
|
const ranksep = clampLayoutNumber(layout.ranksep, 0, 500, 64);
|
|
1910
|
-
|
|
1911
|
-
const marginy = clampLayoutNumber(layout.marginy, 0, 200, 16);
|
|
1912
|
-
return { rankdir, nodesep, ranksep, marginx, marginy };
|
|
2241
|
+
return { rankdir, nodesep, ranksep };
|
|
1913
2242
|
}
|
|
1914
2243
|
function clampLayoutNumber(value, min, max, fallback) {
|
|
1915
2244
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
@@ -1917,6 +2246,121 @@ function clampLayoutNumber(value, min, max, fallback) {
|
|
|
1917
2246
|
}
|
|
1918
2247
|
return Math.max(min, Math.min(max, Math.round(value)));
|
|
1919
2248
|
}
|
|
2249
|
+
function resolveRootSpacing(options) {
|
|
2250
|
+
return readBoxSpacing(options, {
|
|
2251
|
+
marginTop: 16,
|
|
2252
|
+
marginBottom: 16,
|
|
2253
|
+
marginLeft: 16,
|
|
2254
|
+
marginRight: 16,
|
|
2255
|
+
paddingTop: 0,
|
|
2256
|
+
paddingBottom: 0,
|
|
2257
|
+
paddingLeft: 0,
|
|
2258
|
+
paddingRight: 0,
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
2261
|
+
function resolveBoxSpacing(style, fallbackMargin) {
|
|
2262
|
+
return readBoxSpacing(style, {
|
|
2263
|
+
marginTop: fallbackMargin,
|
|
2264
|
+
marginBottom: fallbackMargin,
|
|
2265
|
+
marginLeft: fallbackMargin,
|
|
2266
|
+
marginRight: fallbackMargin,
|
|
2267
|
+
paddingTop: 0,
|
|
2268
|
+
paddingBottom: 0,
|
|
2269
|
+
paddingLeft: 0,
|
|
2270
|
+
paddingRight: 0,
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
function readBoxSpacing(style, defaults) {
|
|
2274
|
+
const source = style ?? {};
|
|
2275
|
+
return {
|
|
2276
|
+
...readCssBoxSpacing(source.margin, 'margin', defaults),
|
|
2277
|
+
...readCssBoxSpacing(source.padding, 'padding', defaults),
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
function readCssBoxSpacing(value, kind, defaults) {
|
|
2281
|
+
const sides = parseCssBoxShorthand(value);
|
|
2282
|
+
const isMargin = kind === 'margin';
|
|
2283
|
+
if (isMargin) {
|
|
2284
|
+
return {
|
|
2285
|
+
marginTop: sides?.top ?? defaults.marginTop,
|
|
2286
|
+
marginRight: sides?.right ?? defaults.marginRight,
|
|
2287
|
+
marginBottom: sides?.bottom ?? defaults.marginBottom,
|
|
2288
|
+
marginLeft: sides?.left ?? defaults.marginLeft,
|
|
2289
|
+
paddingTop: defaults.paddingTop,
|
|
2290
|
+
paddingRight: defaults.paddingRight,
|
|
2291
|
+
paddingBottom: defaults.paddingBottom,
|
|
2292
|
+
paddingLeft: defaults.paddingLeft,
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
return {
|
|
2296
|
+
marginTop: defaults.marginTop,
|
|
2297
|
+
marginRight: defaults.marginRight,
|
|
2298
|
+
marginBottom: defaults.marginBottom,
|
|
2299
|
+
marginLeft: defaults.marginLeft,
|
|
2300
|
+
paddingTop: sides?.top ?? defaults.paddingTop,
|
|
2301
|
+
paddingRight: sides?.right ?? defaults.paddingRight,
|
|
2302
|
+
paddingBottom: sides?.bottom ?? defaults.paddingBottom,
|
|
2303
|
+
paddingLeft: sides?.left ?? defaults.paddingLeft,
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
function parseCssBoxShorthand(value) {
|
|
2307
|
+
const values = Array.isArray(value)
|
|
2308
|
+
? value
|
|
2309
|
+
: typeof value === 'string'
|
|
2310
|
+
? (value.includes(',') ? undefined : value.trim().split(/\s+/).filter((token) => token.length > 0))
|
|
2311
|
+
: typeof value === 'number'
|
|
2312
|
+
? [value]
|
|
2313
|
+
: undefined;
|
|
2314
|
+
if (!values || values.length === 0 || values.length > 4) {
|
|
2315
|
+
return undefined;
|
|
2316
|
+
}
|
|
2317
|
+
const parsed = values.map((entry) => {
|
|
2318
|
+
if (typeof entry === 'number' && Number.isFinite(entry)) {
|
|
2319
|
+
return entry;
|
|
2320
|
+
}
|
|
2321
|
+
if (typeof entry === 'string') {
|
|
2322
|
+
const next = Number.parseFloat(entry);
|
|
2323
|
+
if (Number.isFinite(next)) {
|
|
2324
|
+
return next;
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
return undefined;
|
|
2328
|
+
});
|
|
2329
|
+
if (parsed.some((entry) => entry === undefined)) {
|
|
2330
|
+
return undefined;
|
|
2331
|
+
}
|
|
2332
|
+
const clampCssBoxValue = (entry) => {
|
|
2333
|
+
if (entry === undefined)
|
|
2334
|
+
return undefined;
|
|
2335
|
+
return Math.max(0, Math.min(200, Math.round(entry)));
|
|
2336
|
+
};
|
|
2337
|
+
if (parsed.length === 1) {
|
|
2338
|
+
const single = clampCssBoxValue(parsed[0]);
|
|
2339
|
+
return single === undefined ? undefined : { top: single, right: single, bottom: single, left: single };
|
|
2340
|
+
}
|
|
2341
|
+
if (parsed.length === 2) {
|
|
2342
|
+
const vertical = clampCssBoxValue(parsed[0]);
|
|
2343
|
+
const horizontal = clampCssBoxValue(parsed[1]);
|
|
2344
|
+
if (vertical === undefined || horizontal === undefined)
|
|
2345
|
+
return undefined;
|
|
2346
|
+
return { top: vertical, right: horizontal, bottom: vertical, left: horizontal };
|
|
2347
|
+
}
|
|
2348
|
+
if (parsed.length === 3) {
|
|
2349
|
+
const top = clampCssBoxValue(parsed[0]);
|
|
2350
|
+
const horizontal = clampCssBoxValue(parsed[1]);
|
|
2351
|
+
const bottom = clampCssBoxValue(parsed[2]);
|
|
2352
|
+
if (top === undefined || horizontal === undefined || bottom === undefined)
|
|
2353
|
+
return undefined;
|
|
2354
|
+
return { top, right: horizontal, bottom, left: horizontal };
|
|
2355
|
+
}
|
|
2356
|
+
const top = clampCssBoxValue(parsed[0]);
|
|
2357
|
+
const right = clampCssBoxValue(parsed[1]);
|
|
2358
|
+
const bottom = clampCssBoxValue(parsed[2]);
|
|
2359
|
+
const left = clampCssBoxValue(parsed[3]);
|
|
2360
|
+
if (top === undefined || right === undefined || bottom === undefined || left === undefined)
|
|
2361
|
+
return undefined;
|
|
2362
|
+
return { top, right, bottom, left };
|
|
2363
|
+
}
|
|
1920
2364
|
function parseDiagramStylesheet(options) {
|
|
1921
2365
|
const stylesheet = options?.stylesheet;
|
|
1922
2366
|
if (!Array.isArray(stylesheet)) {
|
|
@@ -1943,7 +2387,7 @@ function parseDiagramStylesheet(options) {
|
|
|
1943
2387
|
return rules;
|
|
1944
2388
|
}
|
|
1945
2389
|
function parseStyleSelector(selector) {
|
|
1946
|
-
const match = /^\s*(node|compartment|port|edge)(?:\.([A-Za-z0-9_-]+))?(?:\s*\[(.+)\]\s*)?$/i.exec(selector);
|
|
2390
|
+
const match = /^\s*(diagram|node|compartment|port|edge)(?:\.([A-Za-z0-9_-]+))?(?:\s*\[(.+)\]\s*)?$/i.exec(selector);
|
|
1947
2391
|
if (!match)
|
|
1948
2392
|
return undefined;
|
|
1949
2393
|
return {
|
|
@@ -1956,7 +2400,13 @@ function parseStyleSelector(selector) {
|
|
|
1956
2400
|
// These pass through as top-level keys so callers can read style.layout, style.width, etc.
|
|
1957
2401
|
const OML_PASSTHROUGH_STYLE_KEYS = new Set([
|
|
1958
2402
|
'layout', 'shape', 'width', 'height',
|
|
2403
|
+
'margin', 'padding', 'router', 'connector',
|
|
2404
|
+
]);
|
|
2405
|
+
const LEGACY_BOX_SPACING_KEYS = new Set([
|
|
2406
|
+
'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
|
|
1959
2407
|
'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
|
|
2408
|
+
'margin-top', 'margin-bottom', 'margin-left', 'margin-right',
|
|
2409
|
+
'padding-top', 'padding-bottom', 'padding-left', 'padding-right',
|
|
1960
2410
|
]);
|
|
1961
2411
|
// Record-valued top-level style keys that map directly into the attrs sub-tree under the same name.
|
|
1962
2412
|
// 'label' and 'icon' cover nodes, compartments, ports, and edges.
|
|
@@ -1978,6 +2428,9 @@ function normalizeDiagramStyle(elementKind, raw) {
|
|
|
1978
2428
|
const key = rawKey.trim();
|
|
1979
2429
|
if (!key || value === undefined || value === null)
|
|
1980
2430
|
continue;
|
|
2431
|
+
if (LEGACY_BOX_SPACING_KEYS.has(key)) {
|
|
2432
|
+
continue;
|
|
2433
|
+
}
|
|
1981
2434
|
// OML node-level keys: pass through to the top level of the normalized style object
|
|
1982
2435
|
if (OML_PASSTHROUGH_STYLE_KEYS.has(key)) {
|
|
1983
2436
|
passthrough[key] = value;
|
|
@@ -2254,6 +2707,17 @@ function resolveEdgeLineAttrs(style) {
|
|
|
2254
2707
|
...(line ?? {}),
|
|
2255
2708
|
};
|
|
2256
2709
|
}
|
|
2710
|
+
function resolveEdgeRouter(style) {
|
|
2711
|
+
const router = asRecord(style.router);
|
|
2712
|
+
return router ?? { name: 'normal' };
|
|
2713
|
+
}
|
|
2714
|
+
function resolveEdgeConnector(style) {
|
|
2715
|
+
const connector = asRecord(style.connector);
|
|
2716
|
+
return connector ?? {
|
|
2717
|
+
name: 'jumpover',
|
|
2718
|
+
args: { size: 5 },
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
2257
2721
|
function resolveEdgeLabelAttrs(style, placement, text) {
|
|
2258
2722
|
const attrs = extractStyleAttrs(style);
|
|
2259
2723
|
const base = asRecord(attrs.label);
|
|
@@ -2276,17 +2740,41 @@ function resolveEdgeLabelBodyAttrs(style, placement) {
|
|
|
2276
2740
|
fillOpacity: 0.9,
|
|
2277
2741
|
stroke: 'none',
|
|
2278
2742
|
strokeWidth: 0,
|
|
2743
|
+
pointerEvents: 'all',
|
|
2279
2744
|
...(base ?? {}),
|
|
2280
2745
|
...(specific ?? {}),
|
|
2281
2746
|
};
|
|
2282
2747
|
}
|
|
2283
|
-
function resolvePortAttrs(style, classes, text) {
|
|
2748
|
+
function resolvePortAttrs(style, classes, text, side = 'right', ownerStroke) {
|
|
2284
2749
|
const attrs = extractStyleAttrs(style);
|
|
2285
2750
|
const body = asRecord(attrs.body);
|
|
2286
2751
|
const icon = asRecord(attrs.icon);
|
|
2287
2752
|
const label = asRecord(attrs.label);
|
|
2288
2753
|
const imageUrl = extractImageHrefFromIcon(icon);
|
|
2289
|
-
|
|
2754
|
+
const labelPosition = side === 'left'
|
|
2755
|
+
? {
|
|
2756
|
+
textAnchor: 'end',
|
|
2757
|
+
x: -10,
|
|
2758
|
+
dy: '0.9em',
|
|
2759
|
+
}
|
|
2760
|
+
: side === 'top'
|
|
2761
|
+
? {
|
|
2762
|
+
textAnchor: 'middle',
|
|
2763
|
+
x: 0,
|
|
2764
|
+
dy: '-0.3em',
|
|
2765
|
+
}
|
|
2766
|
+
: side === 'bottom'
|
|
2767
|
+
? {
|
|
2768
|
+
textAnchor: 'middle',
|
|
2769
|
+
x: 0,
|
|
2770
|
+
dy: '1.4em',
|
|
2771
|
+
}
|
|
2772
|
+
: {
|
|
2773
|
+
textAnchor: 'start',
|
|
2774
|
+
x: 10,
|
|
2775
|
+
dy: '0.9em',
|
|
2776
|
+
};
|
|
2777
|
+
const result = {
|
|
2290
2778
|
body: {
|
|
2291
2779
|
width: 12,
|
|
2292
2780
|
height: 12,
|
|
@@ -2294,7 +2782,7 @@ function resolvePortAttrs(style, classes, text) {
|
|
|
2294
2782
|
y: -6,
|
|
2295
2783
|
class: ['oml-port-body', ...classes].join(' '),
|
|
2296
2784
|
magnet: false,
|
|
2297
|
-
stroke:
|
|
2785
|
+
stroke: ownerStroke ?? CSS_EDITOR_FOREGROUND,
|
|
2298
2786
|
strokeWidth: 1,
|
|
2299
2787
|
fill: CSS_EDITOR_BACKGROUND,
|
|
2300
2788
|
...(body ?? {}),
|
|
@@ -2309,17 +2797,180 @@ function resolvePortAttrs(style, classes, text) {
|
|
|
2309
2797
|
...(icon ?? {}),
|
|
2310
2798
|
...(imageUrl ? { href: imageUrl, xlinkHref: imageUrl, 'xlink:href': imageUrl } : {}),
|
|
2311
2799
|
},
|
|
2312
|
-
|
|
2800
|
+
};
|
|
2801
|
+
if (text) {
|
|
2802
|
+
result.label = {
|
|
2313
2803
|
text,
|
|
2314
2804
|
fill: CSS_EDITOR_FOREGROUND,
|
|
2315
2805
|
fontFamily: 'var(--vscode-editor-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)',
|
|
2316
2806
|
fontSize: 12,
|
|
2317
|
-
|
|
2318
|
-
x: 10,
|
|
2319
|
-
dy: '0.9em',
|
|
2807
|
+
...labelPosition,
|
|
2320
2808
|
...(label ?? {}),
|
|
2321
|
-
}
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2811
|
+
return result;
|
|
2812
|
+
}
|
|
2813
|
+
function resolveBorderPortLabelPosition(side) {
|
|
2814
|
+
return { name: side };
|
|
2815
|
+
}
|
|
2816
|
+
function clamp(value, min, max) {
|
|
2817
|
+
return Math.max(min, Math.min(max, value));
|
|
2818
|
+
}
|
|
2819
|
+
function computeDefaultPortPlacements(graph, nodeById, portsByOwner, boxes) {
|
|
2820
|
+
const placements = new Map();
|
|
2821
|
+
const ownerCenter = (nodeId) => {
|
|
2822
|
+
const box = boxes.get(nodeId);
|
|
2823
|
+
if (!box) {
|
|
2824
|
+
return undefined;
|
|
2825
|
+
}
|
|
2826
|
+
return { x: box.x + (box.width / 2), y: box.y + (box.height / 2) };
|
|
2322
2827
|
};
|
|
2828
|
+
const portIdsByOwner = new Map();
|
|
2829
|
+
for (const [ownerId, ports] of portsByOwner.entries()) {
|
|
2830
|
+
portIdsByOwner.set(ownerId, new Set(ports.map((port) => port.id)));
|
|
2831
|
+
}
|
|
2832
|
+
const peerPortIdsByPort = new Map();
|
|
2833
|
+
const peerCentersByPort = new Map();
|
|
2834
|
+
for (const edge of graph.edges) {
|
|
2835
|
+
const source = nodeById.get(edge.sourceId);
|
|
2836
|
+
const target = nodeById.get(edge.targetId);
|
|
2837
|
+
const sourceOwnerId = source?.kind === 'Port' ? source.parentId : source?.id;
|
|
2838
|
+
const targetOwnerId = target?.kind === 'Port' ? target.parentId : target?.id;
|
|
2839
|
+
if (!sourceOwnerId || !targetOwnerId || sourceOwnerId === targetOwnerId) {
|
|
2840
|
+
continue;
|
|
2841
|
+
}
|
|
2842
|
+
const sourcePeer = ownerCenter(targetOwnerId);
|
|
2843
|
+
const targetPeer = ownerCenter(sourceOwnerId);
|
|
2844
|
+
if (source?.kind === 'Port' && sourcePeer) {
|
|
2845
|
+
const peers = peerCentersByPort.get(source.id) ?? [];
|
|
2846
|
+
peers.push(sourcePeer);
|
|
2847
|
+
peerCentersByPort.set(source.id, peers);
|
|
2848
|
+
if (target?.kind === 'Port') {
|
|
2849
|
+
const peerPortIds = peerPortIdsByPort.get(source.id) ?? [];
|
|
2850
|
+
peerPortIds.push(target.id);
|
|
2851
|
+
peerPortIdsByPort.set(source.id, peerPortIds);
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
if (target?.kind === 'Port' && targetPeer) {
|
|
2855
|
+
const peers = peerCentersByPort.get(target.id) ?? [];
|
|
2856
|
+
peers.push(targetPeer);
|
|
2857
|
+
peerCentersByPort.set(target.id, peers);
|
|
2858
|
+
if (source?.kind === 'Port') {
|
|
2859
|
+
const peerPortIds = peerPortIdsByPort.get(target.id) ?? [];
|
|
2860
|
+
peerPortIds.push(source.id);
|
|
2861
|
+
peerPortIdsByPort.set(target.id, peerPortIds);
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
const anchorFor = (ownerId, side, ratio) => {
|
|
2866
|
+
const box = boxes.get(ownerId);
|
|
2867
|
+
if (!box) {
|
|
2868
|
+
return undefined;
|
|
2869
|
+
}
|
|
2870
|
+
const clampedRatio = clamp(ratio, 0.05, 0.95);
|
|
2871
|
+
if (side === 'left') {
|
|
2872
|
+
return { x: box.x, y: box.y + (box.height * clampedRatio) };
|
|
2873
|
+
}
|
|
2874
|
+
if (side === 'right') {
|
|
2875
|
+
return { x: box.x + box.width, y: box.y + (box.height * clampedRatio) };
|
|
2876
|
+
}
|
|
2877
|
+
if (side === 'top') {
|
|
2878
|
+
return { x: box.x + (box.width * clampedRatio), y: box.y };
|
|
2879
|
+
}
|
|
2880
|
+
return { x: box.x + (box.width * clampedRatio), y: box.y + box.height };
|
|
2881
|
+
};
|
|
2882
|
+
const currentAnchorForPort = (portId) => {
|
|
2883
|
+
const port = nodeById.get(portId);
|
|
2884
|
+
if (!port?.parentId) {
|
|
2885
|
+
return undefined;
|
|
2886
|
+
}
|
|
2887
|
+
const placement = placements.get(portId) ?? { side: 'right', ratio: 0.5 };
|
|
2888
|
+
return anchorFor(port.parentId, placement.side, placement.ratio);
|
|
2889
|
+
};
|
|
2890
|
+
const peerAnchorsForPort = (portId) => {
|
|
2891
|
+
const peerAnchors = [];
|
|
2892
|
+
for (const peerPortId of peerPortIdsByPort.get(portId) ?? []) {
|
|
2893
|
+
const anchor = currentAnchorForPort(peerPortId);
|
|
2894
|
+
if (anchor) {
|
|
2895
|
+
peerAnchors.push(anchor);
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
for (const peerCenter of peerCentersByPort.get(portId) ?? []) {
|
|
2899
|
+
peerAnchors.push(peerCenter);
|
|
2900
|
+
}
|
|
2901
|
+
return peerAnchors;
|
|
2902
|
+
};
|
|
2903
|
+
const candidateCost = (ownerId, side, portId) => {
|
|
2904
|
+
const owner = ownerCenter(ownerId);
|
|
2905
|
+
const candidate = anchorFor(ownerId, side, 0.5);
|
|
2906
|
+
if (!owner || !candidate) {
|
|
2907
|
+
return Number.POSITIVE_INFINITY;
|
|
2908
|
+
}
|
|
2909
|
+
const peerAnchors = peerAnchorsForPort(portId);
|
|
2910
|
+
if (peerAnchors.length === 0) {
|
|
2911
|
+
return side === 'right' ? 0 : 1;
|
|
2912
|
+
}
|
|
2913
|
+
let cost = 0;
|
|
2914
|
+
for (const peer of peerAnchors) {
|
|
2915
|
+
cost += Math.abs(candidate.x - peer.x) + Math.abs(candidate.y - peer.y);
|
|
2916
|
+
}
|
|
2917
|
+
return cost;
|
|
2918
|
+
};
|
|
2919
|
+
const sideOrder = ['right', 'left', 'top', 'bottom'];
|
|
2920
|
+
const primaryCoordinate = (side, portId) => {
|
|
2921
|
+
const peerAnchors = peerAnchorsForPort(portId);
|
|
2922
|
+
if (peerAnchors.length === 0) {
|
|
2923
|
+
return 0;
|
|
2924
|
+
}
|
|
2925
|
+
const values = peerAnchors.map((peer) => ((side === 'left' || side === 'right') ? peer.y : peer.x));
|
|
2926
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
2927
|
+
};
|
|
2928
|
+
for (const [ownerId, ports] of portsByOwner.entries()) {
|
|
2929
|
+
const count = ports.length;
|
|
2930
|
+
for (let index = 0; index < count; index += 1) {
|
|
2931
|
+
placements.set(ports[index].id, {
|
|
2932
|
+
side: 'right',
|
|
2933
|
+
ratio: count <= 0 ? 0.5 : ((index + 1) / (count + 1)),
|
|
2934
|
+
});
|
|
2935
|
+
}
|
|
2936
|
+
const ownerPortIds = portIdsByOwner.get(ownerId) ?? new Set();
|
|
2937
|
+
for (let pass = 0; pass < 2; pass += 1) {
|
|
2938
|
+
for (const port of ports) {
|
|
2939
|
+
let bestSide = 'right';
|
|
2940
|
+
let bestCost = Number.POSITIVE_INFINITY;
|
|
2941
|
+
for (const side of sideOrder) {
|
|
2942
|
+
const cost = candidateCost(ownerId, side, port.id);
|
|
2943
|
+
if (cost < bestCost || (cost === bestCost && sideOrder.indexOf(side) < sideOrder.indexOf(bestSide))) {
|
|
2944
|
+
bestCost = cost;
|
|
2945
|
+
bestSide = side;
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
const existing = placements.get(port.id) ?? { ratio: 0.5, side: bestSide };
|
|
2949
|
+
placements.set(port.id, { side: bestSide, ratio: existing.ratio });
|
|
2950
|
+
}
|
|
2951
|
+
const sideGroups = new Map([
|
|
2952
|
+
['left', []],
|
|
2953
|
+
['right', []],
|
|
2954
|
+
['top', []],
|
|
2955
|
+
['bottom', []],
|
|
2956
|
+
]);
|
|
2957
|
+
for (const port of ports) {
|
|
2958
|
+
sideGroups.get(placements.get(port.id)?.side ?? 'right')?.push(port);
|
|
2959
|
+
}
|
|
2960
|
+
for (const [side, sidePorts] of sideGroups.entries()) {
|
|
2961
|
+
sidePorts.sort((left, right) => (primaryCoordinate(side, left.id) - primaryCoordinate(side, right.id)
|
|
2962
|
+
|| left.id.localeCompare(right.id)));
|
|
2963
|
+
for (let index = 0; index < sidePorts.length; index += 1) {
|
|
2964
|
+
placements.set(sidePorts[index].id, {
|
|
2965
|
+
side,
|
|
2966
|
+
ratio: (index + 1) / (sidePorts.length + 1),
|
|
2967
|
+
});
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
void ownerPortIds;
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
return placements;
|
|
2323
2974
|
}
|
|
2324
2975
|
function toPositiveNumber(value) {
|
|
2325
2976
|
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|