@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,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
2
|
|
|
3
3
|
import 'reflect-metadata';
|
|
4
|
-
import { CanvasMarkdownBlockRenderer } from './renderer.js';
|
|
4
|
+
import { CanvasMarkdownBlockRenderer, displayLabelFromIri } from './renderer.js';
|
|
5
5
|
import type { MdBlockExecutionResult } from './types.js';
|
|
6
6
|
|
|
7
7
|
type NodeKind = 'Node' | 'Compartment' | 'Port';
|
|
@@ -16,9 +16,6 @@ type NodeSpec = {
|
|
|
16
16
|
children: string[];
|
|
17
17
|
width: number;
|
|
18
18
|
height: number;
|
|
19
|
-
hasExplicitWidth: boolean;
|
|
20
|
-
hasExplicitHeight: boolean;
|
|
21
|
-
contentTopPadding: number;
|
|
22
19
|
};
|
|
23
20
|
|
|
24
21
|
type EdgeLabel = {
|
|
@@ -37,7 +34,7 @@ type EdgeSpec = {
|
|
|
37
34
|
};
|
|
38
35
|
|
|
39
36
|
type DiagramStyleRule = {
|
|
40
|
-
elementKind: 'node' | 'compartment' | 'port' | 'edge';
|
|
37
|
+
elementKind: 'diagram' | 'node' | 'compartment' | 'port' | 'edge';
|
|
41
38
|
className?: string;
|
|
42
39
|
condition?: string;
|
|
43
40
|
style: Record<string, unknown>;
|
|
@@ -67,18 +64,25 @@ type NativeLayoutResult = {
|
|
|
67
64
|
contentHeight: number;
|
|
68
65
|
};
|
|
69
66
|
|
|
67
|
+
type BoxSpacing = {
|
|
68
|
+
marginTop: number;
|
|
69
|
+
marginBottom: number;
|
|
70
|
+
marginLeft: number;
|
|
71
|
+
marginRight: number;
|
|
72
|
+
paddingTop: number;
|
|
73
|
+
paddingBottom: number;
|
|
74
|
+
paddingLeft: number;
|
|
75
|
+
paddingRight: number;
|
|
76
|
+
};
|
|
77
|
+
|
|
70
78
|
type DagreLayoutOptions = {
|
|
71
79
|
rankdir: 'LR' | 'RL' | 'TB' | 'BT';
|
|
72
80
|
nodesep: number;
|
|
73
81
|
ranksep: number;
|
|
74
|
-
marginx: number;
|
|
75
|
-
marginy: number;
|
|
76
82
|
};
|
|
77
83
|
type StackLayoutOptions = {
|
|
78
84
|
direction: 'vertical' | 'horizontal';
|
|
79
85
|
gap: number;
|
|
80
|
-
marginx: number;
|
|
81
|
-
marginy: number;
|
|
82
86
|
stretch: boolean;
|
|
83
87
|
};
|
|
84
88
|
type ParentLayoutOptions =
|
|
@@ -89,6 +93,11 @@ type RenderNodeShape = {
|
|
|
89
93
|
bodyTag: 'rect' | 'ellipse';
|
|
90
94
|
bodyDefaults: Record<string, unknown>;
|
|
91
95
|
};
|
|
96
|
+
type PortSide = 'left' | 'right' | 'top' | 'bottom';
|
|
97
|
+
type PortPlacement = {
|
|
98
|
+
side: PortSide;
|
|
99
|
+
ratio: number;
|
|
100
|
+
};
|
|
92
101
|
|
|
93
102
|
const D = 'http://opencaesar.io/oml/diagram#';
|
|
94
103
|
const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
@@ -150,13 +159,16 @@ export class DiagramMarkdownBlockRenderer extends CanvasMarkdownBlockRenderer {
|
|
|
150
159
|
const tripleIndex = indexTriples(rows);
|
|
151
160
|
const stylesheet = parseDiagramStylesheet(result.options);
|
|
152
161
|
const compiled = compileDiagramGraph(tripleIndex, stylesheet);
|
|
153
|
-
const
|
|
162
|
+
const diagramStyle = resolveElementStyle('diagram', 'diagram', [], {}, stylesheet);
|
|
163
|
+
const layoutOptions = resolveDagreLayoutOptions(diagramStyle);
|
|
164
|
+
const rootSpacing = resolveRootSpacing(diagramStyle);
|
|
154
165
|
if (compiled.nodes.length === 0) {
|
|
155
166
|
container.appendChild(this.createMessageContainer('No diagram nodes were inferred from the diagram namespace triples.'));
|
|
156
167
|
return container;
|
|
157
168
|
}
|
|
158
169
|
|
|
159
170
|
void renderWithX6(canvas, baseId, compiled, layoutOptions, {
|
|
171
|
+
rootSpacing,
|
|
160
172
|
downloadSvg: (content: string) => this.requestTextFileDownload(content, 'diagram', 'svg'),
|
|
161
173
|
}).catch((error) => {
|
|
162
174
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -172,7 +184,7 @@ async function renderWithX6(
|
|
|
172
184
|
baseId: string,
|
|
173
185
|
graph: { nodes: NodeSpec[]; edges: EdgeSpec[]; roots: string[] },
|
|
174
186
|
layoutOptions: DagreLayoutOptions,
|
|
175
|
-
actions: { downloadSvg: (content: string) => void }
|
|
187
|
+
actions: { downloadSvg: (content: string) => void; rootSpacing: BoxSpacing }
|
|
176
188
|
): Promise<void> {
|
|
177
189
|
await waitForCanvasReady(canvas);
|
|
178
190
|
const liveCanvas = getLiveCanvas(baseId);
|
|
@@ -181,7 +193,7 @@ async function renderWithX6(
|
|
|
181
193
|
}
|
|
182
194
|
|
|
183
195
|
const GraphCtor = await loadX6GraphCtor();
|
|
184
|
-
const layout = await layoutGraphDagre(graph, layoutOptions);
|
|
196
|
+
const layout = await layoutGraphDagre(graph, layoutOptions, actions.rootSpacing);
|
|
185
197
|
const minHeight = asFiniteNumber(
|
|
186
198
|
parseCssPixels(liveCanvas.style.getPropertyValue('--oml-diagram-min-height')),
|
|
187
199
|
numericCanvasMinHeight(undefined)
|
|
@@ -191,13 +203,15 @@ async function renderWithX6(
|
|
|
191
203
|
let activePortDrag: {
|
|
192
204
|
nodeId: string;
|
|
193
205
|
portId: string;
|
|
194
|
-
|
|
195
|
-
|
|
206
|
+
pointerId: number;
|
|
207
|
+
side: PortSide;
|
|
208
|
+
ratio: number;
|
|
209
|
+
container: Element;
|
|
196
210
|
} | undefined;
|
|
197
211
|
let isResizingCanvas = false;
|
|
198
212
|
const isPortPointerTarget = (event: PointerEvent): boolean => {
|
|
199
213
|
const target = event?.target as Element | null;
|
|
200
|
-
if (target?.closest('.x6-port, .x6-port-body, .oml-port-body')) {
|
|
214
|
+
if (target?.closest('.x6-port, .x6-port-body, .oml-port-body, [port]')) {
|
|
201
215
|
return true;
|
|
202
216
|
}
|
|
203
217
|
const path = typeof event?.composedPath === 'function' ? event.composedPath() : [];
|
|
@@ -205,23 +219,18 @@ async function renderWithX6(
|
|
|
205
219
|
entry instanceof Element
|
|
206
220
|
&& (entry.classList.contains('x6-port')
|
|
207
221
|
|| entry.classList.contains('x6-port-body')
|
|
208
|
-
|| entry.classList.contains('oml-port-body')
|
|
222
|
+
|| entry.classList.contains('oml-port-body')
|
|
223
|
+
|| entry.hasAttribute('port'))
|
|
209
224
|
));
|
|
210
225
|
};
|
|
211
|
-
const findPortIdFromTarget = (target: EventTarget | null): string | undefined => {
|
|
212
|
-
if (!(target instanceof Element)) return undefined;
|
|
213
|
-
const portHost = target.closest('.x6-port');
|
|
214
|
-
if (!portHost) return undefined;
|
|
215
|
-
return portHost.getAttribute('port')
|
|
216
|
-
?? portHost.getAttribute('data-port-id')
|
|
217
|
-
?? undefined;
|
|
218
|
-
};
|
|
219
226
|
|
|
220
227
|
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
221
228
|
const isCompartmentNode = (node: any): boolean => {
|
|
222
229
|
const nodeId = String(node?.id ?? '');
|
|
223
230
|
return nodeById.get(nodeId)?.kind === 'Compartment';
|
|
224
231
|
};
|
|
232
|
+
const boundPortContainers = new WeakSet<Element>();
|
|
233
|
+
let nodeTransform: any;
|
|
225
234
|
|
|
226
235
|
const graphView: any = new GraphCtor({
|
|
227
236
|
container: liveCanvas,
|
|
@@ -236,7 +245,10 @@ async function renderWithX6(
|
|
|
236
245
|
},
|
|
237
246
|
connecting: {
|
|
238
247
|
router: 'normal',
|
|
239
|
-
connector:
|
|
248
|
+
connector: {
|
|
249
|
+
name: 'jumpover',
|
|
250
|
+
args: { size: 5 },
|
|
251
|
+
},
|
|
240
252
|
allowBlank: false,
|
|
241
253
|
allowNode: false,
|
|
242
254
|
allowPort: false,
|
|
@@ -252,13 +264,64 @@ async function renderWithX6(
|
|
|
252
264
|
edgeMovable: false,
|
|
253
265
|
vertexMovable: false,
|
|
254
266
|
arrowheadMovable: false,
|
|
255
|
-
labelMovable:
|
|
267
|
+
labelMovable: true,
|
|
256
268
|
};
|
|
257
269
|
},
|
|
258
270
|
background: {
|
|
259
271
|
color: CSS_CANVAS_BACKGROUND,
|
|
260
272
|
},
|
|
273
|
+
onPortRendered: ({ port, node, container }: { port: { id: string }; node: { id: string }; container: HTMLElement }) => {
|
|
274
|
+
if (boundPortContainers.has(container)) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
boundPortContainers.add(container);
|
|
278
|
+
container.setAttribute('data-port-id', String(port.id));
|
|
279
|
+
container.style.cursor = 'grab';
|
|
280
|
+
container.style.touchAction = 'none';
|
|
281
|
+
container.addEventListener('pointerdown', (event: PointerEvent) => {
|
|
282
|
+
if (typeof graphView.cleanSelection === 'function') {
|
|
283
|
+
graphView.cleanSelection();
|
|
284
|
+
}
|
|
285
|
+
nodeTransform.clearWidgets();
|
|
286
|
+
const owner = graphView.getCellById(String(node.id));
|
|
287
|
+
if (!owner) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
startPortDrag(event.clientX, event.clientY, owner, String(port.id), event.pointerId, container);
|
|
291
|
+
event.preventDefault();
|
|
292
|
+
event.stopPropagation();
|
|
293
|
+
if (typeof event.stopImmediatePropagation === 'function') {
|
|
294
|
+
event.stopImmediatePropagation();
|
|
295
|
+
}
|
|
296
|
+
if (typeof container.setPointerCapture === 'function') {
|
|
297
|
+
try {
|
|
298
|
+
container.setPointerCapture(event.pointerId);
|
|
299
|
+
} catch {
|
|
300
|
+
// Ignore capture failures; window listeners still handle the drag.
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
},
|
|
261
305
|
});
|
|
306
|
+
const x6Mod = await import('@antv/x6');
|
|
307
|
+
const TransformCtor = (x6Mod as any).Transform;
|
|
308
|
+
if (typeof TransformCtor !== 'function') {
|
|
309
|
+
throw new Error('X6 Transform plugin is unavailable in @antv/x6');
|
|
310
|
+
}
|
|
311
|
+
nodeTransform = new TransformCtor({
|
|
312
|
+
resizing: {
|
|
313
|
+
enabled: (node: any) => node?.getData?.()?.kind === 'Node',
|
|
314
|
+
minWidth: 48,
|
|
315
|
+
minHeight: 32,
|
|
316
|
+
orthogonal: true,
|
|
317
|
+
restrict: false,
|
|
318
|
+
autoScroll: false,
|
|
319
|
+
preserveAspectRatio: false,
|
|
320
|
+
allowReverse: false,
|
|
321
|
+
},
|
|
322
|
+
rotating: false,
|
|
323
|
+
});
|
|
324
|
+
graphView.use(nodeTransform);
|
|
262
325
|
const toPlainRect = (value: any): Record<string, number> | undefined => {
|
|
263
326
|
if (!value) return undefined;
|
|
264
327
|
const x = Number(value.x);
|
|
@@ -291,12 +354,7 @@ async function renderWithX6(
|
|
|
291
354
|
for (const list of portsByOwner.values()) {
|
|
292
355
|
list.sort((a, b) => a.id.localeCompare(b.id));
|
|
293
356
|
}
|
|
294
|
-
const
|
|
295
|
-
for (const [ownerId, ports] of portsByOwner.entries()) {
|
|
296
|
-
for (const port of ports) {
|
|
297
|
-
ownerByPortId.set(port.id, ownerId);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
357
|
+
const portPlacementById = computeDefaultPortPlacements(graph, nodeById, portsByOwner, layout.boxes);
|
|
300
358
|
const ordered = [...graph.nodes]
|
|
301
359
|
.filter((node) => node.kind !== 'Port')
|
|
302
360
|
.sort((a, b) => nodeDepth(a.id, nodeById) - nodeDepth(b.id, nodeById));
|
|
@@ -307,20 +365,41 @@ async function renderWithX6(
|
|
|
307
365
|
const labelText = node.labels.length > 0 ? node.labels.join('\n') : localName(node.id);
|
|
308
366
|
const resolvedStyle = node.style;
|
|
309
367
|
const resolvedShape = resolveRenderNodeShape(resolvedStyle);
|
|
368
|
+
const { bodyAttrs, labelAttrs, iconSvgAttrs, iconPathAttrs, imageAttrs } = resolveNodeAttrs(resolvedStyle);
|
|
310
369
|
const x = box.x;
|
|
311
370
|
const y = box.y;
|
|
312
371
|
const ownerPorts = portsByOwner.get(node.id) ?? [];
|
|
313
|
-
const portItems = ownerPorts.map((port
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
372
|
+
const portItems = ownerPorts.map((port) => {
|
|
373
|
+
const placement = portPlacementById.get(port.id) ?? { side: 'right' as PortSide, ratio: 0.5 };
|
|
374
|
+
const ratio = clamp(placement.ratio, 0.05, 0.95);
|
|
375
|
+
const position = placement.side === 'left' || placement.side === 'right'
|
|
376
|
+
? {
|
|
377
|
+
x: placement.side === 'left' ? 0 : box.width,
|
|
378
|
+
y: ratio * box.height,
|
|
379
|
+
}
|
|
380
|
+
: {
|
|
381
|
+
x: ratio * box.width,
|
|
382
|
+
y: placement.side === 'top' ? 0 : box.height,
|
|
383
|
+
};
|
|
384
|
+
const portText = port.labels[0];
|
|
385
|
+
return {
|
|
386
|
+
id: port.id,
|
|
387
|
+
group: 'boundary',
|
|
388
|
+
args: {
|
|
389
|
+
x: position.x,
|
|
390
|
+
y: position.y,
|
|
391
|
+
side: placement.side,
|
|
392
|
+
ratio,
|
|
393
|
+
},
|
|
394
|
+
attrs: resolvePortAttrs(
|
|
395
|
+
port.style,
|
|
396
|
+
port.classes,
|
|
397
|
+
portText,
|
|
398
|
+
placement.side,
|
|
399
|
+
typeof bodyAttrs.stroke === 'string' ? bodyAttrs.stroke : undefined
|
|
400
|
+
),
|
|
401
|
+
};
|
|
402
|
+
});
|
|
324
403
|
const iconPathSelectors = iconPathAttrs.map((_, index) => `iconPath${index}`);
|
|
325
404
|
graphView.addNode({
|
|
326
405
|
id: node.id,
|
|
@@ -424,7 +503,7 @@ async function renderWithX6(
|
|
|
424
503
|
},
|
|
425
504
|
items: portItems,
|
|
426
505
|
} : undefined,
|
|
427
|
-
zIndex:
|
|
506
|
+
zIndex: 50,
|
|
428
507
|
data: {
|
|
429
508
|
kind: node.kind,
|
|
430
509
|
ownerId: node.parentId,
|
|
@@ -561,10 +640,30 @@ async function renderWithX6(
|
|
|
561
640
|
let maxBottom = Number.NEGATIVE_INFINITY;
|
|
562
641
|
const childCells: any[] = [];
|
|
563
642
|
const childDebug: Array<Record<string, unknown>> = [];
|
|
643
|
+
const childGeometryBounds = (child: any): { x: number; y: number; width: number; height: number } | undefined => {
|
|
644
|
+
if (!child || typeof child.size !== 'function') {
|
|
645
|
+
return undefined;
|
|
646
|
+
}
|
|
647
|
+
const size = child.size();
|
|
648
|
+
const width = Number(size?.width);
|
|
649
|
+
const height = Number(size?.height);
|
|
650
|
+
let position: any;
|
|
651
|
+
if (typeof child.getPosition === 'function') {
|
|
652
|
+
position = child.getPosition();
|
|
653
|
+
} else {
|
|
654
|
+
position = child.position;
|
|
655
|
+
}
|
|
656
|
+
const x = Number(position?.x);
|
|
657
|
+
const y = Number(position?.y);
|
|
658
|
+
if (![x, y, width, height].every(Number.isFinite)) {
|
|
659
|
+
return undefined;
|
|
660
|
+
}
|
|
661
|
+
return { x, y, width, height };
|
|
662
|
+
};
|
|
564
663
|
for (const childId of childIds) {
|
|
565
664
|
const child = graphView.getCellById(childId);
|
|
566
|
-
|
|
567
|
-
|
|
665
|
+
const absBBox = childGeometryBounds(child);
|
|
666
|
+
if (!absBBox) continue;
|
|
568
667
|
const relLeft = absBBox.x - containerBBox.x;
|
|
569
668
|
const relTop = absBBox.y - containerBBox.y;
|
|
570
669
|
minLeft = Math.min(minLeft, relLeft);
|
|
@@ -596,10 +695,11 @@ async function renderWithX6(
|
|
|
596
695
|
// container origin (without moving its embedded children, which keep their
|
|
597
696
|
// absolute positions) and expand the size to compensate so the opposite edge
|
|
598
697
|
// is preserved.
|
|
599
|
-
|
|
600
|
-
const
|
|
601
|
-
const
|
|
602
|
-
const
|
|
698
|
+
const spacing = resolveBoxSpacing(containerSpec.style, 0);
|
|
699
|
+
const minInsetX = spacing.marginLeft + spacing.paddingLeft;
|
|
700
|
+
const minInsetY = spacing.marginTop + spacing.paddingTop;
|
|
701
|
+
const shiftX = minLeft < minInsetX ? minLeft - minInsetX : 0;
|
|
702
|
+
const shiftY = minTop < minInsetY ? minTop - minInsetY : 0;
|
|
603
703
|
// Dimensions needed relative to the (possibly shifted) new origin.
|
|
604
704
|
const baseWidth = containerSize.width - shiftX;
|
|
605
705
|
const baseHeight = containerSize.height - shiftY;
|
|
@@ -612,7 +712,8 @@ async function renderWithX6(
|
|
|
612
712
|
childIds,
|
|
613
713
|
containerBBox: toPlainRect(containerBBox),
|
|
614
714
|
containerSize: toPlainRect({ x: 0, y: 0, ...containerSize }),
|
|
615
|
-
|
|
715
|
+
minInsetX,
|
|
716
|
+
minInsetY,
|
|
616
717
|
bounds: { minLeft, minTop, maxRight, maxBottom },
|
|
617
718
|
shift: { shiftX, shiftY },
|
|
618
719
|
childDebug,
|
|
@@ -632,6 +733,7 @@ async function renderWithX6(
|
|
|
632
733
|
to: { width: neededWidth, height: neededHeight },
|
|
633
734
|
});
|
|
634
735
|
container.resize(neededWidth, neededHeight);
|
|
736
|
+
syncOwnedPortPositions(containerId);
|
|
635
737
|
} else {
|
|
636
738
|
logResize('container-no-resize', { containerId });
|
|
637
739
|
}
|
|
@@ -708,106 +810,149 @@ async function renderWithX6(
|
|
|
708
810
|
if (!isPrimaryMover(node, options)) return;
|
|
709
811
|
growAncestorContainers(node);
|
|
710
812
|
});
|
|
813
|
+
const syncOwnedPortPositions = (ownerId: string): void => {
|
|
814
|
+
const owner = graphView.getCellById(ownerId);
|
|
815
|
+
if (!owner || typeof owner.size !== 'function' || typeof owner.getPorts !== 'function' || typeof owner.setPortProp !== 'function') {
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const size = owner.size();
|
|
819
|
+
const portIds = (owner.getPorts() as any[])
|
|
820
|
+
.map((port) => String(port?.id ?? ''))
|
|
821
|
+
.filter((portId) => portId.length > 0);
|
|
822
|
+
for (const portId of portIds) {
|
|
823
|
+
const placement = portPlacementById.get(portId) ?? { side: 'right' as PortSide, ratio: 0.5 };
|
|
824
|
+
const ratio = clamp(placement.ratio, 0.05, 0.95);
|
|
825
|
+
const nextArgs = placement.side === 'left' || placement.side === 'right'
|
|
826
|
+
? {
|
|
827
|
+
x: placement.side === 'left' ? 0 : size.width,
|
|
828
|
+
y: ratio * size.height,
|
|
829
|
+
side: placement.side,
|
|
830
|
+
ratio,
|
|
831
|
+
}
|
|
832
|
+
: {
|
|
833
|
+
x: ratio * size.width,
|
|
834
|
+
y: placement.side === 'top' ? 0 : size.height,
|
|
835
|
+
side: placement.side,
|
|
836
|
+
ratio,
|
|
837
|
+
};
|
|
838
|
+
owner.setPortProp(portId, {
|
|
839
|
+
args: nextArgs,
|
|
840
|
+
label: {
|
|
841
|
+
position: resolveBorderPortLabelPosition(placement.side),
|
|
842
|
+
},
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
graphView.on('node:resizing', ({ node }: { node: any }) => {
|
|
847
|
+
syncOwnedPortPositions(String(node?.id ?? ''));
|
|
848
|
+
});
|
|
849
|
+
graphView.on('node:resized', ({ node, options }: { node: any; options?: Record<string, unknown> }) => {
|
|
850
|
+
if (options?.silent) return;
|
|
851
|
+
syncOwnedPortPositions(String(node?.id ?? ''));
|
|
852
|
+
growAncestorContainers(node);
|
|
853
|
+
});
|
|
711
854
|
|
|
712
855
|
const edgeLabelPosition = (placement: EdgeLabel['placement']): number => {
|
|
713
856
|
if (placement === 'begin') return 0.15;
|
|
714
857
|
if (placement === 'end') return 0.85;
|
|
715
858
|
return 0.5;
|
|
716
859
|
};
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
const undirectedPairKey = (a: string, b: string): string => (a < b ? `${a}<->${b}` : `${b}<->${a}`);
|
|
723
|
-
const edgeById = new Map(graph.edges.map((edge) => [edge.id, edge]));
|
|
860
|
+
const directedPairKey = (sourceId: string, targetId: string): string => `${sourceId}=>${targetId}`;
|
|
861
|
+
const undirectedPairKey = (leftId: string, rightId: string): string => (
|
|
862
|
+
leftId < rightId ? `${leftId}<=>${rightId}` : `${rightId}<=>${leftId}`
|
|
863
|
+
);
|
|
864
|
+
const edgeIdsByPair = new Map<string, string[]>();
|
|
724
865
|
const undirectedEdgeIdsByPair = new Map<string, string[]>();
|
|
725
866
|
for (const edge of graph.edges) {
|
|
726
|
-
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
const key = undirectedPairKey(sourceOwner, targetOwner);
|
|
730
|
-
const ids = undirectedEdgeIdsByPair.get(key) ?? [];
|
|
867
|
+
if (!edge.sourceId || !edge.targetId) continue;
|
|
868
|
+
const key = directedPairKey(edge.sourceId, edge.targetId);
|
|
869
|
+
const ids = edgeIdsByPair.get(key) ?? [];
|
|
731
870
|
ids.push(edge.id);
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
if (edgeIds.length <= 1) {
|
|
871
|
+
edgeIdsByPair.set(key, ids);
|
|
872
|
+
const undirectedKey = undirectedPairKey(edge.sourceId, edge.targetId);
|
|
873
|
+
const pairIds = undirectedEdgeIdsByPair.get(undirectedKey) ?? [];
|
|
874
|
+
pairIds.push(edge.id);
|
|
875
|
+
undirectedEdgeIdsByPair.set(undirectedKey, pairIds);
|
|
876
|
+
}
|
|
877
|
+
const edgePointForEndpoint = (endpointId: string): { x: number; y: number } | undefined => {
|
|
878
|
+
const endpoint = nodeById.get(endpointId);
|
|
879
|
+
if (!endpoint) {
|
|
742
880
|
return undefined;
|
|
743
881
|
}
|
|
744
|
-
|
|
745
|
-
const
|
|
746
|
-
const
|
|
747
|
-
if (!
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
if (
|
|
755
|
-
|
|
756
|
-
});
|
|
757
|
-
const [pairA, pairB] = sourceOwner < targetOwner
|
|
758
|
-
? [sourceOwner, targetOwner]
|
|
759
|
-
: [targetOwner, sourceOwner];
|
|
760
|
-
const forwardIds: string[] = [];
|
|
761
|
-
const reverseIds: string[] = [];
|
|
762
|
-
for (const edgeId of orderedEdgeIds) {
|
|
763
|
-
const pairEdge = edgeById.get(edgeId);
|
|
764
|
-
if (!pairEdge) continue;
|
|
765
|
-
const pairSource = endpointOwnerId(pairEdge.sourceId);
|
|
766
|
-
const pairTarget = endpointOwnerId(pairEdge.targetId);
|
|
767
|
-
if (pairSource === pairA && pairTarget === pairB) {
|
|
768
|
-
forwardIds.push(edgeId);
|
|
769
|
-
} else if (pairSource === pairB && pairTarget === pairA) {
|
|
770
|
-
reverseIds.push(edgeId);
|
|
882
|
+
if (endpoint.kind === 'Port' && endpoint.parentId) {
|
|
883
|
+
const ownerBox = layout.boxes.get(endpoint.parentId);
|
|
884
|
+
const placement = portPlacementById.get(endpoint.id);
|
|
885
|
+
if (!ownerBox || !placement) {
|
|
886
|
+
return undefined;
|
|
887
|
+
}
|
|
888
|
+
const ratio = clamp(placement.ratio, 0.05, 0.95);
|
|
889
|
+
if (placement.side === 'left') {
|
|
890
|
+
return { x: ownerBox.x, y: ownerBox.y + (ownerBox.height * ratio) };
|
|
891
|
+
}
|
|
892
|
+
if (placement.side === 'right') {
|
|
893
|
+
return { x: ownerBox.x + ownerBox.width, y: ownerBox.y + (ownerBox.height * ratio) };
|
|
771
894
|
}
|
|
895
|
+
if (placement.side === 'top') {
|
|
896
|
+
return { x: ownerBox.x + (ownerBox.width * ratio), y: ownerBox.y };
|
|
897
|
+
}
|
|
898
|
+
return { x: ownerBox.x + (ownerBox.width * ratio), y: ownerBox.y + ownerBox.height };
|
|
772
899
|
}
|
|
773
|
-
|
|
774
|
-
if (
|
|
900
|
+
const box = layout.boxes.get(endpointId);
|
|
901
|
+
if (!box) {
|
|
775
902
|
return undefined;
|
|
776
903
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
directionSign = 1;
|
|
783
|
-
laneIndex = forwardIds.indexOf(edge.id);
|
|
784
|
-
laneCount = forwardIds.length;
|
|
785
|
-
} else if (sourceOwner === pairB && targetOwner === pairA) {
|
|
786
|
-
directionSign = -1;
|
|
787
|
-
laneIndex = reverseIds.indexOf(edge.id);
|
|
788
|
-
laneCount = reverseIds.length;
|
|
904
|
+
return { x: box.x + (box.width / 2), y: box.y + (box.height / 2) };
|
|
905
|
+
};
|
|
906
|
+
const fanningVertexForEdge = (edge: EdgeSpec): Array<{ x: number; y: number }> | undefined => {
|
|
907
|
+
if (!edge.sourceId || !edge.targetId) {
|
|
908
|
+
return undefined;
|
|
789
909
|
}
|
|
790
|
-
|
|
910
|
+
const undirectedIds = undirectedEdgeIdsByPair.get(undirectedPairKey(edge.sourceId, edge.targetId)) ?? [];
|
|
911
|
+
if (undirectedIds.length <= 1) {
|
|
791
912
|
return undefined;
|
|
792
913
|
}
|
|
793
|
-
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
if (!pairABox || !pairBBox) {
|
|
914
|
+
const sourcePoint = edgePointForEndpoint(edge.sourceId);
|
|
915
|
+
const targetPoint = edgePointForEndpoint(edge.targetId);
|
|
916
|
+
if (!sourcePoint || !targetPoint) {
|
|
797
917
|
return undefined;
|
|
798
918
|
}
|
|
799
|
-
const sx =
|
|
800
|
-
const sy =
|
|
801
|
-
const tx =
|
|
802
|
-
const ty =
|
|
919
|
+
const sx = sourcePoint.x;
|
|
920
|
+
const sy = sourcePoint.y;
|
|
921
|
+
const tx = targetPoint.x;
|
|
922
|
+
const ty = targetPoint.y;
|
|
803
923
|
const dx = tx - sx;
|
|
804
924
|
const dy = ty - sy;
|
|
805
925
|
const len = Math.hypot(dx, dy);
|
|
806
926
|
if (!Number.isFinite(len) || len < 1) {
|
|
807
927
|
return undefined;
|
|
808
928
|
}
|
|
809
|
-
const
|
|
810
|
-
|
|
929
|
+
const forwardIds = (edgeIdsByPair.get(directedPairKey(edge.sourceId, edge.targetId)) ?? [])
|
|
930
|
+
.slice()
|
|
931
|
+
.sort((left, right) => left.localeCompare(right));
|
|
932
|
+
const reverseIds = (edgeIdsByPair.get(directedPairKey(edge.targetId, edge.sourceId)) ?? [])
|
|
933
|
+
.slice()
|
|
934
|
+
.sort((left, right) => left.localeCompare(right));
|
|
935
|
+
let offset = 0;
|
|
936
|
+
if (forwardIds.length > 0 && reverseIds.length > 0) {
|
|
937
|
+
const forwardIndex = forwardIds.indexOf(edge.id);
|
|
938
|
+
const reverseIndex = reverseIds.indexOf(edge.id);
|
|
939
|
+
if (forwardIndex >= 0) {
|
|
940
|
+
const laneOffset = forwardIndex - ((forwardIds.length - 1) / 2);
|
|
941
|
+
offset = 16 + (laneOffset * 10);
|
|
942
|
+
} else if (reverseIndex >= 0) {
|
|
943
|
+
const laneOffset = reverseIndex - ((reverseIds.length - 1) / 2);
|
|
944
|
+
offset = -16 + (laneOffset * 10);
|
|
945
|
+
} else {
|
|
946
|
+
return undefined;
|
|
947
|
+
}
|
|
948
|
+
} else {
|
|
949
|
+
const laneIndex = forwardIds.indexOf(edge.id);
|
|
950
|
+
if (laneIndex < 0 || forwardIds.length <= 1) {
|
|
951
|
+
return undefined;
|
|
952
|
+
}
|
|
953
|
+
const laneOffset = laneIndex - ((forwardIds.length - 1) / 2);
|
|
954
|
+
offset = laneOffset * 16;
|
|
955
|
+
}
|
|
811
956
|
if (Math.abs(offset) < 0.01) {
|
|
812
957
|
return undefined;
|
|
813
958
|
}
|
|
@@ -844,8 +989,8 @@ async function renderWithX6(
|
|
|
844
989
|
id: edge.id,
|
|
845
990
|
source: resolveEndpoint(edge.sourceId),
|
|
846
991
|
target: resolveEndpoint(edge.targetId),
|
|
847
|
-
router:
|
|
848
|
-
connector:
|
|
992
|
+
router: resolveEdgeRouter(resolvedStyle),
|
|
993
|
+
connector: resolveEdgeConnector(resolvedStyle),
|
|
849
994
|
attrs: {
|
|
850
995
|
line: lineAttrs,
|
|
851
996
|
},
|
|
@@ -862,7 +1007,7 @@ async function renderWithX6(
|
|
|
862
1007
|
labelBody: resolveEdgeLabelBodyAttrs(resolvedStyle, label.placement),
|
|
863
1008
|
},
|
|
864
1009
|
})),
|
|
865
|
-
zIndex:
|
|
1010
|
+
zIndex: 50,
|
|
866
1011
|
});
|
|
867
1012
|
}
|
|
868
1013
|
|
|
@@ -902,49 +1047,113 @@ async function renderWithX6(
|
|
|
902
1047
|
e.stopPropagation();
|
|
903
1048
|
});
|
|
904
1049
|
|
|
905
|
-
const
|
|
1050
|
+
const sidePriority = (side: PortSide): number => {
|
|
1051
|
+
switch (side) {
|
|
1052
|
+
case 'left':
|
|
1053
|
+
return 0;
|
|
1054
|
+
case 'top':
|
|
1055
|
+
return 1;
|
|
1056
|
+
case 'right':
|
|
1057
|
+
return 2;
|
|
1058
|
+
case 'bottom':
|
|
1059
|
+
return 3;
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
const projectPortPlacement = (node: any, clientX: number, clientY: number, preferredSide?: PortSide): PortPlacement | undefined => {
|
|
1063
|
+
if (!node || typeof node.size !== 'function') return undefined;
|
|
1064
|
+
const size = node.size();
|
|
1065
|
+
const position = typeof node.getPosition === 'function'
|
|
1066
|
+
? node.getPosition()
|
|
1067
|
+
: { x: Number(node?.position?.x ?? 0), y: Number(node?.position?.y ?? 0) };
|
|
1068
|
+
const localPoint = typeof graphView.clientToGraph === 'function'
|
|
1069
|
+
? graphView.clientToGraph(clientX, clientY)
|
|
1070
|
+
: { x: clientX, y: clientY };
|
|
1071
|
+
const localX = clamp(localPoint.x - position.x, 0, size.width);
|
|
1072
|
+
const localY = clamp(localPoint.y - position.y, 0, size.height);
|
|
1073
|
+
const candidates: Array<{ side: PortSide; distance: number }> = [
|
|
1074
|
+
{ side: 'left', distance: localX },
|
|
1075
|
+
{ side: 'right', distance: size.width - localX },
|
|
1076
|
+
{ side: 'top', distance: localY },
|
|
1077
|
+
{ side: 'bottom', distance: size.height - localY },
|
|
1078
|
+
];
|
|
1079
|
+
candidates.sort((left, right) => left.distance - right.distance || sidePriority(left.side) - sidePriority(right.side));
|
|
1080
|
+
const closest = candidates[0];
|
|
1081
|
+
const preferredCandidate = preferredSide
|
|
1082
|
+
? candidates.find((candidate) => candidate.side === preferredSide)
|
|
1083
|
+
: undefined;
|
|
1084
|
+
const hysteresis = 8;
|
|
1085
|
+
const side = preferredCandidate && preferredSide && (preferredCandidate.distance - closest.distance) <= hysteresis
|
|
1086
|
+
? preferredSide
|
|
1087
|
+
: closest.side;
|
|
1088
|
+
const ratio = side === 'left' || side === 'right'
|
|
1089
|
+
? (size.height > 0 ? localY / size.height : 0.5)
|
|
1090
|
+
: (size.width > 0 ? localX / size.width : 0.5);
|
|
1091
|
+
return {
|
|
1092
|
+
side,
|
|
1093
|
+
ratio: Math.max(0.05, Math.min(0.95, ratio)),
|
|
1094
|
+
};
|
|
1095
|
+
};
|
|
906
1096
|
const onPointerMove = (event: PointerEvent): void => {
|
|
907
|
-
if (!activePortDrag) return;
|
|
1097
|
+
if (!activePortDrag || event.pointerId !== activePortDrag.pointerId) return;
|
|
908
1098
|
const node = graphView.getCellById(activePortDrag.nodeId);
|
|
909
1099
|
if (!node || typeof node.size !== 'function' || typeof node.getPort !== 'function' || typeof node.setPortProp !== 'function') return;
|
|
1100
|
+
const placement = projectPortPlacement(node, event.clientX, event.clientY, activePortDrag.side);
|
|
1101
|
+
if (!placement) return;
|
|
1102
|
+
if (placement.side === activePortDrag.side && Math.abs(placement.ratio - activePortDrag.ratio) < 0.005) {
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
910
1105
|
const size = node.size();
|
|
911
|
-
const
|
|
912
|
-
|
|
1106
|
+
const nextX = placement.side === 'left'
|
|
1107
|
+
? 0
|
|
1108
|
+
: placement.side === 'right'
|
|
1109
|
+
? size.width
|
|
1110
|
+
: placement.ratio * size.width;
|
|
1111
|
+
const nextY = placement.side === 'top'
|
|
1112
|
+
? 0
|
|
1113
|
+
: placement.side === 'bottom'
|
|
1114
|
+
? size.height
|
|
1115
|
+
: placement.ratio * size.height;
|
|
1116
|
+
node.setPortProp(activePortDrag.portId, 'args/x', nextX);
|
|
913
1117
|
node.setPortProp(activePortDrag.portId, 'args/y', nextY);
|
|
1118
|
+
node.setPortProp(activePortDrag.portId, 'args/side', placement.side);
|
|
1119
|
+
node.setPortProp(activePortDrag.portId, 'args/ratio', placement.ratio);
|
|
1120
|
+
if (placement.side !== activePortDrag.side) {
|
|
1121
|
+
node.setPortProp(activePortDrag.portId, 'label/position', resolveBorderPortLabelPosition(placement.side));
|
|
1122
|
+
}
|
|
1123
|
+
activePortDrag.side = placement.side;
|
|
1124
|
+
activePortDrag.ratio = placement.ratio;
|
|
1125
|
+
portPlacementById.set(activePortDrag.portId, placement);
|
|
914
1126
|
};
|
|
915
|
-
const onPointerUp = (): void => {
|
|
1127
|
+
const onPointerUp = (event: PointerEvent): void => {
|
|
1128
|
+
if (!activePortDrag || event.pointerId !== activePortDrag.pointerId) return;
|
|
1129
|
+
if (typeof activePortDrag.container.releasePointerCapture === 'function') {
|
|
1130
|
+
try {
|
|
1131
|
+
activePortDrag.container.releasePointerCapture(activePortDrag.pointerId);
|
|
1132
|
+
} catch {
|
|
1133
|
+
// Ignore capture release failures.
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
916
1136
|
activePortDrag = undefined;
|
|
917
1137
|
};
|
|
918
1138
|
window.addEventListener('pointermove', onPointerMove);
|
|
919
1139
|
window.addEventListener('pointerup', onPointerUp);
|
|
920
|
-
const startPortDrag = (clientY: number, node: any, portId: string): void => {
|
|
1140
|
+
const startPortDrag = (clientX: number, clientY: number, node: any, portId: string, pointerId: number, container: Element): void => {
|
|
921
1141
|
if (!node || typeof node.getPort !== 'function') return;
|
|
922
|
-
const
|
|
923
|
-
|
|
1142
|
+
const placement = projectPortPlacement(node, clientX, clientY, portPlacementById.get(portId)?.side ?? 'right')
|
|
1143
|
+
?? portPlacementById.get(portId)
|
|
1144
|
+
?? { side: 'right' as PortSide, ratio: 0.5 };
|
|
924
1145
|
activePortDrag = {
|
|
925
1146
|
nodeId: String(node.id),
|
|
926
1147
|
portId: String(portId),
|
|
927
|
-
|
|
928
|
-
|
|
1148
|
+
pointerId,
|
|
1149
|
+
side: placement.side,
|
|
1150
|
+
ratio: placement.ratio,
|
|
1151
|
+
container,
|
|
929
1152
|
};
|
|
930
1153
|
};
|
|
931
|
-
const onCanvasPointerDown = (event: PointerEvent): void => {
|
|
932
|
-
if (!isPortPointerTarget(event)) return;
|
|
933
|
-
const portId = findPortIdFromTarget(event.target);
|
|
934
|
-
if (!portId) return;
|
|
935
|
-
const ownerId = ownerByPortId.get(portId);
|
|
936
|
-
if (!ownerId) return;
|
|
937
|
-
const node = graphView.getCellById(ownerId);
|
|
938
|
-
startPortDrag(event.clientY, node, portId);
|
|
939
|
-
event.preventDefault();
|
|
940
|
-
event.stopPropagation();
|
|
941
|
-
if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
|
|
942
|
-
};
|
|
943
|
-
liveCanvas.addEventListener('pointerdown', onCanvasPointerDown, true);
|
|
944
1154
|
liveCanvas.addEventListener('DOMNodeRemovedFromDocument', () => {
|
|
945
1155
|
window.removeEventListener('pointermove', onPointerMove);
|
|
946
1156
|
window.removeEventListener('pointerup', onPointerUp);
|
|
947
|
-
liveCanvas.removeEventListener('pointerdown', onCanvasPointerDown, true);
|
|
948
1157
|
}, { once: true });
|
|
949
1158
|
|
|
950
1159
|
const resultContainer = liveCanvas.closest('.oml-md-result');
|
|
@@ -1348,7 +1557,8 @@ async function loadDagreLib(): Promise<any> {
|
|
|
1348
1557
|
|
|
1349
1558
|
async function layoutGraphDagre(
|
|
1350
1559
|
graph: { nodes: NodeSpec[]; edges: EdgeSpec[]; roots: string[] },
|
|
1351
|
-
options: DagreLayoutOptions
|
|
1560
|
+
options: DagreLayoutOptions,
|
|
1561
|
+
rootSpacing: BoxSpacing
|
|
1352
1562
|
): Promise<NativeLayoutResult> {
|
|
1353
1563
|
const dagre = await loadDagreLib();
|
|
1354
1564
|
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
@@ -1385,8 +1595,6 @@ async function layoutGraphDagre(
|
|
|
1385
1595
|
stack: {
|
|
1386
1596
|
direction: styleLayout.direction === 'horizontal' ? 'horizontal' : 'vertical',
|
|
1387
1597
|
gap: clampLayoutNumber(styleLayout.gap, 0, 400, 0),
|
|
1388
|
-
marginx: clampLayoutNumber(styleLayout.marginx, 0, 200, 0),
|
|
1389
|
-
marginy: clampLayoutNumber(styleLayout.marginy, 0, 200, 0),
|
|
1390
1598
|
stretch: typeof styleLayout.stretch === 'boolean' ? styleLayout.stretch : true,
|
|
1391
1599
|
},
|
|
1392
1600
|
};
|
|
@@ -1406,29 +1614,40 @@ async function layoutGraphDagre(
|
|
|
1406
1614
|
rankdir,
|
|
1407
1615
|
nodesep: clampLayoutNumber(styleLayout.nodesep, 0, 400, options.nodesep),
|
|
1408
1616
|
ranksep: clampLayoutNumber(styleLayout.ranksep, 0, 500, options.ranksep),
|
|
1409
|
-
marginx: clampLayoutNumber(styleLayout.marginx, 0, 200, options.marginx),
|
|
1410
|
-
marginy: clampLayoutNumber(styleLayout.marginy, 0, 200, options.marginy),
|
|
1411
1617
|
},
|
|
1412
1618
|
};
|
|
1413
1619
|
};
|
|
1414
1620
|
|
|
1415
1621
|
const childPositionById = new Map<string, { x: number; y: number }>();
|
|
1622
|
+
const branchChildForParent = (parentId: string | undefined, nodeId: string): string | undefined => {
|
|
1623
|
+
let cursorId: string | undefined = nodeId;
|
|
1624
|
+
while (cursorId) {
|
|
1625
|
+
const node = nodeById.get(cursorId);
|
|
1626
|
+
if (!node) {
|
|
1627
|
+
return undefined;
|
|
1628
|
+
}
|
|
1629
|
+
if (node.parentId === parentId) {
|
|
1630
|
+
return node.id;
|
|
1631
|
+
}
|
|
1632
|
+
cursorId = node.parentId;
|
|
1633
|
+
}
|
|
1634
|
+
return undefined;
|
|
1635
|
+
};
|
|
1416
1636
|
const edgesBetweenChildren = (parentId: string | undefined, childSet: Set<string>): Array<{ source: string; target: string }> => {
|
|
1417
1637
|
const seen = new Set<string>();
|
|
1418
1638
|
const links: Array<{ source: string; target: string }> = [];
|
|
1419
1639
|
for (const edge of graph.edges) {
|
|
1420
1640
|
const sourceId = endpointOwner(edge.sourceId);
|
|
1421
1641
|
const targetId = endpointOwner(edge.targetId);
|
|
1422
|
-
if (!sourceId || !targetId
|
|
1423
|
-
|
|
1424
|
-
const
|
|
1425
|
-
|
|
1426
|
-
if (!
|
|
1427
|
-
|
|
1428
|
-
const key = `${sourceId}=>${targetId}`;
|
|
1642
|
+
if (!sourceId || !targetId) continue;
|
|
1643
|
+
const sourceBranchId = branchChildForParent(parentId, sourceId);
|
|
1644
|
+
const targetBranchId = branchChildForParent(parentId, targetId);
|
|
1645
|
+
if (!sourceBranchId || !targetBranchId || sourceBranchId === targetBranchId) continue;
|
|
1646
|
+
if (!childSet.has(sourceBranchId) || !childSet.has(targetBranchId)) continue;
|
|
1647
|
+
const key = `${sourceBranchId}=>${targetBranchId}`;
|
|
1429
1648
|
if (seen.has(key)) continue;
|
|
1430
1649
|
seen.add(key);
|
|
1431
|
-
links.push({ source:
|
|
1650
|
+
links.push({ source: sourceBranchId, target: targetBranchId });
|
|
1432
1651
|
}
|
|
1433
1652
|
return links;
|
|
1434
1653
|
};
|
|
@@ -1443,8 +1662,8 @@ async function layoutGraphDagre(
|
|
|
1443
1662
|
}
|
|
1444
1663
|
|
|
1445
1664
|
const parent = parentId ? nodeById.get(parentId) : undefined;
|
|
1446
|
-
const topPadding = parent?.contentTopPadding ?? 0;
|
|
1447
1665
|
const localLayout = layoutOptionsForParent(parentId);
|
|
1666
|
+
const spacing = parent ? resolveBoxSpacing(parent.style, 0) : rootSpacing;
|
|
1448
1667
|
let totalWidth = 0;
|
|
1449
1668
|
let totalHeight = 0;
|
|
1450
1669
|
if (localLayout.type === 'stack') {
|
|
@@ -1452,13 +1671,13 @@ async function layoutGraphDagre(
|
|
|
1452
1671
|
const childNodes = childIds.map((childId) => nodeById.get(childId)).filter((node): node is NodeSpec => !!node);
|
|
1453
1672
|
const maxChildWidth = childNodes.reduce((max, child) => Math.max(max, child.width), 0);
|
|
1454
1673
|
const maxChildHeight = childNodes.reduce((max, child) => Math.max(max, child.height), 0);
|
|
1455
|
-
const parentContentWidth = Math.max(0, (parent?.width ?? 0) -
|
|
1456
|
-
const parentContentHeight = Math.max(0, (parent?.height ?? 0) -
|
|
1674
|
+
const parentContentWidth = Math.max(0, (parent?.width ?? 0) - spacing.marginLeft - spacing.marginRight - spacing.paddingLeft - spacing.paddingRight);
|
|
1675
|
+
const parentContentHeight = Math.max(0, (parent?.height ?? 0) - spacing.marginTop - spacing.marginBottom - spacing.paddingTop - spacing.paddingBottom);
|
|
1457
1676
|
const availableWidth = Math.max(maxChildWidth, parentContentWidth);
|
|
1458
1677
|
const availableHeight = Math.max(maxChildHeight, parentContentHeight);
|
|
1459
1678
|
const isSingleChild = childNodes.length === 1;
|
|
1460
|
-
let cursorX =
|
|
1461
|
-
let cursorY =
|
|
1679
|
+
let cursorX = spacing.marginLeft + spacing.paddingLeft;
|
|
1680
|
+
let cursorY = spacing.marginTop + spacing.paddingTop;
|
|
1462
1681
|
for (const child of childNodes) {
|
|
1463
1682
|
if (local.stretch && isSingleChild) {
|
|
1464
1683
|
child.width = Math.max(0, availableWidth);
|
|
@@ -1481,16 +1700,16 @@ async function layoutGraphDagre(
|
|
|
1481
1700
|
childNodes.reduce((sum, child) => sum + child.height, 0) + (Math.max(0, childNodes.length - 1) * local.gap)
|
|
1482
1701
|
);
|
|
1483
1702
|
const contentWidth = Math.max(0, childNodes.reduce((max, child) => Math.max(max, child.width), 0));
|
|
1484
|
-
totalWidth =
|
|
1485
|
-
totalHeight =
|
|
1703
|
+
totalWidth = spacing.marginLeft + spacing.paddingLeft + contentWidth + spacing.paddingRight + spacing.marginRight;
|
|
1704
|
+
totalHeight = spacing.marginTop + spacing.paddingTop + contentHeight + spacing.paddingBottom + spacing.marginBottom;
|
|
1486
1705
|
} else {
|
|
1487
1706
|
const contentWidth = Math.max(
|
|
1488
1707
|
0,
|
|
1489
1708
|
childNodes.reduce((sum, child) => sum + child.width, 0) + (Math.max(0, childNodes.length - 1) * local.gap)
|
|
1490
1709
|
);
|
|
1491
1710
|
const contentHeight = Math.max(0, childNodes.reduce((max, child) => Math.max(max, child.height), 0));
|
|
1492
|
-
totalWidth =
|
|
1493
|
-
totalHeight =
|
|
1711
|
+
totalWidth = spacing.marginLeft + spacing.paddingLeft + contentWidth + spacing.paddingRight + spacing.marginRight;
|
|
1712
|
+
totalHeight = spacing.marginTop + spacing.paddingTop + contentHeight + spacing.paddingBottom + spacing.marginBottom;
|
|
1494
1713
|
}
|
|
1495
1714
|
} else {
|
|
1496
1715
|
const localOptions = localLayout.dagre;
|
|
@@ -1546,23 +1765,19 @@ async function layoutGraphDagre(
|
|
|
1546
1765
|
const left = laid.x - (width / 2);
|
|
1547
1766
|
const top = laid.y - (height / 2);
|
|
1548
1767
|
childPositionById.set(childId, {
|
|
1549
|
-
x:
|
|
1550
|
-
y:
|
|
1768
|
+
x: spacing.marginLeft + spacing.paddingLeft + (left - minX),
|
|
1769
|
+
y: spacing.marginTop + spacing.paddingTop + (top - minY),
|
|
1551
1770
|
});
|
|
1552
1771
|
}
|
|
1553
1772
|
|
|
1554
1773
|
const contentWidth = Math.max(0, maxX - minX);
|
|
1555
1774
|
const contentHeight = Math.max(0, maxY - minY);
|
|
1556
|
-
totalWidth =
|
|
1557
|
-
totalHeight =
|
|
1775
|
+
totalWidth = spacing.marginLeft + spacing.paddingLeft + contentWidth + spacing.paddingRight + spacing.marginRight;
|
|
1776
|
+
totalHeight = spacing.marginTop + spacing.paddingTop + contentHeight + spacing.paddingBottom + spacing.marginBottom;
|
|
1558
1777
|
}
|
|
1559
1778
|
if (parent) {
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
}
|
|
1563
|
-
if (!parent.hasExplicitHeight) {
|
|
1564
|
-
parent.height = Math.max(parent.height, totalHeight);
|
|
1565
|
-
}
|
|
1779
|
+
parent.width = Math.max(parent.width, totalWidth);
|
|
1780
|
+
parent.height = Math.max(parent.height, totalHeight);
|
|
1566
1781
|
}
|
|
1567
1782
|
return { width: totalWidth, height: totalHeight };
|
|
1568
1783
|
};
|
|
@@ -1582,9 +1797,9 @@ async function layoutGraphDagre(
|
|
|
1582
1797
|
if (localLayout.type === 'stack') {
|
|
1583
1798
|
const local = localLayout.stack;
|
|
1584
1799
|
const childNodes = childIds.map((childId) => nodeById.get(childId)).filter((node): node is NodeSpec => !!node);
|
|
1585
|
-
const
|
|
1586
|
-
const contentWidth = Math.max(0, parent.width -
|
|
1587
|
-
const contentHeight = Math.max(0, parent.height -
|
|
1800
|
+
const spacing = resolveBoxSpacing(parent.style, 0);
|
|
1801
|
+
const contentWidth = Math.max(0, parent.width - spacing.marginLeft - spacing.marginRight - spacing.paddingLeft - spacing.paddingRight);
|
|
1802
|
+
const contentHeight = Math.max(0, parent.height - spacing.marginTop - spacing.marginBottom - spacing.paddingTop - spacing.paddingBottom);
|
|
1588
1803
|
const isSingleChild = childNodes.length === 1;
|
|
1589
1804
|
|
|
1590
1805
|
if (local.stretch) {
|
|
@@ -1602,8 +1817,8 @@ async function layoutGraphDagre(
|
|
|
1602
1817
|
}
|
|
1603
1818
|
}
|
|
1604
1819
|
|
|
1605
|
-
let cursorX =
|
|
1606
|
-
let cursorY =
|
|
1820
|
+
let cursorX = spacing.marginLeft + spacing.paddingLeft;
|
|
1821
|
+
let cursorY = spacing.marginTop + spacing.paddingTop;
|
|
1607
1822
|
for (const child of childNodes) {
|
|
1608
1823
|
childPositionById.set(child.id, { x: cursorX, y: cursorY });
|
|
1609
1824
|
if (local.direction === 'vertical') {
|
|
@@ -1819,9 +2034,6 @@ function compileDiagramGraph(
|
|
|
1819
2034
|
children: [],
|
|
1820
2035
|
width: 160,
|
|
1821
2036
|
height: 70,
|
|
1822
|
-
hasExplicitWidth: false,
|
|
1823
|
-
hasExplicitHeight: false,
|
|
1824
|
-
contentTopPadding: 0,
|
|
1825
2037
|
});
|
|
1826
2038
|
}
|
|
1827
2039
|
|
|
@@ -1899,9 +2111,6 @@ function compileDiagramGraph(
|
|
|
1899
2111
|
const estimated = estimateSize(node.kind, baseLabel, node.labels.length, node.style);
|
|
1900
2112
|
node.width = estimated.width;
|
|
1901
2113
|
node.height = estimated.height;
|
|
1902
|
-
node.hasExplicitWidth = estimated.hasExplicitWidth;
|
|
1903
|
-
node.hasExplicitHeight = estimated.hasExplicitHeight;
|
|
1904
|
-
node.contentTopPadding = resolveContainerTopPadding(node, baseLabel);
|
|
1905
2114
|
}
|
|
1906
2115
|
for (const edge of edges) {
|
|
1907
2116
|
edge.style = styleFor('edge', edge.id, edge.classes, edge.properties);
|
|
@@ -1930,68 +2139,46 @@ function estimateSize(
|
|
|
1930
2139
|
label: string,
|
|
1931
2140
|
labelCount: number,
|
|
1932
2141
|
style: Record<string, unknown>
|
|
1933
|
-
): { width: number; height: number
|
|
2142
|
+
): { width: number; height: number } {
|
|
1934
2143
|
const styledWidth = toPositiveNumber(style.width);
|
|
1935
2144
|
const styledHeight = toPositiveNumber(style.height);
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
2145
|
+
const attrs = extractStyleAttrs(style);
|
|
2146
|
+
const labelAttrs = asRecord(attrs.label);
|
|
2147
|
+
const labelDisplay = typeof labelAttrs?.display === 'string' ? labelAttrs.display.trim().toLowerCase() : '';
|
|
2148
|
+
const labelOpacity = toNonNegativeNumber(labelAttrs?.opacity);
|
|
2149
|
+
const labelVisible = labelDisplay !== 'none' && (labelOpacity === undefined || labelOpacity > 0);
|
|
2150
|
+
const effectiveLabel = labelVisible ? label : '';
|
|
2151
|
+
const effectiveLabelCount = labelVisible ? labelCount : 0;
|
|
1939
2152
|
if (kind === 'Port') {
|
|
1940
2153
|
return {
|
|
1941
|
-
width: styledWidth ??
|
|
1942
|
-
height: styledHeight ??
|
|
1943
|
-
hasExplicitWidth: styledWidth !== undefined,
|
|
1944
|
-
hasExplicitHeight: styledHeight !== undefined,
|
|
2154
|
+
width: Math.max(14, styledWidth ?? 0),
|
|
2155
|
+
height: Math.max(14, styledHeight ?? 0),
|
|
1945
2156
|
};
|
|
1946
2157
|
}
|
|
1947
|
-
const textWidth = Math.max(24,
|
|
1948
|
-
const baseWidth = Math.max(72, Math.min(280, textWidth + 26));
|
|
2158
|
+
const textWidth = effectiveLabel.length > 0 ? Math.max(24, effectiveLabel.length * 7) : 0;
|
|
2159
|
+
const baseWidth = effectiveLabel.length > 0 ? Math.max(72, Math.min(280, textWidth + 26)) : 0;
|
|
1949
2160
|
if (kind === 'Compartment') {
|
|
1950
|
-
const size = {
|
|
2161
|
+
const size = {
|
|
2162
|
+
width: baseWidth,
|
|
2163
|
+
height: effectiveLabelCount > 0 ? Math.max(44, 24 + effectiveLabelCount * 16) : 0,
|
|
2164
|
+
};
|
|
1951
2165
|
return {
|
|
1952
|
-
width: styledWidth ??
|
|
1953
|
-
height: styledHeight ??
|
|
1954
|
-
hasExplicitWidth: styledWidth !== undefined,
|
|
1955
|
-
hasExplicitHeight: styledHeight !== undefined,
|
|
2166
|
+
width: Math.max(size.width, styledWidth ?? 0),
|
|
2167
|
+
height: Math.max(size.height, styledHeight ?? 0),
|
|
1956
2168
|
};
|
|
1957
2169
|
}
|
|
1958
|
-
const size = {
|
|
2170
|
+
const size = {
|
|
2171
|
+
width: baseWidth,
|
|
2172
|
+
height: effectiveLabelCount > 0 ? Math.max(36, 20 + effectiveLabelCount * 16) : 0,
|
|
2173
|
+
};
|
|
1959
2174
|
return {
|
|
1960
|
-
width: styledWidth ??
|
|
1961
|
-
height: styledHeight ??
|
|
1962
|
-
hasExplicitWidth: styledWidth !== undefined,
|
|
1963
|
-
hasExplicitHeight: styledHeight !== undefined,
|
|
2175
|
+
width: Math.max(size.width, styledWidth ?? 0),
|
|
2176
|
+
height: Math.max(size.height, styledHeight ?? 0),
|
|
1964
2177
|
};
|
|
1965
2178
|
}
|
|
1966
2179
|
|
|
1967
|
-
function resolveContainerTopPadding(node: NodeSpec, labelText: string): number {
|
|
1968
|
-
if (node.children.length === 0) {
|
|
1969
|
-
return 0;
|
|
1970
|
-
}
|
|
1971
|
-
const explicitPadding = toNonNegativeNumber(node.style.paddingTop);
|
|
1972
|
-
if (explicitPadding !== undefined) {
|
|
1973
|
-
return Math.ceil(explicitPadding);
|
|
1974
|
-
}
|
|
1975
|
-
const attrs = extractStyleAttrs(node.style);
|
|
1976
|
-
const label = asRecord(attrs.label);
|
|
1977
|
-
if (label?.display === 'none') {
|
|
1978
|
-
return 0;
|
|
1979
|
-
}
|
|
1980
|
-
const labelOpacity = toNonNegativeNumber(label?.opacity);
|
|
1981
|
-
if (labelOpacity !== undefined && labelOpacity <= 0) {
|
|
1982
|
-
return 0;
|
|
1983
|
-
}
|
|
1984
|
-
const fontSize = toPositiveNumber(label?.fontSize) ?? 12;
|
|
1985
|
-
const lineCount = Math.max(1, labelText.split('\n').filter((line) => line.trim().length > 0).length);
|
|
1986
|
-
return Math.ceil((fontSize * 1.2) * lineCount);
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
2180
|
function localName(value: string): string {
|
|
1990
|
-
|
|
1991
|
-
if (hash >= 0 && hash < value.length - 1) return value.slice(hash + 1);
|
|
1992
|
-
const slash = value.lastIndexOf('/');
|
|
1993
|
-
if (slash >= 0 && slash < value.length - 1) return value.slice(slash + 1);
|
|
1994
|
-
return value;
|
|
2181
|
+
return displayLabelFromIri(value);
|
|
1995
2182
|
}
|
|
1996
2183
|
|
|
1997
2184
|
function indexTriples(rows: TripleRow[]): TripleIndex {
|
|
@@ -2046,9 +2233,7 @@ function resolveDagreLayoutOptions(options: Record<string, unknown> | undefined)
|
|
|
2046
2233
|
|
|
2047
2234
|
const nodesep = clampLayoutNumber(layout.nodesep, 0, 400, 28);
|
|
2048
2235
|
const ranksep = clampLayoutNumber(layout.ranksep, 0, 500, 64);
|
|
2049
|
-
|
|
2050
|
-
const marginy = clampLayoutNumber(layout.marginy, 0, 200, 16);
|
|
2051
|
-
return { rankdir, nodesep, ranksep, marginx, marginy };
|
|
2236
|
+
return { rankdir, nodesep, ranksep };
|
|
2052
2237
|
}
|
|
2053
2238
|
|
|
2054
2239
|
function clampLayoutNumber(value: unknown, min: number, max: number, fallback: number): number {
|
|
@@ -2058,6 +2243,129 @@ function clampLayoutNumber(value: unknown, min: number, max: number, fallback: n
|
|
|
2058
2243
|
return Math.max(min, Math.min(max, Math.round(value)));
|
|
2059
2244
|
}
|
|
2060
2245
|
|
|
2246
|
+
function resolveRootSpacing(options: Record<string, unknown> | undefined): BoxSpacing {
|
|
2247
|
+
return readBoxSpacing(options, {
|
|
2248
|
+
marginTop: 16,
|
|
2249
|
+
marginBottom: 16,
|
|
2250
|
+
marginLeft: 16,
|
|
2251
|
+
marginRight: 16,
|
|
2252
|
+
paddingTop: 0,
|
|
2253
|
+
paddingBottom: 0,
|
|
2254
|
+
paddingLeft: 0,
|
|
2255
|
+
paddingRight: 0,
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
function resolveBoxSpacing(style: Record<string, unknown> | undefined, fallbackMargin: number): BoxSpacing {
|
|
2260
|
+
return readBoxSpacing(style, {
|
|
2261
|
+
marginTop: fallbackMargin,
|
|
2262
|
+
marginBottom: fallbackMargin,
|
|
2263
|
+
marginLeft: fallbackMargin,
|
|
2264
|
+
marginRight: fallbackMargin,
|
|
2265
|
+
paddingTop: 0,
|
|
2266
|
+
paddingBottom: 0,
|
|
2267
|
+
paddingLeft: 0,
|
|
2268
|
+
paddingRight: 0,
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
function readBoxSpacing(style: Record<string, unknown> | undefined, defaults: BoxSpacing): BoxSpacing {
|
|
2273
|
+
const source = style ?? {};
|
|
2274
|
+
return {
|
|
2275
|
+
...readCssBoxSpacing(source.margin, 'margin', defaults),
|
|
2276
|
+
...readCssBoxSpacing(source.padding, 'padding', defaults),
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
function readCssBoxSpacing(
|
|
2281
|
+
value: unknown,
|
|
2282
|
+
kind: 'margin' | 'padding',
|
|
2283
|
+
defaults: BoxSpacing
|
|
2284
|
+
): Pick<BoxSpacing, 'marginTop' | 'marginRight' | 'marginBottom' | 'marginLeft' | 'paddingTop' | 'paddingRight' | 'paddingBottom' | 'paddingLeft'> {
|
|
2285
|
+
const sides = parseCssBoxShorthand(value);
|
|
2286
|
+
const isMargin = kind === 'margin';
|
|
2287
|
+
if (isMargin) {
|
|
2288
|
+
return {
|
|
2289
|
+
marginTop: sides?.top ?? defaults.marginTop,
|
|
2290
|
+
marginRight: sides?.right ?? defaults.marginRight,
|
|
2291
|
+
marginBottom: sides?.bottom ?? defaults.marginBottom,
|
|
2292
|
+
marginLeft: sides?.left ?? defaults.marginLeft,
|
|
2293
|
+
paddingTop: defaults.paddingTop,
|
|
2294
|
+
paddingRight: defaults.paddingRight,
|
|
2295
|
+
paddingBottom: defaults.paddingBottom,
|
|
2296
|
+
paddingLeft: defaults.paddingLeft,
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
2299
|
+
return {
|
|
2300
|
+
marginTop: defaults.marginTop,
|
|
2301
|
+
marginRight: defaults.marginRight,
|
|
2302
|
+
marginBottom: defaults.marginBottom,
|
|
2303
|
+
marginLeft: defaults.marginLeft,
|
|
2304
|
+
paddingTop: sides?.top ?? defaults.paddingTop,
|
|
2305
|
+
paddingRight: sides?.right ?? defaults.paddingRight,
|
|
2306
|
+
paddingBottom: sides?.bottom ?? defaults.paddingBottom,
|
|
2307
|
+
paddingLeft: sides?.left ?? defaults.paddingLeft,
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
function parseCssBoxShorthand(value: unknown): { top?: number; right?: number; bottom?: number; left?: number } | undefined {
|
|
2312
|
+
const values = Array.isArray(value)
|
|
2313
|
+
? value
|
|
2314
|
+
: typeof value === 'string'
|
|
2315
|
+
? (value.includes(',') ? undefined : value.trim().split(/\s+/).filter((token) => token.length > 0))
|
|
2316
|
+
: typeof value === 'number'
|
|
2317
|
+
? [value]
|
|
2318
|
+
: undefined;
|
|
2319
|
+
if (!values || values.length === 0 || values.length > 4) {
|
|
2320
|
+
return undefined;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
const parsed = values.map((entry) => {
|
|
2324
|
+
if (typeof entry === 'number' && Number.isFinite(entry)) {
|
|
2325
|
+
return entry;
|
|
2326
|
+
}
|
|
2327
|
+
if (typeof entry === 'string') {
|
|
2328
|
+
const next = Number.parseFloat(entry);
|
|
2329
|
+
if (Number.isFinite(next)) {
|
|
2330
|
+
return next;
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
return undefined;
|
|
2334
|
+
});
|
|
2335
|
+
if (parsed.some((entry) => entry === undefined)) {
|
|
2336
|
+
return undefined;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
const clampCssBoxValue = (entry: number | undefined): number | undefined => {
|
|
2340
|
+
if (entry === undefined) return undefined;
|
|
2341
|
+
return Math.max(0, Math.min(200, Math.round(entry)));
|
|
2342
|
+
};
|
|
2343
|
+
|
|
2344
|
+
if (parsed.length === 1) {
|
|
2345
|
+
const single = clampCssBoxValue(parsed[0]);
|
|
2346
|
+
return single === undefined ? undefined : { top: single, right: single, bottom: single, left: single };
|
|
2347
|
+
}
|
|
2348
|
+
if (parsed.length === 2) {
|
|
2349
|
+
const vertical = clampCssBoxValue(parsed[0]);
|
|
2350
|
+
const horizontal = clampCssBoxValue(parsed[1]);
|
|
2351
|
+
if (vertical === undefined || horizontal === undefined) return undefined;
|
|
2352
|
+
return { top: vertical, right: horizontal, bottom: vertical, left: horizontal };
|
|
2353
|
+
}
|
|
2354
|
+
if (parsed.length === 3) {
|
|
2355
|
+
const top = clampCssBoxValue(parsed[0]);
|
|
2356
|
+
const horizontal = clampCssBoxValue(parsed[1]);
|
|
2357
|
+
const bottom = clampCssBoxValue(parsed[2]);
|
|
2358
|
+
if (top === undefined || horizontal === undefined || bottom === undefined) return undefined;
|
|
2359
|
+
return { top, right: horizontal, bottom, left: horizontal };
|
|
2360
|
+
}
|
|
2361
|
+
const top = clampCssBoxValue(parsed[0]);
|
|
2362
|
+
const right = clampCssBoxValue(parsed[1]);
|
|
2363
|
+
const bottom = clampCssBoxValue(parsed[2]);
|
|
2364
|
+
const left = clampCssBoxValue(parsed[3]);
|
|
2365
|
+
if (top === undefined || right === undefined || bottom === undefined || left === undefined) return undefined;
|
|
2366
|
+
return { top, right, bottom, left };
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2061
2369
|
function parseDiagramStylesheet(options: Record<string, unknown> | undefined): DiagramStyleRule[] {
|
|
2062
2370
|
const stylesheet = options?.stylesheet;
|
|
2063
2371
|
if (!Array.isArray(stylesheet)) {
|
|
@@ -2083,11 +2391,11 @@ function parseDiagramStylesheet(options: Record<string, unknown> | undefined): D
|
|
|
2083
2391
|
|
|
2084
2392
|
function parseStyleSelector(
|
|
2085
2393
|
selector: string
|
|
2086
|
-
): { elementKind: 'node' | 'compartment' | 'port' | 'edge'; className?: string; condition?: string } | undefined {
|
|
2087
|
-
const match = /^\s*(node|compartment|port|edge)(?:\.([A-Za-z0-9_-]+))?(?:\s*\[(.+)\]\s*)?$/i.exec(selector);
|
|
2394
|
+
): { elementKind: 'diagram' | 'node' | 'compartment' | 'port' | 'edge'; className?: string; condition?: string } | undefined {
|
|
2395
|
+
const match = /^\s*(diagram|node|compartment|port|edge)(?:\.([A-Za-z0-9_-]+))?(?:\s*\[(.+)\]\s*)?$/i.exec(selector);
|
|
2088
2396
|
if (!match) return undefined;
|
|
2089
2397
|
return {
|
|
2090
|
-
elementKind: match[1].toLowerCase() as 'node' | 'compartment' | 'port' | 'edge',
|
|
2398
|
+
elementKind: match[1].toLowerCase() as 'diagram' | 'node' | 'compartment' | 'port' | 'edge',
|
|
2091
2399
|
className: match[2]?.trim() || undefined,
|
|
2092
2400
|
condition: match[3]?.trim() || undefined,
|
|
2093
2401
|
};
|
|
@@ -2097,7 +2405,14 @@ function parseStyleSelector(
|
|
|
2097
2405
|
// These pass through as top-level keys so callers can read style.layout, style.width, etc.
|
|
2098
2406
|
const OML_PASSTHROUGH_STYLE_KEYS = new Set([
|
|
2099
2407
|
'layout', 'shape', 'width', 'height',
|
|
2408
|
+
'margin', 'padding', 'router', 'connector',
|
|
2409
|
+
]);
|
|
2410
|
+
|
|
2411
|
+
const LEGACY_BOX_SPACING_KEYS = new Set([
|
|
2412
|
+
'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
|
|
2100
2413
|
'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
|
|
2414
|
+
'margin-top', 'margin-bottom', 'margin-left', 'margin-right',
|
|
2415
|
+
'padding-top', 'padding-bottom', 'padding-left', 'padding-right',
|
|
2101
2416
|
]);
|
|
2102
2417
|
|
|
2103
2418
|
// Record-valued top-level style keys that map directly into the attrs sub-tree under the same name.
|
|
@@ -2111,7 +2426,7 @@ const ATTRS_RECORD_STYLE_KEYS = new Set([
|
|
|
2111
2426
|
]);
|
|
2112
2427
|
|
|
2113
2428
|
function normalizeDiagramStyle(
|
|
2114
|
-
elementKind: 'node' | 'compartment' | 'port' | 'edge',
|
|
2429
|
+
elementKind: 'diagram' | 'node' | 'compartment' | 'port' | 'edge',
|
|
2115
2430
|
raw: Record<string, unknown>
|
|
2116
2431
|
): Record<string, unknown> {
|
|
2117
2432
|
const isEdge = elementKind === 'edge';
|
|
@@ -2126,6 +2441,10 @@ function normalizeDiagramStyle(
|
|
|
2126
2441
|
const key = rawKey.trim();
|
|
2127
2442
|
if (!key || value === undefined || value === null) continue;
|
|
2128
2443
|
|
|
2444
|
+
if (LEGACY_BOX_SPACING_KEYS.has(key)) {
|
|
2445
|
+
continue;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2129
2448
|
// OML node-level keys: pass through to the top level of the normalized style object
|
|
2130
2449
|
if (OML_PASSTHROUGH_STYLE_KEYS.has(key)) { passthrough[key] = value; continue; }
|
|
2131
2450
|
|
|
@@ -2176,7 +2495,7 @@ function normalizeDiagramStyle(
|
|
|
2176
2495
|
}
|
|
2177
2496
|
|
|
2178
2497
|
function resolveElementStyle(
|
|
2179
|
-
elementKind: 'node' | 'compartment' | 'port' | 'edge',
|
|
2498
|
+
elementKind: 'diagram' | 'node' | 'compartment' | 'port' | 'edge',
|
|
2180
2499
|
value: string,
|
|
2181
2500
|
classes: string[],
|
|
2182
2501
|
properties: Record<string, string[]>,
|
|
@@ -2414,6 +2733,19 @@ function resolveEdgeLineAttrs(style: Record<string, unknown>): Record<string, un
|
|
|
2414
2733
|
};
|
|
2415
2734
|
}
|
|
2416
2735
|
|
|
2736
|
+
function resolveEdgeRouter(style: Record<string, unknown>): Record<string, unknown> {
|
|
2737
|
+
const router = asRecord(style.router);
|
|
2738
|
+
return router ?? { name: 'normal' };
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
function resolveEdgeConnector(style: Record<string, unknown>): Record<string, unknown> {
|
|
2742
|
+
const connector = asRecord(style.connector);
|
|
2743
|
+
return connector ?? {
|
|
2744
|
+
name: 'jumpover',
|
|
2745
|
+
args: { size: 5 },
|
|
2746
|
+
};
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2417
2749
|
function resolveEdgeLabelAttrs(
|
|
2418
2750
|
style: Record<string, unknown>,
|
|
2419
2751
|
placement: EdgeLabel['placement'],
|
|
@@ -2444,19 +2776,49 @@ function resolveEdgeLabelBodyAttrs(
|
|
|
2444
2776
|
fillOpacity: 0.9,
|
|
2445
2777
|
stroke: 'none',
|
|
2446
2778
|
strokeWidth: 0,
|
|
2779
|
+
pointerEvents: 'all',
|
|
2447
2780
|
...(base ?? {}),
|
|
2448
2781
|
...(specific ?? {}),
|
|
2449
2782
|
};
|
|
2450
2783
|
}
|
|
2451
2784
|
|
|
2452
|
-
function resolvePortAttrs(
|
|
2785
|
+
function resolvePortAttrs(
|
|
2786
|
+
style: Record<string, unknown>,
|
|
2787
|
+
classes: string[],
|
|
2788
|
+
text: string | undefined,
|
|
2789
|
+
side: PortSide = 'right',
|
|
2790
|
+
ownerStroke?: string
|
|
2791
|
+
): Record<string, unknown> {
|
|
2453
2792
|
const attrs = extractStyleAttrs(style);
|
|
2454
2793
|
const body = asRecord(attrs.body);
|
|
2455
2794
|
const icon = asRecord(attrs.icon);
|
|
2456
2795
|
const label = asRecord(attrs.label);
|
|
2457
2796
|
const imageUrl = extractImageHrefFromIcon(icon);
|
|
2797
|
+
const labelPosition = side === 'left'
|
|
2798
|
+
? {
|
|
2799
|
+
textAnchor: 'end',
|
|
2800
|
+
x: -10,
|
|
2801
|
+
dy: '0.9em',
|
|
2802
|
+
}
|
|
2803
|
+
: side === 'top'
|
|
2804
|
+
? {
|
|
2805
|
+
textAnchor: 'middle',
|
|
2806
|
+
x: 0,
|
|
2807
|
+
dy: '-0.3em',
|
|
2808
|
+
}
|
|
2809
|
+
: side === 'bottom'
|
|
2810
|
+
? {
|
|
2811
|
+
textAnchor: 'middle',
|
|
2812
|
+
x: 0,
|
|
2813
|
+
dy: '1.4em',
|
|
2814
|
+
}
|
|
2815
|
+
: {
|
|
2816
|
+
textAnchor: 'start',
|
|
2817
|
+
x: 10,
|
|
2818
|
+
dy: '0.9em',
|
|
2819
|
+
};
|
|
2458
2820
|
|
|
2459
|
-
|
|
2821
|
+
const result: Record<string, unknown> = {
|
|
2460
2822
|
body: {
|
|
2461
2823
|
width: 12,
|
|
2462
2824
|
height: 12,
|
|
@@ -2464,7 +2826,7 @@ function resolvePortAttrs(style: Record<string, unknown>, classes: string[], tex
|
|
|
2464
2826
|
y: -6,
|
|
2465
2827
|
class: ['oml-port-body', ...classes].join(' '),
|
|
2466
2828
|
magnet: false,
|
|
2467
|
-
stroke:
|
|
2829
|
+
stroke: ownerStroke ?? CSS_EDITOR_FOREGROUND,
|
|
2468
2830
|
strokeWidth: 1,
|
|
2469
2831
|
fill: CSS_EDITOR_BACKGROUND,
|
|
2470
2832
|
...(body ?? {}),
|
|
@@ -2479,17 +2841,196 @@ function resolvePortAttrs(style: Record<string, unknown>, classes: string[], tex
|
|
|
2479
2841
|
...(icon ?? {}),
|
|
2480
2842
|
...(imageUrl ? { href: imageUrl, xlinkHref: imageUrl, 'xlink:href': imageUrl } : {}),
|
|
2481
2843
|
},
|
|
2482
|
-
|
|
2844
|
+
};
|
|
2845
|
+
if (text) {
|
|
2846
|
+
result.label = {
|
|
2483
2847
|
text,
|
|
2484
2848
|
fill: CSS_EDITOR_FOREGROUND,
|
|
2485
2849
|
fontFamily: 'var(--vscode-editor-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)',
|
|
2486
2850
|
fontSize: 12,
|
|
2487
|
-
|
|
2488
|
-
x: 10,
|
|
2489
|
-
dy: '0.9em',
|
|
2851
|
+
...labelPosition,
|
|
2490
2852
|
...(label ?? {}),
|
|
2491
|
-
}
|
|
2853
|
+
};
|
|
2854
|
+
}
|
|
2855
|
+
return result;
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
function resolveBorderPortLabelPosition(side: PortSide): { name: PortSide } {
|
|
2859
|
+
return { name: side };
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
function clamp(value: number, min: number, max: number): number {
|
|
2863
|
+
return Math.max(min, Math.min(max, value));
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
function computeDefaultPortPlacements(
|
|
2867
|
+
graph: { nodes: NodeSpec[]; edges: EdgeSpec[]; roots: string[] },
|
|
2868
|
+
nodeById: Map<string, NodeSpec>,
|
|
2869
|
+
portsByOwner: Map<string, NodeSpec[]>,
|
|
2870
|
+
boxes: Map<string, NodeBox>
|
|
2871
|
+
): Map<string, PortPlacement> {
|
|
2872
|
+
const placements = new Map<string, PortPlacement>();
|
|
2873
|
+
const ownerCenter = (nodeId: string): { x: number; y: number } | undefined => {
|
|
2874
|
+
const box = boxes.get(nodeId);
|
|
2875
|
+
if (!box) {
|
|
2876
|
+
return undefined;
|
|
2877
|
+
}
|
|
2878
|
+
return { x: box.x + (box.width / 2), y: box.y + (box.height / 2) };
|
|
2879
|
+
};
|
|
2880
|
+
const portIdsByOwner = new Map<string, Set<string>>();
|
|
2881
|
+
for (const [ownerId, ports] of portsByOwner.entries()) {
|
|
2882
|
+
portIdsByOwner.set(ownerId, new Set(ports.map((port) => port.id)));
|
|
2883
|
+
}
|
|
2884
|
+
const peerPortIdsByPort = new Map<string, string[]>();
|
|
2885
|
+
const peerCentersByPort = new Map<string, Array<{ x: number; y: number }>>();
|
|
2886
|
+
for (const edge of graph.edges) {
|
|
2887
|
+
const source = nodeById.get(edge.sourceId);
|
|
2888
|
+
const target = nodeById.get(edge.targetId);
|
|
2889
|
+
const sourceOwnerId = source?.kind === 'Port' ? source.parentId : source?.id;
|
|
2890
|
+
const targetOwnerId = target?.kind === 'Port' ? target.parentId : target?.id;
|
|
2891
|
+
if (!sourceOwnerId || !targetOwnerId || sourceOwnerId === targetOwnerId) {
|
|
2892
|
+
continue;
|
|
2893
|
+
}
|
|
2894
|
+
const sourcePeer = ownerCenter(targetOwnerId);
|
|
2895
|
+
const targetPeer = ownerCenter(sourceOwnerId);
|
|
2896
|
+
if (source?.kind === 'Port' && sourcePeer) {
|
|
2897
|
+
const peers = peerCentersByPort.get(source.id) ?? [];
|
|
2898
|
+
peers.push(sourcePeer);
|
|
2899
|
+
peerCentersByPort.set(source.id, peers);
|
|
2900
|
+
if (target?.kind === 'Port') {
|
|
2901
|
+
const peerPortIds = peerPortIdsByPort.get(source.id) ?? [];
|
|
2902
|
+
peerPortIds.push(target.id);
|
|
2903
|
+
peerPortIdsByPort.set(source.id, peerPortIds);
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
if (target?.kind === 'Port' && targetPeer) {
|
|
2907
|
+
const peers = peerCentersByPort.get(target.id) ?? [];
|
|
2908
|
+
peers.push(targetPeer);
|
|
2909
|
+
peerCentersByPort.set(target.id, peers);
|
|
2910
|
+
if (source?.kind === 'Port') {
|
|
2911
|
+
const peerPortIds = peerPortIdsByPort.get(target.id) ?? [];
|
|
2912
|
+
peerPortIds.push(source.id);
|
|
2913
|
+
peerPortIdsByPort.set(target.id, peerPortIds);
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
const anchorFor = (ownerId: string, side: PortSide, ratio: number): { x: number; y: number } | undefined => {
|
|
2919
|
+
const box = boxes.get(ownerId);
|
|
2920
|
+
if (!box) {
|
|
2921
|
+
return undefined;
|
|
2922
|
+
}
|
|
2923
|
+
const clampedRatio = clamp(ratio, 0.05, 0.95);
|
|
2924
|
+
if (side === 'left') {
|
|
2925
|
+
return { x: box.x, y: box.y + (box.height * clampedRatio) };
|
|
2926
|
+
}
|
|
2927
|
+
if (side === 'right') {
|
|
2928
|
+
return { x: box.x + box.width, y: box.y + (box.height * clampedRatio) };
|
|
2929
|
+
}
|
|
2930
|
+
if (side === 'top') {
|
|
2931
|
+
return { x: box.x + (box.width * clampedRatio), y: box.y };
|
|
2932
|
+
}
|
|
2933
|
+
return { x: box.x + (box.width * clampedRatio), y: box.y + box.height };
|
|
2934
|
+
};
|
|
2935
|
+
|
|
2936
|
+
const currentAnchorForPort = (portId: string): { x: number; y: number } | undefined => {
|
|
2937
|
+
const port = nodeById.get(portId);
|
|
2938
|
+
if (!port?.parentId) {
|
|
2939
|
+
return undefined;
|
|
2940
|
+
}
|
|
2941
|
+
const placement = placements.get(portId) ?? { side: 'right' as PortSide, ratio: 0.5 };
|
|
2942
|
+
return anchorFor(port.parentId, placement.side, placement.ratio);
|
|
2492
2943
|
};
|
|
2944
|
+
|
|
2945
|
+
const peerAnchorsForPort = (portId: string): Array<{ x: number; y: number }> => {
|
|
2946
|
+
const peerAnchors: Array<{ x: number; y: number }> = [];
|
|
2947
|
+
for (const peerPortId of peerPortIdsByPort.get(portId) ?? []) {
|
|
2948
|
+
const anchor = currentAnchorForPort(peerPortId);
|
|
2949
|
+
if (anchor) {
|
|
2950
|
+
peerAnchors.push(anchor);
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
for (const peerCenter of peerCentersByPort.get(portId) ?? []) {
|
|
2954
|
+
peerAnchors.push(peerCenter);
|
|
2955
|
+
}
|
|
2956
|
+
return peerAnchors;
|
|
2957
|
+
};
|
|
2958
|
+
|
|
2959
|
+
const candidateCost = (ownerId: string, side: PortSide, portId: string): number => {
|
|
2960
|
+
const owner = ownerCenter(ownerId);
|
|
2961
|
+
const candidate = anchorFor(ownerId, side, 0.5);
|
|
2962
|
+
if (!owner || !candidate) {
|
|
2963
|
+
return Number.POSITIVE_INFINITY;
|
|
2964
|
+
}
|
|
2965
|
+
const peerAnchors = peerAnchorsForPort(portId);
|
|
2966
|
+
if (peerAnchors.length === 0) {
|
|
2967
|
+
return side === 'right' ? 0 : 1;
|
|
2968
|
+
}
|
|
2969
|
+
let cost = 0;
|
|
2970
|
+
for (const peer of peerAnchors) {
|
|
2971
|
+
cost += Math.abs(candidate.x - peer.x) + Math.abs(candidate.y - peer.y);
|
|
2972
|
+
}
|
|
2973
|
+
return cost;
|
|
2974
|
+
};
|
|
2975
|
+
|
|
2976
|
+
const sideOrder: PortSide[] = ['right', 'left', 'top', 'bottom'];
|
|
2977
|
+
const primaryCoordinate = (side: PortSide, portId: string): number => {
|
|
2978
|
+
const peerAnchors = peerAnchorsForPort(portId);
|
|
2979
|
+
if (peerAnchors.length === 0) {
|
|
2980
|
+
return 0;
|
|
2981
|
+
}
|
|
2982
|
+
const values = peerAnchors.map((peer) => ((side === 'left' || side === 'right') ? peer.y : peer.x));
|
|
2983
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
2984
|
+
};
|
|
2985
|
+
|
|
2986
|
+
for (const [ownerId, ports] of portsByOwner.entries()) {
|
|
2987
|
+
const count = ports.length;
|
|
2988
|
+
for (let index = 0; index < count; index += 1) {
|
|
2989
|
+
placements.set(ports[index].id, {
|
|
2990
|
+
side: 'right',
|
|
2991
|
+
ratio: count <= 0 ? 0.5 : ((index + 1) / (count + 1)),
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
const ownerPortIds = portIdsByOwner.get(ownerId) ?? new Set<string>();
|
|
2995
|
+
for (let pass = 0; pass < 2; pass += 1) {
|
|
2996
|
+
for (const port of ports) {
|
|
2997
|
+
let bestSide: PortSide = 'right';
|
|
2998
|
+
let bestCost = Number.POSITIVE_INFINITY;
|
|
2999
|
+
for (const side of sideOrder) {
|
|
3000
|
+
const cost = candidateCost(ownerId, side, port.id);
|
|
3001
|
+
if (cost < bestCost || (cost === bestCost && sideOrder.indexOf(side) < sideOrder.indexOf(bestSide))) {
|
|
3002
|
+
bestCost = cost;
|
|
3003
|
+
bestSide = side;
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
const existing = placements.get(port.id) ?? { ratio: 0.5, side: bestSide };
|
|
3007
|
+
placements.set(port.id, { side: bestSide, ratio: existing.ratio });
|
|
3008
|
+
}
|
|
3009
|
+
const sideGroups = new Map<PortSide, NodeSpec[]>([
|
|
3010
|
+
['left', []],
|
|
3011
|
+
['right', []],
|
|
3012
|
+
['top', []],
|
|
3013
|
+
['bottom', []],
|
|
3014
|
+
]);
|
|
3015
|
+
for (const port of ports) {
|
|
3016
|
+
sideGroups.get(placements.get(port.id)?.side ?? 'right')?.push(port);
|
|
3017
|
+
}
|
|
3018
|
+
for (const [side, sidePorts] of sideGroups.entries()) {
|
|
3019
|
+
sidePorts.sort((left, right) => (
|
|
3020
|
+
primaryCoordinate(side, left.id) - primaryCoordinate(side, right.id)
|
|
3021
|
+
|| left.id.localeCompare(right.id)
|
|
3022
|
+
));
|
|
3023
|
+
for (let index = 0; index < sidePorts.length; index += 1) {
|
|
3024
|
+
placements.set(sidePorts[index].id, {
|
|
3025
|
+
side,
|
|
3026
|
+
ratio: (index + 1) / (sidePorts.length + 1),
|
|
3027
|
+
});
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
void ownerPortIds;
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
return placements;
|
|
2493
3034
|
}
|
|
2494
3035
|
|
|
2495
3036
|
function toPositiveNumber(value: unknown): number | undefined {
|