@cnvx/nodal 0.2.2 → 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.
@@ -0,0 +1,22 @@
1
+ <script lang="ts" module>
2
+ import type { SurfaceEdge } from "./Surface.svelte";
3
+
4
+ export interface NodalEdgeProps {
5
+ edge: SurfaceEdge;
6
+ path: string;
7
+ }
8
+ </script>
9
+
10
+ <script lang="ts">
11
+ const { edge, path }: NodalEdgeProps = $props();
12
+ </script>
13
+
14
+ <path
15
+ d={path}
16
+ fill="none"
17
+ stroke="currentColor"
18
+ stroke-linecap="round"
19
+ stroke-linejoin="round"
20
+ shape-rendering="smooth"
21
+ {...edge.svgPathAttributes || {}}
22
+ />
@@ -0,0 +1,8 @@
1
+ import type { SurfaceEdge } from "./Surface.svelte";
2
+ export interface NodalEdgeProps {
3
+ edge: SurfaceEdge;
4
+ path: string;
5
+ }
6
+ declare const BaseEdge: import("svelte").Component<NodalEdgeProps, {}, "">;
7
+ type BaseEdge = ReturnType<typeof BaseEdge>;
8
+ export default BaseEdge;
@@ -0,0 +1,231 @@
1
+ <script lang="ts" module>
2
+ import BaseEdge from "./BaseEdge.svelte";
3
+ import { SvelteMap } from "svelte/reactivity";
4
+ import {
5
+ eq,
6
+ getBezierPath,
7
+ getSmoothStepPath,
8
+ normaliseAngle,
9
+ sideForAngle,
10
+ type Vector2,
11
+ Anchor,
12
+ } from "./diagram-lib.js";
13
+ import type { HTMLAttributes } from "svelte/elements";
14
+ import type { Writable } from "svelte/store";
15
+
16
+ type PathGenParams =
17
+ | {
18
+ pathGen?: "bezier";
19
+ curvature?: number;
20
+ offset?: Vector2;
21
+ }
22
+ | {
23
+ pathGen: "smoothstep";
24
+ borderRadius?: number;
25
+ center?: Vector2;
26
+ };
27
+
28
+ export type SurfaceEdgeParams = {
29
+ component?: typeof BaseEdge;
30
+ sourceAnchor?: Vector2;
31
+ targetAnchor?: Vector2;
32
+ svgPathAttributes?: HTMLAttributes<SVGPathElement>;
33
+ } & PathGenParams;
34
+
35
+ export type SurfaceEdge = {
36
+ source: string | HTMLElement | string[] | HTMLElement[];
37
+ target: string | HTMLElement | string[] | HTMLElement[];
38
+ } & SurfaceEdgeParams;
39
+ </script>
40
+
41
+ <script lang="ts">
42
+ const {
43
+ hostElement,
44
+ edges: _edges,
45
+ svgAttributes,
46
+ width: _width,
47
+ height: _height,
48
+ }: {
49
+ hostElement: Element;
50
+ edges: SvelteMap<string, SurfaceEdge>;
51
+ width?: Writable<number>;
52
+ height?: Writable<number>;
53
+ svgAttributes?: HTMLAttributes<SVGElement>;
54
+ } = $props();
55
+
56
+ export const width = _width;
57
+ export const height = _height;
58
+ export const edges = _edges;
59
+
60
+ function generateCurvePath(
61
+ x1: number,
62
+ y1: number,
63
+ x2: number,
64
+ y2: number,
65
+ edge: SurfaceEdge,
66
+ ): string {
67
+ const {
68
+ sourceAnchor: source = Anchor.CENTER_CENTER,
69
+ targetAnchor: dest = Anchor.CENTER_CENTER,
70
+ } = edge;
71
+
72
+ function anchorToRads({ x, y }: Vector2) {
73
+ // vector from the node’s centre (0.5, 0.5)
74
+ const dx = x - 0.5; // + right
75
+ const dy = 0.5 - y; // + up (flip screen-y for math coords)
76
+ const raw = Math.atan2(dy, dx); // signed angle
77
+ return normaliseAngle(raw);
78
+ }
79
+
80
+ // CRUCIAL TO INVERT THE Y DIRECTION SINCE IT GOES FROM
81
+ // NEGATIVE TO POSITIVE!!!!!!!!!!!!!
82
+ const leaveAngle = Math.atan2(y1 - y2, x2 - x1);
83
+ const arriveAngle = leaveAngle + Math.PI;
84
+
85
+ const sourcePosition = eq(source, Anchor.CENTER_CENTER)
86
+ ? sideForAngle(leaveAngle)
87
+ : sideForAngle(anchorToRads(source));
88
+
89
+ const targetPosition = eq(dest, Anchor.CENTER_CENTER)
90
+ ? sideForAngle(arriveAngle)
91
+ : sideForAngle(anchorToRads(dest));
92
+
93
+ const props = {
94
+ sourceX: x1,
95
+ sourceY: y1,
96
+ sourcePosition: sourcePosition,
97
+
98
+ targetX: x2,
99
+ targetY: y2,
100
+ targetPosition: targetPosition,
101
+ };
102
+
103
+ edge.pathGen ??= "bezier";
104
+ if (edge.pathGen == "bezier") {
105
+ return getBezierPath({
106
+ ...props,
107
+ curvature: edge.curvature ?? 0.25,
108
+ })[0];
109
+ } else if (edge.pathGen == "smoothstep") {
110
+ const pathgenParams = {
111
+ borderRadius: edge?.borderRadius ?? 15,
112
+ center: edge?.center,
113
+ };
114
+
115
+ // calculate the absolute center from the relative center
116
+ let centerX = undefined,
117
+ centerY = undefined;
118
+
119
+ if (pathgenParams.center) {
120
+ centerX = x1 + pathgenParams.center.x * (x2 - x1);
121
+ centerY = y1 + pathgenParams.center.y * (y2 - y1);
122
+ }
123
+
124
+ return getSmoothStepPath({
125
+ ...props,
126
+ borderRadius: pathgenParams.borderRadius,
127
+ centerX,
128
+ centerY,
129
+ })[0];
130
+ }
131
+
132
+ throw new Error("unreachable");
133
+ }
134
+
135
+ function* joinIterables<T>(...lists: Iterable<T>[]) {
136
+ for (const list of lists) yield* list;
137
+ }
138
+
139
+ function resolveItem(
140
+ item: string | HTMLElement,
141
+ host: ParentNode,
142
+ ): Iterable<HTMLElement> {
143
+ return typeof item === "string"
144
+ ? (host.querySelectorAll<HTMLElement>(
145
+ item,
146
+ ) as NodeListOf<HTMLElement>)
147
+ : [item];
148
+ }
149
+
150
+ export function getNodes(
151
+ input: string | HTMLElement | string[] | HTMLElement[],
152
+ ): HTMLElement[] {
153
+ const parts: Iterable<HTMLElement>[] = Array.isArray(input)
154
+ ? input.map((i) => resolveItem(i, hostElement))
155
+ : [resolveItem(input, hostElement)];
156
+
157
+ return Array.from(joinIterables(...parts));
158
+ }
159
+
160
+ function generateEdgePath(edge: SurfaceEdge) {
161
+ const sourceNodes = getNodes(edge.source);
162
+ const targetNodes = getNodes(edge.target);
163
+
164
+ const paths: string[] = [];
165
+
166
+ sourceNodes.forEach((source) => {
167
+ targetNodes.forEach((target) => {
168
+ if (source === target) {
169
+ console.error(
170
+ `Source and target are the same element: ${edge.source}`,
171
+ );
172
+ return;
173
+ }
174
+
175
+ const sourceAnchor = getNodeAnchor(
176
+ source,
177
+ edge.sourceAnchor ?? Anchor.CENTER_CENTER,
178
+ );
179
+ const targetAnchor = getNodeAnchor(
180
+ target,
181
+ edge.targetAnchor ?? Anchor.CENTER_CENTER,
182
+ );
183
+ paths.push(
184
+ generateCurvePath(
185
+ sourceAnchor.left,
186
+ sourceAnchor.top,
187
+ targetAnchor.left,
188
+ targetAnchor.top,
189
+ edge,
190
+ ),
191
+ );
192
+ });
193
+ });
194
+ // if (!(sourceNode instanceof HTMLElement)) {
195
+ // console.error(`Source node not found: ${edge.source}`);
196
+ // return "";
197
+ // }
198
+
199
+ // if (!(targetNode instanceof HTMLElement)) {
200
+ // console.error(`Target node not found: ${edge.target}`);
201
+ // return "";
202
+ // }
203
+
204
+ return paths;
205
+ }
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
+ </script>
214
+
215
+ <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:-1;"
218
+ {...svgAttributes}
219
+ >
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}
231
+ </svg>
@@ -0,0 +1,39 @@
1
+ import BaseEdge from "./BaseEdge.svelte";
2
+ import { SvelteMap } from "svelte/reactivity";
3
+ import { type Vector2 } from "./diagram-lib.js";
4
+ import type { HTMLAttributes } from "svelte/elements";
5
+ import type { Writable } from "svelte/store";
6
+ type PathGenParams = {
7
+ pathGen?: "bezier";
8
+ curvature?: number;
9
+ offset?: Vector2;
10
+ } | {
11
+ pathGen: "smoothstep";
12
+ borderRadius?: number;
13
+ center?: Vector2;
14
+ };
15
+ export type SurfaceEdgeParams = {
16
+ component?: typeof BaseEdge;
17
+ sourceAnchor?: Vector2;
18
+ targetAnchor?: Vector2;
19
+ svgPathAttributes?: HTMLAttributes<SVGPathElement>;
20
+ } & PathGenParams;
21
+ export type SurfaceEdge = {
22
+ source: string | HTMLElement | string[] | HTMLElement[];
23
+ target: string | HTMLElement | string[] | HTMLElement[];
24
+ } & SurfaceEdgeParams;
25
+ type $$ComponentProps = {
26
+ hostElement: Element;
27
+ edges: SvelteMap<string, SurfaceEdge>;
28
+ width?: Writable<number>;
29
+ height?: Writable<number>;
30
+ svgAttributes?: HTMLAttributes<SVGElement>;
31
+ };
32
+ declare const Surface: import("svelte").Component<$$ComponentProps, {
33
+ width: Writable<number> | undefined;
34
+ height: Writable<number> | undefined;
35
+ edges: SvelteMap<string, SurfaceEdge>;
36
+ getNodes: (input: string | HTMLElement | string[] | HTMLElement[]) => HTMLElement[];
37
+ }, "">;
38
+ type Surface = ReturnType<typeof Surface>;
39
+ export default Surface;
@@ -1,5 +1,3 @@
1
- export declare const browser = true;
2
- export declare const dev: any;
3
1
  export interface Vector2 {
4
2
  x: number;
5
3
  y: number;
@@ -53,7 +51,6 @@ export declare enum Side {
53
51
  Left = 2,
54
52
  Bottom = 3
55
53
  }
56
- export declare function debugSide(s: Side): string;
57
54
  export declare function normaliseAngle(r: number): number;
58
55
  export declare function sideToUnitVector2(s: Side): Vector2;
59
56
  export declare function sideForAngle(rad: number): Side;
@@ -1,6 +1,3 @@
1
- export const browser = !!globalThis?.window;
2
- export const dev = globalThis?.process?.env?.NODE_ENV &&
3
- !globalThis?.process.env.NODE_ENV.toLowerCase().startsWith("prod");
4
1
  export const vector2 = (x, y) => ({ x, y });
5
2
  export const eq = (a, b) => a.x === b.x && a.y === b.y;
6
3
  export const Anchor = {
@@ -21,14 +18,6 @@ export var Side;
21
18
  Side[Side["Left"] = 2] = "Left";
22
19
  Side[Side["Bottom"] = 3] = "Bottom";
23
20
  })(Side || (Side = {}));
