@luciole-sh/flow-graph 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +671 -2
  3. package/dist/Flow.d.ts +41 -0
  4. package/dist/Flow.d.ts.map +1 -0
  5. package/dist/Flow.js +489 -0
  6. package/dist/Flow.js.map +1 -0
  7. package/dist/components.d.ts +28 -0
  8. package/dist/components.d.ts.map +1 -0
  9. package/dist/components.js +100 -0
  10. package/dist/components.js.map +1 -0
  11. package/dist/frame.d.ts +68 -0
  12. package/dist/frame.d.ts.map +1 -0
  13. package/dist/frame.js +136 -0
  14. package/dist/frame.js.map +1 -0
  15. package/dist/geometry.d.ts +60 -0
  16. package/dist/geometry.d.ts.map +1 -0
  17. package/dist/geometry.js +205 -0
  18. package/dist/geometry.js.map +1 -0
  19. package/dist/hooks.d.ts +56 -0
  20. package/dist/hooks.d.ts.map +1 -0
  21. package/dist/hooks.js +65 -0
  22. package/dist/hooks.js.map +1 -0
  23. package/dist/index.d.ts +15 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +14 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/keymap.d.ts +11 -0
  28. package/dist/keymap.d.ts.map +1 -0
  29. package/dist/keymap.js +42 -0
  30. package/dist/keymap.js.map +1 -0
  31. package/dist/navigation.d.ts +32 -0
  32. package/dist/navigation.d.ts.map +1 -0
  33. package/dist/navigation.js +68 -0
  34. package/dist/navigation.js.map +1 -0
  35. package/dist/nodes.d.ts +18 -0
  36. package/dist/nodes.d.ts.map +1 -0
  37. package/dist/nodes.js +46 -0
  38. package/dist/nodes.js.map +1 -0
  39. package/dist/raster.d.ts +112 -0
  40. package/dist/raster.d.ts.map +1 -0
  41. package/dist/raster.js +370 -0
  42. package/dist/raster.js.map +1 -0
  43. package/dist/store.d.ts +104 -0
  44. package/dist/store.d.ts.map +1 -0
  45. package/dist/store.js +186 -0
  46. package/dist/store.js.map +1 -0
  47. package/dist/theme.d.ts +25 -0
  48. package/dist/theme.d.ts.map +1 -0
  49. package/dist/theme.js +39 -0
  50. package/dist/theme.js.map +1 -0
  51. package/dist/types.d.ts +140 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/dist/types.js +2 -0
  54. package/dist/types.js.map +1 -0
  55. package/dist/vendor/xyflow/LICENSE +21 -0
  56. package/dist/vendor/xyflow/bezier.d.ts +26 -0
  57. package/dist/vendor/xyflow/bezier.d.ts.map +1 -0
  58. package/dist/vendor/xyflow/bezier.js +47 -0
  59. package/dist/vendor/xyflow/bezier.js.map +1 -0
  60. package/dist/vendor/xyflow/changes.d.ts +9 -0
  61. package/dist/vendor/xyflow/changes.d.ts.map +1 -0
  62. package/dist/vendor/xyflow/changes.js +133 -0
  63. package/dist/vendor/xyflow/changes.js.map +1 -0
  64. package/dist/vendor/xyflow/smoothstep.d.ts +17 -0
  65. package/dist/vendor/xyflow/smoothstep.d.ts.map +1 -0
  66. package/dist/vendor/xyflow/smoothstep.js +166 -0
  67. package/dist/vendor/xyflow/smoothstep.js.map +1 -0
  68. package/package.json +61 -4
  69. package/src/Flow.tsx +684 -0
  70. package/src/components.tsx +180 -0
  71. package/src/frame.ts +203 -0
  72. package/src/geometry.ts +274 -0
  73. package/src/hooks.tsx +121 -0
  74. package/src/index.ts +21 -0
  75. package/src/keymap.tsx +45 -0
  76. package/src/navigation.ts +86 -0
  77. package/src/nodes.tsx +86 -0
  78. package/src/raster.ts +453 -0
  79. package/src/store.ts +240 -0
  80. package/src/theme.ts +61 -0
  81. package/src/types.ts +130 -0
  82. package/src/vendor/xyflow/LICENSE +21 -0
  83. package/src/vendor/xyflow/README.md +20 -0
  84. package/src/vendor/xyflow/bezier.ts +102 -0
  85. package/src/vendor/xyflow/changes.ts +177 -0
  86. package/src/vendor/xyflow/smoothstep.ts +219 -0
  87. package/tsconfig.json +22 -0
