@dr2rai/raid-canvas 0.3.0 → 0.3.2
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/dist/RaiBridge.d.ts +15 -3
- package/dist/RaiBridge.d.ts.map +1 -1
- package/dist/RaiBridge.js +399 -70
- package/dist/RaiBridge.js.map +1 -1
- package/dist/RaidCanvas.d.ts +2 -0
- package/dist/RaidCanvas.d.ts.map +1 -1
- package/dist/RaidCanvas.js +39 -10
- package/dist/RaidCanvas.js.map +1 -1
- package/dist/X6Shapes.d.ts +7 -0
- package/dist/X6Shapes.d.ts.map +1 -1
- package/dist/X6Shapes.js +146 -22
- package/dist/X6Shapes.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/RaiBridge.ts +454 -75
- package/src/RaidCanvas.tsx +41 -7
- package/src/X6Shapes.ts +151 -22
- package/src/index.ts +1 -0
- package/src/styles/aoaim-theme.css +21 -7
- package/src/types.ts +16 -0
package/src/RaiBridge.ts
CHANGED
|
@@ -24,7 +24,13 @@ import {
|
|
|
24
24
|
type HydrationOptions,
|
|
25
25
|
type SerializationOptions,
|
|
26
26
|
} from './types.js';
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
createAimNode,
|
|
29
|
+
createAimEdge,
|
|
30
|
+
configureAimGraph,
|
|
31
|
+
CascaisPalette,
|
|
32
|
+
wrapAimText,
|
|
33
|
+
} from './X6Shapes.js';
|
|
28
34
|
|
|
29
35
|
export class RaiBridge {
|
|
30
36
|
/**
|
|
@@ -154,6 +160,34 @@ export class RaiBridge {
|
|
|
154
160
|
}
|
|
155
161
|
}
|
|
156
162
|
|
|
163
|
+
// Extract live rendered SVG path data from X6 EdgeView if available
|
|
164
|
+
let pathData: string | undefined = customData.pathData;
|
|
165
|
+
if (!pathData) {
|
|
166
|
+
const edgeView =
|
|
167
|
+
typeof (graph as unknown as { findViewByCell?: (cell: unknown) => unknown }).findViewByCell === 'function'
|
|
168
|
+
? (
|
|
169
|
+
graph as unknown as {
|
|
170
|
+
findViewByCell: (cell: unknown) => {
|
|
171
|
+
getConnectionPathData?: () => string;
|
|
172
|
+
container?: Element;
|
|
173
|
+
} | null;
|
|
174
|
+
}
|
|
175
|
+
).findViewByCell(edge)
|
|
176
|
+
: null;
|
|
177
|
+
|
|
178
|
+
if (edgeView) {
|
|
179
|
+
if (typeof edgeView.getConnectionPathData === 'function') {
|
|
180
|
+
const d = edgeView.getConnectionPathData();
|
|
181
|
+
if (d && d.trim().length > 0) pathData = d;
|
|
182
|
+
}
|
|
183
|
+
if (!pathData && edgeView.container) {
|
|
184
|
+
const pathEl = edgeView.container.querySelector('path[d]');
|
|
185
|
+
const d = pathEl?.getAttribute('d');
|
|
186
|
+
if (d && d.trim().length > 0) pathData = d;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
157
191
|
const edgeData: RaidEdgeData = {
|
|
158
192
|
id: edge.id,
|
|
159
193
|
kind: customData.kind ?? 'association',
|
|
@@ -167,16 +201,20 @@ export class RaiBridge {
|
|
|
167
201
|
...(customData.sourceCardinality !== undefined ? { sourceCardinality: customData.sourceCardinality } : {}),
|
|
168
202
|
...(customData.targetCardinality !== undefined ? { targetCardinality: customData.targetCardinality } : {}),
|
|
169
203
|
bendPoints,
|
|
204
|
+
...(pathData !== undefined ? { pathData } : {}),
|
|
170
205
|
};
|
|
171
206
|
|
|
172
207
|
edges.push(edgeData);
|
|
173
208
|
}
|
|
174
209
|
|
|
210
|
+
const diagramRouting = (graph as unknown as { _aimRoutingMode?: AimRoutingMode })._aimRoutingMode;
|
|
211
|
+
|
|
175
212
|
return {
|
|
176
213
|
diagramId: 'RaidDiagram',
|
|
177
214
|
archetype: 'InteractiveCanvas',
|
|
178
215
|
nodes,
|
|
179
216
|
edges,
|
|
217
|
+
...(diagramRouting !== undefined ? { routing: diagramRouting } : {}),
|
|
180
218
|
};
|
|
181
219
|
}
|
|
182
220
|
|
|
@@ -207,6 +245,79 @@ export class RaiBridge {
|
|
|
207
245
|
return points.map((p) => `${Math.round(p.x)},${Math.round(p.y)}`).join('; ');
|
|
208
246
|
}
|
|
209
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Computes a clean fallback SVG path connecting source and target nodes
|
|
250
|
+
* when running in headless environments (e.g. CLI, tests) without an active DOM.
|
|
251
|
+
*/
|
|
252
|
+
public computeFallbackEdgePath(
|
|
253
|
+
sourceNode: RaidNodeData,
|
|
254
|
+
targetNode: RaidNodeData,
|
|
255
|
+
bendPoints: readonly SvgBendPoint[] = [],
|
|
256
|
+
routingMode: AimRoutingMode = 'manhattan',
|
|
257
|
+
): string {
|
|
258
|
+
const scx = Math.round(sourceNode.bounds.x + sourceNode.bounds.width / 2);
|
|
259
|
+
const scy = Math.round(sourceNode.bounds.y + sourceNode.bounds.height / 2);
|
|
260
|
+
const tcx = Math.round(targetNode.bounds.x + targetNode.bounds.width / 2);
|
|
261
|
+
const tcy = Math.round(targetNode.bounds.y + targetNode.bounds.height / 2);
|
|
262
|
+
|
|
263
|
+
let sx = scx;
|
|
264
|
+
let sy = scy;
|
|
265
|
+
let tx = tcx;
|
|
266
|
+
let ty = tcy;
|
|
267
|
+
|
|
268
|
+
const dx = tcx - scx;
|
|
269
|
+
const dy = tcy - scy;
|
|
270
|
+
|
|
271
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
272
|
+
if (dx > 0) {
|
|
273
|
+
sx = Math.round(sourceNode.bounds.x + sourceNode.bounds.width);
|
|
274
|
+
sy = scy;
|
|
275
|
+
tx = Math.round(targetNode.bounds.x);
|
|
276
|
+
ty = tcy;
|
|
277
|
+
} else {
|
|
278
|
+
sx = Math.round(sourceNode.bounds.x);
|
|
279
|
+
sy = scy;
|
|
280
|
+
tx = Math.round(targetNode.bounds.x + targetNode.bounds.width);
|
|
281
|
+
ty = tcy;
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
if (dy > 0) {
|
|
285
|
+
sx = scx;
|
|
286
|
+
sy = Math.round(sourceNode.bounds.y + sourceNode.bounds.height);
|
|
287
|
+
tx = tcx;
|
|
288
|
+
ty = Math.round(targetNode.bounds.y);
|
|
289
|
+
} else {
|
|
290
|
+
sx = scx;
|
|
291
|
+
sy = Math.round(sourceNode.bounds.y);
|
|
292
|
+
tx = tcx;
|
|
293
|
+
ty = Math.round(targetNode.bounds.y + targetNode.bounds.height);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (bendPoints.length > 0) {
|
|
298
|
+
return `M ${sx} ${sy} ` + bendPoints.map((p) => `L ${Math.round(p.x)} ${Math.round(p.y)}`).join(' ') + ` L ${tx} ${ty}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (routingMode === 'normal') {
|
|
302
|
+
return `M ${sx} ${sy} L ${tx} ${ty}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (routingMode === 'smooth') {
|
|
306
|
+
const midX = Math.round((sx + tx) / 2);
|
|
307
|
+
const midY = Math.round((sy + ty) / 2);
|
|
308
|
+
return `M ${sx} ${sy} Q ${midX} ${sy} ${midX} ${midY} T ${tx} ${ty}`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Manhattan orthogonal default
|
|
312
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
313
|
+
const midX = Math.round((sx + tx) / 2);
|
|
314
|
+
return `M ${sx} ${sy} L ${midX} ${sy} L ${midX} ${ty} L ${tx} ${ty}`;
|
|
315
|
+
} else {
|
|
316
|
+
const midY = Math.round((sy + ty) / 2);
|
|
317
|
+
return `M ${sx} ${sy} L ${sx} ${midY} L ${tx} ${midY} L ${tx} ${ty}`;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
210
321
|
// --------------------------------------------------------------------------
|
|
211
322
|
// Private Helper Implementation
|
|
212
323
|
// --------------------------------------------------------------------------
|
|
@@ -296,8 +407,8 @@ export class RaiBridge {
|
|
|
296
407
|
const sourceNode = nodes.find((n) => n.id === sourceId);
|
|
297
408
|
const targetNode = nodes.find((n) => n.id === targetId);
|
|
298
409
|
|
|
299
|
-
// Infer optimal orthogonal docking ports if
|
|
300
|
-
if (sourceNode && targetNode) {
|
|
410
|
+
// Infer optimal orthogonal docking ports only if explicitly requested in options
|
|
411
|
+
if (options.inferPorts === true && sourceNode && targetNode) {
|
|
301
412
|
const dx =
|
|
302
413
|
targetNode.bounds.x +
|
|
303
414
|
targetNode.bounds.width / 2 -
|
|
@@ -349,10 +460,7 @@ export class RaiBridge {
|
|
|
349
460
|
el.getAttribute(AimSvgContract.ATTR_ROUTING) ??
|
|
350
461
|
el.getAttribute('aim-routing') ??
|
|
351
462
|
undefined;
|
|
352
|
-
const routing =
|
|
353
|
-
rawRouting === 'normal' || rawRouting === 'smooth' || rawRouting === 'manhattan'
|
|
354
|
-
? (rawRouting as AimRoutingMode)
|
|
355
|
-
: undefined;
|
|
463
|
+
const routing = rawRouting ? this.normalizeRouting(rawRouting) : undefined;
|
|
356
464
|
|
|
357
465
|
edges.push({
|
|
358
466
|
id,
|
|
@@ -369,11 +477,18 @@ export class RaiBridge {
|
|
|
369
477
|
});
|
|
370
478
|
}
|
|
371
479
|
|
|
480
|
+
const rawDiagramRouting =
|
|
481
|
+
svgRoot.getAttribute(AimSvgContract.ATTR_ROUTING) ??
|
|
482
|
+
svgRoot.getAttribute('aim-routing') ??
|
|
483
|
+
undefined;
|
|
484
|
+
const diagramRouting = rawDiagramRouting ? this.normalizeRouting(rawDiagramRouting) : undefined;
|
|
485
|
+
|
|
372
486
|
return {
|
|
373
487
|
diagramId: svgRoot.getAttribute('id') ?? 'ImportedDiagram',
|
|
374
488
|
archetype: svgRoot.getAttribute('aim-archetype') ?? 'AOAIMDiagram',
|
|
375
489
|
nodes,
|
|
376
490
|
edges,
|
|
491
|
+
...(diagramRouting !== undefined ? { routing: diagramRouting } : {}),
|
|
377
492
|
};
|
|
378
493
|
}
|
|
379
494
|
|
|
@@ -397,7 +512,7 @@ export class RaiBridge {
|
|
|
397
512
|
}
|
|
398
513
|
|
|
399
514
|
// Check direct geometry attributes (rect, ellipse, circle)
|
|
400
|
-
const rect = el.querySelector('rect') ?? (el.tagName
|
|
515
|
+
const rect = el.querySelector?.('rect') ?? (el.tagName?.toLowerCase() === 'rect' ? el : null);
|
|
401
516
|
if (rect) {
|
|
402
517
|
if (!transform && rect.getAttribute('x')) x = parseFloat(rect.getAttribute('x')!);
|
|
403
518
|
if (!transform && rect.getAttribute('y')) y = parseFloat(rect.getAttribute('y')!);
|
|
@@ -406,7 +521,7 @@ export class RaiBridge {
|
|
|
406
521
|
}
|
|
407
522
|
|
|
408
523
|
const ellipse =
|
|
409
|
-
el.querySelector('ellipse') ?? (el.tagName
|
|
524
|
+
el.querySelector?.('ellipse') ?? (el.tagName?.toLowerCase() === 'ellipse' ? el : null);
|
|
410
525
|
if (ellipse) {
|
|
411
526
|
const rx = parseFloat(ellipse.getAttribute('rx') ?? `${width / 2}`);
|
|
412
527
|
const ry = parseFloat(ellipse.getAttribute('ry') ?? `${height / 2}`);
|
|
@@ -423,7 +538,7 @@ export class RaiBridge {
|
|
|
423
538
|
return { x, y, width, height };
|
|
424
539
|
}
|
|
425
540
|
|
|
426
|
-
|
|
541
|
+
public updateExistingSvg(
|
|
427
542
|
baseSvg: string,
|
|
428
543
|
model: RaidMetamodel,
|
|
429
544
|
_options: SerializationOptions,
|
|
@@ -435,65 +550,313 @@ export class RaiBridge {
|
|
|
435
550
|
const parser = new DOMParser();
|
|
436
551
|
const doc = parser.parseFromString(baseSvg, 'image/svg+xml');
|
|
437
552
|
|
|
438
|
-
//
|
|
553
|
+
// Ensure defs and arrow markers exist for external vector viewers (Preview, Chrome, Safari)
|
|
554
|
+
let defs = doc.querySelector('defs');
|
|
555
|
+
if (!defs) {
|
|
556
|
+
defs = doc.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
|
557
|
+
doc.documentElement.insertBefore(defs, doc.documentElement.firstChild);
|
|
558
|
+
}
|
|
559
|
+
if (!doc.querySelector('#arrow-classic')) {
|
|
560
|
+
const marker = doc.createElementNS('http://www.w3.org/2000/svg', 'marker');
|
|
561
|
+
marker.setAttribute('id', 'arrow-classic');
|
|
562
|
+
marker.setAttribute('viewBox', '0 0 10 10');
|
|
563
|
+
marker.setAttribute('refX', '10');
|
|
564
|
+
marker.setAttribute('refY', '5');
|
|
565
|
+
marker.setAttribute('markerWidth', '7');
|
|
566
|
+
marker.setAttribute('markerHeight', '7');
|
|
567
|
+
marker.setAttribute('orient', 'auto-start-reverse');
|
|
568
|
+
const path = doc.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
569
|
+
path.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z');
|
|
570
|
+
path.setAttribute('fill', CascaisPalette.WarmGraphite);
|
|
571
|
+
marker.appendChild(path);
|
|
572
|
+
defs.appendChild(marker);
|
|
573
|
+
}
|
|
574
|
+
if (!doc.querySelector('#arrow-hollow')) {
|
|
575
|
+
const marker = doc.createElementNS('http://www.w3.org/2000/svg', 'marker');
|
|
576
|
+
marker.setAttribute('id', 'arrow-hollow');
|
|
577
|
+
marker.setAttribute('viewBox', '0 0 12 12');
|
|
578
|
+
marker.setAttribute('refX', '12');
|
|
579
|
+
marker.setAttribute('refY', '6');
|
|
580
|
+
marker.setAttribute('markerWidth', '9');
|
|
581
|
+
marker.setAttribute('markerHeight', '9');
|
|
582
|
+
marker.setAttribute('orient', 'auto-start-reverse');
|
|
583
|
+
const polygon = doc.createElementNS('http://www.w3.org/2000/svg', 'polygon');
|
|
584
|
+
polygon.setAttribute('points', '0 0, 12 6, 0 12');
|
|
585
|
+
polygon.setAttribute('fill', CascaisPalette.ChalkWhite);
|
|
586
|
+
polygon.setAttribute('stroke', CascaisPalette.WarmGraphite);
|
|
587
|
+
polygon.setAttribute('stroke-width', '1.5');
|
|
588
|
+
marker.appendChild(polygon);
|
|
589
|
+
defs.appendChild(marker);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Ensure AOAIM style rules are present
|
|
593
|
+
let style: Element | null = doc.querySelector('style');
|
|
594
|
+
if (!style) {
|
|
595
|
+
style = doc.createElementNS('http://www.w3.org/2000/svg', 'style');
|
|
596
|
+
defs.appendChild(style);
|
|
597
|
+
}
|
|
598
|
+
if (style && !style.textContent?.includes('.aim-act text')) {
|
|
599
|
+
style.textContent = (style.textContent ? style.textContent + '\n' : '') + `
|
|
600
|
+
.aim-node { cursor: pointer; transition: filter 0.15s ease; }
|
|
601
|
+
.aim-node:hover { filter: drop-shadow(0 4px 6px rgba(0,0,0,0.1)); }
|
|
602
|
+
.aim-edge { fill: none; stroke: ${CascaisPalette.WarmGraphite}; stroke-width: 1.5; }
|
|
603
|
+
text { font-family: Inter, system-ui, sans-serif; }
|
|
604
|
+
.aim-act text, .aim-obj text { text-decoration: underline; }
|
|
605
|
+
`;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Update diagram-level routing mode on root <svg>
|
|
609
|
+
const diagramRouting = _options.routingMode ?? model.routing;
|
|
610
|
+
if (diagramRouting) {
|
|
611
|
+
doc.documentElement.setAttribute(AimSvgContract.ATTR_ROUTING, diagramRouting);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Ensure layers exist
|
|
615
|
+
let nodesLayer = doc.querySelector('.aim-nodes-layer');
|
|
616
|
+
if (!nodesLayer) {
|
|
617
|
+
nodesLayer = doc.documentElement;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Remove deleted nodes
|
|
621
|
+
const existingNodeEls = Array.from(doc.querySelectorAll(AimSvgContract.SELECTOR_NODE));
|
|
622
|
+
for (const nodeEl of existingNodeEls) {
|
|
623
|
+
const id = nodeEl.getAttribute(AimSvgContract.ATTR_ID) ?? nodeEl.getAttribute('id');
|
|
624
|
+
if (id && !model.nodes.some((n) => n.id === id)) {
|
|
625
|
+
nodeEl.remove();
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Update or insert nodes with canonical archetype shape markup
|
|
439
630
|
for (const node of model.nodes) {
|
|
440
|
-
|
|
441
|
-
if (el) {
|
|
442
|
-
el.
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
631
|
+
let el = doc.querySelector(`[${AimSvgContract.ATTR_ID}="${node.id}"], [id="${node.id}"]`);
|
|
632
|
+
if (!el) {
|
|
633
|
+
el = doc.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
634
|
+
nodesLayer.appendChild(el);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
el.setAttribute('transform', `translate(${node.bounds.x}, ${node.bounds.y})`);
|
|
638
|
+
el.setAttribute(AimSvgContract.ATTR_NODE, 'true');
|
|
639
|
+
el.setAttribute(AimSvgContract.ATTR_ID, node.id);
|
|
640
|
+
el.setAttribute(AimSvgContract.ATTR_KIND, node.kind);
|
|
641
|
+
el.setAttribute(AimSvgContract.ATTR_DISPLAY_NAME, node.displayName);
|
|
642
|
+
if (node.stereotype) {
|
|
643
|
+
el.setAttribute(AimSvgContract.ATTR_STEREOTYPE, node.stereotype);
|
|
644
|
+
} else {
|
|
645
|
+
el.removeAttribute(AimSvgContract.ATTR_STEREOTYPE);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Replace inner content with canonical archetype shape markup
|
|
649
|
+
const innerSvg = this.renderNodeInnerSvg(node);
|
|
650
|
+
const fragmentDoc = parser.parseFromString(
|
|
651
|
+
`<g xmlns="http://www.w3.org/2000/svg">${innerSvg}</g>`,
|
|
652
|
+
'image/svg+xml',
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
while (el.firstChild) {
|
|
656
|
+
el.removeChild(el.firstChild);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
for (const child of Array.from(fragmentDoc.documentElement.childNodes)) {
|
|
660
|
+
el.appendChild(doc.importNode(child, true));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
let edgesLayer = doc.querySelector('.aim-edges-layer');
|
|
665
|
+
if (!edgesLayer) {
|
|
666
|
+
edgesLayer = doc.documentElement;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Remove deleted edges
|
|
670
|
+
const existingEdgeEls = Array.from(doc.querySelectorAll(AimSvgContract.SELECTOR_EDGE));
|
|
671
|
+
for (const edgeEl of existingEdgeEls) {
|
|
672
|
+
const id = edgeEl.getAttribute(AimSvgContract.ATTR_ID) ?? edgeEl.getAttribute('id');
|
|
673
|
+
if (id && !model.edges.some((e) => e.id === id)) {
|
|
674
|
+
edgeEl.remove();
|
|
451
675
|
}
|
|
452
676
|
}
|
|
453
677
|
|
|
454
|
-
// Update
|
|
678
|
+
// Update or insert edges with live path geometry
|
|
455
679
|
for (const edge of model.edges) {
|
|
456
|
-
|
|
457
|
-
if (el) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
el.setAttribute(AimSvgContract.ATTR_SOURCE, edge.sourceId);
|
|
680
|
+
let el = doc.querySelector(`[${AimSvgContract.ATTR_ID}="${edge.id}"], [id="${edge.id}"]`);
|
|
681
|
+
if (!el) {
|
|
682
|
+
el = doc.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
683
|
+
if (nodesLayer && nodesLayer.parentNode === doc.documentElement) {
|
|
684
|
+
doc.documentElement.insertBefore(el, nodesLayer);
|
|
685
|
+
} else {
|
|
686
|
+
edgesLayer.appendChild(el);
|
|
464
687
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const bendsString = this.formatBendPoints(edge.bendPoints);
|
|
691
|
+
el.setAttribute(AimSvgContract.ATTR_BENDS, bendsString);
|
|
692
|
+
el.setAttribute(AimSvgContract.ATTR_EDGE, 'true');
|
|
693
|
+
el.setAttribute(AimSvgContract.ATTR_ID, edge.id);
|
|
694
|
+
el.setAttribute(AimSvgContract.ATTR_EDGE_KIND, edge.kind);
|
|
695
|
+
if (edge.sourceId !== undefined) {
|
|
696
|
+
el.setAttribute(AimSvgContract.ATTR_SOURCE, edge.sourceId);
|
|
697
|
+
}
|
|
698
|
+
if (edge.targetId !== undefined) {
|
|
699
|
+
el.setAttribute(AimSvgContract.ATTR_TARGET, edge.targetId);
|
|
700
|
+
}
|
|
701
|
+
if (edge.sourcePort !== undefined && edge.sourcePort !== '' && edge.sourcePort !== 'auto') {
|
|
702
|
+
el.setAttribute(AimSvgContract.ATTR_SOURCE_PORT, edge.sourcePort);
|
|
703
|
+
} else {
|
|
704
|
+
el.removeAttribute(AimSvgContract.ATTR_SOURCE_PORT);
|
|
705
|
+
}
|
|
706
|
+
if (edge.targetPort !== undefined && edge.targetPort !== '' && edge.targetPort !== 'auto') {
|
|
707
|
+
el.setAttribute(AimSvgContract.ATTR_TARGET_PORT, edge.targetPort);
|
|
708
|
+
} else {
|
|
709
|
+
el.removeAttribute(AimSvgContract.ATTR_TARGET_PORT);
|
|
710
|
+
}
|
|
711
|
+
if (edge.routing !== undefined) {
|
|
712
|
+
el.setAttribute(AimSvgContract.ATTR_ROUTING, edge.routing);
|
|
713
|
+
}
|
|
714
|
+
if (edge.stereotype !== undefined) {
|
|
715
|
+
el.setAttribute(AimSvgContract.ATTR_STEREOTYPE, edge.stereotype);
|
|
716
|
+
}
|
|
717
|
+
if (edge.sourceCardinality !== undefined) {
|
|
718
|
+
el.setAttribute('aim-source-cardinality', edge.sourceCardinality);
|
|
719
|
+
}
|
|
720
|
+
if (edge.targetCardinality !== undefined) {
|
|
721
|
+
el.setAttribute('aim-target-cardinality', edge.targetCardinality);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Update or inject <path class="aim-edge"> with rendered path geometry
|
|
725
|
+
let pathD = edge.pathData;
|
|
726
|
+
if (!pathD) {
|
|
727
|
+
const sourceNode = model.nodes.find((n) => n.id === edge.sourceId);
|
|
728
|
+
const targetNode = model.nodes.find((n) => n.id === edge.targetId);
|
|
729
|
+
if (sourceNode && targetNode) {
|
|
730
|
+
pathD = this.computeFallbackEdgePath(sourceNode, targetNode, edge.bendPoints, edge.routing ?? model.routing);
|
|
482
731
|
}
|
|
483
|
-
|
|
484
|
-
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
let pathEl = el.querySelector('path.aim-edge') ?? el.querySelector('path');
|
|
735
|
+
if (!pathEl) {
|
|
736
|
+
pathEl = doc.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
737
|
+
pathEl.setAttribute('class', 'aim-edge');
|
|
738
|
+
el.appendChild(pathEl);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (pathD) {
|
|
742
|
+
pathEl.setAttribute('d', pathD);
|
|
743
|
+
}
|
|
744
|
+
pathEl.setAttribute('fill', 'none');
|
|
745
|
+
pathEl.setAttribute('stroke', CascaisPalette.WarmGraphite);
|
|
746
|
+
pathEl.setAttribute('stroke-width', '1.5');
|
|
747
|
+
|
|
748
|
+
if (edge.kind === 'dependency') {
|
|
749
|
+
pathEl.setAttribute('stroke-dasharray', '5,5');
|
|
750
|
+
} else {
|
|
751
|
+
pathEl.removeAttribute('stroke-dasharray');
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const markerEnd = edge.kind === 'generalization' ? 'url(#arrow-hollow)' : 'url(#arrow-classic)';
|
|
755
|
+
pathEl.setAttribute('marker-end', markerEnd);
|
|
756
|
+
|
|
757
|
+
// Update or inject edge label
|
|
758
|
+
if (edge.label) {
|
|
759
|
+
let textEl = el.querySelector('text');
|
|
760
|
+
if (!textEl) {
|
|
761
|
+
textEl = doc.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
762
|
+
el.appendChild(textEl);
|
|
485
763
|
}
|
|
764
|
+
const midPoint =
|
|
765
|
+
edge.bendPoints.length > 0
|
|
766
|
+
? (edge.bendPoints[Math.floor(edge.bendPoints.length / 2)] ?? { x: 50, y: 50 })
|
|
767
|
+
: (() => {
|
|
768
|
+
const s = model.nodes.find((n) => n.id === edge.sourceId);
|
|
769
|
+
const t = model.nodes.find((n) => n.id === edge.targetId);
|
|
770
|
+
if (s && t) {
|
|
771
|
+
return {
|
|
772
|
+
x: Math.round((s.bounds.x + s.bounds.width / 2 + t.bounds.x + t.bounds.width / 2) / 2),
|
|
773
|
+
y: Math.round((s.bounds.y + s.bounds.height / 2 + t.bounds.y + t.bounds.height / 2) / 2),
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
return { x: 50, y: 50 };
|
|
777
|
+
})();
|
|
778
|
+
textEl.setAttribute('x', `${midPoint.x}`);
|
|
779
|
+
textEl.setAttribute('y', `${midPoint.y - 8}`);
|
|
780
|
+
textEl.setAttribute('font-size', '11');
|
|
781
|
+
textEl.setAttribute('fill', CascaisPalette.TextSecondary);
|
|
782
|
+
textEl.setAttribute('text-anchor', 'middle');
|
|
783
|
+
textEl.textContent = edge.label;
|
|
486
784
|
}
|
|
487
785
|
}
|
|
488
786
|
|
|
489
787
|
return new XMLSerializer().serializeToString(doc);
|
|
490
788
|
}
|
|
491
789
|
|
|
492
|
-
private
|
|
790
|
+
private renderSvgText(
|
|
791
|
+
text: string,
|
|
792
|
+
cx: number,
|
|
793
|
+
cy: number,
|
|
794
|
+
fontSize: number,
|
|
795
|
+
fontWeight: string,
|
|
796
|
+
fill: string,
|
|
797
|
+
underline: boolean = false,
|
|
798
|
+
): string {
|
|
799
|
+
const wrapped = wrapAimText(text);
|
|
800
|
+
const lines = wrapped.split('\n');
|
|
801
|
+
const underlineAttr = underline ? ' text-decoration="underline"' : '';
|
|
802
|
+
const weightAttr = fontWeight !== 'normal' ? ` font-weight="${fontWeight}"` : '';
|
|
803
|
+
|
|
804
|
+
if (lines.length <= 1) {
|
|
805
|
+
return ` <text x="${cx}" y="${cy}" font-size="${fontSize}"${weightAttr} fill="${fill}" text-anchor="middle" dominant-baseline="central"${underlineAttr}>${lines[0] ?? ''}</text>\n`;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const lineHeight = fontSize * 1.25;
|
|
809
|
+
const startY = cy - ((lines.length - 1) * lineHeight) / 2;
|
|
810
|
+
|
|
811
|
+
let tspans = '';
|
|
812
|
+
lines.forEach((line, idx) => {
|
|
813
|
+
const y = Math.round(startY + idx * lineHeight);
|
|
814
|
+
tspans += `<tspan x="${cx}" y="${y}">${line}</tspan>`;
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
return ` <text font-size="${fontSize}"${weightAttr} fill="${fill}" text-anchor="middle" dominant-baseline="central"${underlineAttr}>${tspans}</text>\n`;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Generates the canonical inner SVG elements (shapes and styled text)
|
|
822
|
+
* for a given AOAIM node archetype.
|
|
823
|
+
*/
|
|
824
|
+
public renderNodeInnerSvg(node: RaidNodeData): string {
|
|
825
|
+
let svg = '';
|
|
826
|
+
if (node.kind === 'uc') {
|
|
827
|
+
const rx = node.bounds.width / 2;
|
|
828
|
+
const ry = node.bounds.height / 2;
|
|
829
|
+
svg += ` <ellipse cx="${rx}" cy="${ry}" rx="${rx}" ry="${ry}" fill="${CascaisPalette.ChalkWhite}" stroke="${CascaisPalette.NetGold}" stroke-width="2" />\n`;
|
|
830
|
+
svg += this.renderSvgText(node.displayName, rx, ry, 13, 'bold', CascaisPalette.TextPrimary, false);
|
|
831
|
+
} else if (node.kind === 'act') {
|
|
832
|
+
svg += ` <rect width="${node.bounds.width}" height="${node.bounds.height}" rx="12" ry="12" fill="${CascaisPalette.CanvasCream}" stroke="${CascaisPalette.HeraldicGreen}" stroke-width="2" />\n`;
|
|
833
|
+
svg += this.renderSvgText(node.displayName, node.bounds.width / 2, node.bounds.height / 2, 13, '600', CascaisPalette.TextPrimary, true);
|
|
834
|
+
} else if (node.kind === 'obj') {
|
|
835
|
+
svg += ` <rect width="${node.bounds.width}" height="${node.bounds.height}" fill="${CascaisPalette.ChalkWhite}" stroke="${CascaisPalette.SilverLineDark}" stroke-width="1.5" />\n`;
|
|
836
|
+
svg += this.renderSvgText(node.displayName, node.bounds.width / 2, node.bounds.height / 2, 12, 'normal', CascaisPalette.TextPrimary, true);
|
|
837
|
+
} else if (node.kind === 'per') {
|
|
838
|
+
const isInitiating = node.stereotype?.toLowerCase().includes('initiates') ?? false;
|
|
839
|
+
const strokeColor = isInitiating ? CascaisPalette.NetGold : CascaisPalette.WarmGraphite;
|
|
840
|
+
const cx = Math.round(node.bounds.width / 2);
|
|
841
|
+
svg += ` <rect width="${node.bounds.width}" height="${node.bounds.height}" fill="none" stroke="none" />\n`;
|
|
842
|
+
svg += ` <path d="M 61 50 v -4 a 8 8 0 0 0 -8 -8 H 37 a 8 8 0 0 0 -8 8 v 4" fill="none" stroke="${strokeColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />\n`;
|
|
843
|
+
svg += ` <circle cx="45" cy="22" r="8" fill="${CascaisPalette.ChalkWhite}" stroke="${strokeColor}" stroke-width="2" />\n`;
|
|
844
|
+
svg += this.renderSvgText(node.displayName, cx, 68, 12, '500', CascaisPalette.TextPrimary, false);
|
|
845
|
+
} else {
|
|
846
|
+
svg += ` <rect width="${node.bounds.width}" height="${node.bounds.height}" fill="${CascaisPalette.ChalkWhite}" stroke="${CascaisPalette.WarmGraphite}" stroke-width="1.5" />\n`;
|
|
847
|
+
svg += this.renderSvgText(node.displayName, node.bounds.width / 2, node.bounds.height / 2, 12, 'normal', CascaisPalette.TextPrimary, false);
|
|
848
|
+
}
|
|
849
|
+
return svg;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
public generateFreshSvg(model: RaidMetamodel, options: SerializationOptions): string {
|
|
493
853
|
const width = Math.max(800, ...model.nodes.map((n) => n.bounds.x + n.bounds.width + 100));
|
|
494
854
|
const height = Math.max(600, ...model.nodes.map((n) => n.bounds.y + n.bounds.height + 100));
|
|
495
855
|
|
|
496
|
-
|
|
856
|
+
const diagramRouting = options.routingMode ?? model.routing;
|
|
857
|
+
const routingAttr = diagramRouting ? ` ${AimSvgContract.ATTR_ROUTING}="${diagramRouting}"` : '';
|
|
858
|
+
|
|
859
|
+
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" id="${model.diagramId}" aim-archetype="${model.archetype}"${routingAttr}>\n`;
|
|
497
860
|
|
|
498
861
|
// Definitions & Markers
|
|
499
862
|
svg += ` <defs>\n`;
|
|
@@ -509,6 +872,7 @@ export class RaiBridge {
|
|
|
509
872
|
svg += ` .aim-node:hover { filter: drop-shadow(0 4px 6px rgba(0,0,0,0.1)); }\n`;
|
|
510
873
|
svg += ` .aim-edge { fill: none; stroke: ${CascaisPalette.WarmGraphite}; stroke-width: 1.5; }\n`;
|
|
511
874
|
svg += ` text { font-family: Inter, system-ui, sans-serif; }\n`;
|
|
875
|
+
svg += ` .aim-act text, .aim-obj text { text-decoration: underline; }\n`;
|
|
512
876
|
svg += ` </style>\n`;
|
|
513
877
|
}
|
|
514
878
|
svg += ` </defs>\n\n`;
|
|
@@ -522,22 +886,41 @@ export class RaiBridge {
|
|
|
522
886
|
const markerEnd = edge.kind === 'generalization' ? ' marker-end="url(#arrow-hollow)"' : ' marker-end="url(#arrow-classic)"';
|
|
523
887
|
|
|
524
888
|
// Path data construction
|
|
525
|
-
let pathD = '';
|
|
526
|
-
if (
|
|
527
|
-
const
|
|
528
|
-
|
|
889
|
+
let pathD = edge.pathData ?? '';
|
|
890
|
+
if (!pathD) {
|
|
891
|
+
const sourceNode = model.nodes.find((n) => n.id === edge.sourceId);
|
|
892
|
+
const targetNode = model.nodes.find((n) => n.id === edge.targetId);
|
|
893
|
+
if (sourceNode && targetNode) {
|
|
894
|
+
pathD = this.computeFallbackEdgePath(sourceNode, targetNode, edge.bendPoints, edge.routing ?? model.routing);
|
|
895
|
+
} else if (edge.bendPoints.length > 0) {
|
|
896
|
+
const first = edge.bendPoints[0]!;
|
|
897
|
+
pathD = `M ${first.x} ${first.y} ` + edge.bendPoints.slice(1).map((p) => `L ${p.x} ${p.y}`).join(' ');
|
|
898
|
+
}
|
|
529
899
|
}
|
|
530
900
|
|
|
531
|
-
const sourcePortAttr = edge.sourcePort ? ` ${AimSvgContract.ATTR_SOURCE_PORT}="${edge.sourcePort}"` : '';
|
|
532
|
-
const targetPortAttr = edge.targetPort ? ` ${AimSvgContract.ATTR_TARGET_PORT}="${edge.targetPort}"` : '';
|
|
901
|
+
const sourcePortAttr = edge.sourcePort && edge.sourcePort !== 'auto' ? ` ${AimSvgContract.ATTR_SOURCE_PORT}="${edge.sourcePort}"` : '';
|
|
902
|
+
const targetPortAttr = edge.targetPort && edge.targetPort !== 'auto' ? ` ${AimSvgContract.ATTR_TARGET_PORT}="${edge.targetPort}"` : '';
|
|
533
903
|
const routingAttr = edge.routing ? ` ${AimSvgContract.ATTR_ROUTING}="${edge.routing}"` : '';
|
|
534
904
|
|
|
535
905
|
svg += ` <g ${AimSvgContract.ATTR_EDGE}="true" ${AimSvgContract.ATTR_ID}="${edge.id}" ${AimSvgContract.ATTR_EDGE_KIND}="${edge.kind}" ${AimSvgContract.ATTR_SOURCE}="${edge.sourceId}" ${AimSvgContract.ATTR_TARGET}="${edge.targetId}"${sourcePortAttr}${targetPortAttr}${routingAttr} ${AimSvgContract.ATTR_BENDS}="${bendsFormatted}">\n`;
|
|
536
906
|
if (pathD) {
|
|
537
|
-
svg += ` <path d="${pathD}" class="aim-edge"${strokeDash}${markerEnd} />\n`;
|
|
907
|
+
svg += ` <path d="${pathD}" class="aim-edge" fill="none" stroke="${CascaisPalette.WarmGraphite}" stroke-width="1.5"${strokeDash}${markerEnd} />\n`;
|
|
538
908
|
}
|
|
539
909
|
if (edge.label) {
|
|
540
|
-
const midPoint =
|
|
910
|
+
const midPoint =
|
|
911
|
+
edge.bendPoints.length > 0
|
|
912
|
+
? (edge.bendPoints[Math.floor(edge.bendPoints.length / 2)] ?? { x: 50, y: 50 })
|
|
913
|
+
: (() => {
|
|
914
|
+
const s = model.nodes.find((n) => n.id === edge.sourceId);
|
|
915
|
+
const t = model.nodes.find((n) => n.id === edge.targetId);
|
|
916
|
+
if (s && t) {
|
|
917
|
+
return {
|
|
918
|
+
x: Math.round((s.bounds.x + s.bounds.width / 2 + t.bounds.x + t.bounds.width / 2) / 2),
|
|
919
|
+
y: Math.round((s.bounds.y + s.bounds.height / 2 + t.bounds.y + t.bounds.height / 2) / 2),
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
return { x: 50, y: 50 };
|
|
923
|
+
})();
|
|
541
924
|
svg += ` <text x="${midPoint.x}" y="${midPoint.y - 8}" font-size="11" fill="${CascaisPalette.TextSecondary}" text-anchor="middle">${edge.label}</text>\n`;
|
|
542
925
|
}
|
|
543
926
|
svg += ` </g>\n`;
|
|
@@ -548,21 +931,9 @@ export class RaiBridge {
|
|
|
548
931
|
svg += ` <!-- Nodes -->\n`;
|
|
549
932
|
svg += ` <g class="aim-nodes-layer">\n`;
|
|
550
933
|
for (const node of model.nodes) {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const rx = node.bounds.width / 2;
|
|
555
|
-
const ry = node.bounds.height / 2;
|
|
556
|
-
svg += ` <ellipse cx="${rx}" cy="${ry}" rx="${rx}" ry="${ry}" fill="${CascaisPalette.ChalkWhite}" stroke="${CascaisPalette.NetGold}" stroke-width="2" />\n`;
|
|
557
|
-
svg += ` <text x="${rx}" y="${ry}" font-size="13" font-weight="bold" fill="${CascaisPalette.TextPrimary}" text-anchor="middle" dominant-baseline="central">${node.displayName}</text>\n`;
|
|
558
|
-
} else if (node.kind === 'act') {
|
|
559
|
-
svg += ` <rect width="${node.bounds.width}" height="${node.bounds.height}" rx="12" ry="12" fill="${CascaisPalette.CanvasCream}" stroke="${CascaisPalette.HeraldicGreen}" stroke-width="2" />\n`;
|
|
560
|
-
svg += ` <text x="${node.bounds.width / 2}" y="${node.bounds.height / 2}" font-size="13" font-weight="600" fill="${CascaisPalette.TextPrimary}" text-anchor="middle" dominant-baseline="central">${node.displayName}</text>\n`;
|
|
561
|
-
} else {
|
|
562
|
-
svg += ` <rect width="${node.bounds.width}" height="${node.bounds.height}" fill="${CascaisPalette.ChalkWhite}" stroke="${CascaisPalette.WarmGraphite}" stroke-width="1.5" />\n`;
|
|
563
|
-
svg += ` <text x="${node.bounds.width / 2}" y="${node.bounds.height / 2}" font-size="12" fill="${CascaisPalette.TextPrimary}" text-anchor="middle" dominant-baseline="central">${node.displayName}</text>\n`;
|
|
564
|
-
}
|
|
565
|
-
|
|
934
|
+
const stereotypeAttr = node.stereotype ? ` ${AimSvgContract.ATTR_STEREOTYPE}="${node.stereotype}"` : '';
|
|
935
|
+
svg += ` <g ${AimSvgContract.ATTR_NODE}="true" ${AimSvgContract.ATTR_ID}="${node.id}" ${AimSvgContract.ATTR_KIND}="${node.kind}" ${AimSvgContract.ATTR_DISPLAY_NAME}="${node.displayName}"${stereotypeAttr} transform="translate(${node.bounds.x}, ${node.bounds.y})">\n`;
|
|
936
|
+
svg += this.renderNodeInnerSvg(node);
|
|
566
937
|
svg += ` </g>\n`;
|
|
567
938
|
}
|
|
568
939
|
svg += ` </g>\n`;
|
|
@@ -571,6 +942,14 @@ export class RaiBridge {
|
|
|
571
942
|
return svg;
|
|
572
943
|
}
|
|
573
944
|
|
|
945
|
+
public normalizeRouting(raw: string): AimRoutingMode {
|
|
946
|
+
const lower = raw.toLowerCase().trim();
|
|
947
|
+
if (lower === 'orthogonal' || lower === 'manhattan') return 'manhattan';
|
|
948
|
+
if (lower === 'straight' || lower === 'normal') return 'normal';
|
|
949
|
+
if (lower === 'curved' || lower === 'smooth') return 'smooth';
|
|
950
|
+
return 'manhattan';
|
|
951
|
+
}
|
|
952
|
+
|
|
574
953
|
private normalizeKind(kind: string): AimOntologyKind {
|
|
575
954
|
const lower = kind.toLowerCase();
|
|
576
955
|
if (lower === 'uc' || lower === 'usecase') return 'uc';
|