@oml/markdown 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/md/md-execution.d.ts +16 -0
- package/out/md/md-executor.d.ts +1 -0
- package/out/md/md-executor.js +219 -35
- package/out/md/md-executor.js.map +1 -1
- package/out/renderers/chart-renderer.js +72 -4
- package/out/renderers/chart-renderer.js.map +1 -1
- package/out/renderers/diagram-renderer.js +896 -245
- package/out/renderers/diagram-renderer.js.map +1 -1
- package/out/renderers/graph-renderer.js +452 -18
- package/out/renderers/graph-renderer.js.map +1 -1
- package/out/renderers/matrix-renderer.d.ts +0 -2
- package/out/renderers/matrix-renderer.js +45 -40
- package/out/renderers/matrix-renderer.js.map +1 -1
- package/out/renderers/renderer.d.ts +4 -1
- package/out/renderers/renderer.js +98 -0
- package/out/renderers/renderer.js.map +1 -1
- package/out/renderers/table-renderer.d.ts +12 -2
- package/out/renderers/table-renderer.js +126 -39
- package/out/renderers/table-renderer.js.map +1 -1
- package/out/renderers/types.d.ts +16 -0
- package/out/renderers/wikilink-utils.d.ts +1 -0
- package/out/renderers/wikilink-utils.js +60 -32
- package/out/renderers/wikilink-utils.js.map +1 -1
- package/out/static/browser-runtime.bundle.js +8011 -1292
- package/out/static/browser-runtime.bundle.js.map +4 -4
- package/out/static/browser-runtime.js +15 -2
- package/out/static/browser-runtime.js.map +1 -1
- package/package.json +2 -2
- package/src/md/md-execution.ts +20 -0
- package/src/md/md-executor.ts +268 -40
- package/src/renderers/chart-renderer.ts +93 -2
- package/src/renderers/diagram-renderer.ts +964 -253
- package/src/renderers/graph-renderer.ts +512 -12
- package/src/renderers/matrix-renderer.ts +57 -44
- package/src/renderers/renderer.ts +105 -1
- package/src/renderers/table-renderer.ts +190 -41
- package/src/renderers/types.ts +20 -0
- package/src/renderers/wikilink-utils.ts +66 -31
- package/src/static/browser-runtime.ts +20 -2
- package/src/static/markdown-webview.css +44 -15
|
@@ -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';
|
|
@@ -100,7 +109,7 @@ const TYPE_IRIS: Readonly<Record<NodeKind | 'Edge', string>> = {
|
|
|
100
109
|
};
|
|
101
110
|
const LIST_ITEM_IRI = `${D}ListItem`;
|
|
102
111
|
const CSS_EDITOR_FOREGROUND = 'var(--vscode-editor-foreground, var(--oml-static-foreground, #24292f))';
|
|
103
|
-
const CSS_EDITOR_BACKGROUND = 'var(--vscode-editor-background,
|
|
112
|
+
const CSS_EDITOR_BACKGROUND = 'var(--vscode-editor-background, var(--oml-static-background, #ffffff))';
|
|
104
113
|
const CSS_CANVAS_BACKGROUND = 'var(--vscode-editor-background, var(--oml-static-background, #ffffff))';
|
|
105
114
|
const CSS_FOCUS_BORDER = 'var(--vscode-focusBorder, var(--oml-static-link, #0969da))';
|
|
106
115
|
|
|
@@ -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,
|
|
@@ -233,10 +242,14 @@ async function renderWithX6(
|
|
|
233
242
|
minScale: 0.4,
|
|
234
243
|
maxScale: 2.5,
|
|
235
244
|
factor: 1.1,
|
|
245
|
+
modifiers: ['meta', 'ctrl'],
|
|
236
246
|
},
|
|
237
247
|
connecting: {
|
|
238
248
|
router: 'normal',
|
|
239
|
-
connector:
|
|
249
|
+
connector: {
|
|
250
|
+
name: 'jumpover',
|
|
251
|
+
args: { size: 5 },
|
|
252
|
+
},
|
|
240
253
|
allowBlank: false,
|
|
241
254
|
allowNode: false,
|
|
242
255
|
allowPort: false,
|
|
@@ -252,13 +265,130 @@ async function renderWithX6(
|
|
|
252
265
|
edgeMovable: false,
|
|
253
266
|
vertexMovable: false,
|
|
254
267
|
arrowheadMovable: false,
|
|
255
|
-
labelMovable:
|
|
268
|
+
labelMovable: true,
|
|
256
269
|
};
|
|
257
270
|
},
|
|
258
271
|
background: {
|
|
259
272
|
color: CSS_CANVAS_BACKGROUND,
|
|
260
273
|
},
|
|
274
|
+
onPortRendered: ({ port, node, container }: { port: { id: string }; node: { id: string }; container: HTMLElement }) => {
|
|
275
|
+
if (boundPortContainers.has(container)) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
boundPortContainers.add(container);
|
|
279
|
+
const portIri = String(port.id ?? '').trim();
|
|
280
|
+
const applyPortNativeTitle = (): void => {
|
|
281
|
+
if (!portIri) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
container.setAttribute('title', portIri);
|
|
285
|
+
for (const element of Array.from(container.querySelectorAll<HTMLElement | SVGElement>('*'))) {
|
|
286
|
+
element.setAttribute('title', portIri);
|
|
287
|
+
if (element instanceof SVGElement) {
|
|
288
|
+
let titleNode: SVGTitleElement | null = null;
|
|
289
|
+
for (const child of Array.from(element.children)) {
|
|
290
|
+
if (child instanceof SVGTitleElement) {
|
|
291
|
+
titleNode = child;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (!titleNode) {
|
|
296
|
+
titleNode = document.createElementNS('http://www.w3.org/2000/svg', 'title');
|
|
297
|
+
element.insertBefore(titleNode, element.firstChild);
|
|
298
|
+
}
|
|
299
|
+
titleNode.textContent = portIri;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
container.setAttribute('data-port-id', String(port.id));
|
|
304
|
+
applyPortNativeTitle();
|
|
305
|
+
container.style.cursor = 'grab';
|
|
306
|
+
container.style.touchAction = 'none';
|
|
307
|
+
container.addEventListener('mouseenter', (event: MouseEvent) => {
|
|
308
|
+
const iri = portIri;
|
|
309
|
+
if (!iri) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
applyPortNativeTitle();
|
|
313
|
+
const rect = container.getBoundingClientRect();
|
|
314
|
+
liveCanvas.dispatchEvent(new CustomEvent('md-show-iri-hover', {
|
|
315
|
+
bubbles: true,
|
|
316
|
+
detail: {
|
|
317
|
+
iri,
|
|
318
|
+
previewEnabled: /^Mac/i.test(navigator.platform) ? event.metaKey : event.ctrlKey,
|
|
319
|
+
anchorRect: {
|
|
320
|
+
left: rect.left,
|
|
321
|
+
right: rect.right,
|
|
322
|
+
top: rect.top,
|
|
323
|
+
bottom: rect.bottom,
|
|
324
|
+
width: rect.width,
|
|
325
|
+
height: rect.height,
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
}));
|
|
329
|
+
});
|
|
330
|
+
container.addEventListener('mouseleave', () => {
|
|
331
|
+
liveCanvas.dispatchEvent(new CustomEvent('md-hide-iri-hover', { bubbles: true }));
|
|
332
|
+
});
|
|
333
|
+
container.addEventListener('dblclick', (event: MouseEvent) => {
|
|
334
|
+
const iri = String(port.id ?? '').trim();
|
|
335
|
+
if (!iri) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
event.preventDefault();
|
|
339
|
+
event.stopPropagation();
|
|
340
|
+
if (typeof event.stopImmediatePropagation === 'function') {
|
|
341
|
+
event.stopImmediatePropagation();
|
|
342
|
+
}
|
|
343
|
+
liveCanvas.dispatchEvent(new CustomEvent('md-navigate-iri', {
|
|
344
|
+
bubbles: true,
|
|
345
|
+
detail: { iri },
|
|
346
|
+
}));
|
|
347
|
+
});
|
|
348
|
+
container.addEventListener('pointerdown', (event: PointerEvent) => {
|
|
349
|
+
if (typeof graphView.cleanSelection === 'function') {
|
|
350
|
+
graphView.cleanSelection();
|
|
351
|
+
}
|
|
352
|
+
nodeTransform.clearWidgets();
|
|
353
|
+
const owner = graphView.getCellById(String(node.id));
|
|
354
|
+
if (!owner) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
startPortDrag(event.clientX, event.clientY, owner, String(port.id), event.pointerId, container);
|
|
358
|
+
event.preventDefault();
|
|
359
|
+
event.stopPropagation();
|
|
360
|
+
if (typeof event.stopImmediatePropagation === 'function') {
|
|
361
|
+
event.stopImmediatePropagation();
|
|
362
|
+
}
|
|
363
|
+
if (typeof container.setPointerCapture === 'function') {
|
|
364
|
+
try {
|
|
365
|
+
container.setPointerCapture(event.pointerId);
|
|
366
|
+
} catch {
|
|
367
|
+
// Ignore capture failures; window listeners still handle the drag.
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
},
|
|
261
372
|
});
|
|
373
|
+
const x6Mod = await import('@antv/x6');
|
|
374
|
+
const TransformCtor = (x6Mod as any).Transform;
|
|
375
|
+
if (typeof TransformCtor !== 'function') {
|
|
376
|
+
throw new Error('X6 Transform plugin is unavailable in @antv/x6');
|
|
377
|
+
}
|
|
378
|
+
nodeTransform = new TransformCtor({
|
|
379
|
+
resizing: {
|
|
380
|
+
enabled: (node: any) => node?.getData?.()?.kind === 'Node',
|
|
381
|
+
minWidth: 48,
|
|
382
|
+
minHeight: 32,
|
|
383
|
+
orthogonal: true,
|
|
384
|
+
restrict: false,
|
|
385
|
+
autoScroll: false,
|
|
386
|
+
preserveAspectRatio: false,
|
|
387
|
+
allowReverse: false,
|
|
388
|
+
},
|
|
389
|
+
rotating: false,
|
|
390
|
+
});
|
|
391
|
+
graphView.use(nodeTransform);
|
|
262
392
|
const toPlainRect = (value: any): Record<string, number> | undefined => {
|
|
263
393
|
if (!value) return undefined;
|
|
264
394
|
const x = Number(value.x);
|
|
@@ -291,12 +421,7 @@ async function renderWithX6(
|
|
|
291
421
|
for (const list of portsByOwner.values()) {
|
|
292
422
|
list.sort((a, b) => a.id.localeCompare(b.id));
|
|
293
423
|
}
|
|
294
|
-
const
|
|
295
|
-
for (const [ownerId, ports] of portsByOwner.entries()) {
|
|
296
|
-
for (const port of ports) {
|
|
297
|
-
ownerByPortId.set(port.id, ownerId);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
424
|
+
const portPlacementById = computeDefaultPortPlacements(graph, nodeById, portsByOwner, layout.boxes);
|
|
300
425
|
const ordered = [...graph.nodes]
|
|
301
426
|
.filter((node) => node.kind !== 'Port')
|
|
302
427
|
.sort((a, b) => nodeDepth(a.id, nodeById) - nodeDepth(b.id, nodeById));
|
|
@@ -307,20 +432,41 @@ async function renderWithX6(
|
|
|
307
432
|
const labelText = node.labels.length > 0 ? node.labels.join('\n') : localName(node.id);
|
|
308
433
|
const resolvedStyle = node.style;
|
|
309
434
|
const resolvedShape = resolveRenderNodeShape(resolvedStyle);
|
|
435
|
+
const { bodyAttrs, labelAttrs, iconSvgAttrs, iconPathAttrs, imageAttrs } = resolveNodeAttrs(resolvedStyle);
|
|
310
436
|
const x = box.x;
|
|
311
437
|
const y = box.y;
|
|
312
438
|
const ownerPorts = portsByOwner.get(node.id) ?? [];
|
|
313
|
-
const portItems = ownerPorts.map((port
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
439
|
+
const portItems = ownerPorts.map((port) => {
|
|
440
|
+
const placement = portPlacementById.get(port.id) ?? { side: 'right' as PortSide, ratio: 0.5 };
|
|
441
|
+
const ratio = clamp(placement.ratio, 0.05, 0.95);
|
|
442
|
+
const position = placement.side === 'left' || placement.side === 'right'
|
|
443
|
+
? {
|
|
444
|
+
x: placement.side === 'left' ? 0 : box.width,
|
|
445
|
+
y: ratio * box.height,
|
|
446
|
+
}
|
|
447
|
+
: {
|
|
448
|
+
x: ratio * box.width,
|
|
449
|
+
y: placement.side === 'top' ? 0 : box.height,
|
|
450
|
+
};
|
|
451
|
+
const portText = port.labels[0];
|
|
452
|
+
return {
|
|
453
|
+
id: port.id,
|
|
454
|
+
group: 'boundary',
|
|
455
|
+
args: {
|
|
456
|
+
x: position.x,
|
|
457
|
+
y: position.y,
|
|
458
|
+
side: placement.side,
|
|
459
|
+
ratio,
|
|
460
|
+
},
|
|
461
|
+
attrs: resolvePortAttrs(
|
|
462
|
+
port.style,
|
|
463
|
+
port.classes,
|
|
464
|
+
portText,
|
|
465
|
+
placement.side,
|
|
466
|
+
typeof bodyAttrs.stroke === 'string' ? bodyAttrs.stroke : undefined
|
|
467
|
+
),
|
|
468
|
+
};
|
|
469
|
+
});
|
|
324
470
|
const iconPathSelectors = iconPathAttrs.map((_, index) => `iconPath${index}`);
|
|
325
471
|
graphView.addNode({
|
|
326
472
|
id: node.id,
|
|
@@ -424,7 +570,7 @@ async function renderWithX6(
|
|
|
424
570
|
},
|
|
425
571
|
items: portItems,
|
|
426
572
|
} : undefined,
|
|
427
|
-
zIndex:
|
|
573
|
+
zIndex: 50,
|
|
428
574
|
data: {
|
|
429
575
|
kind: node.kind,
|
|
430
576
|
ownerId: node.parentId,
|
|
@@ -561,10 +707,30 @@ async function renderWithX6(
|
|
|
561
707
|
let maxBottom = Number.NEGATIVE_INFINITY;
|
|
562
708
|
const childCells: any[] = [];
|
|
563
709
|
const childDebug: Array<Record<string, unknown>> = [];
|
|
710
|
+
const childGeometryBounds = (child: any): { x: number; y: number; width: number; height: number } | undefined => {
|
|
711
|
+
if (!child || typeof child.size !== 'function') {
|
|
712
|
+
return undefined;
|
|
713
|
+
}
|
|
714
|
+
const size = child.size();
|
|
715
|
+
const width = Number(size?.width);
|
|
716
|
+
const height = Number(size?.height);
|
|
717
|
+
let position: any;
|
|
718
|
+
if (typeof child.getPosition === 'function') {
|
|
719
|
+
position = child.getPosition();
|
|
720
|
+
} else {
|
|
721
|
+
position = child.position;
|
|
722
|
+
}
|
|
723
|
+
const x = Number(position?.x);
|
|
724
|
+
const y = Number(position?.y);
|
|
725
|
+
if (![x, y, width, height].every(Number.isFinite)) {
|
|
726
|
+
return undefined;
|
|
727
|
+
}
|
|
728
|
+
return { x, y, width, height };
|
|
729
|
+
};
|
|
564
730
|
for (const childId of childIds) {
|
|
565
731
|
const child = graphView.getCellById(childId);
|
|
566
|
-
|
|
567
|
-
|
|
732
|
+
const absBBox = childGeometryBounds(child);
|
|
733
|
+
if (!absBBox) continue;
|
|
568
734
|
const relLeft = absBBox.x - containerBBox.x;
|
|
569
735
|
const relTop = absBBox.y - containerBBox.y;
|
|
570
736
|
minLeft = Math.min(minLeft, relLeft);
|
|
@@ -596,10 +762,11 @@ async function renderWithX6(
|
|
|
596
762
|
// container origin (without moving its embedded children, which keep their
|
|
597
763
|
// absolute positions) and expand the size to compensate so the opposite edge
|
|
598
764
|
// is preserved.
|
|
599
|
-
|
|
600
|
-
const
|
|
601
|
-
const
|
|
602
|
-
const
|
|
765
|
+
const spacing = resolveBoxSpacing(containerSpec.style, 0);
|
|
766
|
+
const minInsetX = spacing.marginLeft + spacing.paddingLeft;
|
|
767
|
+
const minInsetY = spacing.marginTop + spacing.paddingTop;
|
|
768
|
+
const shiftX = minLeft < minInsetX ? minLeft - minInsetX : 0;
|
|
769
|
+
const shiftY = minTop < minInsetY ? minTop - minInsetY : 0;
|
|
603
770
|
// Dimensions needed relative to the (possibly shifted) new origin.
|
|
604
771
|
const baseWidth = containerSize.width - shiftX;
|
|
605
772
|
const baseHeight = containerSize.height - shiftY;
|
|
@@ -612,7 +779,8 @@ async function renderWithX6(
|
|
|
612
779
|
childIds,
|
|
613
780
|
containerBBox: toPlainRect(containerBBox),
|
|
614
781
|
containerSize: toPlainRect({ x: 0, y: 0, ...containerSize }),
|
|
615
|
-
|
|
782
|
+
minInsetX,
|
|
783
|
+
minInsetY,
|
|
616
784
|
bounds: { minLeft, minTop, maxRight, maxBottom },
|
|
617
785
|
shift: { shiftX, shiftY },
|
|
618
786
|
childDebug,
|
|
@@ -632,6 +800,7 @@ async function renderWithX6(
|
|
|
632
800
|
to: { width: neededWidth, height: neededHeight },
|
|
633
801
|
});
|
|
634
802
|
container.resize(neededWidth, neededHeight);
|
|
803
|
+
syncOwnedPortPositions(containerId);
|
|
635
804
|
} else {
|
|
636
805
|
logResize('container-no-resize', { containerId });
|
|
637
806
|
}
|
|
@@ -708,106 +877,149 @@ async function renderWithX6(
|
|
|
708
877
|
if (!isPrimaryMover(node, options)) return;
|
|
709
878
|
growAncestorContainers(node);
|
|
710
879
|
});
|
|
880
|
+
const syncOwnedPortPositions = (ownerId: string): void => {
|
|
881
|
+
const owner = graphView.getCellById(ownerId);
|
|
882
|
+
if (!owner || typeof owner.size !== 'function' || typeof owner.getPorts !== 'function' || typeof owner.setPortProp !== 'function') {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
const size = owner.size();
|
|
886
|
+
const portIds = (owner.getPorts() as any[])
|
|
887
|
+
.map((port) => String(port?.id ?? ''))
|
|
888
|
+
.filter((portId) => portId.length > 0);
|
|
889
|
+
for (const portId of portIds) {
|
|
890
|
+
const placement = portPlacementById.get(portId) ?? { side: 'right' as PortSide, ratio: 0.5 };
|
|
891
|
+
const ratio = clamp(placement.ratio, 0.05, 0.95);
|
|
892
|
+
const nextArgs = placement.side === 'left' || placement.side === 'right'
|
|
893
|
+
? {
|
|
894
|
+
x: placement.side === 'left' ? 0 : size.width,
|
|
895
|
+
y: ratio * size.height,
|
|
896
|
+
side: placement.side,
|
|
897
|
+
ratio,
|
|
898
|
+
}
|
|
899
|
+
: {
|
|
900
|
+
x: ratio * size.width,
|
|
901
|
+
y: placement.side === 'top' ? 0 : size.height,
|
|
902
|
+
side: placement.side,
|
|
903
|
+
ratio,
|
|
904
|
+
};
|
|
905
|
+
owner.setPortProp(portId, {
|
|
906
|
+
args: nextArgs,
|
|
907
|
+
label: {
|
|
908
|
+
position: resolveBorderPortLabelPosition(placement.side),
|
|
909
|
+
},
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
graphView.on('node:resizing', ({ node }: { node: any }) => {
|
|
914
|
+
syncOwnedPortPositions(String(node?.id ?? ''));
|
|
915
|
+
});
|
|
916
|
+
graphView.on('node:resized', ({ node, options }: { node: any; options?: Record<string, unknown> }) => {
|
|
917
|
+
if (options?.silent) return;
|
|
918
|
+
syncOwnedPortPositions(String(node?.id ?? ''));
|
|
919
|
+
growAncestorContainers(node);
|
|
920
|
+
});
|
|
711
921
|
|
|
712
922
|
const edgeLabelPosition = (placement: EdgeLabel['placement']): number => {
|
|
713
923
|
if (placement === 'begin') return 0.15;
|
|
714
924
|
if (placement === 'end') return 0.85;
|
|
715
925
|
return 0.5;
|
|
716
926
|
};
|
|
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]));
|
|
927
|
+
const directedPairKey = (sourceId: string, targetId: string): string => `${sourceId}=>${targetId}`;
|
|
928
|
+
const undirectedPairKey = (leftId: string, rightId: string): string => (
|
|
929
|
+
leftId < rightId ? `${leftId}<=>${rightId}` : `${rightId}<=>${leftId}`
|
|
930
|
+
);
|
|
931
|
+
const edgeIdsByPair = new Map<string, string[]>();
|
|
724
932
|
const undirectedEdgeIdsByPair = new Map<string, string[]>();
|
|
725
933
|
for (const edge of graph.edges) {
|
|
726
|
-
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
const key = undirectedPairKey(sourceOwner, targetOwner);
|
|
730
|
-
const ids = undirectedEdgeIdsByPair.get(key) ?? [];
|
|
934
|
+
if (!edge.sourceId || !edge.targetId) continue;
|
|
935
|
+
const key = directedPairKey(edge.sourceId, edge.targetId);
|
|
936
|
+
const ids = edgeIdsByPair.get(key) ?? [];
|
|
731
937
|
ids.push(edge.id);
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
if (edgeIds.length <= 1) {
|
|
938
|
+
edgeIdsByPair.set(key, ids);
|
|
939
|
+
const undirectedKey = undirectedPairKey(edge.sourceId, edge.targetId);
|
|
940
|
+
const pairIds = undirectedEdgeIdsByPair.get(undirectedKey) ?? [];
|
|
941
|
+
pairIds.push(edge.id);
|
|
942
|
+
undirectedEdgeIdsByPair.set(undirectedKey, pairIds);
|
|
943
|
+
}
|
|
944
|
+
const edgePointForEndpoint = (endpointId: string): { x: number; y: number } | undefined => {
|
|
945
|
+
const endpoint = nodeById.get(endpointId);
|
|
946
|
+
if (!endpoint) {
|
|
742
947
|
return undefined;
|
|
743
948
|
}
|
|
744
|
-
|
|
745
|
-
const
|
|
746
|
-
const
|
|
747
|
-
if (!
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
if (
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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);
|
|
949
|
+
if (endpoint.kind === 'Port' && endpoint.parentId) {
|
|
950
|
+
const ownerBox = layout.boxes.get(endpoint.parentId);
|
|
951
|
+
const placement = portPlacementById.get(endpoint.id);
|
|
952
|
+
if (!ownerBox || !placement) {
|
|
953
|
+
return undefined;
|
|
954
|
+
}
|
|
955
|
+
const ratio = clamp(placement.ratio, 0.05, 0.95);
|
|
956
|
+
if (placement.side === 'left') {
|
|
957
|
+
return { x: ownerBox.x, y: ownerBox.y + (ownerBox.height * ratio) };
|
|
958
|
+
}
|
|
959
|
+
if (placement.side === 'right') {
|
|
960
|
+
return { x: ownerBox.x + ownerBox.width, y: ownerBox.y + (ownerBox.height * ratio) };
|
|
961
|
+
}
|
|
962
|
+
if (placement.side === 'top') {
|
|
963
|
+
return { x: ownerBox.x + (ownerBox.width * ratio), y: ownerBox.y };
|
|
771
964
|
}
|
|
965
|
+
return { x: ownerBox.x + (ownerBox.width * ratio), y: ownerBox.y + ownerBox.height };
|
|
772
966
|
}
|
|
773
|
-
|
|
774
|
-
if (
|
|
967
|
+
const box = layout.boxes.get(endpointId);
|
|
968
|
+
if (!box) {
|
|
775
969
|
return undefined;
|
|
776
970
|
}
|
|
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;
|
|
971
|
+
return { x: box.x + (box.width / 2), y: box.y + (box.height / 2) };
|
|
972
|
+
};
|
|
973
|
+
const fanningVertexForEdge = (edge: EdgeSpec): Array<{ x: number; y: number }> | undefined => {
|
|
974
|
+
if (!edge.sourceId || !edge.targetId) {
|
|
975
|
+
return undefined;
|
|
789
976
|
}
|
|
790
|
-
|
|
977
|
+
const undirectedIds = undirectedEdgeIdsByPair.get(undirectedPairKey(edge.sourceId, edge.targetId)) ?? [];
|
|
978
|
+
if (undirectedIds.length <= 1) {
|
|
791
979
|
return undefined;
|
|
792
980
|
}
|
|
793
|
-
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
if (!pairABox || !pairBBox) {
|
|
981
|
+
const sourcePoint = edgePointForEndpoint(edge.sourceId);
|
|
982
|
+
const targetPoint = edgePointForEndpoint(edge.targetId);
|
|
983
|
+
if (!sourcePoint || !targetPoint) {
|
|
797
984
|
return undefined;
|
|
798
985
|
}
|
|
799
|
-
const sx =
|
|
800
|
-
const sy =
|
|
801
|
-
const tx =
|
|
802
|
-
const ty =
|
|
986
|
+
const sx = sourcePoint.x;
|
|
987
|
+
const sy = sourcePoint.y;
|
|
988
|
+
const tx = targetPoint.x;
|
|
989
|
+
const ty = targetPoint.y;
|
|
803
990
|
const dx = tx - sx;
|
|
804
991
|
const dy = ty - sy;
|
|
805
992
|
const len = Math.hypot(dx, dy);
|
|
806
993
|
if (!Number.isFinite(len) || len < 1) {
|
|
807
994
|
return undefined;
|
|
808
995
|
}
|
|
809
|
-
const
|
|
810
|
-
|
|
996
|
+
const forwardIds = (edgeIdsByPair.get(directedPairKey(edge.sourceId, edge.targetId)) ?? [])
|
|
997
|
+
.slice()
|
|
998
|
+
.sort((left, right) => left.localeCompare(right));
|
|
999
|
+
const reverseIds = (edgeIdsByPair.get(directedPairKey(edge.targetId, edge.sourceId)) ?? [])
|
|
1000
|
+
.slice()
|
|
1001
|
+
.sort((left, right) => left.localeCompare(right));
|
|
1002
|
+
let offset = 0;
|
|
1003
|
+
if (forwardIds.length > 0 && reverseIds.length > 0) {
|
|
1004
|
+
const forwardIndex = forwardIds.indexOf(edge.id);
|
|
1005
|
+
const reverseIndex = reverseIds.indexOf(edge.id);
|
|
1006
|
+
if (forwardIndex >= 0) {
|
|
1007
|
+
const laneOffset = forwardIndex - ((forwardIds.length - 1) / 2);
|
|
1008
|
+
offset = 16 + (laneOffset * 10);
|
|
1009
|
+
} else if (reverseIndex >= 0) {
|
|
1010
|
+
const laneOffset = reverseIndex - ((reverseIds.length - 1) / 2);
|
|
1011
|
+
offset = -16 + (laneOffset * 10);
|
|
1012
|
+
} else {
|
|
1013
|
+
return undefined;
|
|
1014
|
+
}
|
|
1015
|
+
} else {
|
|
1016
|
+
const laneIndex = forwardIds.indexOf(edge.id);
|
|
1017
|
+
if (laneIndex < 0 || forwardIds.length <= 1) {
|
|
1018
|
+
return undefined;
|
|
1019
|
+
}
|
|
1020
|
+
const laneOffset = laneIndex - ((forwardIds.length - 1) / 2);
|
|
1021
|
+
offset = laneOffset * 16;
|
|
1022
|
+
}
|
|
811
1023
|
if (Math.abs(offset) < 0.01) {
|
|
812
1024
|
return undefined;
|
|
813
1025
|
}
|
|
@@ -844,8 +1056,8 @@ async function renderWithX6(
|
|
|
844
1056
|
id: edge.id,
|
|
845
1057
|
source: resolveEndpoint(edge.sourceId),
|
|
846
1058
|
target: resolveEndpoint(edge.targetId),
|
|
847
|
-
router:
|
|
848
|
-
connector:
|
|
1059
|
+
router: resolveEdgeRouter(resolvedStyle),
|
|
1060
|
+
connector: resolveEdgeConnector(resolvedStyle),
|
|
849
1061
|
attrs: {
|
|
850
1062
|
line: lineAttrs,
|
|
851
1063
|
},
|
|
@@ -862,7 +1074,7 @@ async function renderWithX6(
|
|
|
862
1074
|
labelBody: resolveEdgeLabelBodyAttrs(resolvedStyle, label.placement),
|
|
863
1075
|
},
|
|
864
1076
|
})),
|
|
865
|
-
zIndex:
|
|
1077
|
+
zIndex: 50,
|
|
866
1078
|
});
|
|
867
1079
|
}
|
|
868
1080
|
|
|
@@ -902,49 +1114,113 @@ async function renderWithX6(
|
|
|
902
1114
|
e.stopPropagation();
|
|
903
1115
|
});
|
|
904
1116
|
|
|
905
|
-
const
|
|
1117
|
+
const sidePriority = (side: PortSide): number => {
|
|
1118
|
+
switch (side) {
|
|
1119
|
+
case 'left':
|
|
1120
|
+
return 0;
|
|
1121
|
+
case 'top':
|
|
1122
|
+
return 1;
|
|
1123
|
+
case 'right':
|
|
1124
|
+
return 2;
|
|
1125
|
+
case 'bottom':
|
|
1126
|
+
return 3;
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
const projectPortPlacement = (node: any, clientX: number, clientY: number, preferredSide?: PortSide): PortPlacement | undefined => {
|
|
1130
|
+
if (!node || typeof node.size !== 'function') return undefined;
|
|
1131
|
+
const size = node.size();
|
|
1132
|
+
const position = typeof node.getPosition === 'function'
|
|
1133
|
+
? node.getPosition()
|
|
1134
|
+
: { x: Number(node?.position?.x ?? 0), y: Number(node?.position?.y ?? 0) };
|
|
1135
|
+
const localPoint = typeof graphView.clientToGraph === 'function'
|
|
1136
|
+
? graphView.clientToGraph(clientX, clientY)
|
|
1137
|
+
: { x: clientX, y: clientY };
|
|
1138
|
+
const localX = clamp(localPoint.x - position.x, 0, size.width);
|
|
1139
|
+
const localY = clamp(localPoint.y - position.y, 0, size.height);
|
|
1140
|
+
const candidates: Array<{ side: PortSide; distance: number }> = [
|
|
1141
|
+
{ side: 'left', distance: localX },
|
|
1142
|
+
{ side: 'right', distance: size.width - localX },
|
|
1143
|
+
{ side: 'top', distance: localY },
|
|
1144
|
+
{ side: 'bottom', distance: size.height - localY },
|
|
1145
|
+
];
|
|
1146
|
+
candidates.sort((left, right) => left.distance - right.distance || sidePriority(left.side) - sidePriority(right.side));
|
|
1147
|
+
const closest = candidates[0];
|
|
1148
|
+
const preferredCandidate = preferredSide
|
|
1149
|
+
? candidates.find((candidate) => candidate.side === preferredSide)
|
|
1150
|
+
: undefined;
|
|
1151
|
+
const hysteresis = 8;
|
|
1152
|
+
const side = preferredCandidate && preferredSide && (preferredCandidate.distance - closest.distance) <= hysteresis
|
|
1153
|
+
? preferredSide
|
|
1154
|
+
: closest.side;
|
|
1155
|
+
const ratio = side === 'left' || side === 'right'
|
|
1156
|
+
? (size.height > 0 ? localY / size.height : 0.5)
|
|
1157
|
+
: (size.width > 0 ? localX / size.width : 0.5);
|
|
1158
|
+
return {
|
|
1159
|
+
side,
|
|
1160
|
+
ratio: Math.max(0.05, Math.min(0.95, ratio)),
|
|
1161
|
+
};
|
|
1162
|
+
};
|
|
906
1163
|
const onPointerMove = (event: PointerEvent): void => {
|
|
907
|
-
if (!activePortDrag) return;
|
|
1164
|
+
if (!activePortDrag || event.pointerId !== activePortDrag.pointerId) return;
|
|
908
1165
|
const node = graphView.getCellById(activePortDrag.nodeId);
|
|
909
1166
|
if (!node || typeof node.size !== 'function' || typeof node.getPort !== 'function' || typeof node.setPortProp !== 'function') return;
|
|
1167
|
+
const placement = projectPortPlacement(node, event.clientX, event.clientY, activePortDrag.side);
|
|
1168
|
+
if (!placement) return;
|
|
1169
|
+
if (placement.side === activePortDrag.side && Math.abs(placement.ratio - activePortDrag.ratio) < 0.005) {
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
910
1172
|
const size = node.size();
|
|
911
|
-
const
|
|
912
|
-
|
|
1173
|
+
const nextX = placement.side === 'left'
|
|
1174
|
+
? 0
|
|
1175
|
+
: placement.side === 'right'
|
|
1176
|
+
? size.width
|
|
1177
|
+
: placement.ratio * size.width;
|
|
1178
|
+
const nextY = placement.side === 'top'
|
|
1179
|
+
? 0
|
|
1180
|
+
: placement.side === 'bottom'
|
|
1181
|
+
? size.height
|
|
1182
|
+
: placement.ratio * size.height;
|
|
1183
|
+
node.setPortProp(activePortDrag.portId, 'args/x', nextX);
|
|
913
1184
|
node.setPortProp(activePortDrag.portId, 'args/y', nextY);
|
|
1185
|
+
node.setPortProp(activePortDrag.portId, 'args/side', placement.side);
|
|
1186
|
+
node.setPortProp(activePortDrag.portId, 'args/ratio', placement.ratio);
|
|
1187
|
+
if (placement.side !== activePortDrag.side) {
|
|
1188
|
+
node.setPortProp(activePortDrag.portId, 'label/position', resolveBorderPortLabelPosition(placement.side));
|
|
1189
|
+
}
|
|
1190
|
+
activePortDrag.side = placement.side;
|
|
1191
|
+
activePortDrag.ratio = placement.ratio;
|
|
1192
|
+
portPlacementById.set(activePortDrag.portId, placement);
|
|
914
1193
|
};
|
|
915
|
-
const onPointerUp = (): void => {
|
|
1194
|
+
const onPointerUp = (event: PointerEvent): void => {
|
|
1195
|
+
if (!activePortDrag || event.pointerId !== activePortDrag.pointerId) return;
|
|
1196
|
+
if (typeof activePortDrag.container.releasePointerCapture === 'function') {
|
|
1197
|
+
try {
|
|
1198
|
+
activePortDrag.container.releasePointerCapture(activePortDrag.pointerId);
|
|
1199
|
+
} catch {
|
|
1200
|
+
// Ignore capture release failures.
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
916
1203
|
activePortDrag = undefined;
|
|
917
1204
|
};
|
|
918
1205
|
window.addEventListener('pointermove', onPointerMove);
|
|
919
1206
|
window.addEventListener('pointerup', onPointerUp);
|
|
920
|
-
const startPortDrag = (clientY: number, node: any, portId: string): void => {
|
|
1207
|
+
const startPortDrag = (clientX: number, clientY: number, node: any, portId: string, pointerId: number, container: Element): void => {
|
|
921
1208
|
if (!node || typeof node.getPort !== 'function') return;
|
|
922
|
-
const
|
|
923
|
-
|
|
1209
|
+
const placement = projectPortPlacement(node, clientX, clientY, portPlacementById.get(portId)?.side ?? 'right')
|
|
1210
|
+
?? portPlacementById.get(portId)
|
|
1211
|
+
?? { side: 'right' as PortSide, ratio: 0.5 };
|
|
924
1212
|
activePortDrag = {
|
|
925
1213
|
nodeId: String(node.id),
|
|
926
1214
|
portId: String(portId),
|
|
927
|
-
|
|
928
|
-
|
|
1215
|
+
pointerId,
|
|
1216
|
+
side: placement.side,
|
|
1217
|
+
ratio: placement.ratio,
|
|
1218
|
+
container,
|
|
929
1219
|
};
|
|
930
1220
|
};
|
|
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
1221
|
liveCanvas.addEventListener('DOMNodeRemovedFromDocument', () => {
|
|
945
1222
|
window.removeEventListener('pointermove', onPointerMove);
|
|
946
1223
|
window.removeEventListener('pointerup', onPointerUp);
|
|
947
|
-
liveCanvas.removeEventListener('pointerdown', onCanvasPointerDown, true);
|
|
948
1224
|
}, { once: true });
|
|
949
1225
|
|
|
950
1226
|
const resultContainer = liveCanvas.closest('.oml-md-result');
|
|
@@ -999,10 +1275,109 @@ async function renderWithX6(
|
|
|
999
1275
|
resizeHandle.addEventListener('pointerup', onResizePointerEnd);
|
|
1000
1276
|
resizeHandle.addEventListener('pointercancel', onResizePointerEnd);
|
|
1001
1277
|
|
|
1278
|
+
installDiagramNodeInteractions(liveCanvas, graphView);
|
|
1002
1279
|
installDiagramToolbar(liveCanvas, graphView, graph, actions);
|
|
1003
1280
|
|
|
1004
1281
|
}
|
|
1005
1282
|
|
|
1283
|
+
function installDiagramNodeInteractions(canvas: HTMLElement, graphView: any): void {
|
|
1284
|
+
const ensureSvgNativeTitle = (element: SVGElement, iri: string): void => {
|
|
1285
|
+
let titleNode: SVGTitleElement | null = null;
|
|
1286
|
+
for (const child of Array.from(element.children)) {
|
|
1287
|
+
if (child instanceof SVGTitleElement) {
|
|
1288
|
+
titleNode = child;
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
if (!titleNode) {
|
|
1293
|
+
titleNode = document.createElementNS('http://www.w3.org/2000/svg', 'title');
|
|
1294
|
+
element.insertBefore(titleNode, element.firstChild);
|
|
1295
|
+
}
|
|
1296
|
+
titleNode.textContent = iri;
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
const applyNativeTooltipTitle = (container: Element | undefined, iri: string, eventTarget?: EventTarget | null): void => {
|
|
1300
|
+
if (!iri) {
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
if (container instanceof HTMLElement || container instanceof SVGElement) {
|
|
1304
|
+
container.setAttribute('title', iri);
|
|
1305
|
+
}
|
|
1306
|
+
if (container instanceof Element) {
|
|
1307
|
+
for (const element of Array.from(container.querySelectorAll<HTMLElement | SVGElement>('*'))) {
|
|
1308
|
+
element.setAttribute('title', iri);
|
|
1309
|
+
if (element instanceof SVGElement) {
|
|
1310
|
+
ensureSvgNativeTitle(element, iri);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
if (container instanceof SVGElement) {
|
|
1314
|
+
ensureSvgNativeTitle(container, iri);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
if (eventTarget instanceof HTMLElement || eventTarget instanceof SVGElement) {
|
|
1318
|
+
eventTarget.setAttribute('title', iri);
|
|
1319
|
+
if (eventTarget instanceof SVGElement) {
|
|
1320
|
+
ensureSvgNativeTitle(eventTarget, iri);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
};
|
|
1324
|
+
|
|
1325
|
+
const clearHover = (): void => {
|
|
1326
|
+
canvas.dispatchEvent(new CustomEvent('md-hide-iri-hover', { bubbles: true }));
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
graphView.on('node:mouseenter', ({ node, e }: { node: any; e?: MouseEvent }) => {
|
|
1330
|
+
const iri = String(node?.id ?? '');
|
|
1331
|
+
if (iri) {
|
|
1332
|
+
const view = graphView.findViewByCell?.(node);
|
|
1333
|
+
const bbox = view?.container?.getBoundingClientRect?.();
|
|
1334
|
+
applyNativeTooltipTitle(view?.container as Element | undefined, iri, e?.target ?? null);
|
|
1335
|
+
if (!bbox) {
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
canvas.dispatchEvent(new CustomEvent('md-show-iri-hover', {
|
|
1339
|
+
bubbles: true,
|
|
1340
|
+
detail: {
|
|
1341
|
+
iri,
|
|
1342
|
+
previewEnabled: !!e && (/^Mac/i.test(navigator.platform) ? e.metaKey : e.ctrlKey),
|
|
1343
|
+
anchorRect: {
|
|
1344
|
+
left: bbox.left,
|
|
1345
|
+
right: bbox.right,
|
|
1346
|
+
top: bbox.top,
|
|
1347
|
+
bottom: bbox.bottom,
|
|
1348
|
+
width: bbox.width,
|
|
1349
|
+
height: bbox.height,
|
|
1350
|
+
},
|
|
1351
|
+
},
|
|
1352
|
+
}));
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
graphView.on('node:mouseleave', () => {
|
|
1357
|
+
clearHover();
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
graphView.on('blank:mousemove', () => {
|
|
1361
|
+
clearHover();
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
graphView.on('node:dblclick', ({ node, e }: { node: any; e: MouseEvent }) => {
|
|
1365
|
+
const iri = String(node?.id ?? '');
|
|
1366
|
+
if (!iri) {
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
e.preventDefault();
|
|
1370
|
+
e.stopPropagation();
|
|
1371
|
+
if (typeof (e as any).stopImmediatePropagation === 'function') {
|
|
1372
|
+
(e as any).stopImmediatePropagation();
|
|
1373
|
+
}
|
|
1374
|
+
canvas.dispatchEvent(new CustomEvent('md-navigate-iri', {
|
|
1375
|
+
bubbles: true,
|
|
1376
|
+
detail: { iri },
|
|
1377
|
+
}));
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1006
1381
|
function installDiagramToolbar(
|
|
1007
1382
|
graphRoot: HTMLElement,
|
|
1008
1383
|
graphView: any,
|
|
@@ -1348,7 +1723,8 @@ async function loadDagreLib(): Promise<any> {
|
|
|
1348
1723
|
|
|
1349
1724
|
async function layoutGraphDagre(
|
|
1350
1725
|
graph: { nodes: NodeSpec[]; edges: EdgeSpec[]; roots: string[] },
|
|
1351
|
-
options: DagreLayoutOptions
|
|
1726
|
+
options: DagreLayoutOptions,
|
|
1727
|
+
rootSpacing: BoxSpacing
|
|
1352
1728
|
): Promise<NativeLayoutResult> {
|
|
1353
1729
|
const dagre = await loadDagreLib();
|
|
1354
1730
|
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
@@ -1385,8 +1761,6 @@ async function layoutGraphDagre(
|
|
|
1385
1761
|
stack: {
|
|
1386
1762
|
direction: styleLayout.direction === 'horizontal' ? 'horizontal' : 'vertical',
|
|
1387
1763
|
gap: clampLayoutNumber(styleLayout.gap, 0, 400, 0),
|
|
1388
|
-
marginx: clampLayoutNumber(styleLayout.marginx, 0, 200, 0),
|
|
1389
|
-
marginy: clampLayoutNumber(styleLayout.marginy, 0, 200, 0),
|
|
1390
1764
|
stretch: typeof styleLayout.stretch === 'boolean' ? styleLayout.stretch : true,
|
|
1391
1765
|
},
|
|
1392
1766
|
};
|
|
@@ -1406,29 +1780,40 @@ async function layoutGraphDagre(
|
|
|
1406
1780
|
rankdir,
|
|
1407
1781
|
nodesep: clampLayoutNumber(styleLayout.nodesep, 0, 400, options.nodesep),
|
|
1408
1782
|
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
1783
|
},
|
|
1412
1784
|
};
|
|
1413
1785
|
};
|
|
1414
1786
|
|
|
1415
1787
|
const childPositionById = new Map<string, { x: number; y: number }>();
|
|
1788
|
+
const branchChildForParent = (parentId: string | undefined, nodeId: string): string | undefined => {
|
|
1789
|
+
let cursorId: string | undefined = nodeId;
|
|
1790
|
+
while (cursorId) {
|
|
1791
|
+
const node = nodeById.get(cursorId);
|
|
1792
|
+
if (!node) {
|
|
1793
|
+
return undefined;
|
|
1794
|
+
}
|
|
1795
|
+
if (node.parentId === parentId) {
|
|
1796
|
+
return node.id;
|
|
1797
|
+
}
|
|
1798
|
+
cursorId = node.parentId;
|
|
1799
|
+
}
|
|
1800
|
+
return undefined;
|
|
1801
|
+
};
|
|
1416
1802
|
const edgesBetweenChildren = (parentId: string | undefined, childSet: Set<string>): Array<{ source: string; target: string }> => {
|
|
1417
1803
|
const seen = new Set<string>();
|
|
1418
1804
|
const links: Array<{ source: string; target: string }> = [];
|
|
1419
1805
|
for (const edge of graph.edges) {
|
|
1420
1806
|
const sourceId = endpointOwner(edge.sourceId);
|
|
1421
1807
|
const targetId = endpointOwner(edge.targetId);
|
|
1422
|
-
if (!sourceId || !targetId
|
|
1423
|
-
|
|
1424
|
-
const
|
|
1425
|
-
|
|
1426
|
-
if (!
|
|
1427
|
-
|
|
1428
|
-
const key = `${sourceId}=>${targetId}`;
|
|
1808
|
+
if (!sourceId || !targetId) continue;
|
|
1809
|
+
const sourceBranchId = branchChildForParent(parentId, sourceId);
|
|
1810
|
+
const targetBranchId = branchChildForParent(parentId, targetId);
|
|
1811
|
+
if (!sourceBranchId || !targetBranchId || sourceBranchId === targetBranchId) continue;
|
|
1812
|
+
if (!childSet.has(sourceBranchId) || !childSet.has(targetBranchId)) continue;
|
|
1813
|
+
const key = `${sourceBranchId}=>${targetBranchId}`;
|
|
1429
1814
|
if (seen.has(key)) continue;
|
|
1430
1815
|
seen.add(key);
|
|
1431
|
-
links.push({ source:
|
|
1816
|
+
links.push({ source: sourceBranchId, target: targetBranchId });
|
|
1432
1817
|
}
|
|
1433
1818
|
return links;
|
|
1434
1819
|
};
|
|
@@ -1443,8 +1828,8 @@ async function layoutGraphDagre(
|
|
|
1443
1828
|
}
|
|
1444
1829
|
|
|
1445
1830
|
const parent = parentId ? nodeById.get(parentId) : undefined;
|
|
1446
|
-
const topPadding = parent?.contentTopPadding ?? 0;
|
|
1447
1831
|
const localLayout = layoutOptionsForParent(parentId);
|
|
1832
|
+
const spacing = parent ? resolveBoxSpacing(parent.style, 0) : rootSpacing;
|
|
1448
1833
|
let totalWidth = 0;
|
|
1449
1834
|
let totalHeight = 0;
|
|
1450
1835
|
if (localLayout.type === 'stack') {
|
|
@@ -1452,13 +1837,13 @@ async function layoutGraphDagre(
|
|
|
1452
1837
|
const childNodes = childIds.map((childId) => nodeById.get(childId)).filter((node): node is NodeSpec => !!node);
|
|
1453
1838
|
const maxChildWidth = childNodes.reduce((max, child) => Math.max(max, child.width), 0);
|
|
1454
1839
|
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) -
|
|
1840
|
+
const parentContentWidth = Math.max(0, (parent?.width ?? 0) - spacing.marginLeft - spacing.marginRight - spacing.paddingLeft - spacing.paddingRight);
|
|
1841
|
+
const parentContentHeight = Math.max(0, (parent?.height ?? 0) - spacing.marginTop - spacing.marginBottom - spacing.paddingTop - spacing.paddingBottom);
|
|
1457
1842
|
const availableWidth = Math.max(maxChildWidth, parentContentWidth);
|
|
1458
1843
|
const availableHeight = Math.max(maxChildHeight, parentContentHeight);
|
|
1459
1844
|
const isSingleChild = childNodes.length === 1;
|
|
1460
|
-
let cursorX =
|
|
1461
|
-
let cursorY =
|
|
1845
|
+
let cursorX = spacing.marginLeft + spacing.paddingLeft;
|
|
1846
|
+
let cursorY = spacing.marginTop + spacing.paddingTop;
|
|
1462
1847
|
for (const child of childNodes) {
|
|
1463
1848
|
if (local.stretch && isSingleChild) {
|
|
1464
1849
|
child.width = Math.max(0, availableWidth);
|
|
@@ -1481,16 +1866,16 @@ async function layoutGraphDagre(
|
|
|
1481
1866
|
childNodes.reduce((sum, child) => sum + child.height, 0) + (Math.max(0, childNodes.length - 1) * local.gap)
|
|
1482
1867
|
);
|
|
1483
1868
|
const contentWidth = Math.max(0, childNodes.reduce((max, child) => Math.max(max, child.width), 0));
|
|
1484
|
-
totalWidth =
|
|
1485
|
-
totalHeight =
|
|
1869
|
+
totalWidth = spacing.marginLeft + spacing.paddingLeft + contentWidth + spacing.paddingRight + spacing.marginRight;
|
|
1870
|
+
totalHeight = spacing.marginTop + spacing.paddingTop + contentHeight + spacing.paddingBottom + spacing.marginBottom;
|
|
1486
1871
|
} else {
|
|
1487
1872
|
const contentWidth = Math.max(
|
|
1488
1873
|
0,
|
|
1489
1874
|
childNodes.reduce((sum, child) => sum + child.width, 0) + (Math.max(0, childNodes.length - 1) * local.gap)
|
|
1490
1875
|
);
|
|
1491
1876
|
const contentHeight = Math.max(0, childNodes.reduce((max, child) => Math.max(max, child.height), 0));
|
|
1492
|
-
totalWidth =
|
|
1493
|
-
totalHeight =
|
|
1877
|
+
totalWidth = spacing.marginLeft + spacing.paddingLeft + contentWidth + spacing.paddingRight + spacing.marginRight;
|
|
1878
|
+
totalHeight = spacing.marginTop + spacing.paddingTop + contentHeight + spacing.paddingBottom + spacing.marginBottom;
|
|
1494
1879
|
}
|
|
1495
1880
|
} else {
|
|
1496
1881
|
const localOptions = localLayout.dagre;
|
|
@@ -1546,23 +1931,19 @@ async function layoutGraphDagre(
|
|
|
1546
1931
|
const left = laid.x - (width / 2);
|
|
1547
1932
|
const top = laid.y - (height / 2);
|
|
1548
1933
|
childPositionById.set(childId, {
|
|
1549
|
-
x:
|
|
1550
|
-
y:
|
|
1934
|
+
x: spacing.marginLeft + spacing.paddingLeft + (left - minX),
|
|
1935
|
+
y: spacing.marginTop + spacing.paddingTop + (top - minY),
|
|
1551
1936
|
});
|
|
1552
1937
|
}
|
|
1553
1938
|
|
|
1554
1939
|
const contentWidth = Math.max(0, maxX - minX);
|
|
1555
1940
|
const contentHeight = Math.max(0, maxY - minY);
|
|
1556
|
-
totalWidth =
|
|
1557
|
-
totalHeight =
|
|
1941
|
+
totalWidth = spacing.marginLeft + spacing.paddingLeft + contentWidth + spacing.paddingRight + spacing.marginRight;
|
|
1942
|
+
totalHeight = spacing.marginTop + spacing.paddingTop + contentHeight + spacing.paddingBottom + spacing.marginBottom;
|
|
1558
1943
|
}
|
|
1559
1944
|
if (parent) {
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
}
|
|
1563
|
-
if (!parent.hasExplicitHeight) {
|
|
1564
|
-
parent.height = Math.max(parent.height, totalHeight);
|
|
1565
|
-
}
|
|
1945
|
+
parent.width = Math.max(parent.width, totalWidth);
|
|
1946
|
+
parent.height = Math.max(parent.height, totalHeight);
|
|
1566
1947
|
}
|
|
1567
1948
|
return { width: totalWidth, height: totalHeight };
|
|
1568
1949
|
};
|
|
@@ -1582,9 +1963,9 @@ async function layoutGraphDagre(
|
|
|
1582
1963
|
if (localLayout.type === 'stack') {
|
|
1583
1964
|
const local = localLayout.stack;
|
|
1584
1965
|
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 -
|
|
1966
|
+
const spacing = resolveBoxSpacing(parent.style, 0);
|
|
1967
|
+
const contentWidth = Math.max(0, parent.width - spacing.marginLeft - spacing.marginRight - spacing.paddingLeft - spacing.paddingRight);
|
|
1968
|
+
const contentHeight = Math.max(0, parent.height - spacing.marginTop - spacing.marginBottom - spacing.paddingTop - spacing.paddingBottom);
|
|
1588
1969
|
const isSingleChild = childNodes.length === 1;
|
|
1589
1970
|
|
|
1590
1971
|
if (local.stretch) {
|
|
@@ -1602,8 +1983,8 @@ async function layoutGraphDagre(
|
|
|
1602
1983
|
}
|
|
1603
1984
|
}
|
|
1604
1985
|
|
|
1605
|
-
let cursorX =
|
|
1606
|
-
let cursorY =
|
|
1986
|
+
let cursorX = spacing.marginLeft + spacing.paddingLeft;
|
|
1987
|
+
let cursorY = spacing.marginTop + spacing.paddingTop;
|
|
1607
1988
|
for (const child of childNodes) {
|
|
1608
1989
|
childPositionById.set(child.id, { x: cursorX, y: cursorY });
|
|
1609
1990
|
if (local.direction === 'vertical') {
|
|
@@ -1819,9 +2200,6 @@ function compileDiagramGraph(
|
|
|
1819
2200
|
children: [],
|
|
1820
2201
|
width: 160,
|
|
1821
2202
|
height: 70,
|
|
1822
|
-
hasExplicitWidth: false,
|
|
1823
|
-
hasExplicitHeight: false,
|
|
1824
|
-
contentTopPadding: 0,
|
|
1825
2203
|
});
|
|
1826
2204
|
}
|
|
1827
2205
|
|
|
@@ -1899,9 +2277,6 @@ function compileDiagramGraph(
|
|
|
1899
2277
|
const estimated = estimateSize(node.kind, baseLabel, node.labels.length, node.style);
|
|
1900
2278
|
node.width = estimated.width;
|
|
1901
2279
|
node.height = estimated.height;
|
|
1902
|
-
node.hasExplicitWidth = estimated.hasExplicitWidth;
|
|
1903
|
-
node.hasExplicitHeight = estimated.hasExplicitHeight;
|
|
1904
|
-
node.contentTopPadding = resolveContainerTopPadding(node, baseLabel);
|
|
1905
2280
|
}
|
|
1906
2281
|
for (const edge of edges) {
|
|
1907
2282
|
edge.style = styleFor('edge', edge.id, edge.classes, edge.properties);
|
|
@@ -1930,62 +2305,44 @@ function estimateSize(
|
|
|
1930
2305
|
label: string,
|
|
1931
2306
|
labelCount: number,
|
|
1932
2307
|
style: Record<string, unknown>
|
|
1933
|
-
): { width: number; height: number
|
|
2308
|
+
): { width: number; height: number } {
|
|
1934
2309
|
const styledWidth = toPositiveNumber(style.width);
|
|
1935
2310
|
const styledHeight = toPositiveNumber(style.height);
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
2311
|
+
const attrs = extractStyleAttrs(style);
|
|
2312
|
+
const labelAttrs = asRecord(attrs.label);
|
|
2313
|
+
const labelDisplay = typeof labelAttrs?.display === 'string' ? labelAttrs.display.trim().toLowerCase() : '';
|
|
2314
|
+
const labelOpacity = toNonNegativeNumber(labelAttrs?.opacity);
|
|
2315
|
+
const labelVisible = labelDisplay !== 'none' && (labelOpacity === undefined || labelOpacity > 0);
|
|
2316
|
+
const effectiveLabel = labelVisible ? label : '';
|
|
2317
|
+
const effectiveLabelCount = labelVisible ? labelCount : 0;
|
|
1939
2318
|
if (kind === 'Port') {
|
|
1940
2319
|
return {
|
|
1941
|
-
width: styledWidth ??
|
|
1942
|
-
height: styledHeight ??
|
|
1943
|
-
hasExplicitWidth: styledWidth !== undefined,
|
|
1944
|
-
hasExplicitHeight: styledHeight !== undefined,
|
|
2320
|
+
width: Math.max(14, styledWidth ?? 0),
|
|
2321
|
+
height: Math.max(14, styledHeight ?? 0),
|
|
1945
2322
|
};
|
|
1946
2323
|
}
|
|
1947
|
-
const textWidth = Math.max(24,
|
|
1948
|
-
const baseWidth = Math.max(72, Math.min(280, textWidth + 26));
|
|
2324
|
+
const textWidth = effectiveLabel.length > 0 ? Math.max(24, effectiveLabel.length * 7) : 0;
|
|
2325
|
+
const baseWidth = effectiveLabel.length > 0 ? Math.max(72, Math.min(280, textWidth + 26)) : 0;
|
|
1949
2326
|
if (kind === 'Compartment') {
|
|
1950
|
-
const size = {
|
|
2327
|
+
const size = {
|
|
2328
|
+
width: baseWidth,
|
|
2329
|
+
height: effectiveLabelCount > 0 ? Math.max(44, 24 + effectiveLabelCount * 16) : 0,
|
|
2330
|
+
};
|
|
1951
2331
|
return {
|
|
1952
|
-
width: styledWidth ??
|
|
1953
|
-
height: styledHeight ??
|
|
1954
|
-
hasExplicitWidth: styledWidth !== undefined,
|
|
1955
|
-
hasExplicitHeight: styledHeight !== undefined,
|
|
2332
|
+
width: Math.max(size.width, styledWidth ?? 0),
|
|
2333
|
+
height: Math.max(size.height, styledHeight ?? 0),
|
|
1956
2334
|
};
|
|
1957
2335
|
}
|
|
1958
|
-
const size = {
|
|
2336
|
+
const size = {
|
|
2337
|
+
width: baseWidth,
|
|
2338
|
+
height: effectiveLabelCount > 0 ? Math.max(36, 20 + effectiveLabelCount * 16) : 0,
|
|
2339
|
+
};
|
|
1959
2340
|
return {
|
|
1960
|
-
width: styledWidth ??
|
|
1961
|
-
height: styledHeight ??
|
|
1962
|
-
hasExplicitWidth: styledWidth !== undefined,
|
|
1963
|
-
hasExplicitHeight: styledHeight !== undefined,
|
|
2341
|
+
width: Math.max(size.width, styledWidth ?? 0),
|
|
2342
|
+
height: Math.max(size.height, styledHeight ?? 0),
|
|
1964
2343
|
};
|
|
1965
2344
|
}
|
|
1966
2345
|
|
|
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
2346
|
function localName(value: string): string {
|
|
1990
2347
|
return displayLabelFromIri(value);
|
|
1991
2348
|
}
|
|
@@ -2042,9 +2399,7 @@ function resolveDagreLayoutOptions(options: Record<string, unknown> | undefined)
|
|
|
2042
2399
|
|
|
2043
2400
|
const nodesep = clampLayoutNumber(layout.nodesep, 0, 400, 28);
|
|
2044
2401
|
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 };
|
|
2402
|
+
return { rankdir, nodesep, ranksep };
|
|
2048
2403
|
}
|
|
2049
2404
|
|
|
2050
2405
|
function clampLayoutNumber(value: unknown, min: number, max: number, fallback: number): number {
|
|
@@ -2054,6 +2409,129 @@ function clampLayoutNumber(value: unknown, min: number, max: number, fallback: n
|
|
|
2054
2409
|
return Math.max(min, Math.min(max, Math.round(value)));
|
|
2055
2410
|
}
|
|
2056
2411
|
|
|
2412
|
+
function resolveRootSpacing(options: Record<string, unknown> | undefined): BoxSpacing {
|
|
2413
|
+
return readBoxSpacing(options, {
|
|
2414
|
+
marginTop: 16,
|
|
2415
|
+
marginBottom: 16,
|
|
2416
|
+
marginLeft: 16,
|
|
2417
|
+
marginRight: 16,
|
|
2418
|
+
paddingTop: 0,
|
|
2419
|
+
paddingBottom: 0,
|
|
2420
|
+
paddingLeft: 0,
|
|
2421
|
+
paddingRight: 0,
|
|
2422
|
+
});
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
function resolveBoxSpacing(style: Record<string, unknown> | undefined, fallbackMargin: number): BoxSpacing {
|
|
2426
|
+
return readBoxSpacing(style, {
|
|
2427
|
+
marginTop: fallbackMargin,
|
|
2428
|
+
marginBottom: fallbackMargin,
|
|
2429
|
+
marginLeft: fallbackMargin,
|
|
2430
|
+
marginRight: fallbackMargin,
|
|
2431
|
+
paddingTop: 0,
|
|
2432
|
+
paddingBottom: 0,
|
|
2433
|
+
paddingLeft: 0,
|
|
2434
|
+
paddingRight: 0,
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
function readBoxSpacing(style: Record<string, unknown> | undefined, defaults: BoxSpacing): BoxSpacing {
|
|
2439
|
+
const source = style ?? {};
|
|
2440
|
+
return {
|
|
2441
|
+
...readCssBoxSpacing(source.margin, 'margin', defaults),
|
|
2442
|
+
...readCssBoxSpacing(source.padding, 'padding', defaults),
|
|
2443
|
+
};
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
function readCssBoxSpacing(
|
|
2447
|
+
value: unknown,
|
|
2448
|
+
kind: 'margin' | 'padding',
|
|
2449
|
+
defaults: BoxSpacing
|
|
2450
|
+
): Pick<BoxSpacing, 'marginTop' | 'marginRight' | 'marginBottom' | 'marginLeft' | 'paddingTop' | 'paddingRight' | 'paddingBottom' | 'paddingLeft'> {
|
|
2451
|
+
const sides = parseCssBoxShorthand(value);
|
|
2452
|
+
const isMargin = kind === 'margin';
|
|
2453
|
+
if (isMargin) {
|
|
2454
|
+
return {
|
|
2455
|
+
marginTop: sides?.top ?? defaults.marginTop,
|
|
2456
|
+
marginRight: sides?.right ?? defaults.marginRight,
|
|
2457
|
+
marginBottom: sides?.bottom ?? defaults.marginBottom,
|
|
2458
|
+
marginLeft: sides?.left ?? defaults.marginLeft,
|
|
2459
|
+
paddingTop: defaults.paddingTop,
|
|
2460
|
+
paddingRight: defaults.paddingRight,
|
|
2461
|
+
paddingBottom: defaults.paddingBottom,
|
|
2462
|
+
paddingLeft: defaults.paddingLeft,
|
|
2463
|
+
};
|
|
2464
|
+
}
|
|
2465
|
+
return {
|
|
2466
|
+
marginTop: defaults.marginTop,
|
|
2467
|
+
marginRight: defaults.marginRight,
|
|
2468
|
+
marginBottom: defaults.marginBottom,
|
|
2469
|
+
marginLeft: defaults.marginLeft,
|
|
2470
|
+
paddingTop: sides?.top ?? defaults.paddingTop,
|
|
2471
|
+
paddingRight: sides?.right ?? defaults.paddingRight,
|
|
2472
|
+
paddingBottom: sides?.bottom ?? defaults.paddingBottom,
|
|
2473
|
+
paddingLeft: sides?.left ?? defaults.paddingLeft,
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
function parseCssBoxShorthand(value: unknown): { top?: number; right?: number; bottom?: number; left?: number } | undefined {
|
|
2478
|
+
const values = Array.isArray(value)
|
|
2479
|
+
? value
|
|
2480
|
+
: typeof value === 'string'
|
|
2481
|
+
? (value.includes(',') ? undefined : value.trim().split(/\s+/).filter((token) => token.length > 0))
|
|
2482
|
+
: typeof value === 'number'
|
|
2483
|
+
? [value]
|
|
2484
|
+
: undefined;
|
|
2485
|
+
if (!values || values.length === 0 || values.length > 4) {
|
|
2486
|
+
return undefined;
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
const parsed = values.map((entry) => {
|
|
2490
|
+
if (typeof entry === 'number' && Number.isFinite(entry)) {
|
|
2491
|
+
return entry;
|
|
2492
|
+
}
|
|
2493
|
+
if (typeof entry === 'string') {
|
|
2494
|
+
const next = Number.parseFloat(entry);
|
|
2495
|
+
if (Number.isFinite(next)) {
|
|
2496
|
+
return next;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
return undefined;
|
|
2500
|
+
});
|
|
2501
|
+
if (parsed.some((entry) => entry === undefined)) {
|
|
2502
|
+
return undefined;
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
const clampCssBoxValue = (entry: number | undefined): number | undefined => {
|
|
2506
|
+
if (entry === undefined) return undefined;
|
|
2507
|
+
return Math.max(0, Math.min(200, Math.round(entry)));
|
|
2508
|
+
};
|
|
2509
|
+
|
|
2510
|
+
if (parsed.length === 1) {
|
|
2511
|
+
const single = clampCssBoxValue(parsed[0]);
|
|
2512
|
+
return single === undefined ? undefined : { top: single, right: single, bottom: single, left: single };
|
|
2513
|
+
}
|
|
2514
|
+
if (parsed.length === 2) {
|
|
2515
|
+
const vertical = clampCssBoxValue(parsed[0]);
|
|
2516
|
+
const horizontal = clampCssBoxValue(parsed[1]);
|
|
2517
|
+
if (vertical === undefined || horizontal === undefined) return undefined;
|
|
2518
|
+
return { top: vertical, right: horizontal, bottom: vertical, left: horizontal };
|
|
2519
|
+
}
|
|
2520
|
+
if (parsed.length === 3) {
|
|
2521
|
+
const top = clampCssBoxValue(parsed[0]);
|
|
2522
|
+
const horizontal = clampCssBoxValue(parsed[1]);
|
|
2523
|
+
const bottom = clampCssBoxValue(parsed[2]);
|
|
2524
|
+
if (top === undefined || horizontal === undefined || bottom === undefined) return undefined;
|
|
2525
|
+
return { top, right: horizontal, bottom, left: horizontal };
|
|
2526
|
+
}
|
|
2527
|
+
const top = clampCssBoxValue(parsed[0]);
|
|
2528
|
+
const right = clampCssBoxValue(parsed[1]);
|
|
2529
|
+
const bottom = clampCssBoxValue(parsed[2]);
|
|
2530
|
+
const left = clampCssBoxValue(parsed[3]);
|
|
2531
|
+
if (top === undefined || right === undefined || bottom === undefined || left === undefined) return undefined;
|
|
2532
|
+
return { top, right, bottom, left };
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2057
2535
|
function parseDiagramStylesheet(options: Record<string, unknown> | undefined): DiagramStyleRule[] {
|
|
2058
2536
|
const stylesheet = options?.stylesheet;
|
|
2059
2537
|
if (!Array.isArray(stylesheet)) {
|
|
@@ -2079,11 +2557,11 @@ function parseDiagramStylesheet(options: Record<string, unknown> | undefined): D
|
|
|
2079
2557
|
|
|
2080
2558
|
function parseStyleSelector(
|
|
2081
2559
|
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);
|
|
2560
|
+
): { elementKind: 'diagram' | 'node' | 'compartment' | 'port' | 'edge'; className?: string; condition?: string } | undefined {
|
|
2561
|
+
const match = /^\s*(diagram|node|compartment|port|edge)(?:\.([A-Za-z0-9_-]+))?(?:\s*\[(.+)\]\s*)?$/i.exec(selector);
|
|
2084
2562
|
if (!match) return undefined;
|
|
2085
2563
|
return {
|
|
2086
|
-
elementKind: match[1].toLowerCase() as 'node' | 'compartment' | 'port' | 'edge',
|
|
2564
|
+
elementKind: match[1].toLowerCase() as 'diagram' | 'node' | 'compartment' | 'port' | 'edge',
|
|
2087
2565
|
className: match[2]?.trim() || undefined,
|
|
2088
2566
|
condition: match[3]?.trim() || undefined,
|
|
2089
2567
|
};
|
|
@@ -2093,7 +2571,14 @@ function parseStyleSelector(
|
|
|
2093
2571
|
// These pass through as top-level keys so callers can read style.layout, style.width, etc.
|
|
2094
2572
|
const OML_PASSTHROUGH_STYLE_KEYS = new Set([
|
|
2095
2573
|
'layout', 'shape', 'width', 'height',
|
|
2574
|
+
'margin', 'padding', 'router', 'connector',
|
|
2575
|
+
]);
|
|
2576
|
+
|
|
2577
|
+
const LEGACY_BOX_SPACING_KEYS = new Set([
|
|
2578
|
+
'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
|
|
2096
2579
|
'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
|
|
2580
|
+
'margin-top', 'margin-bottom', 'margin-left', 'margin-right',
|
|
2581
|
+
'padding-top', 'padding-bottom', 'padding-left', 'padding-right',
|
|
2097
2582
|
]);
|
|
2098
2583
|
|
|
2099
2584
|
// Record-valued top-level style keys that map directly into the attrs sub-tree under the same name.
|
|
@@ -2107,7 +2592,7 @@ const ATTRS_RECORD_STYLE_KEYS = new Set([
|
|
|
2107
2592
|
]);
|
|
2108
2593
|
|
|
2109
2594
|
function normalizeDiagramStyle(
|
|
2110
|
-
elementKind: 'node' | 'compartment' | 'port' | 'edge',
|
|
2595
|
+
elementKind: 'diagram' | 'node' | 'compartment' | 'port' | 'edge',
|
|
2111
2596
|
raw: Record<string, unknown>
|
|
2112
2597
|
): Record<string, unknown> {
|
|
2113
2598
|
const isEdge = elementKind === 'edge';
|
|
@@ -2122,6 +2607,10 @@ function normalizeDiagramStyle(
|
|
|
2122
2607
|
const key = rawKey.trim();
|
|
2123
2608
|
if (!key || value === undefined || value === null) continue;
|
|
2124
2609
|
|
|
2610
|
+
if (LEGACY_BOX_SPACING_KEYS.has(key)) {
|
|
2611
|
+
continue;
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2125
2614
|
// OML node-level keys: pass through to the top level of the normalized style object
|
|
2126
2615
|
if (OML_PASSTHROUGH_STYLE_KEYS.has(key)) { passthrough[key] = value; continue; }
|
|
2127
2616
|
|
|
@@ -2172,7 +2661,7 @@ function normalizeDiagramStyle(
|
|
|
2172
2661
|
}
|
|
2173
2662
|
|
|
2174
2663
|
function resolveElementStyle(
|
|
2175
|
-
elementKind: 'node' | 'compartment' | 'port' | 'edge',
|
|
2664
|
+
elementKind: 'diagram' | 'node' | 'compartment' | 'port' | 'edge',
|
|
2176
2665
|
value: string,
|
|
2177
2666
|
classes: string[],
|
|
2178
2667
|
properties: Record<string, string[]>,
|
|
@@ -2410,6 +2899,19 @@ function resolveEdgeLineAttrs(style: Record<string, unknown>): Record<string, un
|
|
|
2410
2899
|
};
|
|
2411
2900
|
}
|
|
2412
2901
|
|
|
2902
|
+
function resolveEdgeRouter(style: Record<string, unknown>): Record<string, unknown> {
|
|
2903
|
+
const router = asRecord(style.router);
|
|
2904
|
+
return router ?? { name: 'normal' };
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
function resolveEdgeConnector(style: Record<string, unknown>): Record<string, unknown> {
|
|
2908
|
+
const connector = asRecord(style.connector);
|
|
2909
|
+
return connector ?? {
|
|
2910
|
+
name: 'jumpover',
|
|
2911
|
+
args: { size: 5 },
|
|
2912
|
+
};
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2413
2915
|
function resolveEdgeLabelAttrs(
|
|
2414
2916
|
style: Record<string, unknown>,
|
|
2415
2917
|
placement: EdgeLabel['placement'],
|
|
@@ -2440,19 +2942,49 @@ function resolveEdgeLabelBodyAttrs(
|
|
|
2440
2942
|
fillOpacity: 0.9,
|
|
2441
2943
|
stroke: 'none',
|
|
2442
2944
|
strokeWidth: 0,
|
|
2945
|
+
pointerEvents: 'all',
|
|
2443
2946
|
...(base ?? {}),
|
|
2444
2947
|
...(specific ?? {}),
|
|
2445
2948
|
};
|
|
2446
2949
|
}
|
|
2447
2950
|
|
|
2448
|
-
function resolvePortAttrs(
|
|
2951
|
+
function resolvePortAttrs(
|
|
2952
|
+
style: Record<string, unknown>,
|
|
2953
|
+
classes: string[],
|
|
2954
|
+
text: string | undefined,
|
|
2955
|
+
side: PortSide = 'right',
|
|
2956
|
+
ownerStroke?: string
|
|
2957
|
+
): Record<string, unknown> {
|
|
2449
2958
|
const attrs = extractStyleAttrs(style);
|
|
2450
2959
|
const body = asRecord(attrs.body);
|
|
2451
2960
|
const icon = asRecord(attrs.icon);
|
|
2452
2961
|
const label = asRecord(attrs.label);
|
|
2453
2962
|
const imageUrl = extractImageHrefFromIcon(icon);
|
|
2963
|
+
const labelPosition = side === 'left'
|
|
2964
|
+
? {
|
|
2965
|
+
textAnchor: 'end',
|
|
2966
|
+
x: -10,
|
|
2967
|
+
dy: '0.9em',
|
|
2968
|
+
}
|
|
2969
|
+
: side === 'top'
|
|
2970
|
+
? {
|
|
2971
|
+
textAnchor: 'middle',
|
|
2972
|
+
x: 0,
|
|
2973
|
+
dy: '-0.3em',
|
|
2974
|
+
}
|
|
2975
|
+
: side === 'bottom'
|
|
2976
|
+
? {
|
|
2977
|
+
textAnchor: 'middle',
|
|
2978
|
+
x: 0,
|
|
2979
|
+
dy: '1.4em',
|
|
2980
|
+
}
|
|
2981
|
+
: {
|
|
2982
|
+
textAnchor: 'start',
|
|
2983
|
+
x: 10,
|
|
2984
|
+
dy: '0.9em',
|
|
2985
|
+
};
|
|
2454
2986
|
|
|
2455
|
-
|
|
2987
|
+
const result: Record<string, unknown> = {
|
|
2456
2988
|
body: {
|
|
2457
2989
|
width: 12,
|
|
2458
2990
|
height: 12,
|
|
@@ -2460,7 +2992,7 @@ function resolvePortAttrs(style: Record<string, unknown>, classes: string[], tex
|
|
|
2460
2992
|
y: -6,
|
|
2461
2993
|
class: ['oml-port-body', ...classes].join(' '),
|
|
2462
2994
|
magnet: false,
|
|
2463
|
-
stroke:
|
|
2995
|
+
stroke: ownerStroke ?? CSS_EDITOR_FOREGROUND,
|
|
2464
2996
|
strokeWidth: 1,
|
|
2465
2997
|
fill: CSS_EDITOR_BACKGROUND,
|
|
2466
2998
|
...(body ?? {}),
|
|
@@ -2475,17 +3007,196 @@ function resolvePortAttrs(style: Record<string, unknown>, classes: string[], tex
|
|
|
2475
3007
|
...(icon ?? {}),
|
|
2476
3008
|
...(imageUrl ? { href: imageUrl, xlinkHref: imageUrl, 'xlink:href': imageUrl } : {}),
|
|
2477
3009
|
},
|
|
2478
|
-
|
|
3010
|
+
};
|
|
3011
|
+
if (text) {
|
|
3012
|
+
result.label = {
|
|
2479
3013
|
text,
|
|
2480
3014
|
fill: CSS_EDITOR_FOREGROUND,
|
|
2481
3015
|
fontFamily: 'var(--vscode-editor-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)',
|
|
2482
3016
|
fontSize: 12,
|
|
2483
|
-
|
|
2484
|
-
x: 10,
|
|
2485
|
-
dy: '0.9em',
|
|
3017
|
+
...labelPosition,
|
|
2486
3018
|
...(label ?? {}),
|
|
2487
|
-
}
|
|
3019
|
+
};
|
|
3020
|
+
}
|
|
3021
|
+
return result;
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
function resolveBorderPortLabelPosition(side: PortSide): { name: PortSide } {
|
|
3025
|
+
return { name: side };
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
function clamp(value: number, min: number, max: number): number {
|
|
3029
|
+
return Math.max(min, Math.min(max, value));
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
function computeDefaultPortPlacements(
|
|
3033
|
+
graph: { nodes: NodeSpec[]; edges: EdgeSpec[]; roots: string[] },
|
|
3034
|
+
nodeById: Map<string, NodeSpec>,
|
|
3035
|
+
portsByOwner: Map<string, NodeSpec[]>,
|
|
3036
|
+
boxes: Map<string, NodeBox>
|
|
3037
|
+
): Map<string, PortPlacement> {
|
|
3038
|
+
const placements = new Map<string, PortPlacement>();
|
|
3039
|
+
const ownerCenter = (nodeId: string): { x: number; y: number } | undefined => {
|
|
3040
|
+
const box = boxes.get(nodeId);
|
|
3041
|
+
if (!box) {
|
|
3042
|
+
return undefined;
|
|
3043
|
+
}
|
|
3044
|
+
return { x: box.x + (box.width / 2), y: box.y + (box.height / 2) };
|
|
3045
|
+
};
|
|
3046
|
+
const portIdsByOwner = new Map<string, Set<string>>();
|
|
3047
|
+
for (const [ownerId, ports] of portsByOwner.entries()) {
|
|
3048
|
+
portIdsByOwner.set(ownerId, new Set(ports.map((port) => port.id)));
|
|
3049
|
+
}
|
|
3050
|
+
const peerPortIdsByPort = new Map<string, string[]>();
|
|
3051
|
+
const peerCentersByPort = new Map<string, Array<{ x: number; y: number }>>();
|
|
3052
|
+
for (const edge of graph.edges) {
|
|
3053
|
+
const source = nodeById.get(edge.sourceId);
|
|
3054
|
+
const target = nodeById.get(edge.targetId);
|
|
3055
|
+
const sourceOwnerId = source?.kind === 'Port' ? source.parentId : source?.id;
|
|
3056
|
+
const targetOwnerId = target?.kind === 'Port' ? target.parentId : target?.id;
|
|
3057
|
+
if (!sourceOwnerId || !targetOwnerId || sourceOwnerId === targetOwnerId) {
|
|
3058
|
+
continue;
|
|
3059
|
+
}
|
|
3060
|
+
const sourcePeer = ownerCenter(targetOwnerId);
|
|
3061
|
+
const targetPeer = ownerCenter(sourceOwnerId);
|
|
3062
|
+
if (source?.kind === 'Port' && sourcePeer) {
|
|
3063
|
+
const peers = peerCentersByPort.get(source.id) ?? [];
|
|
3064
|
+
peers.push(sourcePeer);
|
|
3065
|
+
peerCentersByPort.set(source.id, peers);
|
|
3066
|
+
if (target?.kind === 'Port') {
|
|
3067
|
+
const peerPortIds = peerPortIdsByPort.get(source.id) ?? [];
|
|
3068
|
+
peerPortIds.push(target.id);
|
|
3069
|
+
peerPortIdsByPort.set(source.id, peerPortIds);
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
if (target?.kind === 'Port' && targetPeer) {
|
|
3073
|
+
const peers = peerCentersByPort.get(target.id) ?? [];
|
|
3074
|
+
peers.push(targetPeer);
|
|
3075
|
+
peerCentersByPort.set(target.id, peers);
|
|
3076
|
+
if (source?.kind === 'Port') {
|
|
3077
|
+
const peerPortIds = peerPortIdsByPort.get(target.id) ?? [];
|
|
3078
|
+
peerPortIds.push(source.id);
|
|
3079
|
+
peerPortIdsByPort.set(target.id, peerPortIds);
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
const anchorFor = (ownerId: string, side: PortSide, ratio: number): { x: number; y: number } | undefined => {
|
|
3085
|
+
const box = boxes.get(ownerId);
|
|
3086
|
+
if (!box) {
|
|
3087
|
+
return undefined;
|
|
3088
|
+
}
|
|
3089
|
+
const clampedRatio = clamp(ratio, 0.05, 0.95);
|
|
3090
|
+
if (side === 'left') {
|
|
3091
|
+
return { x: box.x, y: box.y + (box.height * clampedRatio) };
|
|
3092
|
+
}
|
|
3093
|
+
if (side === 'right') {
|
|
3094
|
+
return { x: box.x + box.width, y: box.y + (box.height * clampedRatio) };
|
|
3095
|
+
}
|
|
3096
|
+
if (side === 'top') {
|
|
3097
|
+
return { x: box.x + (box.width * clampedRatio), y: box.y };
|
|
3098
|
+
}
|
|
3099
|
+
return { x: box.x + (box.width * clampedRatio), y: box.y + box.height };
|
|
3100
|
+
};
|
|
3101
|
+
|
|
3102
|
+
const currentAnchorForPort = (portId: string): { x: number; y: number } | undefined => {
|
|
3103
|
+
const port = nodeById.get(portId);
|
|
3104
|
+
if (!port?.parentId) {
|
|
3105
|
+
return undefined;
|
|
3106
|
+
}
|
|
3107
|
+
const placement = placements.get(portId) ?? { side: 'right' as PortSide, ratio: 0.5 };
|
|
3108
|
+
return anchorFor(port.parentId, placement.side, placement.ratio);
|
|
3109
|
+
};
|
|
3110
|
+
|
|
3111
|
+
const peerAnchorsForPort = (portId: string): Array<{ x: number; y: number }> => {
|
|
3112
|
+
const peerAnchors: Array<{ x: number; y: number }> = [];
|
|
3113
|
+
for (const peerPortId of peerPortIdsByPort.get(portId) ?? []) {
|
|
3114
|
+
const anchor = currentAnchorForPort(peerPortId);
|
|
3115
|
+
if (anchor) {
|
|
3116
|
+
peerAnchors.push(anchor);
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
for (const peerCenter of peerCentersByPort.get(portId) ?? []) {
|
|
3120
|
+
peerAnchors.push(peerCenter);
|
|
3121
|
+
}
|
|
3122
|
+
return peerAnchors;
|
|
3123
|
+
};
|
|
3124
|
+
|
|
3125
|
+
const candidateCost = (ownerId: string, side: PortSide, portId: string): number => {
|
|
3126
|
+
const owner = ownerCenter(ownerId);
|
|
3127
|
+
const candidate = anchorFor(ownerId, side, 0.5);
|
|
3128
|
+
if (!owner || !candidate) {
|
|
3129
|
+
return Number.POSITIVE_INFINITY;
|
|
3130
|
+
}
|
|
3131
|
+
const peerAnchors = peerAnchorsForPort(portId);
|
|
3132
|
+
if (peerAnchors.length === 0) {
|
|
3133
|
+
return side === 'right' ? 0 : 1;
|
|
3134
|
+
}
|
|
3135
|
+
let cost = 0;
|
|
3136
|
+
for (const peer of peerAnchors) {
|
|
3137
|
+
cost += Math.abs(candidate.x - peer.x) + Math.abs(candidate.y - peer.y);
|
|
3138
|
+
}
|
|
3139
|
+
return cost;
|
|
2488
3140
|
};
|
|
3141
|
+
|
|
3142
|
+
const sideOrder: PortSide[] = ['right', 'left', 'top', 'bottom'];
|
|
3143
|
+
const primaryCoordinate = (side: PortSide, portId: string): number => {
|
|
3144
|
+
const peerAnchors = peerAnchorsForPort(portId);
|
|
3145
|
+
if (peerAnchors.length === 0) {
|
|
3146
|
+
return 0;
|
|
3147
|
+
}
|
|
3148
|
+
const values = peerAnchors.map((peer) => ((side === 'left' || side === 'right') ? peer.y : peer.x));
|
|
3149
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
3150
|
+
};
|
|
3151
|
+
|
|
3152
|
+
for (const [ownerId, ports] of portsByOwner.entries()) {
|
|
3153
|
+
const count = ports.length;
|
|
3154
|
+
for (let index = 0; index < count; index += 1) {
|
|
3155
|
+
placements.set(ports[index].id, {
|
|
3156
|
+
side: 'right',
|
|
3157
|
+
ratio: count <= 0 ? 0.5 : ((index + 1) / (count + 1)),
|
|
3158
|
+
});
|
|
3159
|
+
}
|
|
3160
|
+
const ownerPortIds = portIdsByOwner.get(ownerId) ?? new Set<string>();
|
|
3161
|
+
for (let pass = 0; pass < 2; pass += 1) {
|
|
3162
|
+
for (const port of ports) {
|
|
3163
|
+
let bestSide: PortSide = 'right';
|
|
3164
|
+
let bestCost = Number.POSITIVE_INFINITY;
|
|
3165
|
+
for (const side of sideOrder) {
|
|
3166
|
+
const cost = candidateCost(ownerId, side, port.id);
|
|
3167
|
+
if (cost < bestCost || (cost === bestCost && sideOrder.indexOf(side) < sideOrder.indexOf(bestSide))) {
|
|
3168
|
+
bestCost = cost;
|
|
3169
|
+
bestSide = side;
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
const existing = placements.get(port.id) ?? { ratio: 0.5, side: bestSide };
|
|
3173
|
+
placements.set(port.id, { side: bestSide, ratio: existing.ratio });
|
|
3174
|
+
}
|
|
3175
|
+
const sideGroups = new Map<PortSide, NodeSpec[]>([
|
|
3176
|
+
['left', []],
|
|
3177
|
+
['right', []],
|
|
3178
|
+
['top', []],
|
|
3179
|
+
['bottom', []],
|
|
3180
|
+
]);
|
|
3181
|
+
for (const port of ports) {
|
|
3182
|
+
sideGroups.get(placements.get(port.id)?.side ?? 'right')?.push(port);
|
|
3183
|
+
}
|
|
3184
|
+
for (const [side, sidePorts] of sideGroups.entries()) {
|
|
3185
|
+
sidePorts.sort((left, right) => (
|
|
3186
|
+
primaryCoordinate(side, left.id) - primaryCoordinate(side, right.id)
|
|
3187
|
+
|| left.id.localeCompare(right.id)
|
|
3188
|
+
));
|
|
3189
|
+
for (let index = 0; index < sidePorts.length; index += 1) {
|
|
3190
|
+
placements.set(sidePorts[index].id, {
|
|
3191
|
+
side,
|
|
3192
|
+
ratio: (index + 1) / (sidePorts.length + 1),
|
|
3193
|
+
});
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
void ownerPortIds;
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
return placements;
|
|
2489
3200
|
}
|
|
2490
3201
|
|
|
2491
3202
|
function toPositiveNumber(value: unknown): number | undefined {
|