@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
|
@@ -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,62 +2139,44 @@ 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
2181
|
return displayLabelFromIri(value);
|
|
1991
2182
|
}
|
|
@@ -2042,9 +2233,7 @@ function resolveDagreLayoutOptions(options: Record<string, unknown> | undefined)
|
|
|
2042
2233
|
|
|
2043
2234
|
const nodesep = clampLayoutNumber(layout.nodesep, 0, 400, 28);
|
|
2044
2235
|
const ranksep = clampLayoutNumber(layout.ranksep, 0, 500, 64);
|
|
2045
|
-
|
|
2046
|
-
const marginy = clampLayoutNumber(layout.marginy, 0, 200, 16);
|
|
2047
|
-
return { rankdir, nodesep, ranksep, marginx, marginy };
|
|
2236
|
+
return { rankdir, nodesep, ranksep };
|
|
2048
2237
|
}
|
|
2049
2238
|
|
|
2050
2239
|
function clampLayoutNumber(value: unknown, min: number, max: number, fallback: number): number {
|
|
@@ -2054,6 +2243,129 @@ function clampLayoutNumber(value: unknown, min: number, max: number, fallback: n
|
|
|
2054
2243
|
return Math.max(min, Math.min(max, Math.round(value)));
|
|
2055
2244
|
}
|
|
2056
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
|
+
|
|
2057
2369
|
function parseDiagramStylesheet(options: Record<string, unknown> | undefined): DiagramStyleRule[] {
|
|
2058
2370
|
const stylesheet = options?.stylesheet;
|
|
2059
2371
|
if (!Array.isArray(stylesheet)) {
|
|
@@ -2079,11 +2391,11 @@ function parseDiagramStylesheet(options: Record<string, unknown> | undefined): D
|
|
|
2079
2391
|
|
|
2080
2392
|
function parseStyleSelector(
|
|
2081
2393
|
selector: string
|
|
2082
|
-
): { elementKind: 'node' | 'compartment' | 'port' | 'edge'; className?: string; condition?: string } | undefined {
|
|
2083
|
-
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);
|
|
2084
2396
|
if (!match) return undefined;
|
|
2085
2397
|
return {
|
|
2086
|
-
elementKind: match[1].toLowerCase() as 'node' | 'compartment' | 'port' | 'edge',
|
|
2398
|
+
elementKind: match[1].toLowerCase() as 'diagram' | 'node' | 'compartment' | 'port' | 'edge',
|
|
2087
2399
|
className: match[2]?.trim() || undefined,
|
|
2088
2400
|
condition: match[3]?.trim() || undefined,
|
|
2089
2401
|
};
|
|
@@ -2093,7 +2405,14 @@ function parseStyleSelector(
|
|
|
2093
2405
|
// These pass through as top-level keys so callers can read style.layout, style.width, etc.
|
|
2094
2406
|
const OML_PASSTHROUGH_STYLE_KEYS = new Set([
|
|
2095
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',
|
|
2096
2413
|
'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
|
|
2414
|
+
'margin-top', 'margin-bottom', 'margin-left', 'margin-right',
|
|
2415
|
+
'padding-top', 'padding-bottom', 'padding-left', 'padding-right',
|
|
2097
2416
|
]);
|
|
2098
2417
|
|
|
2099
2418
|
// Record-valued top-level style keys that map directly into the attrs sub-tree under the same name.
|
|
@@ -2107,7 +2426,7 @@ const ATTRS_RECORD_STYLE_KEYS = new Set([
|
|
|
2107
2426
|
]);
|
|
2108
2427
|
|
|
2109
2428
|
function normalizeDiagramStyle(
|
|
2110
|
-
elementKind: 'node' | 'compartment' | 'port' | 'edge',
|
|
2429
|
+
elementKind: 'diagram' | 'node' | 'compartment' | 'port' | 'edge',
|
|
2111
2430
|
raw: Record<string, unknown>
|
|
2112
2431
|
): Record<string, unknown> {
|
|
2113
2432
|
const isEdge = elementKind === 'edge';
|
|
@@ -2122,6 +2441,10 @@ function normalizeDiagramStyle(
|
|
|
2122
2441
|
const key = rawKey.trim();
|
|
2123
2442
|
if (!key || value === undefined || value === null) continue;
|
|
2124
2443
|
|
|
2444
|
+
if (LEGACY_BOX_SPACING_KEYS.has(key)) {
|
|
2445
|
+
continue;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2125
2448
|
// OML node-level keys: pass through to the top level of the normalized style object
|
|
2126
2449
|
if (OML_PASSTHROUGH_STYLE_KEYS.has(key)) { passthrough[key] = value; continue; }
|
|
2127
2450
|
|
|
@@ -2172,7 +2495,7 @@ function normalizeDiagramStyle(
|
|
|
2172
2495
|
}
|
|
2173
2496
|
|
|
2174
2497
|
function resolveElementStyle(
|
|
2175
|
-
elementKind: 'node' | 'compartment' | 'port' | 'edge',
|
|
2498
|
+
elementKind: 'diagram' | 'node' | 'compartment' | 'port' | 'edge',
|
|
2176
2499
|
value: string,
|
|
2177
2500
|
classes: string[],
|
|
2178
2501
|
properties: Record<string, string[]>,
|
|
@@ -2410,6 +2733,19 @@ function resolveEdgeLineAttrs(style: Record<string, unknown>): Record<string, un
|
|
|
2410
2733
|
};
|
|
2411
2734
|
}
|
|
2412
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
|
+
|
|
2413
2749
|
function resolveEdgeLabelAttrs(
|
|
2414
2750
|
style: Record<string, unknown>,
|
|
2415
2751
|
placement: EdgeLabel['placement'],
|
|
@@ -2440,19 +2776,49 @@ function resolveEdgeLabelBodyAttrs(
|
|
|
2440
2776
|
fillOpacity: 0.9,
|
|
2441
2777
|
stroke: 'none',
|
|
2442
2778
|
strokeWidth: 0,
|
|
2779
|
+
pointerEvents: 'all',
|
|
2443
2780
|
...(base ?? {}),
|
|
2444
2781
|
...(specific ?? {}),
|
|
2445
2782
|
};
|
|
2446
2783
|
}
|
|
2447
2784
|
|
|
2448
|
-
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> {
|
|
2449
2792
|
const attrs = extractStyleAttrs(style);
|
|
2450
2793
|
const body = asRecord(attrs.body);
|
|
2451
2794
|
const icon = asRecord(attrs.icon);
|
|
2452
2795
|
const label = asRecord(attrs.label);
|
|
2453
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
|
+
};
|
|
2454
2820
|
|
|
2455
|
-
|
|
2821
|
+
const result: Record<string, unknown> = {
|
|
2456
2822
|
body: {
|
|
2457
2823
|
width: 12,
|
|
2458
2824
|
height: 12,
|
|
@@ -2460,7 +2826,7 @@ function resolvePortAttrs(style: Record<string, unknown>, classes: string[], tex
|
|
|
2460
2826
|
y: -6,
|
|
2461
2827
|
class: ['oml-port-body', ...classes].join(' '),
|
|
2462
2828
|
magnet: false,
|
|
2463
|
-
stroke:
|
|
2829
|
+
stroke: ownerStroke ?? CSS_EDITOR_FOREGROUND,
|
|
2464
2830
|
strokeWidth: 1,
|
|
2465
2831
|
fill: CSS_EDITOR_BACKGROUND,
|
|
2466
2832
|
...(body ?? {}),
|
|
@@ -2475,17 +2841,196 @@ function resolvePortAttrs(style: Record<string, unknown>, classes: string[], tex
|
|
|
2475
2841
|
...(icon ?? {}),
|
|
2476
2842
|
...(imageUrl ? { href: imageUrl, xlinkHref: imageUrl, 'xlink:href': imageUrl } : {}),
|
|
2477
2843
|
},
|
|
2478
|
-
|
|
2844
|
+
};
|
|
2845
|
+
if (text) {
|
|
2846
|
+
result.label = {
|
|
2479
2847
|
text,
|
|
2480
2848
|
fill: CSS_EDITOR_FOREGROUND,
|
|
2481
2849
|
fontFamily: 'var(--vscode-editor-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)',
|
|
2482
2850
|
fontSize: 12,
|
|
2483
|
-
|
|
2484
|
-
x: 10,
|
|
2485
|
-
dy: '0.9em',
|
|
2851
|
+
...labelPosition,
|
|
2486
2852
|
...(label ?? {}),
|
|
2487
|
-
}
|
|
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);
|
|
2488
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;
|
|
2489
3034
|
}
|
|
2490
3035
|
|
|
2491
3036
|
function toPositiveNumber(value: unknown): number | undefined {
|