@cnvx/nodal 0.3.2 → 0.4.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.
@@ -14,9 +14,11 @@
14
14
  <path
15
15
  d={path}
16
16
  fill="none"
17
+ stroke-width="2"
17
18
  stroke="currentColor"
18
19
  stroke-linecap="round"
19
20
  stroke-linejoin="round"
20
21
  shape-rendering="smooth"
22
+ vector-effect="non-scaling-stroke"
21
23
  {...edge.svgPathAttributes || {}}
22
24
  />
@@ -12,6 +12,11 @@
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";
15
20
 
16
21
  type PathGenParams =
17
22
  | {
@@ -32,25 +37,44 @@
32
37
  svgPathAttributes?: HTMLAttributes<SVGPathElement>;
33
38
  } & PathGenParams;
34
39
 
40
+ // export type SurfaceEdge = {
41
+
42
+ // } & SurfaceEdgeParams;
43
+
44
+ export interface PrecalculatedEdge {
45
+ x1: number;
46
+ y1: number;
47
+ x2: number;
48
+ y2: number;
49
+ }
50
+
35
51
  export type SurfaceEdge = {
36
52
  source: string | HTMLElement | string[] | HTMLElement[];
37
53
  target: string | HTMLElement | string[] | HTMLElement[];
54
+
55
+ precalculated?: PrecalculatedEdge;
38
56
  } & SurfaceEdgeParams;
39
57
  </script>
40
58
 
41
59
  <script lang="ts">
60
+ let svg: SVGSVGElement | undefined = $state();
61
+
42
62
  const {
43
63
  hostElement,
44
64
  edges: _edges,
45
65
  svgAttributes,
46
66
  width: _width,
47
67
  height: _height,
68
+ fadeInDuration = 80,
69
+ getNodeAnchor = getNodeAnchorWithBoundingClientRect,
48
70
  }: {
49
71
  hostElement: Element;
50
72
  edges: SvelteMap<string, SurfaceEdge>;
51
73
  width?: Writable<number>;
52
74
  height?: Writable<number>;
53
75
  svgAttributes?: HTMLAttributes<SVGElement>;
76
+ getNodeAnchor?: typeof getNodeAnchorFast;
77
+ fadeInDuration?: number;
54
78
  } = $props();
55
79
 
56
80
  export const width = _width;
@@ -158,6 +182,18 @@
158
182
  }
159
183
 
160
184
  function generateEdgePath(edge: SurfaceEdge) {
185
+ if (!svg) return [];
186
+
187
+ if (edge.precalculated) {
188
+ return generateCurvePath(
189
+ edge.precalculated.x1,
190
+ edge.precalculated.y1,
191
+ edge.precalculated.x2,
192
+ edge.precalculated.y2,
193
+ edge,
194
+ );
195
+ }
196
+
161
197
  const sourceNodes = getNodes(edge.source);
162
198
  const targetNodes = getNodes(edge.target);
163
199
 
@@ -175,10 +211,12 @@
175
211
  const sourceAnchor = getNodeAnchor(
176
212
  source,
177
213
  edge.sourceAnchor ?? Anchor.CENTER_CENTER,
214
+ svg!,
178
215
  );
179
216
  const targetAnchor = getNodeAnchor(
180
217
  target,
181
218
  edge.targetAnchor ?? Anchor.CENTER_CENTER,
219
+ svg!,
182
220
  );
183
221
  paths.push(
184
222
  generateCurvePath(
@@ -203,29 +241,23 @@
203
241
 
204
242
  return paths;
205
243
  }
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
244
  </script>
214
245
 
246
+ <!-- style="position:absolute;top:0;right:0;bottom:0;left:0;height:100%;width:100%;overflow:visible;z-index:0;" -->
215
247
  <svg
216
- shape-rendering="crispEdges"
217
- style="position:absolute;top:0;right:0;bottom:0;left:0;height:100%;width:100%;overflow:visible;z-index:0;"
248
+ bind:this={svg}
249
+ preserveAspectRatio="none"
250
+ style="position: absolute; inset: 0px; height: 1px; width: 1px; overflow: visible; z-index: 0;"
218
251
  {...svgAttributes}
219
252
  >
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}
253
+ {#if svg}
254
+ <g in:fade={{ duration: fadeInDuration }}>
255
+ {#each edges.values() as edge}
256
+ {#each generateEdgePath(edge) as path}
257
+ {@const EdgeComponent = edge.component ?? BaseEdge}
258
+ <EdgeComponent {path} {edge} />
259
+ {/each}
260
+ {/each}
261
+ </g>
262
+ {/if}
231
263
  </svg>
@@ -3,6 +3,7 @@ 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";
6
7
  type PathGenParams = {
7
8
  pathGen?: "bezier";
8
9
  curvature?: number;
@@ -18,9 +19,16 @@ export type SurfaceEdgeParams = {
18
19
  targetAnchor?: Vector2;
19
20
  svgPathAttributes?: HTMLAttributes<SVGPathElement>;
20
21
  } & PathGenParams;
22
+ export interface PrecalculatedEdge {
23
+ x1: number;
24
+ y1: number;
25
+ x2: number;
26
+ y2: number;
27
+ }
21
28
  export type SurfaceEdge = {
22
29
  source: string | HTMLElement | string[] | HTMLElement[];
23
30
  target: string | HTMLElement | string[] | HTMLElement[];
31
+ precalculated?: PrecalculatedEdge;
24
32
  } & SurfaceEdgeParams;
25
33
  type $$ComponentProps = {
26
34
  hostElement: Element;
@@ -28,6 +36,8 @@ type $$ComponentProps = {
28
36
  width?: Writable<number>;
29
37
  height?: Writable<number>;
30
38
  svgAttributes?: HTMLAttributes<SVGElement>;
39
+ getNodeAnchor?: typeof getNodeAnchorFast;
40
+ fadeInDuration?: number;
31
41
  };
32
42
  declare const Surface: import("svelte").Component<$$ComponentProps, {
33
43
  width: Writable<number> | undefined;
package/dist/index.d.ts 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";
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.2",
5
+ "version": "0.4.0",
6
6
  "scripts": {
7
7
  "dev": "vite dev",
8
8
  "dev-lib": "svelte-package -w",