@aiready/components 0.14.2 → 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
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,19 @@ 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
|
-
n.x = width / 2 + radius * Math.cos(angle);
|
|
2114
|
-
n.y = height / 2 + radius * Math.sin(angle);
|
|
2115
|
-
n.vx = (Math.random() - 0.5) * 2;
|
|
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("AIReady: Position seeding failed, using random fallback:", error);
|
|
2120
2169
|
seedRandomPositions(nodesCopy, width, height);
|
|
2121
2170
|
}
|
|
2122
|
-
const simulation = d32.forceSimulation(
|
|
2123
|
-
|
|
2124
|
-
);
|
|
2125
|
-
try {
|
|
2126
|
-
const linkForce = d32.forceLink(
|
|
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
|
-
}
|
|
2171
|
+
const simulation = d32.forceSimulation(nodesCopy);
|
|
2172
|
+
applySimulationForces(simulation, linksCopy);
|
|
2173
|
+
configureSimulationParameters(simulation);
|
|
2189
2174
|
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
|
-
};
|
|
2175
|
+
const rafState = { rafId: null, lastUpdate: 0 };
|
|
2176
|
+
setupTickHandler(simulation, nodesCopy, linksCopy, rafState);
|
|
2177
|
+
setupStopTimer(simulation, nodesCopy, linksCopy);
|
|
2178
|
+
return () => cleanupSimulation(simulation, rafState);
|
|
2283
2179
|
}, [
|
|
2284
2180
|
nodesKey,
|
|
2285
2181
|
linksKey,
|
|
@@ -2299,98 +2195,155 @@ function useForceSimulation(initialNodes, initialLinks, options) {
|
|
|
2299
2195
|
tickThrottleMs,
|
|
2300
2196
|
maxSimulationTimeMs
|
|
2301
2197
|
]);
|
|
2302
|
-
const
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2198
|
+
const applySimulationForces = (simulation, linksCopy) => {
|
|
2199
|
+
try {
|
|
2200
|
+
const linkForce = d32.forceLink(linksCopy).id((d) => d.id).distance((d) => d.distance ?? linkDistance).strength(linkStrength);
|
|
2201
|
+
simulation.force(FORCE_NAMES.LINK, linkForce).force(FORCE_NAMES.CHARGE, d32.forceManyBody().strength(chargeStrength)).force(FORCE_NAMES.CENTER, d32.forceCenter(width / 2, height / 2).strength(centerStrength)).force(
|
|
2202
|
+
FORCE_NAMES.COLLISION,
|
|
2203
|
+
d32.forceCollide().radius((d) => (d.size ?? 10) + collisionRadius).strength(collisionStrength)
|
|
2204
|
+
).force(FORCE_NAMES.X, d32.forceX(width / 2).strength(Math.max(0.02, centerStrength * 0.5))).force(FORCE_NAMES.Y, d32.forceY(height / 2).strength(Math.max(0.02, centerStrength * 0.5)));
|
|
2205
|
+
} catch (error) {
|
|
2206
|
+
console.warn("AIReady: Failed to configure simulation forces:", error);
|
|
2207
|
+
}
|
|
2208
|
+
};
|
|
2209
|
+
const configureSimulationParameters = (simulation) => {
|
|
2210
|
+
simulation.alphaDecay(alphaDecay).velocityDecay(velocityDecay).alphaMin(alphaMin).alphaTarget(alphaTarget).alpha(warmAlpha);
|
|
2211
|
+
};
|
|
2212
|
+
const setupStopTimer = (simulation, nodesCopy, linksCopy) => {
|
|
2213
|
+
if (stopTimeoutRef.current) {
|
|
2214
|
+
clearTimeout(stopTimeoutRef.current);
|
|
2215
|
+
}
|
|
2216
|
+
if (maxSimulationTimeMs > 0) {
|
|
2217
|
+
stopTimeoutRef.current = setTimeout(() => {
|
|
2218
|
+
safelyStopSimulation(simulation, nodesCopy, { stabilize: stabilizeOnStop });
|
|
2219
|
+
updateStateAfterStop(nodesCopy, linksCopy, 0);
|
|
2220
|
+
}, maxSimulationTimeMs);
|
|
2221
|
+
}
|
|
2222
|
+
};
|
|
2223
|
+
const updateStateAfterStop = (nodesCopy, linksCopy, currentAlpha) => {
|
|
2224
|
+
setIsRunning(false);
|
|
2225
|
+
setAlpha(currentAlpha);
|
|
2226
|
+
setNodes([...nodesCopy]);
|
|
2227
|
+
setLinks([...linksCopy]);
|
|
2228
|
+
};
|
|
2229
|
+
const setupTickHandler = (simulation, nodesCopy, linksCopy, rafState) => {
|
|
2230
|
+
const handleTick = () => {
|
|
2231
|
+
if (onTick) {
|
|
2311
2232
|
try {
|
|
2312
|
-
|
|
2313
|
-
} catch (
|
|
2314
|
-
console.warn("
|
|
2233
|
+
onTick(nodesCopy, linksCopy, simulation);
|
|
2234
|
+
} catch (error) {
|
|
2235
|
+
console.warn("AIReady: Simulation onTick callback failed:", error);
|
|
2315
2236
|
}
|
|
2316
|
-
stopTimeoutRef.current = null;
|
|
2317
2237
|
}
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2238
|
+
const currentAlpha = simulation.alpha();
|
|
2239
|
+
if (currentAlpha <= alphaMin) {
|
|
2240
|
+
safelyStopSimulation(simulation, nodesCopy, { stabilize: stabilizeOnStop });
|
|
2241
|
+
updateStateAfterStop(nodesCopy, linksCopy, currentAlpha);
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
syncStateOnTick(nodesCopy, linksCopy, currentAlpha, rafState);
|
|
2245
|
+
};
|
|
2246
|
+
simulation.on(EVENT_NAMES.TICK, handleTick);
|
|
2247
|
+
simulation.on(EVENT_NAMES.END, () => setIsRunning(false));
|
|
2248
|
+
};
|
|
2249
|
+
const syncStateOnTick = (nodesCopy, linksCopy, currentAlpha, rafState) => {
|
|
2250
|
+
const now = Date.now();
|
|
2251
|
+
if (rafState.rafId === null && now - rafState.lastUpdate >= tickThrottleMs) {
|
|
2252
|
+
rafState.rafId = requestAnimationFrame(() => {
|
|
2253
|
+
rafState.rafId = null;
|
|
2254
|
+
rafState.lastUpdate = Date.now();
|
|
2255
|
+
setNodes([...nodesCopy]);
|
|
2256
|
+
setLinks([...linksCopy]);
|
|
2257
|
+
setAlpha(currentAlpha);
|
|
2258
|
+
setIsRunning(currentAlpha > alphaMin);
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
2261
|
+
};
|
|
2262
|
+
const cleanupSimulation = (simulation, rafState) => {
|
|
2263
|
+
simulation.on(EVENT_NAMES.TICK, null);
|
|
2264
|
+
if (stopTimeoutRef.current) clearTimeout(stopTimeoutRef.current);
|
|
2265
|
+
if (rafState.rafId !== null) cancelAnimationFrame(rafState.rafId);
|
|
2266
|
+
simulation.stop();
|
|
2267
|
+
};
|
|
2268
|
+
const restartSimulation = useCallback(() => {
|
|
2269
|
+
const sim = simulationRef.current;
|
|
2270
|
+
if (!sim) return;
|
|
2271
|
+
try {
|
|
2272
|
+
sim.alphaTarget(warmAlpha).restart();
|
|
2273
|
+
setIsRunning(true);
|
|
2274
|
+
if (stopTimeoutRef.current) clearTimeout(stopTimeoutRef.current);
|
|
2275
|
+
if (maxSimulationTimeMs > 0) {
|
|
2276
|
+
stopTimeoutRef.current = setTimeout(() => {
|
|
2277
|
+
sim.alpha(0);
|
|
2278
|
+
sim.stop();
|
|
2326
2279
|
setIsRunning(false);
|
|
2327
2280
|
}, maxSimulationTimeMs);
|
|
2328
2281
|
}
|
|
2282
|
+
} catch (error) {
|
|
2283
|
+
console.warn("AIReady: Failed to restart simulation:", error);
|
|
2329
2284
|
}
|
|
2330
|
-
};
|
|
2331
|
-
const
|
|
2285
|
+
}, [warmAlpha, maxSimulationTimeMs]);
|
|
2286
|
+
const stopSimulation = useCallback(() => {
|
|
2332
2287
|
if (simulationRef.current) {
|
|
2333
2288
|
simulationRef.current.stop();
|
|
2334
2289
|
setIsRunning(false);
|
|
2335
2290
|
}
|
|
2336
|
-
};
|
|
2337
|
-
const
|
|
2338
|
-
charge: chargeStrength,
|
|
2339
|
-
link: linkStrength,
|
|
2340
|
-
collision: collisionStrength
|
|
2341
|
-
});
|
|
2342
|
-
const forcesEnabledRef = useRef(true);
|
|
2343
|
-
const setForcesEnabled = (enabled) => {
|
|
2291
|
+
}, []);
|
|
2292
|
+
const setForcesEnabled = useCallback((enabled) => {
|
|
2344
2293
|
const sim = simulationRef.current;
|
|
2345
|
-
if (!sim) return;
|
|
2346
|
-
if (forcesEnabledRef.current === enabled) return;
|
|
2294
|
+
if (!sim || forcesEnabledRef.current === enabled) return;
|
|
2347
2295
|
forcesEnabledRef.current = enabled;
|
|
2348
2296
|
try {
|
|
2349
|
-
const charge = sim.force(
|
|
2350
|
-
|
|
2351
|
-
);
|
|
2352
|
-
if (charge && typeof charge.strength === "function") {
|
|
2297
|
+
const charge = sim.force(FORCE_NAMES.CHARGE);
|
|
2298
|
+
if (charge) {
|
|
2353
2299
|
charge.strength(enabled ? originalForcesRef.current.charge : 0);
|
|
2354
2300
|
}
|
|
2355
|
-
const link = sim.force(
|
|
2356
|
-
if (link
|
|
2301
|
+
const link = sim.force(FORCE_NAMES.LINK);
|
|
2302
|
+
if (link) {
|
|
2357
2303
|
link.strength(enabled ? originalForcesRef.current.link : 0);
|
|
2358
2304
|
}
|
|
2359
|
-
|
|
2360
|
-
|
|
2305
|
+
sim.alpha(warmAlpha).restart();
|
|
2306
|
+
} catch (error) {
|
|
2307
|
+
console.warn("AIReady: Failed to toggle simulation forces:", error);
|
|
2361
2308
|
}
|
|
2362
|
-
};
|
|
2309
|
+
}, [warmAlpha]);
|
|
2363
2310
|
return {
|
|
2364
2311
|
nodes,
|
|
2365
2312
|
links,
|
|
2366
|
-
restart,
|
|
2367
|
-
stop,
|
|
2313
|
+
restart: restartSimulation,
|
|
2314
|
+
stop: stopSimulation,
|
|
2368
2315
|
isRunning,
|
|
2369
2316
|
alpha,
|
|
2370
2317
|
setForcesEnabled
|
|
2371
2318
|
};
|
|
2372
2319
|
}
|
|
2373
2320
|
function useDrag(simulation) {
|
|
2374
|
-
const
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2321
|
+
const handleDragStart = useCallback(
|
|
2322
|
+
(event, node) => {
|
|
2323
|
+
if (!simulation) return;
|
|
2324
|
+
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
2325
|
+
node.fx = node.x;
|
|
2326
|
+
node.fy = node.y;
|
|
2327
|
+
},
|
|
2328
|
+
[simulation]
|
|
2329
|
+
);
|
|
2330
|
+
const handleDragged = useCallback((event, node) => {
|
|
2381
2331
|
node.fx = event.x;
|
|
2382
2332
|
node.fy = event.y;
|
|
2383
|
-
};
|
|
2384
|
-
const
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2333
|
+
}, []);
|
|
2334
|
+
const handleDragEnd = useCallback(
|
|
2335
|
+
(event, node) => {
|
|
2336
|
+
if (!simulation) return;
|
|
2337
|
+
if (!event.active) simulation.alphaTarget(0);
|
|
2338
|
+
node.fx = null;
|
|
2339
|
+
node.fy = null;
|
|
2340
|
+
},
|
|
2341
|
+
[simulation]
|
|
2342
|
+
);
|
|
2390
2343
|
return {
|
|
2391
|
-
onDragStart:
|
|
2392
|
-
onDrag:
|
|
2393
|
-
onDragEnd:
|
|
2344
|
+
onDragStart: handleDragStart,
|
|
2345
|
+
onDrag: handleDragged,
|
|
2346
|
+
onDragEnd: handleDragEnd
|
|
2394
2347
|
};
|
|
2395
2348
|
}
|
|
2396
2349
|
function pinNode(node) {
|