@cnvx/nodal 0.3.3 → 0.5.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.
@@ -3,16 +3,16 @@
3
3
 
4
4
  export interface NodalEdgeProps {
5
5
  edge: SurfaceEdge;
6
- path: string;
6
+ d: string;
7
7
  }
8
8
  </script>
9
9
 
10
10
  <script lang="ts">
11
- const { edge, path }: NodalEdgeProps = $props();
11
+ const { edge, d }: NodalEdgeProps = $props();
12
12
  </script>
13
13
 
14
14
  <path
15
- d={path}
15
+ {d}
16
16
  fill="none"
17
17
  stroke-width="2"
18
18
  stroke="currentColor"
@@ -1,7 +1,7 @@
1
1
  import type { SurfaceEdge } from "./Surface.svelte";
2
2
  export interface NodalEdgeProps {
3
3
  edge: SurfaceEdge;
4
- path: string;
4
+ d: string;
5
5
  }
6
6
  declare const BaseEdge: import("svelte").Component<NodalEdgeProps, {}, "">;
7
7
  type BaseEdge = ReturnType<typeof BaseEdge>;
@@ -12,6 +12,12 @@
12
12
  } from "./diagram-lib.js";
13
13
  import type { HTMLAttributes } from "svelte/elements";
14
14
  import type { Writable } from "svelte/store";
15
+ import {
16
+ getNodeAnchorFast,
17
+ getNodeAnchorWithBoundingClientRect,
18
+ } from "./layout-utils.js";
19
+ import { fade } from "svelte/transition";
20
+ import type { ComponentProps } from "svelte";
15
21
 
16
22
  type PathGenParams =
17
23
  | {
@@ -25,32 +31,52 @@
25
31
  center?: Vector2;
26
32
  };
27
33
 
28
- export type SurfaceEdgeParams = {
29
- component?: typeof BaseEdge;
30
- sourceAnchor?: Vector2;
31
- targetAnchor?: Vector2;
32
- svgPathAttributes?: HTMLAttributes<SVGPathElement>;
33
- } & PathGenParams;
34
+ export type SurfaceEdgeParams<C extends typeof BaseEdge = typeof BaseEdge> =
35
+ {
36
+ component?: C | [C, Omit<ComponentProps<C>, "d" | "edge">];
37
+ sourceAnchor?: Vector2;
38
+ targetAnchor?: Vector2;
39
+ svgPathAttributes?: HTMLAttributes<SVGPathElement>;
40
+ } & PathGenParams;
41
+
42
+ // export type SurfaceEdge = {
43
+
44
+ // } & SurfaceEdgeParams;
45
+
46
+ export interface PrecalculatedEdge {
47
+ x1: number;
48
+ y1: number;
49
+ x2: number;
50
+ y2: number;
51
+ }
34
52
 
35
53
  export type SurfaceEdge = {
36
54
  source: string | HTMLElement | string[] | HTMLElement[];
37
55
  target: string | HTMLElement | string[] | HTMLElement[];
56
+
57
+ precalculated?: PrecalculatedEdge;
38
58
  } & SurfaceEdgeParams;
39
59
  </script>
40
60
 
41
61
  <script lang="ts">
62
+ let svg: SVGSVGElement | undefined = $state();
63
+
42
64
  const {
43
65
  hostElement,
44
66
  edges: _edges,
45
67
  svgAttributes,
46
68
  width: _width,
47
69
  height: _height,
70
+ fadeInDuration = 80,
71
+ getNodeAnchor = getNodeAnchorWithBoundingClientRect,
48
72
  }: {
49
73
  hostElement: Element;
50
74
  edges: SvelteMap<string, SurfaceEdge>;
51
75
  width?: Writable<number>;
52
76
  height?: Writable<number>;
53
77
  svgAttributes?: HTMLAttributes<SVGElement>;
78
+ getNodeAnchor?: typeof getNodeAnchorFast;
79
+ fadeInDuration?: number;
54
80
  } = $props();
55
81
 
56
82
  export const width = _width;
@@ -158,6 +184,18 @@
158
184
  }
