@cnvx/nodal 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BaseEdge.svelte +22 -0
- package/dist/BaseEdge.svelte.d.ts +8 -0
- package/dist/Surface.svelte +231 -0
- package/dist/Surface.svelte.d.ts +39 -0
- package/dist/diagram-lib.d.ts +0 -3
- package/dist/diagram-lib.js +0 -11
- package/dist/index.d.ts +1 -6
- package/dist/index.js +1 -4
- package/dist/svelte-actions.d.ts +13 -0
- package/dist/svelte-actions.js +75 -0
- package/package.json +1 -1
- package/dist/Diagram.svelte +0 -377
- package/dist/Diagram.svelte.d.ts +0 -48
- package/dist/DiagramController.svelte +0 -125
- package/dist/DiagramController.svelte.d.ts +0 -21
- package/dist/DiagramNode.svelte +0 -191
- package/dist/DiagramNode.svelte.d.ts +0 -19
- package/dist/PrerenderDiagram.svelte +0 -23
- package/dist/PrerenderDiagram.svelte.d.ts +0 -4
|
@@ -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;
|
package/dist/diagram-lib.d.ts
CHANGED
|
@@ -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;
|
package/dist/diagram-lib.js
CHANGED
|
@@ -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, type PassthroughDiagramControllerProps } 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
|
|
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, }?: Partial<{
|
|
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
package/dist/Diagram.svelte
DELETED
|
@@ -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>
|
package/dist/Diagram.svelte.d.ts
DELETED
|
@@ -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,125 +0,0 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
2
|
-
export type PassthroughDiagramControllerProps = ({
|
|
3
|
-
eagerLoad?: boolean;
|
|
4
|
-
rootMargin?: string;
|
|
5
|
-
figureAttributes?: HTMLAttributes<HTMLElement>;
|
|
6
|
-
} & (
|
|
7
|
-
| {
|
|
8
|
-
scaleToFit?: boolean;
|
|
9
|
-
width?: never;
|
|
10
|
-
height?: never;
|
|
11
|
-
}
|
|
12
|
-
| {
|
|
13
|
-
scaleToFit?: never;
|
|
14
|
-
width: number;
|
|
15
|
-
height: number;
|
|
16
|
-
}
|
|
17
|
-
)) &
|
|
18
|
-
Omit<HTMLAttributes<HTMLDivElement>, "width" | "height">;
|
|
19
|
-
</script>
|
|
20
|
-
|
|
21
|
-
<script lang="ts">
|
|
22
|
-
import { SvelteMap } from "svelte/reactivity";
|
|
23
|
-
import Diagram, {
|
|
24
|
-
type DiagramNodeDef,
|
|
25
|
-
type DiagramEdgeDef,
|
|
26
|
-
} from "./Diagram.svelte";
|
|
27
|
-
import { onMount, setContext, type Snippet } from "svelte";
|
|
28
|
-
import type { HTMLAttributes } from "svelte/elements";
|
|
29
|
-
import PrerenderDiagram from "./PrerenderDiagram.svelte";
|
|
30
|
-
|
|
31
|
-
let {
|
|
32
|
-
children,
|
|
33
|
-
eagerLoad = false,
|
|
34
|
-
scaleToFit,
|
|
35
|
-
width,
|
|
36
|
-
height,
|
|
37
|
-
rootMargin = "100px", // start a bit before it enters the viewport
|
|
38
|
-
figureAttributes = { inert: true, "aria-hidden": true },
|
|
39
|
-
...rest
|
|
40
|
-
}: PassthroughDiagramControllerProps & { children: Snippet } = $props();
|
|
41
|
-
|
|
42
|
-
const nodes = new SvelteMap<string, DiagramNodeDef>();
|
|
43
|
-
const layers = new SvelteMap<number, Record<string, DiagramNodeDef>>();
|
|
44
|
-
setContext("layerNodeMap", () => layers);
|
|
45
|
-
const edges = new SvelteMap<string, DiagramEdgeDef>();
|
|
46
|
-
|
|
47
|
-
let containerEl: HTMLDivElement | undefined = $state();
|
|
48
|
-
|
|
49
|
-
// If we're SSR (not browser), or eagerLoad is false, render immediately.
|
|
50
|
-
// Otherwise wait until after load + idle + intersection.
|
|
51
|
-
let shouldRender = $derived(!eagerLoad);
|
|
52
|
-
|
|
53
|
-
const initialTime = performance.now();
|
|
54
|
-
|
|
55
|
-
onMount(() => {
|
|
56
|
-
if (!eagerLoad || !containerEl) return;
|
|
57
|
-
|
|
58
|
-
let io: IntersectionObserver | null = null;
|
|
59
|
-
|
|
60
|
-
const idle = (fn: () => void) => {
|
|
61
|
-
const ric = (window as any).requestIdleCallback as
|
|
62
|
-
| ((cb: () => void) => number)
|
|
63
|
-
| undefined;
|
|
64
|
-
if (ric) ric(fn);
|
|
65
|
-
else setTimeout(fn, 0);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const startObserving = () => {
|
|
69
|
-
// In case the element is already visible at this moment
|
|
70
|
-
io = new IntersectionObserver(
|
|
71
|
-
(entries) => {
|
|
72
|
-
if (entries.some((e) => e.isIntersecting)) {
|
|
73
|
-
shouldRender = true;
|
|
74
|
-
io?.disconnect();
|
|
75
|
-
io = null;
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
|
-
{ root: null, rootMargin, threshold: 0 },
|
|
79
|
-
);
|
|
80
|
-
io.observe(containerEl!);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
if (document.readyState === "complete") {
|
|
84
|
-
idle(startObserving);
|
|
85
|
-
} else {
|
|
86
|
-
// Wait until the whole document (including images) has loaded
|
|
87
|
-
window.addEventListener("load", () => idle(startObserving), {
|
|
88
|
-
once: true,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return () => {
|
|
93
|
-
io?.disconnect();
|
|
94
|
-
io = null;
|
|
95
|
-
};
|
|
96
|
-
});
|
|
97
|
-
</script>
|
|
98
|
-
|
|
99
|
-
{#if shouldRender}
|
|
100
|
-
<!-- first pass: register all the nodes and edges -->
|
|
101
|
-
<PrerenderDiagram {nodes} {edges} {children} {figureAttributes} />
|
|
102
|
-
|
|
103
|
-
<!-- second pass: render with computed positions -->
|
|
104
|
-
<div {...rest}>
|
|
105
|
-
<Diagram
|
|
106
|
-
{nodes}
|
|
107
|
-
{edges}
|
|
108
|
-
{scaleToFit}
|
|
109
|
-
{width}
|
|
110
|
-
{height}
|
|
111
|
-
{figureAttributes}
|
|
112
|
-
>
|
|
113
|
-
{@render children()}
|
|
114
|
-
</Diagram>
|
|
115
|
-
</div>
|
|
116
|
-
{:else}
|
|
117
|
-
<!-- Lightweight placeholder / container used for intersection observation. -->
|
|
118
|
-
<div bind:this={containerEl} {...rest} style="min-height: 1px;"></div>
|
|
119
|
-
{/if}
|
|
120
|
-
|
|
121
|
-
<!--
|
|
122
|
-
<svelte:boundary>
|
|
123
|
-
{@const _ = console.log('Finished rendering diagram in', performance.now() - initialTime, 'ms')}
|
|
124
|
-
</svelte:boundary>
|
|
125
|
-
-->
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
export type PassthroughDiagramControllerProps = ({
|
|
2
|
-
eagerLoad?: boolean;
|
|
3
|
-
rootMargin?: string;
|
|
4
|
-
figureAttributes?: HTMLAttributes<HTMLElement>;
|
|
5
|
-
} & ({
|
|
6
|
-
scaleToFit?: boolean;
|
|
7
|
-
width?: never;
|
|
8
|
-
height?: never;
|
|
9
|
-
} | {
|
|
10
|
-
scaleToFit?: never;
|
|
11
|
-
width: number;
|
|
12
|
-
height: number;
|
|
13
|
-
})) & Omit<HTMLAttributes<HTMLDivElement>, "width" | "height">;
|
|
14
|
-
import { type Snippet } from "svelte";
|
|
15
|
-
import type { HTMLAttributes } from "svelte/elements";
|
|
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;
|
package/dist/DiagramNode.svelte
DELETED
|
@@ -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>
|