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
|
@@ -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
|
+
};
|
package/dist/state.cjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __copyProps = (to, from, except, desc) => {
|
|
7
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
8
|
+
for (let key of __getOwnPropNames(from))
|
|
9
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
10
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
11
|
+
}
|
|
12
|
+
return to;
|
|
13
|
+
};
|
|
14
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
15
|
+
|
|
16
|
+
// src/state.ts
|
|
17
|
+
var state_exports = {};
|
|
18
|
+
module.exports = __toCommonJS(state_exports);
|
package/dist/state.d.cts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The unified engine state shape — passed to `step`, returned by `getSnapshot`,
|
|
3
|
+
* and delivered to the `render` callback. All three are operations on the
|
|
4
|
+
* engine's inner state, so they share one type.
|
|
5
|
+
*/
|
|
6
|
+
type State<Data, Params> = {
|
|
7
|
+
data: Data;
|
|
8
|
+
params: Params;
|
|
9
|
+
tick: number;
|
|
10
|
+
status: SimulationStatus;
|
|
11
|
+
/** Duration of the previous step in ms. `0` before the first step has run. */
|
|
12
|
+
stepDurationMs: number;
|
|
13
|
+
};
|
|
14
|
+
type SimulationStatus = 'idle' | 'playing' | 'paused' | 'stopped';
|
|
15
|
+
|
|
16
|
+
export type { SimulationStatus, State };
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The unified engine state shape — passed to `step`, returned by `getSnapshot`,
|
|
3
|
+
* and delivered to the `render` callback. All three are operations on the
|
|
4
|
+
* engine's inner state, so they share one type.
|
|
5
|
+
*/
|
|
6
|
+
type State<Data, Params> = {
|
|
7
|
+
data: Data;
|
|
8
|
+
params: Params;
|
|
9
|
+
tick: number;
|
|
10
|
+
status: SimulationStatus;
|
|
11
|
+
/** Duration of the previous step in ms. `0` before the first step has run. */
|
|
12
|
+
stepDurationMs: number;
|
|
13
|
+
};
|
|
14
|
+
type SimulationStatus = 'idle' | 'playing' | 'paused' | 'stopped';
|
|
15
|
+
|
|
16
|
+
export type { SimulationStatus, State };
|
package/dist/state.js
ADDED
|
File without changes
|
|
@@ -72,14 +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,
|
|
84
|
+
// Worker host owns its own setTimeout-driven loop; rAF wouldn't exist here anyway.
|
|
85
|
+
autoFrame: false,
|
|
81
86
|
});
|
|
82
|
-
postMessage({ kind: 'ready' });
|
|
83
87
|
emitSnapshot();
|
|
84
88
|
break;
|
|
85
89
|
}
|
|
@@ -111,6 +115,11 @@ self.onmessage = async (event) => {
|
|
|
111
115
|
if (!engine) return;
|
|
112
116
|
stopLoop(); engine.resetWith(msg.patch); emitSnapshot();
|
|
113
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;
|
|
114
123
|
case 'destroy':
|
|
115
124
|
stopLoop();
|
|
116
125
|
if (engine) { engine.destroy(); engine = null; }
|
|
@@ -127,7 +136,7 @@ function createSimWorker(options) {
|
|
|
127
136
|
const blobUrl = URL.createObjectURL(blob);
|
|
128
137
|
const worker = new Worker(blobUrl, { type: "module" });
|
|
129
138
|
worker.addEventListener("message", function cleanup(event) {
|
|
130
|
-
if (event.data?.kind === "
|
|
139
|
+
if (event.data?.kind === "snapshot" || event.data?.kind === "error") {
|
|
131
140
|
URL.revokeObjectURL(blobUrl);
|
|
132
141
|
worker.removeEventListener("message", cleanup);
|
|
133
142
|
}
|
|
@@ -1,122 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
engine = engineMod.createEngine({
|
|
52
|
-
init: sim.init,
|
|
53
|
-
step: sim.step,
|
|
54
|
-
shouldStop: sim.shouldStop,
|
|
55
|
-
initialParams: msg.params,
|
|
56
|
-
maxTime: msg.config.maxTime,
|
|
57
|
-
});
|
|
58
|
-
postMessage({ kind: 'ready' });
|
|
59
|
-
emitSnapshot();
|
|
60
|
-
break;
|
|
61
|
-
}
|
|
62
|
-
case 'play':
|
|
63
|
-
if (!engine) return;
|
|
64
|
-
engine.play(); emitSnapshot(); stopLoop(); loopTimer = setTimeout(tickLoop, 0);
|
|
65
|
-
break;
|
|
66
|
-
case 'pause':
|
|
67
|
-
if (!engine) return;
|
|
68
|
-
stopLoop(); engine.pause(); emitSnapshot();
|
|
69
|
-
break;
|
|
70
|
-
case 'stop':
|
|
71
|
-
if (!engine) return;
|
|
72
|
-
stopLoop(); engine.stop(); emitSnapshot();
|
|
73
|
-
break;
|
|
74
|
-
case 'seek':
|
|
75
|
-
if (!engine) return;
|
|
76
|
-
stopLoop(); engine.seek(msg.tick); emitSnapshot();
|
|
77
|
-
break;
|
|
78
|
-
case 'advance':
|
|
79
|
-
if (!engine) return;
|
|
80
|
-
engine.advance(msg.count); emitSnapshot();
|
|
81
|
-
break;
|
|
82
|
-
case 'setParams':
|
|
83
|
-
if (!engine) return;
|
|
84
|
-
engine.setParams(msg.patch); emitSnapshot();
|
|
85
|
-
break;
|
|
86
|
-
case 'resetWith':
|
|
87
|
-
if (!engine) return;
|
|
88
|
-
stopLoop(); engine.resetWith(msg.patch); emitSnapshot();
|
|
89
|
-
break;
|
|
90
|
-
case 'destroy':
|
|
91
|
-
stopLoop();
|
|
92
|
-
if (engine) { engine.destroy(); engine = null; }
|
|
93
|
-
self.close();
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
96
|
-
} catch (err) {
|
|
97
|
-
postMessage({ kind: 'error', error: { message: err.message, stack: err.stack } });
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
`;
|
|
101
|
-
function createSimWorker(options) {
|
|
102
|
-
const blob = new Blob([WORKER_SCRIPT], { type: "text/javascript" });
|
|
103
|
-
const blobUrl = URL.createObjectURL(blob);
|
|
104
|
-
const worker = new Worker(blobUrl, { type: "module" });
|
|
105
|
-
worker.addEventListener("message", function cleanup(event) {
|
|
106
|
-
if (event.data?.kind === "ready" || event.data?.kind === "error") {
|
|
107
|
-
URL.revokeObjectURL(blobUrl);
|
|
108
|
-
worker.removeEventListener("message", cleanup);
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
worker.postMessage({
|
|
112
|
-
kind: "init",
|
|
113
|
-
moduleUrl: options.moduleUrl,
|
|
114
|
-
engineUrl: options.engineUrl,
|
|
115
|
-
params: options.initialParams,
|
|
116
|
-
config: options.config
|
|
117
|
-
});
|
|
118
|
-
return worker;
|
|
119
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
createSimWorker
|
|
3
|
+
} from "../chunk-VPS3ZXWI.js";
|
|
120
4
|
export {
|
|
121
5
|
createSimWorker
|
|
122
6
|
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import '../sim.cjs';
|
|
1
|
+
import { State } from '../state.cjs';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Wire protocol types for main ↔ worker communication.
|
|
@@ -32,21 +31,22 @@ type MainToWorkerMessage<Params> = {
|
|
|
32
31
|
} | {
|
|
33
32
|
kind: 'resetWith';
|
|
34
33
|
patch?: Partial<Params>;
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'setConfig';
|
|
36
|
+
patch: Partial<WorkerConfig>;
|
|
35
37
|
} | {
|
|
36
38
|
kind: 'destroy';
|
|
37
39
|
};
|
|
38
40
|
/** Messages sent from the worker to the main thread. */
|
|
39
41
|
type WorkerToMainMessage<Data, Params> = {
|
|
40
42
|
kind: 'snapshot';
|
|
41
|
-
snapshot:
|
|
43
|
+
snapshot: State<Data, Params>;
|
|
42
44
|
} | {
|
|
43
45
|
kind: 'error';
|
|
44
46
|
error: {
|
|
45
47
|
message: string;
|
|
46
48
|
stack?: string;
|
|
47
49
|
};
|
|
48
|
-
} | {
|
|
49
|
-
kind: 'ready';
|
|
50
50
|
};
|
|
51
51
|
/** Worker-specific configuration passed at init time. */
|
|
52
52
|
type WorkerConfig = {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import '../sim.js';
|
|
1
|
+
import { State } from '../state.js';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Wire protocol types for main ↔ worker communication.
|
|
@@ -32,21 +31,22 @@ type MainToWorkerMessage<Params> = {
|
|
|
32
31
|
} | {
|
|
33
32
|
kind: 'resetWith';
|
|
34
33
|
patch?: Partial<Params>;
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'setConfig';
|
|
36
|
+
patch: Partial<WorkerConfig>;
|
|
35
37
|
} | {
|
|
36
38
|
kind: 'destroy';
|
|
37
39
|
};
|
|
38
40
|
/** Messages sent from the worker to the main thread. */
|
|
39
41
|
type WorkerToMainMessage<Data, Params> = {
|
|
40
42
|
kind: 'snapshot';
|
|
41
|
-
snapshot:
|
|
43
|
+
snapshot: State<Data, Params>;
|
|
42
44
|
} | {
|
|
43
45
|
kind: 'error';
|
|
44
46
|
error: {
|
|
45
47
|
message: string;
|
|
46
48
|
stack?: string;
|
|
47
49
|
};
|
|
48
|
-
} | {
|
|
49
|
-
kind: 'ready';
|
|
50
50
|
};
|
|
51
51
|
/** Worker-specific configuration passed at init time. */
|
|
52
52
|
type WorkerConfig = {
|
|
@@ -33,6 +33,7 @@ function serializeMainMessage(msg) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// src/worker/workerRunner.ts
|
|
36
|
+
var PERF_BUFFER_SIZE = 120;
|
|
36
37
|
function createWorkerRunner(worker, config) {
|
|
37
38
|
const listeners = /* @__PURE__ */ new Set();
|
|
38
39
|
let currentSnapshot = {
|
|
@@ -42,7 +43,7 @@ function createWorkerRunner(worker, config) {
|
|
|
42
43
|
status: "idle",
|
|
43
44
|
stepDurationMs: 0
|
|
44
45
|
};
|
|
45
|
-
|
|
46
|
+
const perfBuffer = [];
|
|
46
47
|
function send(msg) {
|
|
47
48
|
worker.postMessage(serializeMainMessage(msg));
|
|
48
49
|
}
|
|
@@ -51,24 +52,28 @@ function createWorkerRunner(worker, config) {
|
|
|
51
52
|
l(currentSnapshot);
|
|
52
53
|
}
|
|
53
54
|
}
|
|
55
|
+
function pushPerf(snapshot) {
|
|
56
|
+
if (snapshot.tick <= 0) return;
|
|
57
|
+
const last = perfBuffer[perfBuffer.length - 1];
|
|
58
|
+
if (last && last.tick === snapshot.tick) return;
|
|
59
|
+
if (perfBuffer.length >= PERF_BUFFER_SIZE) perfBuffer.shift();
|
|
60
|
+
perfBuffer.push({ tick: snapshot.tick, stepMs: snapshot.stepDurationMs });
|
|
61
|
+
}
|
|
54
62
|
worker.onmessage = (event) => {
|
|
55
63
|
const msg = deserializeWorkerMessage(event.data);
|
|
56
64
|
switch (msg.kind) {
|
|
57
65
|
case "snapshot":
|
|
58
66
|
currentSnapshot = msg.snapshot;
|
|
67
|
+
pushPerf(msg.snapshot);
|
|
59
68
|
emit();
|
|
60
69
|
break;
|
|
61
70
|
case "error":
|
|
62
|
-
errorMessage = msg.error.message;
|
|
63
71
|
currentSnapshot = { ...currentSnapshot, status: "stopped" };
|
|
64
72
|
emit();
|
|
65
73
|
break;
|
|
66
|
-
case "ready":
|
|
67
|
-
break;
|
|
68
74
|
}
|
|
69
75
|
};
|
|
70
|
-
worker.onerror = (
|
|
71
|
-
errorMessage = event.message ?? "Unknown worker error";
|
|
76
|
+
worker.onerror = () => {
|
|
72
77
|
currentSnapshot = { ...currentSnapshot, status: "stopped" };
|
|
73
78
|
emit();
|
|
74
79
|
};
|
|
@@ -87,6 +92,16 @@ function createWorkerRunner(worker, config) {
|
|
|
87
92
|
advance: (count = 1) => send({ kind: "advance", count }),
|
|
88
93
|
setParams: (patch) => send({ kind: "setParams", patch }),
|
|
89
94
|
resetWith: (patch) => send({ kind: "resetWith", patch }),
|
|
95
|
+
setConfig: (patch) => send({ kind: "setConfig", patch }),
|
|
96
|
+
recordDrawTime(tick, ms) {
|
|
97
|
+
for (let i = perfBuffer.length - 1; i >= 0; i--) {
|
|
98
|
+
if (perfBuffer[i].tick === tick) {
|
|
99
|
+
perfBuffer[i].drawMs = ms;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
getPerformance: () => perfBuffer,
|
|
90
105
|
destroy() {
|
|
91
106
|
listeners.clear();
|
|
92
107
|
send({ kind: "destroy" });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TickPerformance } from '../engine.cjs';
|
|
2
|
+
import { State } from '../state.cjs';
|
|
2
3
|
import { WorkerConfig } from './protocol.cjs';
|
|
3
4
|
import '../sim.cjs';
|
|
4
5
|
|
|
@@ -14,8 +15,8 @@ type WorkerRunnerConfig<Params> = {
|
|
|
14
15
|
config: WorkerConfig;
|
|
15
16
|
};
|
|
16
17
|
type WorkerRunner<Data, Params> = {
|
|
17
|
-
getSnapshot: () =>
|
|
18
|
-
subscribe: (listener: (snapshot:
|
|
18
|
+
getSnapshot: () => State<Data, Params>;
|
|
19
|
+
subscribe: (listener: (snapshot: State<Data, Params>) => void) => () => void;
|
|
19
20
|
play: () => void;
|
|
20
21
|
pause: () => void;
|
|
21
22
|
stop: () => void;
|
|
@@ -23,6 +24,13 @@ type WorkerRunner<Data, Params> = {
|
|
|
23
24
|
advance: (count?: number) => void;
|
|
24
25
|
setParams: (patch: Partial<Params>) => void;
|
|
25
26
|
resetWith: (patch?: Partial<Params>) => void;
|
|
27
|
+
setConfig: (patch: Partial<WorkerConfig>) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Record a draw time for a tick. Draws happen on the main thread, so
|
|
30
|
+
* timings are tracked here — they never cross the postMessage boundary.
|
|
31
|
+
*/
|
|
32
|
+
recordDrawTime: (tick: number, ms: number) => void;
|
|
33
|
+
getPerformance: () => readonly TickPerformance[];
|
|
26
34
|
destroy: () => void;
|
|
27
35
|
};
|
|
28
36
|
declare function createWorkerRunner<Data, Params>(worker: Worker, config: WorkerRunnerConfig<Params>): WorkerRunner<Data, Params>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TickPerformance } from '../engine.js';
|
|
2
|
+
import { State } from '../state.js';
|
|
2
3
|
import { WorkerConfig } from './protocol.js';
|
|
3
4
|
import '../sim.js';
|
|
4
5
|
|
|
@@ -14,8 +15,8 @@ type WorkerRunnerConfig<Params> = {
|
|
|
14
15
|
config: WorkerConfig;
|
|
15
16
|
};
|
|
16
17
|
type WorkerRunner<Data, Params> = {
|
|
17
|
-
getSnapshot: () =>
|
|
18
|
-
subscribe: (listener: (snapshot:
|
|
18
|
+
getSnapshot: () => State<Data, Params>;
|
|
19
|
+
subscribe: (listener: (snapshot: State<Data, Params>) => void) => () => void;
|
|
19
20
|
play: () => void;
|
|
20
21
|
pause: () => void;
|
|
21
22
|
stop: () => void;
|
|
@@ -23,6 +24,13 @@ type WorkerRunner<Data, Params> = {
|
|
|
23
24
|
advance: (count?: number) => void;
|
|
24
25
|
setParams: (patch: Partial<Params>) => void;
|
|
25
26
|
resetWith: (patch?: Partial<Params>) => void;
|
|
27
|
+
setConfig: (patch: Partial<WorkerConfig>) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Record a draw time for a tick. Draws happen on the main thread, so
|
|
30
|
+
* timings are tracked here — they never cross the postMessage boundary.
|
|
31
|
+
*/
|
|
32
|
+
recordDrawTime: (tick: number, ms: number) => void;
|
|
33
|
+
getPerformance: () => readonly TickPerformance[];
|
|
26
34
|
destroy: () => void;
|
|
27
35
|
};
|
|
28
36
|
declare function createWorkerRunner<Data, Params>(worker: Worker, config: WorkerRunnerConfig<Params>): WorkerRunner<Data, Params>;
|