vizcraft 0.1.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.
@@ -0,0 +1,742 @@
1
+ import { DEFAULT_VIZ_CSS } from './styles';
2
+ import { defaultCoreAnimationRegistry } from './animations';
3
+ import { defaultCoreOverlayRegistry } from './overlays';
4
+ class VizBuilderImpl {
5
+ _viewBox = { w: 800, h: 600 };
6
+ _nodes = new Map();
7
+ _edges = new Map();
8
+ _overlays = [];
9
+ _nodeOrder = [];
10
+ _edgeOrder = [];
11
+ _gridConfig = null;
12
+ /**
13
+ * Sets the view box.
14
+ * @param w The width of the view box
15
+ * @param h The height of the view box
16
+ * @returns The builder
17
+ */
18
+ view(w, h) {
19
+ this._viewBox = { w, h };
20
+ return this;
21
+ }
22
+ /**
23
+ * Sets the grid configuration.
24
+ * @param cols The number of columns
25
+ * @param rows The number of rows
26
+ * @param padding The padding of the grid
27
+ * @returns The builder
28
+ */
29
+ grid(cols, rows, padding = { x: 20, y: 20 }) {
30
+ this._gridConfig = { cols, rows, padding };
31
+ return this;
32
+ }
33
+ /**
34
+ * Adds an overlay to the scene.
35
+ * @param id The ID of the overlay
36
+ * @param params The parameters of the overlay
37
+ * @param key The key of the overlay
38
+ * @returns The builder
39
+ */
40
+ overlay(id, params, key) {
41
+ this._overlays.push({ id, params, key });
42
+ return this;
43
+ }
44
+ /**
45
+ * Creates a node.
46
+ * @param id The ID of the node
47
+ * @returns The node builder
48
+ */
49
+ node(id) {
50
+ if (!this._nodes.has(id)) {
51
+ // Set default position and shape
52
+ this._nodes.set(id, {
53
+ id,
54
+ pos: { x: 0, y: 0 },
55
+ shape: { kind: 'circle', r: 10 },
56
+ });
57
+ this._nodeOrder.push(id);
58
+ }
59
+ return new NodeBuilderImpl(this, this._nodes.get(id)); // The ! asserts that the node exists, because we just added it
60
+ }
61
+ /**
62
+ * Creates an edge between two nodes.
63
+ * @param from The source node
64
+ * @param to The target node
65
+ * @param id The ID of the edge
66
+ * @returns The edge builder
67
+ */
68
+ edge(from, to, id) {
69
+ const edgeId = id || `${from}->${to}`;
70
+ if (!this._edges.has(edgeId)) {
71
+ this._edges.set(edgeId, { id: edgeId, from, to });
72
+ this._edgeOrder.push(edgeId);
73
+ }
74
+ return new EdgeBuilderImpl(this, this._edges.get(edgeId));
75
+ }
76
+ /**
77
+ * Builds the scene.
78
+ * @returns The scene
79
+ */
80
+ build() {
81
+ this._edges.forEach((edge) => {
82
+ if (!this._nodes.has(edge.from)) {
83
+ console.warn(`VizBuilder: Edge ${edge.id} references missing source node ${edge.from}`);
84
+ }
85
+ if (!this._nodes.has(edge.to)) {
86
+ console.warn(`VizBuilder: Edge ${edge.id} references missing target node ${edge.to}`);
87
+ }
88
+ });
89
+ const nodes = this._nodeOrder.map((id) => this._nodes.get(id));
90
+ const edges = this._edgeOrder.map((id) => this._edges.get(id));
91
+ return {
92
+ viewBox: this._viewBox,
93
+ grid: this._gridConfig || undefined,
94
+ nodes,
95
+ edges,
96
+ overlays: this._overlays,
97
+ };
98
+ }
99
+ _getGridConfig() {
100
+ return this._gridConfig;
101
+ }
102
+ _getViewBox() {
103
+ return this._viewBox;
104
+ }
105
+ /**
106
+ * Returns the SVG string representation of the scene.
107
+ * @deprecated Use `mount` instead
108
+ */
109
+ svg() {
110
+ const scene = this.build();
111
+ return this._renderSceneToSvg(scene);
112
+ }
113
+ /**
114
+ * Mounts the scene to the DOM.
115
+ * @param container The container to mount the scene into
116
+ */
117
+ mount(container) {
118
+ const scene = this.build();
119
+ this._renderSceneToDOM(scene, container);
120
+ }
121
+ /**
122
+ * Renders the scene to the DOM.
123
+ * @param scene The scene to render
124
+ * @param container The container to render the scene into
125
+ */
126
+ _renderSceneToDOM(scene, container) {
127
+ const { viewBox, nodes, edges, overlays } = scene;
128
+ const nodesById = new Map(nodes.map((n) => [n.id, n]));
129
+ const svgNS = 'http://www.w3.org/2000/svg';
130
+ let svg = container.querySelector('svg');
131
+ // Initial Render if SVG doesn't exist
132
+ if (!svg) {
133
+ container.innerHTML = ''; // Safety clear
134
+ svg = document.createElementNS(svgNS, 'svg');
135
+ svg.style.width = '100%';
136
+ svg.style.height = '100%';
137
+ svg.style.overflow = 'visible';
138
+ // Inject Styles
139
+ const style = document.createElement('style');
140
+ style.textContent = DEFAULT_VIZ_CSS;
141
+ svg.appendChild(style);
142
+ // Defs
143
+ const defs = document.createElementNS(svgNS, 'defs');
144
+ const marker = document.createElementNS(svgNS, 'marker');
145
+ marker.setAttribute('id', 'viz-arrow');
146
+ marker.setAttribute('markerWidth', '10');
147
+ marker.setAttribute('markerHeight', '7');
148
+ marker.setAttribute('refX', '9');
149
+ marker.setAttribute('refY', '3.5');
150
+ marker.setAttribute('orient', 'auto');
151
+ const poly = document.createElementNS(svgNS, 'polygon');
152
+ poly.setAttribute('points', '0 0, 10 3.5, 0 7');
153
+ poly.setAttribute('fill', 'currentColor');
154
+ marker.appendChild(poly);
155
+ defs.appendChild(marker);
156
+ svg.appendChild(defs);
157
+ // Layers
158
+ const edgeLayer = document.createElementNS(svgNS, 'g');
159
+ edgeLayer.setAttribute('class', 'viz-layer-edges');
160
+ svg.appendChild(edgeLayer);
161
+ const nodeLayer = document.createElementNS(svgNS, 'g');
162
+ nodeLayer.setAttribute('class', 'viz-layer-nodes');
163
+ svg.appendChild(nodeLayer);
164
+ const overlayLayer = document.createElementNS(svgNS, 'g');
165
+ overlayLayer.setAttribute('class', 'viz-layer-overlays');
166
+ svg.appendChild(overlayLayer);
167
+ container.appendChild(svg);
168
+ }
169
+ // Update ViewBox
170
+ svg.setAttribute('viewBox', `0 0 ${viewBox.w} ${viewBox.h}`);
171
+ const edgeLayer = svg.querySelector('.viz-layer-edges');
172
+ const nodeLayer = svg.querySelector('.viz-layer-nodes');
173
+ const overlayLayer = svg.querySelector('.viz-layer-overlays');
174
+ // --- 1. Reconcile Edges ---
175
+ const existingEdgeGroups = Array.from(edgeLayer.children).filter((el) => el.tagName === 'g');
176
+ const existingEdgesMap = new Map();
177
+ existingEdgeGroups.forEach((el) => {
178
+ const id = el.getAttribute('data-id');
179
+ if (id)
180
+ existingEdgesMap.set(id, el);
181
+ });
182
+ const processedEdgeIds = new Set();
183
+ edges.forEach((edge) => {
184
+ const start = nodesById.get(edge.from);
185
+ const end = nodesById.get(edge.to);
186
+ if (!start || !end)
187
+ return;
188
+ processedEdgeIds.add(edge.id);
189
+ let group = existingEdgesMap.get(edge.id);
190
+ if (!group) {
191
+ group = document.createElementNS(svgNS, 'g');
192
+ group.setAttribute('data-id', edge.id);
193
+ edgeLayer.appendChild(group);
194
+ // Initial creation of children
195
+ const line = document.createElementNS(svgNS, 'line');
196
+ line.setAttribute('class', 'viz-edge');
197
+ group.appendChild(line);
198
+ // Optional parts created on demand later, but structure expected
199
+ }
200
+ // Compute Classes & Styles
201
+ let classes = `viz-edge-group ${edge.className || ''}`;
202
+ // Reset styles
203
+ group.removeAttribute('style');
204
+ if (edge.animations) {
205
+ edge.animations.forEach((spec) => {
206
+ const renderer = defaultCoreAnimationRegistry.getEdgeRenderer(spec.id);
207
+ if (renderer) {
208
+ if (renderer.getClass)
209
+ classes += ` ${renderer.getClass({ spec, element: edge })}`;
210
+ if (renderer.getStyle) {
211
+ const s = renderer.getStyle({ spec, element: edge });
212
+ Object.entries(s).forEach(([k, v]) => {
213
+ group.style.setProperty(k, String(v));
214
+ });
215
+ }
216
+ }
217
+ });
218
+ }
219
+ group.setAttribute('class', classes);
220
+ // Update Line
221
+ const line = group.querySelector('.viz-edge');
222
+ line.setAttribute('x1', String(start.pos.x));
223
+ line.setAttribute('y1', String(start.pos.y));
224
+ line.setAttribute('x2', String(end.pos.x));
225
+ line.setAttribute('y2', String(end.pos.y));
226
+ line.setAttribute('stroke', 'currentColor');
227
+ if (edge.markerEnd === 'arrow') {
228
+ line.setAttribute('marker-end', 'url(#viz-arrow)');
229
+ }
230
+ else {
231
+ line.removeAttribute('marker-end');
232
+ }
233
+ const oldHit = group.querySelector('.viz-edge-hit');
234
+ if (oldHit)
235
+ oldHit.remove();
236
+ if (edge.hitArea || edge.onClick) {
237
+ const hit = document.createElementNS(svgNS, 'line');
238
+ hit.setAttribute('class', 'viz-edge-hit'); // Add class for selection
239
+ hit.setAttribute('x1', String(start.pos.x));
240
+ hit.setAttribute('y1', String(start.pos.y));
241
+ hit.setAttribute('x2', String(end.pos.x));
242
+ hit.setAttribute('y2', String(end.pos.y));
243
+ hit.setAttribute('stroke', 'transparent');
244
+ hit.setAttribute('stroke-width', String(edge.hitArea || 10));
245
+ hit.style.cursor = edge.onClick ? 'pointer' : '';
246
+ if (edge.onClick) {
247
+ hit.addEventListener('click', (e) => {
248
+ e.stopPropagation();
249
+ edge.onClick(edge.id, edge);
250
+ });
251
+ }
252
+ group.appendChild(hit);
253
+ }
254
+ // Label (Recreate vs Update)
255
+ const oldLabel = group.querySelector('.viz-edge-label');
256
+ if (oldLabel)
257
+ oldLabel.remove();
258
+ if (edge.label) {
259
+ const text = document.createElementNS(svgNS, 'text');
260
+ const mx = (start.pos.x + end.pos.x) / 2 + (edge.label.dx || 0);
261
+ const my = (start.pos.y + end.pos.y) / 2 + (edge.label.dy || 0);
262
+ text.setAttribute('x', String(mx));
263
+ text.setAttribute('y', String(my));
264
+ text.setAttribute('class', `viz-edge-label ${edge.label.className || ''}`);
265
+ text.setAttribute('text-anchor', 'middle');
266
+ text.setAttribute('dominant-baseline', 'middle');
267
+ text.textContent = edge.label.text;
268
+ group.appendChild(text);
269
+ }
270
+ });
271
+ // Remove stale edges
272
+ existingEdgeGroups.forEach((el) => {
273
+ const id = el.getAttribute('data-id');
274
+ if (id && !processedEdgeIds.has(id)) {
275
+ el.remove();
276
+ }
277
+ });
278
+ // --- 2. Reconcile Nodes ---
279
+ const existingNodeGroups = Array.from(nodeLayer.children).filter((el) => el.tagName === 'g');
280
+ const existingNodesMap = new Map();
281
+ existingNodeGroups.forEach((el) => {
282
+ const id = el.getAttribute('data-id');
283
+ if (id)
284
+ existingNodesMap.set(id, el);
285
+ });
286
+ const processedNodeIds = new Set();
287
+ nodes.forEach((node) => {
288
+ processedNodeIds.add(node.id);
289
+ let group = existingNodesMap.get(node.id);
290
+ if (!group) {
291
+ group = document.createElementNS(svgNS, 'g');
292
+ group.setAttribute('data-id', node.id);
293
+ nodeLayer.appendChild(group);
294
+ }
295
+ // Calculate Anim Classes
296
+ let classes = `viz-node-group ${node.className || ''}`;
297
+ group.removeAttribute('style');
298
+ if (node.animations) {
299
+ node.animations.forEach((spec) => {
300
+ const renderer = defaultCoreAnimationRegistry.getNodeRenderer(spec.id);
301
+ if (renderer) {
302
+ if (renderer.getClass)
303
+ classes += ` ${renderer.getClass({ spec, element: node })}`;
304
+ if (renderer.getStyle) {
305
+ const s = renderer.getStyle({ spec, element: node });
306
+ Object.entries(s).forEach(([k, v]) => {
307
+ group.style.setProperty(k, String(v));
308
+ });
309
+ }
310
+ }
311
+ });
312
+ }
313
+ group.setAttribute('class', classes);
314
+ // @ts-expect-error: Property _clickHandler does not exist on SVGGElement
315
+ group._clickHandler = node.onClick
316
+ ? (e) => {
317
+ e.stopPropagation();
318
+ node.onClick(node.id, node);
319
+ }
320
+ : null;
321
+ if (!group.hasAttribute('data-click-initialized')) {
322
+ group.addEventListener('click', (e) => {
323
+ // @ts-expect-error: Property _clickHandler does not exist on SVGGElement
324
+ if (group._clickHandler)
325
+ group._clickHandler(e);
326
+ });
327
+ group.setAttribute('data-click-initialized', 'true');
328
+ }
329
+ group.style.cursor = node.onClick ? 'pointer' : '';
330
+ // Shape (Update geometry)
331
+ const { x, y } = node.pos;
332
+ // Ideally we reuse the shape element if the kind hasn't changed.
333
+ // Assuming kind rarely changes for same ID.
334
+ let shape = group.querySelector('.viz-node-shape');
335
+ // If shape doesn't exist or kind changed (simplified check: just recreate if kind mismatch logic needed,
336
+ // but here we just check tag name for simplicity or assume kind is stable).
337
+ const kindMap = {
338
+ circle: 'circle',
339
+ rect: 'rect',
340
+ diamond: 'polygon',
341
+ };
342
+ const expectedTag = kindMap[node.shape.kind];
343
+ if (!shape || shape.tagName !== expectedTag) {
344
+ if (shape)
345
+ shape.remove();
346
+ if (node.shape.kind === 'circle') {
347
+ shape = document.createElementNS(svgNS, 'circle');
348
+ }
349
+ else if (node.shape.kind === 'rect') {
350
+ shape = document.createElementNS(svgNS, 'rect');
351
+ }
352
+ else if (node.shape.kind === 'diamond') {
353
+ shape = document.createElementNS(svgNS, 'polygon');
354
+ }
355
+ shape.setAttribute('class', 'viz-node-shape');
356
+ group.prepend(shape); // Shape always at bottom
357
+ }
358
+ // Update Shape Attributes
359
+ if (node.shape.kind === 'circle') {
360
+ shape.setAttribute('cx', String(x));
361
+ shape.setAttribute('cy', String(y));
362
+ shape.setAttribute('r', String(node.shape.r));
363
+ }
364
+ else if (node.shape.kind === 'rect') {
365
+ shape.setAttribute('x', String(x - node.shape.w / 2));
366
+ shape.setAttribute('y', String(y - node.shape.h / 2));
367
+ shape.setAttribute('width', String(node.shape.w));
368
+ shape.setAttribute('height', String(node.shape.h));
369
+ if (node.shape.rx)
370
+ shape.setAttribute('rx', String(node.shape.rx));
371
+ }
372
+ else if (node.shape.kind === 'diamond') {
373
+ const hw = node.shape.w / 2;
374
+ const hh = node.shape.h / 2;
375
+ const pts = `${x},${y - hh} ${x + hw},${y} ${x},${y + hh} ${x - hw},${y}`;
376
+ shape.setAttribute('points', pts);
377
+ }
378
+ // Label (Recreate for simplicity as usually just text/pos changes)
379
+ let label = group.querySelector('.viz-node-label');
380
+ if (!label && node.label) {
381
+ label = document.createElementNS(svgNS, 'text');
382
+ label.setAttribute('class', 'viz-node-label');
383
+ label.setAttribute('text-anchor', 'middle');
384
+ label.setAttribute('dominant-baseline', 'middle');
385
+ group.appendChild(label);
386
+ }
387
+ if (node.label) {
388
+ const lx = x + (node.label.dx || 0);
389
+ const ly = y + (node.label.dy || 0);
390
+ label.setAttribute('x', String(lx));
391
+ label.setAttribute('y', String(ly));
392
+ // Update class carefully to preserve 'viz-node-label'
393
+ label.setAttribute('class', `viz-node-label ${node.label.className || ''}`);
394
+ label.textContent = node.label.text;
395
+ }
396
+ else if (label) {
397
+ label.remove();
398
+ }
399
+ });
400
+ // Remove stale nodes
401
+ existingNodeGroups.forEach((el) => {
402
+ const id = el.getAttribute('data-id');
403
+ if (id && !processedNodeIds.has(id)) {
404
+ el.remove();
405
+ }
406
+ });
407
+ // --- 3. Reconcile Overlays (Smart) ---
408
+ // 1. Map existing overlay groups
409
+ const existingOverlayGroups = Array.from(overlayLayer.children).filter((el) => el.tagName === 'g');
410
+ const existingOverlaysMap = new Map();
411
+ existingOverlayGroups.forEach((el) => {
412
+ const id = el.getAttribute('data-overlay-id');
413
+ if (id)
414
+ existingOverlaysMap.set(id, el);
415
+ });
416
+ const processedOverlayIds = new Set();
417
+ if (overlays && overlays.length > 0) {
418
+ overlays.forEach((spec) => {
419
+ const renderer = defaultCoreOverlayRegistry.get(spec.id);
420
+ if (renderer) {
421
+ const uniqueKey = spec.key || spec.id;
422
+ processedOverlayIds.add(uniqueKey);
423
+ let group = existingOverlaysMap.get(uniqueKey);
424
+ if (!group) {
425
+ group = document.createElementNS(svgNS, 'g');
426
+ group.setAttribute('data-overlay-id', uniqueKey);
427
+ group.setAttribute('class', `viz-overlay-${spec.id}`);
428
+ overlayLayer.appendChild(group);
429
+ }
430
+ const ctx = {
431
+ spec,
432
+ nodesById,
433
+ edgesById: new Map(edges.map((e) => [e.id, e])),
434
+ scene,
435
+ };
436
+ if (renderer.update) {
437
+ renderer.update(ctx, group);
438
+ }
439
+ else {
440
+ // Fallback: full re-render of this overlay's content
441
+ group.innerHTML = renderer.render(ctx);
442
+ }
443
+ }
444
+ });
445
+ }
446
+ // Remove stale overlays
447
+ existingOverlayGroups.forEach((el) => {
448
+ const id = el.getAttribute('data-overlay-id');
449
+ if (id && !processedOverlayIds.has(id)) {
450
+ el.remove();
451
+ }
452
+ });
453
+ }
454
+ /**
455
+ * Returns the SVG string representation of the scene.
456
+ * @deprecated The use of this method is deprecated. Use `mount` instead.
457
+ * @param scene The scene to render
458
+ * @returns The SVG string representation of the scene
459
+ */
460
+ _renderSceneToSvg(scene) {
461
+ const { viewBox, nodes, edges, overlays } = scene;
462
+ const nodesById = new Map(nodes.map((n) => [n.id, n]));
463
+ const edgesById = new Map(edges.map((e) => [e.id, e]));
464
+ let svgContent = `<svg viewBox="0 0 ${viewBox.w} ${viewBox.h}" xmlns="http://www.w3.org/2000/svg">`;
465
+ // Inject Styles
466
+ svgContent += `<style>${DEFAULT_VIZ_CSS}</style>`;
467
+ // Defs (Arrow Marker)
468
+ svgContent += `
469
+ <defs>
470
+ <marker id="viz-arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
471
+ <polygon points="0 0, 10 3.5, 0 7" fill="currentColor" />
472
+ </marker>
473
+ </defs>`;
474
+ // Render Edges
475
+ svgContent += '<g class="viz-layer-edges">';
476
+ edges.forEach((edge) => {
477
+ const start = nodesById.get(edge.from);
478
+ const end = nodesById.get(edge.to);
479
+ if (!start || !end)
480
+ return;
481
+ // Animations
482
+ let animClasses = '';
483
+ let animStyleStr = '';
484
+ if (edge.animations) {
485
+ edge.animations.forEach((spec) => {
486
+ const renderer = defaultCoreAnimationRegistry.getEdgeRenderer(spec.id);
487
+ if (renderer) {
488
+ if (renderer.getClass) {
489
+ animClasses += ` ${renderer.getClass({ spec, element: edge })}`;
490
+ }
491
+ if (renderer.getStyle) {
492
+ const styles = renderer.getStyle({ spec, element: edge });
493
+ Object.entries(styles).forEach(([k, v]) => {
494
+ animStyleStr += `${k}: ${v}; `;
495
+ });
496
+ }
497
+ }
498
+ });
499
+ }
500
+ const markerEnd = edge.markerEnd === 'arrow' ? 'marker-end="url(#viz-arrow)"' : '';
501
+ svgContent += `<g class="viz-edge-group ${edge.className || ''} ${animClasses}" style="${animStyleStr}">`;
502
+ svgContent += `<line x1="${start.pos.x}" y1="${start.pos.y}" x2="${end.pos.x}" y2="${end.pos.y}" class="viz-edge" ${markerEnd} stroke="currentColor" />`;
503
+ // Edge Label
504
+ if (edge.label) {
505
+ const mx = (start.pos.x + end.pos.x) / 2 + (edge.label.dx || 0);
506
+ const my = (start.pos.y + end.pos.y) / 2 + (edge.label.dy || 0);
507
+ const labelClass = `viz-edge-label ${edge.label.className || ''}`;
508
+ svgContent += `<text x="${mx}" y="${my}" class="${labelClass}" text-anchor="middle" dominant-baseline="middle">${edge.label.text}</text>`;
509
+ }
510
+ svgContent += '</g>';
511
+ });
512
+ svgContent += '</g>';
513
+ // Render Nodes
514
+ svgContent += '<g class="viz-layer-nodes">';
515
+ nodes.forEach((node) => {
516
+ const { x, y } = node.pos;
517
+ const { shape } = node;
518
+ // Animations (Nodes)
519
+ let animClasses = '';
520
+ let animStyleStr = '';
521
+ if (node.animations) {
522
+ node.animations.forEach((spec) => {
523
+ const renderer = defaultCoreAnimationRegistry.getNodeRenderer(spec.id);
524
+ if (renderer) {
525
+ if (renderer.getClass) {
526
+ animClasses += ` ${renderer.getClass({ spec, element: node })}`;
527
+ }
528
+ if (renderer.getStyle) {
529
+ const styles = renderer.getStyle({ spec, element: node });
530
+ Object.entries(styles).forEach(([k, v]) => {
531
+ animStyleStr += `${k}: ${v}; `;
532
+ });
533
+ }
534
+ }
535
+ });
536
+ }
537
+ const className = `viz-node-group ${node.className || ''} ${animClasses}`;
538
+ svgContent += `<g class="${className}" style="${animStyleStr}">`;
539
+ // Shape
540
+ if (shape.kind === 'circle') {
541
+ svgContent += `<circle cx="${x}" cy="${y}" r="${shape.r}" class="viz-node-shape" />`;
542
+ }
543
+ else if (shape.kind === 'rect') {
544
+ svgContent += `<rect x="${x - shape.w / 2}" y="${y - shape.h / 2}" width="${shape.w}" height="${shape.h}" rx="${shape.rx || 0}" class="viz-node-shape" />`;
545
+ }
546
+ else if (shape.kind === 'diamond') {
547
+ const hw = shape.w / 2;
548
+ const hh = shape.h / 2;
549
+ const pts = `${x},${y - hh} ${x + hw},${y} ${x},${y + hh} ${x - hw},${y}`;
550
+ svgContent += `<polygon points="${pts}" class="viz-node-shape" />`;
551
+ }
552
+ // Label
553
+ if (node.label) {
554
+ const lx = x + (node.label.dx || 0);
555
+ const ly = y + (node.label.dy || 0);
556
+ const labelClass = `viz-node-label ${node.label.className || ''}`;
557
+ svgContent += `<text x="${lx}" y="${ly}" class="${labelClass}" text-anchor="middle" dominant-baseline="middle">${node.label.text}</text>`;
558
+ }
559
+ svgContent += '</g>';
560
+ });
561
+ svgContent += '</g>';
562
+ // Render Overlays
563
+ if (overlays && overlays.length > 0) {
564
+ svgContent += '<g class="viz-layer-overlays">';
565
+ overlays.forEach((spec) => {
566
+ const renderer = defaultCoreOverlayRegistry.get(spec.id);
567
+ if (renderer) {
568
+ svgContent += renderer.render({ spec, nodesById, edgesById, scene });
569
+ }
570
+ });
571
+ svgContent += '</g>';
572
+ }
573
+ svgContent += '</svg>';
574
+ return svgContent;
575
+ }
576
+ }
577
+ class NodeBuilderImpl {
578
+ parent;
579
+ nodeDef;
580
+ constructor(parent, nodeDef) {
581
+ this.parent = parent;
582
+ this.nodeDef = nodeDef;
583
+ }
584
+ at(x, y) {
585
+ this.nodeDef.pos = { x, y };
586
+ return this;
587
+ }
588
+ cell(col, row, align = 'center') {
589
+ const grid = this.parent._getGridConfig();
590
+ if (!grid) {
591
+ console.warn('VizBuilder: .cell() called but no grid configured. Use .grid() first.');
592
+ return this;
593
+ }
594
+ const view = this.parent._getViewBox();
595
+ const availableW = view.w - grid.padding.x * 2;
596
+ const availableH = view.h - grid.padding.y * 2;
597
+ const cellW = availableW / grid.cols;
598
+ const cellH = availableH / grid.rows;
599
+ let x = grid.padding.x + col * cellW;
600
+ let y = grid.padding.y + row * cellH;
601
+ // Alignment adjustments
602
+ if (align === 'center') {
603
+ x += cellW / 2;
604
+ y += cellH / 2;
605
+ }
606
+ else if (align === 'end') {
607
+ x += cellW;
608
+ y += cellH;
609
+ }
610
+ this.nodeDef.pos = { x, y };
611
+ return this;
612
+ }
613
+ circle(r) {
614
+ this.nodeDef.shape = { kind: 'circle', r };
615
+ return this;
616
+ }
617
+ rect(w, h, rx) {
618
+ this.nodeDef.shape = { kind: 'rect', w, h, rx };
619
+ return this;
620
+ }
621
+ diamond(w, h) {
622
+ this.nodeDef.shape = { kind: 'diamond', w, h };
623
+ return this;
624
+ }
625
+ label(text, opts) {
626
+ this.nodeDef.label = { text, ...opts };
627
+ return this;
628
+ }
629
+ class(name) {
630
+ if (this.nodeDef.className) {
631
+ this.nodeDef.className += ` ${name}`;
632
+ }
633
+ else {
634
+ this.nodeDef.className = name;
635
+ }
636
+ return this;
637
+ }
638
+ animate(type, config) {
639
+ if (!this.nodeDef.animations) {
640
+ this.nodeDef.animations = [];
641
+ }
642
+ this.nodeDef.animations.push({ id: type, params: config });
643
+ return this;
644
+ }
645
+ data(payload) {
646
+ this.nodeDef.data = payload;
647
+ return this;
648
+ }
649
+ onClick(handler) {
650
+ this.nodeDef.onClick = handler;
651
+ return this;
652
+ }
653
+ done() {
654
+ return this.parent;
655
+ }
656
+ // Chaining
657
+ node(id) {
658
+ return this.parent.node(id);
659
+ }
660
+ edge(from, to, id) {
661
+ return this.parent.edge(from, to, id);
662
+ }
663
+ overlay(id, params, key) {
664
+ return this.parent.overlay(id, params, key);
665
+ }
666
+ build() {
667
+ return this.parent.build();
668
+ }
669
+ svg() {
670
+ return this.parent.svg();
671
+ }
672
+ }
673
+ class EdgeBuilderImpl {
674
+ parent;
675
+ edgeDef;
676
+ constructor(parent, edgeDef) {
677
+ this.parent = parent;
678
+ this.edgeDef = edgeDef;
679
+ }
680
+ straight() {
681
+ // No-op for now as it is default
682
+ return this;
683
+ }
684
+ label(text, opts) {
685
+ this.edgeDef.label = { position: 'mid', text, ...opts };
686
+ return this;
687
+ }
688
+ arrow(enabled = true) {
689
+ this.edgeDef.markerEnd = enabled ? 'arrow' : 'none';
690
+ return this;
691
+ }
692
+ class(name) {
693
+ if (this.edgeDef.className) {
694
+ this.edgeDef.className += ` ${name}`;
695
+ }
696
+ else {
697
+ this.edgeDef.className = name;
698
+ }
699
+ return this;
700
+ }
701
+ animate(type, config) {
702
+ if (!this.edgeDef.animations) {
703
+ this.edgeDef.animations = [];
704
+ }
705
+ this.edgeDef.animations.push({ id: type, params: config });
706
+ return this;
707
+ }
708
+ hitArea(px) {
709
+ this.edgeDef.hitArea = px;
710
+ return this;
711
+ }
712
+ data(payload) {
713
+ this.edgeDef.data = payload;
714
+ return this;
715
+ }
716
+ onClick(handler) {
717
+ this.edgeDef.onClick = handler;
718
+ return this;
719
+ }
720
+ done() {
721
+ return this.parent;
722
+ }
723
+ // Chaining
724
+ node(id) {
725
+ return this.parent.node(id);
726
+ }
727
+ edge(from, to, id) {
728
+ return this.parent.edge(from, to, id);
729
+ }
730
+ overlay(id, params, key) {
731
+ return this.parent.overlay(id, params, key);
732
+ }
733
+ build() {
734
+ return this.parent.build();
735
+ }
736
+ svg() {
737
+ return this.parent.svg();
738
+ }
739
+ }
740
+ export function viz() {
741
+ return new VizBuilderImpl();
742
+ }