@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
|
@@ -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,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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|