@oml/markdown 0.9.0 → 0.11.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-executor.js +2 -9
- package/out/md/md-executor.js.map +1 -1
- package/out/md/md-runtime.js +2 -26
- package/out/md/md-runtime.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 +738 -252
- package/out/renderers/diagram-renderer.js.map +1 -1
- package/out/renderers/graph-renderer.js +5 -9
- package/out/renderers/graph-renderer.js.map +1 -1
- package/out/renderers/renderer.d.ts +3 -0
- package/out/renderers/renderer.js +53 -0
- package/out/renderers/renderer.js.map +1 -1
- package/out/renderers/table-renderer.d.ts +8 -1
- package/out/renderers/table-renderer.js +22 -1
- package/out/renderers/table-renderer.js.map +1 -1
- package/out/renderers/text-renderer.d.ts +0 -1
- package/out/renderers/text-renderer.js +2 -9
- package/out/renderers/text-renderer.js.map +1 -1
- package/out/renderers/wikilink-utils.js +3 -10
- package/out/renderers/wikilink-utils.js.map +1 -1
- package/out/static/browser-runtime.bundle.js +1591 -6408
- package/out/static/browser-runtime.bundle.js.map +4 -4
- package/out/static/browser-runtime.js +3 -0
- package/out/static/browser-runtime.js.map +1 -1
- package/package.json +2 -2
- package/src/md/md-executor.ts +2 -9
- package/src/md/md-runtime.ts +2 -28
- package/src/renderers/chart-renderer.ts +93 -2
- package/src/renderers/diagram-renderer.ts +799 -258
- package/src/renderers/graph-renderer.ts +5 -9
- package/src/renderers/renderer.ts +66 -0
- package/src/renderers/table-renderer.ts +39 -3
- package/src/renderers/text-renderer.ts +2 -7
- package/src/renderers/wikilink-utils.ts +4 -10
- package/src/static/browser-runtime.ts +3 -0
- package/src/static/markdown-webview.css +1361 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
2
|
import 'reflect-metadata';
|
|
3
|
-
import { CanvasMarkdownBlockRenderer } from './renderer.js';
|
|
3
|
+
import { CanvasMarkdownBlockRenderer, displayLabelFromIri } from './renderer.js';
|
|
4
4
|
const D = 'http://opencaesar.io/oml/diagram#';
|
|
5
5
|
const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
6
6
|
const TYPE_IRIS = {
|
|
@@ -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,
|
|
@@ -121,7 +117,10 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
121
117
|
},
|
|
122
118
|
connecting: {
|
|
123
119
|
router: 'normal',
|
|
124
|
-
connector:
|
|
120
|
+
connector: {
|
|
121
|
+
name: 'jumpover',
|
|
122
|
+
args: { size: 5 },
|
|
123
|
+
},
|
|
125
124
|
allowBlank: false,
|
|
126
125
|
allowNode: false,
|
|
127
126
|
allowPort: false,
|
|
@@ -137,13 +136,65 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
137
136
|
edgeMovable: false,
|
|
138
137
|
vertexMovable: false,
|
|
139
138
|
arrowheadMovable: false,
|
|
140
|
-
labelMovable:
|
|
139
|
+
labelMovable: true,
|
|
141
140
|
};
|
|
142
141
|
},
|
|
143
142
|
background: {
|
|
144
143
|
color: CSS_CANVAS_BACKGROUND,
|
|
145
144
|
},
|
|
145
|
+
onPortRendered: ({ port, node, container }) => {
|
|
146
|
+
if (boundPortContainers.has(container)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
boundPortContainers.add(container);
|
|
150
|
+
container.setAttribute('data-port-id', String(port.id));
|
|
151
|
+
container.style.cursor = 'grab';
|
|
152
|
+
container.style.touchAction = 'none';
|
|
153
|
+
container.addEventListener('pointerdown', (event) => {
|
|
154
|
+
if (typeof graphView.cleanSelection === 'function') {
|
|
155
|
+
graphView.cleanSelection();
|
|
156
|
+
}
|
|
157
|
+
nodeTransform.clearWidgets();
|
|
158
|
+
const owner = graphView.getCellById(String(node.id));
|
|
159
|
+
if (!owner) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
startPortDrag(event.clientX, event.clientY, owner, String(port.id), event.pointerId, container);
|
|
163
|
+
event.preventDefault();
|
|
164
|
+
event.stopPropagation();
|
|
165
|
+
if (typeof event.stopImmediatePropagation === 'function') {
|
|
166
|
+
event.stopImmediatePropagation();
|
|
167
|
+
}
|
|
168
|
+
if (typeof container.setPointerCapture === 'function') {
|
|
169
|
+
try {
|
|
170
|
+
container.setPointerCapture(event.pointerId);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Ignore capture failures; window listeners still handle the drag.
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
const x6Mod = await import('@antv/x6');
|
|
180
|
+
const TransformCtor = x6Mod.Transform;
|
|
181
|
+
if (typeof TransformCtor !== 'function') {
|
|
182
|
+
throw new Error('X6 Transform plugin is unavailable in @antv/x6');
|
|
183
|
+
}
|
|
184
|
+
nodeTransform = new TransformCtor({
|
|
185
|
+
resizing: {
|
|
186
|
+
enabled: (node) => node?.getData?.()?.kind === 'Node',
|
|
187
|
+
minWidth: 48,
|
|
188
|
+
minHeight: 32,
|
|
189
|
+
orthogonal: true,
|
|
190
|
+
restrict: false,
|
|
191
|
+
autoScroll: false,
|
|
192
|
+
preserveAspectRatio: false,
|
|
193
|
+
allowReverse: false,
|
|
194
|
+
},
|
|
195
|
+
rotating: false,
|
|
146
196
|
});
|
|
197
|
+
graphView.use(nodeTransform);
|
|
147
198
|
const toPlainRect = (value) => {
|
|
148
199
|
if (!value)
|
|
149
200
|
return undefined;
|
|
@@ -178,12 +229,7 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
178
229
|
for (const list of portsByOwner.values()) {
|
|
179
230
|
list.sort((a, b) => a.id.localeCompare(b.id));
|
|
180
231
|
}
|
|
181
|
-
const
|
|
182
|
-
for (const [ownerId, ports] of portsByOwner.entries()) {
|
|
183
|
-
for (const port of ports) {
|
|
184
|
-
ownerByPortId.set(port.id, ownerId);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
232
|
+
const portPlacementById = computeDefaultPortPlacements(graph, nodeById, portsByOwner, layout.boxes);
|
|
187
233
|
const ordered = [...graph.nodes]
|
|
188
234
|
.filter((node) => node.kind !== 'Port')
|
|
189
235
|
.sort((a, b) => nodeDepth(a.id, nodeById) - nodeDepth(b.id, nodeById));
|
|
@@ -194,19 +240,35 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
194
240
|
const labelText = node.labels.length > 0 ? node.labels.join('\n') : localName(node.id);
|
|
195
241
|
const resolvedStyle = node.style;
|
|
196
242
|
const resolvedShape = resolveRenderNodeShape(resolvedStyle);
|
|
243
|
+
const { bodyAttrs, labelAttrs, iconSvgAttrs, iconPathAttrs, imageAttrs } = resolveNodeAttrs(resolvedStyle);
|
|
197
244
|
const x = box.x;
|
|
198
245
|
const y = box.y;
|
|
199
246
|
const ownerPorts = portsByOwner.get(node.id) ?? [];
|
|
200
|
-
const portItems = ownerPorts.map((port
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
247
|
+
const portItems = ownerPorts.map((port) => {
|
|
248
|
+
const placement = portPlacementById.get(port.id) ?? { side: 'right', ratio: 0.5 };
|
|
249
|
+
const ratio = clamp(placement.ratio, 0.05, 0.95);
|
|
250
|
+
const position = placement.side === 'left' || placement.side === 'right'
|
|
251
|
+
? {
|
|
252
|
+
x: placement.side === 'left' ? 0 : box.width,
|
|
253
|
+
y: ratio * box.height,
|
|
254
|
+
}
|
|
255
|
+
: {
|
|
256
|
+
x: ratio * box.width,
|
|
257
|
+
y: placement.side === 'top' ? 0 : box.height,
|
|
258
|
+
};
|
|
259
|
+
const portText = port.labels[0];
|
|
260
|
+
return {
|
|
261
|
+
id: port.id,
|
|
262
|
+
group: 'boundary',
|
|
263
|
+
args: {
|
|
264
|
+
x: position.x,
|
|
265
|
+
y: position.y,
|
|
266
|
+
side: placement.side,
|
|
267
|
+
ratio,
|
|
268
|
+
},
|
|
269
|
+
attrs: resolvePortAttrs(port.style, port.classes, portText, placement.side, typeof bodyAttrs.stroke === 'string' ? bodyAttrs.stroke : undefined),
|
|
270
|
+
};
|
|
271
|
+
});
|
|
210
272
|
const iconPathSelectors = iconPathAttrs.map((_, index) => `iconPath${index}`);
|
|
211
273
|
graphView.addNode({
|
|
212
274
|
id: node.id,
|
|
@@ -310,7 +372,7 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
310
372
|
},
|
|
311
373
|
items: portItems,
|
|
312
374
|
} : undefined,
|
|
313
|
-
zIndex:
|
|
375
|
+
zIndex: 50,
|
|
314
376
|
data: {
|
|
315
377
|
kind: node.kind,
|
|
316
378
|
ownerId: node.parentId,
|
|
@@ -457,11 +519,32 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
457
519
|
let maxBottom = Number.NEGATIVE_INFINITY;
|
|
458
520
|
const childCells = [];
|
|
459
521
|
const childDebug = [];
|
|
522
|
+
const childGeometryBounds = (child) => {
|
|
523
|
+
if (!child || typeof child.size !== 'function') {
|
|
524
|
+
return undefined;
|
|
525
|
+
}
|
|
526
|
+
const size = child.size();
|
|
527
|
+
const width = Number(size?.width);
|
|
528
|
+
const height = Number(size?.height);
|
|
529
|
+
let position;
|
|
530
|
+
if (typeof child.getPosition === 'function') {
|
|
531
|
+
position = child.getPosition();
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
position = child.position;
|
|
535
|
+
}
|
|
536
|
+
const x = Number(position?.x);
|
|
537
|
+
const y = Number(position?.y);
|
|
538
|
+
if (![x, y, width, height].every(Number.isFinite)) {
|
|
539
|
+
return undefined;
|
|
540
|
+
}
|
|
541
|
+
return { x, y, width, height };
|
|
542
|
+
};
|
|
460
543
|
for (const childId of childIds) {
|
|
461
544
|
const child = graphView.getCellById(childId);
|
|
462
|
-
|
|
545
|
+
const absBBox = childGeometryBounds(child);
|
|
546
|
+
if (!absBBox)
|
|
463
547
|
continue;
|
|
464
|
-
const absBBox = child.getBBox();
|
|
465
548
|
const relLeft = absBBox.x - containerBBox.x;
|
|
466
549
|
const relTop = absBBox.y - containerBBox.y;
|
|
467
550
|
minLeft = Math.min(minLeft, relLeft);
|
|
@@ -493,10 +576,11 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
493
576
|
// container origin (without moving its embedded children, which keep their
|
|
494
577
|
// absolute positions) and expand the size to compensate so the opposite edge
|
|
495
578
|
// is preserved.
|
|
496
|
-
|
|
497
|
-
const
|
|
498
|
-
const
|
|
499
|
-
const
|
|
579
|
+
const spacing = resolveBoxSpacing(containerSpec.style, 0);
|
|
580
|
+
const minInsetX = spacing.marginLeft + spacing.paddingLeft;
|
|
581
|
+
const minInsetY = spacing.marginTop + spacing.paddingTop;
|
|
582
|
+
const shiftX = minLeft < minInsetX ? minLeft - minInsetX : 0;
|
|
583
|
+
const shiftY = minTop < minInsetY ? minTop - minInsetY : 0;
|
|
500
584
|
// Dimensions needed relative to the (possibly shifted) new origin.
|
|
501
585
|
const baseWidth = containerSize.width - shiftX;
|
|
502
586
|
const baseHeight = containerSize.height - shiftY;
|
|
@@ -509,7 +593,8 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
509
593
|
childIds,
|
|
510
594
|
containerBBox: toPlainRect(containerBBox),
|
|
511
595
|
containerSize: toPlainRect({ x: 0, y: 0, ...containerSize }),
|
|
512
|
-
|
|
596
|
+
minInsetX,
|
|
597
|
+
minInsetY,
|
|
513
598
|
bounds: { minLeft, minTop, maxRight, maxBottom },
|
|
514
599
|
shift: { shiftX, shiftY },
|
|
515
600
|
childDebug,
|
|
@@ -526,6 +611,7 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
526
611
|
to: { width: neededWidth, height: neededHeight },
|
|
527
612
|
});
|
|
528
613
|
container.resize(neededWidth, neededHeight);
|
|
614
|
+
syncOwnedPortPositions(containerId);
|
|
529
615
|
}
|
|
530
616
|
else {
|
|
531
617
|
logResize('container-no-resize', { containerId });
|
|
@@ -610,6 +696,48 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
610
696
|
return;
|
|
611
697
|
growAncestorContainers(node);
|
|
612
698
|
});
|
|
699
|
+
const syncOwnedPortPositions = (ownerId) => {
|
|
700
|
+
const owner = graphView.getCellById(ownerId);
|
|
701
|
+
if (!owner || typeof owner.size !== 'function' || typeof owner.getPorts !== 'function' || typeof owner.setPortProp !== 'function') {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const size = owner.size();
|
|
705
|
+
const portIds = owner.getPorts()
|
|
706
|
+
.map((port) => String(port?.id ?? ''))
|
|
707
|
+
.filter((portId) => portId.length > 0);
|
|
708
|
+
for (const portId of portIds) {
|
|
709
|
+
const placement = portPlacementById.get(portId) ?? { side: 'right', ratio: 0.5 };
|
|
710
|
+
const ratio = clamp(placement.ratio, 0.05, 0.95);
|
|
711
|
+
const nextArgs = placement.side === 'left' || placement.side === 'right'
|
|
712
|
+
? {
|
|
713
|
+
x: placement.side === 'left' ? 0 : size.width,
|
|
714
|
+
y: ratio * size.height,
|
|
715
|
+
side: placement.side,
|
|
716
|
+
ratio,
|
|
717
|
+
}
|
|
718
|
+
: {
|
|
719
|
+
x: ratio * size.width,
|
|
720
|
+
y: placement.side === 'top' ? 0 : size.height,
|
|
721
|
+
side: placement.side,
|
|
722
|
+
ratio,
|
|
723
|
+
};
|
|
724
|
+
owner.setPortProp(portId, {
|
|
725
|
+
args: nextArgs,
|
|
726
|
+
label: {
|
|
727
|
+
position: resolveBorderPortLabelPosition(placement.side),
|
|
728
|
+
},
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
graphView.on('node:resizing', ({ node }) => {
|
|
733
|
+
syncOwnedPortPositions(String(node?.id ?? ''));
|
|
734
|
+
});
|
|
735
|
+
graphView.on('node:resized', ({ node, options }) => {
|
|
736
|
+
if (options?.silent)
|
|
737
|
+
return;
|
|
738
|
+
syncOwnedPortPositions(String(node?.id ?? ''));
|
|
739
|
+
growAncestorContainers(node);
|
|
740
|
+
});
|
|
613
741
|
const edgeLabelPosition = (placement) => {
|
|
614
742
|
if (placement === 'begin')
|
|
615
743
|
return 0.15;
|
|
@@ -617,105 +745,104 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
617
745
|
return 0.85;
|
|
618
746
|
return 0.5;
|
|
619
747
|
};
|
|
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]));
|
|
748
|
+
const directedPairKey = (sourceId, targetId) => `${sourceId}=>${targetId}`;
|
|
749
|
+
const undirectedPairKey = (leftId, rightId) => (leftId < rightId ? `${leftId}<=>${rightId}` : `${rightId}<=>${leftId}`);
|
|
750
|
+
const edgeIdsByPair = new Map();
|
|
628
751
|
const undirectedEdgeIdsByPair = new Map();
|
|
629
752
|
for (const edge of graph.edges) {
|
|
630
|
-
|
|
631
|
-
const targetOwner = endpointOwnerId(edge.targetId);
|
|
632
|
-
if (!sourceOwner || !targetOwner || sourceOwner === targetOwner)
|
|
753
|
+
if (!edge.sourceId || !edge.targetId)
|
|
633
754
|
continue;
|
|
634
|
-
const key =
|
|
635
|
-
const ids =
|
|
755
|
+
const key = directedPairKey(edge.sourceId, edge.targetId);
|
|
756
|
+
const ids = edgeIdsByPair.get(key) ?? [];
|
|
636
757
|
ids.push(edge.id);
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
if (edgeIds.length <= 1) {
|
|
758
|
+
edgeIdsByPair.set(key, ids);
|
|
759
|
+
const undirectedKey = undirectedPairKey(edge.sourceId, edge.targetId);
|
|
760
|
+
const pairIds = undirectedEdgeIdsByPair.get(undirectedKey) ?? [];
|
|
761
|
+
pairIds.push(edge.id);
|
|
762
|
+
undirectedEdgeIdsByPair.set(undirectedKey, pairIds);
|
|
763
|
+
}
|
|
764
|
+
const edgePointForEndpoint = (endpointId) => {
|
|
765
|
+
const endpoint = nodeById.get(endpointId);
|
|
766
|
+
if (!endpoint) {
|
|
647
767
|
return undefined;
|
|
648
768
|
}
|
|
649
|
-
|
|
650
|
-
const
|
|
651
|
-
const
|
|
652
|
-
if (!
|
|
653
|
-
return
|
|
654
|
-
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
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);
|
|
769
|
+
if (endpoint.kind === 'Port' && endpoint.parentId) {
|
|
770
|
+
const ownerBox = layout.boxes.get(endpoint.parentId);
|
|
771
|
+
const placement = portPlacementById.get(endpoint.id);
|
|
772
|
+
if (!ownerBox || !placement) {
|
|
773
|
+
return undefined;
|
|
774
|
+
}
|
|
775
|
+
const ratio = clamp(placement.ratio, 0.05, 0.95);
|
|
776
|
+
if (placement.side === 'left') {
|
|
777
|
+
return { x: ownerBox.x, y: ownerBox.y + (ownerBox.height * ratio) };
|
|
677
778
|
}
|
|
678
|
-
|
|
679
|
-
|
|
779
|
+
if (placement.side === 'right') {
|
|
780
|
+
return { x: ownerBox.x + ownerBox.width, y: ownerBox.y + (ownerBox.height * ratio) };
|
|
680
781
|
}
|
|
782
|
+
if (placement.side === 'top') {
|
|
783
|
+
return { x: ownerBox.x + (ownerBox.width * ratio), y: ownerBox.y };
|
|
784
|
+
}
|
|
785
|
+
return { x: ownerBox.x + (ownerBox.width * ratio), y: ownerBox.y + ownerBox.height };
|
|
681
786
|
}
|
|
682
|
-
|
|
683
|
-
if (
|
|
787
|
+
const box = layout.boxes.get(endpointId);
|
|
788
|
+
if (!box) {
|
|
684
789
|
return undefined;
|
|
685
790
|
}
|
|
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;
|
|
791
|
+
return { x: box.x + (box.width / 2), y: box.y + (box.height / 2) };
|
|
792
|
+
};
|
|
793
|
+
const fanningVertexForEdge = (edge) => {
|
|
794
|
+
if (!edge.sourceId || !edge.targetId) {
|
|
795
|
+
return undefined;
|
|
698
796
|
}
|
|
699
|
-
|
|
797
|
+
const undirectedIds = undirectedEdgeIdsByPair.get(undirectedPairKey(edge.sourceId, edge.targetId)) ?? [];
|
|
798
|
+
if (undirectedIds.length <= 1) {
|
|
700
799
|
return undefined;
|
|
701
800
|
}
|
|
702
|
-
const
|
|
703
|
-
const
|
|
704
|
-
if (!
|
|
801
|
+
const sourcePoint = edgePointForEndpoint(edge.sourceId);
|
|
802
|
+
const targetPoint = edgePointForEndpoint(edge.targetId);
|
|
803
|
+
if (!sourcePoint || !targetPoint) {
|
|
705
804
|
return undefined;
|
|
706
805
|
}
|
|
707
|
-
const sx =
|
|
708
|
-
const sy =
|
|
709
|
-
const tx =
|
|
710
|
-
const ty =
|
|
806
|
+
const sx = sourcePoint.x;
|
|
807
|
+
const sy = sourcePoint.y;
|
|
808
|
+
const tx = targetPoint.x;
|
|
809
|
+
const ty = targetPoint.y;
|
|
711
810
|
const dx = tx - sx;
|
|
712
811
|
const dy = ty - sy;
|
|
713
812
|
const len = Math.hypot(dx, dy);
|
|
714
813
|
if (!Number.isFinite(len) || len < 1) {
|
|
715
814
|
return undefined;
|
|
716
815
|
}
|
|
717
|
-
const
|
|
718
|
-
|
|
816
|
+
const forwardIds = (edgeIdsByPair.get(directedPairKey(edge.sourceId, edge.targetId)) ?? [])
|
|
817
|
+
.slice()
|
|
818
|
+
.sort((left, right) => left.localeCompare(right));
|
|
819
|
+
const reverseIds = (edgeIdsByPair.get(directedPairKey(edge.targetId, edge.sourceId)) ?? [])
|
|
820
|
+
.slice()
|
|
821
|
+
.sort((left, right) => left.localeCompare(right));
|
|
822
|
+
let offset = 0;
|
|
823
|
+
if (forwardIds.length > 0 && reverseIds.length > 0) {
|
|
824
|
+
const forwardIndex = forwardIds.indexOf(edge.id);
|
|
825
|
+
const reverseIndex = reverseIds.indexOf(edge.id);
|
|
826
|
+
if (forwardIndex >= 0) {
|
|
827
|
+
const laneOffset = forwardIndex - ((forwardIds.length - 1) / 2);
|
|
828
|
+
offset = 16 + (laneOffset * 10);
|
|
829
|
+
}
|
|
830
|
+
else if (reverseIndex >= 0) {
|
|
831
|
+
const laneOffset = reverseIndex - ((reverseIds.length - 1) / 2);
|
|
832
|
+
offset = -16 + (laneOffset * 10);
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
return undefined;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
const laneIndex = forwardIds.indexOf(edge.id);
|
|
840
|
+
if (laneIndex < 0 || forwardIds.length <= 1) {
|
|
841
|
+
return undefined;
|
|
842
|
+
}
|
|
843
|
+
const laneOffset = laneIndex - ((forwardIds.length - 1) / 2);
|
|
844
|
+
offset = laneOffset * 16;
|
|
845
|
+
}
|
|
719
846
|
if (Math.abs(offset) < 0.01) {
|
|
720
847
|
return undefined;
|
|
721
848
|
}
|
|
@@ -750,8 +877,8 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
750
877
|
id: edge.id,
|
|
751
878
|
source: resolveEndpoint(edge.sourceId),
|
|
752
879
|
target: resolveEndpoint(edge.targetId),
|
|
753
|
-
router:
|
|
754
|
-
connector:
|
|
880
|
+
router: resolveEdgeRouter(resolvedStyle),
|
|
881
|
+
connector: resolveEdgeConnector(resolvedStyle),
|
|
755
882
|
attrs: {
|
|
756
883
|
line: lineAttrs,
|
|
757
884
|
},
|
|
@@ -768,7 +895,7 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
768
895
|
labelBody: resolveEdgeLabelBodyAttrs(resolvedStyle, label.placement),
|
|
769
896
|
},
|
|
770
897
|
})),
|
|
771
|
-
zIndex:
|
|
898
|
+
zIndex: 50,
|
|
772
899
|
});
|
|
773
900
|
}
|
|
774
901
|
// Compartments are structural containers: do not drag them; select parent instead.
|
|
@@ -806,56 +933,120 @@ async function renderWithX6(canvas, baseId, graph, layoutOptions, actions) {
|
|
|
806
933
|
e.preventDefault();
|
|
807
934
|
e.stopPropagation();
|
|
808
935
|
});
|
|
809
|
-
const
|
|
936
|
+
const sidePriority = (side) => {
|
|
937
|
+
switch (side) {
|
|
938
|
+
case 'left':
|
|
939
|
+
return 0;
|
|
940
|
+
case 'top':
|
|
941
|
+
return 1;
|
|
942
|
+
case 'right':
|
|
943
|
+
return 2;
|
|
944
|
+
case 'bottom':
|
|
945
|
+
return 3;
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
const projectPortPlacement = (node, clientX, clientY, preferredSide) => {
|
|
949
|
+
if (!node || typeof node.size !== 'function')
|
|
950
|
+
return undefined;
|
|
951
|
+
const size = node.size();
|
|
952
|
+
const position = typeof node.getPosition === 'function'
|
|
953
|
+
? node.getPosition()
|
|
954
|
+
: { x: Number(node?.position?.x ?? 0), y: Number(node?.position?.y ?? 0) };
|
|
955
|
+
const localPoint = typeof graphView.clientToGraph === 'function'
|
|
956
|
+
? graphView.clientToGraph(clientX, clientY)
|
|
957
|
+
: { x: clientX, y: clientY };
|
|
958
|
+
const localX = clamp(localPoint.x - position.x, 0, size.width);
|
|
959
|
+
const localY = clamp(localPoint.y - position.y, 0, size.height);
|
|
960
|
+
const candidates = [
|
|
961
|
+
{ side: 'left', distance: localX },
|
|
962
|
+
{ side: 'right', distance: size.width - localX },
|
|
963
|
+
{ side: 'top', distance: localY },
|
|
964
|
+
{ side: 'bottom', distance: size.height - localY },
|
|
965
|
+
];
|
|
966
|
+
candidates.sort((left, right) => left.distance - right.distance || sidePriority(left.side) - sidePriority(right.side));
|
|
967
|
+
const closest = candidates[0];
|
|
968
|
+
const preferredCandidate = preferredSide
|
|
969
|
+
? candidates.find((candidate) => candidate.side === preferredSide)
|
|
970
|
+
: undefined;
|
|
971
|
+
const hysteresis = 8;
|
|
972
|
+
const side = preferredCandidate && preferredSide && (preferredCandidate.distance - closest.distance) <= hysteresis
|
|
973
|
+
? preferredSide
|
|
974
|
+
: closest.side;
|
|
975
|
+
const ratio = side === 'left' || side === 'right'
|
|
976
|
+
? (size.height > 0 ? localY / size.height : 0.5)
|
|
977
|
+
: (size.width > 0 ? localX / size.width : 0.5);
|
|
978
|
+
return {
|
|
979
|
+
side,
|
|
980
|
+
ratio: Math.max(0.05, Math.min(0.95, ratio)),
|
|
981
|
+
};
|
|
982
|
+
};
|
|
810
983
|
const onPointerMove = (event) => {
|
|
811
|
-
if (!activePortDrag)
|
|
984
|
+
if (!activePortDrag || event.pointerId !== activePortDrag.pointerId)
|
|
812
985
|
return;
|
|
813
986
|
const node = graphView.getCellById(activePortDrag.nodeId);
|
|
814
987
|
if (!node || typeof node.size !== 'function' || typeof node.getPort !== 'function' || typeof node.setPortProp !== 'function')
|
|
815
988
|
return;
|
|
989
|
+
const placement = projectPortPlacement(node, event.clientX, event.clientY, activePortDrag.side);
|
|
990
|
+
if (!placement)
|
|
991
|
+
return;
|
|
992
|
+
if (placement.side === activePortDrag.side && Math.abs(placement.ratio - activePortDrag.ratio) < 0.005) {
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
816
995
|
const size = node.size();
|
|
817
|
-
const
|
|
818
|
-
|
|
996
|
+
const nextX = placement.side === 'left'
|
|
997
|
+
? 0
|
|
998
|
+
: placement.side === 'right'
|
|
999
|
+
? size.width
|
|
1000
|
+
: placement.ratio * size.width;
|
|
1001
|
+
const nextY = placement.side === 'top'
|
|
1002
|
+
? 0
|
|
1003
|
+
: placement.side === 'bottom'
|
|
1004
|
+
? size.height
|
|
1005
|
+
: placement.ratio * size.height;
|
|
1006
|
+
node.setPortProp(activePortDrag.portId, 'args/x', nextX);
|
|
819
1007
|
node.setPortProp(activePortDrag.portId, 'args/y', nextY);
|
|
1008
|
+
node.setPortProp(activePortDrag.portId, 'args/side', placement.side);
|
|
1009
|
+
node.setPortProp(activePortDrag.portId, 'args/ratio', placement.ratio);
|
|
1010
|
+
if (placement.side !== activePortDrag.side) {
|
|
1011
|
+
node.setPortProp(activePortDrag.portId, 'label/position', resolveBorderPortLabelPosition(placement.side));
|
|
1012
|
+
}
|
|
1013
|
+
activePortDrag.side = placement.side;
|
|
1014
|
+
activePortDrag.ratio = placement.ratio;
|
|
1015
|
+
portPlacementById.set(activePortDrag.portId, placement);
|
|
820
1016
|
};
|
|
821
|
-
const onPointerUp = () => {
|
|
1017
|
+
const onPointerUp = (event) => {
|
|
1018
|
+
if (!activePortDrag || event.pointerId !== activePortDrag.pointerId)
|
|
1019
|
+
return;
|
|
1020
|
+
if (typeof activePortDrag.container.releasePointerCapture === 'function') {
|
|
1021
|
+
try {
|
|
1022
|
+
activePortDrag.container.releasePointerCapture(activePortDrag.pointerId);
|
|
1023
|
+
}
|
|
1024
|
+
catch {
|
|
1025
|
+
// Ignore capture release failures.
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
822
1028
|
activePortDrag = undefined;
|
|
823
1029
|
};
|
|
824
1030
|
window.addEventListener('pointermove', onPointerMove);
|
|
825
1031
|
window.addEventListener('pointerup', onPointerUp);
|
|
826
|
-
const startPortDrag = (clientY, node, portId) => {
|
|
1032
|
+
const startPortDrag = (clientX, clientY, node, portId, pointerId, container) => {
|
|
827
1033
|
if (!node || typeof node.getPort !== 'function')
|
|
828
1034
|
return;
|
|
829
|
-
const
|
|
830
|
-
|
|
1035
|
+
const placement = projectPortPlacement(node, clientX, clientY, portPlacementById.get(portId)?.side ?? 'right')
|
|
1036
|
+
?? portPlacementById.get(portId)
|
|
1037
|
+
?? { side: 'right', ratio: 0.5 };
|
|
831
1038
|
activePortDrag = {
|
|
832
1039
|
nodeId: String(node.id),
|
|
833
1040
|
portId: String(portId),
|
|
834
|
-
|
|
835
|
-
|
|
1041
|
+
pointerId,
|
|
1042
|
+
side: placement.side,
|
|
1043
|
+
ratio: placement.ratio,
|
|
1044
|
+
container,
|
|
836
1045
|
};
|
|
837
1046
|
};
|
|
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
1047
|
liveCanvas.addEventListener('DOMNodeRemovedFromDocument', () => {
|
|
856
1048
|
window.removeEventListener('pointermove', onPointerMove);
|
|
857
1049
|
window.removeEventListener('pointerup', onPointerUp);
|
|
858
|
-
liveCanvas.removeEventListener('pointerdown', onCanvasPointerDown, true);
|
|
859
1050
|
}, { once: true });
|
|
860
1051
|
const resultContainer = liveCanvas.closest('.oml-md-result');
|
|
861
1052
|
const resizeObserver = new ResizeObserver(() => {
|
|
@@ -1238,7 +1429,7 @@ async function loadDagreLib() {
|
|
|
1238
1429
|
dagreLib = mod.default ?? mod;
|
|
1239
1430
|
return dagreLib;
|
|
1240
1431
|
}
|
|
1241
|
-
async function layoutGraphDagre(graph, options) {
|
|
1432
|
+
async function layoutGraphDagre(graph, options, rootSpacing) {
|
|
1242
1433
|
const dagre = await loadDagreLib();
|
|
1243
1434
|
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
1244
1435
|
const layoutNodes = graph.nodes.filter((node) => node.kind !== 'Port');
|
|
@@ -1275,8 +1466,6 @@ async function layoutGraphDagre(graph, options) {
|
|
|
1275
1466
|
stack: {
|
|
1276
1467
|
direction: styleLayout.direction === 'horizontal' ? 'horizontal' : 'vertical',
|
|
1277
1468
|
gap: clampLayoutNumber(styleLayout.gap, 0, 400, 0),
|
|
1278
|
-
marginx: clampLayoutNumber(styleLayout.marginx, 0, 200, 0),
|
|
1279
|
-
marginy: clampLayoutNumber(styleLayout.marginy, 0, 200, 0),
|
|
1280
1469
|
stretch: typeof styleLayout.stretch === 'boolean' ? styleLayout.stretch : true,
|
|
1281
1470
|
},
|
|
1282
1471
|
};
|
|
@@ -1296,33 +1485,43 @@ async function layoutGraphDagre(graph, options) {
|
|
|
1296
1485
|
rankdir,
|
|
1297
1486
|
nodesep: clampLayoutNumber(styleLayout.nodesep, 0, 400, options.nodesep),
|
|
1298
1487
|
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
1488
|
},
|
|
1302
1489
|
};
|
|
1303
1490
|
};
|
|
1304
1491
|
const childPositionById = new Map();
|
|
1492
|
+
const branchChildForParent = (parentId, nodeId) => {
|
|
1493
|
+
let cursorId = nodeId;
|
|
1494
|
+
while (cursorId) {
|
|
1495
|
+
const node = nodeById.get(cursorId);
|
|
1496
|
+
if (!node) {
|
|
1497
|
+
return undefined;
|
|
1498
|
+
}
|
|
1499
|
+
if (node.parentId === parentId) {
|
|
1500
|
+
return node.id;
|
|
1501
|
+
}
|
|
1502
|
+
cursorId = node.parentId;
|
|
1503
|
+
}
|
|
1504
|
+
return undefined;
|
|
1505
|
+
};
|
|
1305
1506
|
const edgesBetweenChildren = (parentId, childSet) => {
|
|
1306
1507
|
const seen = new Set();
|
|
1307
1508
|
const links = [];
|
|
1308
1509
|
for (const edge of graph.edges) {
|
|
1309
1510
|
const sourceId = endpointOwner(edge.sourceId);
|
|
1310
1511
|
const targetId = endpointOwner(edge.targetId);
|
|
1311
|
-
if (!sourceId || !targetId
|
|
1512
|
+
if (!sourceId || !targetId)
|
|
1312
1513
|
continue;
|
|
1313
|
-
|
|
1514
|
+
const sourceBranchId = branchChildForParent(parentId, sourceId);
|
|
1515
|
+
const targetBranchId = branchChildForParent(parentId, targetId);
|
|
1516
|
+
if (!sourceBranchId || !targetBranchId || sourceBranchId === targetBranchId)
|
|
1314
1517
|
continue;
|
|
1315
|
-
|
|
1316
|
-
const targetNode = nodeById.get(targetId);
|
|
1317
|
-
if (!sourceNode || !targetNode)
|
|
1518
|
+
if (!childSet.has(sourceBranchId) || !childSet.has(targetBranchId))
|
|
1318
1519
|
continue;
|
|
1319
|
-
|
|
1320
|
-
continue;
|
|
1321
|
-
const key = `${sourceId}=>${targetId}`;
|
|
1520
|
+
const key = `${sourceBranchId}=>${targetBranchId}`;
|
|
1322
1521
|
if (seen.has(key))
|
|
1323
1522
|
continue;
|
|
1324
1523
|
seen.add(key);
|
|
1325
|
-
links.push({ source:
|
|
1524
|
+
links.push({ source: sourceBranchId, target: targetBranchId });
|
|
1326
1525
|
}
|
|
1327
1526
|
return links;
|
|
1328
1527
|
};
|
|
@@ -1335,8 +1534,8 @@ async function layoutGraphDagre(graph, options) {
|
|
|
1335
1534
|
return { width: 0, height: 0 };
|
|
1336
1535
|
}
|
|
1337
1536
|
const parent = parentId ? nodeById.get(parentId) : undefined;
|
|
1338
|
-
const topPadding = parent?.contentTopPadding ?? 0;
|
|
1339
1537
|
const localLayout = layoutOptionsForParent(parentId);
|
|
1538
|
+
const spacing = parent ? resolveBoxSpacing(parent.style, 0) : rootSpacing;
|
|
1340
1539
|
let totalWidth = 0;
|
|
1341
1540
|
let totalHeight = 0;
|
|
1342
1541
|
if (localLayout.type === 'stack') {
|
|
@@ -1344,13 +1543,13 @@ async function layoutGraphDagre(graph, options) {
|
|
|
1344
1543
|
const childNodes = childIds.map((childId) => nodeById.get(childId)).filter((node) => !!node);
|
|
1345
1544
|
const maxChildWidth = childNodes.reduce((max, child) => Math.max(max, child.width), 0);
|
|
1346
1545
|
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) -
|
|
1546
|
+
const parentContentWidth = Math.max(0, (parent?.width ?? 0) - spacing.marginLeft - spacing.marginRight - spacing.paddingLeft - spacing.paddingRight);
|
|
1547
|
+
const parentContentHeight = Math.max(0, (parent?.height ?? 0) - spacing.marginTop - spacing.marginBottom - spacing.paddingTop - spacing.paddingBottom);
|
|
1349
1548
|
const availableWidth = Math.max(maxChildWidth, parentContentWidth);
|
|
1350
1549
|
const availableHeight = Math.max(maxChildHeight, parentContentHeight);
|
|
1351
1550
|
const isSingleChild = childNodes.length === 1;
|
|
1352
|
-
let cursorX =
|
|
1353
|
-
let cursorY =
|
|
1551
|
+
let cursorX = spacing.marginLeft + spacing.paddingLeft;
|
|
1552
|
+
let cursorY = spacing.marginTop + spacing.paddingTop;
|
|
1354
1553
|
for (const child of childNodes) {
|
|
1355
1554
|
if (local.stretch && isSingleChild) {
|
|
1356
1555
|
child.width = Math.max(0, availableWidth);
|
|
@@ -1373,14 +1572,14 @@ async function layoutGraphDagre(graph, options) {
|
|
|
1373
1572
|
if (local.direction === 'vertical') {
|
|
1374
1573
|
const contentHeight = Math.max(0, childNodes.reduce((sum, child) => sum + child.height, 0) + (Math.max(0, childNodes.length - 1) * local.gap));
|
|
1375
1574
|
const contentWidth = Math.max(0, childNodes.reduce((max, child) => Math.max(max, child.width), 0));
|
|
1376
|
-
totalWidth =
|
|
1377
|
-
totalHeight =
|
|
1575
|
+
totalWidth = spacing.marginLeft + spacing.paddingLeft + contentWidth + spacing.paddingRight + spacing.marginRight;
|
|
1576
|
+
totalHeight = spacing.marginTop + spacing.paddingTop + contentHeight + spacing.paddingBottom + spacing.marginBottom;
|
|
1378
1577
|
}
|
|
1379
1578
|
else {
|
|
1380
1579
|
const contentWidth = Math.max(0, childNodes.reduce((sum, child) => sum + child.width, 0) + (Math.max(0, childNodes.length - 1) * local.gap));
|
|
1381
1580
|
const contentHeight = Math.max(0, childNodes.reduce((max, child) => Math.max(max, child.height), 0));
|
|
1382
|
-
totalWidth =
|
|
1383
|
-
totalHeight =
|
|
1581
|
+
totalWidth = spacing.marginLeft + spacing.paddingLeft + contentWidth + spacing.paddingRight + spacing.marginRight;
|
|
1582
|
+
totalHeight = spacing.marginTop + spacing.paddingTop + contentHeight + spacing.paddingBottom + spacing.marginBottom;
|
|
1384
1583
|
}
|
|
1385
1584
|
}
|
|
1386
1585
|
else {
|
|
@@ -1438,22 +1637,18 @@ async function layoutGraphDagre(graph, options) {
|
|
|
1438
1637
|
const left = laid.x - (width / 2);
|
|
1439
1638
|
const top = laid.y - (height / 2);
|
|
1440
1639
|
childPositionById.set(childId, {
|
|
1441
|
-
x:
|
|
1442
|
-
y:
|
|
1640
|
+
x: spacing.marginLeft + spacing.paddingLeft + (left - minX),
|
|
1641
|
+
y: spacing.marginTop + spacing.paddingTop + (top - minY),
|
|
1443
1642
|
});
|
|
1444
1643
|
}
|
|
1445
1644
|
const contentWidth = Math.max(0, maxX - minX);
|
|
1446
1645
|
const contentHeight = Math.max(0, maxY - minY);
|
|
1447
|
-
totalWidth =
|
|
1448
|
-
totalHeight =
|
|
1646
|
+
totalWidth = spacing.marginLeft + spacing.paddingLeft + contentWidth + spacing.paddingRight + spacing.marginRight;
|
|
1647
|
+
totalHeight = spacing.marginTop + spacing.paddingTop + contentHeight + spacing.paddingBottom + spacing.marginBottom;
|
|
1449
1648
|
}
|
|
1450
1649
|
if (parent) {
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
}
|
|
1454
|
-
if (!parent.hasExplicitHeight) {
|
|
1455
|
-
parent.height = Math.max(parent.height, totalHeight);
|
|
1456
|
-
}
|
|
1650
|
+
parent.width = Math.max(parent.width, totalWidth);
|
|
1651
|
+
parent.height = Math.max(parent.height, totalHeight);
|
|
1457
1652
|
}
|
|
1458
1653
|
return { width: totalWidth, height: totalHeight };
|
|
1459
1654
|
};
|
|
@@ -1471,9 +1666,9 @@ async function layoutGraphDagre(graph, options) {
|
|
|
1471
1666
|
if (localLayout.type === 'stack') {
|
|
1472
1667
|
const local = localLayout.stack;
|
|
1473
1668
|
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 -
|
|
1669
|
+
const spacing = resolveBoxSpacing(parent.style, 0);
|
|
1670
|
+
const contentWidth = Math.max(0, parent.width - spacing.marginLeft - spacing.marginRight - spacing.paddingLeft - spacing.paddingRight);
|
|
1671
|
+
const contentHeight = Math.max(0, parent.height - spacing.marginTop - spacing.marginBottom - spacing.paddingTop - spacing.paddingBottom);
|
|
1477
1672
|
const isSingleChild = childNodes.length === 1;
|
|
1478
1673
|
if (local.stretch) {
|
|
1479
1674
|
for (const child of childNodes) {
|
|
@@ -1490,8 +1685,8 @@ async function layoutGraphDagre(graph, options) {
|
|
|
1490
1685
|
}
|
|
1491
1686
|
}
|
|
1492
1687
|
}
|
|
1493
|
-
let cursorX =
|
|
1494
|
-
let cursorY =
|
|
1688
|
+
let cursorX = spacing.marginLeft + spacing.paddingLeft;
|
|
1689
|
+
let cursorY = spacing.marginTop + spacing.paddingTop;
|
|
1495
1690
|
for (const child of childNodes) {
|
|
1496
1691
|
childPositionById.set(child.id, { x: cursorX, y: cursorY });
|
|
1497
1692
|
if (local.direction === 'vertical') {
|
|
@@ -1692,9 +1887,6 @@ function compileDiagramGraph(index, stylesheet) {
|
|
|
1692
1887
|
children: [],
|
|
1693
1888
|
width: 160,
|
|
1694
1889
|
height: 70,
|
|
1695
|
-
hasExplicitWidth: false,
|
|
1696
|
-
hasExplicitHeight: false,
|
|
1697
|
-
contentTopPadding: 0,
|
|
1698
1890
|
});
|
|
1699
1891
|
}
|
|
1700
1892
|
const validParent = (child, parent) => {
|
|
@@ -1777,9 +1969,6 @@ function compileDiagramGraph(index, stylesheet) {
|
|
|
1777
1969
|
const estimated = estimateSize(node.kind, baseLabel, node.labels.length, node.style);
|
|
1778
1970
|
node.width = estimated.width;
|
|
1779
1971
|
node.height = estimated.height;
|
|
1780
|
-
node.hasExplicitWidth = estimated.hasExplicitWidth;
|
|
1781
|
-
node.hasExplicitHeight = estimated.hasExplicitHeight;
|
|
1782
|
-
node.contentTopPadding = resolveContainerTopPadding(node, baseLabel);
|
|
1783
1972
|
}
|
|
1784
1973
|
for (const edge of edges) {
|
|
1785
1974
|
edge.style = styleFor('edge', edge.id, edge.classes, edge.properties);
|
|
@@ -1807,65 +1996,42 @@ function compileDiagramGraph(index, stylesheet) {
|
|
|
1807
1996
|
function estimateSize(kind, label, labelCount, style) {
|
|
1808
1997
|
const styledWidth = toPositiveNumber(style.width);
|
|
1809
1998
|
const styledHeight = toPositiveNumber(style.height);
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1999
|
+
const attrs = extractStyleAttrs(style);
|
|
2000
|
+
const labelAttrs = asRecord(attrs.label);
|
|
2001
|
+
const labelDisplay = typeof labelAttrs?.display === 'string' ? labelAttrs.display.trim().toLowerCase() : '';
|
|
2002
|
+
const labelOpacity = toNonNegativeNumber(labelAttrs?.opacity);
|
|
2003
|
+
const labelVisible = labelDisplay !== 'none' && (labelOpacity === undefined || labelOpacity > 0);
|
|
2004
|
+
const effectiveLabel = labelVisible ? label : '';
|
|
2005
|
+
const effectiveLabelCount = labelVisible ? labelCount : 0;
|
|
1813
2006
|
if (kind === 'Port') {
|
|
1814
2007
|
return {
|
|
1815
|
-
width: styledWidth ??
|
|
1816
|
-
height: styledHeight ??
|
|
1817
|
-
hasExplicitWidth: styledWidth !== undefined,
|
|
1818
|
-
hasExplicitHeight: styledHeight !== undefined,
|
|
2008
|
+
width: Math.max(14, styledWidth ?? 0),
|
|
2009
|
+
height: Math.max(14, styledHeight ?? 0),
|
|
1819
2010
|
};
|
|
1820
2011
|
}
|
|
1821
|
-
const textWidth = Math.max(24,
|
|
1822
|
-
const baseWidth = Math.max(72, Math.min(280, textWidth + 26));
|
|
2012
|
+
const textWidth = effectiveLabel.length > 0 ? Math.max(24, effectiveLabel.length * 7) : 0;
|
|
2013
|
+
const baseWidth = effectiveLabel.length > 0 ? Math.max(72, Math.min(280, textWidth + 26)) : 0;
|
|
1823
2014
|
if (kind === 'Compartment') {
|
|
1824
|
-
const size = {
|
|
2015
|
+
const size = {
|
|
2016
|
+
width: baseWidth,
|
|
2017
|
+
height: effectiveLabelCount > 0 ? Math.max(44, 24 + effectiveLabelCount * 16) : 0,
|
|
2018
|
+
};
|
|
1825
2019
|
return {
|
|
1826
|
-
width: styledWidth ??
|
|
1827
|
-
height: styledHeight ??
|
|
1828
|
-
hasExplicitWidth: styledWidth !== undefined,
|
|
1829
|
-
hasExplicitHeight: styledHeight !== undefined,
|
|
2020
|
+
width: Math.max(size.width, styledWidth ?? 0),
|
|
2021
|
+
height: Math.max(size.height, styledHeight ?? 0),
|
|
1830
2022
|
};
|
|
1831
2023
|
}
|
|
1832
|
-
const size = {
|
|
2024
|
+
const size = {
|
|
2025
|
+
width: baseWidth,
|
|
2026
|
+
height: effectiveLabelCount > 0 ? Math.max(36, 20 + effectiveLabelCount * 16) : 0,
|
|
2027
|
+
};
|
|
1833
2028
|
return {
|
|
1834
|
-
width: styledWidth ??
|
|
1835
|
-
height: styledHeight ??
|
|
1836
|
-
hasExplicitWidth: styledWidth !== undefined,
|
|
1837
|
-
hasExplicitHeight: styledHeight !== undefined,
|
|
2029
|
+
width: Math.max(size.width, styledWidth ?? 0),
|
|
2030
|
+
height: Math.max(size.height, styledHeight ?? 0),
|
|
1838
2031
|
};
|
|
1839
2032
|
}
|
|
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
2033
|
function localName(value) {
|
|
1862
|
-
|
|
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;
|
|
2034
|
+
return displayLabelFromIri(value);
|
|
1869
2035
|
}
|
|
1870
2036
|
function indexTriples(rows) {
|
|
1871
2037
|
const bySubject = new Map();
|
|
@@ -1913,9 +2079,7 @@ function resolveDagreLayoutOptions(options) {
|
|
|
1913
2079
|
: 'LR';
|
|
1914
2080
|
const nodesep = clampLayoutNumber(layout.nodesep, 0, 400, 28);
|
|
1915
2081
|
const ranksep = clampLayoutNumber(layout.ranksep, 0, 500, 64);
|
|
1916
|
-
|
|
1917
|
-
const marginy = clampLayoutNumber(layout.marginy, 0, 200, 16);
|
|
1918
|
-
return { rankdir, nodesep, ranksep, marginx, marginy };
|
|
2082
|
+
return { rankdir, nodesep, ranksep };
|
|
1919
2083
|
}
|
|
1920
2084
|
function clampLayoutNumber(value, min, max, fallback) {
|
|
1921
2085
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
@@ -1923,6 +2087,121 @@ function clampLayoutNumber(value, min, max, fallback) {
|
|
|
1923
2087
|
}
|
|
1924
2088
|
return Math.max(min, Math.min(max, Math.round(value)));
|
|
1925
2089
|
}
|
|
2090
|
+
function resolveRootSpacing(options) {
|
|
2091
|
+
return readBoxSpacing(options, {
|
|
2092
|
+
marginTop: 16,
|
|
2093
|
+
marginBottom: 16,
|
|
2094
|
+
marginLeft: 16,
|
|
2095
|
+
marginRight: 16,
|
|
2096
|
+
paddingTop: 0,
|
|
2097
|
+
paddingBottom: 0,
|
|
2098
|
+
paddingLeft: 0,
|
|
2099
|
+
paddingRight: 0,
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
function resolveBoxSpacing(style, fallbackMargin) {
|
|
2103
|
+
return readBoxSpacing(style, {
|
|
2104
|
+
marginTop: fallbackMargin,
|
|
2105
|
+
marginBottom: fallbackMargin,
|
|
2106
|
+
marginLeft: fallbackMargin,
|
|
2107
|
+
marginRight: fallbackMargin,
|
|
2108
|
+
paddingTop: 0,
|
|
2109
|
+
paddingBottom: 0,
|
|
2110
|
+
paddingLeft: 0,
|
|
2111
|
+
paddingRight: 0,
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
function readBoxSpacing(style, defaults) {
|
|
2115
|
+
const source = style ?? {};
|
|
2116
|
+
return {
|
|
2117
|
+
...readCssBoxSpacing(source.margin, 'margin', defaults),
|
|
2118
|
+
...readCssBoxSpacing(source.padding, 'padding', defaults),
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
function readCssBoxSpacing(value, kind, defaults) {
|
|
2122
|
+
const sides = parseCssBoxShorthand(value);
|
|
2123
|
+
const isMargin = kind === 'margin';
|
|
2124
|
+
if (isMargin) {
|
|
2125
|
+
return {
|
|
2126
|
+
marginTop: sides?.top ?? defaults.marginTop,
|
|
2127
|
+
marginRight: sides?.right ?? defaults.marginRight,
|
|
2128
|
+
marginBottom: sides?.bottom ?? defaults.marginBottom,
|
|
2129
|
+
marginLeft: sides?.left ?? defaults.marginLeft,
|
|
2130
|
+
paddingTop: defaults.paddingTop,
|
|
2131
|
+
paddingRight: defaults.paddingRight,
|
|
2132
|
+
paddingBottom: defaults.paddingBottom,
|
|
2133
|
+
paddingLeft: defaults.paddingLeft,
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
2136
|
+
return {
|
|
2137
|
+
marginTop: defaults.marginTop,
|
|
2138
|
+
marginRight: defaults.marginRight,
|
|
2139
|
+
marginBottom: defaults.marginBottom,
|
|
2140
|
+
marginLeft: defaults.marginLeft,
|
|
2141
|
+
paddingTop: sides?.top ?? defaults.paddingTop,
|
|
2142
|
+
paddingRight: sides?.right ?? defaults.paddingRight,
|
|
2143
|
+
paddingBottom: sides?.bottom ?? defaults.paddingBottom,
|
|
2144
|
+
paddingLeft: sides?.left ?? defaults.paddingLeft,
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
function parseCssBoxShorthand(value) {
|
|
2148
|
+
const values = Array.isArray(value)
|
|
2149
|
+
? value
|
|
2150
|
+
: typeof value === 'string'
|
|
2151
|
+
? (value.includes(',') ? undefined : value.trim().split(/\s+/).filter((token) => token.length > 0))
|
|
2152
|
+
: typeof value === 'number'
|
|
2153
|
+
? [value]
|
|
2154
|
+
: undefined;
|
|
2155
|
+
if (!values || values.length === 0 || values.length > 4) {
|
|
2156
|
+
return undefined;
|
|
2157
|
+
}
|
|
2158
|
+
const parsed = values.map((entry) => {
|
|
2159
|
+
if (typeof entry === 'number' && Number.isFinite(entry)) {
|
|
2160
|
+
return entry;
|
|
2161
|
+
}
|
|
2162
|
+
if (typeof entry === 'string') {
|
|
2163
|
+
const next = Number.parseFloat(entry);
|
|
2164
|
+
if (Number.isFinite(next)) {
|
|
2165
|
+
return next;
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
return undefined;
|
|
2169
|
+
});
|
|
2170
|
+
if (parsed.some((entry) => entry === undefined)) {
|
|
2171
|
+
return undefined;
|
|
2172
|
+
}
|
|
2173
|
+
const clampCssBoxValue = (entry) => {
|
|
2174
|
+
if (entry === undefined)
|
|
2175
|
+
return undefined;
|
|
2176
|
+
return Math.max(0, Math.min(200, Math.round(entry)));
|
|
2177
|
+
};
|
|
2178
|
+
if (parsed.length === 1) {
|
|
2179
|
+
const single = clampCssBoxValue(parsed[0]);
|
|
2180
|
+
return single === undefined ? undefined : { top: single, right: single, bottom: single, left: single };
|
|
2181
|
+
}
|
|
2182
|
+
if (parsed.length === 2) {
|
|
2183
|
+
const vertical = clampCssBoxValue(parsed[0]);
|
|
2184
|
+
const horizontal = clampCssBoxValue(parsed[1]);
|
|
2185
|
+
if (vertical === undefined || horizontal === undefined)
|
|
2186
|
+
return undefined;
|
|
2187
|
+
return { top: vertical, right: horizontal, bottom: vertical, left: horizontal };
|
|
2188
|
+
}
|
|
2189
|
+
if (parsed.length === 3) {
|
|
2190
|
+
const top = clampCssBoxValue(parsed[0]);
|
|
2191
|
+
const horizontal = clampCssBoxValue(parsed[1]);
|
|
2192
|
+
const bottom = clampCssBoxValue(parsed[2]);
|
|
2193
|
+
if (top === undefined || horizontal === undefined || bottom === undefined)
|
|
2194
|
+
return undefined;
|
|
2195
|
+
return { top, right: horizontal, bottom, left: horizontal };
|
|
2196
|
+
}
|
|
2197
|
+
const top = clampCssBoxValue(parsed[0]);
|
|
2198
|
+
const right = clampCssBoxValue(parsed[1]);
|
|
2199
|
+
const bottom = clampCssBoxValue(parsed[2]);
|
|
2200
|
+
const left = clampCssBoxValue(parsed[3]);
|
|
2201
|
+
if (top === undefined || right === undefined || bottom === undefined || left === undefined)
|
|
2202
|
+
return undefined;
|
|
2203
|
+
return { top, right, bottom, left };
|
|
2204
|
+
}
|
|
1926
2205
|
function parseDiagramStylesheet(options) {
|
|
1927
2206
|
const stylesheet = options?.stylesheet;
|
|
1928
2207
|
if (!Array.isArray(stylesheet)) {
|
|
@@ -1949,7 +2228,7 @@ function parseDiagramStylesheet(options) {
|
|
|
1949
2228
|
return rules;
|
|
1950
2229
|
}
|
|
1951
2230
|
function parseStyleSelector(selector) {
|
|
1952
|
-
const match = /^\s*(node|compartment|port|edge)(?:\.([A-Za-z0-9_-]+))?(?:\s*\[(.+)\]\s*)?$/i.exec(selector);
|
|
2231
|
+
const match = /^\s*(diagram|node|compartment|port|edge)(?:\.([A-Za-z0-9_-]+))?(?:\s*\[(.+)\]\s*)?$/i.exec(selector);
|
|
1953
2232
|
if (!match)
|
|
1954
2233
|
return undefined;
|
|
1955
2234
|
return {
|
|
@@ -1962,7 +2241,13 @@ function parseStyleSelector(selector) {
|
|
|
1962
2241
|
// These pass through as top-level keys so callers can read style.layout, style.width, etc.
|
|
1963
2242
|
const OML_PASSTHROUGH_STYLE_KEYS = new Set([
|
|
1964
2243
|
'layout', 'shape', 'width', 'height',
|
|
2244
|
+
'margin', 'padding', 'router', 'connector',
|
|
2245
|
+
]);
|
|
2246
|
+
const LEGACY_BOX_SPACING_KEYS = new Set([
|
|
2247
|
+
'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
|
|
1965
2248
|
'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
|
|
2249
|
+
'margin-top', 'margin-bottom', 'margin-left', 'margin-right',
|
|
2250
|
+
'padding-top', 'padding-bottom', 'padding-left', 'padding-right',
|
|
1966
2251
|
]);
|
|
1967
2252
|
// Record-valued top-level style keys that map directly into the attrs sub-tree under the same name.
|
|
1968
2253
|
// 'label' and 'icon' cover nodes, compartments, ports, and edges.
|
|
@@ -1984,6 +2269,9 @@ function normalizeDiagramStyle(elementKind, raw) {
|
|
|
1984
2269
|
const key = rawKey.trim();
|
|
1985
2270
|
if (!key || value === undefined || value === null)
|
|
1986
2271
|
continue;
|
|
2272
|
+
if (LEGACY_BOX_SPACING_KEYS.has(key)) {
|
|
2273
|
+
continue;
|
|
2274
|
+
}
|
|
1987
2275
|
// OML node-level keys: pass through to the top level of the normalized style object
|
|
1988
2276
|
if (OML_PASSTHROUGH_STYLE_KEYS.has(key)) {
|
|
1989
2277
|
passthrough[key] = value;
|
|
@@ -2260,6 +2548,17 @@ function resolveEdgeLineAttrs(style) {
|
|
|
2260
2548
|
...(line ?? {}),
|
|
2261
2549
|
};
|
|
2262
2550
|
}
|
|
2551
|
+
function resolveEdgeRouter(style) {
|
|
2552
|
+
const router = asRecord(style.router);
|
|
2553
|
+
return router ?? { name: 'normal' };
|
|
2554
|
+
}
|
|
2555
|
+
function resolveEdgeConnector(style) {
|
|
2556
|
+
const connector = asRecord(style.connector);
|
|
2557
|
+
return connector ?? {
|
|
2558
|
+
name: 'jumpover',
|
|
2559
|
+
args: { size: 5 },
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2263
2562
|
function resolveEdgeLabelAttrs(style, placement, text) {
|
|
2264
2563
|
const attrs = extractStyleAttrs(style);
|
|
2265
2564
|
const base = asRecord(attrs.label);
|
|
@@ -2282,17 +2581,41 @@ function resolveEdgeLabelBodyAttrs(style, placement) {
|
|
|
2282
2581
|
fillOpacity: 0.9,
|
|
2283
2582
|
stroke: 'none',
|
|
2284
2583
|
strokeWidth: 0,
|
|
2584
|
+
pointerEvents: 'all',
|
|
2285
2585
|
...(base ?? {}),
|
|
2286
2586
|
...(specific ?? {}),
|
|
2287
2587
|
};
|
|
2288
2588
|
}
|
|
2289
|
-
function resolvePortAttrs(style, classes, text) {
|
|
2589
|
+
function resolvePortAttrs(style, classes, text, side = 'right', ownerStroke) {
|
|
2290
2590
|
const attrs = extractStyleAttrs(style);
|
|
2291
2591
|
const body = asRecord(attrs.body);
|
|
2292
2592
|
const icon = asRecord(attrs.icon);
|
|
2293
2593
|
const label = asRecord(attrs.label);
|
|
2294
2594
|
const imageUrl = extractImageHrefFromIcon(icon);
|
|
2295
|
-
|
|
2595
|
+
const labelPosition = side === 'left'
|
|
2596
|
+
? {
|
|
2597
|
+
textAnchor: 'end',
|
|
2598
|
+
x: -10,
|
|
2599
|
+
dy: '0.9em',
|
|
2600
|
+
}
|
|
2601
|
+
: side === 'top'
|
|
2602
|
+
? {
|
|
2603
|
+
textAnchor: 'middle',
|
|
2604
|
+
x: 0,
|
|
2605
|
+
dy: '-0.3em',
|
|
2606
|
+
}
|
|
2607
|
+
: side === 'bottom'
|
|
2608
|
+
? {
|
|
2609
|
+
textAnchor: 'middle',
|
|
2610
|
+
x: 0,
|
|
2611
|
+
dy: '1.4em',
|
|
2612
|
+
}
|
|
2613
|
+
: {
|
|
2614
|
+
textAnchor: 'start',
|
|
2615
|
+
x: 10,
|
|
2616
|
+
dy: '0.9em',
|
|
2617
|
+
};
|
|
2618
|
+
const result = {
|
|
2296
2619
|
body: {
|
|
2297
2620
|
width: 12,
|
|
2298
2621
|
height: 12,
|
|
@@ -2300,7 +2623,7 @@ function resolvePortAttrs(style, classes, text) {
|
|
|
2300
2623
|
y: -6,
|
|
2301
2624
|
class: ['oml-port-body', ...classes].join(' '),
|
|
2302
2625
|
magnet: false,
|
|
2303
|
-
stroke:
|
|
2626
|
+
stroke: ownerStroke ?? CSS_EDITOR_FOREGROUND,
|
|
2304
2627
|
strokeWidth: 1,
|
|
2305
2628
|
fill: CSS_EDITOR_BACKGROUND,
|
|
2306
2629
|
...(body ?? {}),
|
|
@@ -2315,17 +2638,180 @@ function resolvePortAttrs(style, classes, text) {
|
|
|
2315
2638
|
...(icon ?? {}),
|
|
2316
2639
|
...(imageUrl ? { href: imageUrl, xlinkHref: imageUrl, 'xlink:href': imageUrl } : {}),
|
|
2317
2640
|
},
|
|
2318
|
-
|
|
2641
|
+
};
|
|
2642
|
+
if (text) {
|
|
2643
|
+
result.label = {
|
|
2319
2644
|
text,
|
|
2320
2645
|
fill: CSS_EDITOR_FOREGROUND,
|
|
2321
2646
|
fontFamily: 'var(--vscode-editor-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)',
|
|
2322
2647
|
fontSize: 12,
|
|
2323
|
-
|
|
2324
|
-
x: 10,
|
|
2325
|
-
dy: '0.9em',
|
|
2648
|
+
...labelPosition,
|
|
2326
2649
|
...(label ?? {}),
|
|
2327
|
-
}
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
return result;
|
|
2653
|
+
}
|
|
2654
|
+
function resolveBorderPortLabelPosition(side) {
|
|
2655
|
+
return { name: side };
|
|
2656
|
+
}
|
|
2657
|
+
function clamp(value, min, max) {
|
|
2658
|
+
return Math.max(min, Math.min(max, value));
|
|
2659
|
+
}
|
|
2660
|
+
function computeDefaultPortPlacements(graph, nodeById, portsByOwner, boxes) {
|
|
2661
|
+
const placements = new Map();
|
|
2662
|
+
const ownerCenter = (nodeId) => {
|
|
2663
|
+
const box = boxes.get(nodeId);
|
|
2664
|
+
if (!box) {
|
|
2665
|
+
return undefined;
|
|
2666
|
+
}
|
|
2667
|
+
return { x: box.x + (box.width / 2), y: box.y + (box.height / 2) };
|
|
2328
2668
|
};
|
|
2669
|
+
const portIdsByOwner = new Map();
|
|
2670
|
+
for (const [ownerId, ports] of portsByOwner.entries()) {
|
|
2671
|
+
portIdsByOwner.set(ownerId, new Set(ports.map((port) => port.id)));
|
|
2672
|
+
}
|
|
2673
|
+
const peerPortIdsByPort = new Map();
|
|
2674
|
+
const peerCentersByPort = new Map();
|
|
2675
|
+
for (const edge of graph.edges) {
|
|
2676
|
+
const source = nodeById.get(edge.sourceId);
|
|
2677
|
+
const target = nodeById.get(edge.targetId);
|
|
2678
|
+
const sourceOwnerId = source?.kind === 'Port' ? source.parentId : source?.id;
|
|
2679
|
+
const targetOwnerId = target?.kind === 'Port' ? target.parentId : target?.id;
|
|
2680
|
+
if (!sourceOwnerId || !targetOwnerId || sourceOwnerId === targetOwnerId) {
|
|
2681
|
+
continue;
|
|
2682
|
+
}
|
|
2683
|
+
const sourcePeer = ownerCenter(targetOwnerId);
|
|
2684
|
+
const targetPeer = ownerCenter(sourceOwnerId);
|
|
2685
|
+
if (source?.kind === 'Port' && sourcePeer) {
|
|
2686
|
+
const peers = peerCentersByPort.get(source.id) ?? [];
|
|
2687
|
+
peers.push(sourcePeer);
|
|
2688
|
+
peerCentersByPort.set(source.id, peers);
|
|
2689
|
+
if (target?.kind === 'Port') {
|
|
2690
|
+
const peerPortIds = peerPortIdsByPort.get(source.id) ?? [];
|
|
2691
|
+
peerPortIds.push(target.id);
|
|
2692
|
+
peerPortIdsByPort.set(source.id, peerPortIds);
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
if (target?.kind === 'Port' && targetPeer) {
|
|
2696
|
+
const peers = peerCentersByPort.get(target.id) ?? [];
|
|
2697
|
+
peers.push(targetPeer);
|
|
2698
|
+
peerCentersByPort.set(target.id, peers);
|
|
2699
|
+
if (source?.kind === 'Port') {
|
|
2700
|
+
const peerPortIds = peerPortIdsByPort.get(target.id) ?? [];
|
|
2701
|
+
peerPortIds.push(source.id);
|
|
2702
|
+
peerPortIdsByPort.set(target.id, peerPortIds);
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
const anchorFor = (ownerId, side, ratio) => {
|
|
2707
|
+
const box = boxes.get(ownerId);
|
|
2708
|
+
if (!box) {
|
|
2709
|
+
return undefined;
|
|
2710
|
+
}
|
|
2711
|
+
const clampedRatio = clamp(ratio, 0.05, 0.95);
|
|
2712
|
+
if (side === 'left') {
|
|
2713
|
+
return { x: box.x, y: box.y + (box.height * clampedRatio) };
|
|
2714
|
+
}
|
|
2715
|
+
if (side === 'right') {
|
|
2716
|
+
return { x: box.x + box.width, y: box.y + (box.height * clampedRatio) };
|
|
2717
|
+
}
|
|
2718
|
+
if (side === 'top') {
|
|
2719
|
+
return { x: box.x + (box.width * clampedRatio), y: box.y };
|
|
2720
|
+
}
|
|
2721
|
+
return { x: box.x + (box.width * clampedRatio), y: box.y + box.height };
|
|
2722
|
+
};
|
|
2723
|
+
const currentAnchorForPort = (portId) => {
|
|
2724
|
+
const port = nodeById.get(portId);
|
|
2725
|
+
if (!port?.parentId) {
|
|
2726
|
+
return undefined;
|
|
2727
|
+
}
|
|
2728
|
+
const placement = placements.get(portId) ?? { side: 'right', ratio: 0.5 };
|
|
2729
|
+
return anchorFor(port.parentId, placement.side, placement.ratio);
|
|
2730
|
+
};
|
|
2731
|
+
const peerAnchorsForPort = (portId) => {
|
|
2732
|
+
const peerAnchors = [];
|
|
2733
|
+
for (const peerPortId of peerPortIdsByPort.get(portId) ?? []) {
|
|
2734
|
+
const anchor = currentAnchorForPort(peerPortId);
|
|
2735
|
+
if (anchor) {
|
|
2736
|
+
peerAnchors.push(anchor);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
for (const peerCenter of peerCentersByPort.get(portId) ?? []) {
|
|
2740
|
+
peerAnchors.push(peerCenter);
|
|
2741
|
+
}
|
|
2742
|
+
return peerAnchors;
|
|
2743
|
+
};
|
|
2744
|
+
const candidateCost = (ownerId, side, portId) => {
|
|
2745
|
+
const owner = ownerCenter(ownerId);
|
|
2746
|
+
const candidate = anchorFor(ownerId, side, 0.5);
|
|
2747
|
+
if (!owner || !candidate) {
|
|
2748
|
+
return Number.POSITIVE_INFINITY;
|
|
2749
|
+
}
|
|
2750
|
+
const peerAnchors = peerAnchorsForPort(portId);
|
|
2751
|
+
if (peerAnchors.length === 0) {
|
|
2752
|
+
return side === 'right' ? 0 : 1;
|
|
2753
|
+
}
|
|
2754
|
+
let cost = 0;
|
|
2755
|
+
for (const peer of peerAnchors) {
|
|
2756
|
+
cost += Math.abs(candidate.x - peer.x) + Math.abs(candidate.y - peer.y);
|
|
2757
|
+
}
|
|
2758
|
+
return cost;
|
|
2759
|
+
};
|
|
2760
|
+
const sideOrder = ['right', 'left', 'top', 'bottom'];
|
|
2761
|
+
const primaryCoordinate = (side, portId) => {
|
|
2762
|
+
const peerAnchors = peerAnchorsForPort(portId);
|
|
2763
|
+
if (peerAnchors.length === 0) {
|
|
2764
|
+
return 0;
|
|
2765
|
+
}
|
|
2766
|
+
const values = peerAnchors.map((peer) => ((side === 'left' || side === 'right') ? peer.y : peer.x));
|
|
2767
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
2768
|
+
};
|
|
2769
|
+
for (const [ownerId, ports] of portsByOwner.entries()) {
|
|
2770
|
+
const count = ports.length;
|
|
2771
|
+
for (let index = 0; index < count; index += 1) {
|
|
2772
|
+
placements.set(ports[index].id, {
|
|
2773
|
+
side: 'right',
|
|
2774
|
+
ratio: count <= 0 ? 0.5 : ((index + 1) / (count + 1)),
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2777
|
+
const ownerPortIds = portIdsByOwner.get(ownerId) ?? new Set();
|
|
2778
|
+
for (let pass = 0; pass < 2; pass += 1) {
|
|
2779
|
+
for (const port of ports) {
|
|
2780
|
+
let bestSide = 'right';
|
|
2781
|
+
let bestCost = Number.POSITIVE_INFINITY;
|
|
2782
|
+
for (const side of sideOrder) {
|
|
2783
|
+
const cost = candidateCost(ownerId, side, port.id);
|
|
2784
|
+
if (cost < bestCost || (cost === bestCost && sideOrder.indexOf(side) < sideOrder.indexOf(bestSide))) {
|
|
2785
|
+
bestCost = cost;
|
|
2786
|
+
bestSide = side;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
const existing = placements.get(port.id) ?? { ratio: 0.5, side: bestSide };
|
|
2790
|
+
placements.set(port.id, { side: bestSide, ratio: existing.ratio });
|
|
2791
|
+
}
|
|
2792
|
+
const sideGroups = new Map([
|
|
2793
|
+
['left', []],
|
|
2794
|
+
['right', []],
|
|
2795
|
+
['top', []],
|
|
2796
|
+
['bottom', []],
|
|
2797
|
+
]);
|
|
2798
|
+
for (const port of ports) {
|
|
2799
|
+
sideGroups.get(placements.get(port.id)?.side ?? 'right')?.push(port);
|
|
2800
|
+
}
|
|
2801
|
+
for (const [side, sidePorts] of sideGroups.entries()) {
|
|
2802
|
+
sidePorts.sort((left, right) => (primaryCoordinate(side, left.id) - primaryCoordinate(side, right.id)
|
|
2803
|
+
|| left.id.localeCompare(right.id)));
|
|
2804
|
+
for (let index = 0; index < sidePorts.length; index += 1) {
|
|
2805
|
+
placements.set(sidePorts[index].id, {
|
|
2806
|
+
side,
|
|
2807
|
+
ratio: (index + 1) / (sidePorts.length + 1),
|
|
2808
|
+
});
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
void ownerPortIds;
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
return placements;
|
|
2329
2815
|
}
|
|
2330
2816
|
function toPositiveNumber(value) {
|
|
2331
2817
|
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|