@dr2rai/raid-canvas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,575 @@
1
+ /**
2
+ * @file X6Shapes.ts
3
+ * @description Custom AntV X6 shape registrations for the AOAIM ontological contract.
4
+ *
5
+ * Implements:
6
+ * - 4-way orthogonal ports (top, right, bottom, left) with magnet snap points.
7
+ * - Manhattan orthogonal routing with rounded corners (radius: 8).
8
+ * - Entity archetypes: UseCase (uc), Activity (act), Class (cls), Object (obj), Person (per).
9
+ * - Cascais Heraldry design tokens.
10
+ */
11
+
12
+ import { Graph, Node, Edge } from '@antv/x6';
13
+ import type {
14
+ AimEdgeKind,
15
+ RaidNodeData,
16
+ RaidEdgeData,
17
+ OrthogonalPortId,
18
+ } from './types.js';
19
+
20
+ /**
21
+ * Cascais Heraldry color palette constants.
22
+ */
23
+ export const CascaisPalette = {
24
+ NetGold: '#F59E0B',
25
+ HeraldicGreen: '#10B981',
26
+ WarmGraphite: '#1F2937',
27
+ GraphiteMuted: '#4B5563',
28
+ SilverLine: '#E5E7EB',
29
+ SilverLineDark: '#D1D5DB',
30
+ ChalkWhite: '#FFFFFF',
31
+ CanvasCream: '#F8FAFC',
32
+ AccentBlue: '#3B82F6',
33
+ TextPrimary: '#111827',
34
+ TextSecondary: '#4B5563',
35
+ } as const;
36
+
37
+ /**
38
+ * Port configuration generating 4 orthogonal snap anchors.
39
+ */
40
+ export function createOrthogonalPorts() {
41
+ const portMarkup = [
42
+ {
43
+ tagName: 'circle',
44
+ selector: 'portBody',
45
+ },
46
+ ];
47
+
48
+ const portAttrs = {
49
+ portBody: {
50
+ r: 4,
51
+ magnet: true,
52
+ stroke: CascaisPalette.WarmGraphite,
53
+ fill: CascaisPalette.ChalkWhite,
54
+ strokeWidth: 1.5,
55
+ style: {
56
+ visibility: 'hidden',
57
+ },
58
+ },
59
+ };
60
+
61
+ return {
62
+ groups: {
63
+ top: {
64
+ position: 'top',
65
+ markup: portMarkup,
66
+ attrs: portAttrs,
67
+ },
68
+ right: {
69
+ position: 'right',
70
+ markup: portMarkup,
71
+ attrs: portAttrs,
72
+ },
73
+ bottom: {
74
+ position: 'bottom',
75
+ markup: portMarkup,
76
+ attrs: portAttrs,
77
+ },
78
+ left: {
79
+ position: 'left',
80
+ markup: portMarkup,
81
+ attrs: portAttrs,
82
+ },
83
+ },
84
+ items: [
85
+ { id: 'port-top' satisfies OrthogonalPortId, group: 'top' },
86
+ { id: 'port-right' satisfies OrthogonalPortId, group: 'right' },
87
+ { id: 'port-bottom' satisfies OrthogonalPortId, group: 'bottom' },
88
+ { id: 'port-left' satisfies OrthogonalPortId, group: 'left' },
89
+ ],
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Guard flag to ensure shapes are registered only once per runtime.
95
+ */
96
+ let shapesRegistered = false;
97
+
98
+ /**
99
+ * Register all AOAIM ontological shapes with AntV X6.
100
+ * Safe to call multiple times (idempotent).
101
+ */
102
+ export function registerAimShapes(): void {
103
+ if (shapesRegistered) {
104
+ return;
105
+ }
106
+
107
+ // 1. AimUseCaseNode ('uc') — Ellipse with Net Gold border
108
+ Node.define({
109
+ shape: 'aim-uc',
110
+ inherit: 'ellipse',
111
+ width: 140,
112
+ height: 70,
113
+ attrs: {
114
+ body: {
115
+ fill: CascaisPalette.ChalkWhite,
116
+ stroke: CascaisPalette.NetGold,
117
+ strokeWidth: 2,
118
+ rx: 70,
119
+ ry: 35,
120
+ class: 'aim-node aim-uc',
121
+ },
122
+ label: {
123
+ text: 'UseCase',
124
+ fill: CascaisPalette.TextPrimary,
125
+ fontSize: 13,
126
+ fontWeight: 'bold',
127
+ fontFamily: 'Inter, system-ui, -apple-system, sans-serif',
128
+ textAnchor: 'middle',
129
+ textVerticalAnchor: 'middle',
130
+ },
131
+ },
132
+ ports: createOrthogonalPorts(),
133
+ });
134
+
135
+ // 2. AimActivityNode ('act') — Rounded rectangle with Heraldic Green border
136
+ Node.define({
137
+ shape: 'aim-act',
138
+ inherit: 'rect',
139
+ width: 150,
140
+ height: 60,
141
+ attrs: {
142
+ body: {
143
+ fill: CascaisPalette.CanvasCream,
144
+ stroke: CascaisPalette.HeraldicGreen,
145
+ strokeWidth: 2,
146
+ rx: 12,
147
+ ry: 12,
148
+ class: 'aim-node aim-act',
149
+ },
150
+ label: {
151
+ text: 'Activity',
152
+ fill: CascaisPalette.TextPrimary,
153
+ fontSize: 13,
154
+ fontWeight: '600',
155
+ fontFamily: 'Inter, system-ui, -apple-system, sans-serif',
156
+ textAnchor: 'middle',
157
+ textVerticalAnchor: 'middle',
158
+ },
159
+ },
160
+ ports: createOrthogonalPorts(),
161
+ });
162
+
163
+ // 3. AimClassNode ('cls') — Compartmentalized class card
164
+ Node.define({
165
+ shape: 'aim-cls',
166
+ inherit: 'rect',
167
+ width: 180,
168
+ height: 100,
169
+ markup: [
170
+ {
171
+ tagName: 'rect',
172
+ selector: 'body',
173
+ },
174
+ {
175
+ tagName: 'rect',
176
+ selector: 'header',
177
+ },
178
+ {
179
+ tagName: 'text',
180
+ selector: 'title',
181
+ },
182
+ {
183
+ tagName: 'line',
184
+ selector: 'divider1',
185
+ },
186
+ {
187
+ tagName: 'text',
188
+ selector: 'attributes',
189
+ },
190
+ {
191
+ tagName: 'line',
192
+ selector: 'divider2',
193
+ },
194
+ {
195
+ tagName: 'text',
196
+ selector: 'methods',
197
+ },
198
+ ],
199
+ attrs: {
200
+ body: {
201
+ fill: CascaisPalette.ChalkWhite,
202
+ stroke: CascaisPalette.WarmGraphite,
203
+ strokeWidth: 1.5,
204
+ class: 'aim-node aim-cls',
205
+ },
206
+ header: {
207
+ fill: CascaisPalette.CanvasCream,
208
+ stroke: 'none',
209
+ height: 28,
210
+ },
211
+ title: {
212
+ text: 'Class',
213
+ fill: CascaisPalette.TextPrimary,
214
+ fontSize: 12,
215
+ fontWeight: 'bold',
216
+ fontFamily: 'Inter, system-ui, -apple-system, sans-serif',
217
+ refX: 0.5,
218
+ refY: 14,
219
+ textAnchor: 'middle',
220
+ textVerticalAnchor: 'middle',
221
+ },
222
+ divider1: {
223
+ stroke: CascaisPalette.SilverLine,
224
+ strokeWidth: 1,
225
+ refX: 0,
226
+ refY: 28,
227
+ refWidth: '100%',
228
+ },
229
+ attributes: {
230
+ text: '+ id: string\n+ state: string',
231
+ fill: CascaisPalette.TextSecondary,
232
+ fontSize: 11,
233
+ fontFamily: 'JetBrains Mono, Menlo, monospace',
234
+ refX: 8,
235
+ refY: 34,
236
+ textAnchor: 'start',
237
+ textVerticalAnchor: 'top',
238
+ },
239
+ divider2: {
240
+ stroke: CascaisPalette.SilverLine,
241
+ strokeWidth: 1,
242
+ refX: 0,
243
+ refY: 65,
244
+ refWidth: '100%',
245
+ },
246
+ methods: {
247
+ text: '+ execute(): void',
248
+ fill: CascaisPalette.TextSecondary,
249
+ fontSize: 11,
250
+ fontFamily: 'JetBrains Mono, Menlo, monospace',
251
+ refX: 8,
252
+ refY: 71,
253
+ textAnchor: 'start',
254
+ textVerticalAnchor: 'top',
255
+ },
256
+ },
257
+ ports: createOrthogonalPorts(),
258
+ });
259
+
260
+ // 4. AimObjectNode ('obj') — Instance card with underlined title
261
+ Node.define({
262
+ shape: 'aim-obj',
263
+ inherit: 'rect',
264
+ width: 160,
265
+ height: 80,
266
+ attrs: {
267
+ body: {
268
+ fill: CascaisPalette.ChalkWhite,
269
+ stroke: CascaisPalette.SilverLineDark,
270
+ strokeWidth: 1.5,
271
+ class: 'aim-node aim-obj',
272
+ },
273
+ label: {
274
+ text: '<u>instance: Type</u>',
275
+ fill: CascaisPalette.TextPrimary,
276
+ fontSize: 12,
277
+ fontFamily: 'Inter, system-ui, -apple-system, sans-serif',
278
+ textAnchor: 'middle',
279
+ textVerticalAnchor: 'middle',
280
+ },
281
+ },
282
+ ports: createOrthogonalPorts(),
283
+ });
284
+
285
+ // 5. AimPersonNode ('per') — Person / Actor role card
286
+ Node.define({
287
+ shape: 'aim-per',
288
+ inherit: 'rect',
289
+ width: 120,
290
+ height: 70,
291
+ attrs: {
292
+ body: {
293
+ fill: CascaisPalette.CanvasCream,
294
+ stroke: CascaisPalette.WarmGraphite,
295
+ strokeWidth: 1.5,
296
+ rx: 6,
297
+ ry: 6,
298
+ class: 'aim-node aim-per',
299
+ },
300
+ label: {
301
+ text: '«actor»\nOperator',
302
+ fill: CascaisPalette.TextPrimary,
303
+ fontSize: 12,
304
+ fontWeight: '500',
305
+ fontFamily: 'Inter, system-ui, -apple-system, sans-serif',
306
+ textAnchor: 'middle',
307
+ textVerticalAnchor: 'middle',
308
+ },
309
+ },
310
+ ports: createOrthogonalPorts(),
311
+ });
312
+
313
+ // 6. AimEdge — Orthogonal Manhattan edge with rounded corners
314
+ Edge.define({
315
+ shape: 'aim-edge',
316
+ inherit: 'edge',
317
+ router: {
318
+ name: 'manhattan',
319
+ args: {
320
+ padding: 20,
321
+ startDirections: ['top', 'right', 'bottom', 'left'],
322
+ endDirections: ['top', 'right', 'bottom', 'left'],
323
+ },
324
+ },
325
+ connector: {
326
+ name: 'rounded',
327
+ args: {
328
+ radius: 8,
329
+ },
330
+ },
331
+ attrs: {
332
+ line: {
333
+ stroke: CascaisPalette.WarmGraphite,
334
+ strokeWidth: 1.5,
335
+ targetMarker: {
336
+ name: 'classic',
337
+ size: 7,
338
+ },
339
+ class: 'aim-edge',
340
+ },
341
+ },
342
+ });
343
+
344
+ shapesRegistered = true;
345
+ }
346
+
347
+ /**
348
+ * Configure an AntV X6 Graph instance with default AOAIM canvas settings:
349
+ * Manhattan routing, orthogonal connection rules, port hover visibility.
350
+ */
351
+ export function configureAimGraph(graph: Graph): void {
352
+ registerAimShapes();
353
+
354
+ // Show ports on node mouseenter, hide on mouseleave
355
+ graph.on('node:mouseenter', ({ node }) => {
356
+ const ports = node.getPorts();
357
+ for (const port of ports) {
358
+ if (port.id) {
359
+ node.portProp(port.id, 'attrs/portBody/style/visibility', 'visible');
360
+ }
361
+ }
362
+ });
363
+
364
+ graph.on('node:mouseleave', ({ node }) => {
365
+ const ports = node.getPorts();
366
+ for (const port of ports) {
367
+ if (port.id) {
368
+ node.portProp(port.id, 'attrs/portBody/style/visibility', 'hidden');
369
+ }
370
+ }
371
+ });
372
+ }
373
+
374
+ /**
375
+ * Factory creating an AntV X6 Node model from a RaidNodeData specification.
376
+ */
377
+ export function createAimNode(data: RaidNodeData): Node.Metadata {
378
+ registerAimShapes();
379
+
380
+ const shapeName = `aim-${data.kind}`;
381
+ const baseMetadata: Node.Metadata = {
382
+ id: data.id,
383
+ shape: shapeName,
384
+ x: data.bounds.x,
385
+ y: data.bounds.y,
386
+ width: data.bounds.width,
387
+ height: data.bounds.height,
388
+ data,
389
+ };
390
+
391
+ // Archetype-specific customization
392
+ switch (data.kind) {
393
+ case 'uc':
394
+ return {
395
+ ...baseMetadata,
396
+ attrs: {
397
+ label: {
398
+ text: data.stereotype ? `${data.stereotype}\n${data.displayName}` : data.displayName,
399
+ },
400
+ },
401
+ };
402
+
403
+ case 'act':
404
+ return {
405
+ ...baseMetadata,
406
+ attrs: {
407
+ label: {
408
+ text: data.stereotype ? `${data.stereotype}\n${data.displayName}` : data.displayName,
409
+ },
410
+ },
411
+ };
412
+
413
+ case 'cls':
414
+ return {
415
+ ...baseMetadata,
416
+ attrs: {
417
+ title: {
418
+ text: data.displayName,
419
+ },
420
+ attributes: {
421
+ text: data.attributes && data.attributes.length > 0 ? data.attributes.join('\n') : '',
422
+ },
423
+ methods: {
424
+ text: data.methods && data.methods.length > 0 ? data.methods.join('\n') : '',
425
+ },
426
+ },
427
+ };
428
+
429
+ case 'obj':
430
+ return {
431
+ ...baseMetadata,
432
+ attrs: {
433
+ label: {
434
+ text: data.displayName,
435
+ },
436
+ },
437
+ };
438
+
439
+ case 'per': {
440
+ const isInitiating = data.stereotype?.toLowerCase().includes('initiates') ?? false;
441
+ return {
442
+ ...baseMetadata,
443
+ attrs: {
444
+ body: {
445
+ stroke: isInitiating ? CascaisPalette.NetGold : CascaisPalette.WarmGraphite,
446
+ strokeWidth: isInitiating ? 2 : 1.5,
447
+ },
448
+ label: {
449
+ text: data.stereotype ? `${data.stereotype}\n${data.displayName}` : data.displayName,
450
+ },
451
+ },
452
+ };
453
+ }
454
+
455
+ default:
456
+ return baseMetadata;
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Factory creating an AntV X6 Edge model from a RaidEdgeData specification.
462
+ */
463
+ export function createAimEdge(data: RaidEdgeData): Edge.Metadata {
464
+ registerAimShapes();
465
+
466
+ const edgeAttrs = getEdgeStyling(data.kind);
467
+
468
+ return {
469
+ id: data.id,
470
+ shape: 'aim-edge',
471
+ source: {
472
+ cell: data.sourceId,
473
+ ...(data.sourcePort !== undefined ? { port: data.sourcePort } : {}),
474
+ },
475
+ target: {
476
+ cell: data.targetId,
477
+ ...(data.targetPort !== undefined ? { port: data.targetPort } : {}),
478
+ },
479
+ vertices: data.bendPoints.map((pt) => ({ x: pt.x, y: pt.y })),
480
+ labels: data.label
481
+ ? [
482
+ {
483
+ attrs: {
484
+ text: {
485
+ text: data.label,
486
+ fill: CascaisPalette.TextSecondary,
487
+ fontSize: 11,
488
+ },
489
+ },
490
+ position: 0.5,
491
+ },
492
+ ]
493
+ : undefined,
494
+ attrs: {
495
+ line: edgeAttrs,
496
+ },
497
+ data,
498
+ };
499
+ }
500
+
501
+ /**
502
+ * Returns SVG path stroke and marker attributes based on AimEdgeKind.
503
+ */
504
+ function getEdgeStyling(kind: AimEdgeKind): Record<string, unknown> {
505
+ switch (kind) {
506
+ case 'dependency':
507
+ return {
508
+ stroke: CascaisPalette.WarmGraphite,
509
+ strokeWidth: 1.5,
510
+ strokeDasharray: '5,5',
511
+ targetMarker: {
512
+ name: 'open',
513
+ size: 8,
514
+ },
515
+ };
516
+
517
+ case 'generalization':
518
+ return {
519
+ stroke: CascaisPalette.WarmGraphite,
520
+ strokeWidth: 1.5,
521
+ targetMarker: {
522
+ name: 'classic',
523
+ size: 10,
524
+ fill: CascaisPalette.ChalkWhite,
525
+ },
526
+ };
527
+
528
+ case 'realization':
529
+ return {
530
+ stroke: CascaisPalette.WarmGraphite,
531
+ strokeWidth: 1.5,
532
+ strokeDasharray: '5,5',
533
+ targetMarker: {
534
+ name: 'classic',
535
+ size: 10,
536
+ fill: CascaisPalette.ChalkWhite,
537
+ },
538
+ };
539
+
540
+ case 'aggregation':
541
+ return {
542
+ stroke: CascaisPalette.WarmGraphite,
543
+ strokeWidth: 1.5,
544
+ sourceMarker: {
545
+ name: 'diamond',
546
+ size: 10,
547
+ fill: CascaisPalette.ChalkWhite,
548
+ },
549
+ targetMarker: null,
550
+ };
551
+
552
+ case 'composition':
553
+ return {
554
+ stroke: CascaisPalette.WarmGraphite,
555
+ strokeWidth: 1.5,
556
+ sourceMarker: {
557
+ name: 'diamond',
558
+ size: 10,
559
+ fill: CascaisPalette.WarmGraphite,
560
+ },
561
+ targetMarker: null,
562
+ };
563
+
564
+ case 'association':
565
+ default:
566
+ return {
567
+ stroke: CascaisPalette.WarmGraphite,
568
+ strokeWidth: 1.5,
569
+ targetMarker: {
570
+ name: 'classic',
571
+ size: 7,
572
+ },
573
+ };
574
+ }
575
+ }
package/src/index.ts ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @file index.ts
3
+ * @description Main public entry point for @dr2rai/raid-canvas.
4
+ *
5
+ * Provides:
6
+ * - AntV X6 custom shape registrations for AOAIM ontological entities.
7
+ * - 4-way orthogonal ports and Manhattan router integration.
8
+ * - Cascais Heraldry design tokens and theme styling.
9
+ * - Bidirectional RaiBridge for hydrating from and serializing to the aim-* SVG contract.
10
+ */
11
+
12
+ // Core Metamodel & SVG Contract Types
13
+ export {
14
+ AimSvgContract,
15
+ type AimOntologyKind,
16
+ type AimEdgeKind,
17
+ type Point,
18
+ type SvgBendPoint,
19
+ type Bounds,
20
+ type OrthogonalPortId,
21
+ type RaidNodeData,
22
+ type RaidEdgeData,
23
+ type RaidMetamodel,
24
+ type HydrationOptions,
25
+ type SerializationOptions,
26
+ } from './types.js';
27
+
28
+ // AntV X6 Custom Shapes & Port Registrations
29
+ export {
30
+ CascaisPalette,
31
+ createOrthogonalPorts,
32
+ registerAimShapes,
33
+ configureAimGraph,
34
+ createAimNode,
35
+ createAimEdge,
36
+ } from './X6Shapes.js';
37
+
38
+ // Bidirectional SVG <-> X6 Synchronization Bridge
39
+ export { RaiBridge } from './RaiBridge.js';