159
185
 
160
186
  function generateEdgePath(edge: SurfaceEdge) {
187
+ if (!svg) return [];
188
+
189
+ if (edge.precalculated) {
190
+ return generateCurvePath(
191
+ edge.precalculated.x1,
192
+ edge.precalculated.y1,
193
+ edge.precalculated.x2,
194
+ edge.precalculated.y2,
195
+ edge,
196
+ );
197
+ }
198
+
161
199
  const sourceNodes = getNodes(edge.source);
162
200
  const targetNodes = getNodes(edge.target);
163
201
 
@@ -175,10 +213,12 @@
175
213
  const sourceAnchor = getNodeAnchor(
176
214
  source,
177
215
  edge.sourceAnchor ?? Anchor.CENTER_CENTER,
216
+ svg!,
178
217
  );
179
218
  const targetAnchor = getNodeAnchor(
180
219
  target,
181
220
  edge.targetAnchor ?? Anchor.CENTER_CENTER,
221
+ svg!,
182
222
  );
183
223
  paths.push(
184
224
  generateCurvePath(
@@ -203,29 +243,28 @@
203
243
 
204
244
  return paths;
205
245
  }
206
-
207
- function getNodeAnchor(node: HTMLElement, anchor: Vector2) {
208
- return {
209
- left: node.offsetLeft + anchor.x * node.clientWidth,
210
- top: node.offsetTop + anchor.y * node.clientHeight,
211
- };
212
- }
213
246
  </script>
214
247
 
248
+ <!-- style="position:absolute;top:0;right:0;bottom:0;left:0;height:100%;width:100%;overflow:visible;z-index:0;" -->
215
249
  <svg
250
+ bind:this={svg}
216
251
  preserveAspectRatio="none"
217
- style="position:absolute;top:0;right:0;bottom:0;left:0;height:100%;width:100%;overflow:visible;z-index:0;"
252
+ style="position: absolute; inset: 0px; height: 1px; width: 1px; overflow: visible; z-index: 0;"
218
253
  {...svgAttributes}
219
254
  >
220
- {#each edges.values() as edge}
221
- {#each generateEdgePath(edge) as path}
222
- {@const EdgeComponent = edge.component ?? BaseEdge}
223
- <EdgeComponent {path} {edge} />
224
- <!-- {@render (edge.snippet ? edge.snippet : defaultEdge)(
225
- edge,
226
- path,
227
- edge.snippetExtraArg,
228
- )} -->
229
- {/each}
230
- {/each}
255
+ {#if svg}
256
+ <g in:fade={{ duration: fadeInDuration }}>
257
+ {#each edges.values() as edge}
258
+ {#each generateEdgePath(edge) as d}
259
+ {@const EdgeComponent = edge.component ?? BaseEdge}
260
+ {#if Array.isArray(EdgeComponent)}
261
+ {@const [EdgeComp, edgeProps] = EdgeComponent}
262
+ <EdgeComp {d} {edge} {...edgeProps} />
263
+ {:else}
264
+ <EdgeComponent {d} {edge} />
265
+ {/if}
266
+ {/each}
267
+ {/each}
268
+ </g>
269
+ {/if}
231
270
  </svg>
@@ -3,6 +3,8 @@ import { SvelteMap } from "svelte/reactivity";
3
3
  import { type Vector2 } from "./diagram-lib.js";
4
4
  import type { HTMLAttributes } from "svelte/elements";
5
5
  import type { Writable } from "svelte/store";
6
+ import { getNodeAnchorFast } from "./layout-utils.js";
7
+ import type { ComponentProps } from "svelte";
6
8
  type PathGenParams = {
7
9
  pathGen?: "bezier";
8
10
  curvature?: number;
@@ -12,15 +14,22 @@ type PathGenParams = {
12
14
  borderRadius?: number;
13
15
  center?: Vector2;
14
16
  };
15
- export type SurfaceEdgeParams = {
16
- component?: typeof BaseEdge;
17
+ export type SurfaceEdgeParams<C extends typeof BaseEdge = typeof BaseEdge> = {
18
+ component?: C | [C, Omit<ComponentProps<C>, "d" | "edge">];
17
19
  sourceAnchor?: Vector2;
18
20
  targetAnchor?: Vector2;
19
21
  svgPathAttributes?: HTMLAttributes<SVGPathElement>;
20
22
  } & PathGenParams;
23
+ export interface PrecalculatedEdge {
24
+ x1: number;
25
+ y1: number;
26
+ x2: number;
27
+ y2: number;
28
+ }
21
29
  export type SurfaceEdge = {
22
30
  source: string | HTMLElement | string[] | HTMLElement[];
23
31
  target: string | HTMLElement | string[] | HTMLElement[];
32
+ precalculated?: PrecalculatedEdge;
24
33
  } & SurfaceEdgeParams;
25
34
  type $$ComponentProps = {
26
35
  hostElement: Element;
@@ -28,6 +37,8 @@ type $$ComponentProps = {
28
37
  width?: Writable<number>;
29
38
  height?: Writable<number>;
30
39
  svgAttributes?: HTMLAttributes<SVGElement>;
40
+ getNodeAnchor?: typeof getNodeAnchorFast;
41
+ fadeInDuration?: number;
31
42
  };
32
43
  declare const Surface: import("svelte").Component<$$ComponentProps, {
33
44
  width: Writable<number> | undefined;
package/dist/index.d.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export * from './diagram-lib.js';
2
+ export * from "./layout-utils.js";
2
3
  export { createNodal, connectTo } from "./svelte-actions.js";
4
+ export type { NodalEdgeProps } from "./BaseEdge.svelte";
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './diagram-lib.js';
2
+ export * from "./layout-utils.js";
2
3
  export { createNodal, connectTo } from "./svelte-actions.js";
@@ -0,0 +1,31 @@
1
+ import type { Vector2 } from "./diagram-lib.js";
2
+ export declare function getElementQuad(el: Element): DOMQuad | {
3
+ p1: {
4
+ x: number;
5
+ y: number;
6
+ };
7
+ p2: {
8
+ x: number;
9
+ y: number;
10
+ };
11
+ p3: {
12
+ x: number;
13
+ y: number;
14
+ };
15
+ p4: {
16
+ x: number;
17
+ y: number;
18
+ };
19
+ };
20
+ export declare function toSvgPoint(svg: SVGGraphicsElement, x: number, y: number): {
21
+ left: number;
22
+ top: number;
23
+ };
24
+ export declare function getNodeAnchorWithBoundingClientRect(el: Element, anchor: Vector2, svg: SVGGraphicsElement): {
25
+ left: number;
26
+ top: number;
27
+ };
28
+ export declare function getNodeAnchorFast(node: HTMLElement, anchor: Vector2, svgElement: SVGGraphicsElement): {
29
+ left: number;
30
+ top: number;
31
+ };
@@ -0,0 +1,44 @@
1
+ export function getElementQuad(el) {
2
+ if (el.getBoxQuads) {
3
+ const quads = el.getBoxQuads();
4
+ if (quads && quads.length)
5
+ return quads[0];
6
+ }
7
+ // Fallback to rect -> quad
8
+ const r = el.getBoundingClientRect();
9
+ return {
10
+ p1: { x: r.left, y: r.top },
11
+ p2: { x: r.right, y: r.top },
12
+ p3: { x: r.right, y: r.bottom },
13
+ p4: { x: r.left, y: r.bottom },
14
+ };
15
+ }
16
+ export function toSvgPoint(svg, x, y) {
17
+ const ctm = svg.getScreenCTM();
18
+ if (!ctm)
19
+ return { left: x, top: y };
20
+ const inv = ctm.inverse();
21
+ const p = new DOMPoint(x, y).matrixTransform(inv);
22
+ return { left: p.x, top: p.y };
23
+ }
24
+ export function getNodeAnchorWithBoundingClientRect(el, anchor, svg) {
25
+ const q = getElementQuad(el);
26
+ // Clamp to [0,1] to avoid surprises.
27
+ const ax = Math.max(0, Math.min(1, anchor.x));
28
+ const ay = Math.max(0, Math.min(1, anchor.y));
29
+ // Bilinear interpolation across the quad:
30
+ // p(ax,ay) = (1-ax)(1-ay) * p1 + ax(1-ay) * p2 + ax*ay * p3 + (1-ax)ay * p4
31
+ const w1 = (1 - ax) * (1 - ay);
32
+ const w2 = ax * (1 - ay);
33
+ const w3 = ax * ay;
34
+ const w4 = (1 - ax) * ay;
35
+ const sx = w1 * q.p1.x + w2 * q.p2.x + w3 * q.p3.x + w4 * q.p4.x;
36
+ const sy = w1 * q.p1.y + w2 * q.p2.y + w3 * q.p3.y + w4 * q.p4.y;
37
+ return toSvgPoint(svg, sx, sy);
38
+ }
39
+ export function getNodeAnchorFast(node, anchor, svgElement) {
40
+ return {
41
+ left: node.offsetLeft + anchor.x * node.clientWidth,
42
+ top: node.offsetTop + anchor.y * node.clientHeight,
43
+ };
44
+ }
@@ -1,13 +1,32 @@
1
1
  import type { Attachment } from "svelte/attachments";
2
2
  import type { HTMLAttributes } from "svelte/elements";
3
- import type { SurfaceEdgeParams } from "./Surface.svelte";
3
+ import { SvelteMap } from "svelte/reactivity";
4
+ import { type Writable } from "svelte/store";
5
+ import type { SurfaceEdge, SurfaceEdgeParams } from "./Surface.svelte";
6
+ import type { Vector2 } from "./diagram-lib.js";
4
7
  type IndividualConnectActionParam = string | (SurfaceEdgeParams & {
5
8
  target: string | string[];
6
9
  }) | (SurfaceEdgeParams & {
7
10
  source: string | string[];
8
11
  });
9
- export declare function createNodal({ svgAttributes, }?: Partial<{
12
+ interface NodalSurfaceElement extends HTMLElement {
13
+ _nodalSurface: {
14
+ internals: {
15
+ width: Writable<number> | undefined;
16
+ height: Writable<number> | undefined;
17
+ edges: SvelteMap<string, SurfaceEdge>;
18
+ };
19
+ schedule: () => void;
20
+ };
21
+ }
22
+ export declare function createNodal({ svgAttributes, getNodeAnchor, fadeInDuration, onMount, }?: Partial<{
10
23
  svgAttributes: HTMLAttributes<SVGElement>;
24
+ onMount: (engine: NodalSurfaceElement["_nodalSurface"]) => void;
25
+ getNodeAnchor: (node: HTMLElement, anchor: Vector2, svgElement: SVGElement) => {
26
+ top: number;
27
+ left: number;
28
+ };
29
+ fadeInDuration: number;
11
30
  }>): Attachment;
12
31
  export declare function connectTo(...edges: IndividualConnectActionParam[]): Attachment;
13
32
  export {};
@@ -3,38 +3,36 @@ import { SvelteMap } from "svelte/reactivity";
3
3
  import { writable } from "svelte/store";
4
4
  import Surface from "./Surface.svelte";
5
5
  function drawCall(surface) {
6
- requestAnimationFrame(() => {
7
- console.debug("Drawing nodal surface edges...");
8
- const connections = surface.querySelectorAll("[data-nodal-connected='true']");
9
- connections.forEach((conn) => {
10
- console.debug("Processing connection for:", conn);
11
- const host = conn;
12
- if (!conn._nodalEdgeConnections ||
13
- conn._nodalEdgeConnections.length === 0) {
14
- console.debug("No edges found for host:", host);
15
- return;
6
+ // console.debug("Drawing nodal surface edges...");
7
+ const connections = surface.querySelectorAll("[data-nodal-connected='true']");
8
+ connections.forEach((conn) => {
9
+ // console.debug("Processing connection for:", conn);
10
+ const host = conn;
11
+ if (!conn._nodalEdgeConnections ||
12
+ conn._nodalEdgeConnections.length === 0) {
13
+ // console.debug("No edges found for host:", host);
14
+ return;
15
+ }
16
+ conn._nodalEdgeConnections.forEach((edge) => {
17
+ let edgeDef;
18
+ if (typeof edge == "string") {
19
+ edgeDef = {
20
+ source: host,
21
+ target: edge,
22
+ };
16
23
  }
17
- conn._nodalEdgeConnections.forEach((edge) => {
18
- let edgeDef;
19
- if (typeof edge == "string") {
20
- edgeDef = {
21
- source: host,
22
- target: edge,
23
- };
24
- }
25
- else {
26
- edgeDef = {
27
- source: "source" in edge ? edge.source : host,
28
- target: "target" in edge ? edge.target : host,
29
- ...edge,
30
- };
31
- }
32
- surface._nodalSurface.edges.set(`${edgeDef.source}->${edgeDef.target}`, edgeDef);
33
- });
24
+ else {
25
+ edgeDef = {
26
+ source: "source" in edge ? edge.source : host,
27
+ target: "target" in edge ? edge.target : host,
28
+ ...edge,
29
+ };
30
+ }
31
+ surface._nodalSurface.internals.edges.set(`${edgeDef.source}->${edgeDef.target}`, edgeDef);
34
32
  });
35
33
  });
36
34
  }
37
- export function createNodal({ svgAttributes = {}, } = {}) {
35
+ export function createNodal({ svgAttributes = {}, getNodeAnchor, fadeInDuration, onMount, } = {}) {
38
36
  return (element) => {
39
37
  console.debug("Creating nodal sruface..");
40
38
  if (!Object.hasOwn(element, "_nodalSurface")) {
@@ -47,23 +45,53 @@ export function createNodal({ svgAttributes = {}, } = {}) {
47
45
  width: writable(element.clientWidth),
48
46
  height: writable(element.clientHeight),
49
47
  svgAttributes,
48
+ getNodeAnchor,
49
+ fadeInDuration,
50
50
  },
51
51
  });
52
+ const engine = {
53
+ _isScheduled: false,
54
+ draw: () => drawCall(element),
55
+ schedule() {
56
+ if (engine._isScheduled)
57
+ return;
58
+ engine._isScheduled = true;
59
+ requestAnimationFrame(() => {
60
+ this._isScheduled = false;
61
+ drawCall(element);
62
+ });
63
+ },
64
+ internals: exports
65
+ };
52
66
  Object.defineProperty(element, "_nodalSurface", {
53
- value: exports,
67
+ value: engine,
54
68
  });
55
69
  const resizeObserver = new ResizeObserver((entries) => {
56
- for (let entry of entries) {
57
- const { width, height } = entry.contentRect;
58
- console.debug("Resizing nodal surface to:", width, height);
59
- exports.width?.set(width);
60
- exports.height?.set(height);
61
- // redraw edges
62
- drawCall(element);
63
- }
70
+ requestAnimationFrame(() => {
71
+ for (let entry of entries) {
72
+ const { width, height } = entry.contentRect;
73
+ // console.debug(
74
+ // "Resizing nodal surface to:",
75
+ // width,
76
+ // height,
77
+ // );
78
+ exports.width?.set(width);
79
+ exports.height?.set(height);
80
+ // redraw edges
81
+ // drawCall(element as NodalSurfaceElement);
82
+ engine.schedule();
83
+ }
84
+ });
64
85
  });
86
+ try {
87
+ onMount?.(engine);
88
+ }
89
+ catch (e) {
90
+ console.error("Error in onMount callback for nodal surface:", e);
91
+ }
65
92
  resizeObserver.observe(element);
66
- drawCall(element);
93
+ engine.schedule();
94
+ // drawCall(element as NodalSurfaceElement);
67
95
  }
68
96
  };
69
97
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@cnvx/nodal",
3
3
  "private": false,
4
4
  "license": "MIT",
5
- "version": "0.3.3",
5
+ "version": "0.5.0",
6
6
  "scripts": {
7
7
  "dev": "vite dev",
8
8
  "dev-lib": "svelte-package -w",