@stuly/anode-react 0.1.0 → 0.1.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 +23 -12
- package/dist/elements/Group.js +3 -3
- package/dist/elements/World.d.ts +48 -11
- package/dist/elements/World.js +24 -2
- package/dist/hooks.d.ts +47 -3
- package/dist/hooks.js +47 -2
- package/package.json +18 -4
package/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
# anode-react
|
|
1
|
+
# @stuly/anode-react
|
|
2
2
|
|
|
3
|
-
React bindings and components for Anode, providing a declarative layer over
|
|
4
|
-
the headless core engine.
|
|
3
|
+
React bindings and components for Anode, providing a declarative layer over the headless core engine.
|
|
5
4
|
|
|
6
5
|
## Installation
|
|
7
6
|
|
|
@@ -9,14 +8,26 @@ the headless core engine.
|
|
|
9
8
|
npm install @stuly/anode-react @stuly/anode
|
|
10
9
|
```
|
|
11
10
|
|
|
12
|
-
##
|
|
11
|
+
## Quick Start
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- **Hooks:**
|
|
17
|
-
- `useAnode()`: Access the core engine.
|
|
18
|
-
- `useSocketValue()`: Subscribe to reactive data flow.
|
|
19
|
-
- `useVisibleNodes()`: Optimized spatial culling.
|
|
20
|
-
- `useEntitySockets()`: Reactive socket management.
|
|
13
|
+
```tsx
|
|
14
|
+
import { AnodeProvider, World } from '@stuly/anode-react';
|
|
21
15
|
|
|
22
|
-
|
|
16
|
+
export default function App() {
|
|
17
|
+
return (
|
|
18
|
+
<AnodeProvider>
|
|
19
|
+
<World />
|
|
20
|
+
</AnodeProvider>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Key Components & Hooks
|
|
26
|
+
|
|
27
|
+
- **`World`**: Primary canvas component.
|
|
28
|
+
- **`AnodeProvider`**: Context provider for the engine.
|
|
29
|
+
- **`useAnode()`**: Access the core engine instance.
|
|
30
|
+
- **`useSocketValue()`**: Subscribe to reactive data flow.
|
|
31
|
+
- **`useVisibleNodes()`**: Optimized spatial culling for large graphs.
|
|
32
|
+
|
|
33
|
+
For detailed documentation, usage examples, and core principles, see the [Full README](https://github.com/stulyproject/anode?tab=readme-ov-file).
|
package/dist/elements/Group.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useAnode, useViewport } from "../context.js";
|
|
2
2
|
import React, { useState } from "react";
|
|
3
|
-
import { Rect, Vec2 } from "@stuly/anode";
|
|
3
|
+
import { Group, Rect, Vec2 } from "@stuly/anode";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
|
|
6
6
|
//#region src/elements/Group.tsx
|
|
7
|
-
const Group = ({ id, children }) => {
|
|
7
|
+
const Group$1 = ({ id, children }) => {
|
|
8
8
|
const ctx = useAnode();
|
|
9
9
|
const { viewport } = useViewport();
|
|
10
10
|
const group = ctx.groups.get(id);
|
|
@@ -83,4 +83,4 @@ const Group = ({ id, children }) => {
|
|
|
83
83
|
};
|
|
84
84
|
|
|
85
85
|
//#endregion
|
|
86
|
-
export { Group };
|
|
86
|
+
export { Group$1 as Group };
|
package/dist/elements/World.d.ts
CHANGED
|
@@ -4,41 +4,78 @@ import React from "react";
|
|
|
4
4
|
import { Context, LinkKind } from "@stuly/anode";
|
|
5
5
|
|
|
6
6
|
//#region src/elements/World.d.ts
|
|
7
|
+
/** Represents the minimal data for a node in declarative sync mode. */
|
|
7
8
|
interface NodeData {
|
|
9
|
+
/** The unique ID for the node. */
|
|
8
10
|
id: number;
|
|
11
|
+
/** The absolute world position. */
|
|
9
12
|
position: {
|
|
10
13
|
x: number;
|
|
11
14
|
y: number;
|
|
12
15
|
};
|
|
16
|
+
/** Type key for the `nodeTypes` map. */
|
|
13
17
|
type?: string;
|
|
18
|
+
/** Custom data stored in `entity.inner`. */
|
|
14
19
|
data?: any;
|
|
15
20
|
}
|
|
21
|
+
/** Represents the minimal data for a link in declarative sync mode. */
|
|
16
22
|
interface LinkData {
|
|
23
|
+
/** The unique ID for the link. */
|
|
17
24
|
id: number;
|
|
25
|
+
/** Source entity ID. */
|
|
18
26
|
source: number;
|
|
27
|
+
/** Name of the source socket. */
|
|
19
28
|
sourceHandle: string;
|
|
29
|
+
/** Target entity ID. */
|
|
20
30
|
target: number;
|
|
31
|
+
/** Name of the target socket. */
|
|
21
32
|
targetHandle: string;
|
|
33
|
+
/** Type key for the `linkTypes` map. */
|
|
22
34
|
type?: string;
|
|
35
|
+
/** Custom data stored in `link.inner`. */
|
|
23
36
|
data?: any;
|
|
37
|
+
/** Routing style for the link. */
|
|
24
38
|
kind?: LinkKind;
|
|
39
|
+
/** Optional custom waypoints. */
|
|
25
40
|
waypoints?: {
|
|
26
41
|
x: number;
|
|
27
42
|
y: number;
|
|
28
43
|
}[];
|
|
29
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The primary canvas component for Anode.
|
|
47
|
+
* Handles interaction (zoom, pan, selection) and synchronizes the React component
|
|
48
|
+
* tree with the internal headless engine.
|
|
49
|
+
*
|
|
50
|
+
* **Behaviors:**
|
|
51
|
+
* 1. **Zoom/Pan:** Implements standard wheel and touch interaction.
|
|
52
|
+
* 2. **Selection:** Multi-select with Shift + Click, Box select with Alt + Click.
|
|
53
|
+
* 3. **Declarative Sync:** If `nodes` or `links` are passed, the core engine
|
|
54
|
+
* automatically mirrors these arrays. Changes made directly in the UI
|
|
55
|
+
* (dragging, deleting) will trigger the corresponding `onNodesChange` callback.
|
|
56
|
+
* 4. **Spatial Culling:** Automatically uses `useVisibleNodes` to optimize rendering.
|
|
57
|
+
*
|
|
58
|
+
* **Usage:**
|
|
59
|
+
* ```tsx
|
|
60
|
+
* <World
|
|
61
|
+
* nodeTypes={{ math: MathNode }}
|
|
62
|
+
* nodes={state.nodes}
|
|
63
|
+
* onNodesChange={newNodes => setState({ nodes: newNodes })}
|
|
64
|
+
* />
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
30
67
|
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;
|
|
68
|
+
/** Elements to overlay on the world (Background, MiniMap, etc.). */children?: React.ReactNode; /** Styles applied to the outer container. */
|
|
69
|
+
style?: React.CSSProperties; /** Styles applied to the selection box overlay. */
|
|
70
|
+
selectionBoxStyle?: React.CSSProperties; /** Map of custom node components by their `type` key. */
|
|
71
|
+
nodeTypes?: Record<string, React.ComponentType<NodeComponentProps>>; /** Map of custom link overlays/UI components by their `type` key. */
|
|
72
|
+
linkTypes?: Record<string, React.ComponentType<LinkComponentProps>>; /** Default style for newly created links. */
|
|
73
|
+
defaultLinkKind?: LinkKind; /** Callback triggered when a user completes a connection via drag-and-drop. */
|
|
74
|
+
onConnect?: (fromId: number, toId: number, ctx: Context<any>) => void; /** Custom validation for link creation. Return false to prevent a connection. */
|
|
75
|
+
isValidConnection?: (from: any, to: any, ctx: Context<any>) => boolean; /** Declarative list of nodes. Use for state-controlled synchronization. */
|
|
76
|
+
nodes?: NodeData[]; /** Declarative list of links. Use for state-controlled synchronization. */
|
|
77
|
+
links?: LinkData[]; /** Callback triggered when nodes are moved or dropped. */
|
|
78
|
+
onNodesChange?: (nodes: NodeData[], ctx: Context<any>) => void; /** Callback triggered when links are updated or deleted. */
|
|
42
79
|
onLinksChange?: (links: LinkData[], ctx: Context<any>) => void;
|
|
43
80
|
}>;
|
|
44
81
|
//#endregion
|
package/dist/elements/World.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AnodeReactContext, useAnode, useSelection, useViewport } from "../context.js";
|
|
2
2
|
import { useEdges, useGroups, useVisibleNodes } from "../hooks.js";
|
|
3
3
|
import { Node } from "./Node.js";
|
|
4
|
-
import { Group } from "./Group.js";
|
|
4
|
+
import { Group as Group$1 } from "./Group.js";
|
|
5
5
|
import { Link as Link$1 } from "./Link.js";
|
|
6
6
|
import React, { useContext, useEffect, useRef, useState } from "react";
|
|
7
7
|
import { Context, LinkKind, Rect, Vec2 } from "@stuly/anode";
|
|
@@ -26,6 +26,28 @@ const getCenter = (t1, t2) => {
|
|
|
26
26
|
y: (t1.clientY + t2.clientY) / 2
|
|
27
27
|
};
|
|
28
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* The primary canvas component for Anode.
|
|
31
|
+
* Handles interaction (zoom, pan, selection) and synchronizes the React component
|
|
32
|
+
* tree with the internal headless engine.
|
|
33
|
+
*
|
|
34
|
+
* **Behaviors:**
|
|
35
|
+
* 1. **Zoom/Pan:** Implements standard wheel and touch interaction.
|
|
36
|
+
* 2. **Selection:** Multi-select with Shift + Click, Box select with Alt + Click.
|
|
37
|
+
* 3. **Declarative Sync:** If `nodes` or `links` are passed, the core engine
|
|
38
|
+
* automatically mirrors these arrays. Changes made directly in the UI
|
|
39
|
+
* (dragging, deleting) will trigger the corresponding `onNodesChange` callback.
|
|
40
|
+
* 4. **Spatial Culling:** Automatically uses `useVisibleNodes` to optimize rendering.
|
|
41
|
+
*
|
|
42
|
+
* **Usage:**
|
|
43
|
+
* ```tsx
|
|
44
|
+
* <World
|
|
45
|
+
* nodeTypes={{ math: MathNode }}
|
|
46
|
+
* nodes={state.nodes}
|
|
47
|
+
* onNodesChange={newNodes => setState({ nodes: newNodes })}
|
|
48
|
+
* />
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
29
51
|
const World = ({ children, style, nodeTypes = {}, linkTypes = {}, defaultLinkKind = LinkKind.BEZIER, onConnect, isValidConnection, selectionBoxStyle, nodes, links: linksProp, onNodesChange, onLinksChange }) => {
|
|
30
52
|
const ctx = useAnode();
|
|
31
53
|
const { viewport: transform, setViewport: setTransform, screenToWorld } = useViewport();
|
|
@@ -595,7 +617,7 @@ const World = ({ children, style, nodeTypes = {}, linkTypes = {}, defaultLinkKin
|
|
|
595
617
|
zIndex: 1,
|
|
596
618
|
pointerEvents: "none"
|
|
597
619
|
},
|
|
598
|
-
children: [groups.map((group) => /* @__PURE__ */ jsx(Group, { id: group.id }, group.id)), entities.map((entity) => {
|
|
620
|
+
children: [groups.map((group) => /* @__PURE__ */ jsx(Group$1, { id: group.id }, group.id)), entities.map((entity) => {
|
|
599
621
|
const Component = nodeTypes[entity.inner?.type || "default"] || DefaultNode;
|
|
600
622
|
return /* @__PURE__ */ jsx(Node, {
|
|
601
623
|
id: entity.id,
|
package/dist/hooks.d.ts
CHANGED
|
@@ -1,15 +1,59 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { Entity, Link } from "@stuly/anode";
|
|
1
|
+
import { Entity, Group, Link } from "@stuly/anode";
|
|
3
2
|
|
|
4
3
|
//#region src/hooks.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Returns an array of all entities currently in the graph.
|
|
6
|
+
* Reactively updates when nodes are created, deleted, or moved.
|
|
7
|
+
*
|
|
8
|
+
* **Usage:**
|
|
9
|
+
* ```tsx
|
|
10
|
+
* const nodes = useNodes();
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
5
13
|
declare const useNodes: () => Entity<any>[];
|
|
14
|
+
/**
|
|
15
|
+
* Optimized hook for rendering a large-scale node graph.
|
|
16
|
+
* Performs a spatial query on the QuadTree to return only the nodes
|
|
17
|
+
* that are currently within the user's viewport (with padding).
|
|
18
|
+
*
|
|
19
|
+
* **Side Effects:** Triggers re-renders only when nodes enter or exit the viewport
|
|
20
|
+
* OR when a visible node is moved.
|
|
21
|
+
*
|
|
22
|
+
* @param containerRect The dimensions of the canvas container. If omitted, defaults to window size.
|
|
23
|
+
* @returns Array of visible Entity objects.
|
|
24
|
+
*/
|
|
6
25
|
declare const useVisibleNodes: (containerRect?: {
|
|
7
26
|
width: number;
|
|
8
27
|
height: number;
|
|
9
28
|
}) => Entity<any>[];
|
|
29
|
+
/**
|
|
30
|
+
* Returns an array of all links currently in the graph.
|
|
31
|
+
* Reactively updates when links are created, deleted, or updated,
|
|
32
|
+
* or when an endpoint entity moves (triggering path recalculation).
|
|
33
|
+
*/
|
|
10
34
|
declare const useEdges: () => Link<any>[];
|
|
35
|
+
/**
|
|
36
|
+
* Returns an array of all sockets associated with a specific entity.
|
|
37
|
+
* Reactively updates when sockets are added or removed from the entity.
|
|
38
|
+
*
|
|
39
|
+
* @param entityId The unique ID of the entity.
|
|
40
|
+
*/
|
|
11
41
|
declare const useEntitySockets: (entityId: number) => any[];
|
|
42
|
+
/**
|
|
43
|
+
* Subscribes to the reactive value of a specific socket.
|
|
44
|
+
*
|
|
45
|
+
* **Cause:** Triggers a re-render only when the value of the socket
|
|
46
|
+
* changes due to engine propagation.
|
|
47
|
+
*
|
|
48
|
+
* @template T The type of the value held by the socket.
|
|
49
|
+
* @param socketId The unique ID of the socket.
|
|
50
|
+
* @returns The current socket value, or null if the socket doesn't exist.
|
|
51
|
+
*/
|
|
12
52
|
declare function useSocketValue<T = any>(socketId: number | null): T;
|
|
13
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Returns an array of all groups currently in the graph.
|
|
55
|
+
* Reactively updates when groups are created or deleted.
|
|
56
|
+
*/
|
|
57
|
+
declare const useGroups: () => Group[];
|
|
14
58
|
//#endregion
|
|
15
59
|
export { useEdges, useEntitySockets, useGroups, useNodes, useSocketValue, useVisibleNodes };
|
package/dist/hooks.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { useAnode, useViewport } from "./context.js";
|
|
2
|
-
import
|
|
3
|
-
import { Entity, Link, Rect } from "@stuly/anode";
|
|
2
|
+
import { useMemo, useSyncExternalStore } from "react";
|
|
3
|
+
import { Entity, Group, Link, Rect } from "@stuly/anode";
|
|
4
4
|
|
|
5
5
|
//#region src/hooks.ts
|
|
6
|
+
/**
|
|
7
|
+
* Returns an array of all entities currently in the graph.
|
|
8
|
+
* Reactively updates when nodes are created, deleted, or moved.
|
|
9
|
+
*
|
|
10
|
+
* **Usage:**
|
|
11
|
+
* ```tsx
|
|
12
|
+
* const nodes = useNodes();
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
6
15
|
const useNodes = () => {
|
|
7
16
|
const ctx = useAnode();
|
|
8
17
|
const store = useMemo(() => {
|
|
@@ -27,6 +36,17 @@ const useNodes = () => {
|
|
|
27
36
|
}, [ctx]);
|
|
28
37
|
return useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
29
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* Optimized hook for rendering a large-scale node graph.
|
|
41
|
+
* Performs a spatial query on the QuadTree to return only the nodes
|
|
42
|
+
* that are currently within the user's viewport (with padding).
|
|
43
|
+
*
|
|
44
|
+
* **Side Effects:** Triggers re-renders only when nodes enter or exit the viewport
|
|
45
|
+
* OR when a visible node is moved.
|
|
46
|
+
*
|
|
47
|
+
* @param containerRect The dimensions of the canvas container. If omitted, defaults to window size.
|
|
48
|
+
* @returns Array of visible Entity objects.
|
|
49
|
+
*/
|
|
30
50
|
const useVisibleNodes = (containerRect) => {
|
|
31
51
|
const ctx = useAnode();
|
|
32
52
|
const { viewport } = useViewport();
|
|
@@ -45,6 +65,11 @@ const useVisibleNodes = (containerRect) => {
|
|
|
45
65
|
useNodes()
|
|
46
66
|
]);
|
|
47
67
|
};
|
|
68
|
+
/**
|
|
69
|
+
* Returns an array of all links currently in the graph.
|
|
70
|
+
* Reactively updates when links are created, deleted, or updated,
|
|
71
|
+
* or when an endpoint entity moves (triggering path recalculation).
|
|
72
|
+
*/
|
|
48
73
|
const useEdges = () => {
|
|
49
74
|
const ctx = useAnode();
|
|
50
75
|
const store = useMemo(() => {
|
|
@@ -69,6 +94,12 @@ const useEdges = () => {
|
|
|
69
94
|
}, [ctx]);
|
|
70
95
|
return useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
71
96
|
};
|
|
97
|
+
/**
|
|
98
|
+
* Returns an array of all sockets associated with a specific entity.
|
|
99
|
+
* Reactively updates when sockets are added or removed from the entity.
|
|
100
|
+
*
|
|
101
|
+
* @param entityId The unique ID of the entity.
|
|
102
|
+
*/
|
|
72
103
|
const useEntitySockets = (entityId) => {
|
|
73
104
|
const ctx = useAnode();
|
|
74
105
|
const store = useMemo(() => {
|
|
@@ -104,6 +135,16 @@ const useEntitySockets = (entityId) => {
|
|
|
104
135
|
}, [ctx, entityId]);
|
|
105
136
|
return useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
106
137
|
};
|
|
138
|
+
/**
|
|
139
|
+
* Subscribes to the reactive value of a specific socket.
|
|
140
|
+
*
|
|
141
|
+
* **Cause:** Triggers a re-render only when the value of the socket
|
|
142
|
+
* changes due to engine propagation.
|
|
143
|
+
*
|
|
144
|
+
* @template T The type of the value held by the socket.
|
|
145
|
+
* @param socketId The unique ID of the socket.
|
|
146
|
+
* @returns The current socket value, or null if the socket doesn't exist.
|
|
147
|
+
*/
|
|
107
148
|
function useSocketValue(socketId) {
|
|
108
149
|
const ctx = useAnode();
|
|
109
150
|
const store = useMemo(() => {
|
|
@@ -127,6 +168,10 @@ function useSocketValue(socketId) {
|
|
|
127
168
|
}, [ctx, socketId]);
|
|
128
169
|
return useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
129
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Returns an array of all groups currently in the graph.
|
|
173
|
+
* Reactively updates when groups are created or deleted.
|
|
174
|
+
*/
|
|
130
175
|
const useGroups = () => {
|
|
131
176
|
const ctx = useAnode();
|
|
132
177
|
const store = useMemo(() => {
|
package/package.json
CHANGED
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stuly/anode-react",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "First-class React bindings for building interactive, declarative node editors with Anode.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"publishConfig": {
|
|
9
9
|
"access": "public"
|
|
10
10
|
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/stulyproject/anode.git"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/stulyproject/anode/issues"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/stulyproject/anode#readme",
|
|
11
19
|
"files": [
|
|
12
20
|
"dist"
|
|
13
21
|
],
|
|
14
|
-
"keywords": [
|
|
22
|
+
"keywords": [
|
|
23
|
+
"react",
|
|
24
|
+
"node-editor",
|
|
25
|
+
"node-graph",
|
|
26
|
+
"anode",
|
|
27
|
+
"declarative"
|
|
28
|
+
],
|
|
15
29
|
"author": "",
|
|
16
30
|
"type": "module",
|
|
17
31
|
"license": "MIT",
|
|
18
32
|
"peerDependencies": {
|
|
19
33
|
"react": "^19.2.4",
|
|
20
|
-
"@stuly/anode": "^0.1.
|
|
34
|
+
"@stuly/anode": "^0.1.1"
|
|
21
35
|
},
|
|
22
36
|
"devDependencies": {
|
|
23
37
|
"@types/react": "^19.2.14",
|