@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/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 = -300,
2074
- linkDistance = 100,
2075
- linkStrength = 1,
2076
- collisionStrength = 1,
2077
- collisionRadius = 10,
2078
- centerStrength = 0.1,
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 = 0.0228,
2082
- velocityDecay = 0.4,
2083
- alphaTarget = 0,
2084
- warmAlpha = 0.3,
2085
- alphaMin = 0.01,
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
- // Optional throttle in milliseconds for tick updates (reduce React re-renders)
2088
- // Lower values = smoother but more CPU; default ~30ms (~33fps)
2089
- stabilizeOnStop = true,
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 = (initialLinks || []).map((l) => {
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.forEach((n, i) => {
2111
- const angle = i * 2 * Math.PI / nodesCopy.length;
2112
- const radius = Math.min(width, height) * 0.45;
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(
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
- 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
- }
2177
+ applySimulationForces(simulation, linksCopy);
2178
+ configureSimulationParameters(simulation);
2189
2179
  simulationRef.current = simulation;
2190
- if (stopTimeoutRef.current != null) {
2191
- try {
2192
- globalThis.clearTimeout(stopTimeoutRef.current);
2193
- } catch (e) {
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 restart = () => {
2303
- if (simulationRef.current) {
2304
- try {
2305
- simulationRef.current.alphaTarget(warmAlpha).restart();
2306
- } catch {
2307
- simulationRef.current.restart();
2308
- }
2309
- setIsRunning(true);
2310
- if (stopTimeoutRef.current != null) {
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
- globalThis.clearTimeout(stopTimeoutRef.current);
2313
- } catch (e) {
2314
- console.warn("Failed to clear simulation timeout:", e);
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
- if (maxSimulationTimeMs && maxSimulationTimeMs > 0) {
2319
- stopTimeoutRef.current = globalThis.setTimeout(() => {
2320
- try {
2321
- simulationRef.current?.alpha(0);
2322
- simulationRef.current?.stop();
2323
- } catch (e) {
2324
- console.warn("Failed to stop simulation:", e);
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 stop = () => {
2332
- if (simulationRef.current) {
2333
- simulationRef.current.stop();
2334
- setIsRunning(false);
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 originalForcesRef = useRef({
2338
- charge: chargeStrength,
2339
- link: linkStrength,
2340
- collision: collisionStrength
2341
- });
2342
- const forcesEnabledRef = useRef(true);
2343
- const setForcesEnabled = (enabled) => {
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
- const charge = sim.force(
2350
- "charge"
2351
- );
2352
- if (charge && typeof charge.strength === "function") {
2353
- charge.strength(enabled ? originalForcesRef.current.charge : 0);
2354
- }
2355
- const link = sim.force("link");
2356
- if (link && typeof link.strength === "function") {
2357
- link.strength(enabled ? originalForcesRef.current.link : 0);
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 (e) {
2360
- console.warn("Failed to toggle simulation forces:", e);
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 dragStarted = (event, node) => {
2375
- if (!simulation) return;
2376
- if (!event.active) simulation.alphaTarget(0.3).restart();
2377
- node.fx = node.x;
2378
- node.fy = node.y;
2379
- };
2380
- const dragged = (event, node) => {
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 dragEnded = (event, node) => {
2385
- if (!simulation) return;
2386
- if (!event.active) simulation.alphaTarget(0);
2387
- node.fx = null;
2388
- node.fy = null;
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: dragStarted,
2392
- onDrag: dragged,
2393
- onDragEnd: dragEnded
2367
+ onDragStart: handleDragStart,
2368
+ onDrag: handleDragged,
2369
+ onDragEnd: handleDragEnd
2394
2370
  };
2395
2371
  }
2396
2372
  function pinNode(node) {