automatick 0.0.2 → 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/dist/{chunk-UU5V7HGM.js → chunk-A375T3UD.js} +1 -1
- package/dist/chunk-LMHH7YPE.js +89 -0
- package/dist/chunk-VPS3ZXWI.js +132 -0
- package/dist/engine.cjs +1 -1
- package/dist/engine.d.cts +8 -4
- package/dist/engine.d.ts +8 -4
- package/dist/engine.js +1 -1
- package/dist/react/Simulation.cjs +294 -99
- package/dist/react/Simulation.d.cts +36 -17
- package/dist/react/Simulation.d.ts +36 -17
- package/dist/react/Simulation.js +84 -99
- package/dist/sim.d.cts +8 -4
- package/dist/sim.d.ts +8 -4
- package/dist/standalone/engine.js +242 -0
- package/dist/standalone/sim.js +11 -0
- package/dist/worker/createSimWorker.cjs +10 -3
- package/dist/worker/createSimWorker.js +3 -121
- package/dist/worker/protocol.d.cts +3 -2
- package/dist/worker/protocol.d.ts +3 -2
- package/dist/worker/workerRunner.cjs +20 -2
- package/dist/worker/workerRunner.d.cts +9 -0
- package/dist/worker/workerRunner.d.ts +9 -0
- package/dist/worker/workerRunner.js +3 -67
- package/package.json +2 -2
package/dist/react/Simulation.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useStableCallback
|
|
3
3
|
} from "../chunk-SK5SHIWY.js";
|
|
4
|
+
import {
|
|
5
|
+
createSimWorker
|
|
6
|
+
} from "../chunk-VPS3ZXWI.js";
|
|
7
|
+
import {
|
|
8
|
+
createWorkerRunner
|
|
9
|
+
} from "../chunk-LMHH7YPE.js";
|
|
4
10
|
import {
|
|
5
11
|
createEngine
|
|
6
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-A375T3UD.js";
|
|
7
13
|
import "../chunk-IKR53C2U.js";
|
|
8
14
|
import {
|
|
9
15
|
EngineContext
|
|
@@ -16,14 +22,29 @@ import {
|
|
|
16
22
|
import React from "react";
|
|
17
23
|
import { jsx } from "react/jsx-runtime";
|
|
18
24
|
function LocalSimulation(props) {
|
|
19
|
-
const {
|
|
25
|
+
const {
|
|
26
|
+
init,
|
|
27
|
+
step,
|
|
28
|
+
shouldStop,
|
|
29
|
+
defaultParams,
|
|
30
|
+
params: paramsProp,
|
|
31
|
+
children,
|
|
32
|
+
autoplay
|
|
33
|
+
} = props;
|
|
20
34
|
const engineRef = React.useRef(null);
|
|
21
35
|
if (!engineRef.current) {
|
|
22
|
-
|
|
36
|
+
let initialParams;
|
|
37
|
+
if (defaultParams && paramsProp) {
|
|
38
|
+
initialParams = { ...defaultParams, ...paramsProp };
|
|
39
|
+
} else if (defaultParams) {
|
|
40
|
+
initialParams = defaultParams;
|
|
41
|
+
} else if (paramsProp) {
|
|
42
|
+
initialParams = paramsProp;
|
|
43
|
+
}
|
|
23
44
|
engineRef.current = createEngine({
|
|
24
|
-
init
|
|
25
|
-
step
|
|
26
|
-
shouldStop
|
|
45
|
+
init,
|
|
46
|
+
step,
|
|
47
|
+
shouldStop,
|
|
27
48
|
initialParams,
|
|
28
49
|
maxTime: props.maxTime,
|
|
29
50
|
delayMs: props.delayMs,
|
|
@@ -65,119 +86,70 @@ function LocalSimulation(props) {
|
|
|
65
86
|
}, [engine]);
|
|
66
87
|
return /* @__PURE__ */ jsx(SimulationProvider, { snapshot, backend: engine, children });
|
|
67
88
|
}
|
|
89
|
+
function engineUrl() {
|
|
90
|
+
return new URL("../standalone/engine.js", import.meta.url).href;
|
|
91
|
+
}
|
|
68
92
|
function WorkerSimulation(props) {
|
|
69
93
|
const { children, autoplay } = props;
|
|
70
|
-
const [
|
|
94
|
+
const [runner, setRunner] = React.useState(
|
|
71
95
|
null
|
|
72
96
|
);
|
|
73
97
|
const [snapshot, setSnapshot] = React.useState(
|
|
74
98
|
null
|
|
75
99
|
);
|
|
76
|
-
const propsRef = React.useRef(props);
|
|
77
|
-
propsRef.current = props;
|
|
78
100
|
React.useEffect(() => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
shouldStop: simModule.shouldStop,
|
|
91
|
-
initialParams,
|
|
92
|
-
maxTime: p.maxTime,
|
|
93
|
-
autoFrame: false
|
|
94
|
-
});
|
|
95
|
-
if (cancelled) {
|
|
96
|
-
engine.destroy();
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
const delayMs = p.delayMs ?? 0;
|
|
100
|
-
const ticksPerFrame = p.ticksPerFrame ?? 1;
|
|
101
|
-
let loopTimer = null;
|
|
102
|
-
function stopLoop() {
|
|
103
|
-
if (loopTimer !== null) {
|
|
104
|
-
clearTimeout(loopTimer);
|
|
105
|
-
loopTimer = null;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
function tickLoop() {
|
|
109
|
-
if (engine.getStatus() !== "playing") return;
|
|
110
|
-
for (let i = 0; i < ticksPerFrame; i++) {
|
|
111
|
-
engine.advance(1);
|
|
112
|
-
const s = engine.getStatus();
|
|
113
|
-
if (s === "stopped") return;
|
|
114
|
-
if (s !== "paused") break;
|
|
115
|
-
}
|
|
116
|
-
if (engine.getStatus() === "paused") {
|
|
117
|
-
engine.play();
|
|
118
|
-
}
|
|
119
|
-
loopTimer = setTimeout(tickLoop, delayMs);
|
|
101
|
+
const moduleUrl = props.worker.toString();
|
|
102
|
+
const initialParams = props.params ?? {};
|
|
103
|
+
const worker = createSimWorker({
|
|
104
|
+
moduleUrl,
|
|
105
|
+
engineUrl: engineUrl(),
|
|
106
|
+
initialParams,
|
|
107
|
+
config: {
|
|
108
|
+
maxTime: props.maxTime,
|
|
109
|
+
delayMs: props.delayMs,
|
|
110
|
+
ticksPerFrame: props.ticksPerFrame,
|
|
111
|
+
snapshotIntervalMs: props.snapshotIntervalMs
|
|
120
112
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
pause: () => {
|
|
130
|
-
stopLoop();
|
|
131
|
-
engine.pause();
|
|
132
|
-
},
|
|
133
|
-
stop: () => {
|
|
134
|
-
stopLoop();
|
|
135
|
-
engine.stop();
|
|
136
|
-
},
|
|
137
|
-
seek: (tick) => {
|
|
138
|
-
stopLoop();
|
|
139
|
-
engine.seek(tick);
|
|
140
|
-
},
|
|
141
|
-
advance: (count) => engine.advance(count),
|
|
142
|
-
setParams: (patch) => engine.setParams(patch),
|
|
143
|
-
resetWith: (patch) => {
|
|
144
|
-
stopLoop();
|
|
145
|
-
engine.resetWith(patch);
|
|
146
|
-
},
|
|
147
|
-
destroy: () => {
|
|
148
|
-
stopLoop();
|
|
149
|
-
engine.destroy();
|
|
150
|
-
},
|
|
151
|
-
recordDrawTime: (tick, ms) => engine.recordDrawTime(tick, ms),
|
|
152
|
-
getPerformance: () => engine.getPerformance()
|
|
153
|
-
};
|
|
154
|
-
if (!cancelled) {
|
|
155
|
-
setBackend(runner);
|
|
156
|
-
setSnapshot(engine.getSnapshot());
|
|
113
|
+
});
|
|
114
|
+
const r = createWorkerRunner(worker, {
|
|
115
|
+
initialParams,
|
|
116
|
+
config: {
|
|
117
|
+
maxTime: props.maxTime,
|
|
118
|
+
delayMs: props.delayMs,
|
|
119
|
+
ticksPerFrame: props.ticksPerFrame,
|
|
120
|
+
snapshotIntervalMs: props.snapshotIntervalMs
|
|
157
121
|
}
|
|
158
|
-
})
|
|
122
|
+
});
|
|
123
|
+
const unsub = r.subscribe((next) => setSnapshot(next));
|
|
124
|
+
setRunner(r);
|
|
159
125
|
return () => {
|
|
160
|
-
|
|
161
|
-
|
|
126
|
+
unsub();
|
|
127
|
+
r.destroy();
|
|
162
128
|
};
|
|
163
129
|
}, []);
|
|
164
130
|
React.useEffect(() => {
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
}, [backend]);
|
|
168
|
-
React.useEffect(() => {
|
|
169
|
-
if (autoplay && backend) backend.play();
|
|
170
|
-
}, [backend, autoplay]);
|
|
131
|
+
if (autoplay && runner) runner.play();
|
|
132
|
+
}, [runner, autoplay]);
|
|
171
133
|
const isFirstRender = React.useRef(true);
|
|
172
134
|
React.useEffect(() => {
|
|
173
135
|
if (isFirstRender.current) {
|
|
174
136
|
isFirstRender.current = false;
|
|
175
137
|
return;
|
|
176
138
|
}
|
|
177
|
-
if (props.params &&
|
|
178
|
-
}, [
|
|
179
|
-
|
|
180
|
-
|
|
139
|
+
if (props.params && runner) runner.setParams(props.params);
|
|
140
|
+
}, [runner, props.params]);
|
|
141
|
+
React.useEffect(() => {
|
|
142
|
+
if (!runner) return;
|
|
143
|
+
if (props.delayMs === void 0 && props.ticksPerFrame === void 0 && props.snapshotIntervalMs === void 0)
|
|
144
|
+
return;
|
|
145
|
+
runner.setConfig({
|
|
146
|
+
delayMs: props.delayMs,
|
|
147
|
+
ticksPerFrame: props.ticksPerFrame,
|
|
148
|
+
snapshotIntervalMs: props.snapshotIntervalMs
|
|
149
|
+
});
|
|
150
|
+
}, [runner, props.delayMs, props.ticksPerFrame, props.snapshotIntervalMs]);
|
|
151
|
+
if (!snapshot || !runner || snapshot.data === void 0) return null;
|
|
152
|
+
return /* @__PURE__ */ jsx(SimulationProvider, { snapshot, backend: runner, children });
|
|
181
153
|
}
|
|
182
154
|
function SimulationProvider({
|
|
183
155
|
snapshot,
|
|
@@ -236,6 +208,19 @@ function Simulation(props) {
|
|
|
236
208
|
if ("worker" in props && props.worker != null) {
|
|
237
209
|
return /* @__PURE__ */ jsx(WorkerSimulation, { ...props });
|
|
238
210
|
}
|
|
211
|
+
if ("sim" in props && props.sim != null) {
|
|
212
|
+
const { sim, ...rest } = props;
|
|
213
|
+
return /* @__PURE__ */ jsx(
|
|
214
|
+
LocalSimulation,
|
|
215
|
+
{
|
|
216
|
+
...rest,
|
|
217
|
+
init: sim.init,
|
|
218
|
+
step: sim.step,
|
|
219
|
+
shouldStop: sim.shouldStop,
|
|
220
|
+
defaultParams: sim.defaultParams
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
}
|
|
239
224
|
return /* @__PURE__ */ jsx(LocalSimulation, { ...props });
|
|
240
225
|
}
|
|
241
226
|
export {
|
package/dist/sim.d.cts
CHANGED
|
@@ -17,7 +17,7 @@ type SimInit<Data, Params> = ((params: Params) => Data) | Data;
|
|
|
17
17
|
*/
|
|
18
18
|
declare function isInitFn<Data, Params>(init: SimInit<Data, Params>): init is (params: Params) => Data;
|
|
19
19
|
/** A simulation module: the pure business logic that automatick drives. */
|
|
20
|
-
type SimModule<Data, Params
|
|
20
|
+
type SimModule<Data, Params = Record<string, never>> = {
|
|
21
21
|
/**
|
|
22
22
|
* Initial simulation state — value or `(params) => Data`. When a value is
|
|
23
23
|
* passed, the engine takes a fresh `structuredClone` on every (re)init so
|
|
@@ -28,8 +28,12 @@ type SimModule<Data, Params> = {
|
|
|
28
28
|
step: (state: State<Data, Params>) => Data;
|
|
29
29
|
/** Optional termination predicate. Checked after each step. If it returns true, the simulation stops. */
|
|
30
30
|
shouldStop?: (data: Data, params: Params) => boolean;
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Default parameter values. Optional — sims without tweakable params can
|
|
33
|
+
* omit this. When omitted, `Params` defaults to `Record<string, never>` and
|
|
34
|
+
* the engine seeds an empty params object.
|
|
35
|
+
*/
|
|
36
|
+
defaultParams?: Params;
|
|
33
37
|
};
|
|
34
38
|
/**
|
|
35
39
|
* Define a simulation module with full type inference.
|
|
@@ -47,7 +51,7 @@ type SimModule<Data, Params> = {
|
|
|
47
51
|
* });
|
|
48
52
|
* ```
|
|
49
53
|
*/
|
|
50
|
-
declare function defineSim<Data, Params
|
|
54
|
+
declare function defineSim<Data, Params = Record<string, never>>(sim: SimModule<Data, Params>): SimModule<Data, Params>;
|
|
51
55
|
/** Extract the Data type from a SimModule. */
|
|
52
56
|
type SimData<M> = M extends SimModule<infer D, infer _P> ? D : never;
|
|
53
57
|
/** Extract the Params type from a SimModule. */
|
package/dist/sim.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ type SimInit<Data, Params> = ((params: Params) => Data) | Data;
|
|
|
17
17
|
*/
|
|
18
18
|
declare function isInitFn<Data, Params>(init: SimInit<Data, Params>): init is (params: Params) => Data;
|
|
19
19
|
/** A simulation module: the pure business logic that automatick drives. */
|
|
20
|
-
type SimModule<Data, Params
|
|
20
|
+
type SimModule<Data, Params = Record<string, never>> = {
|
|
21
21
|
/**
|
|
22
22
|
* Initial simulation state — value or `(params) => Data`. When a value is
|
|
23
23
|
* passed, the engine takes a fresh `structuredClone` on every (re)init so
|
|
@@ -28,8 +28,12 @@ type SimModule<Data, Params> = {
|
|
|
28
28
|
step: (state: State<Data, Params>) => Data;
|
|
29
29
|
/** Optional termination predicate. Checked after each step. If it returns true, the simulation stops. */
|
|
30
30
|
shouldStop?: (data: Data, params: Params) => boolean;
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Default parameter values. Optional — sims without tweakable params can
|
|
33
|
+
* omit this. When omitted, `Params` defaults to `Record<string, never>` and
|
|
34
|
+
* the engine seeds an empty params object.
|
|
35
|
+
*/
|
|
36
|
+
defaultParams?: Params;
|
|
33
37
|
};
|
|
34
38
|
/**
|
|
35
39
|
* Define a simulation module with full type inference.
|
|
@@ -47,7 +51,7 @@ type SimModule<Data, Params> = {
|
|
|
47
51
|
* });
|
|
48
52
|
* ```
|
|
49
53
|
*/
|
|
50
|
-
declare function defineSim<Data, Params
|
|
54
|
+
declare function defineSim<Data, Params = Record<string, never>>(sim: SimModule<Data, Params>): SimModule<Data, Params>;
|
|
51
55
|
/** Extract the Data type from a SimModule. */
|
|
52
56
|
type SimData<M> = M extends SimModule<infer D, infer _P> ? D : never;
|
|
53
57
|
/** Extract the Params type from a SimModule. */
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// src/sim.ts
|
|
2
|
+
function isInitFn(init) {
|
|
3
|
+
return typeof init === "function";
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// src/engine.ts
|
|
7
|
+
var PERF_BUFFER_SIZE = 120;
|
|
8
|
+
var SimulationEngine = class {
|
|
9
|
+
data;
|
|
10
|
+
params;
|
|
11
|
+
tick = 0;
|
|
12
|
+
status = "idle";
|
|
13
|
+
lastUpdateMs = null;
|
|
14
|
+
lastStepMs = 0;
|
|
15
|
+
initFn;
|
|
16
|
+
stepFn;
|
|
17
|
+
shouldStopFn;
|
|
18
|
+
maxTime;
|
|
19
|
+
delayMs;
|
|
20
|
+
ticksPerFrame;
|
|
21
|
+
listeners = /* @__PURE__ */ new Set();
|
|
22
|
+
historyListeners = /* @__PURE__ */ new Set();
|
|
23
|
+
perfBuffer = [];
|
|
24
|
+
rafId = null;
|
|
25
|
+
rafCancel = null;
|
|
26
|
+
constructor(config) {
|
|
27
|
+
const init = config.init;
|
|
28
|
+
if (isInitFn(init)) {
|
|
29
|
+
this.initFn = init;
|
|
30
|
+
} else {
|
|
31
|
+
const seed = structuredClone(init);
|
|
32
|
+
this.initFn = () => structuredClone(seed);
|
|
33
|
+
}
|
|
34
|
+
this.stepFn = config.step;
|
|
35
|
+
this.shouldStopFn = config.shouldStop;
|
|
36
|
+
this.maxTime = config.maxTime;
|
|
37
|
+
this.delayMs = config.delayMs ?? 0;
|
|
38
|
+
this.ticksPerFrame = config.ticksPerFrame ?? 1;
|
|
39
|
+
this.params = config.initialParams ? { ...config.initialParams } : {};
|
|
40
|
+
this.data = this.initFn(this.params);
|
|
41
|
+
if (config.render) {
|
|
42
|
+
this.listeners.add(config.render);
|
|
43
|
+
config.render(this.getSnapshot());
|
|
44
|
+
}
|
|
45
|
+
const autoFrame = config.autoFrame ?? true;
|
|
46
|
+
if (autoFrame) {
|
|
47
|
+
const raf = globalThis.requestAnimationFrame;
|
|
48
|
+
const caf = globalThis.cancelAnimationFrame;
|
|
49
|
+
if (typeof raf === "function" && typeof caf === "function") {
|
|
50
|
+
const loop = (now) => {
|
|
51
|
+
this.handleAnimationFrame(now);
|
|
52
|
+
this.rafId = raf(loop);
|
|
53
|
+
};
|
|
54
|
+
this.rafCancel = caf;
|
|
55
|
+
this.rafId = raf(loop);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
getSnapshot() {
|
|
60
|
+
return {
|
|
61
|
+
data: this.data,
|
|
62
|
+
params: this.params,
|
|
63
|
+
tick: this.tick,
|
|
64
|
+
status: this.status,
|
|
65
|
+
stepDurationMs: this.lastStepMs
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
getStatus() {
|
|
69
|
+
return this.status;
|
|
70
|
+
}
|
|
71
|
+
getPerformance() {
|
|
72
|
+
return this.perfBuffer;
|
|
73
|
+
}
|
|
74
|
+
setDelayMs(ms) {
|
|
75
|
+
this.delayMs = ms;
|
|
76
|
+
}
|
|
77
|
+
setTicksPerFrame(n) {
|
|
78
|
+
this.ticksPerFrame = n;
|
|
79
|
+
}
|
|
80
|
+
recordDrawTime(tick, ms) {
|
|
81
|
+
for (let i = this.perfBuffer.length - 1; i >= 0; i--) {
|
|
82
|
+
if (this.perfBuffer[i].tick === tick) {
|
|
83
|
+
this.perfBuffer[i].drawMs = ms;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
subscribe(listener) {
|
|
89
|
+
this.listeners.add(listener);
|
|
90
|
+
return () => {
|
|
91
|
+
this.listeners.delete(listener);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
subscribeHistory(listener) {
|
|
95
|
+
this.historyListeners.add(listener);
|
|
96
|
+
return () => {
|
|
97
|
+
this.historyListeners.delete(listener);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
play() {
|
|
101
|
+
if (this.status === "stopped") return;
|
|
102
|
+
if (this.status === "playing") return;
|
|
103
|
+
this.status = "playing";
|
|
104
|
+
this.lastUpdateMs = null;
|
|
105
|
+
this.emit();
|
|
106
|
+
}
|
|
107
|
+
pause() {
|
|
108
|
+
if (this.status !== "playing") return;
|
|
109
|
+
this.status = "paused";
|
|
110
|
+
this.emit();
|
|
111
|
+
}
|
|
112
|
+
stop() {
|
|
113
|
+
if (this.status === "idle" || this.status === "stopped") return;
|
|
114
|
+
this.status = "stopped";
|
|
115
|
+
this.emit();
|
|
116
|
+
}
|
|
117
|
+
seek(targetTick) {
|
|
118
|
+
if (this.status === "stopped") return;
|
|
119
|
+
if (targetTick <= this.tick) {
|
|
120
|
+
if (this.status !== "paused") {
|
|
121
|
+
this.status = "paused";
|
|
122
|
+
this.emit();
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.status = "paused";
|
|
127
|
+
this.lastUpdateMs = null;
|
|
128
|
+
this.advanceTicks(targetTick - this.tick);
|
|
129
|
+
if (this.status === "paused") {
|
|
130
|
+
this.emit();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
advance(count = 1) {
|
|
134
|
+
if (this.status === "stopped") return;
|
|
135
|
+
if (this.status === "playing" || this.status === "idle") {
|
|
136
|
+
this.status = "paused";
|
|
137
|
+
}
|
|
138
|
+
this.lastUpdateMs = null;
|
|
139
|
+
this.advanceTicks(count);
|
|
140
|
+
if (this.status === "paused") {
|
|
141
|
+
this.emit();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
setParams(patch) {
|
|
145
|
+
this.params = { ...this.params, ...patch };
|
|
146
|
+
this.emit();
|
|
147
|
+
}
|
|
148
|
+
resetWith(patch) {
|
|
149
|
+
if (patch) {
|
|
150
|
+
this.params = { ...this.params, ...patch };
|
|
151
|
+
}
|
|
152
|
+
this.data = this.initFn(this.params);
|
|
153
|
+
this.tick = 0;
|
|
154
|
+
this.status = "idle";
|
|
155
|
+
this.lastUpdateMs = null;
|
|
156
|
+
this.lastStepMs = 0;
|
|
157
|
+
this.perfBuffer.length = 0;
|
|
158
|
+
this.emit();
|
|
159
|
+
}
|
|
160
|
+
handleAnimationFrame(nowMs) {
|
|
161
|
+
if (this.status !== "playing") return;
|
|
162
|
+
if (this.lastUpdateMs === null) {
|
|
163
|
+
this.lastUpdateMs = nowMs;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (this.delayMs > 0 && nowMs - this.lastUpdateMs < this.delayMs) return;
|
|
167
|
+
this.lastUpdateMs = nowMs;
|
|
168
|
+
this.advanceTicks(this.ticksPerFrame);
|
|
169
|
+
if (this.status === "playing") {
|
|
170
|
+
this.emit();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
destroy() {
|
|
174
|
+
if (this.rafId !== null && this.rafCancel) {
|
|
175
|
+
this.rafCancel(this.rafId);
|
|
176
|
+
}
|
|
177
|
+
this.rafId = null;
|
|
178
|
+
this.rafCancel = null;
|
|
179
|
+
this.listeners.clear();
|
|
180
|
+
this.historyListeners.clear();
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Run step up to `count` ticks. Returns true if all ticks completed
|
|
184
|
+
* without termination, false if stopped early.
|
|
185
|
+
*/
|
|
186
|
+
advanceTicks(count) {
|
|
187
|
+
for (let i = 0; i < count; i++) {
|
|
188
|
+
if (this.maxTime !== void 0 && this.tick >= this.maxTime) {
|
|
189
|
+
this.status = "stopped";
|
|
190
|
+
this.emit();
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
this.tick += 1;
|
|
194
|
+
const t0 = performance.now();
|
|
195
|
+
this.data = this.stepFn({
|
|
196
|
+
data: this.data,
|
|
197
|
+
params: this.params,
|
|
198
|
+
tick: this.tick,
|
|
199
|
+
status: this.status,
|
|
200
|
+
stepDurationMs: this.lastStepMs
|
|
201
|
+
});
|
|
202
|
+
const t1 = performance.now();
|
|
203
|
+
this.lastStepMs = t1 - t0;
|
|
204
|
+
if (this.perfBuffer.length >= PERF_BUFFER_SIZE) {
|
|
205
|
+
this.perfBuffer.shift();
|
|
206
|
+
}
|
|
207
|
+
this.perfBuffer.push({ tick: this.tick, stepMs: this.lastStepMs });
|
|
208
|
+
this.emitHistory();
|
|
209
|
+
if (this.shouldStopFn?.(this.data, this.params)) {
|
|
210
|
+
this.status = "stopped";
|
|
211
|
+
this.emit();
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
if (this.maxTime !== void 0 && this.tick >= this.maxTime) {
|
|
215
|
+
this.status = "stopped";
|
|
216
|
+
this.emit();
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
emit() {
|
|
223
|
+
const snap = this.getSnapshot();
|
|
224
|
+
for (const l of this.listeners) {
|
|
225
|
+
l(snap);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
emitHistory() {
|
|
229
|
+
if (this.historyListeners.size === 0) return;
|
|
230
|
+
const entry = { tick: this.tick, data: this.data };
|
|
231
|
+
for (const l of this.historyListeners) {
|
|
232
|
+
l(entry);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
function createEngine(config) {
|
|
237
|
+
return new SimulationEngine(config);
|
|
238
|
+
}
|
|
239
|
+
export {
|
|
240
|
+
SimulationEngine,
|
|
241
|
+
createEngine
|
|
242
|
+
};
|
|
@@ -72,16 +72,18 @@ self.onmessage = async (event) => {
|
|
|
72
72
|
import(msg.engineUrl),
|
|
73
73
|
]);
|
|
74
74
|
const sim = simMod.default;
|
|
75
|
+
// Merge sim.defaultParams under the patch sent from main \u2014 the main
|
|
76
|
+
// thread sees the sim module via a URL, so it can't apply defaults.
|
|
77
|
+
const initialParams = { ...(sim.defaultParams || {}), ...(msg.params || {}) };
|
|
75
78
|
engine = engineMod.createEngine({
|
|
76
79
|
init: sim.init,
|
|
77
80
|
step: sim.step,
|
|
78
81
|
shouldStop: sim.shouldStop,
|
|
79
|
-
initialParams
|
|
82
|
+
initialParams,
|
|
80
83
|
maxTime: msg.config.maxTime,
|
|
81
84
|
// Worker host owns its own setTimeout-driven loop; rAF wouldn't exist here anyway.
|
|
82
85
|
autoFrame: false,
|
|
83
86
|
});
|
|
84
|
-
postMessage({ kind: 'ready' });
|
|
85
87
|
emitSnapshot();
|
|
86
88
|
break;
|
|
87
89
|
}
|
|
@@ -113,6 +115,11 @@ self.onmessage = async (event) => {
|
|
|
113
115
|
if (!engine) return;
|
|
114
116
|
stopLoop(); engine.resetWith(msg.patch); emitSnapshot();
|
|
115
117
|
break;
|
|
118
|
+
case 'setConfig':
|
|
119
|
+
if (msg.patch.delayMs !== undefined) delayMs = msg.patch.delayMs;
|
|
120
|
+
if (msg.patch.ticksPerFrame !== undefined) ticksPerFrame = msg.patch.ticksPerFrame;
|
|
121
|
+
if (msg.patch.snapshotIntervalMs !== undefined) snapshotIntervalMs = msg.patch.snapshotIntervalMs;
|
|
122
|
+
break;
|
|
116
123
|
case 'destroy':
|
|
117
124
|
stopLoop();
|
|
118
125
|
if (engine) { engine.destroy(); engine = null; }
|
|
@@ -129,7 +136,7 @@ function createSimWorker(options) {
|
|
|
129
136
|
const blobUrl = URL.createObjectURL(blob);
|
|
130
137
|
const worker = new Worker(blobUrl, { type: "module" });
|
|
131
138
|
worker.addEventListener("message", function cleanup(event) {
|
|
132
|
-
if (event.data?.kind === "
|
|
139
|
+
if (event.data?.kind === "snapshot" || event.data?.kind === "error") {
|
|
133
140
|
URL.revokeObjectURL(blobUrl);
|
|
134
141
|
worker.removeEventListener("message", cleanup);
|
|
135
142
|
}
|