@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
package/dist/index.js
CHANGED
|
@@ -2060,6 +2060,16 @@ function stabilizeNodes(nodes) {
|
|
|
2060
2060
|
if (typeof n.y === "number") n.y = Number(n.y.toFixed(3));
|
|
2061
2061
|
});
|
|
2062
2062
|
}
|
|
2063
|
+
function seedCircularPositions(nodes, width, height) {
|
|
2064
|
+
const radius = Math.min(width, height) * 0.45;
|
|
2065
|
+
nodes.forEach((n, i) => {
|
|
2066
|
+
const angle = i * 2 * Math.PI / nodes.length;
|
|
2067
|
+
n.x = width / 2 + radius * Math.cos(angle);
|
|
2068
|
+
n.y = height / 2 + radius * Math.sin(angle);
|
|
2069
|
+
n.vx = (Math.random() - 0.5) * 2;
|
|
2070
|
+
n.vy = (Math.random() - 0.5) * 2;
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2063
2073
|
function seedRandomPositions(nodes, width, height) {
|
|
2064
2074
|
nodes.forEach((n) => {
|
|
2065
2075
|
n.x = Math.random() * width;
|
|
@@ -2068,27 +2078,68 @@ function seedRandomPositions(nodes, width, height) {
|
|
|
2068
2078
|
n.vy = (Math.random() - 0.5) * 10;
|
|
2069
2079
|
});
|
|
2070
2080
|
}
|
|
2081
|
+
function safelyStopSimulation(simulation, nodes, options = {}) {
|
|
2082
|
+
try {
|
|
2083
|
+
if (options.stabilize) {
|
|
2084
|
+
stabilizeNodes(nodes);
|
|
2085
|
+
}
|
|
2086
|
+
simulation.stop();
|
|
2087
|
+
} catch (error) {
|
|
2088
|
+
console.warn("AIReady: Failed to stop simulation safely:", error);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// src/hooks/simulation-constants.ts
|
|
2093
|
+
var SIMULATION_DEFAULTS = {
|
|
2094
|
+
CHARGE_STRENGTH: -300,
|
|
2095
|
+
LINK_DISTANCE: 100,
|
|
2096
|
+
LINK_STRENGTH: 1,
|
|
2097
|
+
COLLISION_STRENGTH: 1,
|
|
2098
|
+
COLLISION_RADIUS: 10,
|
|
2099
|
+
CENTER_STRENGTH: 0.1,
|
|
2100
|
+
ALPHA_DECAY: 0.0228,
|
|
2101
|
+
VELOCITY_DECAY: 0.4,
|
|
2102
|
+
ALPHA_TARGET: 0,
|
|
2103
|
+
WARM_ALPHA: 0.3,
|
|
2104
|
+
ALPHA_MIN: 0.01,
|
|
2105
|
+
TICK_THROTTLE_MS: 33,
|
|
2106
|
+
// ~30 fps
|
|
2107
|
+
MAX_SIMULATION_TIME_MS: 3e3,
|
|
2108
|
+
STABILIZE_ON_STOP: true
|
|
2109
|
+
};
|
|
2110
|
+
var FORCE_NAMES = {
|
|
2111
|
+
LINK: "link",
|
|
2112
|
+
CHARGE: "charge",
|
|
2113
|
+
CENTER: "center",
|
|
2114
|
+
COLLISION: "collision",
|
|
2115
|
+
X: "x",
|
|
2116
|
+
Y: "y"
|
|
2117
|
+
};
|
|
2118
|
+
var EVENT_NAMES = {
|
|
2119
|
+
TICK: "tick",
|
|
2120
|
+
END: "end"
|
|
2121
|
+
};
|
|
2122
|
+
|
|
2123
|
+
// src/hooks/useForceSimulation.ts
|
|
2071
2124
|
function useForceSimulation(initialNodes, initialLinks, options) {
|
|
2072
2125
|
const {
|
|
2073
|
-
chargeStrength =
|
|
2074
|
-
linkDistance =
|
|
2075
|
-
linkStrength =
|
|
2076
|
-
collisionStrength =
|
|
2077
|
-
collisionRadius =
|
|
2078
|
-
centerStrength =
|
|
2126
|
+
chargeStrength = SIMULATION_DEFAULTS.CHARGE_STRENGTH,
|
|
2127
|
+
linkDistance = SIMULATION_DEFAULTS.LINK_DISTANCE,
|
|
2128
|
+
linkStrength = SIMULATION_DEFAULTS.LINK_STRENGTH,
|
|
2129
|
+
collisionStrength = SIMULATION_DEFAULTS.COLLISION_STRENGTH,
|
|
2130
|
+
collisionRadius = SIMULATION_DEFAULTS.COLLISION_RADIUS,
|
|
2131
|
+
centerStrength = SIMULATION_DEFAULTS.CENTER_STRENGTH,
|
|
2079
2132
|
width,
|
|
2080
2133
|
height,
|
|
2081
|
-
alphaDecay =
|
|
2082
|
-
velocityDecay =
|
|
2083
|
-
alphaTarget =
|
|
2084
|
-
warmAlpha =
|
|
2085
|
-
alphaMin =
|
|
2134
|
+
alphaDecay = SIMULATION_DEFAULTS.ALPHA_DECAY,
|
|
2135
|
+
velocityDecay = SIMULATION_DEFAULTS.VELOCITY_DECAY,
|
|
2136
|
+
alphaTarget = SIMULATION_DEFAULTS.ALPHA_TARGET,
|
|
2137
|
+
warmAlpha = SIMULATION_DEFAULTS.WARM_ALPHA,
|
|
2138
|
+
alphaMin = SIMULATION_DEFAULTS.ALPHA_MIN,
|
|
2086
2139
|
onTick,
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
tickThrottleMs = 33,
|
|
2091
|
-
maxSimulationTimeMs = 3e3
|
|
2140
|
+
stabilizeOnStop = SIMULATION_DEFAULTS.STABILIZE_ON_STOP,
|
|
2141
|
+
tickThrottleMs = SIMULATION_DEFAULTS.TICK_THROTTLE_MS,
|
|
2142
|
+
maxSimulationTimeMs = SIMULATION_DEFAULTS.MAX_SIMULATION_TIME_MS
|
|
2092
2143
|
} = options;
|
|
2093
2144
|
const [nodes, setNodes] = useState(initialNodes);
|
|
2094
2145
|
const [links, setLinks] = useState(initialLinks);
|
|
@@ -2096,8 +2147,13 @@ function useForceSimulation(initialNodes, initialLinks, options) {
|
|
|
2096
2147
|
const [alpha, setAlpha] = useState(1);
|
|
2097
2148
|
const simulationRef = useRef(null);
|
|
2098
2149
|
const stopTimeoutRef = useRef(null);
|
|
2150
|
+
const forcesEnabledRef = useRef(true);
|
|
2151
|
+
const originalForcesRef = useRef({
|
|
2152
|
+
charge: chargeStrength,
|
|
2153
|
+
link: linkStrength
|
|
2154
|
+
});
|
|
2099
2155
|
const nodesKey = initialNodes.map((n) => n.id).join("|");
|
|
2100
|
-
const linksKey =
|
|
2156
|
+
const linksKey = initialLinks.map((l) => {
|
|
2101
2157
|
const sourceId = typeof l.source === "string" ? l.source : l.source?.id;
|
|
2102
2158
|
const targetId = typeof l.target === "string" ? l.target : l.target?.id;
|
|
2103
2159
|
const linkType = l.type || "";
|
|
@@ -2107,179 +2163,24 @@ function useForceSimulation(initialNodes, initialLinks, options) {
|
|
|
2107
2163
|
const nodesCopy = initialNodes.map((node) => ({ ...node }));
|
|
2108
2164
|
const linksCopy = initialLinks.map((link) => ({ ...link }));
|
|
2109
2165
|
try {
|
|
2110
|
-
nodesCopy
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
n.vy = (Math.random() - 0.5) * 2;
|
|
2117
|
-
});
|
|
2118
|
-
} catch (e) {
|
|
2119
|
-
console.warn("Failed to seed node positions, falling back to random:", e);
|
|
2166
|
+
seedCircularPositions(nodesCopy, width, height);
|
|
2167
|
+
} catch (error) {
|
|
2168
|
+
console.warn(
|
|
2169
|
+
"AIReady: Position seeding failed, using random fallback:",
|
|
2170
|
+
error
|
|
2171
|
+
);
|
|
2120
2172
|
seedRandomPositions(nodesCopy, width, height);
|
|
2121
2173
|
}
|
|
2122
2174
|
const simulation = d32.forceSimulation(
|
|
2123
2175
|
nodesCopy
|
|
2124
2176
|
);
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
linksCopy
|
|
2128
|
-
);
|
|
2129
|
-
linkForce.id((d) => d.id).distance((d) => {
|
|
2130
|
-
const link = d;
|
|
2131
|
-
return link && link.distance != null ? link.distance : linkDistance;
|
|
2132
|
-
}).strength(linkStrength);
|
|
2133
|
-
simulation.force(
|
|
2134
|
-
"link",
|
|
2135
|
-
linkForce
|
|
2136
|
-
);
|
|
2137
|
-
} catch (e) {
|
|
2138
|
-
console.warn("Failed to configure link force, using fallback:", e);
|
|
2139
|
-
try {
|
|
2140
|
-
simulation.force(
|
|
2141
|
-
"link",
|
|
2142
|
-
d32.forceLink(
|
|
2143
|
-
linksCopy
|
|
2144
|
-
)
|
|
2145
|
-
);
|
|
2146
|
-
} catch (fallbackError) {
|
|
2147
|
-
console.warn("Fallback link force also failed:", fallbackError);
|
|
2148
|
-
}
|
|
2149
|
-
}
|
|
2150
|
-
try {
|
|
2151
|
-
simulation.force(
|
|
2152
|
-
"charge",
|
|
2153
|
-
d32.forceManyBody().strength(chargeStrength)
|
|
2154
|
-
);
|
|
2155
|
-
simulation.force(
|
|
2156
|
-
"center",
|
|
2157
|
-
d32.forceCenter(width / 2, height / 2).strength(centerStrength)
|
|
2158
|
-
);
|
|
2159
|
-
const collide = d32.forceCollide().radius((d) => {
|
|
2160
|
-
const node = d;
|
|
2161
|
-
const nodeSize = node && node.size ? node.size : 10;
|
|
2162
|
-
return nodeSize + collisionRadius;
|
|
2163
|
-
}).strength(collisionStrength);
|
|
2164
|
-
simulation.force("collision", collide);
|
|
2165
|
-
simulation.force(
|
|
2166
|
-
"x",
|
|
2167
|
-
d32.forceX(width / 2).strength(Math.max(0.02, centerStrength * 0.5))
|
|
2168
|
-
);
|
|
2169
|
-
simulation.force(
|
|
2170
|
-
"y",
|
|
2171
|
-
d32.forceY(height / 2).strength(Math.max(0.02, centerStrength * 0.5))
|
|
2172
|
-
);
|
|
2173
|
-
simulation.alphaDecay(alphaDecay);
|
|
2174
|
-
simulation.velocityDecay(velocityDecay);
|
|
2175
|
-
simulation.alphaMin(alphaMin);
|
|
2176
|
-
try {
|
|
2177
|
-
simulation.alphaTarget(alphaTarget);
|
|
2178
|
-
} catch (e) {
|
|
2179
|
-
console.warn("Failed to set alpha target:", e);
|
|
2180
|
-
}
|
|
2181
|
-
try {
|
|
2182
|
-
simulation.alpha(warmAlpha);
|
|
2183
|
-
} catch (e) {
|
|
2184
|
-
console.warn("Failed to set initial alpha:", e);
|
|
2185
|
-
}
|
|
2186
|
-
} catch (e) {
|
|
2187
|
-
console.warn("Failed to configure simulation forces:", e);
|
|
2188
|
-
}
|
|
2177
|
+
applySimulationForces(simulation, linksCopy);
|
|
2178
|
+
configureSimulationParameters(simulation);
|
|
2189
2179
|
simulationRef.current = simulation;
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
console.warn("Failed to clear simulation timeout:", e);
|
|
2195
|
-
}
|
|
2196
|
-
stopTimeoutRef.current = null;
|
|
2197
|
-
}
|
|
2198
|
-
if (maxSimulationTimeMs && maxSimulationTimeMs > 0) {
|
|
2199
|
-
stopTimeoutRef.current = globalThis.setTimeout(() => {
|
|
2200
|
-
try {
|
|
2201
|
-
if (stabilizeOnStop) {
|
|
2202
|
-
stabilizeNodes(nodesCopy);
|
|
2203
|
-
}
|
|
2204
|
-
simulation.alpha(0);
|
|
2205
|
-
simulation.stop();
|
|
2206
|
-
} catch (e) {
|
|
2207
|
-
console.warn("Failed to stop simulation:", e);
|
|
2208
|
-
}
|
|
2209
|
-
setIsRunning(false);
|
|
2210
|
-
setNodes([...nodesCopy]);
|
|
2211
|
-
setLinks([...linksCopy]);
|
|
2212
|
-
}, maxSimulationTimeMs);
|
|
2213
|
-
}
|
|
2214
|
-
let rafId = null;
|
|
2215
|
-
let lastUpdate = 0;
|
|
2216
|
-
const tickHandler = () => {
|
|
2217
|
-
try {
|
|
2218
|
-
if (typeof onTick === "function")
|
|
2219
|
-
onTick(nodesCopy, linksCopy, simulation);
|
|
2220
|
-
} catch (e) {
|
|
2221
|
-
console.warn("Tick callback error:", e);
|
|
2222
|
-
}
|
|
2223
|
-
try {
|
|
2224
|
-
if (simulation.alpha() <= alphaMin) {
|
|
2225
|
-
try {
|
|
2226
|
-
if (stabilizeOnStop) {
|
|
2227
|
-
stabilizeNodes(nodesCopy);
|
|
2228
|
-
}
|
|
2229
|
-
simulation.stop();
|
|
2230
|
-
} catch (e) {
|
|
2231
|
-
console.warn("Failed to stop simulation:", e);
|
|
2232
|
-
}
|
|
2233
|
-
setAlpha(simulation.alpha());
|
|
2234
|
-
setIsRunning(false);
|
|
2235
|
-
setNodes([...nodesCopy]);
|
|
2236
|
-
setLinks([...linksCopy]);
|
|
2237
|
-
return;
|
|
2238
|
-
}
|
|
2239
|
-
} catch (e) {
|
|
2240
|
-
console.warn("Error checking simulation alpha:", e);
|
|
2241
|
-
}
|
|
2242
|
-
const now = Date.now();
|
|
2243
|
-
const shouldUpdate = now - lastUpdate >= tickThrottleMs;
|
|
2244
|
-
if (rafId == null && shouldUpdate) {
|
|
2245
|
-
rafId = (globalThis.requestAnimationFrame || ((cb) => setTimeout(cb, 16)))(() => {
|
|
2246
|
-
rafId = null;
|
|
2247
|
-
lastUpdate = Date.now();
|
|
2248
|
-
setNodes([...nodesCopy]);
|
|
2249
|
-
setLinks([...linksCopy]);
|
|
2250
|
-
setAlpha(simulation.alpha());
|
|
2251
|
-
setIsRunning(simulation.alpha() > simulation.alphaMin());
|
|
2252
|
-
});
|
|
2253
|
-
}
|
|
2254
|
-
};
|
|
2255
|
-
simulation.on("tick", tickHandler);
|
|
2256
|
-
simulation.on("end", () => {
|
|
2257
|
-
setIsRunning(false);
|
|
2258
|
-
});
|
|
2259
|
-
return () => {
|
|
2260
|
-
try {
|
|
2261
|
-
simulation.on("tick", null);
|
|
2262
|
-
} catch (e) {
|
|
2263
|
-
console.warn("Failed to clear simulation tick handler:", e);
|
|
2264
|
-
}
|
|
2265
|
-
if (stopTimeoutRef.current != null) {
|
|
2266
|
-
try {
|
|
2267
|
-
globalThis.clearTimeout(stopTimeoutRef.current);
|
|
2268
|
-
} catch (e) {
|
|
2269
|
-
console.warn("Failed to clear timeout on cleanup:", e);
|
|
2270
|
-
}
|
|
2271
|
-
stopTimeoutRef.current = null;
|
|
2272
|
-
}
|
|
2273
|
-
if (rafId != null) {
|
|
2274
|
-
try {
|
|
2275
|
-
(globalThis.cancelAnimationFrame || ((id) => clearTimeout(id)))(rafId);
|
|
2276
|
-
} catch (e) {
|
|
2277
|
-
console.warn("Failed to cancel animation frame:", e);
|
|
2278
|
-
}
|
|
2279
|
-
rafId = null;
|
|
2280
|
-
}
|
|
2281
|
-
simulation.stop();
|
|
2282
|
-
};
|
|
2180
|
+
const rafState = { rafId: null, lastUpdate: 0 };
|
|
2181
|
+
setupTickHandler(simulation, nodesCopy, linksCopy, rafState);
|
|
2182
|
+
setupStopTimer(simulation, nodesCopy, linksCopy);
|
|
2183
|
+
return () => cleanupSimulation(simulation, rafState);
|
|
2283
2184
|
}, [
|
|
2284
2185
|
nodesKey,
|
|
2285
2186
|
linksKey,
|
|
@@ -2299,98 +2200,173 @@ function useForceSimulation(initialNodes, initialLinks, options) {
|
|
|
2299
2200
|
tickThrottleMs,
|
|
2300
2201
|
maxSimulationTimeMs
|
|
2301
2202
|
]);
|
|
2302
|
-
const
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2203
|
+
const applySimulationForces = (simulation, linksCopy) => {
|
|
2204
|
+
try {
|
|
2205
|
+
const linkForce = d32.forceLink(linksCopy).id((d) => d.id).distance((d) => d.distance ?? linkDistance).strength(linkStrength);
|
|
2206
|
+
simulation.force(FORCE_NAMES.LINK, linkForce).force(FORCE_NAMES.CHARGE, d32.forceManyBody().strength(chargeStrength)).force(
|
|
2207
|
+
FORCE_NAMES.CENTER,
|
|
2208
|
+
d32.forceCenter(width / 2, height / 2).strength(centerStrength)
|
|
2209
|
+
).force(
|
|
2210
|
+
FORCE_NAMES.COLLISION,
|
|
2211
|
+
d32.forceCollide().radius((d) => (d.size ?? 10) + collisionRadius).strength(collisionStrength)
|
|
2212
|
+
).force(
|
|
2213
|
+
FORCE_NAMES.X,
|
|
2214
|
+
d32.forceX(width / 2).strength(Math.max(0.02, centerStrength * 0.5))
|
|
2215
|
+
).force(
|
|
2216
|
+
FORCE_NAMES.Y,
|
|
2217
|
+
d32.forceY(height / 2).strength(Math.max(0.02, centerStrength * 0.5))
|
|
2218
|
+
);
|
|
2219
|
+
} catch (error) {
|
|
2220
|
+
console.warn("AIReady: Failed to configure simulation forces:", error);
|
|
2221
|
+
}
|
|
2222
|
+
};
|
|
2223
|
+
const configureSimulationParameters = (simulation) => {
|
|
2224
|
+
simulation.alphaDecay(alphaDecay).velocityDecay(velocityDecay).alphaMin(alphaMin).alphaTarget(alphaTarget).alpha(warmAlpha);
|
|
2225
|
+
};
|
|
2226
|
+
const setupStopTimer = (simulation, nodesCopy, linksCopy) => {
|
|
2227
|
+
if (stopTimeoutRef.current) {
|
|
2228
|
+
clearTimeout(stopTimeoutRef.current);
|
|
2229
|
+
}
|
|
2230
|
+
if (maxSimulationTimeMs > 0) {
|
|
2231
|
+
stopTimeoutRef.current = setTimeout(() => {
|
|
2232
|
+
safelyStopSimulation(simulation, nodesCopy, {
|
|
2233
|
+
stabilize: stabilizeOnStop
|
|
2234
|
+
});
|
|
2235
|
+
updateStateAfterStop(nodesCopy, linksCopy, 0);
|
|
2236
|
+
}, maxSimulationTimeMs);
|
|
2237
|
+
}
|
|
2238
|
+
};
|
|
2239
|
+
const updateStateAfterStop = (nodesCopy, linksCopy, currentAlpha) => {
|
|
2240
|
+
setIsRunning(false);
|
|
2241
|
+
setAlpha(currentAlpha);
|
|
2242
|
+
setNodes([...nodesCopy]);
|
|
2243
|
+
setLinks([...linksCopy]);
|
|
2244
|
+
};
|
|
2245
|
+
const setupTickHandler = (simulation, nodesCopy, linksCopy, rafState) => {
|
|
2246
|
+
const handleTick = () => {
|
|
2247
|
+
if (onTick) {
|
|
2311
2248
|
try {
|
|
2312
|
-
|
|
2313
|
-
} catch (
|
|
2314
|
-
console.warn("
|
|
2249
|
+
onTick(nodesCopy, linksCopy, simulation);
|
|
2250
|
+
} catch (error) {
|
|
2251
|
+
console.warn("AIReady: Simulation onTick callback failed:", error);
|
|
2315
2252
|
}
|
|
2316
|
-
stopTimeoutRef.current = null;
|
|
2317
2253
|
}
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
}
|
|
2326
|
-
setIsRunning(false);
|
|
2327
|
-
}, maxSimulationTimeMs);
|
|
2254
|
+
const currentAlpha = simulation.alpha();
|
|
2255
|
+
if (currentAlpha <= alphaMin) {
|
|
2256
|
+
safelyStopSimulation(simulation, nodesCopy, {
|
|
2257
|
+
stabilize: stabilizeOnStop
|
|
2258
|
+
});
|
|
2259
|
+
updateStateAfterStop(nodesCopy, linksCopy, currentAlpha);
|
|
2260
|
+
return;
|
|
2328
2261
|
}
|
|
2329
|
-
|
|
2262
|
+
syncStateOnTick(nodesCopy, linksCopy, currentAlpha, rafState);
|
|
2263
|
+
};
|
|
2264
|
+
simulation.on(EVENT_NAMES.TICK, handleTick);
|
|
2265
|
+
simulation.on(EVENT_NAMES.END, () => setIsRunning(false));
|
|
2330
2266
|
};
|
|
2331
|
-
const
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2267
|
+
const syncStateOnTick = (nodesCopy, linksCopy, currentAlpha, rafState) => {
|
|
2268
|
+
const now = Date.now();
|
|
2269
|
+
if (rafState.rafId === null && now - rafState.lastUpdate >= tickThrottleMs) {
|
|
2270
|
+
rafState.rafId = requestAnimationFrame(() => {
|
|
2271
|
+
rafState.rafId = null;
|
|
2272
|
+
rafState.lastUpdate = Date.now();
|
|
2273
|
+
setNodes([...nodesCopy]);
|
|
2274
|
+
setLinks([...linksCopy]);
|
|
2275
|
+
setAlpha(currentAlpha);
|
|
2276
|
+
setIsRunning(currentAlpha > alphaMin);
|
|
2277
|
+
});
|
|
2335
2278
|
}
|
|
2336
2279
|
};
|
|
2337
|
-
const
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
const
|
|
2280
|
+
const cleanupSimulation = (simulation, rafState) => {
|
|
2281
|
+
simulation.on(EVENT_NAMES.TICK, null);
|
|
2282
|
+
if (stopTimeoutRef.current) clearTimeout(stopTimeoutRef.current);
|
|
2283
|
+
if (rafState.rafId !== null) cancelAnimationFrame(rafState.rafId);
|
|
2284
|
+
simulation.stop();
|
|
2285
|
+
};
|
|
2286
|
+
const restartSimulation = useCallback(() => {
|
|
2344
2287
|
const sim = simulationRef.current;
|
|
2345
2288
|
if (!sim) return;
|
|
2346
|
-
if (forcesEnabledRef.current === enabled) return;
|
|
2347
|
-
forcesEnabledRef.current = enabled;
|
|
2348
2289
|
try {
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
);
|
|
2352
|
-
if (
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2290
|
+
sim.alphaTarget(warmAlpha).restart();
|
|
2291
|
+
setIsRunning(true);
|
|
2292
|
+
if (stopTimeoutRef.current) clearTimeout(stopTimeoutRef.current);
|
|
2293
|
+
if (maxSimulationTimeMs > 0) {
|
|
2294
|
+
stopTimeoutRef.current = setTimeout(() => {
|
|
2295
|
+
sim.alpha(0);
|
|
2296
|
+
sim.stop();
|
|
2297
|
+
setIsRunning(false);
|
|
2298
|
+
}, maxSimulationTimeMs);
|
|
2358
2299
|
}
|
|
2359
|
-
} catch (
|
|
2360
|
-
console.warn("Failed to
|
|
2300
|
+
} catch (error) {
|
|
2301
|
+
console.warn("AIReady: Failed to restart simulation:", error);
|
|
2361
2302
|
}
|
|
2362
|
-
};
|
|
2303
|
+
}, [warmAlpha, maxSimulationTimeMs]);
|
|
2304
|
+
const stopSimulation = useCallback(() => {
|
|
2305
|
+
if (simulationRef.current) {
|
|
2306
|
+
simulationRef.current.stop();
|
|
2307
|
+
setIsRunning(false);
|
|
2308
|
+
}
|
|
2309
|
+
}, []);
|
|
2310
|
+
const setForcesEnabled = useCallback(
|
|
2311
|
+
(enabled) => {
|
|
2312
|
+
const sim = simulationRef.current;
|
|
2313
|
+
if (!sim || forcesEnabledRef.current === enabled) return;
|
|
2314
|
+
forcesEnabledRef.current = enabled;
|
|
2315
|
+
try {
|
|
2316
|
+
const charge = sim.force(
|
|
2317
|
+
FORCE_NAMES.CHARGE
|
|
2318
|
+
);
|
|
2319
|
+
if (charge) {
|
|
2320
|
+
charge.strength(enabled ? originalForcesRef.current.charge : 0);
|
|
2321
|
+
}
|
|
2322
|
+
const link = sim.force(FORCE_NAMES.LINK);
|
|
2323
|
+
if (link) {
|
|
2324
|
+
link.strength(enabled ? originalForcesRef.current.link : 0);
|
|
2325
|
+
}
|
|
2326
|
+
sim.alpha(warmAlpha).restart();
|
|
2327
|
+
} catch (error) {
|
|
2328
|
+
console.warn("AIReady: Failed to toggle simulation forces:", error);
|
|
2329
|
+
}
|
|
2330
|
+
},
|
|
2331
|
+
[warmAlpha]
|
|
2332
|
+
);
|
|
2363
2333
|
return {
|
|
2364
2334
|
nodes,
|
|
2365
2335
|
links,
|
|
2366
|
-
restart,
|
|
2367
|
-
stop,
|
|
2336
|
+
restart: restartSimulation,
|
|
2337
|
+
stop: stopSimulation,
|
|
2368
2338
|
isRunning,
|
|
2369
2339
|
alpha,
|
|
2370
2340
|
setForcesEnabled
|
|
2371
2341
|
};
|
|
2372
2342
|
}
|
|
2373
2343
|
function useDrag(simulation) {
|
|
2374
|
-
const
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2344
|
+
const handleDragStart = useCallback(
|
|
2345
|
+
(event, node) => {
|
|
2346
|
+
if (!simulation) return;
|
|
2347
|
+
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
2348
|
+
node.fx = node.x;
|
|
2349
|
+
node.fy = node.y;
|
|
2350
|
+
},
|
|
2351
|
+
[simulation]
|
|
2352
|
+
);
|
|
2353
|
+
const handleDragged = useCallback((event, node) => {
|
|
2381
2354
|
node.fx = event.x;
|
|
2382
2355
|
node.fy = event.y;
|
|
2383
|
-
};
|
|
2384
|
-
const
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2356
|
+
}, []);
|
|
2357
|
+
const handleDragEnd = useCallback(
|
|
2358
|
+
(event, node) => {
|
|
2359
|
+
if (!simulation) return;
|
|
2360
|
+
if (!event.active) simulation.alphaTarget(0);
|
|
2361
|
+
node.fx = null;
|
|
2362
|
+
node.fy = null;
|
|
2363
|
+
},
|
|
2364
|
+
[simulation]
|
|
2365
|
+
);
|
|
2390
2366
|
return {
|
|
2391
|
-
onDragStart:
|
|
2392
|
-
onDrag:
|
|
2393
|
-
onDragEnd:
|
|
2367
|
+
onDragStart: handleDragStart,
|
|
2368
|
+
onDrag: handleDragged,
|
|
2369
|
+
onDragEnd: handleDragEnd
|
|
2394
2370
|
};
|
|
2395
2371
|
}
|
|
2396
2372
|
function pinNode(node) {
|