@stuly/anode-react 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.
- package/LICENSE +19 -0
- package/README.md +22 -0
- package/dist/context.d.ts +34 -0
- package/dist/context.js +65 -0
- package/dist/elements/Background.d.ts +12 -0
- package/dist/elements/Background.js +27 -0
- package/dist/elements/Controls.d.ts +8 -0
- package/dist/elements/Controls.js +96 -0
- package/dist/elements/Group.d.ts +10 -0
- package/dist/elements/Group.js +86 -0
- package/dist/elements/Link.d.ts +16 -0
- package/dist/elements/Link.js +191 -0
- package/dist/elements/MiniMap.d.ts +10 -0
- package/dist/elements/MiniMap.js +80 -0
- package/dist/elements/Node.d.ts +14 -0
- package/dist/elements/Node.js +122 -0
- package/dist/elements/Panel.d.ts +11 -0
- package/dist/elements/Panel.js +39 -0
- package/dist/elements/Socket.d.ts +14 -0
- package/dist/elements/Socket.js +106 -0
- package/dist/elements/World.d.ts +45 -0
- package/dist/elements/World.js +615 -0
- package/dist/hooks.d.ts +15 -0
- package/dist/hooks.js +156 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +13 -0
- package/package.json +31 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
import { AnodeReactContext, useAnode, useSelection, useViewport } from "../context.js";
|
|
2
|
+
import { useEdges, useGroups, useVisibleNodes } from "../hooks.js";
|
|
3
|
+
import { Node } from "./Node.js";
|
|
4
|
+
import { Group } from "./Group.js";
|
|
5
|
+
import { Link as Link$1 } from "./Link.js";
|
|
6
|
+
import React, { useContext, useEffect, useRef, useState } from "react";
|
|
7
|
+
import { Context, LinkKind, Rect, Vec2 } from "@stuly/anode";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
|
|
10
|
+
//#region src/elements/World.tsx
|
|
11
|
+
const DefaultNode = ({ entity }) => /* @__PURE__ */ jsx("div", {
|
|
12
|
+
style: {
|
|
13
|
+
padding: 10,
|
|
14
|
+
background: "white",
|
|
15
|
+
border: "1px solid #ccc",
|
|
16
|
+
borderRadius: 4
|
|
17
|
+
},
|
|
18
|
+
children: entity.inner?.label || `Node ${entity.id}`
|
|
19
|
+
});
|
|
20
|
+
const getDistance = (t1, t2) => {
|
|
21
|
+
return Math.sqrt(Math.pow(t2.clientX - t1.clientX, 2) + Math.pow(t2.clientY - t1.clientY, 2));
|
|
22
|
+
};
|
|
23
|
+
const getCenter = (t1, t2) => {
|
|
24
|
+
return {
|
|
25
|
+
x: (t1.clientX + t2.clientX) / 2,
|
|
26
|
+
y: (t1.clientY + t2.clientY) / 2
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
const World = ({ children, style, nodeTypes = {}, linkTypes = {}, defaultLinkKind = LinkKind.BEZIER, onConnect, isValidConnection, selectionBoxStyle, nodes, links: linksProp, onNodesChange, onLinksChange }) => {
|
|
30
|
+
const ctx = useAnode();
|
|
31
|
+
const { viewport: transform, setViewport: setTransform, screenToWorld } = useViewport();
|
|
32
|
+
const { setScreenToWorld } = useContext(AnodeReactContext);
|
|
33
|
+
const { selection, setSelection } = useSelection();
|
|
34
|
+
const worldRef = useRef(null);
|
|
35
|
+
const [selectionBox, setSelectionBox] = useState(null);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
setScreenToWorld(() => (clientX, clientY) => {
|
|
38
|
+
if (!worldRef.current) return {
|
|
39
|
+
x: clientX,
|
|
40
|
+
y: clientY
|
|
41
|
+
};
|
|
42
|
+
const rect = worldRef.current.getBoundingClientRect();
|
|
43
|
+
return {
|
|
44
|
+
x: (clientX - rect.left - transformRef.current.x) / transformRef.current.k,
|
|
45
|
+
y: (clientY - rect.top - transformRef.current.y) / transformRef.current.k
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
}, [setScreenToWorld]);
|
|
49
|
+
const [containerSize, setContainerSize] = useState({
|
|
50
|
+
width: 0,
|
|
51
|
+
height: 0
|
|
52
|
+
});
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!nodes) return;
|
|
55
|
+
ctx.batch(() => {
|
|
56
|
+
const currentIds = new Set(ctx.entities.keys());
|
|
57
|
+
const incomingIds = new Set(nodes.map((n) => n.id));
|
|
58
|
+
for (const id of currentIds) if (!incomingIds.has(id)) {
|
|
59
|
+
const entity = ctx.entities.get(id);
|
|
60
|
+
if (entity) ctx.dropEntity(entity);
|
|
61
|
+
}
|
|
62
|
+
for (const n of nodes) {
|
|
63
|
+
const entity = ctx.entities.get(n.id);
|
|
64
|
+
const innerData = {
|
|
65
|
+
...n.data || {},
|
|
66
|
+
type: n.type
|
|
67
|
+
};
|
|
68
|
+
if (!entity) ctx.newEntity(innerData, n.id).move(n.position.x, n.position.y);
|
|
69
|
+
else {
|
|
70
|
+
if (Math.abs(entity.position.x - n.position.x) > .01 || Math.abs(entity.position.y - n.position.y) > .01) entity.move(n.position.x, n.position.y);
|
|
71
|
+
entity.setInner(innerData);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}, "Sync Nodes from Props");
|
|
75
|
+
}, [ctx, nodes]);
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!linksProp) return;
|
|
78
|
+
const syncLinks = () => {
|
|
79
|
+
ctx.batch(() => {
|
|
80
|
+
const currentIds = new Set(ctx.links.keys());
|
|
81
|
+
const incomingIds = new Set(linksProp.map((l) => l.id));
|
|
82
|
+
for (const id of currentIds) if (!incomingIds.has(id)) {
|
|
83
|
+
const link = ctx.links.get(id);
|
|
84
|
+
if (link) ctx.dropLink(link);
|
|
85
|
+
}
|
|
86
|
+
for (const l of linksProp) {
|
|
87
|
+
const innerData = {
|
|
88
|
+
...l.data || {},
|
|
89
|
+
type: l.type
|
|
90
|
+
};
|
|
91
|
+
const link = ctx.links.get(l.id);
|
|
92
|
+
if (!link) {
|
|
93
|
+
const fromNode = ctx.entities.get(l.source);
|
|
94
|
+
const toNode = ctx.entities.get(l.target);
|
|
95
|
+
if (fromNode && toNode) {
|
|
96
|
+
const fromSocket = Array.from(fromNode.sockets.values()).find((s) => s.name === l.sourceHandle);
|
|
97
|
+
const toSocket = Array.from(toNode.sockets.values()).find((s) => s.name === l.targetHandle);
|
|
98
|
+
if (fromSocket && toSocket) {
|
|
99
|
+
const newLink = ctx.newLink(fromSocket, toSocket, l.kind || defaultLinkKind, l.id, innerData);
|
|
100
|
+
if (newLink && l.waypoints) newLink.waypoints = l.waypoints.map((p) => new Vec2(p.x, p.y));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
link.inner = innerData;
|
|
105
|
+
if (l.waypoints) link.waypoints = l.waypoints.map((p) => new Vec2(p.x, p.y));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}, "Sync Links from Props");
|
|
109
|
+
};
|
|
110
|
+
syncLinks();
|
|
111
|
+
const h1 = ctx.registerSocketCreateListener(syncLinks);
|
|
112
|
+
const h2 = ctx.registerSocketDropListener(syncLinks);
|
|
113
|
+
return () => {
|
|
114
|
+
ctx.unregisterListener(h1);
|
|
115
|
+
ctx.unregisterListener(h2);
|
|
116
|
+
};
|
|
117
|
+
}, [
|
|
118
|
+
ctx,
|
|
119
|
+
linksProp,
|
|
120
|
+
defaultLinkKind
|
|
121
|
+
]);
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!onNodesChange && !onLinksChange) return;
|
|
124
|
+
const notify = () => {
|
|
125
|
+
if (onNodesChange && nodes) onNodesChange(Array.from(ctx.entities.values()).map((e) => ({
|
|
126
|
+
id: e.id,
|
|
127
|
+
position: {
|
|
128
|
+
x: e.position.x,
|
|
129
|
+
y: e.position.y
|
|
130
|
+
},
|
|
131
|
+
type: e.inner?.type,
|
|
132
|
+
data: e.inner
|
|
133
|
+
})), ctx);
|
|
134
|
+
if (onLinksChange && linksProp) onLinksChange(Array.from(ctx.links.values()).map((l) => {
|
|
135
|
+
const fromSocket = ctx.sockets.get(l.from);
|
|
136
|
+
const toSocket = ctx.sockets.get(l.to);
|
|
137
|
+
return {
|
|
138
|
+
id: l.id,
|
|
139
|
+
source: fromSocket?.entityId || 0,
|
|
140
|
+
sourceHandle: fromSocket?.name || "",
|
|
141
|
+
target: toSocket?.entityId || 0,
|
|
142
|
+
targetHandle: toSocket?.name || "",
|
|
143
|
+
kind: l.kind,
|
|
144
|
+
type: l.inner?.type,
|
|
145
|
+
data: l.inner,
|
|
146
|
+
waypoints: l.waypoints.map((p) => ({
|
|
147
|
+
x: p.x,
|
|
148
|
+
y: p.y
|
|
149
|
+
}))
|
|
150
|
+
};
|
|
151
|
+
}), ctx);
|
|
152
|
+
};
|
|
153
|
+
const handles = [
|
|
154
|
+
ctx.registerEntityCreateListener(notify),
|
|
155
|
+
ctx.registerEntityDropListener(notify),
|
|
156
|
+
ctx.registerEntityMoveListener(notify),
|
|
157
|
+
ctx.registerLinkCreateListener(notify),
|
|
158
|
+
ctx.registerLinkDropListener(notify),
|
|
159
|
+
ctx.registerLinkUpdateListener(notify),
|
|
160
|
+
ctx.registerBulkChangeListener(notify)
|
|
161
|
+
];
|
|
162
|
+
return () => handles.forEach((h) => ctx.unregisterListener(h));
|
|
163
|
+
}, [
|
|
164
|
+
ctx,
|
|
165
|
+
onNodesChange,
|
|
166
|
+
onLinksChange,
|
|
167
|
+
nodes,
|
|
168
|
+
linksProp
|
|
169
|
+
]);
|
|
170
|
+
const transformRef = useRef(transform);
|
|
171
|
+
transformRef.current = transform;
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (!worldRef.current) return;
|
|
174
|
+
const observer = new ResizeObserver((entries) => {
|
|
175
|
+
const entry = entries[0];
|
|
176
|
+
if (!entry) return;
|
|
177
|
+
const { width, height } = entry.contentRect;
|
|
178
|
+
setContainerSize({
|
|
179
|
+
width,
|
|
180
|
+
height
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
observer.observe(worldRef.current);
|
|
184
|
+
return () => observer.disconnect();
|
|
185
|
+
}, []);
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
const el = worldRef.current;
|
|
188
|
+
if (!el) return;
|
|
189
|
+
const onWheelNative = (e) => {
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
const rect = el.getBoundingClientRect();
|
|
192
|
+
const t = transformRef.current;
|
|
193
|
+
const delta = -e.deltaY;
|
|
194
|
+
const factor = Math.pow(1.1, delta / 100);
|
|
195
|
+
const newK = Math.min(Math.max(t.k * factor, .1), 5);
|
|
196
|
+
const mouseX = e.clientX - rect.left;
|
|
197
|
+
const mouseY = e.clientY - rect.top;
|
|
198
|
+
const beforeKMouseX = (mouseX - t.x) / t.k;
|
|
199
|
+
const beforeKMouseY = (mouseY - t.y) / t.k;
|
|
200
|
+
setTransform({
|
|
201
|
+
x: mouseX - beforeKMouseX * newK,
|
|
202
|
+
y: mouseY - beforeKMouseY * newK,
|
|
203
|
+
k: newK
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
el.addEventListener("wheel", onWheelNative, { passive: false });
|
|
207
|
+
return () => el.removeEventListener("wheel", onWheelNative);
|
|
208
|
+
}, [setTransform]);
|
|
209
|
+
const entities = useVisibleNodes(containerSize);
|
|
210
|
+
const links = useEdges();
|
|
211
|
+
const groups = useGroups();
|
|
212
|
+
const [pendingLink, setPendingLink] = useState(null);
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
const onKeyDown = (e) => {
|
|
215
|
+
if (e.key === "Backspace" || e.key === "Delete") {
|
|
216
|
+
if (["INPUT", "TEXTAREA"].includes(e.target.tagName)) return;
|
|
217
|
+
ctx.batch(() => {
|
|
218
|
+
for (const nid of selection.nodes) {
|
|
219
|
+
const entity = ctx.entities.get(nid);
|
|
220
|
+
if (entity) ctx.dropEntity(entity);
|
|
221
|
+
}
|
|
222
|
+
for (const lid of selection.links) {
|
|
223
|
+
const link = ctx.links.get(lid);
|
|
224
|
+
if (link) ctx.dropLink(link);
|
|
225
|
+
}
|
|
226
|
+
}, "Delete Selection");
|
|
227
|
+
setSelection({
|
|
228
|
+
nodes: /* @__PURE__ */ new Set(),
|
|
229
|
+
links: /* @__PURE__ */ new Set()
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "z") {
|
|
233
|
+
if (e.shiftKey) ctx.redo();
|
|
234
|
+
else ctx.undo();
|
|
235
|
+
e.preventDefault();
|
|
236
|
+
}
|
|
237
|
+
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "y") {
|
|
238
|
+
ctx.redo();
|
|
239
|
+
e.preventDefault();
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
window.addEventListener("keydown", onKeyDown);
|
|
243
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
244
|
+
}, [
|
|
245
|
+
ctx,
|
|
246
|
+
selection,
|
|
247
|
+
setSelection
|
|
248
|
+
]);
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
const handleLinkStart = (e) => {
|
|
251
|
+
const { socketId, x, y } = e.detail;
|
|
252
|
+
const rect = worldRef.current?.getBoundingClientRect();
|
|
253
|
+
if (!rect) return;
|
|
254
|
+
const fromSocket = ctx.sockets.get(socketId);
|
|
255
|
+
if (!fromSocket) return;
|
|
256
|
+
const fromWorldPos = ctx.getWorldPosition(fromSocket.entityId);
|
|
257
|
+
setPendingLink({
|
|
258
|
+
fromId: socketId,
|
|
259
|
+
fromPos: new Vec2(fromWorldPos.x + fromSocket.offset.x, fromWorldPos.y + fromSocket.offset.y),
|
|
260
|
+
toPos: new Vec2((x - rect.left - transform.x) / transform.k, (y - rect.top - transform.y) / transform.k),
|
|
261
|
+
isValid: true
|
|
262
|
+
});
|
|
263
|
+
const onMove = (moveEvent) => {
|
|
264
|
+
const touches = moveEvent.touches;
|
|
265
|
+
const clientX = touches ? touches[0]?.clientX ?? 0 : moveEvent.clientX;
|
|
266
|
+
const clientY = touches ? touches[0]?.clientY ?? 0 : moveEvent.clientY;
|
|
267
|
+
const targetSocketEl = (touches ? document.elementFromPoint(clientX, clientY) : moveEvent.target)?.closest(".anode-socket");
|
|
268
|
+
let isValid = true;
|
|
269
|
+
if (targetSocketEl) {
|
|
270
|
+
const toId = parseInt(targetSocketEl.getAttribute("data-socket-id") || "");
|
|
271
|
+
const from = ctx.sockets.get(socketId);
|
|
272
|
+
const to = ctx.sockets.get(toId);
|
|
273
|
+
if (from && to) isValid = ctx.canLink(from, to) && (!isValidConnection || isValidConnection(from, to, ctx));
|
|
274
|
+
}
|
|
275
|
+
setPendingLink((prev) => prev ? {
|
|
276
|
+
...prev,
|
|
277
|
+
toPos: new Vec2((clientX - rect.left - transform.x) / transform.k, (clientY - rect.top - transform.y) / transform.k),
|
|
278
|
+
isValid
|
|
279
|
+
} : null);
|
|
280
|
+
};
|
|
281
|
+
const onUp = (upEvent) => {
|
|
282
|
+
document.removeEventListener("mousemove", onMove);
|
|
283
|
+
document.removeEventListener("mouseup", onUp);
|
|
284
|
+
document.removeEventListener("touchmove", onMove);
|
|
285
|
+
document.removeEventListener("touchend", onUp);
|
|
286
|
+
let target = null;
|
|
287
|
+
const changedTouches = upEvent.changedTouches;
|
|
288
|
+
if (changedTouches) {
|
|
289
|
+
const touch = changedTouches[0];
|
|
290
|
+
target = touch ? document.elementFromPoint(touch.clientX, touch.clientY) : null;
|
|
291
|
+
} else target = upEvent.target;
|
|
292
|
+
const targetSocket = target?.closest(".anode-socket");
|
|
293
|
+
if (targetSocket) {
|
|
294
|
+
const toId = parseInt(targetSocket.getAttribute("data-socket-id") || "");
|
|
295
|
+
const from = ctx.sockets.get(socketId);
|
|
296
|
+
const to = ctx.sockets.get(toId);
|
|
297
|
+
if (from && to) {
|
|
298
|
+
if (ctx.canLink(from, to) && (!isValidConnection || isValidConnection(from, to, ctx))) if (onConnect) onConnect(socketId, toId, ctx);
|
|
299
|
+
else ctx.newLink(from, to, defaultLinkKind);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
setPendingLink(null);
|
|
303
|
+
};
|
|
304
|
+
document.addEventListener("mousemove", onMove);
|
|
305
|
+
document.addEventListener("mouseup", onUp);
|
|
306
|
+
document.addEventListener("touchmove", onMove, { passive: false });
|
|
307
|
+
document.addEventListener("touchend", onUp);
|
|
308
|
+
};
|
|
309
|
+
const handleReconnect = (e) => {
|
|
310
|
+
const { linkId, type, x, y } = e.detail;
|
|
311
|
+
const rect = worldRef.current?.getBoundingClientRect();
|
|
312
|
+
if (!rect) return;
|
|
313
|
+
const link = ctx.links.get(linkId);
|
|
314
|
+
if (!link) return;
|
|
315
|
+
const otherSocketId = type === "from" ? link.to : link.from;
|
|
316
|
+
const otherSocket = ctx.sockets.get(otherSocketId);
|
|
317
|
+
if (!otherSocket) return;
|
|
318
|
+
const otherNodeWorldPos = ctx.getWorldPosition(otherSocket.entityId);
|
|
319
|
+
setPendingLink({
|
|
320
|
+
fromId: otherSocketId,
|
|
321
|
+
fromPos: new Vec2(otherNodeWorldPos.x + otherSocket.offset.x, otherNodeWorldPos.y + otherSocket.offset.y),
|
|
322
|
+
toPos: new Vec2((x - rect.left - transform.x) / transform.k, (y - rect.top - transform.y) / transform.k),
|
|
323
|
+
isValid: true
|
|
324
|
+
});
|
|
325
|
+
const onMove = (moveEvent) => {
|
|
326
|
+
const touches = moveEvent.touches;
|
|
327
|
+
const clientX = touches ? touches[0]?.clientX ?? 0 : moveEvent.clientX;
|
|
328
|
+
const clientY = touches ? touches[0]?.clientY ?? 0 : moveEvent.clientY;
|
|
329
|
+
const targetSocketEl = (touches ? document.elementFromPoint(clientX, clientY) : moveEvent.target)?.closest(".anode-socket");
|
|
330
|
+
let isValid = true;
|
|
331
|
+
if (targetSocketEl) {
|
|
332
|
+
const toId = parseInt(targetSocketEl.getAttribute("data-socket-id") || "");
|
|
333
|
+
const toSocket = ctx.sockets.get(toId);
|
|
334
|
+
if (toSocket) {
|
|
335
|
+
const newFrom = type === "from" ? toSocket : otherSocket;
|
|
336
|
+
const newTo = type === "from" ? otherSocket : toSocket;
|
|
337
|
+
isValid = ctx.canLink(newFrom, newTo) && (!isValidConnection || isValidConnection(newFrom, newTo, ctx));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
setPendingLink((prev) => prev ? {
|
|
341
|
+
...prev,
|
|
342
|
+
toPos: new Vec2((clientX - rect.left - transform.x) / transform.k, (clientY - rect.top - transform.y) / transform.k),
|
|
343
|
+
isValid
|
|
344
|
+
} : null);
|
|
345
|
+
};
|
|
346
|
+
const onUp = (upEvent) => {
|
|
347
|
+
document.removeEventListener("mousemove", onMove);
|
|
348
|
+
document.removeEventListener("mouseup", onUp);
|
|
349
|
+
document.removeEventListener("touchmove", onMove);
|
|
350
|
+
document.removeEventListener("touchend", onUp);
|
|
351
|
+
let target = null;
|
|
352
|
+
const changedTouches = upEvent.changedTouches;
|
|
353
|
+
if (changedTouches) {
|
|
354
|
+
const touch = changedTouches[0];
|
|
355
|
+
target = touch ? document.elementFromPoint(touch.clientX, touch.clientY) : null;
|
|
356
|
+
} else target = upEvent.target;
|
|
357
|
+
const targetSocketEl = target?.closest(".anode-socket");
|
|
358
|
+
if (targetSocketEl) {
|
|
359
|
+
const newSocketId = parseInt(targetSocketEl.getAttribute("data-socket-id") || "");
|
|
360
|
+
const newSocket = ctx.sockets.get(newSocketId);
|
|
361
|
+
if (newSocket) {
|
|
362
|
+
const newFrom = type === "from" ? newSocket : otherSocket;
|
|
363
|
+
const newTo = type === "from" ? otherSocket : newSocket;
|
|
364
|
+
if (ctx.canLink(newFrom, newTo) && (!isValidConnection || isValidConnection(newFrom, newTo, ctx))) ctx.updateLink(link, newFrom.id, newTo.id);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
setPendingLink(null);
|
|
368
|
+
};
|
|
369
|
+
document.addEventListener("mousemove", onMove);
|
|
370
|
+
document.addEventListener("mouseup", onUp);
|
|
371
|
+
document.addEventListener("touchmove", onMove, { passive: false });
|
|
372
|
+
document.addEventListener("touchend", onUp);
|
|
373
|
+
};
|
|
374
|
+
const el = worldRef.current;
|
|
375
|
+
el?.addEventListener("anode-link-start", handleLinkStart);
|
|
376
|
+
el?.addEventListener("anode-link-reconnect", handleReconnect);
|
|
377
|
+
return () => {
|
|
378
|
+
el?.removeEventListener("anode-link-start", handleLinkStart);
|
|
379
|
+
el?.removeEventListener("anode-link-reconnect", handleReconnect);
|
|
380
|
+
};
|
|
381
|
+
}, [
|
|
382
|
+
ctx,
|
|
383
|
+
transform,
|
|
384
|
+
defaultLinkKind,
|
|
385
|
+
onConnect,
|
|
386
|
+
isValidConnection
|
|
387
|
+
]);
|
|
388
|
+
const onMouseDown = (e) => {
|
|
389
|
+
if (e.button !== 0) return;
|
|
390
|
+
if (e.target !== worldRef.current) return;
|
|
391
|
+
if (e.altKey) {
|
|
392
|
+
const rect = worldRef.current.getBoundingClientRect();
|
|
393
|
+
const startX = e.clientX - rect.left;
|
|
394
|
+
const startY = e.clientY - rect.top;
|
|
395
|
+
setSelectionBox({
|
|
396
|
+
startX,
|
|
397
|
+
startY,
|
|
398
|
+
endX: startX,
|
|
399
|
+
endY: startY
|
|
400
|
+
});
|
|
401
|
+
const onMouseMove = (moveEvent) => {
|
|
402
|
+
setSelectionBox((prev) => prev ? {
|
|
403
|
+
...prev,
|
|
404
|
+
endX: moveEvent.clientX - rect.left,
|
|
405
|
+
endY: moveEvent.clientY - rect.top
|
|
406
|
+
} : null);
|
|
407
|
+
};
|
|
408
|
+
const onMouseUp = () => {
|
|
409
|
+
document.removeEventListener("mousemove", onMouseMove);
|
|
410
|
+
document.removeEventListener("mouseup", onMouseUp);
|
|
411
|
+
setSelectionBox((prev) => {
|
|
412
|
+
if (!prev) return null;
|
|
413
|
+
const rect = worldRef.current?.getBoundingClientRect();
|
|
414
|
+
if (!rect) return null;
|
|
415
|
+
const x1 = Math.min(prev.startX, prev.endX);
|
|
416
|
+
const y1 = Math.min(prev.startY, prev.endY);
|
|
417
|
+
const x2 = Math.max(prev.startX, prev.endX);
|
|
418
|
+
const y2 = Math.max(prev.startY, prev.endY);
|
|
419
|
+
const worldTopLeft = screenToWorld(x1 + rect.left, y1 + rect.top);
|
|
420
|
+
const worldBottomRight = screenToWorld(x2 + rect.left, y2 + rect.top);
|
|
421
|
+
const queryRect = new Rect(worldTopLeft.x, worldTopLeft.y, worldBottomRight.x - worldTopLeft.x, worldBottomRight.y - worldTopLeft.y);
|
|
422
|
+
const selectedIds = ctx.quadTree.query(queryRect);
|
|
423
|
+
setSelection({
|
|
424
|
+
nodes: new Set(selectedIds),
|
|
425
|
+
links: /* @__PURE__ */ new Set()
|
|
426
|
+
});
|
|
427
|
+
return null;
|
|
428
|
+
});
|
|
429
|
+
};
|
|
430
|
+
document.addEventListener("mousemove", onMouseMove);
|
|
431
|
+
document.addEventListener("mouseup", onMouseUp);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
setSelection({
|
|
435
|
+
nodes: /* @__PURE__ */ new Set(),
|
|
436
|
+
links: /* @__PURE__ */ new Set()
|
|
437
|
+
});
|
|
438
|
+
const startX = e.clientX - transform.x;
|
|
439
|
+
const startY = e.clientY - transform.y;
|
|
440
|
+
const onMouseMove = (moveEvent) => {
|
|
441
|
+
setTransform({
|
|
442
|
+
x: moveEvent.clientX - startX,
|
|
443
|
+
y: moveEvent.clientY - startY,
|
|
444
|
+
k: transformRef.current.k
|
|
445
|
+
});
|
|
446
|
+
};
|
|
447
|
+
const onMouseUp = () => {
|
|
448
|
+
document.removeEventListener("mousemove", onMouseMove);
|
|
449
|
+
document.removeEventListener("mouseup", onMouseUp);
|
|
450
|
+
};
|
|
451
|
+
document.addEventListener("mousemove", onMouseMove);
|
|
452
|
+
document.addEventListener("mouseup", onMouseUp);
|
|
453
|
+
};
|
|
454
|
+
const onTouchStart = (e) => {
|
|
455
|
+
if (e.touches.length === 1) {
|
|
456
|
+
if (e.target !== worldRef.current) return;
|
|
457
|
+
setSelection({
|
|
458
|
+
nodes: /* @__PURE__ */ new Set(),
|
|
459
|
+
links: /* @__PURE__ */ new Set()
|
|
460
|
+
});
|
|
461
|
+
const touch = e.touches[0];
|
|
462
|
+
if (!touch) return;
|
|
463
|
+
const startX = touch.clientX - transform.x;
|
|
464
|
+
const startY = touch.clientY - transform.y;
|
|
465
|
+
const onTouchMove = (moveEvent) => {
|
|
466
|
+
if (moveEvent.touches.length === 1) {
|
|
467
|
+
const touch = moveEvent.touches[0];
|
|
468
|
+
if (!touch) return;
|
|
469
|
+
setTransform({
|
|
470
|
+
x: touch.clientX - startX,
|
|
471
|
+
y: touch.clientY - startY,
|
|
472
|
+
k: transformRef.current.k
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
const onTouchEnd = () => {
|
|
477
|
+
document.removeEventListener("touchmove", onTouchMove);
|
|
478
|
+
document.removeEventListener("touchend", onTouchEnd);
|
|
479
|
+
};
|
|
480
|
+
document.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
481
|
+
document.addEventListener("touchend", onTouchEnd);
|
|
482
|
+
} else if (e.touches.length === 2) {
|
|
483
|
+
const t1 = e.touches[0];
|
|
484
|
+
const t2 = e.touches[1];
|
|
485
|
+
if (!t1 || !t2) return;
|
|
486
|
+
const initialDist = getDistance(t1, t2);
|
|
487
|
+
const initialCenter = getCenter(t1, t2);
|
|
488
|
+
const initialTransform = { ...transformRef.current };
|
|
489
|
+
const onTouchMove = (moveEvent) => {
|
|
490
|
+
if (moveEvent.touches.length === 2) {
|
|
491
|
+
const mt1 = moveEvent.touches[0];
|
|
492
|
+
const mt2 = moveEvent.touches[1];
|
|
493
|
+
if (!mt1 || !mt2) return;
|
|
494
|
+
const currentDist = getDistance(mt1, mt2);
|
|
495
|
+
const currentCenter = getCenter(mt1, mt2);
|
|
496
|
+
const factor = currentDist / initialDist;
|
|
497
|
+
const newK = Math.min(Math.max(initialTransform.k * factor, .1), 5);
|
|
498
|
+
const rect = worldRef.current?.getBoundingClientRect();
|
|
499
|
+
if (!rect) return;
|
|
500
|
+
const zoomCenterX = initialCenter.x - rect.left;
|
|
501
|
+
const zoomCenterY = initialCenter.y - rect.top;
|
|
502
|
+
const beforeKCenterX = (zoomCenterX - initialTransform.x) / initialTransform.k;
|
|
503
|
+
const beforeKCenterY = (zoomCenterY - initialTransform.y) / initialTransform.k;
|
|
504
|
+
const dx = currentCenter.x - initialCenter.x;
|
|
505
|
+
const dy = currentCenter.y - initialCenter.y;
|
|
506
|
+
setTransform({
|
|
507
|
+
x: zoomCenterX - beforeKCenterX * newK + dx,
|
|
508
|
+
y: zoomCenterY - beforeKCenterY * newK + dy,
|
|
509
|
+
k: newK
|
|
510
|
+
});
|
|
511
|
+
if (moveEvent.cancelable) moveEvent.preventDefault();
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
const onTouchEnd = () => {
|
|
515
|
+
document.removeEventListener("touchmove", onTouchMove);
|
|
516
|
+
document.removeEventListener("touchend", onTouchEnd);
|
|
517
|
+
};
|
|
518
|
+
document.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
519
|
+
document.addEventListener("touchend", onTouchEnd);
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
523
|
+
ref: worldRef,
|
|
524
|
+
className: "anode-world",
|
|
525
|
+
onMouseDown,
|
|
526
|
+
onTouchStart,
|
|
527
|
+
style: {
|
|
528
|
+
position: "relative",
|
|
529
|
+
width: "100%",
|
|
530
|
+
height: "100%",
|
|
531
|
+
overflow: "hidden",
|
|
532
|
+
userSelect: "none",
|
|
533
|
+
background: "#f1f5f9",
|
|
534
|
+
...style
|
|
535
|
+
},
|
|
536
|
+
children: [
|
|
537
|
+
children,
|
|
538
|
+
selectionBox && /* @__PURE__ */ jsx("div", { style: {
|
|
539
|
+
position: "absolute",
|
|
540
|
+
left: Math.min(selectionBox.startX, selectionBox.endX),
|
|
541
|
+
top: Math.min(selectionBox.startY, selectionBox.endY),
|
|
542
|
+
width: Math.abs(selectionBox.endX - selectionBox.startX),
|
|
543
|
+
height: Math.abs(selectionBox.endY - selectionBox.startY),
|
|
544
|
+
border: "1px solid #3b82f6",
|
|
545
|
+
background: "rgba(59, 130, 246, 0.1)",
|
|
546
|
+
pointerEvents: "none",
|
|
547
|
+
zIndex: 1e3,
|
|
548
|
+
...selectionBoxStyle
|
|
549
|
+
} }),
|
|
550
|
+
/* @__PURE__ */ jsxs("div", {
|
|
551
|
+
style: {
|
|
552
|
+
position: "absolute",
|
|
553
|
+
top: 0,
|
|
554
|
+
left: 0,
|
|
555
|
+
width: "100%",
|
|
556
|
+
height: "100%",
|
|
557
|
+
transform: `translate(${transform.x}px, ${transform.y}px) scale(${transform.k})`,
|
|
558
|
+
transformOrigin: "0 0",
|
|
559
|
+
pointerEvents: "none"
|
|
560
|
+
},
|
|
561
|
+
children: [/* @__PURE__ */ jsxs("svg", {
|
|
562
|
+
style: {
|
|
563
|
+
position: "absolute",
|
|
564
|
+
top: 0,
|
|
565
|
+
left: 0,
|
|
566
|
+
width: "100000px",
|
|
567
|
+
height: "100000px",
|
|
568
|
+
pointerEvents: "none",
|
|
569
|
+
zIndex: 0,
|
|
570
|
+
overflow: "visible"
|
|
571
|
+
},
|
|
572
|
+
children: [/* @__PURE__ */ jsx("g", {
|
|
573
|
+
style: { pointerEvents: "auto" },
|
|
574
|
+
children: links.map((link) => {
|
|
575
|
+
const Component = linkTypes[link.inner?.type || "default"];
|
|
576
|
+
return /* @__PURE__ */ jsx(Link$1, {
|
|
577
|
+
id: link.id,
|
|
578
|
+
component: Component
|
|
579
|
+
}, link.id);
|
|
580
|
+
})
|
|
581
|
+
}), pendingLink && /* @__PURE__ */ jsx("line", {
|
|
582
|
+
x1: pendingLink.fromPos.x,
|
|
583
|
+
y1: pendingLink.fromPos.y,
|
|
584
|
+
x2: pendingLink.toPos.x,
|
|
585
|
+
y2: pendingLink.toPos.y,
|
|
586
|
+
stroke: pendingLink.isValid ? "#94a3b8" : "#ef4444",
|
|
587
|
+
strokeWidth: 2 / transform.k,
|
|
588
|
+
strokeDasharray: 4 / transform.k
|
|
589
|
+
})]
|
|
590
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
591
|
+
style: {
|
|
592
|
+
position: "absolute",
|
|
593
|
+
top: 0,
|
|
594
|
+
left: 0,
|
|
595
|
+
zIndex: 1,
|
|
596
|
+
pointerEvents: "none"
|
|
597
|
+
},
|
|
598
|
+
children: [groups.map((group) => /* @__PURE__ */ jsx(Group, { id: group.id }, group.id)), entities.map((entity) => {
|
|
599
|
+
const Component = nodeTypes[entity.inner?.type || "default"] || DefaultNode;
|
|
600
|
+
return /* @__PURE__ */ jsx(Node, {
|
|
601
|
+
id: entity.id,
|
|
602
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
603
|
+
style: { pointerEvents: "auto" },
|
|
604
|
+
children: /* @__PURE__ */ jsx(Component, { entity })
|
|
605
|
+
})
|
|
606
|
+
}, entity.id);
|
|
607
|
+
})]
|
|
608
|
+
})]
|
|
609
|
+
})
|
|
610
|
+
]
|
|
611
|
+
});
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
//#endregion
|
|
615
|
+
export { World };
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as _stuly_anode0 from "@stuly/anode";
|
|
2
|
+
import { Entity, Link } from "@stuly/anode";
|
|
3
|
+
|
|
4
|
+
//#region src/hooks.d.ts
|
|
5
|
+
declare const useNodes: () => Entity<any>[];
|
|
6
|
+
declare const useVisibleNodes: (containerRect?: {
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}) => Entity<any>[];
|
|
10
|
+
declare const useEdges: () => Link<any>[];
|
|
11
|
+
declare const useEntitySockets: (entityId: number) => any[];
|
|
12
|
+
declare function useSocketValue<T = any>(socketId: number | null): T;
|
|
13
|
+
declare const useGroups: () => _stuly_anode0.Group[];
|
|
14
|
+
//#endregion
|
|
15
|
+
export { useEdges, useEntitySockets, useGroups, useNodes, useSocketValue, useVisibleNodes };
|