@oml/markdown 0.10.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/renderers/chart-renderer.js +72 -4
- package/out/renderers/chart-renderer.js.map +1 -1
- package/out/renderers/diagram-renderer.js +736 -244
- package/out/renderers/diagram-renderer.js.map +1 -1
- package/out/renderers/table-renderer.d.ts +8 -0
- package/out/renderers/table-renderer.js +22 -1
- package/out/renderers/table-renderer.js.map +1 -1
- package/out/static/browser-runtime.bundle.js +805 -241
- package/out/static/browser-runtime.bundle.js.map +3 -3
- package/package.json +2 -2
- package/src/renderers/chart-renderer.ts +93 -2
- package/src/renderers/diagram-renderer.ts +797 -252
- package/src/renderers/table-renderer.ts +39 -2
|
@@ -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,57 +1996,40 @@ 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
2034
|
return displayLabelFromIri(value);
|
|
1863
2035
|
}
|
|
@@ -1907,9 +2079,7 @@ function resolveDagreLayoutOptions(options) {
|
|
|
1907
2079
|
: 'LR';
|
|
1908
2080
|
const nodesep = clampLayoutNumber(layout.nodesep, 0, 400, 28);
|
|
1909
2081
|
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 };
|
|
2082
|
+
return { rankdir, nodesep, ranksep };
|
|
1913
2083
|
}
|
|
1914
2084
|
function clampLayoutNumber(value, min, max, fallback) {
|
|
1915
2085
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
@@ -1917,6 +2087,121 @@ function clampLayoutNumber(value, min, max, fallback) {
|
|
|
1917
2087
|
}
|
|
1918
2088
|
return Math.max(min, Math.min(max, Math.round(value)));
|
|
1919
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
|
+
}
|
|
1920
2205
|
function parseDiagramStylesheet(options) {
|
|
1921
2206
|
const stylesheet = options?.stylesheet;
|
|
1922
2207
|
if (!Array.isArray(stylesheet)) {
|
|
@@ -1943,7 +2228,7 @@ function parseDiagramStylesheet(options) {
|
|
|
1943
2228
|
return rules;
|
|
1944
2229
|
}
|
|
1945
2230
|
function parseStyleSelector(selector) {
|
|
1946
|
-
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);
|
|
1947
2232
|
if (!match)
|
|
1948
2233
|
return undefined;
|
|
1949
2234
|
return {
|
|
@@ -1956,7 +2241,13 @@ function parseStyleSelector(selector) {
|
|
|
1956
2241
|
// These pass through as top-level keys so callers can read style.layout, style.width, etc.
|
|
1957
2242
|
const OML_PASSTHROUGH_STYLE_KEYS = new Set([
|
|
1958
2243
|
'layout', 'shape', 'width', 'height',
|
|
2244
|
+
'margin', 'padding', 'router', 'connector',
|
|
2245
|
+
]);
|
|
2246
|
+
const LEGACY_BOX_SPACING_KEYS = new Set([
|
|
2247
|
+
'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
|
|
1959
2248
|
'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
|
|
2249
|
+
'margin-top', 'margin-bottom', 'margin-left', 'margin-right',
|
|
2250
|
+
'padding-top', 'padding-bottom', 'padding-left', 'padding-right',
|
|
1960
2251
|
]);
|
|
1961
2252
|
// Record-valued top-level style keys that map directly into the attrs sub-tree under the same name.
|
|
1962
2253
|
// 'label' and 'icon' cover nodes, compartments, ports, and edges.
|
|
@@ -1978,6 +2269,9 @@ function normalizeDiagramStyle(elementKind, raw) {
|
|
|
1978
2269
|
const key = rawKey.trim();
|
|
1979
2270
|
if (!key || value === undefined || value === null)
|
|
1980
2271
|
continue;
|
|
2272
|
+
if (LEGACY_BOX_SPACING_KEYS.has(key)) {
|
|
2273
|
+
continue;
|
|
2274
|
+
}
|
|
1981
2275
|
// OML node-level keys: pass through to the top level of the normalized style object
|
|
1982
2276
|
if (OML_PASSTHROUGH_STYLE_KEYS.has(key)) {
|
|
1983
2277
|
passthrough[key] = value;
|
|
@@ -2254,6 +2548,17 @@ function resolveEdgeLineAttrs(style) {
|
|
|
2254
2548
|
...(line ?? {}),
|
|
2255
2549
|
};
|
|
2256
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
|
+
}
|
|
2257
2562
|
function resolveEdgeLabelAttrs(style, placement, text) {
|
|
2258
2563
|
const attrs = extractStyleAttrs(style);
|
|
2259
2564
|
const base = asRecord(attrs.label);
|
|
@@ -2276,17 +2581,41 @@ function resolveEdgeLabelBodyAttrs(style, placement) {
|
|
|
2276
2581
|
fillOpacity: 0.9,
|
|
2277
2582
|
stroke: 'none',
|
|
2278
2583
|
strokeWidth: 0,
|
|
2584
|
+
pointerEvents: 'all',
|
|
2279
2585
|
...(base ?? {}),
|
|
2280
2586
|
...(specific ?? {}),
|
|
2281
2587
|
};
|
|
2282
2588
|
}
|
|
2283
|
-
function resolvePortAttrs(style, classes, text) {
|
|
2589
|
+
function resolvePortAttrs(style, classes, text, side = 'right', ownerStroke) {
|
|
2284
2590
|
const attrs = extractStyleAttrs(style);
|
|
2285
2591
|
const body = asRecord(attrs.body);
|
|
2286
2592
|
const icon = asRecord(attrs.icon);
|
|
2287
2593
|
const label = asRecord(attrs.label);
|
|
2288
2594
|
const imageUrl = extractImageHrefFromIcon(icon);
|
|
2289
|
-
|
|
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 = {
|
|
2290
2619
|
body: {
|
|
2291
2620
|
width: 12,
|
|
2292
2621
|
height: 12,
|
|
@@ -2294,7 +2623,7 @@ function resolvePortAttrs(style, classes, text) {
|
|
|
2294
2623
|
y: -6,
|
|
2295
2624
|
class: ['oml-port-body', ...classes].join(' '),
|
|
2296
2625
|
magnet: false,
|
|
2297
|
-
stroke:
|
|
2626
|
+
stroke: ownerStroke ?? CSS_EDITOR_FOREGROUND,
|
|
2298
2627
|
strokeWidth: 1,
|
|
2299
2628
|
fill: CSS_EDITOR_BACKGROUND,
|
|
2300
2629
|
...(body ?? {}),
|
|
@@ -2309,17 +2638,180 @@ function resolvePortAttrs(style, classes, text) {
|
|
|
2309
2638
|
...(icon ?? {}),
|
|
2310
2639
|
...(imageUrl ? { href: imageUrl, xlinkHref: imageUrl, 'xlink:href': imageUrl } : {}),
|
|
2311
2640
|
},
|
|
2312
|
-
|
|
2641
|
+
};
|
|
2642
|
+
if (text) {
|
|
2643
|
+
result.label = {
|
|
2313
2644
|
text,
|
|
2314
2645
|
fill: CSS_EDITOR_FOREGROUND,
|
|
2315
2646
|
fontFamily: 'var(--vscode-editor-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)',
|
|
2316
2647
|
fontSize: 12,
|
|
2317
|
-
|
|
2318
|
-
x: 10,
|
|
2319
|
-
dy: '0.9em',
|
|
2648
|
+
...labelPosition,
|
|
2320
2649
|
...(label ?? {}),
|
|
2321
|
-
}
|
|
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) };
|
|
2322
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;
|
|
2323
2815
|
}
|
|
2324
2816
|
function toPositiveNumber(value) {
|
|
2325
2817
|
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|