@aiready/components 0.14.1 → 0.14.3
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/dist/components/button.d.ts +1 -1
- package/dist/hooks/useForceSimulation.d.ts +1 -1
- package/dist/hooks/useForceSimulation.js +204 -249
- package/dist/hooks/useForceSimulation.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +201 -248
- package/dist/index.js.map +1 -1
- package/dist/{useForceSimulation-BNhxX4gH.d.ts → useForceSimulation-CjLhYevG.d.ts} +4 -3
- package/package.json +2 -2
- package/src/hooks/simulation-constants.ts +39 -0
- package/src/hooks/simulation-helpers.ts +42 -4
- package/src/hooks/useForceSimulation.ts +266 -430
|
@@ -3,7 +3,7 @@ import * as React from 'react';
|
|
|
3
3
|
import { VariantProps } from 'class-variance-authority';
|
|
4
4
|
|
|
5
5
|
declare const buttonVariants: (props?: ({
|
|
6
|
-
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "
|
|
6
|
+
variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | "glow" | "glass" | "accent" | null | undefined;
|
|
7
7
|
size?: "default" | "sm" | "lg" | "icon" | "xs" | null | undefined;
|
|
8
8
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
9
9
|
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { useState, useRef, useEffect } from 'react';
|
|
1
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
2
|
import * as d3 from 'd3';
|
|
3
3
|
|
|
4
|
+
// src/hooks/useForceSimulation.ts
|
|
5
|
+
|
|
4
6
|
// src/hooks/simulation-helpers.ts
|
|
5
7
|
function stabilizeNodes(nodes) {
|
|
6
8
|
nodes.forEach((n) => {
|
|
@@ -10,6 +12,16 @@ function stabilizeNodes(nodes) {
|
|
|
10
12
|
if (typeof n.y === "number") n.y = Number(n.y.toFixed(3));
|
|
11
13
|
});
|
|
12
14
|
}
|
|
15
|
+
function seedCircularPositions(nodes, width, height) {
|
|
16
|
+
const radius = Math.min(width, height) * 0.45;
|
|
17
|
+
nodes.forEach((n, i) => {
|
|
18
|
+
const angle = i * 2 * Math.PI / nodes.length;
|
|
19
|
+
n.x = width / 2 + radius * Math.cos(angle);
|
|
20
|
+
n.y = height / 2 + radius * Math.sin(angle);
|
|
21
|
+
n.vx = (Math.random() - 0.5) * 2;
|
|
22
|
+
n.vy = (Math.random() - 0.5) * 2;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
13
25
|
function seedRandomPositions(nodes, width, height) {
|
|
14
26
|
nodes.forEach((n) => {
|
|
15
27
|
n.x = Math.random() * width;
|
|
@@ -18,27 +30,68 @@ function seedRandomPositions(nodes, width, height) {
|
|
|
18
30
|
n.vy = (Math.random() - 0.5) * 10;
|
|
19
31
|
});
|
|
20
32
|
}
|
|
33
|
+
function safelyStopSimulation(simulation, nodes, options = {}) {
|
|
34
|
+
try {
|
|
35
|
+
if (options.stabilize) {
|
|
36
|
+
stabilizeNodes(nodes);
|
|
37
|
+
}
|
|
38
|
+
simulation.stop();
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.warn("AIReady: Failed to stop simulation safely:", error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/hooks/simulation-constants.ts
|
|
45
|
+
var SIMULATION_DEFAULTS = {
|
|
46
|
+
CHARGE_STRENGTH: -300,
|
|
47
|
+
LINK_DISTANCE: 100,
|
|
48
|
+
LINK_STRENGTH: 1,
|
|
49
|
+
COLLISION_STRENGTH: 1,
|
|
50
|
+
COLLISION_RADIUS: 10,
|
|
51
|
+
CENTER_STRENGTH: 0.1,
|
|
52
|
+
ALPHA_DECAY: 0.0228,
|
|
53
|
+
VELOCITY_DECAY: 0.4,
|
|
54
|
+
ALPHA_TARGET: 0,
|
|
55
|
+
WARM_ALPHA: 0.3,
|
|
56
|
+
ALPHA_MIN: 0.01,
|
|
57
|
+
TICK_THROTTLE_MS: 33,
|
|
58
|
+
// ~30 fps
|
|
59
|
+
MAX_SIMULATION_TIME_MS: 3e3,
|
|
60
|
+
STABILIZE_ON_STOP: true
|
|
61
|
+
};
|
|
62
|
+
var FORCE_NAMES = {
|
|
63
|
+
LINK: "link",
|
|
64
|
+
CHARGE: "charge",
|
|
65
|
+
CENTER: "center",
|
|
66
|
+
COLLISION: "collision",
|
|
67
|
+
X: "x",
|
|
68
|
+
Y: "y"
|
|
69
|
+
};
|
|
70
|
+
var EVENT_NAMES = {
|
|
71
|
+
TICK: "tick",
|
|
72
|
+
END: "end"
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// src/hooks/useForceSimulation.ts
|
|
21
76
|
function useForceSimulation(initialNodes, initialLinks, options) {
|
|
22
77
|
const {
|
|
23
|
-
chargeStrength =
|
|
24
|
-
linkDistance =
|
|
25
|
-
linkStrength =
|
|
26
|
-
collisionStrength =
|
|
27
|
-
collisionRadius =
|
|
28
|
-
centerStrength =
|
|
78
|
+
chargeStrength = SIMULATION_DEFAULTS.CHARGE_STRENGTH,
|
|
79
|
+
linkDistance = SIMULATION_DEFAULTS.LINK_DISTANCE,
|
|
80
|
+
linkStrength = SIMULATION_DEFAULTS.LINK_STRENGTH,
|
|
81
|
+
collisionStrength = SIMULATION_DEFAULTS.COLLISION_STRENGTH,
|
|
82
|
+
collisionRadius = SIMULATION_DEFAULTS.COLLISION_RADIUS,
|
|
83
|
+
centerStrength = SIMULATION_DEFAULTS.CENTER_STRENGTH,
|
|
29
84
|
width,
|
|
30
85
|
height,
|
|
31
|
-
alphaDecay =
|
|
32
|
-
velocityDecay =
|
|
33
|
-
alphaTarget =
|
|
34
|
-
warmAlpha =
|
|
35
|
-
alphaMin =
|
|
86
|
+
alphaDecay = SIMULATION_DEFAULTS.ALPHA_DECAY,
|
|
87
|
+
velocityDecay = SIMULATION_DEFAULTS.VELOCITY_DECAY,
|
|
88
|
+
alphaTarget = SIMULATION_DEFAULTS.ALPHA_TARGET,
|
|
89
|
+
warmAlpha = SIMULATION_DEFAULTS.WARM_ALPHA,
|
|
90
|
+
alphaMin = SIMULATION_DEFAULTS.ALPHA_MIN,
|
|
36
91
|
onTick,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
tickThrottleMs = 33,
|
|
41
|
-
maxSimulationTimeMs = 3e3
|
|
92
|
+
stabilizeOnStop = SIMULATION_DEFAULTS.STABILIZE_ON_STOP,
|
|
93
|
+
tickThrottleMs = SIMULATION_DEFAULTS.TICK_THROTTLE_MS,
|
|
94
|
+
maxSimulationTimeMs = SIMULATION_DEFAULTS.MAX_SIMULATION_TIME_MS
|
|
42
95
|
} = options;
|
|
43
96
|
const [nodes, setNodes] = useState(initialNodes);
|
|
44
97
|
const [links, setLinks] = useState(initialLinks);
|
|
@@ -46,8 +99,13 @@ function useForceSimulation(initialNodes, initialLinks, options) {
|
|
|
46
99
|
const [alpha, setAlpha] = useState(1);
|
|
47
100
|
const simulationRef = useRef(null);
|
|
48
101
|
const stopTimeoutRef = useRef(null);
|
|
102
|
+
const forcesEnabledRef = useRef(true);
|
|
103
|
+
const originalForcesRef = useRef({
|
|
104
|
+
charge: chargeStrength,
|
|
105
|
+
link: linkStrength
|
|
106
|
+
});
|
|
49
107
|
const nodesKey = initialNodes.map((n) => n.id).join("|");
|
|
50
|
-
const linksKey =
|
|
108
|
+
const linksKey = initialLinks.map((l) => {
|
|
51
109
|
const sourceId = typeof l.source === "string" ? l.source : l.source?.id;
|
|
52
110
|
const targetId = typeof l.target === "string" ? l.target : l.target?.id;
|
|
53
111
|
const linkType = l.type || "";
|
|
@@ -57,179 +115,19 @@ function useForceSimulation(initialNodes, initialLinks, options) {
|
|
|
57
115
|
const nodesCopy = initialNodes.map((node) => ({ ...node }));
|
|
58
116
|
const linksCopy = initialLinks.map((link) => ({ ...link }));
|
|
59
117
|
try {
|
|
60
|
-
nodesCopy
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
n.x = width / 2 + radius * Math.cos(angle);
|
|
64
|
-
n.y = height / 2 + radius * Math.sin(angle);
|
|
65
|
-
n.vx = (Math.random() - 0.5) * 2;
|
|
66
|
-
n.vy = (Math.random() - 0.5) * 2;
|
|
67
|
-
});
|
|
68
|
-
} catch (e) {
|
|
69
|
-
console.warn("Failed to seed node positions, falling back to random:", e);
|
|
118
|
+
seedCircularPositions(nodesCopy, width, height);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.warn("AIReady: Position seeding failed, using random fallback:", error);
|
|
70
121
|
seedRandomPositions(nodesCopy, width, height);
|
|
71
122
|
}
|
|
72
|
-
const simulation = d3.forceSimulation(
|
|
73
|
-
|
|
74
|
-
);
|
|
75
|
-
try {
|
|
76
|
-
const linkForce = d3.forceLink(
|
|
77
|
-
linksCopy
|
|
78
|
-
);
|
|
79
|
-
linkForce.id((d) => d.id).distance((d) => {
|
|
80
|
-
const link = d;
|
|
81
|
-
return link && link.distance != null ? link.distance : linkDistance;
|
|
82
|
-
}).strength(linkStrength);
|
|
83
|
-
simulation.force(
|
|
84
|
-
"link",
|
|
85
|
-
linkForce
|
|
86
|
-
);
|
|
87
|
-
} catch (e) {
|
|
88
|
-
console.warn("Failed to configure link force, using fallback:", e);
|
|
89
|
-
try {
|
|
90
|
-
simulation.force(
|
|
91
|
-
"link",
|
|
92
|
-
d3.forceLink(
|
|
93
|
-
linksCopy
|
|
94
|
-
)
|
|
95
|
-
);
|
|
96
|
-
} catch (fallbackError) {
|
|
97
|
-
console.warn("Fallback link force also failed:", fallbackError);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
try {
|
|
101
|
-
simulation.force(
|
|
102
|
-
"charge",
|
|
103
|
-
d3.forceManyBody().strength(chargeStrength)
|
|
104
|
-
);
|
|
105
|
-
simulation.force(
|
|
106
|
-
"center",
|
|
107
|
-
d3.forceCenter(width / 2, height / 2).strength(centerStrength)
|
|
108
|
-
);
|
|
109
|
-
const collide = d3.forceCollide().radius((d) => {
|
|
110
|
-
const node = d;
|
|
111
|
-
const nodeSize = node && node.size ? node.size : 10;
|
|
112
|
-
return nodeSize + collisionRadius;
|
|
113
|
-
}).strength(collisionStrength);
|
|
114
|
-
simulation.force("collision", collide);
|
|
115
|
-
simulation.force(
|
|
116
|
-
"x",
|
|
117
|
-
d3.forceX(width / 2).strength(Math.max(0.02, centerStrength * 0.5))
|
|
118
|
-
);
|
|
119
|
-
simulation.force(
|
|
120
|
-
"y",
|
|
121
|
-
d3.forceY(height / 2).strength(Math.max(0.02, centerStrength * 0.5))
|
|
122
|
-
);
|
|
123
|
-
simulation.alphaDecay(alphaDecay);
|
|
124
|
-
simulation.velocityDecay(velocityDecay);
|
|
125
|
-
simulation.alphaMin(alphaMin);
|
|
126
|
-
try {
|
|
127
|
-
simulation.alphaTarget(alphaTarget);
|
|
128
|
-
} catch (e) {
|
|
129
|
-
console.warn("Failed to set alpha target:", e);
|
|
130
|
-
}
|
|
131
|
-
try {
|
|
132
|
-
simulation.alpha(warmAlpha);
|
|
133
|
-
} catch (e) {
|
|
134
|
-
console.warn("Failed to set initial alpha:", e);
|
|
135
|
-
}
|
|
136
|
-
} catch (e) {
|
|
137
|
-
console.warn("Failed to configure simulation forces:", e);
|
|
138
|
-
}
|
|
123
|
+
const simulation = d3.forceSimulation(nodesCopy);
|
|
124
|
+
applySimulationForces(simulation, linksCopy);
|
|
125
|
+
configureSimulationParameters(simulation);
|
|
139
126
|
simulationRef.current = simulation;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
console.warn("Failed to clear simulation timeout:", e);
|
|
145
|
-
}
|
|
146
|
-
stopTimeoutRef.current = null;
|
|
147
|
-
}
|
|
148
|
-
if (maxSimulationTimeMs && maxSimulationTimeMs > 0) {
|
|
149
|
-
stopTimeoutRef.current = globalThis.setTimeout(() => {
|
|
150
|
-
try {
|
|
151
|
-
if (stabilizeOnStop) {
|
|
152
|
-
stabilizeNodes(nodesCopy);
|
|
153
|
-
}
|
|
154
|
-
simulation.alpha(0);
|
|
155
|
-
simulation.stop();
|
|
156
|
-
} catch (e) {
|
|
157
|
-
console.warn("Failed to stop simulation:", e);
|
|
158
|
-
}
|
|
159
|
-
setIsRunning(false);
|
|
160
|
-
setNodes([...nodesCopy]);
|
|
161
|
-
setLinks([...linksCopy]);
|
|
162
|
-
}, maxSimulationTimeMs);
|
|
163
|
-
}
|
|
164
|
-
let rafId = null;
|
|
165
|
-
let lastUpdate = 0;
|
|
166
|
-
const tickHandler = () => {
|
|
167
|
-
try {
|
|
168
|
-
if (typeof onTick === "function")
|
|
169
|
-
onTick(nodesCopy, linksCopy, simulation);
|
|
170
|
-
} catch (e) {
|
|
171
|
-
console.warn("Tick callback error:", e);
|
|
172
|
-
}
|
|
173
|
-
try {
|
|
174
|
-
if (simulation.alpha() <= alphaMin) {
|
|
175
|
-
try {
|
|
176
|
-
if (stabilizeOnStop) {
|
|
177
|
-
stabilizeNodes(nodesCopy);
|
|
178
|
-
}
|
|
179
|
-
simulation.stop();
|
|
180
|
-
} catch (e) {
|
|
181
|
-
console.warn("Failed to stop simulation:", e);
|
|
182
|
-
}
|
|
183
|
-
setAlpha(simulation.alpha());
|
|
184
|
-
setIsRunning(false);
|
|
185
|
-
setNodes([...nodesCopy]);
|
|
186
|
-
setLinks([...linksCopy]);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
} catch (e) {
|
|
190
|
-
console.warn("Error checking simulation alpha:", e);
|
|
191
|
-
}
|
|
192
|
-
const now = Date.now();
|
|
193
|
-
const shouldUpdate = now - lastUpdate >= tickThrottleMs;
|
|
194
|
-
if (rafId == null && shouldUpdate) {
|
|
195
|
-
rafId = (globalThis.requestAnimationFrame || ((cb) => setTimeout(cb, 16)))(() => {
|
|
196
|
-
rafId = null;
|
|
197
|
-
lastUpdate = Date.now();
|
|
198
|
-
setNodes([...nodesCopy]);
|
|
199
|
-
setLinks([...linksCopy]);
|
|
200
|
-
setAlpha(simulation.alpha());
|
|
201
|
-
setIsRunning(simulation.alpha() > simulation.alphaMin());
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
};
|
|
205
|
-
simulation.on("tick", tickHandler);
|
|
206
|
-
simulation.on("end", () => {
|
|
207
|
-
setIsRunning(false);
|
|
208
|
-
});
|
|
209
|
-
return () => {
|
|
210
|
-
try {
|
|
211
|
-
simulation.on("tick", null);
|
|
212
|
-
} catch (e) {
|
|
213
|
-
console.warn("Failed to clear simulation tick handler:", e);
|
|
214
|
-
}
|
|
215
|
-
if (stopTimeoutRef.current != null) {
|
|
216
|
-
try {
|
|
217
|
-
globalThis.clearTimeout(stopTimeoutRef.current);
|
|
218
|
-
} catch (e) {
|
|
219
|
-
console.warn("Failed to clear timeout on cleanup:", e);
|
|
220
|
-
}
|
|
221
|
-
stopTimeoutRef.current = null;
|
|
222
|
-
}
|
|
223
|
-
if (rafId != null) {
|
|
224
|
-
try {
|
|
225
|
-
(globalThis.cancelAnimationFrame || ((id) => clearTimeout(id)))(rafId);
|
|
226
|
-
} catch (e) {
|
|
227
|
-
console.warn("Failed to cancel animation frame:", e);
|
|
228
|
-
}
|
|
229
|
-
rafId = null;
|
|
230
|
-
}
|
|
231
|
-
simulation.stop();
|
|
232
|
-
};
|
|
127
|
+
const rafState = { rafId: null, lastUpdate: 0 };
|
|
128
|
+
setupTickHandler(simulation, nodesCopy, linksCopy, rafState);
|
|
129
|
+
setupStopTimer(simulation, nodesCopy, linksCopy);
|
|
130
|
+
return () => cleanupSimulation(simulation, rafState);
|
|
233
131
|
}, [
|
|
234
132
|
nodesKey,
|
|
235
133
|
linksKey,
|
|
@@ -249,98 +147,155 @@ function useForceSimulation(initialNodes, initialLinks, options) {
|
|
|
249
147
|
tickThrottleMs,
|
|
250
148
|
maxSimulationTimeMs
|
|
251
149
|
]);
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
150
|
+
const applySimulationForces = (simulation, linksCopy) => {
|
|
151
|
+
try {
|
|
152
|
+
const linkForce = d3.forceLink(linksCopy).id((d) => d.id).distance((d) => d.distance ?? linkDistance).strength(linkStrength);
|
|
153
|
+
simulation.force(FORCE_NAMES.LINK, linkForce).force(FORCE_NAMES.CHARGE, d3.forceManyBody().strength(chargeStrength)).force(FORCE_NAMES.CENTER, d3.forceCenter(width / 2, height / 2).strength(centerStrength)).force(
|
|
154
|
+
FORCE_NAMES.COLLISION,
|
|
155
|
+
d3.forceCollide().radius((d) => (d.size ?? 10) + collisionRadius).strength(collisionStrength)
|
|
156
|
+
).force(FORCE_NAMES.X, d3.forceX(width / 2).strength(Math.max(0.02, centerStrength * 0.5))).force(FORCE_NAMES.Y, d3.forceY(height / 2).strength(Math.max(0.02, centerStrength * 0.5)));
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.warn("AIReady: Failed to configure simulation forces:", error);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const configureSimulationParameters = (simulation) => {
|
|
162
|
+
simulation.alphaDecay(alphaDecay).velocityDecay(velocityDecay).alphaMin(alphaMin).alphaTarget(alphaTarget).alpha(warmAlpha);
|
|
163
|
+
};
|
|
164
|
+
const setupStopTimer = (simulation, nodesCopy, linksCopy) => {
|
|
165
|
+
if (stopTimeoutRef.current) {
|
|
166
|
+
clearTimeout(stopTimeoutRef.current);
|
|
167
|
+
}
|
|
168
|
+
if (maxSimulationTimeMs > 0) {
|
|
169
|
+
stopTimeoutRef.current = setTimeout(() => {
|
|
170
|
+
safelyStopSimulation(simulation, nodesCopy, { stabilize: stabilizeOnStop });
|
|
171
|
+
updateStateAfterStop(nodesCopy, linksCopy, 0);
|
|
172
|
+
}, maxSimulationTimeMs);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
const updateStateAfterStop = (nodesCopy, linksCopy, currentAlpha) => {
|
|
176
|
+
setIsRunning(false);
|
|
177
|
+
setAlpha(currentAlpha);
|
|
178
|
+
setNodes([...nodesCopy]);
|
|
179
|
+
setLinks([...linksCopy]);
|
|
180
|
+
};
|
|
181
|
+
const setupTickHandler = (simulation, nodesCopy, linksCopy, rafState) => {
|
|
182
|
+
const handleTick = () => {
|
|
183
|
+
if (onTick) {
|
|
261
184
|
try {
|
|
262
|
-
|
|
263
|
-
} catch (
|
|
264
|
-
console.warn("
|
|
185
|
+
onTick(nodesCopy, linksCopy, simulation);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.warn("AIReady: Simulation onTick callback failed:", error);
|
|
265
188
|
}
|
|
266
|
-
stopTimeoutRef.current = null;
|
|
267
189
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
190
|
+
const currentAlpha = simulation.alpha();
|
|
191
|
+
if (currentAlpha <= alphaMin) {
|
|
192
|
+
safelyStopSimulation(simulation, nodesCopy, { stabilize: stabilizeOnStop });
|
|
193
|
+
updateStateAfterStop(nodesCopy, linksCopy, currentAlpha);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
syncStateOnTick(nodesCopy, linksCopy, currentAlpha, rafState);
|
|
197
|
+
};
|
|
198
|
+
simulation.on(EVENT_NAMES.TICK, handleTick);
|
|
199
|
+
simulation.on(EVENT_NAMES.END, () => setIsRunning(false));
|
|
200
|
+
};
|
|
201
|
+
const syncStateOnTick = (nodesCopy, linksCopy, currentAlpha, rafState) => {
|
|
202
|
+
const now = Date.now();
|
|
203
|
+
if (rafState.rafId === null && now - rafState.lastUpdate >= tickThrottleMs) {
|
|
204
|
+
rafState.rafId = requestAnimationFrame(() => {
|
|
205
|
+
rafState.rafId = null;
|
|
206
|
+
rafState.lastUpdate = Date.now();
|
|
207
|
+
setNodes([...nodesCopy]);
|
|
208
|
+
setLinks([...linksCopy]);
|
|
209
|
+
setAlpha(currentAlpha);
|
|
210
|
+
setIsRunning(currentAlpha > alphaMin);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const cleanupSimulation = (simulation, rafState) => {
|
|
215
|
+
simulation.on(EVENT_NAMES.TICK, null);
|
|
216
|
+
if (stopTimeoutRef.current) clearTimeout(stopTimeoutRef.current);
|
|
217
|
+
if (rafState.rafId !== null) cancelAnimationFrame(rafState.rafId);
|
|
218
|
+
simulation.stop();
|
|
219
|
+
};
|
|
220
|
+
const restartSimulation = useCallback(() => {
|
|
221
|
+
const sim = simulationRef.current;
|
|
222
|
+
if (!sim) return;
|
|
223
|
+
try {
|
|
224
|
+
sim.alphaTarget(warmAlpha).restart();
|
|
225
|
+
setIsRunning(true);
|
|
226
|
+
if (stopTimeoutRef.current) clearTimeout(stopTimeoutRef.current);
|
|
227
|
+
if (maxSimulationTimeMs > 0) {
|
|
228
|
+
stopTimeoutRef.current = setTimeout(() => {
|
|
229
|
+
sim.alpha(0);
|
|
230
|
+
sim.stop();
|
|
276
231
|
setIsRunning(false);
|
|
277
232
|
}, maxSimulationTimeMs);
|
|
278
233
|
}
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.warn("AIReady: Failed to restart simulation:", error);
|
|
279
236
|
}
|
|
280
|
-
};
|
|
281
|
-
const
|
|
237
|
+
}, [warmAlpha, maxSimulationTimeMs]);
|
|
238
|
+
const stopSimulation = useCallback(() => {
|
|
282
239
|
if (simulationRef.current) {
|
|
283
240
|
simulationRef.current.stop();
|
|
284
241
|
setIsRunning(false);
|
|
285
242
|
}
|
|
286
|
-
};
|
|
287
|
-
const
|
|
288
|
-
charge: chargeStrength,
|
|
289
|
-
link: linkStrength,
|
|
290
|
-
collision: collisionStrength
|
|
291
|
-
});
|
|
292
|
-
const forcesEnabledRef = useRef(true);
|
|
293
|
-
const setForcesEnabled = (enabled) => {
|
|
243
|
+
}, []);
|
|
244
|
+
const setForcesEnabled = useCallback((enabled) => {
|
|
294
245
|
const sim = simulationRef.current;
|
|
295
|
-
if (!sim) return;
|
|
296
|
-
if (forcesEnabledRef.current === enabled) return;
|
|
246
|
+
if (!sim || forcesEnabledRef.current === enabled) return;
|
|
297
247
|
forcesEnabledRef.current = enabled;
|
|
298
248
|
try {
|
|
299
|
-
const charge = sim.force(
|
|
300
|
-
|
|
301
|
-
);
|
|
302
|
-
if (charge && typeof charge.strength === "function") {
|
|
249
|
+
const charge = sim.force(FORCE_NAMES.CHARGE);
|
|
250
|
+
if (charge) {
|
|
303
251
|
charge.strength(enabled ? originalForcesRef.current.charge : 0);
|
|
304
252
|
}
|
|
305
|
-
const link = sim.force(
|
|
306
|
-
if (link
|
|
253
|
+
const link = sim.force(FORCE_NAMES.LINK);
|
|
254
|
+
if (link) {
|
|
307
255
|
link.strength(enabled ? originalForcesRef.current.link : 0);
|
|
308
256
|
}
|
|
309
|
-
|
|
310
|
-
|
|
257
|
+
sim.alpha(warmAlpha).restart();
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.warn("AIReady: Failed to toggle simulation forces:", error);
|
|
311
260
|
}
|
|
312
|
-
};
|
|
261
|
+
}, [warmAlpha]);
|
|
313
262
|
return {
|
|
314
263
|
nodes,
|
|
315
264
|
links,
|
|
316
|
-
restart,
|
|
317
|
-
stop,
|
|
265
|
+
restart: restartSimulation,
|
|
266
|
+
stop: stopSimulation,
|
|
318
267
|
isRunning,
|
|
319
268
|
alpha,
|
|
320
269
|
setForcesEnabled
|
|
321
270
|
};
|
|
322
271
|
}
|
|
323
272
|
function useDrag(simulation) {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
273
|
+
const handleDragStart = useCallback(
|
|
274
|
+
(event, node) => {
|
|
275
|
+
if (!simulation) return;
|
|
276
|
+
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
277
|
+
node.fx = node.x;
|
|
278
|
+
node.fy = node.y;
|
|
279
|
+
},
|
|
280
|
+
[simulation]
|
|
281
|
+
);
|
|
282
|
+
const handleDragged = useCallback((event, node) => {
|
|
331
283
|
node.fx = event.x;
|
|
332
284
|
node.fy = event.y;
|
|
333
|
-
};
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
285
|
+
}, []);
|
|
286
|
+
const handleDragEnd = useCallback(
|
|
287
|
+
(event, node) => {
|
|
288
|
+
if (!simulation) return;
|
|
289
|
+
if (!event.active) simulation.alphaTarget(0);
|
|
290
|
+
node.fx = null;
|
|
291
|
+
node.fy = null;
|
|
292
|
+
},
|
|
293
|
+
[simulation]
|
|
294
|
+
);
|
|
340
295
|
return {
|
|
341
|
-
onDragStart:
|
|
342
|
-
onDrag:
|
|
343
|
-
onDragEnd:
|
|
296
|
+
onDragStart: handleDragStart,
|
|
297
|
+
onDrag: handleDragged,
|
|
298
|
+
onDragEnd: handleDragEnd
|
|
344
299
|
};
|
|
345
300
|
}
|
|
346
301
|
|