@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/dist/RaiBridge.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* the X6 graph, serializing them back into the SVG document while preserving semantic fidelity.
|
|
11
11
|
*/
|
|
12
12
|
import { Graph } from '@antv/x6';
|
|
13
|
-
import { type RaidMetamodel, type SvgBendPoint, type HydrationOptions, type SerializationOptions } from './types.js';
|
|
13
|
+
import { type AimRoutingMode, type RaidNodeData, type RaidMetamodel, type SvgBendPoint, type HydrationOptions, type SerializationOptions } from './types.js';
|
|
14
14
|
export declare class RaiBridge {
|
|
15
15
|
/**
|
|
16
16
|
* Hydrates an AntV X6 graph from an SVG source string or DOM Element.
|
|
@@ -42,11 +42,23 @@ export declare class RaiBridge {
|
|
|
42
42
|
* Formats bend points into the canonical aim-bends format: "x1,y1; x2,y2"
|
|
43
43
|
*/
|
|
44
44
|
formatBendPoints(points: readonly SvgBendPoint[]): string;
|
|
45
|
+
/**
|
|
46
|
+
* Computes a clean fallback SVG path connecting source and target nodes
|
|
47
|
+
* when running in headless environments (e.g. CLI, tests) without an active DOM.
|
|
48
|
+
*/
|
|
49
|
+
computeFallbackEdgePath(sourceNode: RaidNodeData, targetNode: RaidNodeData, bendPoints?: readonly SvgBendPoint[], routingMode?: AimRoutingMode): string;
|
|
45
50
|
private resolveSvgElement;
|
|
46
51
|
private extractMetamodel;
|
|
47
52
|
private extractBounds;
|
|
48
|
-
|
|
49
|
-
private
|
|
53
|
+
updateExistingSvg(baseSvg: string, model: RaidMetamodel, _options: SerializationOptions): string;
|
|
54
|
+
private renderSvgText;
|
|
55
|
+
/**
|
|
56
|
+
* Generates the canonical inner SVG elements (shapes and styled text)
|
|
57
|
+
* for a given AOAIM node archetype.
|
|
58
|
+
*/
|
|
59
|
+
renderNodeInnerSvg(node: RaidNodeData): string;
|
|
60
|
+
generateFreshSvg(model: RaidMetamodel, options: SerializationOptions): string;
|
|
61
|
+
normalizeRouting(raw: string): AimRoutingMode;
|
|
50
62
|
private normalizeKind;
|
|
51
63
|
private normalizeEdgeKind;
|
|
52
64
|
private kindFromShape;
|
package/dist/RaiBridge.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RaiBridge.d.ts","sourceRoot":"","sources":["../src/RaiBridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACjC,OAAO,
|
|
1
|
+
{"version":3,"file":"RaiBridge.d.ts","sourceRoot":"","sources":["../src/RaiBridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACjC,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,YAAY,EAEjB,KAAK,aAAa,EAClB,KAAK,YAAY,EAEjB,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EAC1B,MAAM,YAAY,CAAC;AASpB,qBAAa,SAAS;IACpB;;;;;;;OAOG;IACI,cAAc,CACnB,SAAS,EAAE,MAAM,GAAG,OAAO,EAC3B,KAAK,EAAE,KAAK,EACZ,OAAO,GAAE,gBAAqB,GAC7B,aAAa,CAuBf;IAED;;;;;;;OAOG;IACI,cAAc,CACnB,KAAK,EAAE,KAAK,EACZ,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,GAAE,oBAAyB,GACjC,MAAM,CAQR;IAED;;OAEG;IACI,kBAAkB,CAAC,KAAK,EAAE,KAAK,GAAG,aAAa,CAyHrD;IAED;;OAEG;IACI,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,EAAE,CAevD;IAED;;OAEG;IACI,gBAAgB,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,CAE/D;IAED;;;OAGG;IACI,uBAAuB,CAC5B,UAAU,EAAE,YAAY,EACxB,UAAU,EAAE,YAAY,EACxB,UAAU,GAAE,SAAS,YAAY,EAAO,EACxC,WAAW,GAAE,cAA4B,GACxC,MAAM,CA8DR;IAMD,OAAO,CAAC,iBAAiB;IAkBzB,OAAO,CAAC,gBAAgB;IAwJxB,OAAO,CAAC,aAAa;IA8Cd,iBAAiB,CACtB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,aAAa,EACpB,QAAQ,EAAE,oBAAoB,GAC7B,MAAM,CAmPR;IAED,OAAO,CAAC,aAAa;IA8BrB;;;OAGG;IACI,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CA0BpD;IAEM,gBAAgB,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,oBAAoB,GAAG,MAAM,CA2FnF;IAEM,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAMnD;IAED,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,aAAa;CAQtB"}
|
package/dist/RaiBridge.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* the X6 graph, serializing them back into the SVG document while preserving semantic fidelity.
|
|
11
11
|
*/
|
|
12
12
|
import { AimSvgContract, } from './types.js';
|
|
13
|
-
import { createAimNode, createAimEdge, configureAimGraph, CascaisPalette } from './X6Shapes.js';
|
|
13
|
+
import { createAimNode, createAimEdge, configureAimGraph, CascaisPalette, wrapAimText, } from './X6Shapes.js';
|
|
14
14
|
export class RaiBridge {
|
|
15
15
|
/**
|
|
16
16
|
* Hydrates an AntV X6 graph from an SVG source string or DOM Element.
|
|
@@ -116,6 +116,26 @@ export class RaiBridge {
|
|
|
116
116
|
routing = 'manhattan';
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
+
// Extract live rendered SVG path data from X6 EdgeView if available
|
|
120
|
+
let pathData = customData.pathData;
|
|
121
|
+
if (!pathData) {
|
|
122
|
+
const edgeView = typeof graph.findViewByCell === 'function'
|
|
123
|
+
? graph.findViewByCell(edge)
|
|
124
|
+
: null;
|
|
125
|
+
if (edgeView) {
|
|
126
|
+
if (typeof edgeView.getConnectionPathData === 'function') {
|
|
127
|
+
const d = edgeView.getConnectionPathData();
|
|
128
|
+
if (d && d.trim().length > 0)
|
|
129
|
+
pathData = d;
|
|
130
|
+
}
|
|
131
|
+
if (!pathData && edgeView.container) {
|
|
132
|
+
const pathEl = edgeView.container.querySelector('path[d]');
|
|
133
|
+
const d = pathEl?.getAttribute('d');
|
|
134
|
+
if (d && d.trim().length > 0)
|
|
135
|
+
pathData = d;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
119
139
|
const edgeData = {
|
|
120
140
|
id: edge.id,
|
|
121
141
|
kind: customData.kind ?? 'association',
|
|
@@ -129,14 +149,17 @@ export class RaiBridge {
|
|
|
129
149
|
...(customData.sourceCardinality !== undefined ? { sourceCardinality: customData.sourceCardinality } : {}),
|
|
130
150
|
...(customData.targetCardinality !== undefined ? { targetCardinality: customData.targetCardinality } : {}),
|
|
131
151
|
bendPoints,
|
|
152
|
+
...(pathData !== undefined ? { pathData } : {}),
|
|
132
153
|
};
|
|
133
154
|
edges.push(edgeData);
|
|
134
155
|
}
|
|
156
|
+
const diagramRouting = graph._aimRoutingMode;
|
|
135
157
|
return {
|
|
136
158
|
diagramId: 'RaidDiagram',
|
|
137
159
|
archetype: 'InteractiveCanvas',
|
|
138
160
|
nodes,
|
|
139
161
|
edges,
|
|
162
|
+
...(diagramRouting !== undefined ? { routing: diagramRouting } : {}),
|
|
140
163
|
};
|
|
141
164
|
}
|
|
142
165
|
/**
|
|
@@ -163,6 +186,70 @@ export class RaiBridge {
|
|
|
163
186
|
formatBendPoints(points) {
|
|
164
187
|
return points.map((p) => `${Math.round(p.x)},${Math.round(p.y)}`).join('; ');
|
|
165
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Computes a clean fallback SVG path connecting source and target nodes
|
|
191
|
+
* when running in headless environments (e.g. CLI, tests) without an active DOM.
|
|
192
|
+
*/
|
|
193
|
+
computeFallbackEdgePath(sourceNode, targetNode, bendPoints = [], routingMode = 'manhattan') {
|
|
194
|
+
const scx = Math.round(sourceNode.bounds.x + sourceNode.bounds.width / 2);
|
|
195
|
+
const scy = Math.round(sourceNode.bounds.y + sourceNode.bounds.height / 2);
|
|
196
|
+
const tcx = Math.round(targetNode.bounds.x + targetNode.bounds.width / 2);
|
|
197
|
+
const tcy = Math.round(targetNode.bounds.y + targetNode.bounds.height / 2);
|
|
198
|
+
let sx = scx;
|
|
199
|
+
let sy = scy;
|
|
200
|
+
let tx = tcx;
|
|
201
|
+
let ty = tcy;
|
|
202
|
+
const dx = tcx - scx;
|
|
203
|
+
const dy = tcy - scy;
|
|
204
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
205
|
+
if (dx > 0) {
|
|
206
|
+
sx = Math.round(sourceNode.bounds.x + sourceNode.bounds.width);
|
|
207
|
+
sy = scy;
|
|
208
|
+
tx = Math.round(targetNode.bounds.x);
|
|
209
|
+
ty = tcy;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
sx = Math.round(sourceNode.bounds.x);
|
|
213
|
+
sy = scy;
|
|
214
|
+
tx = Math.round(targetNode.bounds.x + targetNode.bounds.width);
|
|
215
|
+
ty = tcy;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
if (dy > 0) {
|
|
220
|
+
sx = scx;
|
|
221
|
+
sy = Math.round(sourceNode.bounds.y + sourceNode.bounds.height);
|
|
222
|
+
tx = tcx;
|
|
223
|
+
ty = Math.round(targetNode.bounds.y);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
sx = scx;
|
|
227
|
+
sy = Math.round(sourceNode.bounds.y);
|
|
228
|
+
tx = tcx;
|
|
229
|
+
ty = Math.round(targetNode.bounds.y + targetNode.bounds.height);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (bendPoints.length > 0) {
|
|
233
|
+
return `M ${sx} ${sy} ` + bendPoints.map((p) => `L ${Math.round(p.x)} ${Math.round(p.y)}`).join(' ') + ` L ${tx} ${ty}`;
|
|
234
|
+
}
|
|
235
|
+
if (routingMode === 'normal') {
|
|
236
|
+
return `M ${sx} ${sy} L ${tx} ${ty}`;
|
|
237
|
+
}
|
|
238
|
+
if (routingMode === 'smooth') {
|
|
239
|
+
const midX = Math.round((sx + tx) / 2);
|
|
240
|
+
const midY = Math.round((sy + ty) / 2);
|
|
241
|
+
return `M ${sx} ${sy} Q ${midX} ${sy} ${midX} ${midY} T ${tx} ${ty}`;
|
|
242
|
+
}
|
|
243
|
+
// Manhattan orthogonal default
|
|
244
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
245
|
+
const midX = Math.round((sx + tx) / 2);
|
|
246
|
+
return `M ${sx} ${sy} L ${midX} ${sy} L ${midX} ${ty} L ${tx} ${ty}`;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
const midY = Math.round((sy + ty) / 2);
|
|
250
|
+
return `M ${sx} ${sy} L ${sx} ${midY} L ${tx} ${midY} L ${tx} ${ty}`;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
166
253
|
// --------------------------------------------------------------------------
|
|
167
254
|
// Private Helper Implementation
|
|
168
255
|
// --------------------------------------------------------------------------
|
|
@@ -227,8 +314,8 @@ export class RaiBridge {
|
|
|
227
314
|
let targetPort = el.getAttribute(AimSvgContract.ATTR_TARGET_PORT) ?? undefined;
|
|
228
315
|
const sourceNode = nodes.find((n) => n.id === sourceId);
|
|
229
316
|
const targetNode = nodes.find((n) => n.id === targetId);
|
|
230
|
-
// Infer optimal orthogonal docking ports if
|
|
231
|
-
if (sourceNode && targetNode) {
|
|
317
|
+
// Infer optimal orthogonal docking ports only if explicitly requested in options
|
|
318
|
+
if (options.inferPorts === true && sourceNode && targetNode) {
|
|
232
319
|
const dx = targetNode.bounds.x +
|
|
233
320
|
targetNode.bounds.width / 2 -
|
|
234
321
|
(sourceNode.bounds.x + sourceNode.bounds.width / 2);
|
|
@@ -271,9 +358,7 @@ export class RaiBridge {
|
|
|
271
358
|
const rawRouting = el.getAttribute(AimSvgContract.ATTR_ROUTING) ??
|
|
272
359
|
el.getAttribute('aim-routing') ??
|
|
273
360
|
undefined;
|
|
274
|
-
const routing = rawRouting
|
|
275
|
-
? rawRouting
|
|
276
|
-
: undefined;
|
|
361
|
+
const routing = rawRouting ? this.normalizeRouting(rawRouting) : undefined;
|
|
277
362
|
edges.push({
|
|
278
363
|
id,
|
|
279
364
|
kind,
|
|
@@ -288,11 +373,16 @@ export class RaiBridge {
|
|
|
288
373
|
bendPoints: effectiveBendPoints,
|
|
289
374
|
});
|
|
290
375
|
}
|
|
376
|
+
const rawDiagramRouting = svgRoot.getAttribute(AimSvgContract.ATTR_ROUTING) ??
|
|
377
|
+
svgRoot.getAttribute('aim-routing') ??
|
|
378
|
+
undefined;
|
|
379
|
+
const diagramRouting = rawDiagramRouting ? this.normalizeRouting(rawDiagramRouting) : undefined;
|
|
291
380
|
return {
|
|
292
381
|
diagramId: svgRoot.getAttribute('id') ?? 'ImportedDiagram',
|
|
293
382
|
archetype: svgRoot.getAttribute('aim-archetype') ?? 'AOAIMDiagram',
|
|
294
383
|
nodes,
|
|
295
384
|
edges,
|
|
385
|
+
...(diagramRouting !== undefined ? { routing: diagramRouting } : {}),
|
|
296
386
|
};
|
|
297
387
|
}
|
|
298
388
|
extractBounds(el, defaultSize = { width: 140, height: 60 }) {
|
|
@@ -310,7 +400,7 @@ export class RaiBridge {
|
|
|
310
400
|
}
|
|
311
401
|
}
|
|
312
402
|
// Check direct geometry attributes (rect, ellipse, circle)
|
|
313
|
-
const rect = el.querySelector('rect') ?? (el.tagName
|
|
403
|
+
const rect = el.querySelector?.('rect') ?? (el.tagName?.toLowerCase() === 'rect' ? el : null);
|
|
314
404
|
if (rect) {
|
|
315
405
|
if (!transform && rect.getAttribute('x'))
|
|
316
406
|
x = parseFloat(rect.getAttribute('x'));
|
|
@@ -321,7 +411,7 @@ export class RaiBridge {
|
|
|
321
411
|
if (rect.getAttribute('height'))
|
|
322
412
|
height = parseFloat(rect.getAttribute('height'));
|
|
323
413
|
}
|
|
324
|
-
const ellipse = el.querySelector('ellipse') ?? (el.tagName
|
|
414
|
+
const ellipse = el.querySelector?.('ellipse') ?? (el.tagName?.toLowerCase() === 'ellipse' ? el : null);
|
|
325
415
|
if (ellipse) {
|
|
326
416
|
const rx = parseFloat(ellipse.getAttribute('rx') ?? `${width / 2}`);
|
|
327
417
|
const ry = parseFloat(ellipse.getAttribute('ry') ?? `${height / 2}`);
|
|
@@ -342,60 +432,281 @@ export class RaiBridge {
|
|
|
342
432
|
}
|
|
343
433
|
const parser = new DOMParser();
|
|
344
434
|
const doc = parser.parseFromString(baseSvg, 'image/svg+xml');
|
|
345
|
-
//
|
|
435
|
+
// Ensure defs and arrow markers exist for external vector viewers (Preview, Chrome, Safari)
|
|
436
|
+
let defs = doc.querySelector('defs');
|
|
437
|
+
if (!defs) {
|
|
438
|
+
defs = doc.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
|
439
|
+
doc.documentElement.insertBefore(defs, doc.documentElement.firstChild);
|
|
440
|
+
}
|
|
441
|
+
if (!doc.querySelector('#arrow-classic')) {
|
|
442
|
+
const marker = doc.createElementNS('http://www.w3.org/2000/svg', 'marker');
|
|
443
|
+
marker.setAttribute('id', 'arrow-classic');
|
|
444
|
+
marker.setAttribute('viewBox', '0 0 10 10');
|
|
445
|
+
marker.setAttribute('refX', '10');
|
|
446
|
+
marker.setAttribute('refY', '5');
|
|
447
|
+
marker.setAttribute('markerWidth', '7');
|
|
448
|
+
marker.setAttribute('markerHeight', '7');
|
|
449
|
+
marker.setAttribute('orient', 'auto-start-reverse');
|
|
450
|
+
const path = doc.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
451
|
+
path.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z');
|
|
452
|
+
path.setAttribute('fill', CascaisPalette.WarmGraphite);
|
|
453
|
+
marker.appendChild(path);
|
|
454
|
+
defs.appendChild(marker);
|
|
455
|
+
}
|
|
456
|
+
if (!doc.querySelector('#arrow-hollow')) {
|
|
457
|
+
const marker = doc.createElementNS('http://www.w3.org/2000/svg', 'marker');
|
|
458
|
+
marker.setAttribute('id', 'arrow-hollow');
|
|
459
|
+
marker.setAttribute('viewBox', '0 0 12 12');
|
|
460
|
+
marker.setAttribute('refX', '12');
|
|
461
|
+
marker.setAttribute('refY', '6');
|
|
462
|
+
marker.setAttribute('markerWidth', '9');
|
|
463
|
+
marker.setAttribute('markerHeight', '9');
|
|
464
|
+
marker.setAttribute('orient', 'auto-start-reverse');
|
|
465
|
+
const polygon = doc.createElementNS('http://www.w3.org/2000/svg', 'polygon');
|
|
466
|
+
polygon.setAttribute('points', '0 0, 12 6, 0 12');
|
|
467
|
+
polygon.setAttribute('fill', CascaisPalette.ChalkWhite);
|
|
468
|
+
polygon.setAttribute('stroke', CascaisPalette.WarmGraphite);
|
|
469
|
+
polygon.setAttribute('stroke-width', '1.5');
|
|
470
|
+
marker.appendChild(polygon);
|
|
471
|
+
defs.appendChild(marker);
|
|
472
|
+
}
|
|
473
|
+
// Ensure AOAIM style rules are present
|
|
474
|
+
let style = doc.querySelector('style');
|
|
475
|
+
if (!style) {
|
|
476
|
+
style = doc.createElementNS('http://www.w3.org/2000/svg', 'style');
|
|
477
|
+
defs.appendChild(style);
|
|
478
|
+
}
|
|
479
|
+
if (style && !style.textContent?.includes('.aim-act text')) {
|
|
480
|
+
style.textContent = (style.textContent ? style.textContent + '\n' : '') + `
|
|
481
|
+
.aim-node { cursor: pointer; transition: filter 0.15s ease; }
|
|
482
|
+
.aim-node:hover { filter: drop-shadow(0 4px 6px rgba(0,0,0,0.1)); }
|
|
483
|
+
.aim-edge { fill: none; stroke: ${CascaisPalette.WarmGraphite}; stroke-width: 1.5; }
|
|
484
|
+
text { font-family: Inter, system-ui, sans-serif; }
|
|
485
|
+
.aim-act text, .aim-obj text { text-decoration: underline; }
|
|
486
|
+
`;
|
|
487
|
+
}
|
|
488
|
+
// Update diagram-level routing mode on root <svg>
|
|
489
|
+
const diagramRouting = _options.routingMode ?? model.routing;
|
|
490
|
+
if (diagramRouting) {
|
|
491
|
+
doc.documentElement.setAttribute(AimSvgContract.ATTR_ROUTING, diagramRouting);
|
|
492
|
+
}
|
|
493
|
+
// Ensure layers exist
|
|
494
|
+
let nodesLayer = doc.querySelector('.aim-nodes-layer');
|
|
495
|
+
if (!nodesLayer) {
|
|
496
|
+
nodesLayer = doc.documentElement;
|
|
497
|
+
}
|
|
498
|
+
// Remove deleted nodes
|
|
499
|
+
const existingNodeEls = Array.from(doc.querySelectorAll(AimSvgContract.SELECTOR_NODE));
|
|
500
|
+
for (const nodeEl of existingNodeEls) {
|
|
501
|
+
const id = nodeEl.getAttribute(AimSvgContract.ATTR_ID) ?? nodeEl.getAttribute('id');
|
|
502
|
+
if (id && !model.nodes.some((n) => n.id === id)) {
|
|
503
|
+
nodeEl.remove();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// Update or insert nodes with canonical archetype shape markup
|
|
346
507
|
for (const node of model.nodes) {
|
|
347
|
-
|
|
348
|
-
if (el) {
|
|
349
|
-
el.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
508
|
+
let el = doc.querySelector(`[${AimSvgContract.ATTR_ID}="${node.id}"], [id="${node.id}"]`);
|
|
509
|
+
if (!el) {
|
|
510
|
+
el = doc.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
511
|
+
nodesLayer.appendChild(el);
|
|
512
|
+
}
|
|
513
|
+
el.setAttribute('transform', `translate(${node.bounds.x}, ${node.bounds.y})`);
|
|
514
|
+
el.setAttribute(AimSvgContract.ATTR_NODE, 'true');
|
|
515
|
+
el.setAttribute(AimSvgContract.ATTR_ID, node.id);
|
|
516
|
+
el.setAttribute(AimSvgContract.ATTR_KIND, node.kind);
|
|
517
|
+
el.setAttribute(AimSvgContract.ATTR_DISPLAY_NAME, node.displayName);
|
|
518
|
+
if (node.stereotype) {
|
|
519
|
+
el.setAttribute(AimSvgContract.ATTR_STEREOTYPE, node.stereotype);
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
el.removeAttribute(AimSvgContract.ATTR_STEREOTYPE);
|
|
523
|
+
}
|
|
524
|
+
// Replace inner content with canonical archetype shape markup
|
|
525
|
+
const innerSvg = this.renderNodeInnerSvg(node);
|
|
526
|
+
const fragmentDoc = parser.parseFromString(`<g xmlns="http://www.w3.org/2000/svg">${innerSvg}</g>`, 'image/svg+xml');
|
|
527
|
+
while (el.firstChild) {
|
|
528
|
+
el.removeChild(el.firstChild);
|
|
529
|
+
}
|
|
530
|
+
for (const child of Array.from(fragmentDoc.documentElement.childNodes)) {
|
|
531
|
+
el.appendChild(doc.importNode(child, true));
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
let edgesLayer = doc.querySelector('.aim-edges-layer');
|
|
535
|
+
if (!edgesLayer) {
|
|
536
|
+
edgesLayer = doc.documentElement;
|
|
537
|
+
}
|
|
538
|
+
// Remove deleted edges
|
|
539
|
+
const existingEdgeEls = Array.from(doc.querySelectorAll(AimSvgContract.SELECTOR_EDGE));
|
|
540
|
+
for (const edgeEl of existingEdgeEls) {
|
|
541
|
+
const id = edgeEl.getAttribute(AimSvgContract.ATTR_ID) ?? edgeEl.getAttribute('id');
|
|
542
|
+
if (id && !model.edges.some((e) => e.id === id)) {
|
|
543
|
+
edgeEl.remove();
|
|
357
544
|
}
|
|
358
545
|
}
|
|
359
|
-
// Update
|
|
546
|
+
// Update or insert edges with live path geometry
|
|
360
547
|
for (const edge of model.edges) {
|
|
361
|
-
|
|
362
|
-
if (el) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
el.setAttribute(AimSvgContract.ATTR_EDGE_KIND, edge.kind);
|
|
367
|
-
if (edge.sourceId !== undefined) {
|
|
368
|
-
el.setAttribute(AimSvgContract.ATTR_SOURCE, edge.sourceId);
|
|
369
|
-
}
|
|
370
|
-
if (edge.targetId !== undefined) {
|
|
371
|
-
el.setAttribute(AimSvgContract.ATTR_TARGET, edge.targetId);
|
|
372
|
-
}
|
|
373
|
-
if (edge.sourcePort !== undefined) {
|
|
374
|
-
el.setAttribute(AimSvgContract.ATTR_SOURCE_PORT, edge.sourcePort);
|
|
375
|
-
}
|
|
376
|
-
if (edge.targetPort !== undefined) {
|
|
377
|
-
el.setAttribute(AimSvgContract.ATTR_TARGET_PORT, edge.targetPort);
|
|
378
|
-
}
|
|
379
|
-
if (edge.routing !== undefined) {
|
|
380
|
-
el.setAttribute(AimSvgContract.ATTR_ROUTING, edge.routing);
|
|
548
|
+
let el = doc.querySelector(`[${AimSvgContract.ATTR_ID}="${edge.id}"], [id="${edge.id}"]`);
|
|
549
|
+
if (!el) {
|
|
550
|
+
el = doc.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
551
|
+
if (nodesLayer && nodesLayer.parentNode === doc.documentElement) {
|
|
552
|
+
doc.documentElement.insertBefore(el, nodesLayer);
|
|
381
553
|
}
|
|
382
|
-
|
|
383
|
-
|
|
554
|
+
else {
|
|
555
|
+
edgesLayer.appendChild(el);
|
|
384
556
|
}
|
|
385
|
-
|
|
386
|
-
|
|
557
|
+
}
|
|
558
|
+
const bendsString = this.formatBendPoints(edge.bendPoints);
|
|
559
|
+
el.setAttribute(AimSvgContract.ATTR_BENDS, bendsString);
|
|
560
|
+
el.setAttribute(AimSvgContract.ATTR_EDGE, 'true');
|
|
561
|
+
el.setAttribute(AimSvgContract.ATTR_ID, edge.id);
|
|
562
|
+
el.setAttribute(AimSvgContract.ATTR_EDGE_KIND, edge.kind);
|
|
563
|
+
if (edge.sourceId !== undefined) {
|
|
564
|
+
el.setAttribute(AimSvgContract.ATTR_SOURCE, edge.sourceId);
|
|
565
|
+
}
|
|
566
|
+
if (edge.targetId !== undefined) {
|
|
567
|
+
el.setAttribute(AimSvgContract.ATTR_TARGET, edge.targetId);
|
|
568
|
+
}
|
|
569
|
+
if (edge.sourcePort !== undefined && edge.sourcePort !== '' && edge.sourcePort !== 'auto') {
|
|
570
|
+
el.setAttribute(AimSvgContract.ATTR_SOURCE_PORT, edge.sourcePort);
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
el.removeAttribute(AimSvgContract.ATTR_SOURCE_PORT);
|
|
574
|
+
}
|
|
575
|
+
if (edge.targetPort !== undefined && edge.targetPort !== '' && edge.targetPort !== 'auto') {
|
|
576
|
+
el.setAttribute(AimSvgContract.ATTR_TARGET_PORT, edge.targetPort);
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
el.removeAttribute(AimSvgContract.ATTR_TARGET_PORT);
|
|
580
|
+
}
|
|
581
|
+
if (edge.routing !== undefined) {
|
|
582
|
+
el.setAttribute(AimSvgContract.ATTR_ROUTING, edge.routing);
|
|
583
|
+
}
|
|
584
|
+
if (edge.stereotype !== undefined) {
|
|
585
|
+
el.setAttribute(AimSvgContract.ATTR_STEREOTYPE, edge.stereotype);
|
|
586
|
+
}
|
|
587
|
+
if (edge.sourceCardinality !== undefined) {
|
|
588
|
+
el.setAttribute('aim-source-cardinality', edge.sourceCardinality);
|
|
589
|
+
}
|
|
590
|
+
if (edge.targetCardinality !== undefined) {
|
|
591
|
+
el.setAttribute('aim-target-cardinality', edge.targetCardinality);
|
|
592
|
+
}
|
|
593
|
+
// Update or inject <path class="aim-edge"> with rendered path geometry
|
|
594
|
+
let pathD = edge.pathData;
|
|
595
|
+
if (!pathD) {
|
|
596
|
+
const sourceNode = model.nodes.find((n) => n.id === edge.sourceId);
|
|
597
|
+
const targetNode = model.nodes.find((n) => n.id === edge.targetId);
|
|
598
|
+
if (sourceNode && targetNode) {
|
|
599
|
+
pathD = this.computeFallbackEdgePath(sourceNode, targetNode, edge.bendPoints, edge.routing ?? model.routing);
|
|
387
600
|
}
|
|
388
|
-
|
|
389
|
-
|
|
601
|
+
}
|
|
602
|
+
let pathEl = el.querySelector('path.aim-edge') ?? el.querySelector('path');
|
|
603
|
+
if (!pathEl) {
|
|
604
|
+
pathEl = doc.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
605
|
+
pathEl.setAttribute('class', 'aim-edge');
|
|
606
|
+
el.appendChild(pathEl);
|
|
607
|
+
}
|
|
608
|
+
if (pathD) {
|
|
609
|
+
pathEl.setAttribute('d', pathD);
|
|
610
|
+
}
|
|
611
|
+
pathEl.setAttribute('fill', 'none');
|
|
612
|
+
pathEl.setAttribute('stroke', CascaisPalette.WarmGraphite);
|
|
613
|
+
pathEl.setAttribute('stroke-width', '1.5');
|
|
614
|
+
if (edge.kind === 'dependency') {
|
|
615
|
+
pathEl.setAttribute('stroke-dasharray', '5,5');
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
pathEl.removeAttribute('stroke-dasharray');
|
|
619
|
+
}
|
|
620
|
+
const markerEnd = edge.kind === 'generalization' ? 'url(#arrow-hollow)' : 'url(#arrow-classic)';
|
|
621
|
+
pathEl.setAttribute('marker-end', markerEnd);
|
|
622
|
+
// Update or inject edge label
|
|
623
|
+
if (edge.label) {
|
|
624
|
+
let textEl = el.querySelector('text');
|
|
625
|
+
if (!textEl) {
|
|
626
|
+
textEl = doc.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
627
|
+
el.appendChild(textEl);
|
|
390
628
|
}
|
|
629
|
+
const midPoint = edge.bendPoints.length > 0
|
|
630
|
+
? (edge.bendPoints[Math.floor(edge.bendPoints.length / 2)] ?? { x: 50, y: 50 })
|
|
631
|
+
: (() => {
|
|
632
|
+
const s = model.nodes.find((n) => n.id === edge.sourceId);
|
|
633
|
+
const t = model.nodes.find((n) => n.id === edge.targetId);
|
|
634
|
+
if (s && t) {
|
|
635
|
+
return {
|
|
636
|
+
x: Math.round((s.bounds.x + s.bounds.width / 2 + t.bounds.x + t.bounds.width / 2) / 2),
|
|
637
|
+
y: Math.round((s.bounds.y + s.bounds.height / 2 + t.bounds.y + t.bounds.height / 2) / 2),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
return { x: 50, y: 50 };
|
|
641
|
+
})();
|
|
642
|
+
textEl.setAttribute('x', `${midPoint.x}`);
|
|
643
|
+
textEl.setAttribute('y', `${midPoint.y - 8}`);
|
|
644
|
+
textEl.setAttribute('font-size', '11');
|
|
645
|
+
textEl.setAttribute('fill', CascaisPalette.TextSecondary);
|
|
646
|
+
textEl.setAttribute('text-anchor', 'middle');
|
|
647
|
+
textEl.textContent = edge.label;
|
|
391
648
|
}
|
|
392
649
|
}
|
|
393
650
|
return new XMLSerializer().serializeToString(doc);
|
|
394
651
|
}
|
|
652
|
+
renderSvgText(text, cx, cy, fontSize, fontWeight, fill, underline = false) {
|
|
653
|
+
const wrapped = wrapAimText(text);
|
|
654
|
+
const lines = wrapped.split('\n');
|
|
655
|
+
const underlineAttr = underline ? ' text-decoration="underline"' : '';
|
|
656
|
+
const weightAttr = fontWeight !== 'normal' ? ` font-weight="${fontWeight}"` : '';
|
|
657
|
+
if (lines.length <= 1) {
|
|
658
|
+
return ` <text x="${cx}" y="${cy}" font-size="${fontSize}"${weightAttr} fill="${fill}" text-anchor="middle" dominant-baseline="central"${underlineAttr}>${lines[0] ?? ''}</text>\n`;
|
|
659
|
+
}
|
|
660
|
+
const lineHeight = fontSize * 1.25;
|
|
661
|
+
const startY = cy - ((lines.length - 1) * lineHeight) / 2;
|
|
662
|
+
let tspans = '';
|
|
663
|
+
lines.forEach((line, idx) => {
|
|
664
|
+
const y = Math.round(startY + idx * lineHeight);
|
|
665
|
+
tspans += `<tspan x="${cx}" y="${y}">${line}</tspan>`;
|
|
666
|
+
});
|
|
667
|
+
return ` <text font-size="${fontSize}"${weightAttr} fill="${fill}" text-anchor="middle" dominant-baseline="central"${underlineAttr}>${tspans}</text>\n`;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Generates the canonical inner SVG elements (shapes and styled text)
|
|
671
|
+
* for a given AOAIM node archetype.
|
|
672
|
+
*/
|
|
673
|
+
renderNodeInnerSvg(node) {
|
|
674
|
+
let svg = '';
|
|
675
|
+
if (node.kind === 'uc') {
|
|
676
|
+
const rx = node.bounds.width / 2;
|
|
677
|
+
const ry = node.bounds.height / 2;
|
|
678
|
+
svg += ` <ellipse cx="${rx}" cy="${ry}" rx="${rx}" ry="${ry}" fill="${CascaisPalette.ChalkWhite}" stroke="${CascaisPalette.NetGold}" stroke-width="2" />\n`;
|
|
679
|
+
svg += this.renderSvgText(node.displayName, rx, ry, 13, 'bold', CascaisPalette.TextPrimary, false);
|
|
680
|
+
}
|
|
681
|
+
else if (node.kind === 'act') {
|
|
682
|
+
svg += ` <rect width="${node.bounds.width}" height="${node.bounds.height}" rx="12" ry="12" fill="${CascaisPalette.CanvasCream}" stroke="${CascaisPalette.HeraldicGreen}" stroke-width="2" />\n`;
|
|
683
|
+
svg += this.renderSvgText(node.displayName, node.bounds.width / 2, node.bounds.height / 2, 13, '600', CascaisPalette.TextPrimary, true);
|
|
684
|
+
}
|
|
685
|
+
else if (node.kind === 'obj') {
|
|
686
|
+
svg += ` <rect width="${node.bounds.width}" height="${node.bounds.height}" fill="${CascaisPalette.ChalkWhite}" stroke="${CascaisPalette.SilverLineDark}" stroke-width="1.5" />\n`;
|
|
687
|
+
svg += this.renderSvgText(node.displayName, node.bounds.width / 2, node.bounds.height / 2, 12, 'normal', CascaisPalette.TextPrimary, true);
|
|
688
|
+
}
|
|
689
|
+
else if (node.kind === 'per') {
|
|
690
|
+
const isInitiating = node.stereotype?.toLowerCase().includes('initiates') ?? false;
|
|
691
|
+
const strokeColor = isInitiating ? CascaisPalette.NetGold : CascaisPalette.WarmGraphite;
|
|
692
|
+
const cx = Math.round(node.bounds.width / 2);
|
|
693
|
+
svg += ` <rect width="${node.bounds.width}" height="${node.bounds.height}" fill="none" stroke="none" />\n`;
|
|
694
|
+
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`;
|
|
695
|
+
svg += ` <circle cx="45" cy="22" r="8" fill="${CascaisPalette.ChalkWhite}" stroke="${strokeColor}" stroke-width="2" />\n`;
|
|
696
|
+
svg += this.renderSvgText(node.displayName, cx, 68, 12, '500', CascaisPalette.TextPrimary, false);
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
svg += ` <rect width="${node.bounds.width}" height="${node.bounds.height}" fill="${CascaisPalette.ChalkWhite}" stroke="${CascaisPalette.WarmGraphite}" stroke-width="1.5" />\n`;
|
|
700
|
+
svg += this.renderSvgText(node.displayName, node.bounds.width / 2, node.bounds.height / 2, 12, 'normal', CascaisPalette.TextPrimary, false);
|
|
701
|
+
}
|
|
702
|
+
return svg;
|
|
703
|
+
}
|
|
395
704
|
generateFreshSvg(model, options) {
|
|
396
705
|
const width = Math.max(800, ...model.nodes.map((n) => n.bounds.x + n.bounds.width + 100));
|
|
397
706
|
const height = Math.max(600, ...model.nodes.map((n) => n.bounds.y + n.bounds.height + 100));
|
|
398
|
-
|
|
707
|
+
const diagramRouting = options.routingMode ?? model.routing;
|
|
708
|
+
const routingAttr = diagramRouting ? ` ${AimSvgContract.ATTR_ROUTING}="${diagramRouting}"` : '';
|
|
709
|
+
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" id="${model.diagramId}" aim-archetype="${model.archetype}"${routingAttr}>\n`;
|
|
399
710
|
// Definitions & Markers
|
|
400
711
|
svg += ` <defs>\n`;
|
|
401
712
|
svg += ` <marker id="arrow-classic" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">\n`;
|
|
@@ -410,6 +721,7 @@ export class RaiBridge {
|
|
|
410
721
|
svg += ` .aim-node:hover { filter: drop-shadow(0 4px 6px rgba(0,0,0,0.1)); }\n`;
|
|
411
722
|
svg += ` .aim-edge { fill: none; stroke: ${CascaisPalette.WarmGraphite}; stroke-width: 1.5; }\n`;
|
|
412
723
|
svg += ` text { font-family: Inter, system-ui, sans-serif; }\n`;
|
|
724
|
+
svg += ` .aim-act text, .aim-obj text { text-decoration: underline; }\n`;
|
|
413
725
|
svg += ` </style>\n`;
|
|
414
726
|
}
|
|
415
727
|
svg += ` </defs>\n\n`;
|
|
@@ -421,20 +733,39 @@ export class RaiBridge {
|
|
|
421
733
|
const strokeDash = edge.kind === 'dependency' ? ' stroke-dasharray="5,5"' : '';
|
|
422
734
|
const markerEnd = edge.kind === 'generalization' ? ' marker-end="url(#arrow-hollow)"' : ' marker-end="url(#arrow-classic)"';
|
|
423
735
|
// Path data construction
|
|
424
|
-
let pathD = '';
|
|
425
|
-
if (
|
|
426
|
-
const
|
|
427
|
-
|
|
736
|
+
let pathD = edge.pathData ?? '';
|
|
737
|
+
if (!pathD) {
|
|
738
|
+
const sourceNode = model.nodes.find((n) => n.id === edge.sourceId);
|
|
739
|
+
const targetNode = model.nodes.find((n) => n.id === edge.targetId);
|
|
740
|
+
if (sourceNode && targetNode) {
|
|
741
|
+
pathD = this.computeFallbackEdgePath(sourceNode, targetNode, edge.bendPoints, edge.routing ?? model.routing);
|
|
742
|
+
}
|
|
743
|
+
else if (edge.bendPoints.length > 0) {
|
|
744
|
+
const first = edge.bendPoints[0];
|
|
745
|
+
pathD = `M ${first.x} ${first.y} ` + edge.bendPoints.slice(1).map((p) => `L ${p.x} ${p.y}`).join(' ');
|
|
746
|
+
}
|
|
428
747
|
}
|
|
429
|
-
const sourcePortAttr = edge.sourcePort ? ` ${AimSvgContract.ATTR_SOURCE_PORT}="${edge.sourcePort}"` : '';
|
|
430
|
-
const targetPortAttr = edge.targetPort ? ` ${AimSvgContract.ATTR_TARGET_PORT}="${edge.targetPort}"` : '';
|
|
748
|
+
const sourcePortAttr = edge.sourcePort && edge.sourcePort !== 'auto' ? ` ${AimSvgContract.ATTR_SOURCE_PORT}="${edge.sourcePort}"` : '';
|
|
749
|
+
const targetPortAttr = edge.targetPort && edge.targetPort !== 'auto' ? ` ${AimSvgContract.ATTR_TARGET_PORT}="${edge.targetPort}"` : '';
|
|
431
750
|
const routingAttr = edge.routing ? ` ${AimSvgContract.ATTR_ROUTING}="${edge.routing}"` : '';
|
|
432
751
|
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`;
|
|
433
752
|
if (pathD) {
|
|
434
|
-
svg += ` <path d="${pathD}" class="aim-edge"${strokeDash}${markerEnd} />\n`;
|
|
753
|
+
svg += ` <path d="${pathD}" class="aim-edge" fill="none" stroke="${CascaisPalette.WarmGraphite}" stroke-width="1.5"${strokeDash}${markerEnd} />\n`;
|
|
435
754
|
}
|
|
436
755
|
if (edge.label) {
|
|
437
|
-
const midPoint = edge.bendPoints
|
|
756
|
+
const midPoint = edge.bendPoints.length > 0
|
|
757
|
+
? (edge.bendPoints[Math.floor(edge.bendPoints.length / 2)] ?? { x: 50, y: 50 })
|
|
758
|
+
: (() => {
|
|
759
|
+
const s = model.nodes.find((n) => n.id === edge.sourceId);
|
|
760
|
+
const t = model.nodes.find((n) => n.id === edge.targetId);
|
|
761
|
+
if (s && t) {
|
|
762
|
+
return {
|
|
763
|
+
x: Math.round((s.bounds.x + s.bounds.width / 2 + t.bounds.x + t.bounds.width / 2) / 2),
|
|
764
|
+
y: Math.round((s.bounds.y + s.bounds.height / 2 + t.bounds.y + t.bounds.height / 2) / 2),
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
return { x: 50, y: 50 };
|
|
768
|
+
})();
|
|
438
769
|
svg += ` <text x="${midPoint.x}" y="${midPoint.y - 8}" font-size="11" fill="${CascaisPalette.TextSecondary}" text-anchor="middle">${edge.label}</text>\n`;
|
|
439
770
|
}
|
|
440
771
|
svg += ` </g>\n`;
|
|
@@ -444,27 +775,25 @@ export class RaiBridge {
|
|
|
444
775
|
svg += ` <!-- Nodes -->\n`;
|
|
445
776
|
svg += ` <g class="aim-nodes-layer">\n`;
|
|
446
777
|
for (const node of model.nodes) {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const ry = node.bounds.height / 2;
|
|
451
|
-
svg += ` <ellipse cx="${rx}" cy="${ry}" rx="${rx}" ry="${ry}" fill="${CascaisPalette.ChalkWhite}" stroke="${CascaisPalette.NetGold}" stroke-width="2" />\n`;
|
|
452
|
-
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`;
|
|
453
|
-
}
|
|
454
|
-
else if (node.kind === 'act') {
|
|
455
|
-
svg += ` <rect width="${node.bounds.width}" height="${node.bounds.height}" rx="12" ry="12" fill="${CascaisPalette.CanvasCream}" stroke="${CascaisPalette.HeraldicGreen}" stroke-width="2" />\n`;
|
|
456
|
-
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`;
|
|
457
|
-
}
|
|
458
|
-
else {
|
|
459
|
-
svg += ` <rect width="${node.bounds.width}" height="${node.bounds.height}" fill="${CascaisPalette.ChalkWhite}" stroke="${CascaisPalette.WarmGraphite}" stroke-width="1.5" />\n`;
|
|
460
|
-
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`;
|
|
461
|
-
}
|
|
778
|
+
const stereotypeAttr = node.stereotype ? ` ${AimSvgContract.ATTR_STEREOTYPE}="${node.stereotype}"` : '';
|
|
779
|
+
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`;
|
|
780
|
+
svg += this.renderNodeInnerSvg(node);
|
|
462
781
|
svg += ` </g>\n`;
|
|
463
782
|
}
|
|
464
783
|
svg += ` </g>\n`;
|
|
465
784
|
svg += `</svg>\n`;
|
|
466
785
|
return svg;
|
|
467
786
|
}
|
|
787
|
+
normalizeRouting(raw) {
|
|
788
|
+
const lower = raw.toLowerCase().trim();
|
|
789
|
+
if (lower === 'orthogonal' || lower === 'manhattan')
|
|
790
|
+
return 'manhattan';
|
|
791
|
+
if (lower === 'straight' || lower === 'normal')
|
|
792
|
+
return 'normal';
|
|
793
|
+
if (lower === 'curved' || lower === 'smooth')
|
|
794
|
+
return 'smooth';
|
|
795
|
+
return 'manhattan';
|
|
796
|
+
}
|
|
468
797
|
normalizeKind(kind) {
|
|
469
798
|
const lower = kind.toLowerCase();
|
|
470
799
|
if (lower === 'uc' || lower === 'usecase')
|