@stuly/anode-react 0.1.0 → 0.1.2
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/context.d.ts +30 -0
- package/dist/context.js +34 -0
- package/dist/elements/Background.d.ts +11 -0
- package/dist/elements/Background.js +11 -0
- package/dist/elements/Controls.d.ts +11 -0
- package/dist/elements/Controls.js +11 -0
- package/dist/elements/Group.js +3 -3
- package/dist/elements/MiniMap.d.ts +11 -0
- package/dist/elements/MiniMap.js +11 -0
- package/dist/elements/Panel.d.ts +13 -0
- package/dist/elements/Panel.js +13 -0
- package/dist/elements/World.d.ts +52 -11
- package/dist/elements/World.js +48 -19
- 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/context.d.ts
CHANGED
|
@@ -7,7 +7,18 @@ interface Viewport {
|
|
|
7
7
|
y: number;
|
|
8
8
|
k: number;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Accesses the underlying headless Anode engine instance.
|
|
12
|
+
*
|
|
13
|
+
* @returns The `Context` instance for direct graph manipulation.
|
|
14
|
+
*/
|
|
10
15
|
declare const useAnode: () => Context<any>;
|
|
16
|
+
/**
|
|
17
|
+
* Accesses the current viewport transformation (pan/zoom) and coordinate
|
|
18
|
+
* conversion utilities.
|
|
19
|
+
*
|
|
20
|
+
* @returns An object containing the current `viewport` and functions to update it.
|
|
21
|
+
*/
|
|
11
22
|
declare const useViewport: () => {
|
|
12
23
|
viewport: Viewport;
|
|
13
24
|
setViewport: (v: Viewport) => void;
|
|
@@ -16,6 +27,11 @@ declare const useViewport: () => {
|
|
|
16
27
|
y: number;
|
|
17
28
|
};
|
|
18
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* Accesses the current selection state for nodes and links.
|
|
32
|
+
*
|
|
33
|
+
* @returns An object containing the `selection` sets and a `setSelection` updater.
|
|
34
|
+
*/
|
|
19
35
|
declare const useSelection: () => {
|
|
20
36
|
selection: {
|
|
21
37
|
nodes: Set<number>;
|
|
@@ -26,6 +42,20 @@ declare const useSelection: () => {
|
|
|
26
42
|
links: Set<number>;
|
|
27
43
|
}>>;
|
|
28
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* The root provider for any Anode React application.
|
|
47
|
+
* Wraps the internal headless engine and provides reactive state for
|
|
48
|
+
* viewport and selection.
|
|
49
|
+
*
|
|
50
|
+
* **Usage:**
|
|
51
|
+
* ```tsx
|
|
52
|
+
* <AnodeProvider>
|
|
53
|
+
* <World>
|
|
54
|
+
* <Background />
|
|
55
|
+
* </World>
|
|
56
|
+
* </AnodeProvider>
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
29
59
|
declare const AnodeProvider: React.FC<{
|
|
30
60
|
children: React.ReactNode;
|
|
31
61
|
context?: Context;
|
package/dist/context.js
CHANGED
|
@@ -3,12 +3,27 @@ import { Context } from "@stuly/anode";
|
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
|
|
5
5
|
//#region src/context.tsx
|
|
6
|
+
/**
|
|
7
|
+
* Internal context for Anode's React state, including the engine instance,
|
|
8
|
+
* viewport transformation, and selection state.
|
|
9
|
+
*/
|
|
6
10
|
const AnodeReactContext = createContext(null);
|
|
11
|
+
/**
|
|
12
|
+
* Accesses the underlying headless Anode engine instance.
|
|
13
|
+
*
|
|
14
|
+
* @returns The `Context` instance for direct graph manipulation.
|
|
15
|
+
*/
|
|
7
16
|
const useAnode = () => {
|
|
8
17
|
const value = useContext(AnodeReactContext);
|
|
9
18
|
if (!value) throw new Error("useAnode must be used within an AnodeProvider");
|
|
10
19
|
return value.ctx;
|
|
11
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* Accesses the current viewport transformation (pan/zoom) and coordinate
|
|
23
|
+
* conversion utilities.
|
|
24
|
+
*
|
|
25
|
+
* @returns An object containing the current `viewport` and functions to update it.
|
|
26
|
+
*/
|
|
12
27
|
const useViewport = () => {
|
|
13
28
|
const value = useContext(AnodeReactContext);
|
|
14
29
|
if (!value) throw new Error("useViewport must be used within an AnodeProvider");
|
|
@@ -18,6 +33,11 @@ const useViewport = () => {
|
|
|
18
33
|
screenToWorld: value.screenToWorld
|
|
19
34
|
};
|
|
20
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* Accesses the current selection state for nodes and links.
|
|
38
|
+
*
|
|
39
|
+
* @returns An object containing the `selection` sets and a `setSelection` updater.
|
|
40
|
+
*/
|
|
21
41
|
const useSelection = () => {
|
|
22
42
|
const value = useContext(AnodeReactContext);
|
|
23
43
|
if (!value) throw new Error("useSelection must be used within an AnodeProvider");
|
|
@@ -26,6 +46,20 @@ const useSelection = () => {
|
|
|
26
46
|
setSelection: value.setSelection
|
|
27
47
|
};
|
|
28
48
|
};
|
|
49
|
+
/**
|
|
50
|
+
* The root provider for any Anode React application.
|
|
51
|
+
* Wraps the internal headless engine and provides reactive state for
|
|
52
|
+
* viewport and selection.
|
|
53
|
+
*
|
|
54
|
+
* **Usage:**
|
|
55
|
+
* ```tsx
|
|
56
|
+
* <AnodeProvider>
|
|
57
|
+
* <World>
|
|
58
|
+
* <Background />
|
|
59
|
+
* </World>
|
|
60
|
+
* </AnodeProvider>
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
29
63
|
const AnodeProvider = ({ children, context }) => {
|
|
30
64
|
const [ctx] = useState(() => context ?? new Context());
|
|
31
65
|
const [viewport, setViewport] = useState({
|
|
@@ -7,6 +7,17 @@ interface BackgroundProps {
|
|
|
7
7
|
gap?: number;
|
|
8
8
|
pattern?: 'dots' | 'lines';
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* A decorative grid or dot pattern overlay for the canvas.
|
|
12
|
+
* Automatically pans and scales with the viewport.
|
|
13
|
+
*
|
|
14
|
+
* **Usage:**
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <World>
|
|
17
|
+
* <Background pattern="dots" color="#ccc" />
|
|
18
|
+
* </World>
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
10
21
|
declare const Background: React.FC<BackgroundProps>;
|
|
11
22
|
//#endregion
|
|
12
23
|
export { Background };
|
|
@@ -3,6 +3,17 @@ import React from "react";
|
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
|
|
5
5
|
//#region src/elements/Background.tsx
|
|
6
|
+
/**
|
|
7
|
+
* A decorative grid or dot pattern overlay for the canvas.
|
|
8
|
+
* Automatically pans and scales with the viewport.
|
|
9
|
+
*
|
|
10
|
+
* **Usage:**
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <World>
|
|
13
|
+
* <Background pattern="dots" color="#ccc" />
|
|
14
|
+
* </World>
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
6
17
|
const Background = ({ color = "#cbd5e1", size = 1, gap = 20, pattern = "dots" }) => {
|
|
7
18
|
const { viewport } = useViewport();
|
|
8
19
|
const scaledGap = gap * viewport.k;
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/elements/Controls.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* A floating UI control panel providing standard canvas interactions
|
|
6
|
+
* like zooming in/out and fitting all nodes into the current view.
|
|
7
|
+
*
|
|
8
|
+
* **Usage:**
|
|
9
|
+
* ```tsx
|
|
10
|
+
* <World>
|
|
11
|
+
* <Controls />
|
|
12
|
+
* </World>
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
4
15
|
declare const Controls: React.FC<{
|
|
5
16
|
style?: React.CSSProperties;
|
|
6
17
|
}>;
|
|
@@ -3,6 +3,17 @@ import React from "react";
|
|
|
3
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
|
|
5
5
|
//#region src/elements/Controls.tsx
|
|
6
|
+
/**
|
|
7
|
+
* A floating UI control panel providing standard canvas interactions
|
|
8
|
+
* like zooming in/out and fitting all nodes into the current view.
|
|
9
|
+
*
|
|
10
|
+
* **Usage:**
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <World>
|
|
13
|
+
* <Controls />
|
|
14
|
+
* </World>
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
6
17
|
const Controls = ({ style }) => {
|
|
7
18
|
const { viewport, setViewport } = useViewport();
|
|
8
19
|
const ctx = useAnode();
|
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 };
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/elements/MiniMap.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* A simplified bird's-eye view of the entire graph, providing
|
|
6
|
+
* context and a visual indicator of the current viewport.
|
|
7
|
+
*
|
|
8
|
+
* **Usage:**
|
|
9
|
+
* ```tsx
|
|
10
|
+
* <World>
|
|
11
|
+
* <MiniMap width={200} height={150} />
|
|
12
|
+
* </World>
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
4
15
|
declare const MiniMap: React.FC<{
|
|
5
16
|
width?: number;
|
|
6
17
|
height?: number;
|
package/dist/elements/MiniMap.js
CHANGED
|
@@ -3,6 +3,17 @@ import React, { useMemo } from "react";
|
|
|
3
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
|
|
5
5
|
//#region src/elements/MiniMap.tsx
|
|
6
|
+
/**
|
|
7
|
+
* A simplified bird's-eye view of the entire graph, providing
|
|
8
|
+
* context and a visual indicator of the current viewport.
|
|
9
|
+
*
|
|
10
|
+
* **Usage:**
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <World>
|
|
13
|
+
* <MiniMap width={200} height={150} />
|
|
14
|
+
* </World>
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
6
17
|
const MiniMap = ({ width = 200, height = 150, style }) => {
|
|
7
18
|
const ctx = useAnode();
|
|
8
19
|
const { viewport } = useViewport();
|
package/dist/elements/Panel.d.ts
CHANGED
|
@@ -2,6 +2,19 @@ import React from "react";
|
|
|
2
2
|
|
|
3
3
|
//#region src/elements/Panel.d.ts
|
|
4
4
|
type PanelPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
5
|
+
/**
|
|
6
|
+
* A helper component to overlay custom UI elements (like toolbars or sidebars)
|
|
7
|
+
* at specific anchor points on the canvas.
|
|
8
|
+
*
|
|
9
|
+
* **Usage:**
|
|
10
|
+
* ```tsx
|
|
11
|
+
* <World>
|
|
12
|
+
* <Panel position="top-right">
|
|
13
|
+
* <button>Custom Action</button>
|
|
14
|
+
* </Panel>
|
|
15
|
+
* </World>
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
5
18
|
declare const Panel: React.FC<{
|
|
6
19
|
position?: PanelPosition;
|
|
7
20
|
children?: React.ReactNode;
|
package/dist/elements/Panel.js
CHANGED
|
@@ -2,6 +2,19 @@ import React from "react";
|
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
|
|
4
4
|
//#region src/elements/Panel.tsx
|
|
5
|
+
/**
|
|
6
|
+
* A helper component to overlay custom UI elements (like toolbars or sidebars)
|
|
7
|
+
* at specific anchor points on the canvas.
|
|
8
|
+
*
|
|
9
|
+
* **Usage:**
|
|
10
|
+
* ```tsx
|
|
11
|
+
* <World>
|
|
12
|
+
* <Panel position="top-right">
|
|
13
|
+
* <button>Custom Action</button>
|
|
14
|
+
* </Panel>
|
|
15
|
+
* </World>
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
5
18
|
const Panel = ({ position = "top-left", children, style }) => {
|
|
6
19
|
const getPositionStyle = () => {
|
|
7
20
|
switch (position) {
|
package/dist/elements/World.d.ts
CHANGED
|
@@ -4,41 +4,82 @@ 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:** Standard wheel zoom and drag-to-pan interaction.
|
|
52
|
+
* 2. **Selection:** Multi-select with `Shift + Click`, Box select with `Alt + Click`.
|
|
53
|
+
* 3. **Keybindings:**
|
|
54
|
+
* - `Backspace` / `Delete`: Remove selected nodes and links.
|
|
55
|
+
* - `Ctrl + Z` / `Cmd + Z`: Undo the last action.
|
|
56
|
+
* - `Ctrl + Shift + Z` / `Ctrl + Y`: Redo the last undone action.
|
|
57
|
+
* 4. **Declarative Sync:** If `nodes` or `links` are passed, the core engine
|
|
58
|
+
* automatically mirrors these arrays. Changes made directly in the UI
|
|
59
|
+
* (dragging, deleting) will trigger the corresponding `onNodesChange` callback.
|
|
60
|
+
* 5. **Spatial Culling:** Automatically uses `useVisibleNodes` to optimize rendering.
|
|
61
|
+
*
|
|
62
|
+
* **Usage:**
|
|
63
|
+
* ```tsx
|
|
64
|
+
* <World
|
|
65
|
+
* nodeTypes={{ math: MathNode }}
|
|
66
|
+
* nodes={state.nodes}
|
|
67
|
+
* onNodesChange={newNodes => setState({ nodes: newNodes })}
|
|
68
|
+
* />
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
30
71
|
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;
|
|
72
|
+
/** Elements to overlay on the world (Background, MiniMap, etc.). */children?: React.ReactNode; /** Styles applied to the outer container. */
|
|
73
|
+
style?: React.CSSProperties; /** Styles applied to the selection box overlay. */
|
|
74
|
+
selectionBoxStyle?: React.CSSProperties; /** Map of custom node components by their `type` key. */
|
|
75
|
+
nodeTypes?: Record<string, React.ComponentType<NodeComponentProps>>; /** Map of custom link overlays/UI components by their `type` key. */
|
|
76
|
+
linkTypes?: Record<string, React.ComponentType<LinkComponentProps>>; /** Default style for newly created links. */
|
|
77
|
+
defaultLinkKind?: LinkKind; /** Callback triggered when a user completes a connection via drag-and-drop. */
|
|
78
|
+
onConnect?: (fromId: number, toId: number, ctx: Context<any>) => void; /** Custom validation for link creation. Return false to prevent a connection. */
|
|
79
|
+
isValidConnection?: (from: any, to: any, ctx: Context<any>) => boolean; /** Declarative list of nodes. Use for state-controlled synchronization. */
|
|
80
|
+
nodes?: NodeData[]; /** Declarative list of links. Use for state-controlled synchronization. */
|
|
81
|
+
links?: LinkData[]; /** Callback triggered when nodes are moved or dropped. */
|
|
82
|
+
onNodesChange?: (nodes: NodeData[], ctx: Context<any>) => void; /** Callback triggered when links are updated or deleted. */
|
|
42
83
|
onLinksChange?: (links: LinkData[], ctx: Context<any>) => void;
|
|
43
84
|
}>;
|
|
44
85
|
//#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,32 @@ 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:** Standard wheel zoom and drag-to-pan interaction.
|
|
36
|
+
* 2. **Selection:** Multi-select with `Shift + Click`, Box select with `Alt + Click`.
|
|
37
|
+
* 3. **Keybindings:**
|
|
38
|
+
* - `Backspace` / `Delete`: Remove selected nodes and links.
|
|
39
|
+
* - `Ctrl + Z` / `Cmd + Z`: Undo the last action.
|
|
40
|
+
* - `Ctrl + Shift + Z` / `Ctrl + Y`: Redo the last undone action.
|
|
41
|
+
* 4. **Declarative Sync:** If `nodes` or `links` are passed, the core engine
|
|
42
|
+
* automatically mirrors these arrays. Changes made directly in the UI
|
|
43
|
+
* (dragging, deleting) will trigger the corresponding `onNodesChange` callback.
|
|
44
|
+
* 5. **Spatial Culling:** Automatically uses `useVisibleNodes` to optimize rendering.
|
|
45
|
+
*
|
|
46
|
+
* **Usage:**
|
|
47
|
+
* ```tsx
|
|
48
|
+
* <World
|
|
49
|
+
* nodeTypes={{ math: MathNode }}
|
|
50
|
+
* nodes={state.nodes}
|
|
51
|
+
* onNodesChange={newNodes => setState({ nodes: newNodes })}
|
|
52
|
+
* />
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
29
55
|
const World = ({ children, style, nodeTypes = {}, linkTypes = {}, defaultLinkKind = LinkKind.BEZIER, onConnect, isValidConnection, selectionBoxStyle, nodes, links: linksProp, onNodesChange, onLinksChange }) => {
|
|
30
56
|
const ctx = useAnode();
|
|
31
57
|
const { viewport: transform, setViewport: setTransform, screenToWorld } = useViewport();
|
|
@@ -392,6 +418,8 @@ const World = ({ children, style, nodeTypes = {}, linkTypes = {}, defaultLinkKin
|
|
|
392
418
|
const rect = worldRef.current.getBoundingClientRect();
|
|
393
419
|
const startX = e.clientX - rect.left;
|
|
394
420
|
const startY = e.clientY - rect.top;
|
|
421
|
+
let currentX = startX;
|
|
422
|
+
let currentY = startY;
|
|
395
423
|
setSelectionBox({
|
|
396
424
|
startX,
|
|
397
425
|
startY,
|
|
@@ -399,33 +427,34 @@ const World = ({ children, style, nodeTypes = {}, linkTypes = {}, defaultLinkKin
|
|
|
399
427
|
endY: startY
|
|
400
428
|
});
|
|
401
429
|
const onMouseMove = (moveEvent) => {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
430
|
+
currentX = moveEvent.clientX - rect.left;
|
|
431
|
+
currentY = moveEvent.clientY - rect.top;
|
|
432
|
+
setSelectionBox({
|
|
433
|
+
startX,
|
|
434
|
+
startY,
|
|
435
|
+
endX: currentX,
|
|
436
|
+
endY: currentY
|
|
437
|
+
});
|
|
407
438
|
};
|
|
408
439
|
const onMouseUp = () => {
|
|
409
440
|
document.removeEventListener("mousemove", onMouseMove);
|
|
410
441
|
document.removeEventListener("mouseup", onMouseUp);
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
const
|
|
419
|
-
const worldTopLeft = screenToWorld(x1 + rect.left, y1 + rect.top);
|
|
420
|
-
const worldBottomRight = screenToWorld(x2 + rect.left, y2 + rect.top);
|
|
442
|
+
const x1 = Math.min(startX, currentX);
|
|
443
|
+
const y1 = Math.min(startY, currentY);
|
|
444
|
+
const x2 = Math.max(startX, currentX);
|
|
445
|
+
const y2 = Math.max(startY, currentY);
|
|
446
|
+
const worldRect = worldRef.current?.getBoundingClientRect();
|
|
447
|
+
if (worldRect) {
|
|
448
|
+
const worldTopLeft = screenToWorld(x1 + worldRect.left, y1 + worldRect.top);
|
|
449
|
+
const worldBottomRight = screenToWorld(x2 + worldRect.left, y2 + worldRect.top);
|
|
421
450
|
const queryRect = new Rect(worldTopLeft.x, worldTopLeft.y, worldBottomRight.x - worldTopLeft.x, worldBottomRight.y - worldTopLeft.y);
|
|
422
451
|
const selectedIds = ctx.quadTree.query(queryRect);
|
|
423
452
|
setSelection({
|
|
424
453
|
nodes: new Set(selectedIds),
|
|
425
454
|
links: /* @__PURE__ */ new Set()
|
|
426
455
|
});
|
|
427
|
-
|
|
428
|
-
|
|
456
|
+
}
|
|
457
|
+
setSelectionBox(null);
|
|
429
458
|
};
|
|
430
459
|
document.addEventListener("mousemove", onMouseMove);
|
|
431
460
|
document.addEventListener("mouseup", onMouseUp);
|
|
@@ -595,7 +624,7 @@ const World = ({ children, style, nodeTypes = {}, linkTypes = {}, defaultLinkKin
|
|
|
595
624
|
zIndex: 1,
|
|
596
625
|
pointerEvents: "none"
|
|
597
626
|
},
|
|
598
|
-
children: [groups.map((group) => /* @__PURE__ */ jsx(Group, { id: group.id }, group.id)), entities.map((entity) => {
|
|
627
|
+
children: [groups.map((group) => /* @__PURE__ */ jsx(Group$1, { id: group.id }, group.id)), entities.map((entity) => {
|
|
599
628
|
const Component = nodeTypes[entity.inner?.type || "default"] || DefaultNode;
|
|
600
629
|
return /* @__PURE__ */ jsx(Node, {
|
|
601
630
|
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.2",
|
|
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.2"
|
|
21
35
|
},
|
|
22
36
|
"devDependencies": {
|
|
23
37
|
"@types/react": "^19.2.14",
|