automatick 0.0.1 → 0.0.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/README.md +2 -0
- package/dist/{chunk-YNLOTPDY.js → chunk-A375T3UD.js} +39 -3
- package/dist/chunk-IKR53C2U.js +12 -0
- package/dist/chunk-LMHH7YPE.js +89 -0
- package/dist/chunk-VPS3ZXWI.js +132 -0
- package/dist/engine.cjs +42 -3
- package/dist/engine.d.cts +42 -18
- package/dist/engine.d.ts +42 -18
- package/dist/engine.js +2 -1
- package/dist/react/EngineContext.d.cts +4 -3
- package/dist/react/EngineContext.d.ts +4 -3
- package/dist/react/Simulation.cjs +341 -102
- package/dist/react/Simulation.d.cts +36 -16
- package/dist/react/Simulation.d.ts +36 -16
- package/dist/react/Simulation.js +93 -100
- package/dist/react/SimulationContext.d.cts +1 -2
- package/dist/react/SimulationContext.d.ts +1 -2
- package/dist/react/hooks.d.cts +1 -1
- package/dist/react/hooks.d.ts +1 -1
- package/dist/react/useSimulationCanvas.cjs +2 -10
- package/dist/react/useSimulationCanvas.d.cts +4 -7
- package/dist/react/useSimulationCanvas.d.ts +4 -7
- package/dist/react/useSimulationCanvas.js +2 -10
- package/dist/sim.cjs +7 -2
- package/dist/sim.d.cts +34 -14
- package/dist/sim.d.ts +34 -14
- package/dist/sim.js +6 -5
- package/dist/standalone/engine.js +242 -0
- package/dist/standalone/sim.js +11 -0
- package/dist/state.cjs +18 -0
- package/dist/state.d.cts +16 -0
- package/dist/state.d.ts +16 -0
- package/dist/state.js +0 -0
- package/dist/worker/createSimWorker.cjs +12 -3
- package/dist/worker/createSimWorker.d.cts +1 -2
- package/dist/worker/createSimWorker.d.ts +1 -2
- package/dist/worker/createSimWorker.js +3 -119
- package/dist/worker/protocol.d.cts +5 -5
- package/dist/worker/protocol.d.ts +5 -5
- package/dist/worker/workerRunner.cjs +21 -6
- package/dist/worker/workerRunner.d.cts +11 -3
- package/dist/worker/workerRunner.d.ts +11 -3
- package/dist/worker/workerRunner.js +3 -70
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -101,6 +101,8 @@ Useful when `step` is expensive (large grids, n-body simulations, fluid solvers)
|
|
|
101
101
|
| `automatick/react/performance` | `<PerformanceOverlay>` |
|
|
102
102
|
| `automatick/react/context` | `<SimulationContext>` |
|
|
103
103
|
|
|
104
|
+
All entry points use named exports. If you'd rather have a single namespace object, use `import * as Automatick from 'automatick'` and call `Automatick.createEngine(...)` — same for any subpath.
|
|
105
|
+
|
|
104
106
|
## License
|
|
105
107
|
|
|
106
108
|
MIT
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isInitFn
|
|
3
|
+
} from "./chunk-IKR53C2U.js";
|
|
4
|
+
|
|
1
5
|
// src/engine.ts
|
|
2
6
|
var PERF_BUFFER_SIZE = 120;
|
|
3
7
|
var SimulationEngine = class {
|
|
@@ -16,15 +20,40 @@ var SimulationEngine = class {
|
|
|
16
20
|
listeners = /* @__PURE__ */ new Set();
|
|
17
21
|
historyListeners = /* @__PURE__ */ new Set();
|
|
18
22
|
perfBuffer = [];
|
|
23
|
+
rafId = null;
|
|
24
|
+
rafCancel = null;
|
|
19
25
|
constructor(config) {
|
|
20
|
-
|
|
26
|
+
const init = config.init;
|
|
27
|
+
if (isInitFn(init)) {
|
|
28
|
+
this.initFn = init;
|
|
29
|
+
} else {
|
|
30
|
+
const seed = structuredClone(init);
|
|
31
|
+
this.initFn = () => structuredClone(seed);
|
|
32
|
+
}
|
|
21
33
|
this.stepFn = config.step;
|
|
22
34
|
this.shouldStopFn = config.shouldStop;
|
|
23
35
|
this.maxTime = config.maxTime;
|
|
24
36
|
this.delayMs = config.delayMs ?? 0;
|
|
25
37
|
this.ticksPerFrame = config.ticksPerFrame ?? 1;
|
|
26
|
-
this.params = { ...config.initialParams };
|
|
38
|
+
this.params = config.initialParams ? { ...config.initialParams } : {};
|
|
27
39
|
this.data = this.initFn(this.params);
|
|
40
|
+
if (config.render) {
|
|
41
|
+
this.listeners.add(config.render);
|
|
42
|
+
config.render(this.getSnapshot());
|
|
43
|
+
}
|
|
44
|
+
const autoFrame = config.autoFrame ?? true;
|
|
45
|
+
if (autoFrame) {
|
|
46
|
+
const raf = globalThis.requestAnimationFrame;
|
|
47
|
+
const caf = globalThis.cancelAnimationFrame;
|
|
48
|
+
if (typeof raf === "function" && typeof caf === "function") {
|
|
49
|
+
const loop = (now) => {
|
|
50
|
+
this.handleAnimationFrame(now);
|
|
51
|
+
this.rafId = raf(loop);
|
|
52
|
+
};
|
|
53
|
+
this.rafCancel = caf;
|
|
54
|
+
this.rafId = raf(loop);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
28
57
|
}
|
|
29
58
|
getSnapshot() {
|
|
30
59
|
return {
|
|
@@ -141,6 +170,11 @@ var SimulationEngine = class {
|
|
|
141
170
|
}
|
|
142
171
|
}
|
|
143
172
|
destroy() {
|
|
173
|
+
if (this.rafId !== null && this.rafCancel) {
|
|
174
|
+
this.rafCancel(this.rafId);
|
|
175
|
+
}
|
|
176
|
+
this.rafId = null;
|
|
177
|
+
this.rafCancel = null;
|
|
144
178
|
this.listeners.clear();
|
|
145
179
|
this.historyListeners.clear();
|
|
146
180
|
}
|
|
@@ -160,7 +194,9 @@ var SimulationEngine = class {
|
|
|
160
194
|
this.data = this.stepFn({
|
|
161
195
|
data: this.data,
|
|
162
196
|
params: this.params,
|
|
163
|
-
tick: this.tick
|
|
197
|
+
tick: this.tick,
|
|
198
|
+
status: this.status,
|
|
199
|
+
stepDurationMs: this.lastStepMs
|
|
164
200
|
});
|
|
165
201
|
const t1 = performance.now();
|
|
166
202
|
this.lastStepMs = t1 - t0;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// src/worker/serialize.ts
|
|
2
|
+
function deserializeWorkerMessage(raw) {
|
|
3
|
+
return raw;
|
|
4
|
+
}
|
|
5
|
+
function serializeMainMessage(msg) {
|
|
6
|
+
return msg;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// src/worker/workerRunner.ts
|
|
10
|
+
var PERF_BUFFER_SIZE = 120;
|
|
11
|
+
function createWorkerRunner(worker, config) {
|
|
12
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
13
|
+
let currentSnapshot = {
|
|
14
|
+
data: void 0,
|
|
15
|
+
params: config.initialParams,
|
|
16
|
+
tick: 0,
|
|
17
|
+
status: "idle",
|
|
18
|
+
stepDurationMs: 0
|
|
19
|
+
};
|
|
20
|
+
const perfBuffer = [];
|
|
21
|
+
function send(msg) {
|
|
22
|
+
worker.postMessage(serializeMainMessage(msg));
|
|
23
|
+
}
|
|
24
|
+
function emit() {
|
|
25
|
+
for (const l of listeners) {
|
|
26
|
+
l(currentSnapshot);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function pushPerf(snapshot) {
|
|
30
|
+
if (snapshot.tick <= 0) return;
|
|
31
|
+
const last = perfBuffer[perfBuffer.length - 1];
|
|
32
|
+
if (last && last.tick === snapshot.tick) return;
|
|
33
|
+
if (perfBuffer.length >= PERF_BUFFER_SIZE) perfBuffer.shift();
|
|
34
|
+
perfBuffer.push({ tick: snapshot.tick, stepMs: snapshot.stepDurationMs });
|
|
35
|
+
}
|
|
36
|
+
worker.onmessage = (event) => {
|
|
37
|
+
const msg = deserializeWorkerMessage(event.data);
|
|
38
|
+
switch (msg.kind) {
|
|
39
|
+
case "snapshot":
|
|
40
|
+
currentSnapshot = msg.snapshot;
|
|
41
|
+
pushPerf(msg.snapshot);
|
|
42
|
+
emit();
|
|
43
|
+
break;
|
|
44
|
+
case "error":
|
|
45
|
+
currentSnapshot = { ...currentSnapshot, status: "stopped" };
|
|
46
|
+
emit();
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
worker.onerror = () => {
|
|
51
|
+
currentSnapshot = { ...currentSnapshot, status: "stopped" };
|
|
52
|
+
emit();
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
getSnapshot: () => currentSnapshot,
|
|
56
|
+
subscribe(listener) {
|
|
57
|
+
listeners.add(listener);
|
|
58
|
+
return () => {
|
|
59
|
+
listeners.delete(listener);
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
play: () => send({ kind: "play" }),
|
|
63
|
+
pause: () => send({ kind: "pause" }),
|
|
64
|
+
stop: () => send({ kind: "stop" }),
|
|
65
|
+
seek: (tick) => send({ kind: "seek", tick }),
|
|
66
|
+
advance: (count = 1) => send({ kind: "advance", count }),
|
|
67
|
+
setParams: (patch) => send({ kind: "setParams", patch }),
|
|
68
|
+
resetWith: (patch) => send({ kind: "resetWith", patch }),
|
|
69
|
+
setConfig: (patch) => send({ kind: "setConfig", patch }),
|
|
70
|
+
recordDrawTime(tick, ms) {
|
|
71
|
+
for (let i = perfBuffer.length - 1; i >= 0; i--) {
|
|
72
|
+
if (perfBuffer[i].tick === tick) {
|
|
73
|
+
perfBuffer[i].drawMs = ms;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
getPerformance: () => perfBuffer,
|
|
79
|
+
destroy() {
|
|
80
|
+
listeners.clear();
|
|
81
|
+
send({ kind: "destroy" });
|
|
82
|
+
worker.terminate();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export {
|
|
88
|
+
createWorkerRunner
|
|
89
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// src/worker/createSimWorker.ts
|
|
2
|
+
var WORKER_SCRIPT = `
|
|
3
|
+
let engine = null;
|
|
4
|
+
let loopTimer = null;
|
|
5
|
+
let snapshotIntervalMs = 16;
|
|
6
|
+
let lastSnapshotMs = 0;
|
|
7
|
+
let ticksPerFrame = 1;
|
|
8
|
+
let delayMs = 0;
|
|
9
|
+
|
|
10
|
+
function emitSnapshot() {
|
|
11
|
+
if (!engine) return;
|
|
12
|
+
postMessage({ kind: 'snapshot', snapshot: engine.getSnapshot() });
|
|
13
|
+
lastSnapshotMs = performance.now();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function tickLoop() {
|
|
17
|
+
if (!engine || engine.getStatus() !== 'playing') return;
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < ticksPerFrame; i++) {
|
|
20
|
+
engine.advance(1);
|
|
21
|
+
const s = engine.getStatus();
|
|
22
|
+
if (s === 'stopped') { emitSnapshot(); return; }
|
|
23
|
+
if (s !== 'paused') break;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// advance() transitions to paused; resume playing for the next batch
|
|
27
|
+
if (engine.getStatus() === 'paused') engine.play();
|
|
28
|
+
|
|
29
|
+
if (performance.now() - lastSnapshotMs >= snapshotIntervalMs) emitSnapshot();
|
|
30
|
+
loopTimer = setTimeout(tickLoop, delayMs);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function stopLoop() {
|
|
34
|
+
if (loopTimer !== null) { clearTimeout(loopTimer); loopTimer = null; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
self.onmessage = async (event) => {
|
|
38
|
+
const msg = event.data;
|
|
39
|
+
try {
|
|
40
|
+
switch (msg.kind) {
|
|
41
|
+
case 'init': {
|
|
42
|
+
delayMs = msg.config.delayMs || 0;
|
|
43
|
+
ticksPerFrame = msg.config.ticksPerFrame || 1;
|
|
44
|
+
snapshotIntervalMs = msg.config.snapshotIntervalMs || 16;
|
|
45
|
+
|
|
46
|
+
const [simMod, engineMod] = await Promise.all([
|
|
47
|
+
import(msg.moduleUrl),
|
|
48
|
+
import(msg.engineUrl),
|
|
49
|
+
]);
|
|
50
|
+
const sim = simMod.default;
|
|
51
|
+
// Merge sim.defaultParams under the patch sent from main \u2014 the main
|
|
52
|
+
// thread sees the sim module via a URL, so it can't apply defaults.
|
|
53
|
+
const initialParams = { ...(sim.defaultParams || {}), ...(msg.params || {}) };
|
|
54
|
+
engine = engineMod.createEngine({
|
|
55
|
+
init: sim.init,
|
|
56
|
+
step: sim.step,
|
|
57
|
+
shouldStop: sim.shouldStop,
|
|
58
|
+
initialParams,
|
|
59
|
+
maxTime: msg.config.maxTime,
|
|
60
|
+
// Worker host owns its own setTimeout-driven loop; rAF wouldn't exist here anyway.
|
|
61
|
+
autoFrame: false,
|
|
62
|
+
});
|
|
63
|
+
emitSnapshot();
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case 'play':
|
|
67
|
+
if (!engine) return;
|
|
68
|
+
engine.play(); emitSnapshot(); stopLoop(); loopTimer = setTimeout(tickLoop, 0);
|
|
69
|
+
break;
|
|
70
|
+
case 'pause':
|
|
71
|
+
if (!engine) return;
|
|
72
|
+
stopLoop(); engine.pause(); emitSnapshot();
|
|
73
|
+
break;
|
|
74
|
+
case 'stop':
|
|
75
|
+
if (!engine) return;
|
|
76
|
+
stopLoop(); engine.stop(); emitSnapshot();
|
|
77
|
+
break;
|
|
78
|
+
case 'seek':
|
|
79
|
+
if (!engine) return;
|
|
80
|
+
stopLoop(); engine.seek(msg.tick); emitSnapshot();
|
|
81
|
+
break;
|
|
82
|
+
case 'advance':
|
|
83
|
+
if (!engine) return;
|
|
84
|
+
engine.advance(msg.count); emitSnapshot();
|
|
85
|
+
break;
|
|
86
|
+
case 'setParams':
|
|
87
|
+
if (!engine) return;
|
|
88
|
+
engine.setParams(msg.patch); emitSnapshot();
|
|
89
|
+
break;
|
|
90
|
+
case 'resetWith':
|
|
91
|
+
if (!engine) return;
|
|
92
|
+
stopLoop(); engine.resetWith(msg.patch); emitSnapshot();
|
|
93
|
+
break;
|
|
94
|
+
case 'setConfig':
|
|
95
|
+
if (msg.patch.delayMs !== undefined) delayMs = msg.patch.delayMs;
|
|
96
|
+
if (msg.patch.ticksPerFrame !== undefined) ticksPerFrame = msg.patch.ticksPerFrame;
|
|
97
|
+
if (msg.patch.snapshotIntervalMs !== undefined) snapshotIntervalMs = msg.patch.snapshotIntervalMs;
|
|
98
|
+
break;
|
|
99
|
+
case 'destroy':
|
|
100
|
+
stopLoop();
|
|
101
|
+
if (engine) { engine.destroy(); engine = null; }
|
|
102
|
+
self.close();
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
postMessage({ kind: 'error', error: { message: err.message, stack: err.stack } });
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
`;
|
|
110
|
+
function createSimWorker(options) {
|
|
111
|
+
const blob = new Blob([WORKER_SCRIPT], { type: "text/javascript" });
|
|
112
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
113
|
+
const worker = new Worker(blobUrl, { type: "module" });
|
|
114
|
+
worker.addEventListener("message", function cleanup(event) {
|
|
115
|
+
if (event.data?.kind === "snapshot" || event.data?.kind === "error") {
|
|
116
|
+
URL.revokeObjectURL(blobUrl);
|
|
117
|
+
worker.removeEventListener("message", cleanup);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
worker.postMessage({
|
|
121
|
+
kind: "init",
|
|
122
|
+
moduleUrl: options.moduleUrl,
|
|
123
|
+
engineUrl: options.engineUrl,
|
|
124
|
+
params: options.initialParams,
|
|
125
|
+
config: options.config
|
|
126
|
+
});
|
|
127
|
+
return worker;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
createSimWorker
|
|
132
|
+
};
|
package/dist/engine.cjs
CHANGED
|
@@ -24,6 +24,13 @@ __export(engine_exports, {
|
|
|
24
24
|
createEngine: () => createEngine
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(engine_exports);
|
|
27
|
+
|
|
28
|
+
// src/sim.ts
|
|
29
|
+
function isInitFn(init) {
|
|
30
|
+
return typeof init === "function";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/engine.ts
|
|
27
34
|
var PERF_BUFFER_SIZE = 120;
|
|
28
35
|
var SimulationEngine = class {
|
|
29
36
|
data;
|
|
@@ -41,15 +48,40 @@ var SimulationEngine = class {
|
|
|
41
48
|
listeners = /* @__PURE__ */ new Set();
|
|
42
49
|
historyListeners = /* @__PURE__ */ new Set();
|
|
43
50
|
perfBuffer = [];
|
|
51
|
+
rafId = null;
|
|
52
|
+
rafCancel = null;
|
|
44
53
|
constructor(config) {
|
|
45
|
-
|
|
54
|
+
const init = config.init;
|
|
55
|
+
if (isInitFn(init)) {
|
|
56
|
+
this.initFn = init;
|
|
57
|
+
} else {
|
|
58
|
+
const seed = structuredClone(init);
|
|
59
|
+
this.initFn = () => structuredClone(seed);
|
|
60
|
+
}
|
|
46
61
|
this.stepFn = config.step;
|
|
47
62
|
this.shouldStopFn = config.shouldStop;
|
|
48
63
|
this.maxTime = config.maxTime;
|
|
49
64
|
this.delayMs = config.delayMs ?? 0;
|
|
50
65
|
this.ticksPerFrame = config.ticksPerFrame ?? 1;
|
|
51
|
-
this.params = { ...config.initialParams };
|
|
66
|
+
this.params = config.initialParams ? { ...config.initialParams } : {};
|
|
52
67
|
this.data = this.initFn(this.params);
|
|
68
|
+
if (config.render) {
|
|
69
|
+
this.listeners.add(config.render);
|
|
70
|
+
config.render(this.getSnapshot());
|
|
71
|
+
}
|
|
72
|
+
const autoFrame = config.autoFrame ?? true;
|
|
73
|
+
if (autoFrame) {
|
|
74
|
+
const raf = globalThis.requestAnimationFrame;
|
|
75
|
+
const caf = globalThis.cancelAnimationFrame;
|
|
76
|
+
if (typeof raf === "function" && typeof caf === "function") {
|
|
77
|
+
const loop = (now) => {
|
|
78
|
+
this.handleAnimationFrame(now);
|
|
79
|
+
this.rafId = raf(loop);
|
|
80
|
+
};
|
|
81
|
+
this.rafCancel = caf;
|
|
82
|
+
this.rafId = raf(loop);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
53
85
|
}
|
|
54
86
|
getSnapshot() {
|
|
55
87
|
return {
|
|
@@ -166,6 +198,11 @@ var SimulationEngine = class {
|
|
|
166
198
|
}
|
|
167
199
|
}
|
|
168
200
|
destroy() {
|
|
201
|
+
if (this.rafId !== null && this.rafCancel) {
|
|
202
|
+
this.rafCancel(this.rafId);
|
|
203
|
+
}
|
|
204
|
+
this.rafId = null;
|
|
205
|
+
this.rafCancel = null;
|
|
169
206
|
this.listeners.clear();
|
|
170
207
|
this.historyListeners.clear();
|
|
171
208
|
}
|
|
@@ -185,7 +222,9 @@ var SimulationEngine = class {
|
|
|
185
222
|
this.data = this.stepFn({
|
|
186
223
|
data: this.data,
|
|
187
224
|
params: this.params,
|
|
188
|
-
tick: this.tick
|
|
225
|
+
tick: this.tick,
|
|
226
|
+
status: this.status,
|
|
227
|
+
stepDurationMs: this.lastStepMs
|
|
189
228
|
});
|
|
190
229
|
const t1 = performance.now();
|
|
191
230
|
this.lastStepMs = t1 - t0;
|
package/dist/engine.d.cts
CHANGED
|
@@ -1,28 +1,50 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SimInit } from './sim.cjs';
|
|
2
|
+
import { State, SimulationStatus } from './state.cjs';
|
|
2
3
|
|
|
3
|
-
type SimulationStatus = 'idle' | 'playing' | 'paused' | 'stopped';
|
|
4
4
|
type TickPerformance = {
|
|
5
5
|
tick: number;
|
|
6
6
|
stepMs: number;
|
|
7
7
|
drawMs?: number;
|
|
8
8
|
};
|
|
9
|
-
type EngineConfig<Data, Params
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
type EngineConfig<Data, Params = Record<string, never>> = {
|
|
10
|
+
/**
|
|
11
|
+
* Initial simulation state — value or `(params) => Data`. See `SimInit`.
|
|
12
|
+
* When a value is passed, the engine `structuredClone`s it on each (re)init.
|
|
13
|
+
*/
|
|
14
|
+
init: SimInit<Data, Params>;
|
|
15
|
+
step: (state: State<Data, Params>) => Data;
|
|
12
16
|
shouldStop?: (data: Data, params: Params) => boolean;
|
|
13
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Initial param values. Optional — when omitted, the engine seeds an empty
|
|
19
|
+
* params object and `Params` defaults to `Record<string, never>`.
|
|
20
|
+
*/
|
|
21
|
+
initialParams?: Params;
|
|
14
22
|
maxTime?: number;
|
|
15
23
|
delayMs?: number;
|
|
16
24
|
ticksPerFrame?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Optional render callback — sugar for the vanilla path. When provided, the
|
|
27
|
+
* engine calls it once with the initial state (right after init) and on
|
|
28
|
+
* every state emit thereafter, equivalent to `engine.subscribe(render)`
|
|
29
|
+
* followed by an initial paint. `subscribe` remains the lower-level
|
|
30
|
+
* primitive; React adapter and worker callers wire their own subscribers.
|
|
31
|
+
*/
|
|
32
|
+
render?: (snapshot: State<Data, Params>) => void;
|
|
33
|
+
/**
|
|
34
|
+
* Drive `handleAnimationFrame` from an internal `requestAnimationFrame` loop.
|
|
35
|
+
* Defaults to `true` so vanilla callers don't have to write the loop. The
|
|
36
|
+
* React adapter and worker host pass `false` because they either own the
|
|
37
|
+
* frame loop themselves or run in an environment that has none. The loop is
|
|
38
|
+
* created at construction and torn down by `destroy()`. If
|
|
39
|
+
* `globalThis.requestAnimationFrame` is missing (server, worker), the option
|
|
40
|
+
* is silently a no-op.
|
|
41
|
+
*
|
|
42
|
+
* Note: when `true`, the rAF closure pins this engine in memory — vanilla
|
|
43
|
+
* consumers must call `destroy()` to let it be garbage-collected.
|
|
44
|
+
*/
|
|
45
|
+
autoFrame?: boolean;
|
|
17
46
|
};
|
|
18
|
-
|
|
19
|
-
data: Data;
|
|
20
|
-
params: Params;
|
|
21
|
-
tick: number;
|
|
22
|
-
status: SimulationStatus;
|
|
23
|
-
stepDurationMs: number;
|
|
24
|
-
};
|
|
25
|
-
declare class SimulationEngine<Data, Params> {
|
|
47
|
+
declare class SimulationEngine<Data, Params = Record<string, never>> {
|
|
26
48
|
private data;
|
|
27
49
|
private params;
|
|
28
50
|
private tick;
|
|
@@ -38,14 +60,16 @@ declare class SimulationEngine<Data, Params> {
|
|
|
38
60
|
private readonly listeners;
|
|
39
61
|
private readonly historyListeners;
|
|
40
62
|
private readonly perfBuffer;
|
|
63
|
+
private rafId;
|
|
64
|
+
private rafCancel;
|
|
41
65
|
constructor(config: EngineConfig<Data, Params>);
|
|
42
|
-
getSnapshot():
|
|
66
|
+
getSnapshot(): State<Data, Params>;
|
|
43
67
|
getStatus(): SimulationStatus;
|
|
44
68
|
getPerformance(): readonly TickPerformance[];
|
|
45
69
|
setDelayMs(ms: number): void;
|
|
46
70
|
setTicksPerFrame(n: number): void;
|
|
47
71
|
recordDrawTime(tick: number, ms: number): void;
|
|
48
|
-
subscribe(listener: (snapshot:
|
|
72
|
+
subscribe(listener: (snapshot: State<Data, Params>) => void): () => void;
|
|
49
73
|
subscribeHistory(listener: (entry: {
|
|
50
74
|
tick: number;
|
|
51
75
|
data: Data;
|
|
@@ -67,6 +91,6 @@ declare class SimulationEngine<Data, Params> {
|
|
|
67
91
|
private emit;
|
|
68
92
|
private emitHistory;
|
|
69
93
|
}
|
|
70
|
-
declare function createEngine<Data, Params
|
|
94
|
+
declare function createEngine<Data, Params = Record<string, never>>(config: EngineConfig<Data, Params>): SimulationEngine<Data, Params>;
|
|
71
95
|
|
|
72
|
-
export { type EngineConfig,
|
|
96
|
+
export { type EngineConfig, SimulationEngine, SimulationStatus, State, type TickPerformance, createEngine };
|
package/dist/engine.d.ts
CHANGED
|
@@ -1,28 +1,50 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SimInit } from './sim.js';
|
|
2
|
+
import { State, SimulationStatus } from './state.js';
|
|
2
3
|
|
|
3
|
-
type SimulationStatus = 'idle' | 'playing' | 'paused' | 'stopped';
|
|
4
4
|
type TickPerformance = {
|
|
5
5
|
tick: number;
|
|
6
6
|
stepMs: number;
|
|
7
7
|
drawMs?: number;
|
|
8
8
|
};
|
|
9
|
-
type EngineConfig<Data, Params
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
type EngineConfig<Data, Params = Record<string, never>> = {
|
|
10
|
+
/**
|
|
11
|
+
* Initial simulation state — value or `(params) => Data`. See `SimInit`.
|
|
12
|
+
* When a value is passed, the engine `structuredClone`s it on each (re)init.
|
|
13
|
+
*/
|
|
14
|
+
init: SimInit<Data, Params>;
|
|
15
|
+
step: (state: State<Data, Params>) => Data;
|
|
12
16
|
shouldStop?: (data: Data, params: Params) => boolean;
|
|
13
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Initial param values. Optional — when omitted, the engine seeds an empty
|
|
19
|
+
* params object and `Params` defaults to `Record<string, never>`.
|
|
20
|
+
*/
|
|
21
|
+
initialParams?: Params;
|
|
14
22
|
maxTime?: number;
|
|
15
23
|
delayMs?: number;
|
|
16
24
|
ticksPerFrame?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Optional render callback — sugar for the vanilla path. When provided, the
|
|
27
|
+
* engine calls it once with the initial state (right after init) and on
|
|
28
|
+
* every state emit thereafter, equivalent to `engine.subscribe(render)`
|
|
29
|
+
* followed by an initial paint. `subscribe` remains the lower-level
|
|
30
|
+
* primitive; React adapter and worker callers wire their own subscribers.
|
|
31
|
+
*/
|
|
32
|
+
render?: (snapshot: State<Data, Params>) => void;
|
|
33
|
+
/**
|
|
34
|
+
* Drive `handleAnimationFrame` from an internal `requestAnimationFrame` loop.
|
|
35
|
+
* Defaults to `true` so vanilla callers don't have to write the loop. The
|
|
36
|
+
* React adapter and worker host pass `false` because they either own the
|
|
37
|
+
* frame loop themselves or run in an environment that has none. The loop is
|
|
38
|
+
* created at construction and torn down by `destroy()`. If
|
|
39
|
+
* `globalThis.requestAnimationFrame` is missing (server, worker), the option
|
|
40
|
+
* is silently a no-op.
|
|
41
|
+
*
|
|
42
|
+
* Note: when `true`, the rAF closure pins this engine in memory — vanilla
|
|
43
|
+
* consumers must call `destroy()` to let it be garbage-collected.
|
|
44
|
+
*/
|
|
45
|
+
autoFrame?: boolean;
|
|
17
46
|
};
|
|
18
|
-
|
|
19
|
-
data: Data;
|
|
20
|
-
params: Params;
|
|
21
|
-
tick: number;
|
|
22
|
-
status: SimulationStatus;
|
|
23
|
-
stepDurationMs: number;
|
|
24
|
-
};
|
|
25
|
-
declare class SimulationEngine<Data, Params> {
|
|
47
|
+
declare class SimulationEngine<Data, Params = Record<string, never>> {
|
|
26
48
|
private data;
|
|
27
49
|
private params;
|
|
28
50
|
private tick;
|
|
@@ -38,14 +60,16 @@ declare class SimulationEngine<Data, Params> {
|
|
|
38
60
|
private readonly listeners;
|
|
39
61
|
private readonly historyListeners;
|
|
40
62
|
private readonly perfBuffer;
|
|
63
|
+
private rafId;
|
|
64
|
+
private rafCancel;
|
|
41
65
|
constructor(config: EngineConfig<Data, Params>);
|
|
42
|
-
getSnapshot():
|
|
66
|
+
getSnapshot(): State<Data, Params>;
|
|
43
67
|
getStatus(): SimulationStatus;
|
|
44
68
|
getPerformance(): readonly TickPerformance[];
|
|
45
69
|
setDelayMs(ms: number): void;
|
|
46
70
|
setTicksPerFrame(n: number): void;
|
|
47
71
|
recordDrawTime(tick: number, ms: number): void;
|
|
48
|
-
subscribe(listener: (snapshot:
|
|
72
|
+
subscribe(listener: (snapshot: State<Data, Params>) => void): () => void;
|
|
49
73
|
subscribeHistory(listener: (entry: {
|
|
50
74
|
tick: number;
|
|
51
75
|
data: Data;
|
|
@@ -67,6 +91,6 @@ declare class SimulationEngine<Data, Params> {
|
|
|
67
91
|
private emit;
|
|
68
92
|
private emitHistory;
|
|
69
93
|
}
|
|
70
|
-
declare function createEngine<Data, Params
|
|
94
|
+
declare function createEngine<Data, Params = Record<string, never>>(config: EngineConfig<Data, Params>): SimulationEngine<Data, Params>;
|
|
71
95
|
|
|
72
|
-
export { type EngineConfig,
|
|
96
|
+
export { type EngineConfig, SimulationEngine, SimulationStatus, State, type TickPerformance, createEngine };
|
package/dist/engine.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { TickPerformance } from '../engine.cjs';
|
|
3
|
+
import { State } from '../state.cjs';
|
|
3
4
|
import '../sim.cjs';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -8,8 +9,8 @@ import '../sim.cjs';
|
|
|
8
9
|
* and to record draw timing back to the engine's performance buffer.
|
|
9
10
|
*/
|
|
10
11
|
type EngineContextValue = {
|
|
11
|
-
subscribe: (listener: (snapshot:
|
|
12
|
-
getSnapshot: () =>
|
|
12
|
+
subscribe: (listener: (snapshot: State<unknown, unknown>) => void) => () => void;
|
|
13
|
+
getSnapshot: () => State<unknown, unknown>;
|
|
13
14
|
recordDrawTime: (tick: number, ms: number) => void;
|
|
14
15
|
getPerformance: () => readonly TickPerformance[];
|
|
15
16
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { TickPerformance } from '../engine.js';
|
|
3
|
+
import { State } from '../state.js';
|
|
3
4
|
import '../sim.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -8,8 +9,8 @@ import '../sim.js';
|
|
|
8
9
|
* and to record draw timing back to the engine's performance buffer.
|
|
9
10
|
*/
|
|
10
11
|
type EngineContextValue = {
|
|
11
|
-
subscribe: (listener: (snapshot:
|
|
12
|
-
getSnapshot: () =>
|
|
12
|
+
subscribe: (listener: (snapshot: State<unknown, unknown>) => void) => () => void;
|
|
13
|
+
getSnapshot: () => State<unknown, unknown>;
|
|
13
14
|
recordDrawTime: (tick: number, ms: number) => void;
|
|
14
15
|
getPerformance: () => readonly TickPerformance[];
|
|
15
16
|
};
|