@cnvx/nodal 0.0.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/README.md +58 -0
- package/dist/Diagram.svelte +324 -0
- package/dist/Diagram.svelte.d.ts +43 -0
- package/dist/DiagramController.svelte +42 -0
- package/dist/DiagramController.svelte.d.ts +8 -0
- package/dist/DiagramNode.svelte +231 -0
- package/dist/DiagramNode.svelte.d.ts +22 -0
- package/dist/PrerenderDiagram.svelte +18 -0
- package/dist/PrerenderDiagram.svelte.d.ts +4 -0
- package/dist/diagram-lib.d.ts +203 -0
- package/dist/diagram-lib.js +356 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Svelte library
|
|
2
|
+
|
|
3
|
+
Everything you need to build a Svelte library, powered by [`sv`](https://npmjs.com/package/sv).
|
|
4
|
+
|
|
5
|
+
Read more about creating a library [in the docs](https://svelte.dev/docs/kit/packaging).
|
|
6
|
+
|
|
7
|
+
## Creating a project
|
|
8
|
+
|
|
9
|
+
If you're seeing this, you've probably already done this step. Congrats!
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# create a new project in the current directory
|
|
13
|
+
npx sv create
|
|
14
|
+
|
|
15
|
+
# create a new project in my-app
|
|
16
|
+
npx sv create my-app
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Developing
|
|
20
|
+
|
|
21
|
+
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm run dev
|
|
25
|
+
|
|
26
|
+
# or start the server and open the app in a new browser tab
|
|
27
|
+
npm run dev -- --open
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Everything inside `src/lib` is part of your library, everything inside `src/routes` can be used as a showcase or preview app.
|
|
31
|
+
|
|
32
|
+
## Building
|
|
33
|
+
|
|
34
|
+
To build your library:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm run package
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
To create a production version of your showcase app:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm run build
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
You can preview the production build with `npm run preview`.
|
|
47
|
+
|
|
48
|
+
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
|
49
|
+
|
|
50
|
+
## Publishing
|
|
51
|
+
|
|
52
|
+
Go into the `package.json` and give your package the desired name through the `"name"` option. Also consider adding a `"license"` field and point it to a `LICENSE` file which you can create from a template (one popular option is the [MIT license](https://opensource.org/license/mit/)).
|
|
53
|
+
|
|
54
|
+
To publish your library to [npm](https://www.npmjs.com):
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm publish
|
|
58
|
+
```
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import { browser, dev } from "$app/environment";
|
|
3
|
+
import { onMount, setContext, type Snippet } from "svelte";
|
|
4
|
+
import { SvelteMap } from "svelte/reactivity";
|
|
5
|
+
import {
|
|
6
|
+
debugSide,
|
|
7
|
+
eq,
|
|
8
|
+
getBezierPath,
|
|
9
|
+
getSmoothStepPath,
|
|
10
|
+
normaliseAngle,
|
|
11
|
+
Side,
|
|
12
|
+
sideForAngle,
|
|
13
|
+
unitVectorFromAngle,
|
|
14
|
+
vector2,
|
|
15
|
+
type Vector2,
|
|
16
|
+
Anchor,
|
|
17
|
+
} from "./diagram-lib";
|
|
18
|
+
import { draw } from "svelte/transition";
|
|
19
|
+
|
|
20
|
+
export interface DiagramNode {
|
|
21
|
+
id: string;
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
width?: number;
|
|
25
|
+
height?: number;
|
|
26
|
+
|
|
27
|
+
// this will make the node and all of its connections only client side
|
|
28
|
+
clientOnly?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type PathGenParams =
|
|
32
|
+
| {
|
|
33
|
+
pathGen?: "bezier";
|
|
34
|
+
curvature?: number;
|
|
35
|
+
offset?: Vector2;
|
|
36
|
+
}
|
|
37
|
+
| {
|
|
38
|
+
pathGen: "smoothstep";
|
|
39
|
+
borderRadius?: number;
|
|
40
|
+
center?: Vector2;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type DiagramEdgeParams = {
|
|
44
|
+
// target: string;
|
|
45
|
+
snippet?: Snippet<[edge: DiagramEdge, path: string, extra: any]>;
|
|
46
|
+
snippetExtraArg?: any;
|
|
47
|
+
|
|
48
|
+
sourceAnchor?: Vector2;
|
|
49
|
+
targetAnchor?: Vector2;
|
|
50
|
+
|
|
51
|
+
class?: string;
|
|
52
|
+
style?: string;
|
|
53
|
+
zIndex?: number;
|
|
54
|
+
} & PathGenParams;
|
|
55
|
+
|
|
56
|
+
export type DiagramEdge = {
|
|
57
|
+
source: string;
|
|
58
|
+
target: string;
|
|
59
|
+
} & DiagramEdgeParams;
|
|
60
|
+
|
|
61
|
+
export type DiagramProps = {
|
|
62
|
+
nodes: SvelteMap<string, DiagramNode>;
|
|
63
|
+
edges: SvelteMap<string, DiagramEdge>;
|
|
64
|
+
children: Snippet;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// const getNodeOrigin = (node: DiagramNode) => node.origin ?? vector2(0.0, 0.5);
|
|
68
|
+
// export const getNodeOrigin = (node: DiagramNode) => node.origin ?? vector2(0.5, 0.5);
|
|
69
|
+
// export const getNodeOrigin = (node: DiagramNode) => vector2(0.0, 0.0);
|
|
70
|
+
|
|
71
|
+
const getNodeSize = (node: DiagramNode) => ({
|
|
72
|
+
x: node.width ?? 0,
|
|
73
|
+
y: node.height ?? 0,
|
|
74
|
+
});
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<script lang="ts">
|
|
78
|
+
let { nodes, edges, children }: DiagramProps = $props();
|
|
79
|
+
export function generateCurvePath(
|
|
80
|
+
x1: number,
|
|
81
|
+
y1: number,
|
|
82
|
+
x2: number,
|
|
83
|
+
y2: number,
|
|
84
|
+
edge: DiagramEdge,
|
|
85
|
+
): string {
|
|
86
|
+
const {
|
|
87
|
+
sourceAnchor: source = Anchor.CENTER_CENTER,
|
|
88
|
+
targetAnchor: dest = Anchor.CENTER_CENTER,
|
|
89
|
+
} = edge;
|
|
90
|
+
|
|
91
|
+
function anchorToRads({ x, y }: Vector2) {
|
|
92
|
+
// vector from the node’s centre (0.5, 0.5)
|
|
93
|
+
const dx = x - 0.5; // + right
|
|
94
|
+
const dy = 0.5 - y; // + up (flip screen-y for math coords)
|
|
95
|
+
const raw = Math.atan2(dy, dx); // signed angle
|
|
96
|
+
return normaliseAngle(raw);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// CRUCIAL TO INVEST THE Y DIRECTION SINCE IT GOES FROM
|
|
100
|
+
// NEGATIVE TO POSITIVE!!!!!!!!!!!!!
|
|
101
|
+
const leaveAngle = Math.atan2(y1 - y2, x2 - x1);
|
|
102
|
+
const arriveAngle = leaveAngle + Math.PI;
|
|
103
|
+
|
|
104
|
+
const sourcePosition = eq(source, Anchor.CENTER_CENTER)
|
|
105
|
+
? sideForAngle(leaveAngle)
|
|
106
|
+
: sideForAngle(anchorToRads(source));
|
|
107
|
+
|
|
108
|
+
const targetPosition = eq(dest, Anchor.CENTER_CENTER)
|
|
109
|
+
? sideForAngle(arriveAngle)
|
|
110
|
+
: sideForAngle(anchorToRads(dest));
|
|
111
|
+
|
|
112
|
+
const props = {
|
|
113
|
+
sourceX: x1,
|
|
114
|
+
sourceY: y1,
|
|
115
|
+
sourcePosition: sourcePosition,
|
|
116
|
+
|
|
117
|
+
targetX: x2,
|
|
118
|
+
targetY: y2,
|
|
119
|
+
targetPosition: targetPosition,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
edge.pathGen ??= "bezier";
|
|
123
|
+
if (edge.pathGen == "bezier") {
|
|
124
|
+
return getBezierPath({
|
|
125
|
+
...props,
|
|
126
|
+
curvature: edge.curvature ?? 0.25,
|
|
127
|
+
})[0];
|
|
128
|
+
} else if (edge.pathGen == "smoothstep") {
|
|
129
|
+
const pathgenParams = {
|
|
130
|
+
borderRadius: edge?.borderRadius ?? 15,
|
|
131
|
+
center: edge?.center,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// calculate the absolute center from the relative center
|
|
135
|
+
let centerX = undefined,
|
|
136
|
+
centerY = undefined;
|
|
137
|
+
|
|
138
|
+
if (pathgenParams.center) {
|
|
139
|
+
centerX = x1 + pathgenParams.center.x * (x2 - x1);
|
|
140
|
+
centerY = y1 + pathgenParams.center.y * (y2 - y1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return getSmoothStepPath({
|
|
144
|
+
...props,
|
|
145
|
+
borderRadius: pathgenParams.borderRadius,
|
|
146
|
+
centerX,
|
|
147
|
+
centerY,
|
|
148
|
+
})[0];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
throw new Error("unreachable");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function calculateDimensions(_nodes: typeof nodes) {
|
|
155
|
+
console.time("dim");
|
|
156
|
+
let newMin = vector2(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
|
|
157
|
+
let newMax = vector2(Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER);
|
|
158
|
+
|
|
159
|
+
for (let node of _nodes.values()) {
|
|
160
|
+
// if (!browser && node.clientOnly) continue;
|
|
161
|
+
|
|
162
|
+
const size = getNodeSize(node);
|
|
163
|
+
// const origin = getNodeOrigin(node);
|
|
164
|
+
|
|
165
|
+
const left = node.x;
|
|
166
|
+
const top = node.y;
|
|
167
|
+
|
|
168
|
+
const right = left + size.x;
|
|
169
|
+
const bottom = top + size.y;
|
|
170
|
+
|
|
171
|
+
newMin = vector2(Math.min(newMin.x, left), Math.min(newMin.y, top));
|
|
172
|
+
newMax = vector2(
|
|
173
|
+
Math.max(newMax.x, right),
|
|
174
|
+
Math.max(newMax.y, bottom),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.timeEnd("dim");
|
|
179
|
+
return { min: newMin, max: newMax };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// const dimensions = $derived(calculateDimensions(nodes));
|
|
183
|
+
// let dimensions = $derived(calculateDimensions(nodes));
|
|
184
|
+
let dimensions = $state(calculateDimensions(nodes));
|
|
185
|
+
$effect(() => {
|
|
186
|
+
const newDimensions = calculateDimensions(nodes);
|
|
187
|
+
dimensions.min = newDimensions.min;
|
|
188
|
+
dimensions.max = newDimensions.max;
|
|
189
|
+
});
|
|
190
|
+
// let dimensions = calculateDimensions(nodes);
|
|
191
|
+
// onMount(() => (dimensions = calculateDimensions(nodes)));
|
|
192
|
+
|
|
193
|
+
setContext("nodeMap", () => nodes);
|
|
194
|
+
setContext("edgeMap", () => edges);
|
|
195
|
+
setContext("dimensions", () => dimensions);
|
|
196
|
+
|
|
197
|
+
let width = $derived(Math.max(dimensions.max.x - dimensions.min.x, 1));
|
|
198
|
+
let height = $derived(Math.max(dimensions.max.y - dimensions.min.y, 1));
|
|
199
|
+
|
|
200
|
+
function generateEdgePath(edge: DiagramEdge) {
|
|
201
|
+
const sourceNode = nodes.get(edge.source)!;
|
|
202
|
+
const targetNode = nodes.get(edge.target)!;
|
|
203
|
+
|
|
204
|
+
const sourceAnchor = getNodeAnchor(
|
|
205
|
+
sourceNode,
|
|
206
|
+
edge.sourceAnchor ?? Anchor.CENTER_CENTER,
|
|
207
|
+
);
|
|
208
|
+
const targetAnchor = getNodeAnchor(
|
|
209
|
+
targetNode,
|
|
210
|
+
edge.targetAnchor ?? Anchor.CENTER_CENTER,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
return generateCurvePath(
|
|
214
|
+
sourceAnchor.left,
|
|
215
|
+
sourceAnchor.top,
|
|
216
|
+
targetAnchor.left,
|
|
217
|
+
targetAnchor.top,
|
|
218
|
+
edge,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function getNodeAnchor(node: DiagramNode, anchor: Vector2) {
|
|
223
|
+
const size = getNodeSize(node);
|
|
224
|
+
|
|
225
|
+
// if (!browser && !eq(anchor, Anchor.CENTER_CENTER) && eq(size, vector2(0, 0))) {
|
|
226
|
+
// throw new Error(
|
|
227
|
+
// `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`
|
|
228
|
+
// );
|
|
229
|
+
// }
|
|
230
|
+
|
|
231
|
+
const left = node.x - dimensions.min.x + anchor.x * size.x;
|
|
232
|
+
const top = node.y - dimensions.min.y + anchor.y * size.y;
|
|
233
|
+
|
|
234
|
+
return { left, top };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let edgesByZIndexPlane = $derived(
|
|
238
|
+
edges.values().reduce((acc, edge) => {
|
|
239
|
+
const zIndex = edge.zIndex ?? 0;
|
|
240
|
+
if (!acc.has(zIndex)) {
|
|
241
|
+
acc.set(zIndex, []);
|
|
242
|
+
}
|
|
243
|
+
acc.get(zIndex)!.push(edge);
|
|
244
|
+
return acc;
|
|
245
|
+
}, new Map<number, DiagramEdge[]>()),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
// let depthMap = new SvelteMap<number, [DiagramEdge[], DiagramNode[]]>();
|
|
249
|
+
// $effect(() => {
|
|
250
|
+
// if (nodes || edges) {
|
|
251
|
+
// depthMap.clear();
|
|
252
|
+
// for (const node of nodes.values()) {
|
|
253
|
+
// // depthMap.has(node.zIndex)
|
|
254
|
+
// }
|
|
255
|
+
// }
|
|
256
|
+
// });
|
|
257
|
+
|
|
258
|
+
onMount(() => {
|
|
259
|
+
dimensions = calculateDimensions(nodes);
|
|
260
|
+
});
|
|
261
|
+
</script>
|
|
262
|
+
|
|
263
|
+
{#snippet defaultEdge(edge: DiagramEdge, edgePath: string)}
|
|
264
|
+
<path
|
|
265
|
+
d={edgePath}
|
|
266
|
+
fill="none"
|
|
267
|
+
stroke="currentColor"
|
|
268
|
+
class={edge.class}
|
|
269
|
+
vector-effect="non-scaling-stroke"
|
|
270
|
+
stroke-linecap="round"
|
|
271
|
+
stroke-linejoin="round"
|
|
272
|
+
shape-rendering="smooth"
|
|
273
|
+
style={edge.style}
|
|
274
|
+
/>
|
|
275
|
+
{/snippet}
|
|
276
|
+
|
|
277
|
+
<figure
|
|
278
|
+
aria-label="Diagram"
|
|
279
|
+
aria-hidden={!dev}
|
|
280
|
+
inert={!dev}
|
|
281
|
+
role="img"
|
|
282
|
+
style="position:relative;width:{width}px;height:{height}px;overflow:show;user-select:none;"
|
|
283
|
+
>
|
|
284
|
+
<!-- <svg class="absolute top-0 right-0 bottom-0 left-0 z-0 h-full w-full overflow-visible"> -->
|
|
285
|
+
<!-- {#each edges.values() as edge, i}
|
|
286
|
+
{@const sourceNode = nodes.get(edge.source)}
|
|
287
|
+
{@const targetNode = nodes.get(edge.target)}
|
|
288
|
+
|
|
289
|
+
{#if sourceNode && targetNode && !(!browser && (sourceNode.clientOnly || targetNode.clientOnly))}
|
|
290
|
+
<svg
|
|
291
|
+
class="absolute top-0 right-0 bottom-0 left-0 h-full w-full overflow-visible"
|
|
292
|
+
style="z-index:{edge.zIndex ?? 0};"
|
|
293
|
+
>
|
|
294
|
+
{@render (edge.snippet ? edge.snippet : defaultEdge)(
|
|
295
|
+
edge,
|
|
296
|
+
generateEdgePath(edge),
|
|
297
|
+
edge.snippetExtraArg
|
|
298
|
+
)}
|
|
299
|
+
</svg>
|
|
300
|
+
{/if}
|
|
301
|
+
{/each} -->
|
|
302
|
+
<!-- </svg> -->
|
|
303
|
+
|
|
304
|
+
{#each edgesByZIndexPlane as [zIndex, edges]}
|
|
305
|
+
<svg
|
|
306
|
+
shape-rendering="crispEdges"
|
|
307
|
+
style="z-index:{zIndex};position:absolute;top:0;right:0;bottom:0;left:0;height:100%;width:100%;overflow:visible;"
|
|
308
|
+
>
|
|
309
|
+
{#each edges as edge}
|
|
310
|
+
{@const sourceNode = nodes.get(edge.source)}
|
|
311
|
+
{@const targetNode = nodes.get(edge.target)}
|
|
312
|
+
{#if sourceNode && targetNode && !(!browser && (sourceNode.clientOnly || targetNode.clientOnly))}
|
|
313
|
+
{@render (edge.snippet ? edge.snippet : defaultEdge)(
|
|
314
|
+
edge,
|
|
315
|
+
generateEdgePath(edge),
|
|
316
|
+
edge.snippetExtraArg,
|
|
317
|
+
)}
|
|
318
|
+
{/if}
|
|
319
|
+
{/each}
|
|
320
|
+
</svg>
|
|
321
|
+
{/each}
|
|
322
|
+
|
|
323
|
+
{@render children()}
|
|
324
|
+
</figure>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type Snippet } from "svelte";
|
|
2
|
+
import { SvelteMap } from "svelte/reactivity";
|
|
3
|
+
import { type Vector2 } from "./diagram-lib";
|
|
4
|
+
export interface DiagramNode {
|
|
5
|
+
id: string;
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
width?: number;
|
|
9
|
+
height?: number;
|
|
10
|
+
clientOnly?: boolean;
|
|
11
|
+
}
|
|
12
|
+
type PathGenParams = {
|
|
13
|
+
pathGen?: "bezier";
|
|
14
|
+
curvature?: number;
|
|
15
|
+
offset?: Vector2;
|
|
16
|
+
} | {
|
|
17
|
+
pathGen: "smoothstep";
|
|
18
|
+
borderRadius?: number;
|
|
19
|
+
center?: Vector2;
|
|
20
|
+
};
|
|
21
|
+
export type DiagramEdgeParams = {
|
|
22
|
+
snippet?: Snippet<[edge: DiagramEdge, path: string, extra: any]>;
|
|
23
|
+
snippetExtraArg?: any;
|
|
24
|
+
sourceAnchor?: Vector2;
|
|
25
|
+
targetAnchor?: Vector2;
|
|
26
|
+
class?: string;
|
|
27
|
+
style?: string;
|
|
28
|
+
zIndex?: number;
|
|
29
|
+
} & PathGenParams;
|
|
30
|
+
export type DiagramEdge = {
|
|
31
|
+
source: string;
|
|
32
|
+
target: string;
|
|
33
|
+
} & DiagramEdgeParams;
|
|
34
|
+
export type DiagramProps = {
|
|
35
|
+
nodes: SvelteMap<string, DiagramNode>;
|
|
36
|
+
edges: SvelteMap<string, DiagramEdge>;
|
|
37
|
+
children: Snippet;
|
|
38
|
+
};
|
|
39
|
+
declare const Diagram: import("svelte").Component<DiagramProps, {
|
|
40
|
+
generateCurvePath: (x1: number, y1: number, x2: number, y2: number, edge: DiagramEdge) => string;
|
|
41
|
+
}, "">;
|
|
42
|
+
type Diagram = ReturnType<typeof Diagram>;
|
|
43
|
+
export default Diagram;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { SvelteMap } from 'svelte/reactivity';
|
|
3
|
+
import Diagram, { type DiagramNode, type DiagramEdge } from './Diagram.svelte';
|
|
4
|
+
import { createRawSnippet, flushSync, onMount, setContext, type Snippet } from 'svelte';
|
|
5
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
6
|
+
import PrerenderDiagram from './PrerenderDiagram.svelte';
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
children,
|
|
10
|
+
...rest
|
|
11
|
+
}: {
|
|
12
|
+
children: Snippet;
|
|
13
|
+
} & HTMLAttributes<HTMLDivElement> = $props();
|
|
14
|
+
|
|
15
|
+
const nodes = new SvelteMap<string, DiagramNode>();
|
|
16
|
+
|
|
17
|
+
const edges = new SvelteMap<string, DiagramEdge>();
|
|
18
|
+
|
|
19
|
+
// let initialDiagramContainer: HTMLElement;
|
|
20
|
+
|
|
21
|
+
// onMount(() => {
|
|
22
|
+
// initialDiagramContainer.remove();
|
|
23
|
+
// });
|
|
24
|
+
|
|
25
|
+
const initialTime = performance.now();
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<!-- first time to register all the nodes and edges -->
|
|
29
|
+
<PrerenderDiagram {nodes} {edges} {children} />
|
|
30
|
+
|
|
31
|
+
<!-- second time to actually render it after we calculate all the relative positions and total widths and heights -->
|
|
32
|
+
<!-- this is necessary because we need to calculate the relative positions of the nodes and edges -->
|
|
33
|
+
<!-- TODO: Investigate how to do this properly with hydrate and mount and render https://svelte.dev/docs/svelte/imperative-component-api#hydrate -->
|
|
34
|
+
<div {...rest}>
|
|
35
|
+
<Diagram {nodes} {edges}>
|
|
36
|
+
{@render children()}
|
|
37
|
+
</Diagram>
|
|
38
|
+
</div>
|
|
39
|
+
<!--
|
|
40
|
+
<svelte:boundary>
|
|
41
|
+
{@const _ = console.log('Finished rendering diagram in', performance.now() - initialTime, 'ms')}
|
|
42
|
+
</svelte:boundary> -->
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
children: Snippet;
|
|
5
|
+
} & HTMLAttributes<HTMLDivElement>;
|
|
6
|
+
declare const DiagramController: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type DiagramController = ReturnType<typeof DiagramController>;
|
|
8
|
+
export default DiagramController;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
getContext,
|
|
4
|
+
onMount,
|
|
5
|
+
tick,
|
|
6
|
+
type ComponentProps,
|
|
7
|
+
type Snippet,
|
|
8
|
+
} from "svelte";
|
|
9
|
+
import type {
|
|
10
|
+
DiagramNode,
|
|
11
|
+
DiagramEdgeParams,
|
|
12
|
+
DiagramEdge,
|
|
13
|
+
} from "./Diagram.svelte";
|
|
14
|
+
import type { SvelteMap } from "svelte/reactivity";
|
|
15
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
16
|
+
import { vector2, type Vector2 } from "./diagram-lib";
|
|
17
|
+
|
|
18
|
+
// type IndividualConnectActionParam = string | Omit<DiagramEdge, 'source'>;
|
|
19
|
+
type IndividualConnectActionParam =
|
|
20
|
+
| string
|
|
21
|
+
| (DiagramEdgeParams & { target: string });
|
|
22
|
+
type IndividualConnectSourceActionParam =
|
|
23
|
+
| string
|
|
24
|
+
| (DiagramEdgeParams & { source: string });
|
|
25
|
+
type DiagramNodeConnectParam =
|
|
26
|
+
| IndividualConnectActionParam
|
|
27
|
+
| IndividualConnectActionParam[];
|
|
28
|
+
type DiagramNodeConnectSourceParam =
|
|
29
|
+
| IndividualConnectSourceActionParam
|
|
30
|
+
| IndividualConnectSourceActionParam[];
|
|
31
|
+
|
|
32
|
+
export type DiagramNodeProps = {
|
|
33
|
+
children?: Snippet;
|
|
34
|
+
connect?: DiagramNodeConnectParam;
|
|
35
|
+
connectSource?: DiagramNodeConnectSourceParam;
|
|
36
|
+
autosize?: boolean;
|
|
37
|
+
origin?: Vector2;
|
|
38
|
+
} & Omit<DiagramNode, "snippet"> &
|
|
39
|
+
HTMLAttributes<HTMLDivElement>;
|
|
40
|
+
|
|
41
|
+
let {
|
|
42
|
+
children,
|
|
43
|
+
connect,
|
|
44
|
+
connectSource: connectFrom,
|
|
45
|
+
id,
|
|
46
|
+
x,
|
|
47
|
+
y,
|
|
48
|
+
width,
|
|
49
|
+
height,
|
|
50
|
+
autosize,
|
|
51
|
+
clientOnly,
|
|
52
|
+
origin,
|
|
53
|
+
class: className,
|
|
54
|
+
style: inlineStyles,
|
|
55
|
+
...rest
|
|
56
|
+
}: DiagramNodeProps = $props();
|
|
57
|
+
|
|
58
|
+
const nodeMap = (
|
|
59
|
+
getContext("nodeMap") as () => SvelteMap<string, DiagramNode>
|
|
60
|
+
)();
|
|
61
|
+
const edgeMap = (
|
|
62
|
+
getContext("edgeMap") as () => SvelteMap<string, DiagramEdge>
|
|
63
|
+
)();
|
|
64
|
+
let dimensions = (
|
|
65
|
+
getContext("dimensions") as () =>
|
|
66
|
+
| { min: Vector2; max: Vector2 }
|
|
67
|
+
| undefined
|
|
68
|
+
)();
|
|
69
|
+
|
|
70
|
+
if (!origin && (width || height) && !autosize) {
|
|
71
|
+
origin = vector2(0.5, 0.5);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let absolutePosition = $derived({
|
|
75
|
+
x: x - (origin?.x ?? 0.5) * (width ?? 0),
|
|
76
|
+
y: y - (origin?.y ?? 0.5) * (height ?? 0),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// const nodeDef: DiagramNode = $derived({ id, x, y, width, height, clientOnly, snippet: children });
|
|
80
|
+
const nodeDef: DiagramNode = $derived({
|
|
81
|
+
id,
|
|
82
|
+
x: absolutePosition.x,
|
|
83
|
+
y: absolutePosition.y,
|
|
84
|
+
width,
|
|
85
|
+
height,
|
|
86
|
+
clientOnly: clientOnly || autosize,
|
|
87
|
+
snippet: children,
|
|
88
|
+
});
|
|
89
|
+
const prerender = $state.snapshot(getContext("prerendering"));
|
|
90
|
+
|
|
91
|
+
// console.log('set', nodeDef.id, nodeDef);
|
|
92
|
+
|
|
93
|
+
// const source = elementNodeMap.get(element);
|
|
94
|
+
// if (!source) {
|
|
95
|
+
// return console.warn(`Could not find source element for`, element);
|
|
96
|
+
// }
|
|
97
|
+
|
|
98
|
+
let mounted = $state(false);
|
|
99
|
+
|
|
100
|
+
const previousEdgeIds = new Set();
|
|
101
|
+
|
|
102
|
+
// TODO: this should be done only if clientWidth is needed
|
|
103
|
+
let clientWidth: number = $state(0);
|
|
104
|
+
let clientHeight: number = $state(0);
|
|
105
|
+
|
|
106
|
+
// if (origin) {
|
|
107
|
+
// nodeDef.x -= (origin?.x ?? 0) * $state.snapshot(nodeDef.width ?? 0);
|
|
108
|
+
// nodeDef.y -= (origin?.y ?? 0) * $state.snapshot(nodeDef.height ?? 0);
|
|
109
|
+
// }
|
|
110
|
+
|
|
111
|
+
if (autosize) {
|
|
112
|
+
onMount(() => {
|
|
113
|
+
width = clientWidth;
|
|
114
|
+
height = clientHeight;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
onMount(async () => {
|
|
119
|
+
if (nodeDef.clientOnly) {
|
|
120
|
+
await tick();
|
|
121
|
+
mounted = true;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
function updateEdgeFrom(
|
|
126
|
+
param: IndividualConnectSourceActionParam,
|
|
127
|
+
index: number = 0,
|
|
128
|
+
) {
|
|
129
|
+
const getEdgeId = (source: string, i: number) =>
|
|
130
|
+
`${source}:${nodeDef.id}:(${i})`;
|
|
131
|
+
// const edgeId = getEdgeId();
|
|
132
|
+
if (typeof param == "string") {
|
|
133
|
+
const source = param;
|
|
134
|
+
|
|
135
|
+
const edgeId = getEdgeId(source, index);
|
|
136
|
+
previousEdgeIds.add(edgeId);
|
|
137
|
+
|
|
138
|
+
edgeMap.set(edgeId, { target: nodeDef.id, source });
|
|
139
|
+
} else {
|
|
140
|
+
const edgeId = getEdgeId(param.source, index);
|
|
141
|
+
previousEdgeIds.add(edgeId);
|
|
142
|
+
|
|
143
|
+
(param as DiagramEdge).target = nodeDef.id;
|
|
144
|
+
edgeMap.set(edgeId, param as DiagramEdge);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function updateEdge(
|
|
149
|
+
param: IndividualConnectActionParam,
|
|
150
|
+
index: number = 0,
|
|
151
|
+
) {
|
|
152
|
+
const getEdgeId = (target: string, i: number) =>
|
|
153
|
+
`${nodeDef.id}:${target}:(${i})`;
|
|
154
|
+
// const edgeId = getEdgeId();
|
|
155
|
+
if (typeof param == "string") {
|
|
156
|
+
const target = param;
|
|
157
|
+
|
|
158
|
+
const edgeId = getEdgeId(target, index);
|
|
159
|
+
previousEdgeIds.add(edgeId);
|
|
160
|
+
|
|
161
|
+
edgeMap.set(edgeId, { source: nodeDef.id, target });
|
|
162
|
+
} else {
|
|
163
|
+
const edgeId = getEdgeId(param.target, index);
|
|
164
|
+
previousEdgeIds.add(edgeId);
|
|
165
|
+
|
|
166
|
+
(param as DiagramEdge).source = nodeDef.id;
|
|
167
|
+
edgeMap.set(edgeId, param as DiagramEdge);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (connect) {
|
|
172
|
+
if (!Array.isArray(connect)) {
|
|
173
|
+
updateEdge(connect);
|
|
174
|
+
} else {
|
|
175
|
+
connect.forEach(updateEdge);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (connectFrom) {
|
|
180
|
+
if (!Array.isArray(connectFrom)) {
|
|
181
|
+
updateEdgeFrom(connectFrom);
|
|
182
|
+
} else {
|
|
183
|
+
connectFrom.forEach(updateEdgeFrom);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let left = $derived(nodeDef.x - (dimensions?.min.x ?? 0));
|
|
188
|
+
let top = $derived(nodeDef.y - (dimensions?.min.y ?? 0));
|
|
189
|
+
|
|
190
|
+
$effect(() => {
|
|
191
|
+
nodeMap.set(nodeDef.id, nodeDef);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// nodeMap.set(nodeDef.id, nodeDef);
|
|
195
|
+
$effect(() => {
|
|
196
|
+
// nodeDef.x -= (origin?.x ?? 0.5) * (nodeDef?.width ?? clientWidth ?? 0);
|
|
197
|
+
// nodeDef.y -= (origin?.y ?? 0.5) * (nodeDef?.height ?? clientHeight ?? 0);
|
|
198
|
+
nodeMap.set(nodeDef.id, nodeDef);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// $inspect(
|
|
202
|
+
// 'mounted render diagramNode',
|
|
203
|
+
// nodeDef.id,
|
|
204
|
+
// left,
|
|
205
|
+
// top,
|
|
206
|
+
// nodeDef.width,
|
|
207
|
+
// nodeDef.height,
|
|
208
|
+
// dimensions
|
|
209
|
+
// );
|
|
210
|
+
</script>
|
|
211
|
+
|
|
212
|
+
<div
|
|
213
|
+
class={className?.toString() || ""}
|
|
214
|
+
style={`position:absolute;
|
|
215
|
+
${!origin ? "transform:translate(-50%, -50%)" : ""};
|
|
216
|
+
top:${top}px;left:${left}px;${nodeDef.clientOnly && !mounted ? "opacity:0" : ""} ${inlineStyles || ""}`}
|
|
217
|
+
style:width={nodeDef.width ? nodeDef.width + "px" : "auto"}
|
|
218
|
+
style:height={nodeDef.height ? nodeDef.height + "px" : "auto"}
|
|
219
|
+
{...rest}
|
|
220
|
+
>
|
|
221
|
+
{#if autosize}
|
|
222
|
+
<div
|
|
223
|
+
class="nodal-autosize"
|
|
224
|
+
style="position:absolute;top:0;right:0;left:0;bottom:0;z-index:-50;"
|
|
225
|
+
bind:clientWidth
|
|
226
|
+
bind:clientHeight
|
|
227
|
+
></div>
|
|
228
|
+
{/if}
|
|
229
|
+
<!-- <div class="absolute top-0 left-0 w-full h-full bg-red-500"> {nodeDef.clientOnly} {mounted}</div> -->
|
|
230
|
+
{@render children?.()}
|
|
231
|
+
</div>
|