vizcraft 0.2.1 → 0.3.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # vizcraft
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`c5ffe75`](https://github.com/ChipiKaf/vizcraft/commit/c5ffe7546a2e2148618db057c24aea01ecf097e0) Thanks [@ChipiKaf](https://github.com/ChipiKaf)! - Add improved overlay api
8
+
9
+ ## 0.2.2
10
+
11
+ ### Patch Changes
12
+
13
+ - [`7c9eb18`](https://github.com/ChipiKaf/vizcraft/commit/7c9eb185e727bde899b4779c4661d8b176db8549) Thanks [@ChipiKaf](https://github.com/ChipiKaf)! - update documentation
14
+
3
15
  ## 0.2.1
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![Snapshot](https://github.com/ChipiKaf/vizcraft/actions/workflows/snapshot.yml/badge.svg)](https://github.com/ChipiKaf/vizcraft/actions/workflows/snapshot.yml)
8
8
  [![license](https://img.shields.io/npm/l/vizcraft.svg)](LICENSE)
9
9
 
10
- 📖 Full documentation: https://vizcraft-docs.vercel.app/
10
+ 📖 Full documentation: [docs here](https://vizcraft-docs.vercel.app/docs/intro)
11
11
 
12
12
  **A declarative, builder-based library for creating animated SVG network visualizations and algorithm demos.**
13
13
 
@@ -41,7 +41,6 @@ import { viz } from 'vizcraft';
41
41
  const builder = viz().view(800, 600);
42
42
 
43
43
  builder
44
- .view(500, 500)
45
44
  .node('a')
46
45
  .at(100, 100)
47
46
  .circle(15)
@@ -57,21 +56,21 @@ const container = document.getElementById('viz-basic');
57
56
  if (container) builder.mount(container);
58
57
  ```
59
58
 
60
- More walkthroughs and examples: https://vizcraft-docs.vercel.app/
59
+ More walkthroughs and examples: [docs here](https://vizcraft-docs.vercel.app/docs/examples).
61
60
 
62
61
  ## 📚 Documentation (Topics)
63
62
 
64
- Full documentation site: https://vizcraft-docs.vercel.app/
63
+ Full documentation site: [docs here](https://vizcraft-docs.vercel.app/docs/intro)
65
64
 
66
- For a guided walkthrough, the repo docs are organized like this:
65
+ Docs topics (same as the sidebar):
67
66
 
68
- - [Introduction](../../packages/docs/docs/intro.md)
69
- - [Examples](../../packages/docs/docs/examples.mdx)
70
- - [Essentials](../../packages/docs/docs/essentials.mdx)
71
- - [Animations](../../packages/docs/docs/animations/index.mdx)
72
- - [Animation Builder API](../../packages/docs/docs/animations/animation-builder-api.mdx)
73
- - [Advanced](../../packages/docs/docs/advanced.mdx)
74
- - [Types](../../packages/docs/docs/types.mdx)
67
+ - [Introduction](https://vizcraft-docs.vercel.app/docs/intro)
68
+ - [Examples](https://vizcraft-docs.vercel.app/docs/examples)
69
+ - [Essentials](https://vizcraft-docs.vercel.app/docs/essentials)
70
+ - [Animations](https://vizcraft-docs.vercel.app/docs/animations)
71
+ - [Animation Builder API](https://vizcraft-docs.vercel.app/docs/animations/animation-builder-api)
72
+ - [Advanced](https://vizcraft-docs.vercel.app/docs/advanced)
73
+ - [Types](https://vizcraft-docs.vercel.app/docs/types)
75
74
 
76
75
  Run the docs locally (monorepo):
77
76
 
@@ -122,7 +121,7 @@ b.edge('n1', 'n2')
122
121
 
123
122
  ### Animations
124
123
 
125
- See the full Animations guide: https://vizcraft-docs.vercel.app/
124
+ See the full Animations guide [docs here](https://vizcraft-docs.vercel.app/docs/animations).
126
125
 
127
126
  VizCraft supports **two complementary animation approaches**:
128
127
 
@@ -153,7 +152,7 @@ b.node('a')
153
152
 
154
153
  2. **Data-only timeline animations (`AnimationSpec`)** (sequenced tweens)
155
154
 
156
- - Author with `builder.animate((anim) => ...)`.
155
+ - Author with `builder.animate((aBuilder) => ...)`.
157
156
  - VizCraft stores compiled specs on the scene as `scene.animationSpecs`.
158
157
  - Play them with `builder.play()`.
159
158
 
@@ -174,8 +173,8 @@ b.node('a')
174
173
  .arrow()
175
174
  .done();
176
175
 
177
- b.animate((anim) =>
178
- anim
176
+ b.animate((aBuilder) =>
177
+ aBuilder
179
178
  .node('a')
180
179
  .to({ x: 200, opacity: 0.35 }, { duration: 600 })
181
180
  .node('b')
@@ -206,8 +205,10 @@ b.node('a')
206
205
  .edge('a', 'b', 'e1')
207
206
  .done();
208
207
 
209
- b.animate((anim) =>
210
- anim.edge('a', 'b', 'e1').to({ strokeDashoffset: -120 }, { duration: 900 })
208
+ b.animate((aBuilder) =>
209
+ aBuilder
210
+ .edge('a', 'b', 'e1')
211
+ .to({ strokeDashoffset: -120 }, { duration: 900 })
211
212
  );
212
213
  ```
213
214
 
@@ -216,8 +217,8 @@ b.animate((anim) =>
216
217
  Specs can carry adapter extensions so you can animate your own numeric properties:
217
218
 
218
219
  ```ts
219
- b.animate((anim) =>
220
- anim
220
+ b.animate((aBuilder) =>
221
+ aBuilder
221
222
  .extendAdapter((adapter) => {
222
223
  adapter.register?.('node', 'r', {
223
224
  get: (target) => adapter.get(target, 'r'),
@@ -38,6 +38,8 @@ export declare class AnimationBuilder {
38
38
  extendAdapter(cb: ExtendAdapter): this;
39
39
  /** Select a node by id (compiles to target `node:<id>`). */
40
40
  node(id: string): this;
41
+ /** Select an overlay by key (compiles to target `overlay:<key>`). */
42
+ overlay(key: string): this;
41
43
  /**
42
44
  * Select an edge.
43
45
  *
@@ -30,6 +30,11 @@ export class AnimationBuilder {
30
30
  this.currentTarget = toTarget('node', id);
31
31
  return this;
32
32
  }
33
+ /** Select an overlay by key (compiles to target `overlay:<key>`). */
34
+ overlay(key) {
35
+ this.currentTarget = toTarget('overlay', key);
36
+ return this;
37
+ }
33
38
  edge(a, b, c) {
34
39
  const id = b === undefined ? a : (c ?? `${a}->${b}`);
35
40
  this.currentTarget = toTarget('edge', id);
@@ -56,7 +61,7 @@ export class AnimationBuilder {
56
61
  */
57
62
  to(props, opts) {
58
63
  if (!this.currentTarget) {
59
- throw new Error('AnimationBuilder.to(): no target selected (call node(...) or edge(...))');
64
+ throw new Error('AnimationBuilder.to(): no target selected (call node(...), edge(...), or overlay(...))');
60
65
  }
61
66
  const duration = Math.max(0, opts.duration);
62
67
  const easing = opts.easing;
@@ -1,4 +1,4 @@
1
- export type AnimationTarget = (`node:${string}` | `edge:${string}`) | (string & {});
1
+ export type AnimationTarget = (`node:${string}` | `edge:${string}` | `overlay:${string}`) | (string & {});
2
2
  export type CoreAnimProperty = 'x' | 'y' | 'opacity' | 'scale' | 'rotation' | 'strokeDashoffset';
3
3
  export type AnimProperty = CoreAnimProperty | (string & {});
4
4
  export type Ease = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut';
@@ -1,13 +1,21 @@
1
+ import { OVERLAY_RUNTIME_DIRTY } from '../types';
1
2
  import { createRegistryAdapter } from './registryAdapter';
2
3
  export function createVizCraftAdapter(scene, requestRender) {
3
4
  const nodesById = new Map(scene.nodes.map((n) => [n.id, n]));
4
5
  const edgesById = new Map(scene.edges.map((e) => [e.id, e]));
6
+ const overlays = scene.overlays ?? [];
7
+ const overlaysByKey = new Map();
8
+ for (const spec of overlays) {
9
+ const key = spec.key ?? spec.id;
10
+ overlaysByKey.set(key, spec);
11
+ }
5
12
  const adapter = createRegistryAdapter({
6
13
  flush: requestRender,
7
14
  });
8
15
  // register node/edge target resolvers and props using ergonomic handles
9
16
  const node = adapter.kind('node', (id) => nodesById.get(id));
10
17
  const edge = adapter.kind('edge', (id) => edgesById.get(id));
18
+ const overlay = adapter.kind('overlay', (key) => overlaysByKey.get(key));
11
19
  node
12
20
  .prop('x', {
13
21
  get: (el) => {
@@ -75,5 +83,64 @@ export function createVizCraftAdapter(scene, requestRender) {
75
83
  e.runtime.strokeDashoffset = v;
76
84
  },
77
85
  });
78
- return adapter;
86
+ // Overlay params: allow animating arbitrary numeric fields on `spec.params`.
87
+ //
88
+ // This intentionally uses a generic reader/writer so users can animate
89
+ // custom overlays without needing adapter extensions.
90
+ const overlayParamReader = (el, prop) => {
91
+ const spec = el;
92
+ const params = spec.params;
93
+ const v = params?.[prop];
94
+ return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
95
+ };
96
+ const overlayParamWriter = (el, prop, value) => {
97
+ const spec = el;
98
+ const existing = spec.params;
99
+ const params = existing && typeof existing === 'object' && !Array.isArray(existing)
100
+ ? existing
101
+ : {};
102
+ params[prop] = value;
103
+ // Ensure we keep reference stable in case params was undefined or non-object.
104
+ spec.params = params;
105
+ // Mark dirty so patchRuntime can avoid re-rendering unaffected overlays.
106
+ spec[OVERLAY_RUNTIME_DIRTY] = true;
107
+ };
108
+ const resolveOverlayFromTarget = (target) => {
109
+ const t = String(target);
110
+ if (!t.startsWith('overlay:'))
111
+ return undefined;
112
+ const key = t.slice('overlay:'.length);
113
+ return overlaysByKey.get(key);
114
+ };
115
+ // Register core overlay props we know are numeric today.
116
+ // Users can still register more via adapter extensions if they prefer explicitness.
117
+ overlay.prop('progress', {
118
+ get: (el) => overlayParamReader(el, 'progress'),
119
+ set: (el, v) => overlayParamWriter(el, 'progress', v),
120
+ });
121
+ // Make overlays fully generic: any numeric `spec.params[prop]` is animatable.
122
+ //
123
+ // `createRegistryAdapter` requires per-prop registration; for overlays we provide
124
+ // a fallback so custom overlays don't need adapter extensions.
125
+ const baseGet = adapter.get;
126
+ const baseSet = adapter.set;
127
+ return {
128
+ ...adapter,
129
+ get(target, prop) {
130
+ const v = baseGet(target, prop);
131
+ if (v !== undefined)
132
+ return v;
133
+ const spec = resolveOverlayFromTarget(target);
134
+ if (!spec)
135
+ return undefined;
136
+ return overlayParamReader(spec, String(prop));
137
+ },
138
+ set(target, prop, value) {
139
+ baseSet(target, prop, value);
140
+ const spec = resolveOverlayFromTarget(target);
141
+ if (!spec)
142
+ return;
143
+ overlayParamWriter(spec, String(prop), value);
144
+ },
145
+ };
79
146
  }
package/dist/builder.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { VizScene, VizNode, VizEdge, NodeLabel, EdgeLabel, AnimationConfig, VizGridConfig } from './types';
1
+ import type { VizScene, VizNode, VizEdge, NodeLabel, EdgeLabel, AnimationConfig, OverlayId, OverlayParams, VizGridConfig } from './types';
2
+ import { OverlayBuilder } from './overlayBuilder';
2
3
  import type { AnimationSpec } from './anim/spec';
3
4
  import { type AnimationBuilder, type AnimatableProps, type TweenOptions } from './anim/animationBuilder';
4
5
  import { type PlaybackController } from './anim/playback';
@@ -13,6 +14,9 @@ interface VizBuilder {
13
14
  * The compiled spec is also stored on the built scene as `scene.animationSpecs`.
14
15
  */
15
16
  animate(cb: (anim: AnimationBuilder) => unknown): AnimationSpec;
17
+ /** Fluent overlay authoring (compiles to overlay specs and stores on the built scene). */
18
+ overlay(cb: (overlay: OverlayBuilder) => unknown): VizBuilder;
19
+ overlay<K extends OverlayId>(id: K, params: OverlayParams<K>, key?: string): VizBuilder;
16
20
  overlay<T>(id: string, params: T, key?: string): VizBuilder;
17
21
  node(id: string): NodeBuilder;
18
22
  edge(from: string, to: string, id?: string): EdgeBuilder;
@@ -67,6 +71,8 @@ interface NodeBuilder {
67
71
  done(): VizBuilder;
68
72
  node(id: string): NodeBuilder;
69
73
  edge(from: string, to: string, id?: string): EdgeBuilder;
74
+ overlay(cb: (overlay: OverlayBuilder) => unknown): VizBuilder;
75
+ overlay<K extends OverlayId>(id: K, params: OverlayParams<K>, key?: string): VizBuilder;
70
76
  overlay<T>(id: string, params: T, key?: string): VizBuilder;
71
77
  build(): VizScene;
72
78
  svg(): string;
@@ -87,6 +93,8 @@ interface EdgeBuilder {
87
93
  done(): VizBuilder;
88
94
  node(id: string): NodeBuilder;
89
95
  edge(from: string, to: string, id?: string): EdgeBuilder;
96
+ overlay(cb: (overlay: OverlayBuilder) => unknown): VizBuilder;
97
+ overlay<K extends OverlayId>(id: K, params: OverlayParams<K>, key?: string): VizBuilder;
90
98
  overlay<T>(id: string, params: T, key?: string): VizBuilder;
91
99
  build(): VizScene;
92
100
  svg(): string;
package/dist/builder.js CHANGED
@@ -1,6 +1,8 @@
1
+ import { OVERLAY_RUNTIME_DIRTY } from './types';
1
2
  import { DEFAULT_VIZ_CSS } from './styles';
2
3
  import { defaultCoreAnimationRegistry } from './animations';
3
4
  import { defaultCoreOverlayRegistry } from './overlays';
5
+ import { OverlayBuilder } from './overlayBuilder';
4
6
  import { createRuntimePatchCtx, patchRuntime, } from './runtimePatcher';
5
7
  import { buildAnimationSpec, } from './anim/animationBuilder';
6
8
  import { createBuilderPlayback, } from './anim/playback';
@@ -75,14 +77,16 @@ class VizBuilderImpl {
75
77
  this._gridConfig = { cols, rows, padding };
76
78
  return this;
77
79
  }
78
- /**
79
- * Adds an overlay to the scene.
80
- * @param id The ID of the overlay
81
- * @param params The parameters of the overlay
82
- * @param key The key of the overlay
83
- * @returns The builder
84
- */
85
- overlay(id, params, key) {
80
+ overlay(arg1, arg2, arg3) {
81
+ if (typeof arg1 === 'function') {
82
+ const overlay = new OverlayBuilder();
83
+ arg1(overlay);
84
+ this._overlays.push(...overlay.build());
85
+ return this;
86
+ }
87
+ const id = arg1;
88
+ const params = arg2;
89
+ const key = arg3;
86
90
  this._overlays.push({ id, params, key });
87
91
  return this;
88
92
  }
@@ -274,6 +278,92 @@ class VizBuilderImpl {
274
278
  runtimePatchCtxBySvg.set(svg, ctx);
275
279
  }
276
280
  patchRuntime(scene, ctx);
281
+ // Keep overlays in sync during animation playback.
282
+ //
283
+ // Animations flush via `patchRuntime()` (to avoid full re-mounts). Nodes/edges
284
+ // are patched via `runtimePatcher`, but overlays are registry-rendered and
285
+ // need an explicit reconcile pass to reflect animated `spec.params` changes.
286
+ const overlayLayer = svg.querySelector('[data-viz-layer="overlays"]') ||
287
+ svg.querySelector('.viz-layer-overlays');
288
+ if (overlayLayer) {
289
+ const overlays = scene.overlays ?? [];
290
+ const nodesById = new Map(scene.nodes.map((n) => [n.id, n]));
291
+ const edgesById = new Map(scene.edges.map((e) => [e.id, e]));
292
+ const svgNS = 'http://www.w3.org/2000/svg';
293
+ // 1) Map existing overlay groups
294
+ const existingOverlayGroups = Array.from(overlayLayer.children).filter((el) => el.tagName === 'g');
295
+ const existingOverlaysMap = new Map();
296
+ existingOverlayGroups.forEach((el) => {
297
+ const id = el.getAttribute('data-overlay-id');
298
+ if (id)
299
+ existingOverlaysMap.set(id, el);
300
+ });
301
+ // Fast decision: if nothing is dirty and keys match, skip overlay work.
302
+ const overlayKeyCountMatches = overlays.length === existingOverlaysMap.size;
303
+ let needsOverlayPass = !overlayKeyCountMatches;
304
+ const dirtyOverlays = [];
305
+ if (!needsOverlayPass) {
306
+ for (const spec of overlays) {
307
+ const uniqueKey = spec.key || spec.id;
308
+ if (!existingOverlaysMap.has(uniqueKey)) {
309
+ needsOverlayPass = true;
310
+ break;
311
+ }
312
+ if (spec[OVERLAY_RUNTIME_DIRTY]) {
313
+ dirtyOverlays.push(spec);
314
+ }
315
+ }
316
+ }
317
+ if (!needsOverlayPass && dirtyOverlays.length === 0)
318
+ return;
319
+ const processedOverlayIds = new Set();
320
+ // 2) Render/update overlays
321
+ const toUpdate = needsOverlayPass ? overlays : dirtyOverlays;
322
+ toUpdate.forEach((spec) => {
323
+ const renderer = defaultCoreOverlayRegistry.get(spec.id);
324
+ if (!renderer)
325
+ return;
326
+ const uniqueKey = spec.key || spec.id;
327
+ processedOverlayIds.add(uniqueKey);
328
+ let group = existingOverlaysMap.get(uniqueKey);
329
+ if (!group) {
330
+ group = document.createElementNS(svgNS, 'g');
331
+ group.setAttribute('data-overlay-id', uniqueKey);
332
+ group.setAttribute('data-viz-role', 'overlay-group');
333
+ overlayLayer.appendChild(group);
334
+ }
335
+ // Keep wrapper class in sync even when reusing an existing group.
336
+ const expectedClass = `viz-overlay-${spec.id}${spec.className ? ` ${spec.className}` : ''}`;
337
+ const currentClass = group.getAttribute('class');
338
+ if (currentClass !== expectedClass) {
339
+ group.setAttribute('class', expectedClass);
340
+ }
341
+ const overlayCtx = {
342
+ spec,
343
+ nodesById,
344
+ edgesById,
345
+ scene,
346
+ registry: defaultCoreOverlayRegistry,
347
+ };
348
+ if (renderer.update) {
349
+ renderer.update(overlayCtx, group);
350
+ }
351
+ else {
352
+ group.innerHTML = renderer.render(overlayCtx);
353
+ }
354
+ // Clear dirty flag after successful update.
355
+ delete spec[OVERLAY_RUNTIME_DIRTY];
356
+ });
357
+ // 3) Remove stale overlays only if keys may have changed.
358
+ if (!overlayKeyCountMatches) {
359
+ existingOverlayGroups.forEach((el) => {
360
+ const id = el.getAttribute('data-overlay-id');
361
+ if (id && !processedOverlayIds.has(id)) {
362
+ el.remove();
363
+ }
364
+ });
365
+ }
366
+ }
277
367
  }
278
368
  /**
279
369
  * Renders the scene to the DOM.
@@ -620,15 +710,17 @@ class VizBuilderImpl {
620
710
  if (!group) {
621
711
  group = document.createElementNS(svgNS, 'g');
622
712
  group.setAttribute('data-overlay-id', uniqueKey);
623
- group.setAttribute('class', `viz-overlay-${spec.id}`);
624
713
  group.setAttribute('data-viz-role', 'overlay-group');
625
714
  overlayLayer.appendChild(group);
626
715
  }
716
+ // Keep wrapper class in sync even when reusing an existing group.
717
+ group.setAttribute('class', `viz-overlay-${spec.id}${spec.className ? ` ${spec.className}` : ''}`);
627
718
  const ctx = {
628
719
  spec,
629
720
  nodesById,
630
721
  edgesById: new Map(edges.map((e) => [e.id, e])),
631
722
  scene,
723
+ registry: defaultCoreOverlayRegistry,
632
724
  };
633
725
  if (renderer.update) {
634
726
  renderer.update(ctx, group);
@@ -798,7 +890,13 @@ class VizBuilderImpl {
798
890
  overlays.forEach((spec) => {
799
891
  const renderer = defaultCoreOverlayRegistry.get(spec.id);
800
892
  if (renderer) {
801
- svgContent += renderer.render({ spec, nodesById, edgesById, scene });
893
+ svgContent += renderer.render({
894
+ spec,
895
+ nodesById,
896
+ edgesById,
897
+ scene,
898
+ registry: defaultCoreOverlayRegistry,
899
+ });
802
900
  }
803
901
  });
804
902
  svgContent += '</g>';
@@ -932,8 +1030,10 @@ class NodeBuilderImpl {
932
1030
  edge(from, to, id) {
933
1031
  return this.parent.edge(from, to, id);
934
1032
  }
935
- overlay(id, params, key) {
936
- return this.parent.overlay(id, params, key);
1033
+ overlay(arg1, arg2, arg3) {
1034
+ if (typeof arg1 === 'function')
1035
+ return this.parent.overlay(arg1);
1036
+ return this.parent.overlay(arg1, arg2, arg3);
937
1037
  }
938
1038
  build() {
939
1039
  return this.parent.build();
@@ -1026,8 +1126,10 @@ class EdgeBuilderImpl {
1026
1126
  edge(from, to, id) {
1027
1127
  return this.parent.edge(from, to, id || `${from}->${to}`); // Default ID to from->to
1028
1128
  }
1029
- overlay(id, params, key) {
1030
- return this.parent.overlay(id, params, key);
1129
+ overlay(arg1, arg2, arg3) {
1130
+ if (typeof arg1 === 'function')
1131
+ return this.parent.overlay(arg1);
1132
+ return this.parent.overlay(arg1, arg2, arg3);
1031
1133
  }
1032
1134
  build() {
1033
1135
  return this.parent.build();
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export * from './builder';
3
3
  export * from './styles';
4
4
  export * from './animations';
5
5
  export * from './overlays';
6
+ export * from './overlayBuilder';
6
7
  export * from './anim/spec';
7
8
  export * from './anim/animationBuilder';
8
9
  export * from './anim/playback';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export * from './builder';
3
3
  export * from './styles';
4
4
  export * from './animations';
5
5
  export * from './overlays';
6
+ export * from './overlayBuilder';
6
7
  export * from './anim/spec';
7
8
  export * from './anim/animationBuilder';
8
9
  export * from './anim/playback';
@@ -0,0 +1,50 @@
1
+ import type { OverlayId, OverlayParams, VizOverlaySpec } from './types';
2
+ import type { CircleOverlayParams, GroupOverlayParams, RectOverlayParams, TextOverlayParams } from './overlays';
3
+ export type OverlayAddOptions = {
4
+ /** Optional stable key used to uniquely identify an overlay instance. */
5
+ key?: string;
6
+ /** Optional class applied by the overlay renderer. */
7
+ className?: string;
8
+ };
9
+ /**
10
+ * Fluent overlay authoring.
11
+ *
12
+ * - Produces portable `VizOverlaySpec[]` data (rendering remains registry-driven).
13
+ * - Typed by `OverlayKindRegistry` when available, with a back-compat escape hatch.
14
+ */
15
+ export declare class OverlayBuilder {
16
+ private readonly specs;
17
+ private readonly keyCounters;
18
+ /**
19
+ * Add an overlay spec.
20
+ *
21
+ * Overload 1: typed overlay ids via `OverlayKindRegistry`.
22
+ */
23
+ add<K extends OverlayId>(id: K, params: OverlayParams<K>, options?: OverlayAddOptions): this;
24
+ /**
25
+ * Add an overlay spec.
26
+ *
27
+ * Overload 2: back-compat escape hatch for arbitrary ids.
28
+ */
29
+ add(id: string, params: any, options?: OverlayAddOptions): this;
30
+ /** Remove overlays by key, or (if unkeyed) by id. */
31
+ remove(keyOrId: string): this;
32
+ /** Remove all overlays. */
33
+ clear(): this;
34
+ build(): VizOverlaySpec[];
35
+ /** Add a generic rectangle overlay (built-in, no custom registry needed). */
36
+ rect(params: RectOverlayParams, options?: OverlayAddOptions): this;
37
+ /** Add a generic circle overlay (built-in, no custom registry needed). */
38
+ circle(params: CircleOverlayParams, options?: OverlayAddOptions): this;
39
+ /** Add a generic text overlay (built-in, no custom registry needed). */
40
+ text(params: TextOverlayParams, options?: OverlayAddOptions): this;
41
+ /**
42
+ * Add a composite group overlay (built-in).
43
+ *
44
+ * Children are authored via the callback and rendered inside a single SVG <g>.
45
+ * You can animate the group by targeting its key and tweening `x`, `y`, `scale`, `rotation`, `opacity`.
46
+ */
47
+ group(params: Omit<GroupOverlayParams, 'children'>, buildChildren: (overlay: OverlayBuilder) => unknown, options?: OverlayAddOptions): this;
48
+ }
49
+ /** Convenience helper for one-off overlay list compilation. */
50
+ export declare function buildOverlaySpecs(cb: (overlay: OverlayBuilder) => unknown): VizOverlaySpec[];
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Fluent overlay authoring.
3
+ *
4
+ * - Produces portable `VizOverlaySpec[]` data (rendering remains registry-driven).
5
+ * - Typed by `OverlayKindRegistry` when available, with a back-compat escape hatch.
6
+ */
7
+ export class OverlayBuilder {
8
+ specs = [];
9
+ keyCounters = new Map();
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ add(id, params, options) {
12
+ const className = options?.className;
13
+ // If a user adds multiple overlays of the same id without keys, they will
14
+ // collide at reconcile time (since the DOM layer uses `key || id`).
15
+ // We auto-generate a stable-ish key in that case.
16
+ let key = options?.key;
17
+ if (!key) {
18
+ const hasUnkeyedSameId = this.specs.some((s) => s.id === id && (s.key === undefined || s.key === ''));
19
+ if (hasUnkeyedSameId) {
20
+ const next = (this.keyCounters.get(id) ?? 0) + 1;
21
+ this.keyCounters.set(id, next);
22
+ key = `${id}#${next}`;
23
+ }
24
+ }
25
+ this.specs.push({ id, params, key, className });
26
+ return this;
27
+ }
28
+ /** Remove overlays by key, or (if unkeyed) by id. */
29
+ remove(keyOrId) {
30
+ for (let i = this.specs.length - 1; i >= 0; i--) {
31
+ const s = this.specs[i];
32
+ if (!s)
33
+ continue;
34
+ const matchesKey = s.key === keyOrId;
35
+ const matchesUnkeyedId = !s.key && s.id === keyOrId;
36
+ if (matchesKey || matchesUnkeyedId)
37
+ this.specs.splice(i, 1);
38
+ }
39
+ return this;
40
+ }
41
+ /** Remove all overlays. */
42
+ clear() {
43
+ this.specs.length = 0;
44
+ this.keyCounters.clear();
45
+ return this;
46
+ }
47
+ build() {
48
+ return [...this.specs];
49
+ }
50
+ /** Add a generic rectangle overlay (built-in, no custom registry needed). */
51
+ rect(params, options) {
52
+ return this.add('rect', params, options);
53
+ }
54
+ /** Add a generic circle overlay (built-in, no custom registry needed). */
55
+ circle(params, options) {
56
+ return this.add('circle', params, options);
57
+ }
58
+ /** Add a generic text overlay (built-in, no custom registry needed). */
59
+ text(params, options) {
60
+ return this.add('text', params, options);
61
+ }
62
+ /**
63
+ * Add a composite group overlay (built-in).
64
+ *
65
+ * Children are authored via the callback and rendered inside a single SVG <g>.
66
+ * You can animate the group by targeting its key and tweening `x`, `y`, `scale`, `rotation`, `opacity`.
67
+ */
68
+ group(params, buildChildren, options) {
69
+ const childOverlay = new OverlayBuilder();
70
+ buildChildren(childOverlay);
71
+ const children = childOverlay.build();
72
+ return this.add('group', { ...params, children }, options);
73
+ }
74
+ }
75
+ /** Convenience helper for one-off overlay list compilation. */
76
+ export function buildOverlaySpecs(cb) {
77
+ const overlay = new OverlayBuilder();
78
+ cb(overlay);
79
+ return overlay.build();
80
+ }
@@ -1,9 +1,115 @@
1
1
  import type { VizNode, VizEdge, VizOverlaySpec, VizScene } from './types';
2
+ export type SignalOverlayParams = {
3
+ from: string;
4
+ to: string;
5
+ progress: number;
6
+ magnitude?: number;
7
+ };
8
+ export type GridLabelsOverlayParams = {
9
+ colLabels?: Record<number, string>;
10
+ rowLabels?: Record<number, string>;
11
+ yOffset?: number;
12
+ xOffset?: number;
13
+ };
14
+ export interface DataPoint {
15
+ id: string;
16
+ currentNodeId: string;
17
+ [key: string]: any;
18
+ }
19
+ export type DataPointsOverlayParams = {
20
+ points: DataPoint[];
21
+ };
22
+ export type RectOverlayParams = {
23
+ x: number;
24
+ y: number;
25
+ w: number;
26
+ h: number;
27
+ rx?: number;
28
+ ry?: number;
29
+ opacity?: number;
30
+ /** SVG fill (defaults to a visible blue). Can be overridden by CSS via className. */
31
+ fill?: string;
32
+ /** SVG stroke (defaults to a visible blue). Can be overridden by CSS via className. */
33
+ stroke?: string;
34
+ /** SVG stroke-width (defaults to 3). Can be overridden by CSS via className. */
35
+ strokeWidth?: number;
36
+ };
37
+ export type CircleOverlayParams = {
38
+ x: number;
39
+ y: number;
40
+ r: number;
41
+ opacity?: number;
42
+ /** SVG fill (defaults to a visible blue). Can be overridden by CSS via className. */
43
+ fill?: string;
44
+ /** SVG stroke (defaults to a visible blue). Can be overridden by CSS via className. */
45
+ stroke?: string;
46
+ /** SVG stroke-width (defaults to 3). Can be overridden by CSS via className. */
47
+ strokeWidth?: number;
48
+ };
49
+ export type TextOverlayParams = {
50
+ x: number;
51
+ y: number;
52
+ text: string;
53
+ opacity?: number;
54
+ /** SVG fill color (defaults to #111). Can be overridden by CSS via className. */
55
+ fill?: string;
56
+ fontSize?: number;
57
+ fontWeight?: string | number;
58
+ textAnchor?: 'start' | 'middle' | 'end';
59
+ dominantBaseline?: string;
60
+ };
61
+ export type GroupOverlayParams = {
62
+ /**
63
+ * Translate (group-local origin).
64
+ *
65
+ * If `from`/`to` are provided, these act as an additional offset.
66
+ */
67
+ x?: number;
68
+ y?: number;
69
+ /**
70
+ * Optional node ids used to drive the group's position via `progress`.
71
+ *
72
+ * When set, the group will translate along the line from `from` to `to`.
73
+ */
74
+ from?: string;
75
+ to?: string;
76
+ /** Interpolation 0..1 used when `from`/`to` are set. */
77
+ progress?: number;
78
+ /**
79
+ * Optional "pulse" value 0..1.
80
+ *
81
+ * When provided, it scales the group slightly (in addition to `scale`).
82
+ */
83
+ magnitude?: number;
84
+ /** Scale around group origin. */
85
+ scale?: number;
86
+ /** Rotate (degrees) around group origin. */
87
+ rotation?: number;
88
+ /** Group opacity (multiplies with child opacity). */
89
+ opacity?: number;
90
+ /** Child overlays rendered inside this group. Coordinates are group-local. */
91
+ children: VizOverlaySpec[];
92
+ };
93
+ declare module './types' {
94
+ interface OverlayKindRegistry {
95
+ signal: SignalOverlayParams;
96
+ 'grid-labels': GridLabelsOverlayParams;
97
+ 'data-points': DataPointsOverlayParams;
98
+ /** Generic overlay primitives (no custom registry needed). */
99
+ rect: RectOverlayParams;
100
+ circle: CircleOverlayParams;
101
+ text: TextOverlayParams;
102
+ /** Overlay container that can hold child overlays and be animated as a unit. */
103
+ group: GroupOverlayParams;
104
+ }
105
+ }
2
106
  export interface CoreOverlayRenderContext<T = any> {
3
107
  spec: VizOverlaySpec<T>;
4
108
  nodesById: Map<string, VizNode>;
5
109
  edgesById: Map<string, VizEdge>;
6
110
  scene: VizScene;
111
+ /** Registry reference (useful for composite overlays like `group`). */
112
+ registry?: CoreOverlayRegistry;
7
113
  }
8
114
  export interface CoreOverlayRenderer<T = any> {
9
115
  render: (ctx: CoreOverlayRenderContext<T>) => string;
@@ -14,25 +120,11 @@ export declare class CoreOverlayRegistry {
14
120
  register(id: string, renderer: CoreOverlayRenderer): this;
15
121
  get(id: string): CoreOverlayRenderer<any> | undefined;
16
122
  }
17
- export declare const coreSignalOverlay: CoreOverlayRenderer<{
18
- from: string;
19
- to: string;
20
- progress: number;
21
- magnitude?: number;
22
- }>;
23
- export declare const coreGridLabelsOverlay: CoreOverlayRenderer<{
24
- colLabels?: Record<number, string>;
25
- rowLabels?: Record<number, string>;
26
- yOffset?: number;
27
- xOffset?: number;
28
- }>;
29
- interface DataPoint {
30
- id: string;
31
- currentNodeId: string;
32
- [key: string]: any;
33
- }
34
- export declare const coreDataPointOverlay: CoreOverlayRenderer<{
35
- points: DataPoint[];
36
- }>;
123
+ export declare const coreSignalOverlay: CoreOverlayRenderer<SignalOverlayParams>;
124
+ export declare const coreGridLabelsOverlay: CoreOverlayRenderer<GridLabelsOverlayParams>;
125
+ export declare const coreDataPointOverlay: CoreOverlayRenderer<DataPointsOverlayParams>;
126
+ export declare const coreRectOverlay: CoreOverlayRenderer<RectOverlayParams>;
127
+ export declare const coreCircleOverlay: CoreOverlayRenderer<CircleOverlayParams>;
128
+ export declare const coreTextOverlay: CoreOverlayRenderer<TextOverlayParams>;
129
+ export declare const coreGroupOverlay: CoreOverlayRenderer<GroupOverlayParams>;
37
130
  export declare const defaultCoreOverlayRegistry: CoreOverlayRegistry;
38
- export {};
package/dist/overlays.js CHANGED
@@ -64,6 +64,7 @@ export const coreGridLabelsOverlay = {
64
64
  return output;
65
65
  },
66
66
  };
67
+ // ... (OverlayRegistry and other exports remain unchanged) ...
67
68
  // Built-in Overlay: Data Points
68
69
  export const coreDataPointOverlay = {
69
70
  render: ({ spec, nodesById }) => {
@@ -133,7 +134,324 @@ export const coreDataPointOverlay = {
133
134
  });
134
135
  },
135
136
  };
137
+ // Generic Overlay: Rect
138
+ export const coreRectOverlay = {
139
+ render: ({ spec }) => {
140
+ const { x, y, w, h, rx, ry, opacity, fill, stroke, strokeWidth } = spec.params;
141
+ const cls = spec.className ?? 'viz-overlay-rect';
142
+ const rxAttr = rx !== undefined ? ` rx="${rx}"` : '';
143
+ const ryAttr = ry !== undefined ? ` ry="${ry}"` : '';
144
+ const opAttr = opacity !== undefined ? ` opacity="${opacity}"` : '';
145
+ const usingDefaultFill = fill === undefined;
146
+ const usingDefaultStroke = stroke === undefined;
147
+ const resolvedFill = fill ?? '#3b82f6';
148
+ const resolvedStroke = stroke ?? '#3b82f6';
149
+ const resolvedStrokeWidth = strokeWidth ?? 3;
150
+ const fillOpacityAttr = usingDefaultFill ? ' fill-opacity="0.12"' : '';
151
+ const strokeOpacityAttr = usingDefaultStroke ? ' stroke-opacity="0.9"' : '';
152
+ return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="${resolvedFill}"${fillOpacityAttr} stroke="${resolvedStroke}"${strokeOpacityAttr} stroke-width="${resolvedStrokeWidth}"${rxAttr}${ryAttr}${opAttr} class="${cls}" />`;
153
+ },
154
+ update: ({ spec }, container) => {
155
+ const svgNS = 'http://www.w3.org/2000/svg';
156
+ const { x, y, w, h, rx, ry, opacity, fill, stroke, strokeWidth } = spec.params;
157
+ const cls = spec.className ?? 'viz-overlay-rect';
158
+ let rect = container.querySelector('rect');
159
+ if (!rect) {
160
+ rect = document.createElementNS(svgNS, 'rect');
161
+ container.appendChild(rect);
162
+ }
163
+ rect.setAttribute('x', String(x));
164
+ rect.setAttribute('y', String(y));
165
+ rect.setAttribute('width', String(w));
166
+ rect.setAttribute('height', String(h));
167
+ if (fill === undefined) {
168
+ rect.setAttribute('fill', '#3b82f6');
169
+ rect.setAttribute('fill-opacity', '0.12');
170
+ }
171
+ else {
172
+ rect.setAttribute('fill', fill);
173
+ rect.removeAttribute('fill-opacity');
174
+ }
175
+ if (stroke === undefined) {
176
+ rect.setAttribute('stroke', '#3b82f6');
177
+ rect.setAttribute('stroke-opacity', '0.9');
178
+ }
179
+ else {
180
+ rect.setAttribute('stroke', stroke);
181
+ rect.removeAttribute('stroke-opacity');
182
+ }
183
+ rect.setAttribute('stroke-width', String(strokeWidth ?? 3));
184
+ if (rx !== undefined)
185
+ rect.setAttribute('rx', String(rx));
186
+ else
187
+ rect.removeAttribute('rx');
188
+ if (ry !== undefined)
189
+ rect.setAttribute('ry', String(ry));
190
+ else
191
+ rect.removeAttribute('ry');
192
+ if (opacity !== undefined)
193
+ rect.setAttribute('opacity', String(opacity));
194
+ else
195
+ rect.removeAttribute('opacity');
196
+ rect.setAttribute('class', cls);
197
+ },
198
+ };
199
+ // Generic Overlay: Circle
200
+ export const coreCircleOverlay = {
201
+ render: ({ spec }) => {
202
+ const { x, y, r, opacity, fill, stroke, strokeWidth } = spec.params;
203
+ const cls = spec.className ?? 'viz-overlay-circle';
204
+ const opAttr = opacity !== undefined ? ` opacity="${opacity}"` : '';
205
+ const usingDefaultFill = fill === undefined;
206
+ const usingDefaultStroke = stroke === undefined;
207
+ const resolvedFill = fill ?? '#3b82f6';
208
+ const resolvedStroke = stroke ?? '#3b82f6';
209
+ const resolvedStrokeWidth = strokeWidth ?? 3;
210
+ const fillOpacityAttr = usingDefaultFill ? ' fill-opacity="0.12"' : '';
211
+ const strokeOpacityAttr = usingDefaultStroke ? ' stroke-opacity="0.9"' : '';
212
+ return `<circle cx="${x}" cy="${y}" r="${r}" fill="${resolvedFill}"${fillOpacityAttr} stroke="${resolvedStroke}"${strokeOpacityAttr} stroke-width="${resolvedStrokeWidth}"${opAttr} class="${cls}" />`;
213
+ },
214
+ update: ({ spec }, container) => {
215
+ const svgNS = 'http://www.w3.org/2000/svg';
216
+ const { x, y, r, opacity, fill, stroke, strokeWidth } = spec.params;
217
+ const cls = spec.className ?? 'viz-overlay-circle';
218
+ let circle = container.querySelector('circle');
219
+ if (!circle) {
220
+ circle = document.createElementNS(svgNS, 'circle');
221
+ container.appendChild(circle);
222
+ }
223
+ circle.setAttribute('cx', String(x));
224
+ circle.setAttribute('cy', String(y));
225
+ circle.setAttribute('r', String(r));
226
+ if (fill === undefined) {
227
+ circle.setAttribute('fill', '#3b82f6');
228
+ circle.setAttribute('fill-opacity', '0.12');
229
+ }
230
+ else {
231
+ circle.setAttribute('fill', fill);
232
+ circle.removeAttribute('fill-opacity');
233
+ }
234
+ if (stroke === undefined) {
235
+ circle.setAttribute('stroke', '#3b82f6');
236
+ circle.setAttribute('stroke-opacity', '0.9');
237
+ }
238
+ else {
239
+ circle.setAttribute('stroke', stroke);
240
+ circle.removeAttribute('stroke-opacity');
241
+ }
242
+ circle.setAttribute('stroke-width', String(strokeWidth ?? 3));
243
+ if (opacity !== undefined)
244
+ circle.setAttribute('opacity', String(opacity));
245
+ else
246
+ circle.removeAttribute('opacity');
247
+ circle.setAttribute('class', cls);
248
+ },
249
+ };
250
+ // Generic Overlay: Text
251
+ export const coreTextOverlay = {
252
+ render: ({ spec }) => {
253
+ const { x, y, text, opacity, fill, fontSize, fontWeight, textAnchor, dominantBaseline, } = spec.params;
254
+ const cls = spec.className ?? 'viz-overlay-text';
255
+ const opAttr = opacity !== undefined ? ` opacity="${opacity}"` : '';
256
+ const fsAttr = fontSize !== undefined ? ` font-size="${fontSize}"` : '';
257
+ const fwAttr = fontWeight !== undefined ? ` font-weight="${fontWeight}"` : '';
258
+ const taAttr = textAnchor !== undefined ? ` text-anchor="${textAnchor}"` : '';
259
+ const dbAttr = dominantBaseline !== undefined
260
+ ? ` dominant-baseline="${dominantBaseline}"`
261
+ : '';
262
+ // Basic text rendering; users should avoid untrusted HTML here.
263
+ const resolvedFill = fill ?? '#111';
264
+ return `<text x="${x}" y="${y}" fill="${resolvedFill}"${opAttr}${fsAttr}${fwAttr}${taAttr}${dbAttr} class="${cls}">${text}</text>`;
265
+ },
266
+ update: ({ spec }, container) => {
267
+ const svgNS = 'http://www.w3.org/2000/svg';
268
+ const { x, y, text, opacity, fill, fontSize, fontWeight, textAnchor, dominantBaseline, } = spec.params;
269
+ const cls = spec.className ?? 'viz-overlay-text';
270
+ let el = container.querySelector('text');
271
+ if (!el) {
272
+ el = document.createElementNS(svgNS, 'text');
273
+ container.appendChild(el);
274
+ }
275
+ el.setAttribute('x', String(x));
276
+ el.setAttribute('y', String(y));
277
+ el.setAttribute('fill', fill ?? '#111');
278
+ if (opacity !== undefined)
279
+ el.setAttribute('opacity', String(opacity));
280
+ else
281
+ el.removeAttribute('opacity');
282
+ if (fontSize !== undefined)
283
+ el.setAttribute('font-size', String(fontSize));
284
+ else
285
+ el.removeAttribute('font-size');
286
+ if (fontWeight !== undefined)
287
+ el.setAttribute('font-weight', String(fontWeight));
288
+ else
289
+ el.removeAttribute('font-weight');
290
+ if (textAnchor !== undefined)
291
+ el.setAttribute('text-anchor', textAnchor);
292
+ else
293
+ el.removeAttribute('text-anchor');
294
+ if (dominantBaseline !== undefined)
295
+ el.setAttribute('dominant-baseline', dominantBaseline);
296
+ else
297
+ el.removeAttribute('dominant-baseline');
298
+ el.setAttribute('class', cls);
299
+ el.textContent = text;
300
+ },
301
+ };
302
+ function groupTransform(params) {
303
+ const tx = params.x ?? 0;
304
+ const ty = params.y ?? 0;
305
+ const s = params.scale ?? 1;
306
+ const r = params.rotation ?? 0;
307
+ // translate first so scale/rotation occur around the group origin.
308
+ const parts = [`translate(${tx}, ${ty})`];
309
+ if (r)
310
+ parts.push(`rotate(${r})`);
311
+ if (s !== 1)
312
+ parts.push(`scale(${s})`);
313
+ return parts.join(' ');
314
+ }
315
+ function clamp01(v) {
316
+ if (v < 0)
317
+ return 0;
318
+ if (v > 1)
319
+ return 1;
320
+ return v;
321
+ }
322
+ function effectiveNodePos(node) {
323
+ return {
324
+ x: node.runtime?.x ?? node.pos.x,
325
+ y: node.runtime?.y ?? node.pos.y,
326
+ };
327
+ }
328
+ function resolveGroupTransformInputs(params, nodesById) {
329
+ const baseX = params.x ?? 0;
330
+ const baseY = params.y ?? 0;
331
+ let x = baseX;
332
+ let y = baseY;
333
+ if (params.from && params.to) {
334
+ const start = nodesById.get(params.from);
335
+ const end = nodesById.get(params.to);
336
+ if (start && end) {
337
+ const p = clamp01(params.progress ?? 0);
338
+ const a = effectiveNodePos(start);
339
+ const b = effectiveNodePos(end);
340
+ x = a.x + (b.x - a.x) * p + baseX;
341
+ y = a.y + (b.y - a.y) * p + baseY;
342
+ }
343
+ }
344
+ const userScale = params.scale ?? 1;
345
+ const m = params.magnitude;
346
+ const magScale = m === undefined ? 1 : 0.85 + 0.3 * clamp01(Math.abs(m));
347
+ const scale = userScale * magScale;
348
+ return {
349
+ x,
350
+ y,
351
+ scale,
352
+ rotation: params.rotation ?? 0,
353
+ };
354
+ }
355
+ // Composite Overlay: Group
356
+ export const coreGroupOverlay = {
357
+ render: ({ spec, nodesById, edgesById, scene, registry }) => {
358
+ const { children, opacity } = spec.params;
359
+ const inputs = resolveGroupTransformInputs(spec.params, nodesById);
360
+ const tr = groupTransform(inputs);
361
+ const opAttr = opacity !== undefined ? ` opacity="${opacity}"` : '';
362
+ const reg = registry;
363
+ if (!reg) {
364
+ // Best-effort render even if registry is missing.
365
+ return `<g transform="${tr}"${opAttr}></g>`;
366
+ }
367
+ let output = `<g transform="${tr}"${opAttr}>`;
368
+ children.forEach((childSpec, idx) => {
369
+ const renderer = reg.get(childSpec.id);
370
+ if (!renderer)
371
+ return;
372
+ const childCtx = {
373
+ spec: childSpec,
374
+ nodesById,
375
+ edgesById,
376
+ scene,
377
+ registry: reg,
378
+ };
379
+ // Wrap children in their own <g> so update() has stable containers.
380
+ const key = childSpec.key
381
+ ? `key:${childSpec.key}`
382
+ : `idx:${idx}:${childSpec.id}`;
383
+ output += `<g data-viz-role="overlay-child" data-overlay-child-id="${key}">`;
384
+ output += renderer.render(childCtx);
385
+ output += '</g>';
386
+ });
387
+ output += '</g>';
388
+ return output;
389
+ },
390
+ update: ({ spec, nodesById, edgesById, scene, registry }, container) => {
391
+ const reg = registry;
392
+ if (!reg)
393
+ return;
394
+ const { children, opacity } = spec.params;
395
+ const inputs = resolveGroupTransformInputs(spec.params, nodesById);
396
+ container.setAttribute('transform', groupTransform(inputs));
397
+ if (opacity !== undefined) {
398
+ container.setAttribute('opacity', String(opacity));
399
+ }
400
+ else {
401
+ container.removeAttribute('opacity');
402
+ }
403
+ const svgNS = 'http://www.w3.org/2000/svg';
404
+ const existing = new Map();
405
+ Array.from(container.children).forEach((child) => {
406
+ if (child instanceof SVGGElement) {
407
+ const id = child.getAttribute('data-overlay-child-id');
408
+ if (id)
409
+ existing.set(id, child);
410
+ }
411
+ });
412
+ const keep = new Set();
413
+ children.forEach((childSpec, idx) => {
414
+ const renderer = reg.get(childSpec.id);
415
+ if (!renderer)
416
+ return;
417
+ const key = childSpec.key
418
+ ? `key:${childSpec.key}`
419
+ : `idx:${idx}:${childSpec.id}`;
420
+ keep.add(key);
421
+ let childGroup = existing.get(key);
422
+ if (!childGroup) {
423
+ childGroup = document.createElementNS(svgNS, 'g');
424
+ childGroup.setAttribute('data-viz-role', 'overlay-child');
425
+ childGroup.setAttribute('data-overlay-child-id', key);
426
+ container.appendChild(childGroup);
427
+ }
428
+ const childCtx = {
429
+ spec: childSpec,
430
+ nodesById,
431
+ edgesById,
432
+ scene,
433
+ registry: reg,
434
+ };
435
+ if (renderer.update) {
436
+ renderer.update(childCtx, childGroup);
437
+ }
438
+ else {
439
+ childGroup.innerHTML = renderer.render(childCtx);
440
+ }
441
+ });
442
+ existing.forEach((el, id) => {
443
+ if (!keep.has(id))
444
+ el.remove();
445
+ });
446
+ },
447
+ };
136
448
  export const defaultCoreOverlayRegistry = new CoreOverlayRegistry()
137
449
  .register('signal', coreSignalOverlay)
138
450
  .register('grid-labels', coreGridLabelsOverlay)
139
- .register('data-points', coreDataPointOverlay);
451
+ .register('data-points', coreDataPointOverlay)
452
+ // Generic primitives
453
+ .register('rect', coreRectOverlay)
454
+ .register('circle', coreCircleOverlay)
455
+ .register('text', coreTextOverlay)
456
+ // Composite overlays
457
+ .register('group', coreGroupOverlay);
package/dist/styles.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const DEFAULT_VIZ_CSS = "\n.viz-canvas {\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n\n.viz-node-label,\n.viz-edge-label {\n dominant-baseline: middle;\n alignment-baseline: middle;\n transform: translateY(-0.1em);\n}\n\n.viz-canvas svg {\n width: 100%;\n height: 100%;\n overflow: visible;\n}\n\n/* Keyframes */\n@keyframes vizFlow {\n from {\n stroke-dashoffset: 20;\n }\n to {\n stroke-dashoffset: 0;\n }\n}\n\n/* Animation Classes */\n\n/* Flow Animation (Dashed line moving) */\n.viz-anim-flow .viz-edge {\n stroke-dasharray: 5, 5;\n animation: vizFlow var(--viz-anim-duration, 2s) linear infinite;\n}\n\n/* Node Transition */\n.viz-node-group {\n transition: transform 0.3s ease-out, opacity 0.3s ease-out;\n}\n\n/* Overlay Classes */\n.viz-grid-label {\n fill: #6B7280;\n font-size: 14px;\n font-weight: 600;\n opacity: 1;\n}\n\n.viz-signal {\n fill: #3B82F6;\n cursor: pointer;\n pointer-events: all; \n transition: transform 0.2s ease-out, fill 0.2s ease-out;\n}\n\n.viz-signal .viz-signal-shape {\n fill: inherit;\n}\n\n.viz-signal:hover {\n fill: #60A5FA;\n transform: scale(1.5);\n}\n\n.viz-data-point {\n fill: #F59E0B;\n transition: cx 0.3s ease-out, cy 0.3s ease-out;\n}\n";
1
+ export declare const DEFAULT_VIZ_CSS = "\n.viz-canvas {\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n\n.viz-node-label,\n.viz-edge-label {\n text-anchor: middle;\n dominant-baseline: middle;\n alignment-baseline: middle;\n transform: translateY(0);\n}\n\n.viz-canvas svg {\n width: 100%;\n height: 100%;\n overflow: visible;\n}\n\n/* Keyframes */\n@keyframes vizFlow {\n from {\n stroke-dashoffset: 20;\n }\n to {\n stroke-dashoffset: 0;\n }\n}\n\n/* Animation Classes */\n\n/* Flow Animation (Dashed line moving) */\n.viz-anim-flow .viz-edge {\n stroke-dasharray: 5, 5;\n animation: vizFlow var(--viz-anim-duration, 2s) linear infinite;\n}\n\n/* Node Transition */\n.viz-node-group {\n transition: transform 0.3s ease-out, opacity 0.3s ease-out;\n}\n\n/* Overlay Classes */\n.viz-grid-label {\n fill: #6B7280;\n font-size: 14px;\n font-weight: 600;\n opacity: 1;\n}\n\n.viz-signal {\n fill: #3B82F6;\n cursor: pointer;\n pointer-events: all; \n transition: transform 0.2s ease-out, fill 0.2s ease-out;\n}\n\n.viz-signal .viz-signal-shape {\n fill: inherit;\n}\n\n.viz-signal:hover {\n fill: #60A5FA;\n transform: scale(1.5);\n}\n\n.viz-data-point {\n fill: #F59E0B;\n transition: cx 0.3s ease-out, cy 0.3s ease-out;\n}\n";
package/dist/styles.js CHANGED
@@ -9,9 +9,10 @@ export const DEFAULT_VIZ_CSS = `
9
9
 
10
10
  .viz-node-label,
11
11
  .viz-edge-label {
12
+ text-anchor: middle;
12
13
  dominant-baseline: middle;
13
14
  alignment-baseline: middle;
14
- transform: translateY(-0.1em);
15
+ transform: translateY(0);
15
16
  }
16
17
 
17
18
  .viz-canvas svg {
package/dist/types.d.ts CHANGED
@@ -86,6 +86,34 @@ export interface VizEdge {
86
86
  onClick?: (id: string, edge: VizEdge) => void;
87
87
  animations?: VizAnimSpec[];
88
88
  }
89
+ /**
90
+ * Overlay kind -> params mapping.
91
+ *
92
+ * This interface is intentionally empty in core and is meant to be augmented by:
93
+ * - core overlays (in this repo)
94
+ * - downstream libraries/apps (via TS module augmentation)
95
+ */
96
+ export interface OverlayKindRegistry {
97
+ }
98
+ /** Internal runtime flag used to mark overlays as needing a DOM update. */
99
+ export declare const OVERLAY_RUNTIME_DIRTY: unique symbol;
100
+ /** String overlay ids that are known/typed via `OverlayKindRegistry`. */
101
+ export type KnownOverlayId = Extract<keyof OverlayKindRegistry, string>;
102
+ /** Any overlay id (typed known ids + arbitrary custom ids). */
103
+ export type OverlayId = KnownOverlayId | (string & {});
104
+ /**
105
+ * Params type for a given overlay id.
106
+ * - Known ids resolve to their registered params type.
107
+ * - Unknown/custom ids fall back to `unknown` (escape hatch).
108
+ */
109
+ export type OverlayParams<K extends string> = K extends KnownOverlayId ? OverlayKindRegistry[K] : unknown;
110
+ /** A type-safe overlay spec keyed by overlay id. */
111
+ export type TypedVizOverlaySpec<K extends OverlayId = OverlayId> = {
112
+ id: K;
113
+ key?: string;
114
+ params: OverlayParams<K>;
115
+ className?: string;
116
+ };
89
117
  export type VizOverlaySpec<T = any> = {
90
118
  id: string;
91
119
  key?: string;
package/dist/types.js CHANGED
@@ -1 +1,2 @@
1
- export {};
1
+ /** Internal runtime flag used to mark overlays as needing a DOM update. */
2
+ export const OVERLAY_RUNTIME_DIRTY = Symbol('vizcraft.overlay.runtimeDirty');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vizcraft",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "A fluent, type-safe SVG scene builder for composing nodes, edges, animations, and overlays with incremental DOM updates and no framework dependency.",
5
5
  "keywords": [
6
6
  "visualization",