@dr2rai/raid-canvas 0.3.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/types.d.ts CHANGED
@@ -105,6 +105,8 @@ export interface RaidEdgeData {
105
105
  readonly routing?: AimRoutingMode;
106
106
  /** User-editable or router-computed Manhattan bend points. */
107
107
  readonly bendPoints: readonly SvgBendPoint[];
108
+ /** Precomputed or live SVG path data ('M ... L ...') for standalone vector rendering. */
109
+ readonly pathData?: string;
108
110
  }
109
111
  /**
110
112
  * Full in-memory metamodel representation corresponding to a `.raid` manifest.
@@ -120,6 +122,8 @@ export interface RaidMetamodel {
120
122
  readonly nodes: readonly RaidNodeData[];
121
123
  /** Relationships connecting projected nodes. */
122
124
  readonly edges: readonly RaidEdgeData[];
125
+ /** Diagram-level edge routing mode ('manhattan', 'normal', 'smooth'). */
126
+ readonly routing?: AimRoutingMode;
123
127
  /** Additional diagram-level metadata. */
124
128
  readonly metadata?: Readonly<Record<string, unknown>>;
125
129
  }
@@ -152,6 +156,12 @@ export interface HydrationOptions {
152
156
  readonly clearGraph?: boolean;
153
157
  /** If true, automatically executes Manhattan routing if bend points are missing. Default: true. */
154
158
  readonly autoRouteEdges?: boolean;
159
+ /**
160
+ * If true, infers docking ports (e.g. port-right, port-left) based on node geometry when ports
161
+ * are omitted in the SVG. When false (default), edges bind directly to node cells without ports,
162
+ * enabling dynamic Manhattan center-aiming routing. Default: false.
163
+ */
164
+ readonly inferPorts?: boolean;
155
165
  /** Default fallback dimensions when width/height are unspecified in SVG. */
156
166
  readonly defaultNodeSize?: {
157
167
  readonly width: number;
@@ -168,5 +178,7 @@ export interface SerializationOptions {
168
178
  readonly format?: boolean;
169
179
  /** Inject Cascais design token CSS variables into `<defs><style>`. Default: true. */
170
180
  readonly embedStyles?: boolean;
181
+ /** Optional diagram-level routing mode to serialize onto root `<svg>` tag. */
182
+ readonly routingMode?: AimRoutingMode;
171
183
  }
172
184
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;;GAQG;AACH,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AAEnE;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE/D;;GAEG;AACH,MAAM,MAAM,WAAW,GACnB,aAAa,GACb,YAAY,GACZ,gBAAgB,GAChB,aAAa,GACb,aAAa,GACb,aAAa,CAAC;AAElB;;GAEG;AACH,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,YAAY,GAAG,aAAa,GAAG,WAAW,CAAC;AAEvF;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAEpB,oCAAoC;IACpC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAE/B,6CAA6C;IAC7C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAE7B,yCAAyC;IACzC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAE5B,kEAAkE;IAClE,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAExC,2DAA2D;IAC3D,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAErC,kCAAkC;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB,gFAAgF;IAChF,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACzD;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,8BAA8B;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAEpB,qCAAqC;IACrC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAE3B,qCAAqC;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,qCAAqC;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,4CAA4C;IAC5C,QAAQ,CAAC,UAAU,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAAC;IAEhD,4CAA4C;IAC5C,QAAQ,CAAC,UAAU,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAAC;IAEhD,gCAAgC;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAExB,iEAAiE;IACjE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAE7B,wEAAwE;IACxE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAEpC,wEAAwE;IACxE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAEpC,wEAAwE;IACxE,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC;IAElC,8DAA8D;IAC9D,QAAQ,CAAC,UAAU,EAAE,SAAS,YAAY,EAAE,CAAC;IAE7C,yFAAyF;IACzF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,qDAAqD;IACrD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAE3B,qCAAqC;IACrC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAExB,+FAA+F;IAC/F,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAE3B,6CAA6C;IAC7C,QAAQ,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,CAAC;IAExC,gDAAgD;IAChD,QAAQ,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,CAAC;IAExC,yEAAyE;IACzE,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC;IAElC,yCAAyC;IACzC,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED;;GAEG;AACH,eAAO,MAAM,cAAc;aAEzB,SAAS,EAAE,UAAU;aACrB,OAAO,EAAE,QAAQ;aACjB,SAAS,EAAE,UAAU;aACrB,iBAAiB,EAAE,kBAAkB;aACrC,eAAe,EAAE,gBAAgB;aACjC,cAAc,EAAE,eAAe;aAG/B,SAAS,EAAE,UAAU;aACrB,cAAc,EAAE,eAAe;aAC/B,WAAW,EAAE,YAAY;aACzB,WAAW,EAAE,YAAY;aACzB,gBAAgB,EAAE,iBAAiB;aACnC,gBAAgB,EAAE,iBAAiB;aACnC,YAAY,EAAE,aAAa;aAC3B,UAAU,EAAE,WAAW;aAGvB,aAAa,EAAE,sCAAsC;aACrD,aAAa,EAAE,iCAAiC;CACxC,CAAC;AAEX;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,oFAAoF;IACpF,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAE9B,mGAAmG;IACnG,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAElC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAE9B,4EAA4E;IAC5E,QAAQ,CAAC,eAAe,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAChF;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAE1B,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAE1B,qFAAqF;IACrF,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAE/B,8EAA8E;IAC9E,QAAQ,CAAC,WAAW,CAAC,EAAE,cAAc,CAAC;CACvC"}
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA+JH;;GAEG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,4BAA4B;IAC5B,SAAS,EAAE,UAAU;IACrB,OAAO,EAAE,QAAQ;IACjB,SAAS,EAAE,UAAU;IACrB,iBAAiB,EAAE,kBAAkB;IACrC,eAAe,EAAE,gBAAgB;IACjC,cAAc,EAAE,eAAe;IAE/B,4BAA4B;IAC5B,SAAS,EAAE,UAAU;IACrB,cAAc,EAAE,eAAe;IAC/B,WAAW,EAAE,YAAY;IACzB,WAAW,EAAE,YAAY;IACzB,gBAAgB,EAAE,iBAAiB;IACnC,gBAAgB,EAAE,iBAAiB;IACnC,YAAY,EAAE,aAAa;IAC3B,UAAU,EAAE,WAAW;IAEvB,4BAA4B;IAC5B,aAAa,EAAE,sCAAsC;IACrD,aAAa,EAAE,iCAAiC;CACxC,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAqKH;;GAEG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,4BAA4B;IAC5B,SAAS,EAAE,UAAU;IACrB,OAAO,EAAE,QAAQ;IACjB,SAAS,EAAE,UAAU;IACrB,iBAAiB,EAAE,kBAAkB;IACrC,eAAe,EAAE,gBAAgB;IACjC,cAAc,EAAE,eAAe;IAE/B,4BAA4B;IAC5B,SAAS,EAAE,UAAU;IACrB,cAAc,EAAE,eAAe;IAC/B,WAAW,EAAE,YAAY;IACzB,WAAW,EAAE,YAAY;IACzB,gBAAgB,EAAE,iBAAiB;IACnC,gBAAgB,EAAE,iBAAiB;IACnC,YAAY,EAAE,aAAa;IAC3B,UAAU,EAAE,WAAW;IAEvB,4BAA4B;IAC5B,aAAa,EAAE,sCAAsC;IACrD,aAAa,EAAE,iCAAiC;CACxC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dr2rai/raid-canvas",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Interactive AntV X6 diagram canvas for AOAIM (.raid) models and ontological SVG contracts",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
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 { createAimNode, createAimEdge, configureAimGraph, CascaisPalette } from './X6Shapes.js';
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 not explicitly declared
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.toLowerCase() === 'rect' ? el : null);
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.toLowerCase() === 'ellipse' ? el : null);
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
- private updateExistingSvg(
541
+ public updateExistingSvg(
427
542
  baseSvg: string,
428
543
  model: RaidMetamodel,
429
544
  _options: SerializationOptions,
@@ -435,6 +550,51 @@ export class RaiBridge {
435
550
  const parser = new DOMParser();
436
551
  const doc = parser.parseFromString(baseSvg, 'image/svg+xml');
437
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
+
438
598
  // Update node positions and transforms
439
599
  for (const node of model.nodes) {
440
600
  const el = doc.querySelector(`[${AimSvgContract.ATTR_ID}="${node.id}"], [id="${node.id}"]`);
@@ -465,11 +625,15 @@ export class RaiBridge {
465
625
  if (edge.targetId !== undefined) {
466
626
  el.setAttribute(AimSvgContract.ATTR_TARGET, edge.targetId);
467
627
  }
468
- if (edge.sourcePort !== undefined) {
628
+ if (edge.sourcePort !== undefined && edge.sourcePort !== '' && edge.sourcePort !== 'auto') {
469
629
  el.setAttribute(AimSvgContract.ATTR_SOURCE_PORT, edge.sourcePort);
630
+ } else {
631
+ el.removeAttribute(AimSvgContract.ATTR_SOURCE_PORT);
470
632
  }
471
- if (edge.targetPort !== undefined) {
633
+ if (edge.targetPort !== undefined && edge.targetPort !== '' && edge.targetPort !== 'auto') {
472
634
  el.setAttribute(AimSvgContract.ATTR_TARGET_PORT, edge.targetPort);
635
+ } else {
636
+ el.removeAttribute(AimSvgContract.ATTR_TARGET_PORT);
473
637
  }
474
638
  if (edge.routing !== undefined) {
475
639
  el.setAttribute(AimSvgContract.ATTR_ROUTING, edge.routing);
@@ -483,17 +647,83 @@ export class RaiBridge {
483
647
  if (edge.targetCardinality !== undefined) {
484
648
  el.setAttribute('aim-target-cardinality', edge.targetCardinality);
485
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);
486
683
  }
487
684
  }
488
685
 
489
686
  return new XMLSerializer().serializeToString(doc);
490
687
  }
491
688
 
492
- private generateFreshSvg(model: RaidMetamodel, options: SerializationOptions): string {
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 {
493
720
  const width = Math.max(800, ...model.nodes.map((n) => n.bounds.x + n.bounds.width + 100));
494
721
  const height = Math.max(600, ...model.nodes.map((n) => n.bounds.y + n.bounds.height + 100));
495
722
 
496
- let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" id="${model.diagramId}" aim-archetype="${model.archetype}">\n`;
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`;
497
727
 
498
728
  // Definitions & Markers
499
729
  svg += ` <defs>\n`;
@@ -509,6 +739,7 @@ export class RaiBridge {
509
739
  svg += ` .aim-node:hover { filter: drop-shadow(0 4px 6px rgba(0,0,0,0.1)); }\n`;
510
740
  svg += ` .aim-edge { fill: none; stroke: ${CascaisPalette.WarmGraphite}; stroke-width: 1.5; }\n`;
511
741
  svg += ` text { font-family: Inter, system-ui, sans-serif; }\n`;
742
+ svg += ` .aim-act text, .aim-obj text { text-decoration: underline; }\n`;
512
743
  svg += ` </style>\n`;
513
744
  }
514
745
  svg += ` </defs>\n\n`;
@@ -522,22 +753,41 @@ export class RaiBridge {
522
753
  const markerEnd = edge.kind === 'generalization' ? ' marker-end="url(#arrow-hollow)"' : ' marker-end="url(#arrow-classic)"';
523
754
 
524
755
  // Path data construction
525
- let pathD = '';
526
- if (edge.bendPoints.length > 0) {
527
- const first = edge.bendPoints[0]!;
528
- pathD = `M ${first.x} ${first.y} ` + edge.bendPoints.slice(1).map((p) => `L ${p.x} ${p.y}`).join(' ');
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
+ }
529
766
  }
530
767
 
531
- const sourcePortAttr = edge.sourcePort ? ` ${AimSvgContract.ATTR_SOURCE_PORT}="${edge.sourcePort}"` : '';
532
- const targetPortAttr = edge.targetPort ? ` ${AimSvgContract.ATTR_TARGET_PORT}="${edge.targetPort}"` : '';
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}"` : '';
533
770
  const routingAttr = edge.routing ? ` ${AimSvgContract.ATTR_ROUTING}="${edge.routing}"` : '';
534
771
 
535
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`;
536
773
  if (pathD) {
537
- 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`;
538
775
  }
539
776
  if (edge.label) {
540
- const midPoint = edge.bendPoints[Math.floor(edge.bendPoints.length / 2)] ?? { x: 50, y: 50 };
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
+ })();
541
791
  svg += ` <text x="${midPoint.x}" y="${midPoint.y - 8}" font-size="11" fill="${CascaisPalette.TextSecondary}" text-anchor="middle">${edge.label}</text>\n`;
542
792
  }
543
793
  svg += ` </g>\n`;
@@ -554,13 +804,24 @@ export class RaiBridge {
554
804
  const rx = node.bounds.width / 2;
555
805
  const ry = node.bounds.height / 2;
556
806
  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`;
807
+ svg += this.renderSvgText(node.displayName, rx, ry, 13, 'bold', CascaisPalette.TextPrimary, false);
558
808
  } else if (node.kind === 'act') {
559
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`;
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`;
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);
561
822
  } else {
562
823
  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`;
824
+ svg += this.renderSvgText(node.displayName, node.bounds.width / 2, node.bounds.height / 2, 12, 'normal', CascaisPalette.TextPrimary, false);
564
825
  }
565
826
 
566
827
  svg += ` </g>\n`;
@@ -571,6 +832,14 @@ export class RaiBridge {
571
832
  return svg;
572
833
  }
573
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
+
574
843
  private normalizeKind(kind: string): AimOntologyKind {
575
844
  const lower = kind.toLowerCase();
576
845
  if (lower === 'uc' || lower === 'usecase') return 'uc';