@weasel-js/simulation 0.0.0-stage → 1.9.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/LICENSE +21 -0
- package/README.md +48 -2
- package/dist/chunk-WKNSC4F6.js +106 -0
- package/dist/chunk-WKNSC4F6.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/react.d.ts +14 -0
- package/dist/react.js +115 -0
- package/dist/react.js.map +1 -0
- package/dist/types-DMxo2jGG.d.ts +109 -0
- package/package.json +54 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 orochi235
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @weasel-js/simulation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Continuous N-body simulation — force-directed layout, physics-ish motion.
|
|
4
|
+
|
|
5
|
+
Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
|
|
6
|
+
scene-graph canvas kit for React. See the
|
|
7
|
+
[API reference](https://orochi235.github.io/weasel/api/).
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @weasel-js/simulation
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`createSimulation` is the root entry and needs no React or DOM. `useSimulation`
|
|
14
|
+
is in `@weasel-js/simulation/react`.
|
|
15
|
+
|
|
16
|
+
`createSimulation` is the velocity-Verlet integrator and nothing else: `tick()`
|
|
17
|
+
is the only thing that moves a node, so a caller wanting the whole relaxation at
|
|
18
|
+
once — a layout pass, a pre-warm — runs it to settle and reads the answer.
|
|
19
|
+
`useSimulation` is that integrator on a frame loop.
|
|
20
|
+
|
|
21
|
+
Forces are pluggable functions matching **d3-force's protocol**, so d3-force's
|
|
22
|
+
bundled forces (`forceManyBody`, `forceLink`, `forceCenter`, …) work here
|
|
23
|
+
without translation. That compatibility is deliberate — don't invent a parallel
|
|
24
|
+
force signature.
|
|
25
|
+
|
|
26
|
+
## The kit doesn't write to your scene
|
|
27
|
+
|
|
28
|
+
`onTick(nodes)` fires after each integration step and **the consumer decides**
|
|
29
|
+
how, and whether, to write through to scene ops. The simulation holds its own
|
|
30
|
+
node array; it never touches the adapter.
|
|
31
|
+
|
|
32
|
+
That matters for undo: a simulation running at 60fps would otherwise produce 60
|
|
33
|
+
history entries per second. Typical wiring is to let the sim run free and commit
|
|
34
|
+
a single op when it settles (or on user action), not per tick.
|
|
35
|
+
|
|
36
|
+
## Alpha and settling
|
|
37
|
+
|
|
38
|
+
The usual d3 convention: `alpha` decays each tick by `DEFAULT_ALPHA_DECAY`
|
|
39
|
+
until it drops below `DEFAULT_ALPHA_MIN`, at which point the layout is
|
|
40
|
+
considered settled and the loop can stop. `DEFAULT_VELOCITY_DECAY` is the
|
|
41
|
+
per-tick friction.
|
|
42
|
+
|
|
43
|
+
Reheating (bumping alpha back up) is how you restart motion after a user drags
|
|
44
|
+
a node — otherwise a settled simulation stays settled.
|
|
45
|
+
|
|
46
|
+
## Related
|
|
47
|
+
|
|
48
|
+
`@weasel-js/d3` is the packaged integration if you want the d3 ecosystem wired
|
|
49
|
+
up for you rather than assembling forces by hand.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var DEFAULT_ALPHA_DECAY = 1 - Math.pow(1e-3, 1 / 300);
|
|
3
|
+
var DEFAULT_ALPHA_MIN = 1e-3;
|
|
4
|
+
var DEFAULT_VELOCITY_DECAY = 0.4;
|
|
5
|
+
|
|
6
|
+
// src/createSimulation.ts
|
|
7
|
+
function createSimulation(opts) {
|
|
8
|
+
let nodes = opts.nodes;
|
|
9
|
+
let forces = opts.forces ?? [];
|
|
10
|
+
let alpha = opts.alpha ?? 1;
|
|
11
|
+
let alphaMin = opts.alphaMin ?? DEFAULT_ALPHA_MIN;
|
|
12
|
+
let alphaDecay = opts.alphaDecay ?? DEFAULT_ALPHA_DECAY;
|
|
13
|
+
let alphaTarget = opts.alphaTarget ?? 0;
|
|
14
|
+
let velocityDecay = opts.velocityDecay ?? DEFAULT_VELOCITY_DECAY;
|
|
15
|
+
const random = opts.random ?? Math.random;
|
|
16
|
+
function prime(list) {
|
|
17
|
+
list.forEach((n, i) => {
|
|
18
|
+
n.index = i;
|
|
19
|
+
if (n.vx == null) n.vx = 0;
|
|
20
|
+
if (n.vy == null) n.vy = 0;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
prime(nodes);
|
|
24
|
+
for (const f of forces) f.initialize?.(nodes, random);
|
|
25
|
+
function step() {
|
|
26
|
+
alpha += (alphaTarget - alpha) * alphaDecay;
|
|
27
|
+
for (const f of forces) f(alpha);
|
|
28
|
+
const friction = 1 - velocityDecay;
|
|
29
|
+
for (const n of nodes) {
|
|
30
|
+
if (n.fx == null) {
|
|
31
|
+
n.vx = (n.vx ?? 0) * friction;
|
|
32
|
+
if (!Number.isFinite(n.vx)) n.vx = 0;
|
|
33
|
+
n.x += n.vx;
|
|
34
|
+
if (!Number.isFinite(n.x)) n.x = 0;
|
|
35
|
+
} else {
|
|
36
|
+
n.x = n.fx;
|
|
37
|
+
n.vx = 0;
|
|
38
|
+
}
|
|
39
|
+
if (n.fy == null) {
|
|
40
|
+
n.vy = (n.vy ?? 0) * friction;
|
|
41
|
+
if (!Number.isFinite(n.vy)) n.vy = 0;
|
|
42
|
+
n.y += n.vy;
|
|
43
|
+
if (!Number.isFinite(n.y)) n.y = 0;
|
|
44
|
+
} else {
|
|
45
|
+
n.y = n.fy;
|
|
46
|
+
n.vy = 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const core = {
|
|
51
|
+
get nodes() {
|
|
52
|
+
return nodes;
|
|
53
|
+
},
|
|
54
|
+
setNodes(next) {
|
|
55
|
+
nodes = next;
|
|
56
|
+
prime(nodes);
|
|
57
|
+
for (const f of forces) f.initialize?.(nodes, random);
|
|
58
|
+
return core;
|
|
59
|
+
},
|
|
60
|
+
setForces(next) {
|
|
61
|
+
const old = forces;
|
|
62
|
+
forces = next;
|
|
63
|
+
for (const f of next) {
|
|
64
|
+
if (!old.includes(f)) f.initialize?.(nodes, random);
|
|
65
|
+
}
|
|
66
|
+
return core;
|
|
67
|
+
},
|
|
68
|
+
alpha(value) {
|
|
69
|
+
if (value === void 0) return alpha;
|
|
70
|
+
alpha = value;
|
|
71
|
+
return core;
|
|
72
|
+
},
|
|
73
|
+
alphaTarget(value) {
|
|
74
|
+
if (value === void 0) return alphaTarget;
|
|
75
|
+
alphaTarget = value;
|
|
76
|
+
return core;
|
|
77
|
+
},
|
|
78
|
+
alphaDecay(value) {
|
|
79
|
+
if (value === void 0) return alphaDecay;
|
|
80
|
+
alphaDecay = value;
|
|
81
|
+
return core;
|
|
82
|
+
},
|
|
83
|
+
alphaMin(value) {
|
|
84
|
+
if (value === void 0) return alphaMin;
|
|
85
|
+
alphaMin = value;
|
|
86
|
+
return core;
|
|
87
|
+
},
|
|
88
|
+
velocityDecay(value) {
|
|
89
|
+
if (value === void 0) return velocityDecay;
|
|
90
|
+
velocityDecay = value;
|
|
91
|
+
return core;
|
|
92
|
+
},
|
|
93
|
+
tick(iterations = 1) {
|
|
94
|
+
for (let i = 0; i < iterations; i++) step();
|
|
95
|
+
return core;
|
|
96
|
+
},
|
|
97
|
+
isSettled() {
|
|
98
|
+
return alpha < alphaMin && alphaTarget === 0;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
return core;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { DEFAULT_ALPHA_DECAY, DEFAULT_ALPHA_MIN, DEFAULT_VELOCITY_DECAY, createSimulation };
|
|
105
|
+
//# sourceMappingURL=chunk-WKNSC4F6.js.map
|
|
106
|
+
//# sourceMappingURL=chunk-WKNSC4F6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/createSimulation.ts"],"names":[],"mappings":"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","file":"chunk-WKNSC4F6.js","sourcesContent":["/**\n * Minimal node interface for the simulation primitive. Matches d3-force's\n * node shape exactly so d3-force's bundled forces work without translation.\n *\n * Consumers extend this with arbitrary additional fields (id, label, etc.).\n */\nexport interface SimulationNode {\n x: number;\n y: number;\n vx?: number;\n vy?: number;\n /** Pin x to this value when non-null. The integrator skips x/vx for the node. */\n fx?: number | null;\n fy?: number | null;\n /** Array index, written by `initialize`. Some forces (e.g. forceLink) rely on it. */\n index?: number;\n}\n\n/**\n * Force protocol — contract-compatible with d3-force. A force is a function\n * called once per tick with the current alpha; it mutates `vx`/`vy` on nodes.\n * Optional `initialize` lets the force compute setup (link indices, etc.).\n */\nexport interface SimulationForce<TNode extends SimulationNode = SimulationNode> {\n (alpha: number): void;\n initialize?(nodes: TNode[], random?: () => number): void;\n}\n\n/** Options for `useSimulation`: everything {@link SimulationOptions} carries,\n * plus the loop's own callbacks and clock. */\nexport interface UseSimulationOptions<TNode extends SimulationNode>\n extends SimulationOptions<TNode> {\n /** Fired after each integration step. */\n onTick?: (nodes: TNode[]) => void;\n /** Fired once when the sim settles (alpha < alphaMin && alphaTarget === 0). */\n onEnd?: () => void;\n\n // Test injection (mirrors useAnimator):\n /** Optional rAF injection. Defaults to window.requestAnimationFrame. */\n requestFrame?: (cb: (t: number) => void) => number;\n cancelFrame?: (handle: number) => void;\n}\n\n/**\n * A force simulation over a set of nodes, with no clock of its own.\n *\n * Each tick applies every force and then integrates velocities, scaled by\n * `alpha` — a temperature that decays toward `alphaTarget` so the layout\n * settles instead of jittering forever. Raise `alphaTarget` to reheat it,\n * which is what a drag does.\n *\n * Nodes are mutated in place; the simulation holds the array, not a copy.\n *\n * Nothing here advances time. {@link Simulation} adds the frame loop; a caller\n * that wants the whole relaxation at once — a layout pass, a pre-warm, a test\n * — drives {@link SimulationCore.tick} itself.\n */\nexport interface SimulationCore<TNode extends SimulationNode> {\n readonly nodes: TNode[];\n\n setNodes(nodes: TNode[]): this;\n setForces(forces: SimulationForce<TNode>[]): this;\n\n alpha(): number;\n alpha(value: number): this;\n alphaTarget(): number;\n alphaTarget(value: number): this;\n alphaDecay(): number;\n alphaDecay(value: number): this;\n alphaMin(): number;\n alphaMin(value: number): this;\n velocityDecay(): number;\n velocityDecay(value: number): this;\n\n /**\n * Run the tick body synchronously `iterations` times. Does NOT fire\n * onTick or onEnd (matches d3-force). Used for offscreen pre-warm and tests.\n */\n tick(iterations?: number): this;\n\n isSettled(): boolean;\n}\n\n/** A {@link SimulationCore} running on a frame loop. */\nexport interface Simulation<TNode extends SimulationNode> extends SimulationCore<TNode> {\n /** Resume the RAF loop. If alpha < alphaMin, reset alpha to 1. */\n restart(): this;\n /** Cancel the RAF loop. Does not change alpha. */\n stop(): this;\n}\n\n/** What {@link SimulationCore} needs: the nodes, the forces acting on them,\n * and the cooling schedule. {@link UseSimulationOptions} adds the loop's. */\nexport interface SimulationOptions<TNode extends SimulationNode> {\n /** Mutable array of nodes. Kit + forces mutate vx/vy/x/y in place. */\n nodes: TNode[];\n /** Forces applied per tick. Each may implement `initialize` for setup. */\n forces?: SimulationForce<TNode>[];\n\n /** Initial alpha. Default 1. */\n alpha?: number;\n /** Settle threshold. When alpha drops below and alphaTarget is 0, onEnd fires. Default 0.001. */\n alphaMin?: number;\n /** Per-tick decay rate toward alphaTarget. Default 1 - 0.001^(1/300) ≈ 0.0228. */\n alphaDecay?: number;\n /** Target alpha. Set above alphaMin to keep sim warm (e.g. during drag). Default 0. */\n alphaTarget?: number;\n /** Friction multiplier on velocity per tick: vx *= (1 - velocityDecay). Default 0.4. */\n velocityDecay?: number;\n /** Handed to each force's `initialize`. Default `Math.random`; pass a seeded\n * source to make a run reproducible. */\n random?: () => number;\n}\n\n/** Default alpha decay: such that alpha drops from 1 to alphaMin (0.001) in 300 ticks. */\nexport const DEFAULT_ALPHA_DECAY = 1 - Math.pow(0.001, 1 / 300);\n/** Alpha below which a cooling simulation is considered settled. */\nexport const DEFAULT_ALPHA_MIN = 0.001;\n/** Fraction of velocity shed each tick — friction. Higher settles sooner and\n * overshoots less. */\nexport const DEFAULT_VELOCITY_DECAY = 0.4;\n","/**\n * The integrator, with no clock attached.\n *\n * Velocity-Verlet over a pluggable force list, contract-compatible with\n * d3-force so d3's own bundled forces drop in untranslated. Nothing here\n * advances time: `tick()` is the only thing that moves a node, and the caller\n * decides how often to call it. `useSimulation` calls it once a frame; a\n * layout pass calls it a few hundred times in one go and reads the result.\n *\n * Splitting it out is what lets a *pure* function run a relaxation — a React\n * hook cannot, and the alternative was a second integrator drifting away from\n * this one.\n */\nimport {\n DEFAULT_ALPHA_DECAY,\n DEFAULT_ALPHA_MIN,\n DEFAULT_VELOCITY_DECAY,\n type SimulationCore,\n type SimulationForce,\n type SimulationNode,\n type SimulationOptions,\n} from './types';\n\n/**\n * A d3-force-compatible simulation with no clock: nothing moves until `tick()`.\n * Nodes are mutated in place, and primed with `index`, `vx` and `vy` on creation.\n */\nexport function createSimulation<TNode extends SimulationNode>(\n opts: SimulationOptions<TNode>,\n): SimulationCore<TNode> {\n let nodes = opts.nodes;\n let forces = opts.forces ?? [];\n let alpha = opts.alpha ?? 1;\n let alphaMin = opts.alphaMin ?? DEFAULT_ALPHA_MIN;\n let alphaDecay = opts.alphaDecay ?? DEFAULT_ALPHA_DECAY;\n let alphaTarget = opts.alphaTarget ?? 0;\n let velocityDecay = opts.velocityDecay ?? DEFAULT_VELOCITY_DECAY;\n const random = opts.random ?? Math.random;\n\n /** `index` is what `forceLink` and friends address nodes by, and a d3 force\n * does `n.vx += …` without checking, so both must exist before the first\n * tick or the first tick produces NaN. */\n function prime(list: TNode[]): void {\n list.forEach((n, i) => {\n n.index = i;\n if (n.vx == null) n.vx = 0;\n if (n.vy == null) n.vy = 0;\n });\n }\n\n prime(nodes);\n for (const f of forces) f.initialize?.(nodes, random);\n\n function step(): void {\n alpha += (alphaTarget - alpha) * alphaDecay;\n for (const f of forces) f(alpha);\n\n const friction = 1 - velocityDecay;\n for (const n of nodes) {\n if (n.fx == null) {\n n.vx = (n.vx ?? 0) * friction;\n // A non-finite velocity — force overflow, or two nodes exactly\n // coincident — would reach the renderer as geometry and cost\n // pathological tessellation work without throwing anywhere.\n if (!Number.isFinite(n.vx)) n.vx = 0;\n n.x += n.vx;\n if (!Number.isFinite(n.x)) n.x = 0;\n } else {\n n.x = n.fx;\n n.vx = 0;\n }\n if (n.fy == null) {\n n.vy = (n.vy ?? 0) * friction;\n if (!Number.isFinite(n.vy)) n.vy = 0;\n n.y += n.vy;\n if (!Number.isFinite(n.y)) n.y = 0;\n } else {\n n.y = n.fy;\n n.vy = 0;\n }\n }\n }\n\n /* eslint-disable @typescript-eslint/no-explicit-any */\n const core: any = {\n get nodes() {\n return nodes;\n },\n setNodes(next: TNode[]) {\n nodes = next;\n prime(nodes);\n for (const f of forces) f.initialize?.(nodes, random);\n return core;\n },\n setForces(next: SimulationForce<TNode>[]) {\n const old = forces;\n forces = next;\n for (const f of next) {\n if (!old.includes(f)) f.initialize?.(nodes, random);\n }\n return core;\n },\n alpha(value?: number) {\n if (value === undefined) return alpha;\n alpha = value;\n return core;\n },\n alphaTarget(value?: number) {\n if (value === undefined) return alphaTarget;\n alphaTarget = value;\n return core;\n },\n alphaDecay(value?: number) {\n if (value === undefined) return alphaDecay;\n alphaDecay = value;\n return core;\n },\n alphaMin(value?: number) {\n if (value === undefined) return alphaMin;\n alphaMin = value;\n return core;\n },\n velocityDecay(value?: number) {\n if (value === undefined) return velocityDecay;\n velocityDecay = value;\n return core;\n },\n tick(iterations: number = 1) {\n for (let i = 0; i < iterations; i++) step();\n return core;\n },\n isSettled() {\n return alpha < alphaMin && alphaTarget === 0;\n },\n };\n /* eslint-enable @typescript-eslint/no-explicit-any */\n return core as SimulationCore<TNode>;\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { S as SimulationNode, a as SimulationOptions, b as SimulationCore } from './types-DMxo2jGG.js';
|
|
2
|
+
export { D as DEFAULT_ALPHA_DECAY, c as DEFAULT_ALPHA_MIN, d as DEFAULT_VELOCITY_DECAY, e as Simulation, f as SimulationForce, U as UseSimulationOptions } from './types-DMxo2jGG.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The integrator, with no clock attached.
|
|
6
|
+
*
|
|
7
|
+
* Velocity-Verlet over a pluggable force list, contract-compatible with
|
|
8
|
+
* d3-force so d3's own bundled forces drop in untranslated. Nothing here
|
|
9
|
+
* advances time: `tick()` is the only thing that moves a node, and the caller
|
|
10
|
+
* decides how often to call it. `useSimulation` calls it once a frame; a
|
|
11
|
+
* layout pass calls it a few hundred times in one go and reads the result.
|
|
12
|
+
*
|
|
13
|
+
* Splitting it out is what lets a *pure* function run a relaxation — a React
|
|
14
|
+
* hook cannot, and the alternative was a second integrator drifting away from
|
|
15
|
+
* this one.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A d3-force-compatible simulation with no clock: nothing moves until `tick()`.
|
|
20
|
+
* Nodes are mutated in place, and primed with `index`, `vx` and `vy` on creation.
|
|
21
|
+
*/
|
|
22
|
+
declare function createSimulation<TNode extends SimulationNode>(opts: SimulationOptions<TNode>): SimulationCore<TNode>;
|
|
23
|
+
|
|
24
|
+
export { SimulationCore, SimulationNode, SimulationOptions, createSimulation };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { S as SimulationNode, U as UseSimulationOptions, e as Simulation } from './types-DMxo2jGG.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Continuous N-body simulation primitive: `createSimulation`'s integrator on a
|
|
5
|
+
* frame loop.
|
|
6
|
+
*
|
|
7
|
+
* The kit is adapter-agnostic — `onTick(nodes)` fires after each integration
|
|
8
|
+
* step and the consumer decides how (and whether) to write through to scene
|
|
9
|
+
* state. Per-tick history bypass and settle-commit semantics are consumer
|
|
10
|
+
* concerns; the kit doesn't have an opinion.
|
|
11
|
+
*/
|
|
12
|
+
declare function useSimulation<TNode extends SimulationNode>(opts: UseSimulationOptions<TNode>): Simulation<TNode>;
|
|
13
|
+
|
|
14
|
+
export { UseSimulationOptions, useSimulation };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createSimulation } from './chunk-WKNSC4F6.js';
|
|
2
|
+
import { useRef, useLayoutEffect, useMemo } from 'react';
|
|
3
|
+
import { useLatest, useVisibleRaf } from '@weasel-js/react';
|
|
4
|
+
|
|
5
|
+
function useSimulation(opts) {
|
|
6
|
+
const optsRef = useLatest(opts);
|
|
7
|
+
const coreRef = useRef(null);
|
|
8
|
+
coreRef.current ??= createSimulation(opts);
|
|
9
|
+
const core = coreRef.current;
|
|
10
|
+
const endedRef = useRef(false);
|
|
11
|
+
const mountedRef = useRef(true);
|
|
12
|
+
const frameLoop = useVisibleRaf(
|
|
13
|
+
() => {
|
|
14
|
+
rafTick();
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
requestFrame: opts.requestFrame,
|
|
18
|
+
cancelFrame: opts.cancelFrame
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
const stopRaf = () => {
|
|
22
|
+
frameLoop.cancel();
|
|
23
|
+
};
|
|
24
|
+
const rafTick = () => {
|
|
25
|
+
if (!mountedRef.current) return;
|
|
26
|
+
core.tick();
|
|
27
|
+
optsRef.current.onTick?.(core.nodes);
|
|
28
|
+
if (core.alpha() < core.alphaMin()) {
|
|
29
|
+
stopRaf();
|
|
30
|
+
if (!endedRef.current) {
|
|
31
|
+
endedRef.current = true;
|
|
32
|
+
optsRef.current.onEnd?.();
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
frameLoop.request();
|
|
37
|
+
};
|
|
38
|
+
const startRaf = () => {
|
|
39
|
+
if (!mountedRef.current) return;
|
|
40
|
+
frameLoop.request();
|
|
41
|
+
};
|
|
42
|
+
useLayoutEffect(() => {
|
|
43
|
+
mountedRef.current = true;
|
|
44
|
+
frameLoop.request();
|
|
45
|
+
return () => {
|
|
46
|
+
mountedRef.current = false;
|
|
47
|
+
frameLoop.cancel();
|
|
48
|
+
};
|
|
49
|
+
}, [frameLoop]);
|
|
50
|
+
const handle = useMemo(() => {
|
|
51
|
+
const h = {
|
|
52
|
+
get nodes() {
|
|
53
|
+
return core.nodes;
|
|
54
|
+
},
|
|
55
|
+
setNodes(newNodes) {
|
|
56
|
+
core.setNodes(newNodes);
|
|
57
|
+
return h;
|
|
58
|
+
},
|
|
59
|
+
setForces(newForces) {
|
|
60
|
+
core.setForces(newForces);
|
|
61
|
+
return h;
|
|
62
|
+
},
|
|
63
|
+
alpha(value) {
|
|
64
|
+
if (value === void 0) return core.alpha();
|
|
65
|
+
core.alpha(value);
|
|
66
|
+
if (value >= core.alphaMin()) endedRef.current = false;
|
|
67
|
+
return h;
|
|
68
|
+
},
|
|
69
|
+
alphaTarget(value) {
|
|
70
|
+
if (value === void 0) return core.alphaTarget();
|
|
71
|
+
core.alphaTarget(value);
|
|
72
|
+
if (value > 0) endedRef.current = false;
|
|
73
|
+
return h;
|
|
74
|
+
},
|
|
75
|
+
alphaDecay(value) {
|
|
76
|
+
if (value === void 0) return core.alphaDecay();
|
|
77
|
+
core.alphaDecay(value);
|
|
78
|
+
return h;
|
|
79
|
+
},
|
|
80
|
+
alphaMin(value) {
|
|
81
|
+
if (value === void 0) return core.alphaMin();
|
|
82
|
+
core.alphaMin(value);
|
|
83
|
+
return h;
|
|
84
|
+
},
|
|
85
|
+
velocityDecay(value) {
|
|
86
|
+
if (value === void 0) return core.velocityDecay();
|
|
87
|
+
core.velocityDecay(value);
|
|
88
|
+
return h;
|
|
89
|
+
},
|
|
90
|
+
restart() {
|
|
91
|
+
if (core.alpha() < core.alphaMin()) core.alpha(1);
|
|
92
|
+
endedRef.current = false;
|
|
93
|
+
startRaf();
|
|
94
|
+
return h;
|
|
95
|
+
},
|
|
96
|
+
stop() {
|
|
97
|
+
stopRaf();
|
|
98
|
+
return h;
|
|
99
|
+
},
|
|
100
|
+
tick(iterations = 1) {
|
|
101
|
+
core.tick(iterations);
|
|
102
|
+
return h;
|
|
103
|
+
},
|
|
104
|
+
isSettled() {
|
|
105
|
+
return core.isSettled();
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
return h;
|
|
109
|
+
}, []);
|
|
110
|
+
return handle;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { useSimulation };
|
|
114
|
+
//# sourceMappingURL=react.js.map
|
|
115
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useSimulation.ts"],"names":[],"mappings":";;;;AAoBO,SAAS,cACd,IAAA,EACmB;AACnB,EAAA,MAAM,OAAA,GAAU,UAAU,IAAI,CAAA;AAE9B,EAAA,MAAM,OAAA,GAAU,OAAqC,IAAI,CAAA;AACzD,EAAA,OAAA,CAAQ,OAAA,KAAY,iBAAiB,IAAI,CAAA;AACzC,EAAA,MAAM,OAAO,OAAA,CAAQ,OAAA;AAGrB,EAAA,MAAM,QAAA,GAAW,OAAO,KAAK,CAAA;AAC7B,EAAA,MAAM,UAAA,GAAa,OAAO,IAAI,CAAA;AAO9B,EAAA,MAAM,SAAA,GAAY,aAAA;AAAA,IAChB,MAAM;AAAE,MAAA,OAAA,EAAQ;AAAA,IAAG,CAAA;AAAA,IACnB;AAAA,MACE,cAAc,IAAA,CAAK,YAAA;AAAA,MACnB,aAAa,IAAA,CAAK;AAAA;AACpB,GACF;AAEA,EAAA,MAAM,UAAU,MAAY;AAAE,IAAA,SAAA,CAAU,MAAA,EAAO;AAAA,EAAG,CAAA;AAElD,EAAA,MAAM,UAAU,MAAY;AAC1B,IAAA,IAAI,CAAC,WAAW,OAAA,EAAS;AAEzB,IAAA,IAAA,CAAK,IAAA,EAAK;AACV,IAAA,OAAA,CAAQ,OAAA,CAAQ,MAAA,GAAS,IAAA,CAAK,KAAK,CAAA;AAEnC,IAAA,IAAI,IAAA,CAAK,KAAA,EAAM,GAAI,IAAA,CAAK,UAAS,EAAG;AAClC,MAAA,OAAA,EAAQ;AACR,MAAA,IAAI,CAAC,SAAS,OAAA,EAAS;AACrB,QAAA,QAAA,CAAS,OAAA,GAAU,IAAA;AACnB,QAAA,OAAA,CAAQ,QAAQ,KAAA,IAAQ;AAAA,MAC1B;AACA,MAAA;AAAA,IACF;AAEA,IAAA,SAAA,CAAU,OAAA,EAAQ;AAAA,EACpB,CAAA;AAEA,EAAA,MAAM,WAAW,MAAY;AAC3B,IAAA,IAAI,CAAC,WAAW,OAAA,EAAS;AACzB,IAAA,SAAA,CAAU,OAAA,EAAQ;AAAA,EACpB,CAAA;AAKA,EAAA,eAAA,CAAgB,MAAM;AACpB,IAAA,UAAA,CAAW,OAAA,GAAU,IAAA;AACrB,IAAA,SAAA,CAAU,OAAA,EAAQ;AAClB,IAAA,OAAO,MAAM;AACX,MAAA,UAAA,CAAW,OAAA,GAAU,KAAA;AACrB,MAAA,SAAA,CAAU,MAAA,EAAO;AAAA,IACnB,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,SAAS,CAAC,CAAA;AAGd,EAAA,MAAM,MAAA,GAAS,QAA2B,MAAM;AAE9C,IAAA,MAAM,CAAA,GAAS;AAAA,MACb,IAAI,KAAA,GAAQ;AACV,QAAA,OAAO,IAAA,CAAK,KAAA;AAAA,MACd,CAAA;AAAA,MACA,SAAS,QAAA,EAAmB;AAC1B,QAAA,IAAA,CAAK,SAAS,QAAQ,CAAA;AACtB,QAAA,OAAO,CAAA;AAAA,MACT,CAAA;AAAA,MACA,UAAU,SAAA,EAAqC;AAC7C,QAAA,IAAA,CAAK,UAAU,SAAS,CAAA;AACxB,QAAA,OAAO,CAAA;AAAA,MACT,CAAA;AAAA,MACA,MAAM,KAAA,EAAgB;AACpB,QAAA,IAAI,KAAA,KAAU,MAAA,EAAW,OAAO,IAAA,CAAK,KAAA,EAAM;AAC3C,QAAA,IAAA,CAAK,MAAM,KAAK,CAAA;AAChB,QAAA,IAAI,KAAA,IAAS,IAAA,CAAK,QAAA,EAAS,WAAY,OAAA,GAAU,KAAA;AACjD,QAAA,OAAO,CAAA;AAAA,MACT,CAAA;AAAA,MACA,YAAY,KAAA,EAAgB;AAC1B,QAAA,IAAI,KAAA,KAAU,MAAA,EAAW,OAAO,IAAA,CAAK,WAAA,EAAY;AACjD,QAAA,IAAA,CAAK,YAAY,KAAK,CAAA;AACtB,QAAA,IAAI,KAAA,GAAQ,CAAA,EAAG,QAAA,CAAS,OAAA,GAAU,KAAA;AAClC,QAAA,OAAO,CAAA;AAAA,MACT,CAAA;AAAA,MACA,WAAW,KAAA,EAAgB;AACzB,QAAA,IAAI,KAAA,KAAU,MAAA,EAAW,OAAO,IAAA,CAAK,UAAA,EAAW;AAChD,QAAA,IAAA,CAAK,WAAW,KAAK,CAAA;AACrB,QAAA,OAAO,CAAA;AAAA,MACT,CAAA;AAAA,MACA,SAAS,KAAA,EAAgB;AACvB,QAAA,IAAI,KAAA,KAAU,MAAA,EAAW,OAAO,IAAA,CAAK,QAAA,EAAS;AAC9C,QAAA,IAAA,CAAK,SAAS,KAAK,CAAA;AACnB,QAAA,OAAO,CAAA;AAAA,MACT,CAAA;AAAA,MACA,cAAc,KAAA,EAAgB;AAC5B,QAAA,IAAI,KAAA,KAAU,MAAA,EAAW,OAAO,IAAA,CAAK,aAAA,EAAc;AACnD,QAAA,IAAA,CAAK,cAAc,KAAK,CAAA;AACxB,QAAA,OAAO,CAAA;AAAA,MACT,CAAA;AAAA,MACA,OAAA,GAAU;AACR,QAAA,IAAI,IAAA,CAAK,OAAM,GAAI,IAAA,CAAK,UAAS,EAAG,IAAA,CAAK,MAAM,CAAC,CAAA;AAChD,QAAA,QAAA,CAAS,OAAA,GAAU,KAAA;AACnB,QAAA,QAAA,EAAS;AACT,QAAA,OAAO,CAAA;AAAA,MACT,CAAA;AAAA,MACA,IAAA,GAAO;AACL,QAAA,OAAA,EAAQ;AACR,QAAA,OAAO,CAAA;AAAA,MACT,CAAA;AAAA,MACA,IAAA,CAAK,aAAqB,CAAA,EAAG;AAC3B,QAAA,IAAA,CAAK,KAAK,UAAU,CAAA;AACpB,QAAA,OAAO,CAAA;AAAA,MACT,CAAA;AAAA,MACA,SAAA,GAAY;AACV,QAAA,OAAO,KAAK,SAAA,EAAU;AAAA,MACxB;AAAA,KACF;AACA,IAAA,OAAO,CAAA;AAAA,EAGT,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,OAAO,MAAA;AACT","file":"react.js","sourcesContent":["import { useLayoutEffect, useMemo, useRef } from 'react';\nimport { useLatest, useVisibleRaf } from '@weasel-js/react';\nimport { createSimulation } from './createSimulation';\nimport {\n type Simulation,\n type SimulationCore,\n type SimulationForce,\n type SimulationNode,\n type UseSimulationOptions,\n} from './types';\n\n/**\n * Continuous N-body simulation primitive: `createSimulation`'s integrator on a\n * frame loop.\n *\n * The kit is adapter-agnostic — `onTick(nodes)` fires after each integration\n * step and the consumer decides how (and whether) to write through to scene\n * state. Per-tick history bypass and settle-commit semantics are consumer\n * concerns; the kit doesn't have an opinion.\n */\nexport function useSimulation<TNode extends SimulationNode>(\n opts: UseSimulationOptions<TNode>,\n): Simulation<TNode> {\n const optsRef = useLatest(opts);\n\n const coreRef = useRef<SimulationCore<TNode> | null>(null);\n coreRef.current ??= createSimulation(opts);\n const core = coreRef.current;\n\n /** Whether onEnd has fired since the last energization (restart / alpha set / target raise). */\n const endedRef = useRef(false);\n const mountedRef = useRef(true);\n\n // The loop runs behind the visibility gate: a settling simulation stops\n // integrating on a page nobody is looking at, and picks up where it left off.\n // Its step is fixed rather than time-based, so there is no clock to rebase.\n // The gate defaults an absent clock to `requestAnimationFrame`, so an\n // injected one passes straight through rather than being defaulted twice.\n const frameLoop = useVisibleRaf(\n () => { rafTick(); },\n {\n requestFrame: opts.requestFrame,\n cancelFrame: opts.cancelFrame,\n },\n );\n\n const stopRaf = (): void => { frameLoop.cancel(); };\n\n const rafTick = (): void => {\n if (!mountedRef.current) return;\n\n core.tick();\n optsRef.current.onTick?.(core.nodes);\n\n if (core.alpha() < core.alphaMin()) {\n stopRaf();\n if (!endedRef.current) {\n endedRef.current = true;\n optsRef.current.onEnd?.();\n }\n return;\n }\n\n frameLoop.request();\n };\n\n const startRaf = (): void => {\n if (!mountedRef.current) return;\n frameLoop.request();\n };\n\n // Starts on every mount, StrictMode's simulated remount included, and in a\n // layout effect so a host's own layout effects can already `stop()` it.\n // `createSimulation` has already primed the nodes.\n useLayoutEffect(() => {\n mountedRef.current = true;\n frameLoop.request();\n return () => {\n mountedRef.current = false;\n frameLoop.cancel();\n };\n }, [frameLoop]);\n\n // Stable handle. All methods read through the core, so a fixed identity is safe.\n const handle = useMemo<Simulation<TNode>>(() => {\n /* eslint-disable @typescript-eslint/no-explicit-any */\n const h: any = {\n get nodes() {\n return core.nodes;\n },\n setNodes(newNodes: TNode[]) {\n core.setNodes(newNodes);\n return h;\n },\n setForces(newForces: SimulationForce<TNode>[]) {\n core.setForces(newForces);\n return h;\n },\n alpha(value?: number) {\n if (value === undefined) return core.alpha();\n core.alpha(value);\n if (value >= core.alphaMin()) endedRef.current = false;\n return h;\n },\n alphaTarget(value?: number) {\n if (value === undefined) return core.alphaTarget();\n core.alphaTarget(value);\n if (value > 0) endedRef.current = false;\n return h;\n },\n alphaDecay(value?: number) {\n if (value === undefined) return core.alphaDecay();\n core.alphaDecay(value);\n return h;\n },\n alphaMin(value?: number) {\n if (value === undefined) return core.alphaMin();\n core.alphaMin(value);\n return h;\n },\n velocityDecay(value?: number) {\n if (value === undefined) return core.velocityDecay();\n core.velocityDecay(value);\n return h;\n },\n restart() {\n if (core.alpha() < core.alphaMin()) core.alpha(1);\n endedRef.current = false;\n startRaf();\n return h;\n },\n stop() {\n stopRaf();\n return h;\n },\n tick(iterations: number = 1) {\n core.tick(iterations);\n return h;\n },\n isSettled() {\n return core.isSettled();\n },\n };\n return h as Simulation<TNode>;\n /* eslint-enable @typescript-eslint/no-explicit-any */\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return handle;\n}\n"]}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal node interface for the simulation primitive. Matches d3-force's
|
|
3
|
+
* node shape exactly so d3-force's bundled forces work without translation.
|
|
4
|
+
*
|
|
5
|
+
* Consumers extend this with arbitrary additional fields (id, label, etc.).
|
|
6
|
+
*/
|
|
7
|
+
interface SimulationNode {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
vx?: number;
|
|
11
|
+
vy?: number;
|
|
12
|
+
/** Pin x to this value when non-null. The integrator skips x/vx for the node. */
|
|
13
|
+
fx?: number | null;
|
|
14
|
+
fy?: number | null;
|
|
15
|
+
/** Array index, written by `initialize`. Some forces (e.g. forceLink) rely on it. */
|
|
16
|
+
index?: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Force protocol — contract-compatible with d3-force. A force is a function
|
|
20
|
+
* called once per tick with the current alpha; it mutates `vx`/`vy` on nodes.
|
|
21
|
+
* Optional `initialize` lets the force compute setup (link indices, etc.).
|
|
22
|
+
*/
|
|
23
|
+
interface SimulationForce<TNode extends SimulationNode = SimulationNode> {
|
|
24
|
+
(alpha: number): void;
|
|
25
|
+
initialize?(nodes: TNode[], random?: () => number): void;
|
|
26
|
+
}
|
|
27
|
+
/** Options for `useSimulation`: everything {@link SimulationOptions} carries,
|
|
28
|
+
* plus the loop's own callbacks and clock. */
|
|
29
|
+
interface UseSimulationOptions<TNode extends SimulationNode> extends SimulationOptions<TNode> {
|
|
30
|
+
/** Fired after each integration step. */
|
|
31
|
+
onTick?: (nodes: TNode[]) => void;
|
|
32
|
+
/** Fired once when the sim settles (alpha < alphaMin && alphaTarget === 0). */
|
|
33
|
+
onEnd?: () => void;
|
|
34
|
+
/** Optional rAF injection. Defaults to window.requestAnimationFrame. */
|
|
35
|
+
requestFrame?: (cb: (t: number) => void) => number;
|
|
36
|
+
cancelFrame?: (handle: number) => void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A force simulation over a set of nodes, with no clock of its own.
|
|
40
|
+
*
|
|
41
|
+
* Each tick applies every force and then integrates velocities, scaled by
|
|
42
|
+
* `alpha` — a temperature that decays toward `alphaTarget` so the layout
|
|
43
|
+
* settles instead of jittering forever. Raise `alphaTarget` to reheat it,
|
|
44
|
+
* which is what a drag does.
|
|
45
|
+
*
|
|
46
|
+
* Nodes are mutated in place; the simulation holds the array, not a copy.
|
|
47
|
+
*
|
|
48
|
+
* Nothing here advances time. {@link Simulation} adds the frame loop; a caller
|
|
49
|
+
* that wants the whole relaxation at once — a layout pass, a pre-warm, a test
|
|
50
|
+
* — drives {@link SimulationCore.tick} itself.
|
|
51
|
+
*/
|
|
52
|
+
interface SimulationCore<TNode extends SimulationNode> {
|
|
53
|
+
readonly nodes: TNode[];
|
|
54
|
+
setNodes(nodes: TNode[]): this;
|
|
55
|
+
setForces(forces: SimulationForce<TNode>[]): this;
|
|
56
|
+
alpha(): number;
|
|
57
|
+
alpha(value: number): this;
|
|
58
|
+
alphaTarget(): number;
|
|
59
|
+
alphaTarget(value: number): this;
|
|
60
|
+
alphaDecay(): number;
|
|
61
|
+
alphaDecay(value: number): this;
|
|
62
|
+
alphaMin(): number;
|
|
63
|
+
alphaMin(value: number): this;
|
|
64
|
+
velocityDecay(): number;
|
|
65
|
+
velocityDecay(value: number): this;
|
|
66
|
+
/**
|
|
67
|
+
* Run the tick body synchronously `iterations` times. Does NOT fire
|
|
68
|
+
* onTick or onEnd (matches d3-force). Used for offscreen pre-warm and tests.
|
|
69
|
+
*/
|
|
70
|
+
tick(iterations?: number): this;
|
|
71
|
+
isSettled(): boolean;
|
|
72
|
+
}
|
|
73
|
+
/** A {@link SimulationCore} running on a frame loop. */
|
|
74
|
+
interface Simulation<TNode extends SimulationNode> extends SimulationCore<TNode> {
|
|
75
|
+
/** Resume the RAF loop. If alpha < alphaMin, reset alpha to 1. */
|
|
76
|
+
restart(): this;
|
|
77
|
+
/** Cancel the RAF loop. Does not change alpha. */
|
|
78
|
+
stop(): this;
|
|
79
|
+
}
|
|
80
|
+
/** What {@link SimulationCore} needs: the nodes, the forces acting on them,
|
|
81
|
+
* and the cooling schedule. {@link UseSimulationOptions} adds the loop's. */
|
|
82
|
+
interface SimulationOptions<TNode extends SimulationNode> {
|
|
83
|
+
/** Mutable array of nodes. Kit + forces mutate vx/vy/x/y in place. */
|
|
84
|
+
nodes: TNode[];
|
|
85
|
+
/** Forces applied per tick. Each may implement `initialize` for setup. */
|
|
86
|
+
forces?: SimulationForce<TNode>[];
|
|
87
|
+
/** Initial alpha. Default 1. */
|
|
88
|
+
alpha?: number;
|
|
89
|
+
/** Settle threshold. When alpha drops below and alphaTarget is 0, onEnd fires. Default 0.001. */
|
|
90
|
+
alphaMin?: number;
|
|
91
|
+
/** Per-tick decay rate toward alphaTarget. Default 1 - 0.001^(1/300) ≈ 0.0228. */
|
|
92
|
+
alphaDecay?: number;
|
|
93
|
+
/** Target alpha. Set above alphaMin to keep sim warm (e.g. during drag). Default 0. */
|
|
94
|
+
alphaTarget?: number;
|
|
95
|
+
/** Friction multiplier on velocity per tick: vx *= (1 - velocityDecay). Default 0.4. */
|
|
96
|
+
velocityDecay?: number;
|
|
97
|
+
/** Handed to each force's `initialize`. Default `Math.random`; pass a seeded
|
|
98
|
+
* source to make a run reproducible. */
|
|
99
|
+
random?: () => number;
|
|
100
|
+
}
|
|
101
|
+
/** Default alpha decay: such that alpha drops from 1 to alphaMin (0.001) in 300 ticks. */
|
|
102
|
+
declare const DEFAULT_ALPHA_DECAY: number;
|
|
103
|
+
/** Alpha below which a cooling simulation is considered settled. */
|
|
104
|
+
declare const DEFAULT_ALPHA_MIN = 0.001;
|
|
105
|
+
/** Fraction of velocity shed each tick — friction. Higher settles sooner and
|
|
106
|
+
* overshoots less. */
|
|
107
|
+
declare const DEFAULT_VELOCITY_DECAY = 0.4;
|
|
108
|
+
|
|
109
|
+
export { DEFAULT_ALPHA_DECAY as D, type SimulationNode as S, type UseSimulationOptions as U, type SimulationOptions as a, type SimulationCore as b, DEFAULT_ALPHA_MIN as c, DEFAULT_VELOCITY_DECAY as d, type Simulation as e, type SimulationForce as f };
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weasel-js/simulation",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.9.4",
|
|
4
|
+
"description": "A velocity-Verlet N-body integrator that takes d3-force's forces, and a hook that runs it on a frame loop. The root entry has no React and no DOM.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./react": {
|
|
17
|
+
"import": "./dist/react.js",
|
|
18
|
+
"types": "./dist/react.d.ts"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"react": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"peerDependenciesMeta": {
|
|
26
|
+
"react": { "optional": true }
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@weasel-js/react": "1.9.4"
|
|
30
|
+
},
|
|
31
|
+
"author": "orochi235",
|
|
32
|
+
"homepage": "https://orochi235.github.io/weasel/",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/orochi235/weasel.git",
|
|
36
|
+
"directory": "packages/simulation"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/orochi235/weasel/issues"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=22"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist",
|
|
46
|
+
"README.md",
|
|
47
|
+
"LICENSE"
|
|
48
|
+
],
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsup"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public",
|
|
54
|
+
"provenance": true
|
|
55
|
+
}
|
|
56
|
+
}
|