@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.
@@ -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 };