24
- export function debugSide(s) {
25
- return {
26
- [Side.Right]: 'Right',
27
- [Side.Top]: 'Top',
28
- [Side.Left]: 'Left',
29
- [Side.Bottom]: 'Bottom'
30
- }[s];
31
- }
32
21
  export function normaliseAngle(r) {
33
22
  return ((r % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);
34
23
  }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,2 @@
1
- export { default as Diagram } from './Diagram.svelte';
2
- export { default as DiagramController } from './DiagramController.svelte';
3
- export { default as DiagramNode } from './DiagramNode.svelte';
4
- export { default as PrerenderDiagram } from './PrerenderDiagram.svelte';
5
1
  export * from './diagram-lib.js';
6
- export type { DiagramEdgeDef as DiagramEdge, DiagramEdgeParams, DiagramProps } from './Diagram.svelte';
7
- export type { DiagramNodeProps } from './DiagramNode.svelte';
2
+ export { createNodal, connectTo } from "./svelte-actions.js";
package/dist/index.js CHANGED
@@ -1,5 +1,2 @@
1
- export { default as Diagram } from './Diagram.svelte';
2
- export { default as DiagramController } from './DiagramController.svelte';
3
- export { default as DiagramNode } from './DiagramNode.svelte';
4
- export { default as PrerenderDiagram } from './PrerenderDiagram.svelte';
5
1
  export * from './diagram-lib.js';
2
+ export { createNodal, connectTo } from "./svelte-actions.js";
@@ -0,0 +1,13 @@
1
+ import type { Attachment } from "svelte/attachments";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ import type { SurfaceEdgeParams } from "./Surface.svelte";
4
+ type IndividualConnectActionParam = string | (SurfaceEdgeParams & {
5
+ target: string | string[];
6
+ }) | (SurfaceEdgeParams & {
7
+ source: string | string[];
8
+ });
9
+ export declare function createNodal({ svgAttributes, }: {
10
+ svgAttributes: HTMLAttributes<SVGElement>;
11
+ }): Attachment;
12
+ export declare function connectTo(...edges: IndividualConnectActionParam[]): Attachment;
13
+ export {};
@@ -0,0 +1,75 @@
1
+ import { mount } from "svelte";
2
+ import { SvelteMap } from "svelte/reactivity";
3
+ import { writable } from "svelte/store";
4
+ import Surface from "./Surface.svelte";
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;
16
+ }
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
+ });
34
+ });
35
+ });
36
+ }
37
+ export function createNodal({ svgAttributes = {}, }) {
38
+ return (element) => {
39
+ console.debug("Creating nodal sruface..");
40
+ if (!Object.hasOwn(element, "_nodalSurface")) {
41
+ const edges = new SvelteMap();
42
+ const exports = mount(Surface, {
43
+ target: element,
44
+ props: {
45
+ hostElement: element,
46
+ edges,
47
+ width: writable(element.clientWidth),
48
+ height: writable(element.clientHeight),
49
+ svgAttributes,
50
+ },
51
+ });
52
+ Object.defineProperty(element, "_nodalSurface", {
53
+ value: exports,
54
+ });
55
+ 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
+ }
64
+ });
65
+ resizeObserver.observe(element);
66
+ drawCall(element);
67
+ }
68
+ };
69
+ }
70
+ export function connectTo(...edges) {
71
+ return (host) => {
72
+ host.setAttribute("data-nodal-connected", "true");
73
+ host._nodalEdgeConnections = edges;
74
+ };
75
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@cnvx/nodal",
3
3
  "private": false,
4
4
  "license": "MIT",
5
- "version": "0.2.2",
5
+ "version": "0.3.0",
6
6
  "scripts": {
7
7
  "dev": "vite dev",
8
8
  "dev-lib": "svelte-package -w",
@@ -1,377 +0,0 @@
1
- <script lang="ts" module>
2
- import { onMount, setContext, type Snippet } from "svelte";
3
- import { SvelteMap } from "svelte/reactivity";
4
- import {
5
- eq,
6
- getBezierPath,
7
- getSmoothStepPath,
8
- normaliseAngle,
9
- Side,
10
- sideForAngle,
11
- vector2,
12
- type Vector2,
13
- Anchor,
14
- browser,
15
- dev,
16
- } from "./diagram-lib.js";
17
- import type { HTMLAttributes } from "svelte/elements";
18
-
19
- export interface DiagramNodeDef {
20
- id: string;
21
- x: number;
22
- y: number;
23
- width?: number;
24
- height?: number;
25
-
26
- // this will make the node and all of its connections only client side
27
- clientOnly?: boolean;
28
- }
29
-
30
- type PathGenParams =
31
- | {
32
- pathGen?: "bezier";
33
- curvature?: number;
34
- offset?: Vector2;
35
- }
36
- | {
37
- pathGen: "smoothstep";
38
- borderRadius?: number;
39
- center?: Vector2;
40
- };
41
-
42
- export type DiagramEdgeParams = {
43
- // target: string;
44
- snippet?: Snippet<[edge: DiagramEdgeDef, path: string, extra: any]>;
45
- snippetExtraArg?: any;
46
-
47
- sourceAnchor?: Vector2;
48
- targetAnchor?: Vector2;
49
-
50
- class?: string;
51
- style?: string;
52
- zIndex?: number;
53
- } & PathGenParams;
54
-
55
- export type DiagramEdgeDef = {
56
- source: string;
57
- target: string;
58
- } & DiagramEdgeParams;
59
-
60
- export type DiagramProps = {
61
- nodes: SvelteMap<string, DiagramNodeDef>;
62
- edges: SvelteMap<string, DiagramEdgeDef>;
63
- children: Snippet;
64
- scaleToFit?: boolean;
65
- width?: number;
66
- height?: number;
67
- figureAttributes: HTMLAttributes<HTMLElement>;
68
- };
69
-
70
- // const getNodeOrigin = (node: DiagramNode) => node.origin ?? vector2(0.0, 0.5);
71
- // export const getNodeOrigin = (node: DiagramNode) => node.origin ?? vector2(0.5, 0.5);
72
- // export const getNodeOrigin = (node: DiagramNode) => vector2(0.0, 0.0);
73
-
74
- const getNodeSize = (node: DiagramNodeDef) => ({
75
- x: node.width ?? 0,
76
- y: node.height ?? 0,
77
- });
78
- </script>
79
-
80
- <script lang="ts">
81
- let {
82
- nodes,
83
- edges,
84
- children,
85
- scaleToFit,
86
- figureAttributes,
87
- width: userDefinedDiagramWidth,
88
- height: userDefinedDiagramHeight,
89
- }: DiagramProps = $props();
90
- export function generateCurvePath(
91
- x1: number,
92
- y1: number,
93
- x2: number,
94
- y2: number,
95
- edge: DiagramEdgeDef,
96
- ): string {
97
- const {
98
- sourceAnchor: source = Anchor.CENTER_CENTER,
99
- targetAnchor: dest = Anchor.CENTER_CENTER,
100
- } = edge;
101
-
102
- function anchorToRads({ x, y }: Vector2) {
103
- // vector from the node’s centre (0.5, 0.5)
104
- const dx = x - 0.5; // + right
105
- const dy = 0.5 - y; // + up (flip screen-y for math coords)
106
- const raw = Math.atan2(dy, dx); // signed angle
107
- return normaliseAngle(raw);
108
- }
109
-
110
- // CRUCIAL TO INVERT THE Y DIRECTION SINCE IT GOES FROM
111
- // NEGATIVE TO POSITIVE!!!!!!!!!!!!!
112
- const leaveAngle = Math.atan2(y1 - y2, x2 - x1);
113
- const arriveAngle = leaveAngle + Math.PI;
114
-
115
- const sourcePosition = eq(source, Anchor.CENTER_CENTER)
116
- ? sideForAngle(leaveAngle)
117
- : sideForAngle(anchorToRads(source));
118
-
119
- const targetPosition = eq(dest, Anchor.CENTER_CENTER)
120
- ? sideForAngle(arriveAngle)
121
- : sideForAngle(anchorToRads(dest));
122
-
123
- const props = {
124
- sourceX: x1,
125
- sourceY: y1,
126
- sourcePosition: sourcePosition,
127
-
128
- targetX: x2,
129
- targetY: y2,
130
- targetPosition: targetPosition,
131
- };
132
-
133
- edge.pathGen ??= "bezier";
134
- if (edge.pathGen == "bezier") {
135
- return getBezierPath({
136
- ...props,
137
- curvature: edge.curvature ?? 0.25,
138
- })[0];
139
- } else if (edge.pathGen == "smoothstep") {
140
- const pathgenParams = {
141
- borderRadius: edge?.borderRadius ?? 15,
142
- center: edge?.center,
143
- };
144
-
145
- // calculate the absolute center from the relative center
146
- let centerX = undefined,
147
- centerY = undefined;
148
-
149
- if (pathgenParams.center) {
150
- centerX = x1 + pathgenParams.center.x * (x2 - x1);
151
- centerY = y1 + pathgenParams.center.y * (y2 - y1);
152
- }
153
-
154
- return getSmoothStepPath({
155
- ...props,
156
- borderRadius: pathgenParams.borderRadius,
157
- centerX,
158
- centerY,
159
- })[0];
160
- }
161
-
162
- throw new Error("unreachable");
163
- }
164
-
165
- function calculateDimensions(_nodes: typeof nodes) {
166
- if (userDefinedDiagramHeight && userDefinedDiagramWidth) {
167
- return {
168
- min: vector2(0, 0),
169
- max: vector2(userDefinedDiagramWidth, userDefinedDiagramHeight),
170
- };
171
- }
172
-
173
- // console.time("dim");
174
- let newMin = vector2(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
175
- let newMax = vector2(Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER);
176
-
177
- for (let node of _nodes.values()) {
178
- // if (!browser && node.clientOnly) continue;
179
-
180
- const size = getNodeSize(node);
181
- // const origin = getNodeOrigin(node);
182
-
183
- const left = node.x;
184
- const top = node.y;
185
-
186
- const right = left + size.x;
187
- const bottom = top + size.y;
188
-
189
- newMin = vector2(Math.min(newMin.x, left), Math.min(newMin.y, top));
190
- newMax = vector2(
191
- Math.max(newMax.x, right),
192
- Math.max(newMax.y, bottom),
193
- );
194
- }
195
-
196
- // console.timeEnd("dim");
197
- return { min: newMin, max: newMax };
198
- }
199
-
200
- $inspect({ userDefinedDiagramHeight, userDefinedDiagramWidth });
201
- // const dimensions = $derived(calculateDimensions(nodes));
202
- // let dimensions = $derived(calculateDimensions(nodes));
203
- let dimensions = $state(calculateDimensions(nodes));
204
-
205
- $effect(() => {
206
- const newDimensions = calculateDimensions(nodes);
207
- dimensions.min = newDimensions.min;
208
- dimensions.max = newDimensions.max;
209
- });
210
-
211
- $inspect({ dimensions });
212
- // let dimensions = calculateDimensions(nodes);
213
- // onMount(() => (dimensions = calculateDimensions(nodes)));
214
-
215
- setContext("nodeMap", () => nodes);
216
- setContext("edgeMap", () => edges);
217
- setContext("dimensions", () => dimensions);
218
- setContext("prerendering", false);
219
-
220
- let width = $derived(
221
- // userDefinedDiagramWidth ??
222
- Math.max(dimensions.max.x - dimensions.min.x, 1),
223
- );
224
- let height = $derived(
225
- // userDefinedDiagramHeight ??
226
- Math.max(dimensions.max.y - dimensions.min.y, 1),
227
- );
228
-
229
- function generateEdgePath(edge: DiagramEdgeDef) {
230
- const sourceNode = nodes.get(edge.source)!;
231
- const targetNode = nodes.get(edge.target)!;
232
-
233
- const sourceAnchor = getNodeAnchor(
234
- sourceNode,
235
- edge.sourceAnchor ?? Anchor.CENTER_CENTER,
236
- );
237
- const targetAnchor = getNodeAnchor(
238
- targetNode,
239
- edge.targetAnchor ?? Anchor.CENTER_CENTER,
240
- );
241
-
242
- return generateCurvePath(
243
- sourceAnchor.left,
244
- sourceAnchor.top,
245
- targetAnchor.left,
246
- targetAnchor.top,
247
- edge,
248
- );
249
- }
250
-
251
- function getNodeAnchor(node: DiagramNodeDef, anchor: Vector2) {
252
- const size = getNodeSize(node);
253
-
254
- // if (!browser && !eq(anchor, Anchor.CENTER_CENTER) && eq(size, vector2(0, 0))) {
255
- // throw new Error(
256
- // `To use anchor other than CENTER,CENTER please set the width and height of the node explicitly or set autosize to true for the node\n\nNode '${node.id}' does not have explicity width or height and thus cannot be connected with a relative anchor`
257
- // );
258
- // }
259
-
260
- const left = node.x - dimensions.min.x + anchor.x * size.x;
261
- const top = node.y - dimensions.min.y + anchor.y * size.y;
262
-
263
- return { left, top };
264
- }
265
-
266
- // TODO: desperate need for refactoring
267
- let edgesByZIndexPlane = $derived(
268
- Array.from(edges.values()).reduce((acc, edge) => {
269
- const zIndex = edge.zIndex ?? 0;
270
- if (!acc.has(zIndex)) {
271
- acc.set(zIndex, []);
272
- }
273
- acc.get(zIndex)!.push(edge);
274
- return acc;
275
- }, new Map<number, DiagramEdgeDef[]>()),
276
- );
277
-
278
- // let depthMap = new SvelteMap<number, [DiagramEdge[], DiagramNode[]]>();
279
- // $effect(() => {
280
- // if (nodes || edges) {
281
- // depthMap.clear();
282
- // for (const node of nodes.values()) {
283
- // // depthMap.has(node.zIndex)
284
- // }
285
- // }
286
- // });
287
-
288
- let diagramContainer: HTMLElement | null = null;
289
- let scale = $state(1);
290
- onMount(() => {
291
- if (scaleToFit) {
292
- if (userDefinedDiagramHeight || userDefinedDiagramWidth) {
293
- throw new Error(
294
- "Cannot use user defined width/height with scaleToFit",
295
- );
296
- }
297
-
298
- console.log(diagramContainer?.parentElement);
299
- const scaleY = diagramContainer?.parentElement?.clientHeight
300
- ? diagramContainer.parentElement?.clientHeight / height
301
- : 1;
302
- const scaleX = diagramContainer?.parentElement?.clientWidth
303
- ? diagramContainer?.parentElement?.clientWidth / width
304
- : 1;
305
-
306
- scale = Math.min(scaleX, scaleY);
307
- }
308
- dimensions = calculateDimensions(nodes);
309
- });
310
- </script>
311
-
312
- {#snippet defaultEdge(edge: DiagramEdgeDef, edgePath: string)}
313
- <path
314
- d={edgePath}
315
- fill="none"
316
- stroke="currentColor"
317
- class={edge.class}
318
- vector-effect="non-scaling-stroke"
319
- stroke-linecap="round"
320
- stroke-linejoin="round"
321
- shape-rendering="smooth"
322
- style={edge.style}
323
- />
324
- {/snippet}
325
-
326
- <figure
327
- bind:this={diagramContainer}
328
- aria-label="Diagram"
329
- aria-hidden={!dev}
330
- inert={!dev}
331
- {...figureAttributes}
332
- role="img"
333
- style="position:relative;width:{width}px;height:{height}px;overflow:visible;user-select:none;transform:scale({scale});transform-origin:center center;"
334
- >
335
- <!-- <svg class="absolute top-0 right-0 bottom-0 left-0 z-0 h-full w-full overflow-visible"> -->
336
- <!-- {#each edges.values() as edge, i}
337
- {@const sourceNode = nodes.get(edge.source)}
338
- {@const targetNode = nodes.get(edge.target)}
339
-
340
- {#if sourceNode && targetNode && !(!browser && (sourceNode.clientOnly || targetNode.clientOnly))}
341
- <svg
342
- class="absolute top-0 right-0 bottom-0 left-0 h-full w-full overflow-visible"
343
- style="z-index:{edge.zIndex ?? 0};"
344
- >
345
- {@render (edge.snippet ? edge.snippet : defaultEdge)(
346
- edge,
347
- generateEdgePath(edge),
348
- edge.snippetExtraArg
349
- )}
350
- </svg>
351
- {/if}
352
- {/each} -->
353
- <!-- </svg> -->
354
-
355
- {#each edgesByZIndexPlane as [zIndex, edges]}
356
- <svg
357
- shape-rendering="crispEdges"
358
- style="z-index:{zIndex};position:absolute;top:0;right:0;bottom:0;left:0;height:100%;width:100%;overflow:visible;"
359
- >
360
- {#each edges as edge}
361
- {@const sourceNode = nodes.get(edge.source)}
362
- {@const targetNode = nodes.get(edge.target)}
363
- {#if sourceNode && targetNode && !(!browser && (sourceNode.clientOnly || targetNode.clientOnly))}
364
- {@render (edge.snippet ? edge.snippet : defaultEdge)(
365
- edge,
366
- generateEdgePath(edge),
367
- edge.snippetExtraArg,
368
- )}
369
- {/if}
370
- {/each}
371
- </svg>
372
- {/each}
373
-
374
- {#key dimensions}
375
- {@render children()}
376
- {/key}
377
- </figure>
@@ -1,48 +0,0 @@
1
- import { type Snippet } from "svelte";
2
- import { SvelteMap } from "svelte/reactivity";
3
- import { type Vector2 } from "./diagram-lib.js";
4
- import type { HTMLAttributes } from "svelte/elements";
5
- export interface DiagramNodeDef {
6
- id: string;
7
- x: number;
8
- y: number;
9
- width?: number;
10
- height?: number;
11
- clientOnly?: boolean;
12
- }
13
- type PathGenParams = {
14
- pathGen?: "bezier";
15
- curvature?: number;
16
- offset?: Vector2;
17
- } | {
18
- pathGen: "smoothstep";
19
- borderRadius?: number;
20
- center?: Vector2;
21
- };
22
- export type DiagramEdgeParams = {
23
- snippet?: Snippet<[edge: DiagramEdgeDef, path: string, extra: any]>;
24
- snippetExtraArg?: any;
25
- sourceAnchor?: Vector2;
26
- targetAnchor?: Vector2;
27
- class?: string;
28
- style?: string;
29
- zIndex?: number;
30
- } & PathGenParams;
31
- export type DiagramEdgeDef = {
32
- source: string;
33
- target: string;
34
- } & DiagramEdgeParams;
35
- export type DiagramProps = {
36
- nodes: SvelteMap<string, DiagramNodeDef>;
37
- edges: SvelteMap<string, DiagramEdgeDef>;
38
- children: Snippet;
39
- scaleToFit?: boolean;
40
- width?: number;
41
- height?: number;
42
- figureAttributes: HTMLAttributes<HTMLElement>;
43
- };
44
- declare const Diagram: import("svelte").Component<DiagramProps, {
45
- generateCurvePath: (x1: number, y1: number, x2: number, y2: number, edge: DiagramEdgeDef) => string;
46
- }, "">;
47
- type Diagram = ReturnType<typeof Diagram>;
48
- export default Diagram;
@@ -1,123 +0,0 @@
1
- <script lang="ts">
2
- import { SvelteMap } from "svelte/reactivity";
3
- import Diagram, {
4
- type DiagramNodeDef,
5
- type DiagramEdgeDef,
6
- } from "./Diagram.svelte";
7
- import { onMount, setContext, type Snippet } from "svelte";
8
- import type { HTMLAttributes } from "svelte/elements";
9
- import PrerenderDiagram from "./PrerenderDiagram.svelte";
10
-
11
- type PassthroughDiagramControllerProps = ({
12
- eagerLoad?: boolean;
13
- rootMargin?: string;
14
- figureAttributes?: HTMLAttributes<HTMLElement>;
15
- } & (
16
- | {
17
- scaleToFit?: boolean;
18
- width?: never;
19
- height?: never;
20
- }
21
- | {
22
- scaleToFit?: never;
23
- width: number;
24
- height: number;
25
- }
26
- )) &
27
- Omit<HTMLAttributes<HTMLDivElement>, "width" | "height">;
28
-
29
- let {
30
- children,
31
- eagerLoad = false,
32
- scaleToFit,
33
- width,
34
- height,
35
- rootMargin = "100px", // start a bit before it enters the viewport
36
- figureAttributes = { inert: true, "aria-hidden": true },
37
- ...rest
38
- }: PassthroughDiagramControllerProps & { children: Snippet } = $props();
39
-
40
- const nodes = new SvelteMap<string, DiagramNodeDef>();
41
- const layers = new SvelteMap<number, Record<string, DiagramNodeDef>>();
42
- setContext("layerNodeMap", () => layers);
43
- const edges = new SvelteMap<string, DiagramEdgeDef>();
44
-
45
- let containerEl: HTMLDivElement | undefined = $state();
46
-
47
- // If we're SSR (not browser), or eagerLoad is false, render immediately.
48
- // Otherwise wait until after load + idle + intersection.
49
- let shouldRender = $derived(!eagerLoad);
50
-
51
- const initialTime = performance.now();
52
-
53
- onMount(() => {
54
- if (!eagerLoad || !containerEl) return;
55
-
56
- let io: IntersectionObserver | null = null;
57
-
58
- const idle = (fn: () => void) => {
59
- const ric = (window as any).requestIdleCallback as
60
- | ((cb: () => void) => number)
61
- | undefined;
62
- if (ric) ric(fn);
63
- else setTimeout(fn, 0);
64
- };
65
-
66
- const startObserving = () => {
67
- // In case the element is already visible at this moment
68
- io = new IntersectionObserver(
69
- (entries) => {
70
- if (entries.some((e) => e.isIntersecting)) {
71
- shouldRender = true;
72
- io?.disconnect();
73
- io = null;
74
- }
75
- },
76
- { root: null, rootMargin, threshold: 0 },
77
- );
78
- io.observe(containerEl!);
79
- };
80
-
81
- if (document.readyState === "complete") {
82
- idle(startObserving);
83
- } else {
84
- // Wait until the whole document (including images) has loaded
85
- window.addEventListener("load", () => idle(startObserving), {
86
- once: true,
87
- });
88
- }
89
-
90
- return () => {
91
- io?.disconnect();
92
- io = null;
93
- };
94
- });
95
- </script>
96
-
97
- {#if shouldRender}
98
- <!-- first pass: register all the nodes and edges -->
99
- <PrerenderDiagram {nodes} {edges} {children} {figureAttributes} />
100
-
101
- <!-- second pass: render with computed positions -->
102
- <div {...rest}>
103
- <Diagram
104
- {nodes}
105
- {edges}
106
- {scaleToFit}
107
- {width}
108
- {height}
109
- {figureAttributes}
110
- >
111
- {@render children()}
112
- </Diagram>
113
- </div>
114
- {:else}
115
- <!-- Lightweight placeholder / container used for intersection observation. -->
116
- <div bind:this={containerEl} {...rest} style="min-height: 1px;"></div>
117
- {/if}
118
-
119
- <!--
120
- <svelte:boundary>
121
- {@const _ = console.log('Finished rendering diagram in', performance.now() - initialTime, 'ms')}
122
- </svelte:boundary>
123
- -->
@@ -1,21 +0,0 @@
1
- import { type Snippet } from "svelte";
2
- import type { HTMLAttributes } from "svelte/elements";
3
- type PassthroughDiagramControllerProps = ({
4
- eagerLoad?: boolean;
5
- rootMargin?: string;
6
- figureAttributes?: HTMLAttributes<HTMLElement>;
7
- } & ({
8
- scaleToFit?: boolean;
9
- width?: never;
10
- height?: never;
11
- } | {
12
- scaleToFit?: never;
13
- width: number;
14
- height: number;
15
- })) & Omit<HTMLAttributes<HTMLDivElement>, "width" | "height">;
16
- type $$ComponentProps = PassthroughDiagramControllerProps & {
17
- children: Snippet;
18
- };
19
- declare const DiagramController: import("svelte").Component<$$ComponentProps, {}, "">;
20
- type DiagramController = ReturnType<typeof DiagramController>;
21
- export default DiagramController;
@@ -1,191 +0,0 @@
1
- <script lang="ts">
2
- import { getContext, onMount, tick, type Snippet } from "svelte";
3
- import type {
4
- DiagramNodeDef,
5
- DiagramEdgeParams,
6
- DiagramEdgeDef,
7
- } from "./Diagram.svelte";
8
- import type { SvelteMap } from "svelte/reactivity";
9
- import type { HTMLAttributes } from "svelte/elements";
10
- import { vector2, type Vector2 } from "./diagram-lib.js";
11
-
12
- type IndividualConnectActionParam =
13
- | string
14
- | (DiagramEdgeParams & { target: string })
15
- | (DiagramEdgeParams & { source: string });
16
-
17
- type DiagramNodeConnectParam =
18
- | IndividualConnectActionParam
19
- | IndividualConnectActionParam[];
20
-
21
- export type DiagramNodeProps = {
22
- children?: Snippet;
23
- connect?: DiagramNodeConnectParam;
24
- autosize?: boolean;
25
- origin?: Vector2;
26
- } & Omit<DiagramNodeDef, "snippet"> &
27
- HTMLAttributes<HTMLDivElement>;
28
-
29
- let {
30
- children,
31
- connect,
32
- // connectSource: connectFrom,
33
- id,
34
- x,
35
- y,
36
- width,
37
- height,
38
- autosize,
39
- clientOnly,
40
- origin,
41
- class: className,
42
- style: inlineStyles,
43
- ...rest
44
- }: DiagramNodeProps = $props();
45
-
46
- const nodeMap = (
47
- getContext("nodeMap") as () => SvelteMap<string, DiagramNodeDef>
48
- )();
49
- const edgeMap = (
50
- getContext("edgeMap") as () => SvelteMap<string, DiagramEdgeDef>
51
- )();
52
-
53
- let dimensions = (
54
- getContext("dimensions") as () =>
55
- | { min: Vector2; max: Vector2 }
56
- | undefined
57
- )();
58
-
59
- if (!origin && (width || height) && !autosize) {
60
- origin = vector2(0.5, 0.5);
61
- }
62
-
63
- let absolutePosition = $derived({
64
- x: x - (origin?.x ?? 0.5) * (width ?? 0),
65
- y: y - (origin?.y ?? 0.5) * (height ?? 0),
66
- });
67
-
68
- const nodeDef: DiagramNodeDef = $derived({
69
- id,
70
- x: absolutePosition.x,
71
- y: absolutePosition.y,
72
- width,
73
- height,
74
- clientOnly: clientOnly || autosize,
75
- snippet: children,
76
- });
77
-
78
- let mounted = $state(false);
79
- const previousEdgeIds = new Set();
80
-
81
- // TODO: this should be done only if clientWidth is needed
82
- let clientWidth: number = $state(0);
83
- let clientHeight: number = $state(0);
84
-
85
- // if (origin) {
86
- // nodeDef.x -= (origin?.x ?? 0) * $state.snapshot(nodeDef.width ?? 0);
87
- // nodeDef.y -= (origin?.y ?? 0) * $state.snapshot(nodeDef.height ?? 0);
88
- // }
89
-
90
- onMount(async () => {
91
- if (autosize) {
92
- width = clientWidth;
93
- height = clientHeight;
94
- }
95
-
96
- if (nodeDef.clientOnly) {
97
- await tick();
98
- mounted = true;
99
- }
100
- });
101
-
102
- function updateEdge(
103
- param: IndividualConnectActionParam,
104
- index: number = 0,
105
- ) {
106
- const selfId = nodeDef.id;
107
- const getEdgeId = ({
108
- source,
109
- target,
110
- index,
111
- }: {
112
- source: string;
113
- target: string;
114
- index: number;
115
- }) => `${source}:${target}:(${index})`;
116
-
117
- if (typeof param == "string") {
118
- const target = param;
119
-
120
- const edgeId = getEdgeId({ source: selfId, target, index });
121
- previousEdgeIds.add(edgeId);
122
-
123
- edgeMap.set(edgeId, { source: nodeDef.id, target });
124
- } else if ("target" in param) {
125
- // const edgeId = getEdgeId(param.target, index);
126
- const edgeId = getEdgeId({
127
- source: selfId,
128
- target: param.target,
129
- index,
130
- });
131
- previousEdgeIds.add(edgeId);
132
- (param as DiagramEdgeDef).source = nodeDef.id;
133
- edgeMap.set(edgeId, param as DiagramEdgeDef);
134
- } else if ("source" in param) {
135
- const edgeId = getEdgeId({
136
- source: param.source,
137
- target: selfId,
138
- index,
139
- });
140
- previousEdgeIds.add(edgeId);
141
- (param as DiagramEdgeDef).target = selfId;
142
- edgeMap.set(edgeId, param as DiagramEdgeDef);
143
- }
144
- }
145
-
146
- if (connect) {
147
- if (!Array.isArray(connect)) {
148
- updateEdge(connect);
149
- } else {
150
- connect.forEach(updateEdge);
151
- }
152
- }
153
-
154
- let left = $derived(nodeDef.x - (dimensions?.min.x ?? 0));
155
- let top = $derived(nodeDef.y - (dimensions?.min.y ?? 0));
156
-
157
- nodeMap.set(nodeDef.id, nodeDef);
158
- $effect(() => {
159
- nodeMap.set(nodeDef.id, nodeDef);
160
- });
161
-
162
- // $inspect(
163
- // 'mounted render diagramNode',
164
- // nodeDef.id,
165
- // left,
166
- // top,
167
- // nodeDef.width,
168
- // nodeDef.height,
169
- // dimensions
170
- // );
171
- </script>
172
-
173
- <div
174
- class={className?.toString() || ""}
175
- style={`position:absolute;
176
- ${!origin ? "transform:translate(-50%, -50%)" : ""};
177
- top:${top}px;left:${left}px;${nodeDef.clientOnly && !mounted ? "opacity:0" : ""} ${inlineStyles || ""}`}
178
- style:width={nodeDef.width ? nodeDef.width + "px" : "auto"}
179
- style:height={nodeDef.height ? nodeDef.height + "px" : "auto"}
180
- {...rest}
181
- >
182
- {#if autosize}
183
- <div
184
- class="nodal-autosize"
185
- style="position:absolute;top:0;right:0;left:0;bottom:0;z-index:-50;"
186
- bind:clientWidth
187
- bind:clientHeight
188
- ></div>
189
- {/if}
190
- {@render children?.()}
191
- </div>
@@ -1,19 +0,0 @@
1
- import { type Snippet } from "svelte";
2
- import type { DiagramNodeDef, DiagramEdgeParams } from "./Diagram.svelte";
3
- import type { HTMLAttributes } from "svelte/elements";
4
- import { type Vector2 } from "./diagram-lib.js";
5
- type IndividualConnectActionParam = string | (DiagramEdgeParams & {
6
- target: string;
7
- }) | (DiagramEdgeParams & {
8
- source: string;
9
- });
10
- type DiagramNodeConnectParam = IndividualConnectActionParam | IndividualConnectActionParam[];
11
- export type DiagramNodeProps = {
12
- children?: Snippet;
13
- connect?: DiagramNodeConnectParam;
14
- autosize?: boolean;
15
- origin?: Vector2;
16
- } & Omit<DiagramNodeDef, "snippet"> & HTMLAttributes<HTMLDivElement>;
17
- declare const DiagramNode: import("svelte").Component<DiagramNodeProps, {}, "">;
18
- type DiagramNode = ReturnType<typeof DiagramNode>;
19
- export default DiagramNode;
@@ -1,23 +0,0 @@
1
- <script lang="ts">
2
- import { setContext } from "svelte";
3
- import type { DiagramProps } from "./Diagram.svelte";
4
- import { vector2 } from "./diagram-lib.js";
5
-
6
- let { nodes, edges, children }: DiagramProps = $props();
7
-
8
- setContext("nodeMap", () => nodes);
9
- setContext("edgeMap", () => edges);
10
- setContext("dimensions", () => ({
11
- min: vector2(0, 0),
12
- max: vector2(0, 0),
13
- }));
14
- setContext("prerendering", true);
15
- </script>
16
-
17
- <template>
18
- {@render children()}
19
- </template>
20
-
21
- <svelte:boundary>
22
- {@const _ = setContext("prerendering", false)}
23
- </svelte:boundary>
@@ -1,4 +0,0 @@
1
- import type { DiagramProps } from "./Diagram.svelte";
2
- declare const PrerenderDiagram: import("svelte").Component<DiagramProps, {}, "">;
3
- type PrerenderDiagram = ReturnType<typeof PrerenderDiagram>;
4
- export default PrerenderDiagram;