@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,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Entity } from "@stuly/anode";
|
|
3
|
+
|
|
4
|
+
//#region src/elements/Node.d.ts
|
|
5
|
+
interface NodeProps {
|
|
6
|
+
id: number;
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
interface NodeComponentProps {
|
|
10
|
+
entity: Entity;
|
|
11
|
+
}
|
|
12
|
+
declare const Node: React.FC<NodeProps>;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { Node, NodeComponentProps };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { useAnode, useSelection, useViewport } from "../context.js";
|
|
2
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Entity } from "@stuly/anode";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
|
|
6
|
+
//#region src/elements/Node.tsx
|
|
7
|
+
const Node = ({ id, children }) => {
|
|
8
|
+
const ctx = useAnode();
|
|
9
|
+
const { viewport } = useViewport();
|
|
10
|
+
const { selection, setSelection } = useSelection();
|
|
11
|
+
const entity = ctx.entities.get(id);
|
|
12
|
+
const [, setTick] = useState(0);
|
|
13
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!entity) return;
|
|
16
|
+
const onMove = (e) => {
|
|
17
|
+
if (e.id === id) setTick((t) => t + 1);
|
|
18
|
+
};
|
|
19
|
+
const handle = ctx.registerEntityMoveListener(onMove);
|
|
20
|
+
return () => {
|
|
21
|
+
ctx.unregisterListener(handle);
|
|
22
|
+
};
|
|
23
|
+
}, [
|
|
24
|
+
ctx,
|
|
25
|
+
id,
|
|
26
|
+
entity
|
|
27
|
+
]);
|
|
28
|
+
if (!entity) return null;
|
|
29
|
+
const worldPos = ctx.getWorldPosition(id);
|
|
30
|
+
const isSelected = selection.nodes.has(id);
|
|
31
|
+
return /* @__PURE__ */ jsx("div", {
|
|
32
|
+
className: "anode-node",
|
|
33
|
+
style: {
|
|
34
|
+
position: "absolute",
|
|
35
|
+
left: worldPos.x,
|
|
36
|
+
top: worldPos.y,
|
|
37
|
+
transform: "translate(-50%, -50%)",
|
|
38
|
+
cursor: isDragging ? "grabbing" : "grab",
|
|
39
|
+
outline: isSelected ? "2px solid #3b82f6" : "none",
|
|
40
|
+
borderRadius: 4,
|
|
41
|
+
transition: isDragging ? "none" : "left 0.15s ease-out, top 0.15s ease-out",
|
|
42
|
+
zIndex: isDragging ? 1e3 : 1
|
|
43
|
+
},
|
|
44
|
+
onMouseDown: (e) => {
|
|
45
|
+
if (e.button !== 0) return;
|
|
46
|
+
setIsDragging(true);
|
|
47
|
+
if (e.shiftKey) setSelection((prev) => {
|
|
48
|
+
const next = new Set(prev.nodes);
|
|
49
|
+
if (next.has(id)) next.delete(id);
|
|
50
|
+
else next.add(id);
|
|
51
|
+
return {
|
|
52
|
+
...prev,
|
|
53
|
+
nodes: next
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
else setSelection({
|
|
57
|
+
nodes: new Set([id]),
|
|
58
|
+
links: /* @__PURE__ */ new Set()
|
|
59
|
+
});
|
|
60
|
+
const startX = e.clientX;
|
|
61
|
+
const startY = e.clientY;
|
|
62
|
+
const startPosX = entity.position.x;
|
|
63
|
+
const startPosY = entity.position.y;
|
|
64
|
+
const onMouseMove = (moveEvent) => {
|
|
65
|
+
const dx = (moveEvent.clientX - startX) / viewport.k;
|
|
66
|
+
const dy = (moveEvent.clientY - startY) / viewport.k;
|
|
67
|
+
let newX = startPosX + dx;
|
|
68
|
+
let newY = startPosY + dy;
|
|
69
|
+
const gridSize = 15;
|
|
70
|
+
newX = Math.round(newX / gridSize) * gridSize;
|
|
71
|
+
newY = Math.round(newY / gridSize) * gridSize;
|
|
72
|
+
entity.move(newX, newY);
|
|
73
|
+
};
|
|
74
|
+
const onMouseUp = () => {
|
|
75
|
+
setIsDragging(false);
|
|
76
|
+
document.removeEventListener("mousemove", onMouseMove);
|
|
77
|
+
document.removeEventListener("mouseup", onMouseUp);
|
|
78
|
+
};
|
|
79
|
+
document.addEventListener("mousemove", onMouseMove);
|
|
80
|
+
document.addEventListener("mouseup", onMouseUp);
|
|
81
|
+
e.stopPropagation();
|
|
82
|
+
},
|
|
83
|
+
onTouchStart: (e) => {
|
|
84
|
+
setIsDragging(true);
|
|
85
|
+
setSelection({
|
|
86
|
+
nodes: new Set([id]),
|
|
87
|
+
links: /* @__PURE__ */ new Set()
|
|
88
|
+
});
|
|
89
|
+
const touch = e.touches[0];
|
|
90
|
+
if (!touch) return;
|
|
91
|
+
const startX = touch.clientX;
|
|
92
|
+
const startY = touch.clientY;
|
|
93
|
+
const startPosX = entity.position.x;
|
|
94
|
+
const startPosY = entity.position.y;
|
|
95
|
+
const onTouchMove = (moveEvent) => {
|
|
96
|
+
const touch = moveEvent.touches[0];
|
|
97
|
+
if (!touch) return;
|
|
98
|
+
const dx = (touch.clientX - startX) / viewport.k;
|
|
99
|
+
const dy = (touch.clientY - startY) / viewport.k;
|
|
100
|
+
let newX = startPosX + dx;
|
|
101
|
+
let newY = startPosY + dy;
|
|
102
|
+
const gridSize = 15;
|
|
103
|
+
newX = Math.round(newX / gridSize) * gridSize;
|
|
104
|
+
newY = Math.round(newY / gridSize) * gridSize;
|
|
105
|
+
entity.move(newX, newY);
|
|
106
|
+
if (moveEvent.cancelable) moveEvent.preventDefault();
|
|
107
|
+
};
|
|
108
|
+
const onTouchEnd = () => {
|
|
109
|
+
setIsDragging(false);
|
|
110
|
+
document.removeEventListener("touchmove", onTouchMove);
|
|
111
|
+
document.removeEventListener("touchend", onTouchEnd);
|
|
112
|
+
};
|
|
113
|
+
document.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
114
|
+
document.addEventListener("touchend", onTouchEnd);
|
|
115
|
+
e.stopPropagation();
|
|
116
|
+
},
|
|
117
|
+
children
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
122
|
+
export { Node };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/elements/Panel.d.ts
|
|
4
|
+
type PanelPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
5
|
+
declare const Panel: React.FC<{
|
|
6
|
+
position?: PanelPosition;
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
style?: React.CSSProperties;
|
|
9
|
+
}>;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { Panel };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/elements/Panel.tsx
|
|
5
|
+
const Panel = ({ position = "top-left", children, style }) => {
|
|
6
|
+
const getPositionStyle = () => {
|
|
7
|
+
switch (position) {
|
|
8
|
+
case "top-left": return {
|
|
9
|
+
top: 20,
|
|
10
|
+
left: 20
|
|
11
|
+
};
|
|
12
|
+
case "top-right": return {
|
|
13
|
+
top: 20,
|
|
14
|
+
right: 20
|
|
15
|
+
};
|
|
16
|
+
case "bottom-left": return {
|
|
17
|
+
bottom: 20,
|
|
18
|
+
left: 20
|
|
19
|
+
};
|
|
20
|
+
case "bottom-right": return {
|
|
21
|
+
bottom: 20,
|
|
22
|
+
right: 20
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
return /* @__PURE__ */ jsx("div", {
|
|
27
|
+
className: `anode-panel anode-panel-${position}`,
|
|
28
|
+
style: {
|
|
29
|
+
position: "absolute",
|
|
30
|
+
zIndex: 100,
|
|
31
|
+
...getPositionStyle(),
|
|
32
|
+
...style
|
|
33
|
+
},
|
|
34
|
+
children
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
export { Panel };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { SocketKind } from "@stuly/anode";
|
|
3
|
+
|
|
4
|
+
//#region src/elements/Socket.d.ts
|
|
5
|
+
interface SocketProps {
|
|
6
|
+
entityId: number;
|
|
7
|
+
kind: SocketKind;
|
|
8
|
+
name?: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
style?: React.CSSProperties;
|
|
11
|
+
}
|
|
12
|
+
declare const Socket: React.FC<SocketProps>;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { Socket };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useAnode, useViewport } from "../context.js";
|
|
2
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { SocketKind } from "@stuly/anode";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
|
|
6
|
+
//#region src/elements/Socket.tsx
|
|
7
|
+
const Socket = ({ entityId, kind, name, className, style }) => {
|
|
8
|
+
const ctx = useAnode();
|
|
9
|
+
const { viewport } = useViewport();
|
|
10
|
+
const ref = useRef(null);
|
|
11
|
+
const [socketId, setSocketId] = useState(null);
|
|
12
|
+
const createdSocketId = useRef(null);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const entity = ctx.entities.get(entityId);
|
|
15
|
+
if (!entity) return;
|
|
16
|
+
const existingSocket = Array.from(entity.sockets.values()).find((s) => s.kind === kind && s.name === name);
|
|
17
|
+
if (existingSocket) {
|
|
18
|
+
createdSocketId.current = existingSocket.id;
|
|
19
|
+
setSocketId(existingSocket.id);
|
|
20
|
+
} else if (createdSocketId.current === null) {
|
|
21
|
+
const socket = ctx.newSocket(entity, kind, name);
|
|
22
|
+
createdSocketId.current = socket.id;
|
|
23
|
+
setSocketId(socket.id);
|
|
24
|
+
}
|
|
25
|
+
return () => {
|
|
26
|
+
createdSocketId.current = null;
|
|
27
|
+
setSocketId(null);
|
|
28
|
+
};
|
|
29
|
+
}, [
|
|
30
|
+
ctx,
|
|
31
|
+
entityId,
|
|
32
|
+
kind,
|
|
33
|
+
name
|
|
34
|
+
]);
|
|
35
|
+
const updateOffset = useCallback(() => {
|
|
36
|
+
const sid = socketId ?? createdSocketId.current;
|
|
37
|
+
if (!ref.current || sid === null) return;
|
|
38
|
+
if (!ctx.entities.get(entityId)) return;
|
|
39
|
+
const socket = ctx.sockets.get(sid);
|
|
40
|
+
if (!socket) return;
|
|
41
|
+
const rect = ref.current.getBoundingClientRect();
|
|
42
|
+
const parentNode = ref.current.closest(".anode-node");
|
|
43
|
+
if (!parentNode) return;
|
|
44
|
+
const parentRect = parentNode.getBoundingClientRect();
|
|
45
|
+
socket.offset.set((rect.left + rect.width / 2 - (parentRect.left + parentRect.width / 2)) / viewport.k, (rect.top + rect.height / 2 - (parentRect.top + parentRect.height / 2)) / viewport.k);
|
|
46
|
+
ctx.notifySocketMove(socket);
|
|
47
|
+
}, [
|
|
48
|
+
ctx,
|
|
49
|
+
entityId,
|
|
50
|
+
socketId,
|
|
51
|
+
viewport.k
|
|
52
|
+
]);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
updateOffset();
|
|
55
|
+
window.addEventListener("resize", updateOffset);
|
|
56
|
+
return () => window.removeEventListener("resize", updateOffset);
|
|
57
|
+
}, [updateOffset]);
|
|
58
|
+
return /* @__PURE__ */ jsx("div", {
|
|
59
|
+
ref,
|
|
60
|
+
className: `anode-socket ${className || ""}`,
|
|
61
|
+
"data-socket-id": socketId,
|
|
62
|
+
"data-socket-kind": kind,
|
|
63
|
+
style: {
|
|
64
|
+
width: 12,
|
|
65
|
+
height: 12,
|
|
66
|
+
borderRadius: "50%",
|
|
67
|
+
background: kind === SocketKind.INPUT ? "#4ade80" : "#f87171",
|
|
68
|
+
border: "2px solid white",
|
|
69
|
+
cursor: "crosshair",
|
|
70
|
+
...style
|
|
71
|
+
},
|
|
72
|
+
onMouseDown: (e) => {
|
|
73
|
+
e.stopPropagation();
|
|
74
|
+
if (socketId === null) return;
|
|
75
|
+
const event = new CustomEvent("anode-link-start", {
|
|
76
|
+
bubbles: true,
|
|
77
|
+
detail: {
|
|
78
|
+
socketId,
|
|
79
|
+
kind,
|
|
80
|
+
x: e.clientX,
|
|
81
|
+
y: e.clientY
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
ref.current?.dispatchEvent(event);
|
|
85
|
+
},
|
|
86
|
+
onTouchStart: (e) => {
|
|
87
|
+
e.stopPropagation();
|
|
88
|
+
if (socketId === null) return;
|
|
89
|
+
const touch = e.touches[0];
|
|
90
|
+
if (!touch) return;
|
|
91
|
+
const event = new CustomEvent("anode-link-start", {
|
|
92
|
+
bubbles: true,
|
|
93
|
+
detail: {
|
|
94
|
+
socketId,
|
|
95
|
+
kind,
|
|
96
|
+
x: touch.clientX,
|
|
97
|
+
y: touch.clientY
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
ref.current?.dispatchEvent(event);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
//#endregion
|
|
106
|
+
export { Socket };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { NodeComponentProps } from "./Node.js";
|
|
2
|
+
import { LinkComponentProps } from "./Link.js";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Context, LinkKind } from "@stuly/anode";
|
|
5
|
+
|
|
6
|
+
//#region src/elements/World.d.ts
|
|
7
|
+
interface NodeData {
|
|
8
|
+
id: number;
|
|
9
|
+
position: {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
};
|
|
13
|
+
type?: string;
|
|
14
|
+
data?: any;
|
|
15
|
+
}
|
|
16
|
+
interface LinkData {
|
|
17
|
+
id: number;
|
|
18
|
+
source: number;
|
|
19
|
+
sourceHandle: string;
|
|
20
|
+
target: number;
|
|
21
|
+
targetHandle: string;
|
|
22
|
+
type?: string;
|
|
23
|
+
data?: any;
|
|
24
|
+
kind?: LinkKind;
|
|
25
|
+
waypoints?: {
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
}[];
|
|
29
|
+
}
|
|
30
|
+
declare const World: React.FC<{
|
|
31
|
+
children?: React.ReactNode;
|
|
32
|
+
style?: React.CSSProperties;
|
|
33
|
+
selectionBoxStyle?: React.CSSProperties;
|
|
34
|
+
nodeTypes?: Record<string, React.ComponentType<NodeComponentProps>>;
|
|
35
|
+
linkTypes?: Record<string, React.ComponentType<LinkComponentProps>>;
|
|
36
|
+
defaultLinkKind?: LinkKind;
|
|
37
|
+
onConnect?: (fromId: number, toId: number, ctx: Context<any>) => void;
|
|
38
|
+
isValidConnection?: (from: any, to: any, ctx: Context<any>) => boolean;
|
|
39
|
+
nodes?: NodeData[];
|
|
40
|
+
links?: LinkData[];
|
|
41
|
+
onNodesChange?: (nodes: NodeData[], ctx: Context<any>) => void;
|
|
42
|
+
onLinksChange?: (links: LinkData[], ctx: Context<any>) => void;
|
|
43
|
+
}>;
|
|
44
|
+
//#endregion
|
|
45
|
+
export { LinkData, NodeData, World };
|