@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 ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2026 luxluth <delphin.blehoussi93@gmail.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # anode-react
2
+
3
+ React bindings and components for Anode, providing a declarative layer over
4
+ the headless core engine.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ npm install @stuly/anode-react @stuly/anode
10
+ ```
11
+
12
+ ## Components & Hooks
13
+
14
+ - **World:** The primary canvas component for rendering the node graph.
15
+ - **Socket:** A component for rendering connection points within nodes.
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.
21
+
22
+ For detailed documentation and usage examples, see the [root README](../../README.md).
@@ -0,0 +1,34 @@
1
+ import React from "react";
2
+ import { Context } from "@stuly/anode";
3
+
4
+ //#region src/context.d.ts
5
+ interface Viewport {
6
+ x: number;
7
+ y: number;
8
+ k: number;
9
+ }
10
+ declare const useAnode: () => Context<any>;
11
+ declare const useViewport: () => {
12
+ viewport: Viewport;
13
+ setViewport: (v: Viewport) => void;
14
+ screenToWorld: (clientX: number, clientY: number) => {
15
+ x: number;
16
+ y: number;
17
+ };
18
+ };
19
+ declare const useSelection: () => {
20
+ selection: {
21
+ nodes: Set<number>;
22
+ links: Set<number>;
23
+ };
24
+ setSelection: React.Dispatch<React.SetStateAction<{
25
+ nodes: Set<number>;
26
+ links: Set<number>;
27
+ }>>;
28
+ };
29
+ declare const AnodeProvider: React.FC<{
30
+ children: React.ReactNode;
31
+ context?: Context;
32
+ }>;
33
+ //#endregion
34
+ export { AnodeProvider, useAnode, useSelection, useViewport };
@@ -0,0 +1,65 @@
1
+ import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
2
+ import { Context } from "@stuly/anode";
3
+ import { jsx } from "react/jsx-runtime";
4
+
5
+ //#region src/context.tsx
6
+ const AnodeReactContext = createContext(null);
7
+ const useAnode = () => {
8
+ const value = useContext(AnodeReactContext);
9
+ if (!value) throw new Error("useAnode must be used within an AnodeProvider");
10
+ return value.ctx;
11
+ };
12
+ const useViewport = () => {
13
+ const value = useContext(AnodeReactContext);
14
+ if (!value) throw new Error("useViewport must be used within an AnodeProvider");
15
+ return {
16
+ viewport: value.viewport,
17
+ setViewport: value.setViewport,
18
+ screenToWorld: value.screenToWorld
19
+ };
20
+ };
21
+ const useSelection = () => {
22
+ const value = useContext(AnodeReactContext);
23
+ if (!value) throw new Error("useSelection must be used within an AnodeProvider");
24
+ return {
25
+ selection: value.selection,
26
+ setSelection: value.setSelection
27
+ };
28
+ };
29
+ const AnodeProvider = ({ children, context }) => {
30
+ const [ctx] = useState(() => context ?? new Context());
31
+ const [viewport, setViewport] = useState({
32
+ x: 0,
33
+ y: 0,
34
+ k: 1
35
+ });
36
+ const [screenToWorld, setScreenToWorld] = useState(() => (x, y) => ({
37
+ x,
38
+ y
39
+ }));
40
+ const [selection, setSelection] = useState({
41
+ nodes: /* @__PURE__ */ new Set(),
42
+ links: /* @__PURE__ */ new Set()
43
+ });
44
+ const value = useMemo(() => ({
45
+ ctx,
46
+ viewport,
47
+ setViewport,
48
+ screenToWorld,
49
+ setScreenToWorld,
50
+ selection,
51
+ setSelection
52
+ }), [
53
+ ctx,
54
+ viewport,
55
+ screenToWorld,
56
+ selection
57
+ ]);
58
+ return /* @__PURE__ */ jsx(AnodeReactContext.Provider, {
59
+ value,
60
+ children
61
+ });
62
+ };
63
+
64
+ //#endregion
65
+ export { AnodeProvider, AnodeReactContext, useAnode, useSelection, useViewport };
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+
3
+ //#region src/elements/Background.d.ts
4
+ interface BackgroundProps {
5
+ color?: string;
6
+ size?: number;
7
+ gap?: number;
8
+ pattern?: 'dots' | 'lines';
9
+ }
10
+ declare const Background: React.FC<BackgroundProps>;
11
+ //#endregion
12
+ export { Background };
@@ -0,0 +1,27 @@
1
+ import { useViewport } from "../context.js";
2
+ import React from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+
5
+ //#region src/elements/Background.tsx
6
+ const Background = ({ color = "#cbd5e1", size = 1, gap = 20, pattern = "dots" }) => {
7
+ const { viewport } = useViewport();
8
+ const scaledGap = gap * viewport.k;
9
+ const scaledSize = size * viewport.k;
10
+ const offsetX = viewport.x % scaledGap;
11
+ const offsetY = viewport.y % scaledGap;
12
+ return /* @__PURE__ */ jsx("div", { style: {
13
+ position: "absolute",
14
+ top: 0,
15
+ left: 0,
16
+ width: "100%",
17
+ height: "100%",
18
+ pointerEvents: "none",
19
+ zIndex: 0,
20
+ backgroundImage: pattern === "dots" ? `radial-gradient(${color} ${scaledSize}px, transparent 0)` : `linear-gradient(to right, ${color} ${Math.max(1, viewport.k)}px, transparent 1px), linear-gradient(to bottom, ${color} ${Math.max(1, viewport.k)}px, transparent 1px)`,
21
+ backgroundSize: `${scaledGap}px ${scaledGap}px`,
22
+ backgroundPosition: `${offsetX}px ${offsetY}px`
23
+ } });
24
+ };
25
+
26
+ //#endregion
27
+ export { Background };
@@ -0,0 +1,8 @@
1
+ import React from "react";
2
+
3
+ //#region src/elements/Controls.d.ts
4
+ declare const Controls: React.FC<{
5
+ style?: React.CSSProperties;
6
+ }>;
7
+ //#endregion
8
+ export { Controls };
@@ -0,0 +1,96 @@
1
+ import { useAnode, useViewport } from "../context.js";
2
+ import React from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+
5
+ //#region src/elements/Controls.tsx
6
+ const Controls = ({ style }) => {
7
+ const { viewport, setViewport } = useViewport();
8
+ const ctx = useAnode();
9
+ const onZoomIn = () => {
10
+ setViewport({
11
+ ...viewport,
12
+ k: Math.min(viewport.k * 1.2, 5)
13
+ });
14
+ };
15
+ const onZoomOut = () => {
16
+ setViewport({
17
+ ...viewport,
18
+ k: Math.max(viewport.k / 1.2, .1)
19
+ });
20
+ };
21
+ const onFitView = () => {
22
+ if (ctx.entities.size === 0) {
23
+ setViewport({
24
+ x: 0,
25
+ y: 0,
26
+ k: 1
27
+ });
28
+ return;
29
+ }
30
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
31
+ for (const entity of ctx.entities.values()) {
32
+ const pos = ctx.getWorldPosition(entity.id);
33
+ minX = Math.min(minX, pos.x);
34
+ minY = Math.min(minY, pos.y);
35
+ maxX = Math.max(maxX, pos.x);
36
+ maxY = Math.max(maxY, pos.y);
37
+ }
38
+ const padding = 50;
39
+ const w = maxX - minX + padding * 2;
40
+ const h = maxY - minY + padding * 2;
41
+ const containerW = window.innerWidth;
42
+ const containerH = window.innerHeight;
43
+ const k = Math.min(containerW / w, containerH / h, 1);
44
+ setViewport({
45
+ x: (containerW - (maxX + minX) * k) / 2,
46
+ y: (containerH - (maxY + minY) * k) / 2,
47
+ k
48
+ });
49
+ };
50
+ return /* @__PURE__ */ jsxs("div", {
51
+ className: "anode-controls",
52
+ style: {
53
+ position: "absolute",
54
+ bottom: 20,
55
+ left: 20,
56
+ display: "flex",
57
+ flexDirection: "column",
58
+ gap: 5,
59
+ zIndex: 100,
60
+ ...style
61
+ },
62
+ children: [
63
+ /* @__PURE__ */ jsx("button", {
64
+ onClick: onZoomIn,
65
+ style: buttonStyle,
66
+ children: "+"
67
+ }),
68
+ /* @__PURE__ */ jsx("button", {
69
+ onClick: onZoomOut,
70
+ style: buttonStyle,
71
+ children: "-"
72
+ }),
73
+ /* @__PURE__ */ jsx("button", {
74
+ onClick: onFitView,
75
+ style: buttonStyle,
76
+ children: "w"
77
+ })
78
+ ]
79
+ });
80
+ };
81
+ const buttonStyle = {
82
+ width: 30,
83
+ height: 30,
84
+ background: "white",
85
+ border: "1px solid #ccc",
86
+ borderRadius: 4,
87
+ cursor: "pointer",
88
+ display: "flex",
89
+ alignItems: "center",
90
+ justifyContent: "center",
91
+ fontSize: 18,
92
+ fontWeight: "bold"
93
+ };
94
+
95
+ //#endregion
96
+ export { Controls };
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+
3
+ //#region src/elements/Group.d.ts
4
+ interface GroupProps {
5
+ id: number;
6
+ children?: React.ReactNode;
7
+ }
8
+ declare const Group: React.FC<GroupProps>;
9
+ //#endregion
10
+ export { Group, GroupProps };
@@ -0,0 +1,86 @@
1
+ import { useAnode, useViewport } from "../context.js";
2
+ import React, { useState } from "react";
3
+ import { Rect, Vec2 } from "@stuly/anode";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+
6
+ //#region src/elements/Group.tsx
7
+ const Group = ({ id, children }) => {
8
+ const ctx = useAnode();
9
+ const { viewport } = useViewport();
10
+ const group = ctx.groups.get(id);
11
+ const [isDragging, setIsDragging] = useState(false);
12
+ if (!group) return null;
13
+ const worldPos = ctx.getGroupWorldPosition(id);
14
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
15
+ const calculateBounds = (g) => {
16
+ for (const eid of g.entities) if (ctx.entities.get(eid)) {
17
+ const p = ctx.getWorldPosition(eid);
18
+ minX = Math.min(minX, p.x - 50);
19
+ minY = Math.min(minY, p.y - 50);
20
+ maxX = Math.max(maxX, p.x + 150);
21
+ maxY = Math.max(maxY, p.y + 100);
22
+ }
23
+ for (const gid of g.groups) {
24
+ const childGroup = ctx.groups.get(gid);
25
+ if (childGroup) calculateBounds(childGroup);
26
+ }
27
+ };
28
+ calculateBounds(group);
29
+ const hasChildren = minX !== Infinity;
30
+ const width = hasChildren ? maxX - minX : 200;
31
+ const height = hasChildren ? maxY - minY : 200;
32
+ const x = hasChildren ? minX : worldPos.x;
33
+ const y = hasChildren ? minY : worldPos.y;
34
+ const onMouseDown = (e) => {
35
+ if (e.button !== 0) return;
36
+ e.stopPropagation();
37
+ setIsDragging(true);
38
+ e.clientX;
39
+ e.clientY;
40
+ let lastX = e.clientX;
41
+ let lastY = e.clientY;
42
+ const onMouseMoveDelta = (moveEvent) => {
43
+ const dx = (moveEvent.clientX - lastX) / viewport.k;
44
+ const dy = (moveEvent.clientY - lastY) / viewport.k;
45
+ lastX = moveEvent.clientX;
46
+ lastY = moveEvent.clientY;
47
+ ctx.moveGroup(group, dx, dy);
48
+ };
49
+ const onMouseUp = () => {
50
+ setIsDragging(false);
51
+ document.removeEventListener("mousemove", onMouseMoveDelta);
52
+ document.removeEventListener("mouseup", onMouseUp);
53
+ };
54
+ document.addEventListener("mousemove", onMouseMoveDelta);
55
+ document.addEventListener("mouseup", onMouseUp);
56
+ };
57
+ return /* @__PURE__ */ jsxs("div", {
58
+ className: "anode-group",
59
+ onMouseDown,
60
+ style: {
61
+ position: "absolute",
62
+ left: x,
63
+ top: y,
64
+ width,
65
+ height,
66
+ background: "rgba(0, 0, 0, 0.05)",
67
+ border: "1px dashed #ccc",
68
+ borderRadius: 8,
69
+ zIndex: -1,
70
+ cursor: isDragging ? "grabbing" : "grab",
71
+ pointerEvents: "auto"
72
+ },
73
+ children: [/* @__PURE__ */ jsx("div", {
74
+ style: {
75
+ padding: 8,
76
+ fontSize: 12,
77
+ color: "#999",
78
+ userSelect: "none"
79
+ },
80
+ children: group.name || `Group ${id}`
81
+ }), children]
82
+ });
83
+ };
84
+
85
+ //#endregion
86
+ export { Group };
@@ -0,0 +1,16 @@
1
+ import React from "react";
2
+ import { Link } from "@stuly/anode";
3
+
4
+ //#region src/elements/Link.d.ts
5
+ interface LinkComponentProps {
6
+ id: number;
7
+ link: Link;
8
+ }
9
+ interface LinkProps {
10
+ id: number;
11
+ style?: React.CSSProperties;
12
+ component?: React.ComponentType<LinkComponentProps> | undefined;
13
+ }
14
+ declare const Link$1: React.FC<LinkProps>;
15
+ //#endregion
16
+ export { Link$1 as Link, LinkComponentProps };
@@ -0,0 +1,191 @@
1
+ import { useAnode, useSelection, useViewport } from "../context.js";
2
+ import React, { useEffect, useState } from "react";
3
+ import { Link, Vec2, getLinkCenter, getLinkPath, getLinkPoints } from "@stuly/anode";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+
6
+ //#region src/elements/Link.tsx
7
+ const Link$1 = ({ id, style, component: Component }) => {
8
+ const ctx = useAnode();
9
+ const { viewport, screenToWorld } = useViewport();
10
+ const { selection, setSelection } = useSelection();
11
+ const link = ctx.links.get(id);
12
+ const [, setTick] = useState(0);
13
+ useEffect(() => {
14
+ if (!link) return;
15
+ const onUpdate = () => setTick((t) => t + 1);
16
+ const h1 = ctx.registerEntityMoveListener(onUpdate);
17
+ const h2 = ctx.registerSocketMoveListener(onUpdate);
18
+ const h3 = ctx.registerSocketCreateListener(onUpdate);
19
+ const h4 = ctx.registerLinkUpdateListener((l) => {
20
+ if (l.id === id) onUpdate();
21
+ });
22
+ return () => {
23
+ ctx.unregisterListener(h1);
24
+ ctx.unregisterListener(h2);
25
+ ctx.unregisterListener(h3);
26
+ ctx.unregisterListener(h4);
27
+ };
28
+ }, [
29
+ ctx,
30
+ link,
31
+ id
32
+ ]);
33
+ if (!link) return null;
34
+ const d = getLinkPath(ctx, link);
35
+ const pts = getLinkPoints(ctx, link);
36
+ const center = getLinkCenter(ctx, link);
37
+ if (!d || !pts) return null;
38
+ const isSelected = selection.links.has(id);
39
+ const onClick = (e) => {
40
+ e.stopPropagation();
41
+ if (e.shiftKey) setSelection((prev) => {
42
+ const next = new Set(prev.links);
43
+ if (next.has(id)) next.delete(id);
44
+ else next.add(id);
45
+ return {
46
+ ...prev,
47
+ links: next
48
+ };
49
+ });
50
+ else setSelection({
51
+ nodes: /* @__PURE__ */ new Set(),
52
+ links: new Set([id])
53
+ });
54
+ };
55
+ const onDoubleClick = (e) => {
56
+ e.stopPropagation();
57
+ const { x, y } = screenToWorld(e.clientX, e.clientY);
58
+ const newWaypoints = [...link.waypoints, new Vec2(x, y)];
59
+ ctx.setLinkWaypoints(link, newWaypoints);
60
+ };
61
+ const onHandleMouseDown = (e, type) => {
62
+ e.stopPropagation();
63
+ const event = new CustomEvent("anode-link-reconnect", {
64
+ bubbles: true,
65
+ detail: {
66
+ linkId: id,
67
+ type,
68
+ x: e.clientX,
69
+ y: e.clientY
70
+ }
71
+ });
72
+ e.target.dispatchEvent(event);
73
+ };
74
+ const onWaypointMouseDown = (e, index) => {
75
+ e.stopPropagation();
76
+ const startX = e.clientX;
77
+ const startY = e.clientY;
78
+ const initialWaypoints = link.waypoints.map((p) => p.clone());
79
+ const onMouseMove = (moveEvent) => {
80
+ const dx = (moveEvent.clientX - startX) / viewport.k;
81
+ const dy = (moveEvent.clientY - startY) / viewport.k;
82
+ link.waypoints = initialWaypoints.map((p, i) => {
83
+ if (i === index) return new Vec2(p.x + dx, p.y + dy);
84
+ return p;
85
+ });
86
+ setTick((t) => t + 1);
87
+ };
88
+ const onMouseUp = () => {
89
+ document.removeEventListener("mousemove", onMouseMove);
90
+ document.removeEventListener("mouseup", onMouseUp);
91
+ ctx.setLinkWaypoints(link, link.waypoints);
92
+ };
93
+ document.addEventListener("mousemove", onMouseMove);
94
+ document.addEventListener("mouseup", onMouseUp);
95
+ };
96
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("g", {
97
+ onClick,
98
+ onDoubleClick,
99
+ style: { cursor: "pointer" },
100
+ children: [
101
+ /* @__PURE__ */ jsx("path", {
102
+ d,
103
+ fill: "none",
104
+ stroke: "transparent",
105
+ strokeWidth: 15
106
+ }),
107
+ /* @__PURE__ */ jsx("path", {
108
+ d,
109
+ fill: "none",
110
+ stroke: isSelected ? "#3b82f6" : "#94a3b8",
111
+ strokeWidth: isSelected ? 3 : 2,
112
+ style: {
113
+ transition: "stroke 0.2s, stroke-width 0.2s",
114
+ ...style
115
+ }
116
+ }),
117
+ isSelected && /* @__PURE__ */ jsxs(Fragment, { children: [
118
+ /* @__PURE__ */ jsx("circle", {
119
+ cx: pts.from.x,
120
+ cy: pts.from.y,
121
+ r: 5,
122
+ fill: "white",
123
+ stroke: "#3b82f6",
124
+ strokeWidth: 2,
125
+ onMouseDown: (e) => onHandleMouseDown(e, "from"),
126
+ style: {
127
+ cursor: "crosshair",
128
+ pointerEvents: "auto"
129
+ }
130
+ }),
131
+ /* @__PURE__ */ jsx("circle", {
132
+ cx: pts.to.x,
133
+ cy: pts.to.y,
134
+ r: 5,
135
+ fill: "white",
136
+ stroke: "#3b82f6",
137
+ strokeWidth: 2,
138
+ onMouseDown: (e) => onHandleMouseDown(e, "to"),
139
+ style: {
140
+ cursor: "crosshair",
141
+ pointerEvents: "auto"
142
+ }
143
+ }),
144
+ link.waypoints.map((p, i) => /* @__PURE__ */ jsx("circle", {
145
+ cx: p.x,
146
+ cy: p.y,
147
+ r: 4,
148
+ fill: "#3b82f6",
149
+ onMouseDown: (e) => onWaypointMouseDown(e, i),
150
+ onContextMenu: (e) => {
151
+ e.preventDefault();
152
+ e.stopPropagation();
153
+ const newWaypoints = link.waypoints.filter((_, idx) => idx !== i);
154
+ ctx.setLinkWaypoints(link, newWaypoints);
155
+ },
156
+ style: {
157
+ cursor: "move",
158
+ pointerEvents: "auto"
159
+ }
160
+ }, i)),
161
+ " "
162
+ ] })
163
+ ]
164
+ }), Component && center && /* @__PURE__ */ jsx("foreignObject", {
165
+ x: center.x - 50,
166
+ y: center.y - 25,
167
+ width: 100,
168
+ height: 50,
169
+ style: {
170
+ overflow: "visible",
171
+ pointerEvents: "none"
172
+ },
173
+ children: /* @__PURE__ */ jsx("div", {
174
+ style: {
175
+ width: "100%",
176
+ height: "100%",
177
+ display: "flex",
178
+ alignItems: "center",
179
+ justifyContent: "center",
180
+ pointerEvents: "auto"
181
+ },
182
+ children: /* @__PURE__ */ jsx(Component, {
183
+ id,
184
+ link
185
+ })
186
+ })
187
+ })] });
188
+ };
189
+
190
+ //#endregion
191
+ export { Link$1 as Link };
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+
3
+ //#region src/elements/MiniMap.d.ts
4
+ declare const MiniMap: React.FC<{
5
+ width?: number;
6
+ height?: number;
7
+ style?: React.CSSProperties;
8
+ }>;
9
+ //#endregion
10
+ export { MiniMap };
@@ -0,0 +1,80 @@
1
+ import { useAnode, useViewport } from "../context.js";
2
+ import React, { useMemo } from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+
5
+ //#region src/elements/MiniMap.tsx
6
+ const MiniMap = ({ width = 200, height = 150, style }) => {
7
+ const ctx = useAnode();
8
+ const { viewport } = useViewport();
9
+ const bounds = useMemo(() => {
10
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
11
+ if (ctx.entities.size === 0) return {
12
+ x: 0,
13
+ y: 0,
14
+ w: 1e3,
15
+ h: 1e3
16
+ };
17
+ for (const entity of ctx.entities.values()) {
18
+ const pos = ctx.getWorldPosition(entity.id);
19
+ minX = Math.min(minX, pos.x);
20
+ minY = Math.min(minY, pos.y);
21
+ maxX = Math.max(maxX, pos.x);
22
+ maxY = Math.max(maxY, pos.y);
23
+ }
24
+ const padding = 100;
25
+ minX -= padding;
26
+ minY -= padding;
27
+ maxX += padding;
28
+ maxY += padding;
29
+ return {
30
+ x: minX,
31
+ y: minY,
32
+ w: maxX - minX,
33
+ h: maxY - minY
34
+ };
35
+ }, [ctx.entities.size, Array.from(ctx.entities.values()).map((e) => e.position.x + e.position.y).join(",")]);
36
+ const scale = Math.min(width / bounds.w, height / bounds.h);
37
+ return /* @__PURE__ */ jsx("div", {
38
+ className: "anode-minimap",
39
+ style: {
40
+ position: "absolute",
41
+ bottom: 20,
42
+ right: 20,
43
+ width,
44
+ height,
45
+ background: "rgba(255, 255, 255, 0.8)",
46
+ border: "1px solid #ccc",
47
+ borderRadius: 4,
48
+ overflow: "hidden",
49
+ pointerEvents: "none",
50
+ zIndex: 100,
51
+ ...style
52
+ },
53
+ children: /* @__PURE__ */ jsxs("svg", {
54
+ width,
55
+ height,
56
+ viewBox: `${bounds.x} ${bounds.y} ${bounds.w} ${bounds.h}`,
57
+ children: [Array.from(ctx.entities.values()).map((entity) => {
58
+ const pos = ctx.getWorldPosition(entity.id);
59
+ return /* @__PURE__ */ jsx("rect", {
60
+ x: pos.x - 25,
61
+ y: pos.y - 25,
62
+ width: 50,
63
+ height: 50,
64
+ fill: "#94a3b8"
65
+ }, entity.id);
66
+ }), /* @__PURE__ */ jsx("rect", {
67
+ x: -viewport.x / viewport.k,
68
+ y: -viewport.y / viewport.k,
69
+ width: width / (scale * viewport.k),
70
+ height: height / (scale * viewport.k),
71
+ fill: "rgba(59, 130, 246, 0.1)",
72
+ stroke: "#3b82f6",
73
+ strokeWidth: 2 / scale
74
+ })]
75
+ })
76
+ });
77
+ };
78
+
79
+ //#endregion
80
+ export { MiniMap };