@dr2rai/raid-canvas 0.2.0 → 0.3.1
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 +10 -3
- package/dist/RaiBridge.d.ts.map +1 -1
- package/dist/RaiBridge.js +288 -16
- package/dist/RaiBridge.js.map +1 -1
- package/dist/RaidCanvas.d.ts +10 -2
- package/dist/RaidCanvas.d.ts.map +1 -1
- package/dist/RaidCanvas.js +169 -27
- package/dist/RaidCanvas.js.map +1 -1
- package/dist/X6Shapes.d.ts +15 -1
- package/dist/X6Shapes.d.ts.map +1 -1
- package/dist/X6Shapes.js +196 -22
- package/dist/X6Shapes.js.map +1 -1
- package/dist/index.d.ts +2 -2
- 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 +23 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/RaiBridge.ts +332 -19
- package/src/RaidCanvas.tsx +183 -25
- package/src/X6Shapes.ts +204 -22
- package/src/index.ts +3 -0
- package/src/styles/aoaim-theme.css +21 -7
- package/src/types.ts +29 -1
package/src/RaiBridge.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
AimSvgContract,
|
|
16
16
|
type AimOntologyKind,
|
|
17
17
|
type AimEdgeKind,
|
|
18
|
+
type AimRoutingMode,
|
|
18
19
|
type RaidNodeData,
|
|
19
20
|
type RaidEdgeData,
|
|
20
21
|
type RaidMetamodel,
|
|
@@ -23,7 +24,13 @@ import {
|
|
|
23
24
|
type HydrationOptions,
|
|
24
25
|
type SerializationOptions,
|
|
25
26
|
} from './types.js';
|
|
26
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
createAimNode,
|
|
29
|
+
createAimEdge,
|
|
30
|
+
configureAimGraph,
|
|
31
|
+
CascaisPalette,
|
|
32
|
+
wrapAimText,
|
|
33
|
+
} from './X6Shapes.js';
|
|
27
34
|
|
|
28
35
|
export class RaiBridge {
|
|
29
36
|
/**
|
|
@@ -138,6 +145,49 @@ export class RaiBridge {
|
|
|
138
145
|
const targetPort = edge.getTargetPortId();
|
|
139
146
|
const label = (edge.getLabels()?.[0]?.attrs?.['text']?.['text'] as string | undefined) ?? customData.label;
|
|
140
147
|
|
|
148
|
+
let routing: AimRoutingMode | undefined = customData.routing;
|
|
149
|
+
if (!routing) {
|
|
150
|
+
const router = edge.getRouter();
|
|
151
|
+
const routerName = typeof router === 'string' ? router : router?.name;
|
|
152
|
+
const connector = edge.getConnector();
|
|
153
|
+
const connectorName = typeof connector === 'string' ? connector : connector?.name;
|
|
154
|
+
if (connectorName === 'smooth') {
|
|
155
|
+
routing = 'smooth';
|
|
156
|
+
} else if (routerName === 'normal') {
|
|
157
|
+
routing = 'normal';
|
|
158
|
+
} else if (routerName === 'manhattan') {
|
|
159
|
+
routing = 'manhattan';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
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
|
+
|
|
141
191
|
const edgeData: RaidEdgeData = {
|
|
142
192
|
id: edge.id,
|
|
143
193
|
kind: customData.kind ?? 'association',
|
|
@@ -145,21 +195,26 @@ export class RaiBridge {
|
|
|
145
195
|
targetId: target.id,
|
|
146
196
|
...(sourcePort !== undefined ? { sourcePort } : {}),
|
|
147
197
|
...(targetPort !== undefined ? { targetPort } : {}),
|
|
198
|
+
...(routing !== undefined ? { routing } : {}),
|
|
148
199
|
...(label !== undefined ? { label } : {}),
|
|
149
200
|
...(customData.stereotype !== undefined ? { stereotype: customData.stereotype } : {}),
|
|
150
201
|
...(customData.sourceCardinality !== undefined ? { sourceCardinality: customData.sourceCardinality } : {}),
|
|
151
202
|
...(customData.targetCardinality !== undefined ? { targetCardinality: customData.targetCardinality } : {}),
|
|
152
203
|
bendPoints,
|
|
204
|
+
...(pathData !== undefined ? { pathData } : {}),
|
|
153
205
|
};
|
|
154
206
|
|
|
155
207
|
edges.push(edgeData);
|
|
156
208
|
}
|
|
157
209
|
|
|
210
|
+
const diagramRouting = (graph as unknown as { _aimRoutingMode?: AimRoutingMode })._aimRoutingMode;
|
|
211
|
+
|
|
158
212
|
return {
|
|
159
213
|
diagramId: 'RaidDiagram',
|
|
160
214
|
archetype: 'InteractiveCanvas',
|
|
161
215
|
nodes,
|
|
162
216
|
edges,
|
|
217
|
+
...(diagramRouting !== undefined ? { routing: diagramRouting } : {}),
|
|
163
218
|
};
|
|
164
219
|
}
|
|
165
220
|
|
|
@@ -190,6 +245,79 @@ export class RaiBridge {
|
|
|
190
245
|
return points.map((p) => `${Math.round(p.x)},${Math.round(p.y)}`).join('; ');
|
|
191
246
|
}
|
|
192
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
|
+
|
|
193
321
|
// --------------------------------------------------------------------------
|
|
194
322
|
// Private Helper Implementation
|
|
195
323
|
// --------------------------------------------------------------------------
|
|
@@ -279,8 +407,8 @@ export class RaiBridge {
|
|
|
279
407
|
const sourceNode = nodes.find((n) => n.id === sourceId);
|
|
280
408
|
const targetNode = nodes.find((n) => n.id === targetId);
|
|
281
409
|
|
|
282
|
-
// Infer optimal orthogonal docking ports if
|
|
283
|
-
if (sourceNode && targetNode) {
|
|
410
|
+
// Infer optimal orthogonal docking ports only if explicitly requested in options
|
|
411
|
+
if (options.inferPorts === true && sourceNode && targetNode) {
|
|
284
412
|
const dx =
|
|
285
413
|
targetNode.bounds.x +
|
|
286
414
|
targetNode.bounds.width / 2 -
|
|
@@ -328,6 +456,11 @@ export class RaiBridge {
|
|
|
328
456
|
el.getAttribute('aim-source-cardinality') ?? undefined;
|
|
329
457
|
const targetCardinality =
|
|
330
458
|
el.getAttribute('aim-target-cardinality') ?? undefined;
|
|
459
|
+
const rawRouting =
|
|
460
|
+
el.getAttribute(AimSvgContract.ATTR_ROUTING) ??
|
|
461
|
+
el.getAttribute('aim-routing') ??
|
|
462
|
+
undefined;
|
|
463
|
+
const routing = rawRouting ? this.normalizeRouting(rawRouting) : undefined;
|
|
331
464
|
|
|
332
465
|
edges.push({
|
|
333
466
|
id,
|
|
@@ -336,6 +469,7 @@ export class RaiBridge {
|
|
|
336
469
|
targetId,
|
|
337
470
|
...(sourcePort !== undefined ? { sourcePort } : {}),
|
|
338
471
|
...(targetPort !== undefined ? { targetPort } : {}),
|
|
472
|
+
...(routing !== undefined ? { routing } : {}),
|
|
339
473
|
...(label !== undefined ? { label } : {}),
|
|
340
474
|
...(sourceCardinality !== undefined ? { sourceCardinality } : {}),
|
|
341
475
|
...(targetCardinality !== undefined ? { targetCardinality } : {}),
|
|
@@ -343,11 +477,18 @@ export class RaiBridge {
|
|
|
343
477
|
});
|
|
344
478
|
}
|
|
345
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
|
+
|
|
346
486
|
return {
|
|
347
487
|
diagramId: svgRoot.getAttribute('id') ?? 'ImportedDiagram',
|
|
348
488
|
archetype: svgRoot.getAttribute('aim-archetype') ?? 'AOAIMDiagram',
|
|
349
489
|
nodes,
|
|
350
490
|
edges,
|
|
491
|
+
...(diagramRouting !== undefined ? { routing: diagramRouting } : {}),
|
|
351
492
|
};
|
|
352
493
|
}
|
|
353
494
|
|
|
@@ -371,7 +512,7 @@ export class RaiBridge {
|
|
|
371
512
|
}
|
|
372
513
|
|
|
373
514
|
// Check direct geometry attributes (rect, ellipse, circle)
|
|
374
|
-
const rect = el.querySelector('rect') ?? (el.tagName
|
|
515
|
+
const rect = el.querySelector?.('rect') ?? (el.tagName?.toLowerCase() === 'rect' ? el : null);
|
|
375
516
|
if (rect) {
|
|
376
517
|
if (!transform && rect.getAttribute('x')) x = parseFloat(rect.getAttribute('x')!);
|
|
377
518
|
if (!transform && rect.getAttribute('y')) y = parseFloat(rect.getAttribute('y')!);
|
|
@@ -380,7 +521,7 @@ export class RaiBridge {
|
|
|
380
521
|
}
|
|
381
522
|
|
|
382
523
|
const ellipse =
|
|
383
|
-
el.querySelector('ellipse') ?? (el.tagName
|
|
524
|
+
el.querySelector?.('ellipse') ?? (el.tagName?.toLowerCase() === 'ellipse' ? el : null);
|
|
384
525
|
if (ellipse) {
|
|
385
526
|
const rx = parseFloat(ellipse.getAttribute('rx') ?? `${width / 2}`);
|
|
386
527
|
const ry = parseFloat(ellipse.getAttribute('ry') ?? `${height / 2}`);
|
|
@@ -397,7 +538,7 @@ export class RaiBridge {
|
|
|
397
538
|
return { x, y, width, height };
|
|
398
539
|
}
|
|
399
540
|
|
|
400
|
-
|
|
541
|
+
public updateExistingSvg(
|
|
401
542
|
baseSvg: string,
|
|
402
543
|
model: RaidMetamodel,
|
|
403
544
|
_options: SerializationOptions,
|
|
@@ -409,6 +550,51 @@ export class RaiBridge {
|
|
|
409
550
|
const parser = new DOMParser();
|
|
410
551
|
const doc = parser.parseFromString(baseSvg, 'image/svg+xml');
|
|
411
552
|
|
|
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
|
+
// Update diagram-level routing mode on root <svg>
|
|
593
|
+
const diagramRouting = _options.routingMode ?? model.routing;
|
|
594
|
+
if (diagramRouting) {
|
|
595
|
+
doc.documentElement.setAttribute(AimSvgContract.ATTR_ROUTING, diagramRouting);
|
|
596
|
+
}
|
|
597
|
+
|
|
412
598
|
// Update node positions and transforms
|
|
413
599
|
for (const node of model.nodes) {
|
|
414
600
|
const el = doc.querySelector(`[${AimSvgContract.ATTR_ID}="${node.id}"], [id="${node.id}"]`);
|
|
@@ -433,7 +619,25 @@ export class RaiBridge {
|
|
|
433
619
|
el.setAttribute(AimSvgContract.ATTR_BENDS, bendsString);
|
|
434
620
|
el.setAttribute(AimSvgContract.ATTR_EDGE, 'true');
|
|
435
621
|
el.setAttribute(AimSvgContract.ATTR_EDGE_KIND, edge.kind);
|
|
436
|
-
|
|
622
|
+
if (edge.sourceId !== undefined) {
|
|
623
|
+
el.setAttribute(AimSvgContract.ATTR_SOURCE, edge.sourceId);
|
|
624
|
+
}
|
|
625
|
+
if (edge.targetId !== undefined) {
|
|
626
|
+
el.setAttribute(AimSvgContract.ATTR_TARGET, edge.targetId);
|
|
627
|
+
}
|
|
628
|
+
if (edge.sourcePort !== undefined && edge.sourcePort !== '' && edge.sourcePort !== 'auto') {
|
|
629
|
+
el.setAttribute(AimSvgContract.ATTR_SOURCE_PORT, edge.sourcePort);
|
|
630
|
+
} else {
|
|
631
|
+
el.removeAttribute(AimSvgContract.ATTR_SOURCE_PORT);
|
|
632
|
+
}
|
|
633
|
+
if (edge.targetPort !== undefined && edge.targetPort !== '' && edge.targetPort !== 'auto') {
|
|
634
|
+
el.setAttribute(AimSvgContract.ATTR_TARGET_PORT, edge.targetPort);
|
|
635
|
+
} else {
|
|
636
|
+
el.removeAttribute(AimSvgContract.ATTR_TARGET_PORT);
|
|
637
|
+
}
|
|
638
|
+
if (edge.routing !== undefined) {
|
|
639
|
+
el.setAttribute(AimSvgContract.ATTR_ROUTING, edge.routing);
|
|
640
|
+
}
|
|
437
641
|
if (edge.stereotype !== undefined) {
|
|
438
642
|
el.setAttribute(AimSvgContract.ATTR_STEREOTYPE, edge.stereotype);
|
|
439
643
|
}
|
|
@@ -443,17 +647,83 @@ export class RaiBridge {
|
|
|
443
647
|
if (edge.targetCardinality !== undefined) {
|
|
444
648
|
el.setAttribute('aim-target-cardinality', edge.targetCardinality);
|
|
445
649
|
}
|
|
650
|
+
|
|
651
|
+
// Update or inject <path class="aim-edge"> with rendered path geometry
|
|
652
|
+
let pathD = edge.pathData;
|
|
653
|
+
if (!pathD) {
|
|
654
|
+
const sourceNode = model.nodes.find((n) => n.id === edge.sourceId);
|
|
655
|
+
const targetNode = model.nodes.find((n) => n.id === edge.targetId);
|
|
656
|
+
if (sourceNode && targetNode) {
|
|
657
|
+
pathD = this.computeFallbackEdgePath(sourceNode, targetNode, edge.bendPoints, edge.routing ?? model.routing);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
let pathEl = el.querySelector('path.aim-edge') ?? el.querySelector('path');
|
|
662
|
+
if (!pathEl) {
|
|
663
|
+
pathEl = doc.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
664
|
+
pathEl.setAttribute('class', 'aim-edge');
|
|
665
|
+
el.appendChild(pathEl);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (pathD) {
|
|
669
|
+
pathEl.setAttribute('d', pathD);
|
|
670
|
+
}
|
|
671
|
+
pathEl.setAttribute('fill', 'none');
|
|
672
|
+
pathEl.setAttribute('stroke', CascaisPalette.WarmGraphite);
|
|
673
|
+
pathEl.setAttribute('stroke-width', '1.5');
|
|
674
|
+
|
|
675
|
+
if (edge.kind === 'dependency') {
|
|
676
|
+
pathEl.setAttribute('stroke-dasharray', '5,5');
|
|
677
|
+
} else {
|
|
678
|
+
pathEl.removeAttribute('stroke-dasharray');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const markerEnd = edge.kind === 'generalization' ? 'url(#arrow-hollow)' : 'url(#arrow-classic)';
|
|
682
|
+
pathEl.setAttribute('marker-end', markerEnd);
|
|
446
683
|
}
|
|
447
684
|
}
|
|
448
685
|
|
|
449
686
|
return new XMLSerializer().serializeToString(doc);
|
|
450
687
|
}
|
|
451
688
|
|
|
452
|
-
private
|
|
689
|
+
private renderSvgText(
|
|
690
|
+
text: string,
|
|
691
|
+
cx: number,
|
|
692
|
+
cy: number,
|
|
693
|
+
fontSize: number,
|
|
694
|
+
fontWeight: string,
|
|
695
|
+
fill: string,
|
|
696
|
+
underline: boolean = false,
|
|
697
|
+
): string {
|
|
698
|
+
const wrapped = wrapAimText(text);
|
|
699
|
+
const lines = wrapped.split('\n');
|
|
700
|
+
const underlineAttr = underline ? ' text-decoration="underline"' : '';
|
|
701
|
+
const weightAttr = fontWeight !== 'normal' ? ` font-weight="${fontWeight}"` : '';
|
|
702
|
+
|
|
703
|
+
if (lines.length <= 1) {
|
|
704
|
+
return ` <text x="${cx}" y="${cy}" font-size="${fontSize}"${weightAttr} fill="${fill}" text-anchor="middle" dominant-baseline="central"${underlineAttr}>${lines[0] ?? ''}</text>\n`;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const lineHeight = fontSize * 1.25;
|
|
708
|
+
const startY = cy - ((lines.length - 1) * lineHeight) / 2;
|
|
709
|
+
|
|
710
|
+
let tspans = '';
|
|
711
|
+
lines.forEach((line, idx) => {
|
|
712
|
+
const y = Math.round(startY + idx * lineHeight);
|
|
713
|
+
tspans += `<tspan x="${cx}" y="${y}">${line}</tspan>`;
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
return ` <text font-size="${fontSize}"${weightAttr} fill="${fill}" text-anchor="middle" dominant-baseline="central"${underlineAttr}>${tspans}</text>\n`;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
public generateFreshSvg(model: RaidMetamodel, options: SerializationOptions): string {
|
|
453
720
|
const width = Math.max(800, ...model.nodes.map((n) => n.bounds.x + n.bounds.width + 100));
|
|
454
721
|
const height = Math.max(600, ...model.nodes.map((n) => n.bounds.y + n.bounds.height + 100));
|
|
455
722
|
|
|
456
|
-
|
|
723
|
+
const diagramRouting = options.routingMode ?? model.routing;
|
|
724
|
+
const routingAttr = diagramRouting ? ` ${AimSvgContract.ATTR_ROUTING}="${diagramRouting}"` : '';
|
|
725
|
+
|
|
726
|
+
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" id="${model.diagramId}" aim-archetype="${model.archetype}"${routingAttr}>\n`;
|
|
457
727
|
|
|
458
728
|
// Definitions & Markers
|
|
459
729
|
svg += ` <defs>\n`;
|
|
@@ -469,6 +739,7 @@ export class RaiBridge {
|
|
|
469
739
|
svg += ` .aim-node:hover { filter: drop-shadow(0 4px 6px rgba(0,0,0,0.1)); }\n`;
|
|
470
740
|
svg += ` .aim-edge { fill: none; stroke: ${CascaisPalette.WarmGraphite}; stroke-width: 1.5; }\n`;
|
|
471
741
|
svg += ` text { font-family: Inter, system-ui, sans-serif; }\n`;
|
|
742
|
+
svg += ` .aim-act text, .aim-obj text { text-decoration: underline; }\n`;
|
|
472
743
|
svg += ` </style>\n`;
|
|
473
744
|
}
|
|
474
745
|
svg += ` </defs>\n\n`;
|
|
@@ -482,18 +753,41 @@ export class RaiBridge {
|
|
|
482
753
|
const markerEnd = edge.kind === 'generalization' ? ' marker-end="url(#arrow-hollow)"' : ' marker-end="url(#arrow-classic)"';
|
|
483
754
|
|
|
484
755
|
// Path data construction
|
|
485
|
-
let pathD = '';
|
|
486
|
-
if (
|
|
487
|
-
const
|
|
488
|
-
|
|
756
|
+
let pathD = edge.pathData ?? '';
|
|
757
|
+
if (!pathD) {
|
|
758
|
+
const sourceNode = model.nodes.find((n) => n.id === edge.sourceId);
|
|
759
|
+
const targetNode = model.nodes.find((n) => n.id === edge.targetId);
|
|
760
|
+
if (sourceNode && targetNode) {
|
|
761
|
+
pathD = this.computeFallbackEdgePath(sourceNode, targetNode, edge.bendPoints, edge.routing ?? model.routing);
|
|
762
|
+
} else if (edge.bendPoints.length > 0) {
|
|
763
|
+
const first = edge.bendPoints[0]!;
|
|
764
|
+
pathD = `M ${first.x} ${first.y} ` + edge.bendPoints.slice(1).map((p) => `L ${p.x} ${p.y}`).join(' ');
|
|
765
|
+
}
|
|
489
766
|
}
|
|
490
767
|
|
|
491
|
-
|
|
768
|
+
const sourcePortAttr = edge.sourcePort && edge.sourcePort !== 'auto' ? ` ${AimSvgContract.ATTR_SOURCE_PORT}="${edge.sourcePort}"` : '';
|
|
769
|
+
const targetPortAttr = edge.targetPort && edge.targetPort !== 'auto' ? ` ${AimSvgContract.ATTR_TARGET_PORT}="${edge.targetPort}"` : '';
|
|
770
|
+
const routingAttr = edge.routing ? ` ${AimSvgContract.ATTR_ROUTING}="${edge.routing}"` : '';
|
|
771
|
+
|
|
772
|
+
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`;
|
|
492
773
|
if (pathD) {
|
|
493
|
-
svg += ` <path d="${pathD}" class="aim-edge"${strokeDash}${markerEnd} />\n`;
|
|
774
|
+
svg += ` <path d="${pathD}" class="aim-edge" fill="none" stroke="${CascaisPalette.WarmGraphite}" stroke-width="1.5"${strokeDash}${markerEnd} />\n`;
|
|
494
775
|
}
|
|
495
776
|
if (edge.label) {
|
|
496
|
-
const midPoint =
|
|
777
|
+
const midPoint =
|
|
778
|
+
edge.bendPoints.length > 0
|
|
779
|
+
? (edge.bendPoints[Math.floor(edge.bendPoints.length / 2)] ?? { x: 50, y: 50 })
|
|
780
|
+
: (() => {
|
|
781
|
+
const s = model.nodes.find((n) => n.id === edge.sourceId);
|
|
782
|
+
const t = model.nodes.find((n) => n.id === edge.targetId);
|
|
783
|
+
if (s && t) {
|
|
784
|
+
return {
|
|
785
|
+
x: Math.round((s.bounds.x + s.bounds.width / 2 + t.bounds.x + t.bounds.width / 2) / 2),
|
|
786
|
+
y: Math.round((s.bounds.y + s.bounds.height / 2 + t.bounds.y + t.bounds.height / 2) / 2),
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
return { x: 50, y: 50 };
|
|
790
|
+
})();
|
|
497
791
|
svg += ` <text x="${midPoint.x}" y="${midPoint.y - 8}" font-size="11" fill="${CascaisPalette.TextSecondary}" text-anchor="middle">${edge.label}</text>\n`;
|
|
498
792
|
}
|
|
499
793
|
svg += ` </g>\n`;
|
|
@@ -510,13 +804,24 @@ export class RaiBridge {
|
|
|
510
804
|
const rx = node.bounds.width / 2;
|
|
511
805
|
const ry = node.bounds.height / 2;
|
|
512
806
|
svg += ` <ellipse cx="${rx}" cy="${ry}" rx="${rx}" ry="${ry}" fill="${CascaisPalette.ChalkWhite}" stroke="${CascaisPalette.NetGold}" stroke-width="2" />\n`;
|
|
513
|
-
svg +=
|
|
807
|
+
svg += this.renderSvgText(node.displayName, rx, ry, 13, 'bold', CascaisPalette.TextPrimary, false);
|
|
514
808
|
} else if (node.kind === 'act') {
|
|
515
809
|
svg += ` <rect width="${node.bounds.width}" height="${node.bounds.height}" rx="12" ry="12" fill="${CascaisPalette.CanvasCream}" stroke="${CascaisPalette.HeraldicGreen}" stroke-width="2" />\n`;
|
|
516
|
-
svg +=
|
|
810
|
+
svg += this.renderSvgText(node.displayName, node.bounds.width / 2, node.bounds.height / 2, 13, '600', CascaisPalette.TextPrimary, true);
|
|
811
|
+
} else if (node.kind === 'obj') {
|
|
812
|
+
svg += ` <rect width="${node.bounds.width}" height="${node.bounds.height}" fill="${CascaisPalette.ChalkWhite}" stroke="${CascaisPalette.SilverLineDark}" stroke-width="1.5" />\n`;
|
|
813
|
+
svg += this.renderSvgText(node.displayName, node.bounds.width / 2, node.bounds.height / 2, 12, 'normal', CascaisPalette.TextPrimary, true);
|
|
814
|
+
} else if (node.kind === 'per') {
|
|
815
|
+
const isInitiating = node.stereotype?.toLowerCase().includes('initiates') ?? false;
|
|
816
|
+
const strokeColor = isInitiating ? CascaisPalette.NetGold : CascaisPalette.WarmGraphite;
|
|
817
|
+
const cx = Math.round(node.bounds.width / 2);
|
|
818
|
+
svg += ` <rect width="${node.bounds.width}" height="${node.bounds.height}" fill="none" stroke="none" />\n`;
|
|
819
|
+
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`;
|
|
820
|
+
svg += ` <circle cx="45" cy="22" r="8" fill="${CascaisPalette.ChalkWhite}" stroke="${strokeColor}" stroke-width="2" />\n`;
|
|
821
|
+
svg += this.renderSvgText(node.displayName, cx, 68, 12, '500', CascaisPalette.TextPrimary, false);
|
|
517
822
|
} else {
|
|
518
823
|
svg += ` <rect width="${node.bounds.width}" height="${node.bounds.height}" fill="${CascaisPalette.ChalkWhite}" stroke="${CascaisPalette.WarmGraphite}" stroke-width="1.5" />\n`;
|
|
519
|
-
svg +=
|
|
824
|
+
svg += this.renderSvgText(node.displayName, node.bounds.width / 2, node.bounds.height / 2, 12, 'normal', CascaisPalette.TextPrimary, false);
|
|
520
825
|
}
|
|
521
826
|
|
|
522
827
|
svg += ` </g>\n`;
|
|
@@ -527,6 +832,14 @@ export class RaiBridge {
|
|
|
527
832
|
return svg;
|
|
528
833
|
}
|
|
529
834
|
|
|
835
|
+
public normalizeRouting(raw: string): AimRoutingMode {
|
|
836
|
+
const lower = raw.toLowerCase().trim();
|
|
837
|
+
if (lower === 'orthogonal' || lower === 'manhattan') return 'manhattan';
|
|
838
|
+
if (lower === 'straight' || lower === 'normal') return 'normal';
|
|
839
|
+
if (lower === 'curved' || lower === 'smooth') return 'smooth';
|
|
840
|
+
return 'manhattan';
|
|
841
|
+
}
|
|
842
|
+
|
|
530
843
|
private normalizeKind(kind: string): AimOntologyKind {
|
|
531
844
|
const lower = kind.toLowerCase();
|
|
532
845
|
if (lower === 'uc' || lower === 'usecase') return 'uc';
|