@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.
@@ -0,0 +1,22 @@
1
+ import { type Snippet } from "svelte";
2
+ import type { DiagramNode, DiagramEdgeParams } from "./Diagram.svelte";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+ import { type Vector2 } from "./diagram-lib";
5
+ type IndividualConnectActionParam = string | (DiagramEdgeParams & {
6
+ target: string;
7
+ });
8
+ type IndividualConnectSourceActionParam = string | (DiagramEdgeParams & {
9
+ source: string;
10
+ });
11
+ type DiagramNodeConnectParam = IndividualConnectActionParam | IndividualConnectActionParam[];
12
+ type DiagramNodeConnectSourceParam = IndividualConnectSourceActionParam | IndividualConnectSourceActionParam[];
13
+ export type DiagramNodeProps = {
14
+ children?: Snippet;
15
+ connect?: DiagramNodeConnectParam;
16
+ connectSource?: DiagramNodeConnectSourceParam;
17
+ autosize?: boolean;
18
+ origin?: Vector2;
19
+ } & Omit<DiagramNode, "snippet"> & HTMLAttributes<HTMLDivElement>;
20
+ declare const DiagramNode: any;
21
+ type DiagramNode = ReturnType<typeof DiagramNode>;
22
+ export default DiagramNode;
@@ -0,0 +1,18 @@
1
+ <script lang="ts">
2
+ import { setContext } from 'svelte';
3
+ import type { DiagramProps } from './Diagram.svelte';
4
+ import { vector2 } from './diagram-lib';
5
+
6
+ let { nodes, edges, children }: DiagramProps = $props();
7
+
8
+ setContext('nodeMap', () => nodes);
9
+ setContext('edgeMap', () => edges);
10
+ setContext('dimensions', () => ({ min: vector2(0, 0), max: vector2(0, 0) }));
11
+ setContext('prerendering', true);
12
+ </script>
13
+
14
+ <template>
15
+ {@render children()}
16
+ </template>
17
+
18
+ <svelte:boundary>{@const _ = setContext('prerendering', false)}</svelte:boundary>
@@ -0,0 +1,4 @@
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;
@@ -0,0 +1,203 @@
1
+ export interface Vector2 {
2
+ x: number;
3
+ y: number;
4
+ }
5
+ export declare const vector2: (x: number, y: number) => {
6
+ x: number;
7
+ y: number;
8
+ };
9
+ export declare const eq: (a: Vector2, b: Vector2) => boolean;
10
+ export declare const Anchor: {
11
+ readonly TOP_LEFT: {
12
+ x: number;
13
+ y: number;
14
+ };
15
+ readonly TOP_RIGHT: {
16
+ x: number;
17
+ y: number;
18
+ };
19
+ readonly BOTTOM_LEFT: {
20
+ x: number;
21
+ y: number;
22
+ };
23
+ readonly BOTTOM_RIGHT: {
24
+ x: number;
25
+ y: number;
26
+ };
27
+ readonly CENTER_LEFT: {
28
+ x: number;
29
+ y: number;
30
+ };
31
+ readonly CENTER_RIGHT: {
32
+ x: number;
33
+ y: number;
34
+ };
35
+ readonly CENTER_TOP: {
36
+ x: number;
37
+ y: number;
38
+ };
39
+ readonly CENTER_BOTTOM: {
40
+ x: number;
41
+ y: number;
42
+ };
43
+ readonly CENTER_CENTER: {
44
+ x: number;
45
+ y: number;
46
+ };
47
+ };
48
+ export declare enum Side {
49
+ Right = 0,
50
+ Top = 1,
51
+ Left = 2,
52
+ Bottom = 3
53
+ }
54
+ export declare function debugSide(s: Side): string;
55
+ export declare function normaliseAngle(r: number): number;
56
+ export declare function sideToUnitVector2(s: Side): Vector2;
57
+ export declare function sideForAngle(rad: number): Side;
58
+ export declare function unitVectorFromAngle(rad: number): Vector2;
59
+ export type GetBezierPathParams = {
60
+ /** The `x` position of the source handle. */
61
+ sourceX: number;
62
+ /** The `y` position of the source handle. */
63
+ sourceY: number;
64
+ /**
65
+ * The position of the source handle.
66
+ * @default Side.Bottom
67
+ */
68
+ sourcePosition?: Side;
69
+ /** The `x` position of the target handle. */
70
+ targetX: number;
71
+ /** The `y` position of the target handle. */
72
+ targetY: number;
73
+ /**
74
+ * The position of the target handle.
75
+ * @default Side.Top
76
+ */
77
+ targetPosition?: Side;
78
+ /**
79
+ * The curvature of the bezier edge.
80
+ * @default 0.25
81
+ */
82
+ curvature?: number;
83
+ };
84
+ export type GetControlWithCurvatureParams = {
85
+ pos: Side;
86
+ x1: number;
87
+ y1: number;
88
+ x2: number;
89
+ y2: number;
90
+ c: number;
91
+ };
92
+ export declare function getBezierEdgeCenter({ sourceX, sourceY, targetX, targetY, sourceControlX, sourceControlY, targetControlX, targetControlY }: {
93
+ sourceX: number;
94
+ sourceY: number;
95
+ targetX: number;
96
+ targetY: number;
97
+ sourceControlX: number;
98
+ sourceControlY: number;
99
+ targetControlX: number;
100
+ targetControlY: number;
101
+ }): [number, number, number, number];
102
+ /**
103
+ * The `getBezierPath` util returns everything you need to render a bezier edge
104
+ *between two nodes.
105
+ * @public
106
+ * @returns A path string you can use in an SVG, the `labelX` and `labelY` position (center of path)
107
+ * and `offsetX`, `offsetY` between source handle and label.
108
+ * - `path`: the path to use in an SVG `<path>` element.
109
+ * - `labelX`: the `x` position you can use to render a label for this edge.
110
+ * - `labelY`: the `y` position you can use to render a label for this edge.
111
+ * - `offsetX`: the absolute difference between the source `x` position and the `x` position of the
112
+ * middle of this path.
113
+ * - `offsetY`: the absolute difference between the source `y` position and the `y` position of the
114
+ * middle of this path.
115
+ * @example
116
+ * ```js
117
+ * const source = { x: 0, y: 20 };
118
+ * const target = { x: 150, y: 100 };
119
+ *
120
+ * const [path, labelX, labelY, offsetX, offsetY] = getBezierPath({
121
+ * sourceX: source.x,
122
+ * sourceY: source.y,
123
+ * sourcePosition: Side.Right,
124
+ * targetX: target.x,
125
+ * targetY: target.y,
126
+ * targetPosition: Side.Left,
127
+ *});
128
+ *```
129
+ *
130
+ * @remarks This function returns a tuple (aka a fixed-size array) to make it easier to
131
+ *work with multiple edge paths at once.
132
+ */
133
+ export declare function getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition, curvature }: GetBezierPathParams): [
134
+ path: string,
135
+ labelX: number,
136
+ labelY: number,
137
+ offsetX: number,
138
+ offsetY: number
139
+ ];
140
+ export declare function getEdgeCenter({ sourceX, sourceY, targetX, targetY, }: {
141
+ sourceX: number;
142
+ sourceY: number;
143
+ targetX: number;
144
+ targetY: number;
145
+ }): [number, number, number, number];
146
+ export interface GetSmoothStepPathParams {
147
+ /** The `x` position of the source handle. */
148
+ sourceX: number;
149
+ /** The `y` position of the source handle. */
150
+ sourceY: number;
151
+ /**
152
+ * The position of the source handle.
153
+ * @default Side.Bottom
154
+ */
155
+ sourcePosition?: Side;
156
+ /** The `x` position of the target handle. */
157
+ targetX: number;
158
+ /** The `y` position of the target handle. */
159
+ targetY: number;
160
+ /**
161
+ * The position of the target handle.
162
+ * @default Side.Top
163
+ */
164
+ targetPosition?: Side;
165
+ /** @default 5 */
166
+ borderRadius?: number;
167
+ centerX?: number;
168
+ centerY?: number;
169
+ /** @default 20 */
170
+ offset?: number;
171
+ }
172
+ /**
173
+ * The `getSmoothStepPath` util returns everything you need to render a stepped path
174
+ * between two nodes. The `borderRadius` property can be used to choose how rounded
175
+ * the corners of those steps are.
176
+ * @public
177
+ * @returns A path string you can use in an SVG, the `labelX` and `labelY` position (center of path)
178
+ * and `offsetX`, `offsetY` between source handle and label.
179
+ *
180
+ * - `path`: the path to use in an SVG `<path>` element.
181
+ * - `labelX`: the `x` position you can use to render a label for this edge.
182
+ * - `labelY`: the `y` position you can use to render a label for this edge.
183
+ * - `offsetX`: the absolute difference between the source `x` position and the `x` position of the
184
+ * middle of this path.
185
+ * - `offsetY`: the absolute difference between the source `y` position and the `y` position of the
186
+ * middle of this path.
187
+ * @example
188
+ * ```js
189
+ * const source = { x: 0, y: 20 };
190
+ * const target = { x: 150, y: 100 };
191
+ *
192
+ * const [path, labelX, labelY, offsetX, offsetY] = getSmoothStepPath({
193
+ * sourceX: source.x,
194
+ * sourceY: source.y,
195
+ * sourcePosition: Side.Right,
196
+ * targetX: target.x,
197
+ * targetY: target.y,
198
+ * targetPosition: Side.Left,
199
+ * });
200
+ * ```
201
+ * @remarks This function returns a tuple (aka a fixed-size array) to make it easier to work with multiple edge paths at once.
202
+ */
203
+ export declare function getSmoothStepPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition, borderRadius, centerX, centerY, offset, }: GetSmoothStepPathParams): [path: string, labelX: number, labelY: number, offsetX: number, offsetY: number];
@@ -0,0 +1,356 @@
1
+ export const vector2 = (x, y) => ({ x, y });
2
+ export const eq = (a, b) => a.x === b.x && a.y === b.y;
3
+ export const Anchor = {
4
+ TOP_LEFT: vector2(0, 0),
5
+ TOP_RIGHT: vector2(1, 0),
6
+ BOTTOM_LEFT: vector2(0, 1),
7
+ BOTTOM_RIGHT: vector2(1, 1),
8
+ CENTER_LEFT: vector2(0, 0.5),
9
+ CENTER_RIGHT: vector2(1, 0.5),
10
+ CENTER_TOP: vector2(0.5, 0),
11
+ CENTER_BOTTOM: vector2(0.5, 1),
12
+ CENTER_CENTER: vector2(0.5, 0.5),
13
+ };
14
+ export var Side;
15
+ (function (Side) {
16
+ Side[Side["Right"] = 0] = "Right";
17
+ Side[Side["Top"] = 1] = "Top";
18
+ Side[Side["Left"] = 2] = "Left";
19
+ Side[Side["Bottom"] = 3] = "Bottom";
20
+ })(Side || (Side = {}));
21
+ export function debugSide(s) {
22
+ return {
23
+ [Side.Right]: 'Right',
24
+ [Side.Top]: 'Top',
25
+ [Side.Left]: 'Left',
26
+ [Side.Bottom]: 'Bottom'
27
+ }[s];
28
+ }
29
+ export function normaliseAngle(r) {
30
+ return ((r % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);
31
+ }
32
+ export function sideToUnitVector2(s) {
33
+ return {
34
+ [Side.Top]: { x: 0, y: 1 },
35
+ [Side.Left]: { x: -1, y: 0 },
36
+ [Side.Right]: { x: 1, y: 0 },
37
+ [Side.Bottom]: { x: 0, y: -1 }
38
+ }[s];
39
+ }
40
+ export function sideForAngle(rad) {
41
+ const a = normaliseAngle(rad);
42
+ if (a >= (7 * Math.PI) / 4 || a < Math.PI / 4)
43
+ return Side.Right; // 7π/4 → π/4
44
+ if (a >= Math.PI / 4 && a < (3 * Math.PI) / 4)
45
+ return Side.Top; // π/4 → 3π/4
46
+ if (a >= (3 * Math.PI) / 4 && a < (5 * Math.PI) / 4)
47
+ return Side.Left; // 3π/4 → 5π/4
48
+ return Side.Bottom;
49
+ }
50
+ ;
51
+ export function unitVectorFromAngle(rad) {
52
+ return {
53
+ x: Math.cos(rad),
54
+ y: Math.sin(rad)
55
+ };
56
+ }
57
+ export function getBezierEdgeCenter({ sourceX, sourceY, targetX, targetY, sourceControlX, sourceControlY, targetControlX, targetControlY }) {
58
+ /*
59
+ * cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
60
+ * https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
61
+ */
62
+ const centerX = sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125;
63
+ const centerY = sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125;
64
+ const offsetX = Math.abs(centerX - sourceX);
65
+ const offsetY = Math.abs(centerY - sourceY);
66
+ return [centerX, centerY, offsetX, offsetY];
67
+ }
68
+ function calculateControlOffset(distance, curvature) {
69
+ if (distance >= 0) {
70
+ return 0.5 * distance;
71
+ }
72
+ return curvature * 25 * Math.sqrt(-distance);
73
+ }
74
+ function getControlWithCurvature({ pos, x1, y1, x2, y2, c }) {
75
+ switch (pos) {
76
+ case Side.Left:
77
+ return [x1 - calculateControlOffset(x1 - x2, c), y1];
78
+ case Side.Right:
79
+ return [x1 + calculateControlOffset(x2 - x1, c), y1];
80
+ case Side.Top:
81
+ return [x1, y1 - calculateControlOffset(y1 - y2, c)];
82
+ case Side.Bottom:
83
+ return [x1, y1 + calculateControlOffset(y2 - y1, c)];
84
+ }
85
+ }
86
+ /**
87
+ * The `getBezierPath` util returns everything you need to render a bezier edge
88
+ *between two nodes.
89
+ * @public
90
+ * @returns A path string you can use in an SVG, the `labelX` and `labelY` position (center of path)
91
+ * and `offsetX`, `offsetY` between source handle and label.
92
+ * - `path`: the path to use in an SVG `<path>` element.
93
+ * - `labelX`: the `x` position you can use to render a label for this edge.
94
+ * - `labelY`: the `y` position you can use to render a label for this edge.
95
+ * - `offsetX`: the absolute difference between the source `x` position and the `x` position of the
96
+ * middle of this path.
97
+ * - `offsetY`: the absolute difference between the source `y` position and the `y` position of the
98
+ * middle of this path.
99
+ * @example
100
+ * ```js
101
+ * const source = { x: 0, y: 20 };
102
+ * const target = { x: 150, y: 100 };
103
+ *
104
+ * const [path, labelX, labelY, offsetX, offsetY] = getBezierPath({
105
+ * sourceX: source.x,
106
+ * sourceY: source.y,
107
+ * sourcePosition: Side.Right,
108
+ * targetX: target.x,
109
+ * targetY: target.y,
110
+ * targetPosition: Side.Left,
111
+ *});
112
+ *```
113
+ *
114
+ * @remarks This function returns a tuple (aka a fixed-size array) to make it easier to
115
+ *work with multiple edge paths at once.
116
+ */
117
+ export function getBezierPath({ sourceX, sourceY, sourcePosition = Side.Bottom, targetX, targetY, targetPosition = Side.Top, curvature = 0.25 }) {
118
+ const [sourceControlX, sourceControlY] = getControlWithCurvature({
119
+ pos: sourcePosition,
120
+ x1: sourceX,
121
+ y1: sourceY,
122
+ x2: targetX,
123
+ y2: targetY,
124
+ c: curvature
125
+ });
126
+ const [targetControlX, targetControlY] = getControlWithCurvature({
127
+ pos: targetPosition,
128
+ x1: targetX,
129
+ y1: targetY,
130
+ x2: sourceX,
131
+ y2: sourceY,
132
+ c: curvature
133
+ });
134
+ const [labelX, labelY, offsetX, offsetY] = getBezierEdgeCenter({
135
+ sourceX,
136
+ sourceY,
137
+ targetX,
138
+ targetY,
139
+ sourceControlX,
140
+ sourceControlY,
141
+ targetControlX,
142
+ targetControlY
143
+ });
144
+ return [
145
+ `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`,
146
+ labelX,
147
+ labelY,
148
+ offsetX,
149
+ offsetY
150
+ ];
151
+ }
152
+ // https://github.com/xyflow/xyflow/blob/main/packages/system/src/utils/edges/smoothstep-edge.ts
153
+ export function getEdgeCenter({ sourceX, sourceY, targetX, targetY, }) {
154
+ const xOffset = Math.abs(targetX - sourceX) / 2;
155
+ const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset;
156
+ const yOffset = Math.abs(targetY - sourceY) / 2;
157
+ const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
158
+ return [centerX, centerY, xOffset, yOffset];
159
+ }
160
+ const handleDirections = {
161
+ [Side.Left]: { x: -1, y: 0 },
162
+ [Side.Right]: { x: 1, y: 0 },
163
+ [Side.Top]: { x: 0, y: -1 },
164
+ [Side.Bottom]: { x: 0, y: 1 },
165
+ };
166
+ const getDirection = ({ source, sourcePosition = Side.Bottom, target, }) => {
167
+ if (sourcePosition === Side.Left || sourcePosition === Side.Right) {
168
+ return source.x < target.x ? { x: 1, y: 0 } : { x: -1, y: 0 };
169
+ }
170
+ return source.y < target.y ? { x: 0, y: 1 } : { x: 0, y: -1 };
171
+ };
172
+ const distance = (a, b) => Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2));
173
+ /*
174
+ * With this function we try to mimic an orthogonal edge routing behaviour
175
+ * It's not as good as a real orthogonal edge routing, but it's faster and good enough as a default for step and smooth step edges
176
+ */
177
+ function getPoints({ source, sourcePosition = Side.Bottom, target, targetPosition = Side.Top, center, offset, }) {
178
+ const sourceDir = handleDirections[sourcePosition];
179
+ const targetDir = handleDirections[targetPosition];
180
+ const sourceGapped = { x: source.x + sourceDir.x * offset, y: source.y + sourceDir.y * offset };
181
+ const targetGapped = { x: target.x + targetDir.x * offset, y: target.y + targetDir.y * offset };
182
+ const dir = getDirection({
183
+ source: sourceGapped,
184
+ sourcePosition,
185
+ target: targetGapped,
186
+ });
187
+ const dirAccessor = dir.x !== 0 ? 'x' : 'y';
188
+ const currDir = dir[dirAccessor];
189
+ let points = [];
190
+ let centerX, centerY;
191
+ const sourceGapOffset = { x: 0, y: 0 };
192
+ const targetGapOffset = { x: 0, y: 0 };
193
+ const [defaultCenterX, defaultCenterY, defaultOffsetX, defaultOffsetY] = getEdgeCenter({
194
+ sourceX: source.x,
195
+ sourceY: source.y,
196
+ targetX: target.x,
197
+ targetY: target.y,
198
+ });
199
+ // opposite handle positions, default case
200
+ if (sourceDir[dirAccessor] * targetDir[dirAccessor] === -1) {
201
+ centerX = center.x ?? defaultCenterX;
202
+ centerY = center.y ?? defaultCenterY;
203
+ /*
204
+ * --->
205
+ * |
206
+ * >---
207
+ */
208
+ const verticalSplit = [
209
+ { x: centerX, y: sourceGapped.y },
210
+ { x: centerX, y: targetGapped.y },
211
+ ];
212
+ /*
213
+ * |
214
+ * ---
215
+ * |
216
+ */
217
+ const horizontalSplit = [
218
+ { x: sourceGapped.x, y: centerY },
219
+ { x: targetGapped.x, y: centerY },
220
+ ];
221
+ if (sourceDir[dirAccessor] === currDir) {
222
+ points = dirAccessor === 'x' ? verticalSplit : horizontalSplit;
223
+ }
224
+ else {
225
+ points = dirAccessor === 'x' ? horizontalSplit : verticalSplit;
226
+ }
227
+ }
228
+ else {
229
+ // sourceTarget means we take x from source and y from target, targetSource is the opposite
230
+ const sourceTarget = [{ x: sourceGapped.x, y: targetGapped.y }];
231
+ const targetSource = [{ x: targetGapped.x, y: sourceGapped.y }];
232
+ // this handles edges with same handle positions
233
+ if (dirAccessor === 'x') {
234
+ points = sourceDir.x === currDir ? targetSource : sourceTarget;
235
+ }
236
+ else {
237
+ points = sourceDir.y === currDir ? sourceTarget : targetSource;
238
+ }
239
+ if (sourcePosition === targetPosition) {
240
+ const diff = Math.abs(source[dirAccessor] - target[dirAccessor]);
241
+ // if an edge goes from right to right for example (sourcePosition === targetPosition) and the distance between source.x and target.x is less than the offset, the added point and the gapped source/target will overlap. This leads to a weird edge path. To avoid this we add a gapOffset to the source/target
242
+ if (diff <= offset) {
243
+ const gapOffset = Math.min(offset - 1, offset - diff);
244
+ if (sourceDir[dirAccessor] === currDir) {
245
+ sourceGapOffset[dirAccessor] = (sourceGapped[dirAccessor] > source[dirAccessor] ? -1 : 1) * gapOffset;
246
+ }
247
+ else {
248
+ targetGapOffset[dirAccessor] = (targetGapped[dirAccessor] > target[dirAccessor] ? -1 : 1) * gapOffset;
249
+ }
250
+ }
251
+ }
252
+ // these are conditions for handling mixed handle positions like Right -> Bottom for example
253
+ if (sourcePosition !== targetPosition) {
254
+ const dirAccessorOpposite = dirAccessor === 'x' ? 'y' : 'x';
255
+ const isSameDir = sourceDir[dirAccessor] === targetDir[dirAccessorOpposite];
256
+ const sourceGtTargetOppo = sourceGapped[dirAccessorOpposite] > targetGapped[dirAccessorOpposite];
257
+ const sourceLtTargetOppo = sourceGapped[dirAccessorOpposite] < targetGapped[dirAccessorOpposite];
258
+ const flipSourceTarget = (sourceDir[dirAccessor] === 1 && ((!isSameDir && sourceGtTargetOppo) || (isSameDir && sourceLtTargetOppo))) ||
259
+ (sourceDir[dirAccessor] !== 1 && ((!isSameDir && sourceLtTargetOppo) || (isSameDir && sourceGtTargetOppo)));
260
+ if (flipSourceTarget) {
261
+ points = dirAccessor === 'x' ? sourceTarget : targetSource;
262
+ }
263
+ }
264
+ const sourceGapPoint = { x: sourceGapped.x + sourceGapOffset.x, y: sourceGapped.y + sourceGapOffset.y };
265
+ const targetGapPoint = { x: targetGapped.x + targetGapOffset.x, y: targetGapped.y + targetGapOffset.y };
266
+ const maxXDistance = Math.max(Math.abs(sourceGapPoint.x - points[0].x), Math.abs(targetGapPoint.x - points[0].x));
267
+ const maxYDistance = Math.max(Math.abs(sourceGapPoint.y - points[0].y), Math.abs(targetGapPoint.y - points[0].y));
268
+ // we want to place the label on the longest segment of the edge
269
+ if (maxXDistance >= maxYDistance) {
270
+ centerX = (sourceGapPoint.x + targetGapPoint.x) / 2;
271
+ centerY = points[0].y;
272
+ }
273
+ else {
274
+ centerX = points[0].x;
275
+ centerY = (sourceGapPoint.y + targetGapPoint.y) / 2;
276
+ }
277
+ }
278
+ const pathPoints = [
279
+ source,
280
+ { x: sourceGapped.x + sourceGapOffset.x, y: sourceGapped.y + sourceGapOffset.y },
281
+ ...points,
282
+ { x: targetGapped.x + targetGapOffset.x, y: targetGapped.y + targetGapOffset.y },
283
+ target,
284
+ ];
285
+ return [pathPoints, centerX, centerY, defaultOffsetX, defaultOffsetY];
286
+ }
287
+ function getBend(a, b, c, size) {
288
+ const bendSize = Math.min(distance(a, b) / 2, distance(b, c) / 2, size);
289
+ const { x, y } = b;
290
+ // no bend
291
+ if ((a.x === x && x === c.x) || (a.y === y && y === c.y)) {
292
+ return `L${x} ${y}`;
293
+ }
294
+ // first segment is horizontal
295
+ if (a.y === y) {
296
+ const xDir = a.x < c.x ? -1 : 1;
297
+ const yDir = a.y < c.y ? 1 : -1;
298
+ return `L ${x + bendSize * xDir},${y}Q ${x},${y} ${x},${y + bendSize * yDir}`;
299
+ }
300
+ const xDir = a.x < c.x ? 1 : -1;
301
+ const yDir = a.y < c.y ? -1 : 1;
302
+ return `L ${x},${y + bendSize * yDir}Q ${x},${y} ${x + bendSize * xDir},${y}`;
303
+ }
304
+ /**
305
+ * The `getSmoothStepPath` util returns everything you need to render a stepped path
306
+ * between two nodes. The `borderRadius` property can be used to choose how rounded
307
+ * the corners of those steps are.
308
+ * @public
309
+ * @returns A path string you can use in an SVG, the `labelX` and `labelY` position (center of path)
310
+ * and `offsetX`, `offsetY` between source handle and label.
311
+ *
312
+ * - `path`: the path to use in an SVG `<path>` element.
313
+ * - `labelX`: the `x` position you can use to render a label for this edge.
314
+ * - `labelY`: the `y` position you can use to render a label for this edge.
315
+ * - `offsetX`: the absolute difference between the source `x` position and the `x` position of the
316
+ * middle of this path.
317
+ * - `offsetY`: the absolute difference between the source `y` position and the `y` position of the
318
+ * middle of this path.
319
+ * @example
320
+ * ```js
321
+ * const source = { x: 0, y: 20 };
322
+ * const target = { x: 150, y: 100 };
323
+ *
324
+ * const [path, labelX, labelY, offsetX, offsetY] = getSmoothStepPath({
325
+ * sourceX: source.x,
326
+ * sourceY: source.y,
327
+ * sourcePosition: Side.Right,
328
+ * targetX: target.x,
329
+ * targetY: target.y,
330
+ * targetPosition: Side.Left,
331
+ * });
332
+ * ```
333
+ * @remarks This function returns a tuple (aka a fixed-size array) to make it easier to work with multiple edge paths at once.
334
+ */
335
+ export function getSmoothStepPath({ sourceX, sourceY, sourcePosition = Side.Bottom, targetX, targetY, targetPosition = Side.Top, borderRadius = 5, centerX, centerY, offset = 20, }) {
336
+ const [points, labelX, labelY, offsetX, offsetY] = getPoints({
337
+ source: { x: sourceX, y: sourceY },
338
+ sourcePosition,
339
+ target: { x: targetX, y: targetY },
340
+ targetPosition,
341
+ center: { x: centerX, y: centerY },
342
+ offset,
343
+ });
344
+ const path = points.reduce((res, p, i) => {
345
+ let segment = '';
346
+ if (i > 0 && i < points.length - 1) {
347
+ segment = getBend(points[i - 1], p, points[i + 1], borderRadius);
348
+ }
349
+ else {
350
+ segment = `${i === 0 ? 'M' : 'L'}${p.x} ${p.y}`;
351
+ }
352
+ res += segment;
353
+ return res;
354
+ }, '');
355
+ return [path, labelX, labelY, offsetX, offsetY];
356
+ }
@@ -0,0 +1,7 @@
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
+ export * from './diagram-lib';
6
+ export type { DiagramNode as DiagramNodeType, DiagramEdge, DiagramEdgeParams, DiagramProps } from './Diagram.svelte';
7
+ export type { DiagramNodeProps } from './DiagramNode.svelte';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
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
+ export * from './diagram-lib';
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@cnvx/nodal",
3
+ "private": false,
4
+ "version": "0.0.1",
5
+ "scripts": {
6
+ "dev": "vite dev",
7
+ "build": "vite build && npm run prepack",
8
+ "preview": "vite preview",
9
+ "prepare": "svelte-kit sync || echo ''",
10
+ "prepack": "svelte-kit sync && svelte-package && publint",
11
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
12
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
13
+ "format": "prettier --write .",
14
+ "lint": "prettier --check . && eslint .",
15
+ "test:unit": "vitest",
16
+ "test": "npm run test:unit -- --run"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "!dist/**/*.test.*",
21
+ "!dist/**/*.spec.*"
22
+ ],
23
+ "sideEffects": [
24
+ "**/*.css"
25
+ ],
26
+ "svelte": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "type": "module",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "svelte": "./dist/index.js"
33
+ }
34
+ },
35
+ "peerDependencies": {
36
+ "svelte": "^5.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "@eslint/compat": "^1.2.5",
40
+ "@eslint/js": "^9.18.0",
41
+ "@sveltejs/adapter-auto": "^6.0.0",
42
+ "@sveltejs/kit": "^2.22.0",
43
+ "@sveltejs/package": "^2.0.0",
44
+ "@sveltejs/vite-plugin-svelte": "^6.0.0",
45
+ "@vitest/browser": "^3.2.3",
46
+ "eslint": "^9.18.0",
47
+ "eslint-config-prettier": "^10.0.1",
48
+ "eslint-plugin-svelte": "^3.0.0",
49
+ "globals": "^16.0.0",
50
+ "playwright": "^1.53.0",
51
+ "prettier": "^3.4.2",
52
+ "prettier-plugin-svelte": "^3.3.3",
53
+ "publint": "^0.3.2",
54
+ "svelte": "^5.0.0",
55
+ "svelte-check": "^4.0.0",
56
+ "typescript": "^5.0.0",
57
+ "typescript-eslint": "^8.20.0",
58
+ "vite": "^7.0.4",
59
+ "vitest": "^3.2.3",
60
+ "vitest-browser-svelte": "^0.1.0"
61
+ },
62
+ "keywords": [
63
+ "svelte"
64
+ ]
65
+ }