@aiready/components 0.14.2 → 0.14.4
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/hooks/useForceSimulation.d.ts +1 -1
- package/dist/hooks/useForceSimulation.js +231 -253
- package/dist/hooks/useForceSimulation.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +228 -252
- 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 +306 -421
|
@@ -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,24 @@ 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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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(
|
|
121
|
+
"AIReady: Position seeding failed, using random fallback:",
|
|
122
|
+
error
|
|
123
|
+
);
|
|
70
124
|
seedRandomPositions(nodesCopy, width, height);
|
|
71
125
|
}
|
|
72
126
|
const simulation = d3.forceSimulation(
|
|
73
127
|
nodesCopy
|
|
74
128
|
);
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
}
|
|
129
|
+
applySimulationForces(simulation, linksCopy);
|
|
130
|
+
configureSimulationParameters(simulation);
|
|
139
131
|
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
|
-
};
|
|
132
|
+
const rafState = { rafId: null, lastUpdate: 0 };
|
|
133
|
+
setupTickHandler(simulation, nodesCopy, linksCopy, rafState);
|
|
134
|
+
setupStopTimer(simulation, nodesCopy, linksCopy);
|
|
135
|
+
return () => cleanupSimulation(simulation, rafState);
|
|
233
136
|
}, [
|
|
234
137
|
nodesKey,
|
|
235
138
|
linksKey,
|
|
@@ -249,98 +152,173 @@ function useForceSimulation(initialNodes, initialLinks, options) {
|
|
|
249
152
|
tickThrottleMs,
|
|
250
153
|
maxSimulationTimeMs
|
|
251
154
|
]);
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
155
|
+
const applySimulationForces = (simulation, linksCopy) => {
|
|
156
|
+
try {
|
|
157
|
+
const linkForce = d3.forceLink(linksCopy).id((d) => d.id).distance((d) => d.distance ?? linkDistance).strength(linkStrength);
|
|
158
|
+
simulation.force(FORCE_NAMES.LINK, linkForce).force(FORCE_NAMES.CHARGE, d3.forceManyBody().strength(chargeStrength)).force(
|
|
159
|
+
FORCE_NAMES.CENTER,
|
|
160
|
+
d3.forceCenter(width / 2, height / 2).strength(centerStrength)
|
|
161
|
+
).force(
|
|
162
|
+
FORCE_NAMES.COLLISION,
|
|
163
|
+
d3.forceCollide().radius((d) => (d.size ?? 10) + collisionRadius).strength(collisionStrength)
|
|
164
|
+
).force(
|
|
165
|
+
FORCE_NAMES.X,
|
|
166
|
+
d3.forceX(width / 2).strength(Math.max(0.02, centerStrength * 0.5))
|
|
167
|
+
).force(
|
|
168
|
+
FORCE_NAMES.Y,
|
|
169
|
+
d3.forceY(height / 2).strength(Math.max(0.02, centerStrength * 0.5))
|
|
170
|
+
);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.warn("AIReady: Failed to configure simulation forces:", error);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
const configureSimulationParameters = (simulation) => {
|
|
176
|
+
simulation.alphaDecay(alphaDecay).velocityDecay(velocityDecay).alphaMin(alphaMin).alphaTarget(alphaTarget).alpha(warmAlpha);
|
|
177
|
+
};
|
|
178
|
+
const setupStopTimer = (simulation, nodesCopy, linksCopy) => {
|
|
179
|
+
if (stopTimeoutRef.current) {
|
|
180
|
+
clearTimeout(stopTimeoutRef.current);
|
|
181
|
+
}
|
|
182
|
+
if (maxSimulationTimeMs > 0) {
|
|
183
|
+
stopTimeoutRef.current = setTimeout(() => {
|
|
184
|
+
safelyStopSimulation(simulation, nodesCopy, {
|
|
185
|
+
stabilize: stabilizeOnStop
|
|
186
|
+
});
|
|
187
|
+
updateStateAfterStop(nodesCopy, linksCopy, 0);
|
|
188
|
+
}, maxSimulationTimeMs);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const updateStateAfterStop = (nodesCopy, linksCopy, currentAlpha) => {
|
|
192
|
+
setIsRunning(false);
|
|
193
|
+
setAlpha(currentAlpha);
|
|
194
|
+
setNodes([...nodesCopy]);
|
|
195
|
+
setLinks([...linksCopy]);
|
|
196
|
+
};
|
|
197
|
+
const setupTickHandler = (simulation, nodesCopy, linksCopy, rafState) => {
|
|
198
|
+
const handleTick = () => {
|
|
199
|
+
if (onTick) {
|
|
261
200
|
try {
|
|
262
|
-
|
|
263
|
-
} catch (
|
|
264
|
-
console.warn("
|
|
201
|
+
onTick(nodesCopy, linksCopy, simulation);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.warn("AIReady: Simulation onTick callback failed:", error);
|
|
265
204
|
}
|
|
266
|
-
stopTimeoutRef.current = null;
|
|
267
205
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
setIsRunning(false);
|
|
277
|
-
}, maxSimulationTimeMs);
|
|
206
|
+
const currentAlpha = simulation.alpha();
|
|
207
|
+
if (currentAlpha <= alphaMin) {
|
|
208
|
+
safelyStopSimulation(simulation, nodesCopy, {
|
|
209
|
+
stabilize: stabilizeOnStop
|
|
210
|
+
});
|
|
211
|
+
updateStateAfterStop(nodesCopy, linksCopy, currentAlpha);
|
|
212
|
+
return;
|
|
278
213
|
}
|
|
279
|
-
|
|
214
|
+
syncStateOnTick(nodesCopy, linksCopy, currentAlpha, rafState);
|
|
215
|
+
};
|
|
216
|
+
simulation.on(EVENT_NAMES.TICK, handleTick);
|
|
217
|
+
simulation.on(EVENT_NAMES.END, () => setIsRunning(false));
|
|
280
218
|
};
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
219
|
+
const syncStateOnTick = (nodesCopy, linksCopy, currentAlpha, rafState) => {
|
|
220
|
+
const now = Date.now();
|
|
221
|
+
if (rafState.rafId === null && now - rafState.lastUpdate >= tickThrottleMs) {
|
|
222
|
+
rafState.rafId = requestAnimationFrame(() => {
|
|
223
|
+
rafState.rafId = null;
|
|
224
|
+
rafState.lastUpdate = Date.now();
|
|
225
|
+
setNodes([...nodesCopy]);
|
|
226
|
+
setLinks([...linksCopy]);
|
|
227
|
+
setAlpha(currentAlpha);
|
|
228
|
+
setIsRunning(currentAlpha > alphaMin);
|
|
229
|
+
});
|
|
285
230
|
}
|
|
286
231
|
};
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
232
|
+
const cleanupSimulation = (simulation, rafState) => {
|
|
233
|
+
simulation.on(EVENT_NAMES.TICK, null);
|
|
234
|
+
if (stopTimeoutRef.current) clearTimeout(stopTimeoutRef.current);
|
|
235
|
+
if (rafState.rafId !== null) cancelAnimationFrame(rafState.rafId);
|
|
236
|
+
simulation.stop();
|
|
237
|
+
};
|
|
238
|
+
const restartSimulation = useCallback(() => {
|
|
294
239
|
const sim = simulationRef.current;
|
|
295
240
|
if (!sim) return;
|
|
296
|
-
if (forcesEnabledRef.current === enabled) return;
|
|
297
|
-
forcesEnabledRef.current = enabled;
|
|
298
241
|
try {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
);
|
|
302
|
-
if (
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
242
|
+
sim.alphaTarget(warmAlpha).restart();
|
|
243
|
+
setIsRunning(true);
|
|
244
|
+
if (stopTimeoutRef.current) clearTimeout(stopTimeoutRef.current);
|
|
245
|
+
if (maxSimulationTimeMs > 0) {
|
|
246
|
+
stopTimeoutRef.current = setTimeout(() => {
|
|
247
|
+
sim.alpha(0);
|
|
248
|
+
sim.stop();
|
|
249
|
+
setIsRunning(false);
|
|
250
|
+
}, maxSimulationTimeMs);
|
|
308
251
|
}
|
|
309
|
-
} catch (
|
|
310
|
-
console.warn("Failed to
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.warn("AIReady: Failed to restart simulation:", error);
|
|
311
254
|
}
|
|
312
|
-
};
|
|
255
|
+
}, [warmAlpha, maxSimulationTimeMs]);
|
|
256
|
+
const stopSimulation = useCallback(() => {
|
|
257
|
+
if (simulationRef.current) {
|
|
258
|
+
simulationRef.current.stop();
|
|
259
|
+
setIsRunning(false);
|
|
260
|
+
}
|
|
261
|
+
}, []);
|
|
262
|
+
const setForcesEnabled = useCallback(
|
|
263
|
+
(enabled) => {
|
|
264
|
+
const sim = simulationRef.current;
|
|
265
|
+
if (!sim || forcesEnabledRef.current === enabled) return;
|
|
266
|
+
forcesEnabledRef.current = enabled;
|
|
267
|
+
try {
|
|
268
|
+
const charge = sim.force(
|
|
269
|
+
FORCE_NAMES.CHARGE
|
|
270
|
+
);
|
|
271
|
+
if (charge) {
|
|
272
|
+
charge.strength(enabled ? originalForcesRef.current.charge : 0);
|
|
273
|
+
}
|
|
274
|
+
const link = sim.force(FORCE_NAMES.LINK);
|
|
275
|
+
if (link) {
|
|
276
|
+
link.strength(enabled ? originalForcesRef.current.link : 0);
|
|
277
|
+
}
|
|
278
|
+
sim.alpha(warmAlpha).restart();
|
|
279
|
+
} catch (error) {
|
|
280
|
+
console.warn("AIReady: Failed to toggle simulation forces:", error);
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
[warmAlpha]
|
|
284
|
+
);
|
|
313
285
|
return {
|
|
314
286
|
nodes,
|
|
315
287
|
links,
|
|
316
|
-
restart,
|
|
317
|
-
stop,
|
|
288
|
+
restart: restartSimulation,
|
|
289
|
+
stop: stopSimulation,
|
|
318
290
|
isRunning,
|
|
319
291
|
alpha,
|
|
320
292
|
setForcesEnabled
|
|
321
293
|
};
|
|
322
294
|
}
|
|
323
295
|
function useDrag(simulation) {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
296
|
+
const handleDragStart = useCallback(
|
|
297
|
+
(event, node) => {
|
|
298
|
+
if (!simulation) return;
|
|
299
|
+
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
300
|
+
node.fx = node.x;
|
|
301
|
+
node.fy = node.y;
|
|
302
|
+
},
|
|
303
|
+
[simulation]
|
|
304
|
+
);
|
|
305
|
+
const handleDragged = useCallback((event, node) => {
|
|
331
306
|
node.fx = event.x;
|
|
332
307
|
node.fy = event.y;
|
|
333
|
-
};
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
308
|
+
}, []);
|
|
309
|
+
const handleDragEnd = useCallback(
|
|
310
|
+
(event, node) => {
|
|
311
|
+
if (!simulation) return;
|
|
312
|
+
if (!event.active) simulation.alphaTarget(0);
|
|
313
|
+
node.fx = null;
|
|
314
|
+
node.fy = null;
|
|
315
|
+
},
|
|
316
|
+
[simulation]
|
|
317
|
+
);
|
|
340
318
|
return {
|
|
341
|
-
onDragStart:
|
|
342
|
-
onDrag:
|
|
343
|
-
onDragEnd:
|
|
319
|
+
onDragStart: handleDragStart,
|
|
320
|
+
onDrag: handleDragged,
|
|
321
|
+
onDragEnd: handleDragEnd
|
|
344
322
|
};
|
|
345
323
|
}
|
|
346
324
|
|