package/src/theme.ts ADDED
@@ -0,0 +1,61 @@
1
+ import { createContext, useContext } from "react";
2
+ import type { Role } from "./raster.ts";
3
+
4
+ /** The canvas's colors, as `#rrggbb`. `<Flow theme>` overrides any of them. */
5
+ export type FlowTheme = {
6
+ text: string;
7
+ muted: string;
8
+ border: string;
9
+ selected: string;
10
+ /** A node proposed as the target of a connection, and the connection's line. */
11
+ connect: string;
12
+ group: string;
13
+ edge: string;
14
+ animated: string;
15
+ label: string;
16
+ labelBg: string;
17
+ marker: string;
18
+ background: string;
19
+ handle: string;
20
+ panelBg: string;
21
+ };
22
+
23
+ export const defaultTheme: FlowTheme = {
24
+ text: "#e6edf3",
25
+ muted: "#8b98a5",
26
+ border: "#4d5966",
27
+ selected: "#67d9bc",
28
+ connect: "#f2cc60",
29
+ group: "#3b4450",
30
+ edge: "#6e7c8a",
31
+ animated: "#79c0ff",
32
+ label: "#c9d1d9",
33
+ labelBg: "#1f262e",
34
+ marker: "#8b98a5",
35
+ background: "#2d353e",
36
+ handle: "#f2cc60",
37
+ panelBg: "#161b22",
38
+ };
39
+
40
+ export const ThemeContext = createContext<FlowTheme>(defaultTheme);
41
+ export const useFlowTheme = () => useContext(ThemeContext);
42
+
43
+ /** The color a grid cell of `role` is drawn in. */
44
+ export function roleColor(theme: FlowTheme, role: Role): string {
45
+ switch (role) {
46
+ case "edge":
47
+ return theme.edge;
48
+ case "selected":
49
+ return theme.selected;
50
+ case "animated":
51
+ return theme.animated;
52
+ case "label":
53
+ return theme.label;
54
+ case "marker":
55
+ return theme.marker;
56
+ case "background":
57
+ return theme.background;
58
+ case "handle":
59
+ return theme.handle;
60
+ }
61
+ }
package/src/types.ts ADDED
@@ -0,0 +1,130 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ /**
4
+ * The graph's vocabulary, after React Flow's (`@xyflow/system` types): a node, an edge,
5
+ * the changes an interaction proposes, the viewport. Coordinates are terminal cells: `x`
6
+ * counts columns, `y` rows, and a row is about twice as tall as a column is wide.
7
+ */
8
+
9
+ export type XY = { x: number; y: number };
10
+ export type Rect = { x: number; y: number; width: number; height: number };
11
+
12
+ /** Which side of a node a handle sits on. */
13
+ export type Position = "left" | "right" | "top" | "bottom";
14
+
15
+ /**
16
+ * Semantic zoom: the terminal cannot scale glyphs, so zooming out spaces nodes closer and
17
+ * draws less of each. `full` renders the node's component, `compact` its label on one
18
+ * row, `dot` a single cell.
19
+ */
20
+ export type Detail = "full" | "compact" | "dot";
21
+
22
+ /** A node as the application keeps it. `position` is relative to `parentId`'s, if any. */
23
+ export type Node<Data extends Record<string, unknown> = Record<string, unknown>> = {
24
+ id: string;
25
+ position: XY;
26
+ data: Data;
27
+ /** A key of `nodeTypes`, or a built-in: `default`, `input`, `output`, `group`. */
28
+ type?: string;
29
+ selected?: boolean;
30
+ dragging?: boolean;
31
+ hidden?: boolean;
32
+ /** A group node this one sits in: its position is relative to the group's. */
33
+ parentId?: string;
34
+ /** Fixed size in cells (a group needs one); otherwise the rendered size is measured. */
35
+ width?: number;
36
+ height?: number;
37
+ /** Where edges leave (default `right`) and arrive (default `left`) without a `<Handle>`. */
38
+ sourcePosition?: Position;
39
+ targetPosition?: Position;
40
+ draggable?: boolean;
41
+ selectable?: boolean;
42
+ connectable?: boolean;
43
+ zIndex?: number;
44
+ /** The node's color where the canvas draws it itself: a dot when zoomed out, the minimap. */
45
+ color?: string;
46
+ };
47
+
48
+ export type MarkerType = "arrow" | "none";
49
+
50
+ export type Edge<Data extends Record<string, unknown> = Record<string, unknown>> = {
51
+ id: string;
52
+ source: string;
53
+ target: string;
54
+ sourceHandle?: string | null;
55
+ targetHandle?: string | null;
56
+ /** A key of `edgeTypes`, or a built-in: `smoothstep` (default), `step`, `straight`, `bezier`. */
57
+ type?: string;
58
+ label?: string;
59
+ /** Dashes that run from source to target. */
60
+ animated?: boolean;
61
+ selected?: boolean;
62
+ hidden?: boolean;
63
+ /** Default `arrow`. */
64
+ markerEnd?: MarkerType;
65
+ markerStart?: MarkerType;
66
+ /** A color for the line, as `#rrggbb`. */
67
+ color?: string;
68
+ data?: Data;
69
+ };
70
+
71
+ export type Connection = {
72
+ source: string;
73
+ target: string;
74
+ sourceHandle: string | null;
75
+ targetHandle: string | null;
76
+ };
77
+
78
+ export type Viewport = { x: number; y: number; zoom: number };
79
+
80
+ // The changes, as @xyflow/system's types/changes.ts names them.
81
+ export type NodePositionChange = {
82
+ id: string;
83
+ type: "position";
84
+ position?: XY;
85
+ dragging?: boolean;
86
+ };
87
+ export type NodeDimensionsChange = {
88
+ id: string;
89
+ type: "dimensions";
90
+ dimensions: { width: number; height: number };
91
+ };
92
+ export type SelectionChange = { id: string; type: "select"; selected: boolean };
93
+ export type RemoveChange = { id: string; type: "remove" };
94
+ export type AddChange<T> = { type: "add"; item: T; index?: number };
95
+ export type ReplaceChange<T> = { id: string; type: "replace"; item: T };
96
+
97
+ export type NodeChange<N extends Node = Node> =
98
+ | NodePositionChange
99
+ | NodeDimensionsChange
100
+ | SelectionChange
101
+ | RemoveChange
102
+ | AddChange<N>
103
+ | ReplaceChange<N>;
104
+ export type EdgeChange<E extends Edge = Edge> =
105
+ | SelectionChange
106
+ | RemoveChange
107
+ | AddChange<E>
108
+ | ReplaceChange<E>;
109
+
110
+ export type HandleType = "source" | "target";
111
+ /** A handle a node declared with `<Handle>`. */
112
+ export type HandleSpec = { id: string | null; type: HandleType; position: Position };
113
+
114
+ /** A node type: a function component of `NodeProps`. */
115
+ export type NodeComponent<N extends Node = Node> = (props: NodeProps<N>) => ReactNode;
116
+
117
+ /** What a node component receives. */
118
+ export type NodeProps<N extends Node = Node> = {
119
+ id: string;
120
+ data: N["data"];
121
+ type: string;
122
+ selected: boolean;
123
+ dragging: boolean;
124
+ /** `full` or `compact`: at `dot`, the canvas draws the node itself. */
125
+ detail: Detail;
126
+ /** Keyboard connection under way and this node is the proposed target. */
127
+ connectTarget: boolean;
128
+ width?: number;
129
+ height?: number;
130
+ };
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019-2025 webkid GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,20 @@
1
+ # Code repris de xyflow
2
+
3
+ Les fichiers de ce répertoire sont adaptés de [xyflow](https://github.com/xyflow/xyflow)
4
+ (React Flow), sous licence MIT ; la notice d'origine est dans [LICENSE](LICENSE).
5
+
6
+ Révision : `3d35b57317576b0916c0bfeaaedd573aaacc2839` (`@xyflow/system` 0.0.83,
7
+ `@xyflow/react` 12.12.0).
8
+
9
+ | Fichier | Origine | Adaptation |
10
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
11
+ | `smoothstep.ts` | `packages/system/src/utils/edges/smoothstep-edge.ts` (`getPoints`) | Les points du tracé orthogonal sont rendus tels quels (pas de chemin SVG ni de virages arrondis) : la grille les dessine en caractères de boîte. |
12
+ | `bezier.ts` | `packages/system/src/utils/edges/bezier-edge.ts` (`calculateControlOffset`, `getControlWithCurvature`, `getBezierEdgeCenter`), `utils/edges/general.ts` (`getEdgeCenter`) | Points de contrôle rendus en nombres, pas en chemin SVG ; `Position` en chaînes. |
13
+ | `changes.ts` | `packages/react/src/utils/changes.ts` (`applyChanges`, `applyChange`), `packages/system/src/utils/edges/general.ts` (`addEdge`, `getEdgeId`, `connectionExists`) | Typé sans `any` ; pas de `dimensions`/`resizing` sur les attributs (le terminal mesure, il ne redimensionne pas) ; `addEdge` sans `onError`. |
14
+
15
+ Pourquoi recopier plutôt que dépendre de `@xyflow/system` : le paquet s'importe sous Bun
16
+ sans DOM, mais il tire d3 (`d3-drag`, `d3-zoom`, `d3-selection`, `d3-interpolate`,
17
+ `d3-transition` et leurs types, 15 paquets) pour du code qui ne sert pas au terminal
18
+ (`XYPanZoom`, `XYDrag`, `XYHandle`, `XYResizer`, mesure par `getBoundingClientRect`), et ses
19
+ fonctions de tracé ne rendent que des chaînes SVG qu'il faudrait reparser. Ce qui sert tient
20
+ en quelques fonctions pures, reprises ici.
@@ -0,0 +1,102 @@
1
+ // Adapted from xyflow, packages/system/src/utils/edges/bezier-edge.ts and
2
+ // utils/edges/general.ts (MIT, see LICENSE and README.md in this directory): the control
3
+ // points and centers as numbers instead of an SVG path.
4
+ import type { Position } from "../../types.ts";
5
+
6
+ const HALF = 0.5;
7
+ // cubic bezier t=0.5 mid point weights: (1-t)^3, 3t(1-t)^2, 3t^2(1-t), t^3
8
+ const END_WEIGHT = 0.125;
9
+ const CONTROL_WEIGHT = 0.375;
10
+ const CURVATURE_SCALE = 25;
11
+
12
+ // this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB)
13
+ export function getEdgeCenter({
14
+ sourceX,
15
+ sourceY,
16
+ targetX,
17
+ targetY,
18
+ }: {
19
+ sourceX: number;
20
+ sourceY: number;
21
+ targetX: number;
22
+ targetY: number;
23
+ }): [number, number, number, number] {
24
+ const xOffset = Math.abs(targetX - sourceX) / 2;
25
+ const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset;
26
+
27
+ const yOffset = Math.abs(targetY - sourceY) / 2;
28
+ const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
29
+
30
+ return [centerX, centerY, xOffset, yOffset];
31
+ }
32
+
33
+ export function getBezierEdgeCenter({
34
+ sourceX,
35
+ sourceY,
36
+ targetX,
37
+ targetY,
38
+ sourceControlX,
39
+ sourceControlY,
40
+ targetControlX,
41
+ targetControlY,
42
+ }: {
43
+ sourceX: number;
44
+ sourceY: number;
45
+ targetX: number;
46
+ targetY: number;
47
+ sourceControlX: number;
48
+ sourceControlY: number;
49
+ targetControlX: number;
50
+ targetControlY: number;
51
+ }): [number, number] {
52
+ /*
53
+ * cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
54
+ * https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
55
+ */
56
+ const centerX =
57
+ sourceX * END_WEIGHT +
58
+ sourceControlX * CONTROL_WEIGHT +
59
+ targetControlX * CONTROL_WEIGHT +
60
+ targetX * END_WEIGHT;
61
+ const centerY =
62
+ sourceY * END_WEIGHT +
63
+ sourceControlY * CONTROL_WEIGHT +
64
+ targetControlY * CONTROL_WEIGHT +
65
+ targetY * END_WEIGHT;
66
+ return [centerX, centerY];
67
+ }
68
+
69
+ function calculateControlOffset(distance: number, curvature: number): number {
70
+ if (distance >= 0) {
71
+ return HALF * distance;
72
+ }
73
+
74
+ return curvature * CURVATURE_SCALE * Math.sqrt(-distance);
75
+ }
76
+
77
+ export function getControlWithCurvature({
78
+ pos,
79
+ x1,
80
+ y1,
81
+ x2,
82
+ y2,
83
+ c,
84
+ }: {
85
+ pos: Position;
86
+ x1: number;
87
+ y1: number;
88
+ x2: number;
89
+ y2: number;
90
+ c: number;
91
+ }): [number, number] {
92
+ switch (pos) {
93
+ case "left":
94
+ return [x1 - calculateControlOffset(x1 - x2, c), y1];
95
+ case "right":
96
+ return [x1 + calculateControlOffset(x2 - x1, c), y1];
97
+ case "top":
98
+ return [x1, y1 - calculateControlOffset(y1 - y2, c)];
99
+ case "bottom":
100
+ return [x1, y1 + calculateControlOffset(y2 - y1, c)];
101
+ }
102
+ }
@@ -0,0 +1,177 @@
1
+ // Adapted from xyflow, packages/react/src/utils/changes.ts and
2
+ // packages/system/src/utils/edges/general.ts (MIT, see LICENSE and README.md in this
3
+ // directory): typed without `any`, and without the resizer's attributes.
4
+ import type { Connection, Edge, EdgeChange, Node, NodeChange } from "../../types.ts";
5
+
6
+ type Change<T> =
7
+ | { type: "add"; item: T; index?: number }
8
+ | { type: "replace"; id: string; item: T }
9
+ | { type: "remove"; id: string }
10
+ | { type: "select"; id: string; selected: boolean }
11
+ | { type: "position"; id: string; position?: Element["position"]; dragging?: boolean }
12
+ | { type: "dimensions"; id: string };
13
+ type Element = { id: string; selected?: boolean; position?: unknown; dragging?: boolean };
14
+
15
+ /*
16
+ * This function applies changes to nodes or edges that are triggered by the canvas.
17
+ * When you drag a node for example, the canvas sends a position change update.
18
+ * This function then applies the changes and returns the updated elements.
19
+ */
20
+ function applyChanges<T extends Element>(
21
+ changes: readonly Change<T>[],
22
+ elements: readonly T[],
23
+ ): T[] {
24
+ const updatedElements: T[] = [];
25
+ /*
26
+ * By storing a map of changes for each element, we can a quick lookup as we
27
+ * iterate over the elements array!
28
+ */
29
+ const changesMap = new Map<string, Change<T>[]>();
30
+ const addItemChanges: { item: T; index?: number }[] = [];
31
+
32
+ for (const change of changes) {
33
+ if (change.type === "add") {
34
+ addItemChanges.push(change);
35
+ continue;
36
+ } else if (change.type === "remove" || change.type === "replace") {
37
+ /*
38
+ * For a 'remove' change we can safely ignore any other changes queued for
39
+ * the same element, it's going to be removed anyway!
40
+ */
41
+ changesMap.set(change.id, [change]);
42
+ } else {
43
+ const elementChanges = changesMap.get(change.id);
44
+
45
+ if (elementChanges) {
46
+ /*
47
+ * If we have some changes queued already, we can do a mutable update of
48
+ * that array and save ourselves some copying.
49
+ */
50
+ elementChanges.push(change);
51
+ } else {
52
+ changesMap.set(change.id, [change]);
53
+ }
54
+ }
55
+ }
56
+
57
+ for (const element of elements) {
58
+ const queued = changesMap.get(element.id);
59
+
60
+ /*
61
+ * When there are no changes for an element we can just push it unmodified,
62
+ * no need to copy it.
63
+ */
64
+ if (!queued) {
65
+ updatedElements.push(element);
66
+ continue;
67
+ }
68
+
69
+ const [first] = queued;
70
+ // If we have a 'remove' change queued, it'll be the only change in the array
71
+ if (first?.type === "remove") {
72
+ continue;
73
+ }
74
+
75
+ if (first?.type === "replace") {
76
+ updatedElements.push({ ...first.item });
77
+ continue;
78
+ }
79
+
80
+ /**
81
+ * For other types of changes, we want to start with a shallow copy of the
82
+ * object so React knows this element has changed. Sequential changes will
83
+ * each _mutate_ this object, so there's only ever one copy.
84
+ */
85
+ const updatedElement = { ...element };
86
+
87
+ for (const change of queued) {
88
+ applyChange(change, updatedElement);
89
+ }
90
+
91
+ updatedElements.push(updatedElement);
92
+ }
93
+
94
+ /*
95
+ * we need to wait for all changes to be applied before adding new items
96
+ * to be able to add them at the correct index
97
+ */
98
+ for (const change of addItemChanges) {
99
+ if (change.index !== undefined) {
100
+ updatedElements.splice(change.index, 0, { ...change.item });
101
+ } else {
102
+ updatedElements.push({ ...change.item });
103
+ }
104
+ }
105
+
106
+ return updatedElements;
107
+ }
108
+
109
+ // A connection spread into an edge is one once it has an id: this narrows it to `E`.
110
+ function isElement<T>(item: unknown): item is T {
111
+ return typeof item === "object" && item !== null && "id" in item;
112
+ }
113
+
114
+ // Applies a single change to an element. This is a *mutable* update.
115
+ function applyChange<T extends Element>(change: Change<T>, element: T): void {
116
+ switch (change.type) {
117
+ case "select": {
118
+ element.selected = change.selected;
119
+ break;
120
+ }
121
+
122
+ case "position": {
123
+ if (typeof change.position !== "undefined") {
124
+ element.position = change.position;
125
+ }
126
+
127
+ if (typeof change.dragging !== "undefined") {
128
+ element.dragging = change.dragging;
129
+ }
130
+
131
+ break;
132
+ }
133
+ }
134
+ }
135
+
136
+ /** Applies what the canvas proposes (drag, selection, removal…) to the application's nodes. */
137
+ export function applyNodeChanges<N extends Node = Node>(
138
+ changes: readonly NodeChange<N>[],
139
+ nodes: readonly N[],
140
+ ): N[] {
141
+ return applyChanges(changes, nodes);
142
+ }
143
+
144
+ /** Applies what the canvas proposes to the application's edges. */
145
+ export function applyEdgeChanges<E extends Edge = Edge>(
146
+ changes: readonly EdgeChange<E>[],
147
+ edges: readonly E[],
148
+ ): E[] {
149
+ return applyChanges(changes, edges);
150
+ }
151
+
152
+ export const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection | Edge) =>
153
+ `xy-edge__${source}${sourceHandle || ""}-${target}${targetHandle || ""}`;
154
+
155
+ const connectionExists = (edge: Edge, edges: readonly Edge[]) =>
156
+ edges.some(
157
+ (el) =>
158
+ el.source === edge.source &&
159
+ el.target === edge.target &&
160
+ (el.sourceHandle === edge.sourceHandle || (!el.sourceHandle && !edge.sourceHandle)) &&
161
+ (el.targetHandle === edge.targetHandle || (!el.targetHandle && !edge.targetHandle)),
162
+ );
163
+
164
+ /** `edges` with a new edge for `connection`, unless the same connection exists already. */
165
+ export function addEdge<E extends Edge = Edge>(
166
+ connection: (Connection & Partial<Omit<E, keyof Connection>>) | E,
167
+ edges: readonly E[],
168
+ getId: (connection: Connection | Edge) => string = getEdgeId,
169
+ ): E[] {
170
+ if (!connection.source || !connection.target) return [...edges];
171
+ const known = "id" in connection ? connection.id : undefined;
172
+ const edge: Edge = { ...connection, id: typeof known === "string" ? known : getId(connection) };
173
+ if (connectionExists(edge, edges)) return [...edges];
174
+ if (edge.sourceHandle === null) delete edge.sourceHandle;
175
+ if (edge.targetHandle === null) delete edge.targetHandle;
176
+ return isElement<E>(edge) ? [...edges, edge] : [...edges];
177
+ }