automatick 0.0.1 → 0.0.2

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.
Files changed (40) hide show
  1. package/README.md +2 -0
  2. package/dist/chunk-IKR53C2U.js +12 -0
  3. package/dist/{chunk-YNLOTPDY.js → chunk-UU5V7HGM.js} +38 -2
  4. package/dist/engine.cjs +41 -2
  5. package/dist/engine.d.cts +34 -14
  6. package/dist/engine.d.ts +34 -14
  7. package/dist/engine.js +2 -1
  8. package/dist/react/EngineContext.d.cts +4 -3
  9. package/dist/react/EngineContext.d.ts +4 -3
  10. package/dist/react/Simulation.cjs +49 -5
  11. package/dist/react/Simulation.d.cts +1 -0
  12. package/dist/react/Simulation.d.ts +1 -0
  13. package/dist/react/Simulation.js +12 -4
  14. package/dist/react/SimulationContext.d.cts +1 -2
  15. package/dist/react/SimulationContext.d.ts +1 -2
  16. package/dist/react/hooks.d.cts +1 -1
  17. package/dist/react/hooks.d.ts +1 -1
  18. package/dist/react/useSimulationCanvas.cjs +2 -10
  19. package/dist/react/useSimulationCanvas.d.cts +4 -7
  20. package/dist/react/useSimulationCanvas.d.ts +4 -7
  21. package/dist/react/useSimulationCanvas.js +2 -10
  22. package/dist/sim.cjs +7 -2
  23. package/dist/sim.d.cts +26 -10
  24. package/dist/sim.d.ts +26 -10
  25. package/dist/sim.js +6 -5
  26. package/dist/state.cjs +18 -0
  27. package/dist/state.d.cts +16 -0
  28. package/dist/state.d.ts +16 -0
  29. package/dist/state.js +0 -0
  30. package/dist/worker/createSimWorker.cjs +2 -0
  31. package/dist/worker/createSimWorker.d.cts +1 -2
  32. package/dist/worker/createSimWorker.d.ts +1 -2
  33. package/dist/worker/createSimWorker.js +2 -0
  34. package/dist/worker/protocol.d.cts +2 -3
  35. package/dist/worker/protocol.d.ts +2 -3
  36. package/dist/worker/workerRunner.cjs +1 -4
  37. package/dist/worker/workerRunner.d.cts +3 -4
  38. package/dist/worker/workerRunner.d.ts +3 -4
  39. package/dist/worker/workerRunner.js +1 -4
  40. 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
@@ -0,0 +1,12 @@
1
+ // src/sim.ts
2
+ function isInitFn(init) {
3
+ return typeof init === "function";
4
+ }
5
+ function defineSim(sim) {
6
+ return sim;
7
+ }
8
+
9
+ export {
10
+ isInitFn,
11
+ defineSim
12
+ };
@@ -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,8 +20,16 @@ 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
- this.initFn = config.init;
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;
@@ -25,6 +37,23 @@ var SimulationEngine = class {
25
37
  this.ticksPerFrame = config.ticksPerFrame ?? 1;
26
38
  this.params = { ...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;
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,8 +48,16 @@ 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
- this.initFn = config.init;
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;
@@ -50,6 +65,23 @@ var SimulationEngine = class {
50
65
  this.ticksPerFrame = config.ticksPerFrame ?? 1;
51
66
  this.params = { ...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,26 +1,44 @@
1
- import { StepArgs } from './sim.cjs';
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
9
  type EngineConfig<Data, Params> = {
10
- init: (params: Params) => Data;
11
- step: (args: StepArgs<Data, Params>) => Data;
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
  initialParams: Params;
14
18
  maxTime?: number;
15
19
  delayMs?: number;
16
20
  ticksPerFrame?: number;
17
- };
18
- type EngineSnapshot<Data, Params> = {
19
- data: Data;
20
- params: Params;
21
- tick: number;
22
- status: SimulationStatus;
23
- stepDurationMs: number;
21
+ /**
22
+ * Optional render callback — sugar for the vanilla path. When provided, the
23
+ * engine calls it once with the initial state (right after init) and on
24
+ * every state emit thereafter, equivalent to `engine.subscribe(render)`
25
+ * followed by an initial paint. `subscribe` remains the lower-level
26
+ * primitive; React adapter and worker callers wire their own subscribers.
27
+ */
28
+ render?: (snapshot: State<Data, Params>) => void;
29
+ /**
30
+ * Drive `handleAnimationFrame` from an internal `requestAnimationFrame` loop.
31
+ * Defaults to `true` so vanilla callers don't have to write the loop. The
32
+ * React adapter and worker host pass `false` because they either own the
33
+ * frame loop themselves or run in an environment that has none. The loop is
34
+ * created at construction and torn down by `destroy()`. If
35
+ * `globalThis.requestAnimationFrame` is missing (server, worker), the option
36
+ * is silently a no-op.
37
+ *
38
+ * Note: when `true`, the rAF closure pins this engine in memory — vanilla
39
+ * consumers must call `destroy()` to let it be garbage-collected.
40
+ */
41
+ autoFrame?: boolean;
24
42
  };
25
43
  declare class SimulationEngine<Data, Params> {
26
44
  private data;
@@ -38,14 +56,16 @@ declare class SimulationEngine<Data, Params> {
38
56
  private readonly listeners;
39
57
  private readonly historyListeners;
40
58
  private readonly perfBuffer;
59
+ private rafId;
60
+ private rafCancel;
41
61
  constructor(config: EngineConfig<Data, Params>);
42
- getSnapshot(): EngineSnapshot<Data, Params>;
62
+ getSnapshot(): State<Data, Params>;
43
63
  getStatus(): SimulationStatus;
44
64
  getPerformance(): readonly TickPerformance[];
45
65
  setDelayMs(ms: number): void;
46
66
  setTicksPerFrame(n: number): void;
47
67
  recordDrawTime(tick: number, ms: number): void;
48
- subscribe(listener: (snapshot: EngineSnapshot<Data, Params>) => void): () => void;
68
+ subscribe(listener: (snapshot: State<Data, Params>) => void): () => void;
49
69
  subscribeHistory(listener: (entry: {
50
70
  tick: number;
51
71
  data: Data;
@@ -69,4 +89,4 @@ declare class SimulationEngine<Data, Params> {
69
89
  }
70
90
  declare function createEngine<Data, Params>(config: EngineConfig<Data, Params>): SimulationEngine<Data, Params>;
71
91
 
72
- export { type EngineConfig, type EngineSnapshot, SimulationEngine, type SimulationStatus, type TickPerformance, createEngine };
92
+ export { type EngineConfig, SimulationEngine, SimulationStatus, State, type TickPerformance, createEngine };
package/dist/engine.d.ts CHANGED
@@ -1,26 +1,44 @@
1
- import { StepArgs } from './sim.js';
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
9
  type EngineConfig<Data, Params> = {
10
- init: (params: Params) => Data;
11
- step: (args: StepArgs<Data, Params>) => Data;
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
  initialParams: Params;
14
18
  maxTime?: number;
15
19
  delayMs?: number;
16
20
  ticksPerFrame?: number;
17
- };
18
- type EngineSnapshot<Data, Params> = {
19
- data: Data;
20
- params: Params;
21
- tick: number;
22
- status: SimulationStatus;
23
- stepDurationMs: number;
21
+ /**
22
+ * Optional render callback — sugar for the vanilla path. When provided, the
23
+ * engine calls it once with the initial state (right after init) and on
24
+ * every state emit thereafter, equivalent to `engine.subscribe(render)`
25
+ * followed by an initial paint. `subscribe` remains the lower-level
26
+ * primitive; React adapter and worker callers wire their own subscribers.
27
+ */
28
+ render?: (snapshot: State<Data, Params>) => void;
29
+ /**
30
+ * Drive `handleAnimationFrame` from an internal `requestAnimationFrame` loop.
31
+ * Defaults to `true` so vanilla callers don't have to write the loop. The
32
+ * React adapter and worker host pass `false` because they either own the
33
+ * frame loop themselves or run in an environment that has none. The loop is
34
+ * created at construction and torn down by `destroy()`. If
35
+ * `globalThis.requestAnimationFrame` is missing (server, worker), the option
36
+ * is silently a no-op.
37
+ *
38
+ * Note: when `true`, the rAF closure pins this engine in memory — vanilla
39
+ * consumers must call `destroy()` to let it be garbage-collected.
40
+ */
41
+ autoFrame?: boolean;
24
42
  };
25
43
  declare class SimulationEngine<Data, Params> {
26
44
  private data;
@@ -38,14 +56,16 @@ declare class SimulationEngine<Data, Params> {
38
56
  private readonly listeners;
39
57
  private readonly historyListeners;
40
58
  private readonly perfBuffer;
59
+ private rafId;
60
+ private rafCancel;
41
61
  constructor(config: EngineConfig<Data, Params>);
42
- getSnapshot(): EngineSnapshot<Data, Params>;
62
+ getSnapshot(): State<Data, Params>;
43
63
  getStatus(): SimulationStatus;
44
64
  getPerformance(): readonly TickPerformance[];
45
65
  setDelayMs(ms: number): void;
46
66
  setTicksPerFrame(n: number): void;
47
67
  recordDrawTime(tick: number, ms: number): void;
48
- subscribe(listener: (snapshot: EngineSnapshot<Data, Params>) => void): () => void;
68
+ subscribe(listener: (snapshot: State<Data, Params>) => void): () => void;
49
69
  subscribeHistory(listener: (entry: {
50
70
  tick: number;
51
71
  data: Data;
@@ -69,4 +89,4 @@ declare class SimulationEngine<Data, Params> {
69
89
  }
70
90
  declare function createEngine<Data, Params>(config: EngineConfig<Data, Params>): SimulationEngine<Data, Params>;
71
91
 
72
- export { type EngineConfig, type EngineSnapshot, SimulationEngine, type SimulationStatus, type TickPerformance, createEngine };
92
+ export { type EngineConfig, SimulationEngine, SimulationStatus, State, type TickPerformance, createEngine };
package/dist/engine.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  SimulationEngine,
3
3
  createEngine
4
- } from "./chunk-YNLOTPDY.js";
4
+ } from "./chunk-UU5V7HGM.js";
5
+ import "./chunk-IKR53C2U.js";
5
6
  export {
6
7
  SimulationEngine,
7
8
  createEngine
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
- import { EngineSnapshot, TickPerformance } from '../engine.cjs';
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: EngineSnapshot<unknown, unknown>) => void) => () => void;
12
- getSnapshot: () => EngineSnapshot<unknown, unknown>;
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 { EngineSnapshot, TickPerformance } from '../engine.js';
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: EngineSnapshot<unknown, unknown>) => void) => () => void;
12
- getSnapshot: () => EngineSnapshot<unknown, unknown>;
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
  };
@@ -35,6 +35,11 @@ __export(Simulation_exports, {
35
35
  module.exports = __toCommonJS(Simulation_exports);
36
36
  var import_react4 = __toESM(require("react"), 1);
37
37
 
38
+ // src/sim.ts
39
+ function isInitFn(init) {
40
+ return typeof init === "function";
41
+ }
42
+
38
43
  // src/engine.ts
39
44
  var PERF_BUFFER_SIZE = 120;
40
45
  var SimulationEngine = class {
@@ -53,8 +58,16 @@ var SimulationEngine = class {
53
58
  listeners = /* @__PURE__ */ new Set();
54
59
  historyListeners = /* @__PURE__ */ new Set();
55
60
  perfBuffer = [];
61
+ rafId = null;
62
+ rafCancel = null;
56
63
  constructor(config) {
57
- this.initFn = config.init;
64
+ const init = config.init;
65
+ if (isInitFn(init)) {
66
+ this.initFn = init;
67
+ } else {
68
+ const seed = structuredClone(init);
69
+ this.initFn = () => structuredClone(seed);
70
+ }
58
71
  this.stepFn = config.step;
59
72
  this.shouldStopFn = config.shouldStop;
60
73
  this.maxTime = config.maxTime;
@@ -62,6 +75,23 @@ var SimulationEngine = class {
62
75
  this.ticksPerFrame = config.ticksPerFrame ?? 1;
63
76
  this.params = { ...config.initialParams };
64
77
  this.data = this.initFn(this.params);
78
+ if (config.render) {
79
+ this.listeners.add(config.render);
80
+ config.render(this.getSnapshot());
81
+ }
82
+ const autoFrame = config.autoFrame ?? true;
83
+ if (autoFrame) {
84
+ const raf = globalThis.requestAnimationFrame;
85
+ const caf = globalThis.cancelAnimationFrame;
86
+ if (typeof raf === "function" && typeof caf === "function") {
87
+ const loop = (now) => {
88
+ this.handleAnimationFrame(now);
89
+ this.rafId = raf(loop);
90
+ };
91
+ this.rafCancel = caf;
92
+ this.rafId = raf(loop);
93
+ }
94
+ }
65
95
  }
66
96
  getSnapshot() {
67
97
  return {
@@ -178,6 +208,11 @@ var SimulationEngine = class {
178
208
  }
179
209
  }
180
210
  destroy() {
211
+ if (this.rafId !== null && this.rafCancel) {
212
+ this.rafCancel(this.rafId);
213
+ }
214
+ this.rafId = null;
215
+ this.rafCancel = null;
181
216
  this.listeners.clear();
182
217
  this.historyListeners.clear();
183
218
  }
@@ -197,7 +232,9 @@ var SimulationEngine = class {
197
232
  this.data = this.stepFn({
198
233
  data: this.data,
199
234
  params: this.params,
200
- tick: this.tick
235
+ tick: this.tick,
236
+ status: this.status,
237
+ stepDurationMs: this.lastStepMs
201
238
  });
202
239
  const t1 = performance.now();
203
240
  this.lastStepMs = t1 - t0;
@@ -271,7 +308,9 @@ function LocalSimulation(props) {
271
308
  initialParams,
272
309
  maxTime: props.maxTime,
273
310
  delayMs: props.delayMs,
274
- ticksPerFrame: props.ticksPerFrame
311
+ ticksPerFrame: props.ticksPerFrame,
312
+ // The React adapter drives its own rAF loop tied to component lifecycle.
313
+ autoFrame: false
275
314
  });
276
315
  }
277
316
  const engine = engineRef.current;
@@ -312,7 +351,9 @@ function WorkerSimulation(props) {
312
351
  const [backend, setBackend] = import_react4.default.useState(
313
352
  null
314
353
  );
315
- const [snapshot, setSnapshot] = import_react4.default.useState(null);
354
+ const [snapshot, setSnapshot] = import_react4.default.useState(
355
+ null
356
+ );
316
357
  const propsRef = import_react4.default.useRef(props);
317
358
  propsRef.current = props;
318
359
  import_react4.default.useEffect(() => {
@@ -329,7 +370,8 @@ function WorkerSimulation(props) {
329
370
  step: simModule.step,
330
371
  shouldStop: simModule.shouldStop,
331
372
  initialParams,
332
- maxTime: p.maxTime
373
+ maxTime: p.maxTime,
374
+ autoFrame: false
333
375
  });
334
376
  if (cancelled) {
335
377
  engine.destroy();
@@ -452,6 +494,8 @@ function SimulationProvider({
452
494
  );
453
495
  const engineValue = import_react4.default.useMemo(
454
496
  () => ({
497
+ // TODO(#14): casts here bridge EngineContext's non-generic shape to
498
+ // the typed Backend<Data, Params>. Make EngineContext generic to drop them.
455
499
  subscribe: (listener) => backend.subscribe(
456
500
  listener
457
501
  ),
@@ -1,6 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import React from 'react';
3
3
  import { SimModule } from '../sim.cjs';
4
+ import '../state.cjs';
4
5
 
5
6
  type SimulationPropsLocal<Data, Params> = {
6
7
  sim: SimModule<Data, Params>;
@@ -1,6 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import React from 'react';
3
3
  import { SimModule } from '../sim.js';
4
+ import '../state.js';
4
5
 
5
6
  type SimulationPropsLocal<Data, Params> = {
6
7
  sim: SimModule<Data, Params>;
@@ -3,7 +3,8 @@ import {
3
3
  } from "../chunk-SK5SHIWY.js";
4
4
  import {
5
5
  createEngine
6
- } from "../chunk-YNLOTPDY.js";
6
+ } from "../chunk-UU5V7HGM.js";
7
+ import "../chunk-IKR53C2U.js";
7
8
  import {
8
9
  EngineContext
9
10
  } from "../chunk-OA6FGXTP.js";
@@ -26,7 +27,9 @@ function LocalSimulation(props) {
26
27
  initialParams,
27
28
  maxTime: props.maxTime,
28
29
  delayMs: props.delayMs,
29
- ticksPerFrame: props.ticksPerFrame
30
+ ticksPerFrame: props.ticksPerFrame,
31
+ // The React adapter drives its own rAF loop tied to component lifecycle.
32
+ autoFrame: false
30
33
  });
31
34
  }
32
35
  const engine = engineRef.current;
@@ -67,7 +70,9 @@ function WorkerSimulation(props) {
67
70
  const [backend, setBackend] = React.useState(
68
71
  null
69
72
  );
70
- const [snapshot, setSnapshot] = React.useState(null);
73
+ const [snapshot, setSnapshot] = React.useState(
74
+ null
75
+ );
71
76
  const propsRef = React.useRef(props);
72
77
  propsRef.current = props;
73
78
  React.useEffect(() => {
@@ -84,7 +89,8 @@ function WorkerSimulation(props) {
84
89
  step: simModule.step,
85
90
  shouldStop: simModule.shouldStop,
86
91
  initialParams,
87
- maxTime: p.maxTime
92
+ maxTime: p.maxTime,
93
+ autoFrame: false
88
94
  });
89
95
  if (cancelled) {
90
96
  engine.destroy();
@@ -207,6 +213,8 @@ function SimulationProvider({
207
213
  );
208
214
  const engineValue = React.useMemo(
209
215
  () => ({
216
+ // TODO(#14): casts here bridge EngineContext's non-generic shape to
217
+ // the typed Backend<Data, Params>. Make EngineContext generic to drop them.
210
218
  subscribe: (listener) => backend.subscribe(
211
219
  listener
212
220
  ),
@@ -1,6 +1,5 @@
1
1
  import React from 'react';
2
- import { SimulationStatus } from '../engine.cjs';
3
- import '../sim.cjs';
2
+ import { SimulationStatus } from '../state.cjs';
4
3
 
5
4
  type SimulationContextValue<Data, Params> = {
6
5
  data: Data;
@@ -1,6 +1,5 @@
1
1
  import React from 'react';
2
- import { SimulationStatus } from '../engine.js';
3
- import '../sim.js';
2
+ import { SimulationStatus } from '../state.js';
4
3
 
5
4
  type SimulationContextValue<Data, Params> = {
6
5
  data: Data;
@@ -1,7 +1,7 @@
1
1
  import { SimulationContextValue } from './SimulationContext.cjs';
2
2
  import { SimModule } from '../sim.cjs';
3
3
  import 'react';
4
- import '../engine.cjs';
4
+ import '../state.cjs';
5
5
 
6
6
  /**
7
7
  * Helper type: extract Data and Params from a SimModule type, or use them directly.
@@ -1,7 +1,7 @@
1
1
  import { SimulationContextValue } from './SimulationContext.js';
2
2
  import { SimModule } from '../sim.js';
3
3
  import 'react';
4
- import '../engine.js';
4
+ import '../state.js';
5
5
 
6
6
  /**
7
7
  * Helper type: extract Data and Params from a SimModule type, or use them directly.
@@ -59,11 +59,7 @@ function useSimulationCanvas(draw) {
59
59
  if (ctx) {
60
60
  const snap = engineCtx.getSnapshot();
61
61
  const t0 = performance.now();
62
- drawRef.current(ctx, {
63
- data: snap.data,
64
- params: snap.params,
65
- tick: snap.tick
66
- });
62
+ drawRef.current(ctx, snap);
67
63
  engineCtx.recordDrawTime(snap.tick, performance.now() - t0);
68
64
  }
69
65
  }
@@ -74,11 +70,7 @@ function useSimulationCanvas(draw) {
74
70
  const ctx = canvas2.getContext("2d");
75
71
  if (!ctx) return;
76
72
  const t0 = performance.now();
77
- drawRef.current(ctx, {
78
- data: snap.data,
79
- params: snap.params,
80
- tick: snap.tick
81
- });
73
+ drawRef.current(ctx, snap);
82
74
  engineCtx.recordDrawTime(snap.tick, performance.now() - t0);
83
75
  }
84
76
  );
@@ -1,18 +1,15 @@
1
1
  import React from 'react';
2
2
  import { SimModule } from '../sim.cjs';
3
+ import { State } from '../state.cjs';
3
4
 
4
5
  /**
5
6
  * Draw callback signature for useSimulationCanvas.
6
- * Called imperatively on every engine snapshot — no React re-render involved.
7
+ * Called imperatively on every engine state emit — no React re-render involved.
7
8
  */
8
- type CanvasDrawFn<Data, Params> = (ctx: CanvasRenderingContext2D, snapshot: {
9
- data: Data;
10
- params: Params;
11
- tick: number;
12
- }) => void;
9
+ type CanvasDrawFn<Data, Params> = (ctx: CanvasRenderingContext2D, snapshot: State<Data, Params>) => void;
13
10
  type InferDraw<T, FallbackParams> = T extends SimModule<infer D, infer P> ? CanvasDrawFn<D, P> : CanvasDrawFn<T, FallbackParams>;
14
11
  /**
15
- * Subscribe to simulation snapshots and draw to a canvas without React re-renders.
12
+ * Subscribe to simulation state and draw to a canvas without React re-renders.
16
13
  *
17
14
  * Returns a ref to attach to a `<canvas>` element. The `draw` callback is called
18
15
  * directly from the engine's subscription — it bypasses React's state/reconciliation
@@ -1,18 +1,15 @@
1
1
  import React from 'react';
2
2
  import { SimModule } from '../sim.js';
3
+ import { State } from '../state.js';
3
4
 
4
5
  /**
5
6
  * Draw callback signature for useSimulationCanvas.
6
- * Called imperatively on every engine snapshot — no React re-render involved.
7
+ * Called imperatively on every engine state emit — no React re-render involved.
7
8
  */
8
- type CanvasDrawFn<Data, Params> = (ctx: CanvasRenderingContext2D, snapshot: {
9
- data: Data;
10
- params: Params;
11
- tick: number;
12
- }) => void;
9
+ type CanvasDrawFn<Data, Params> = (ctx: CanvasRenderingContext2D, snapshot: State<Data, Params>) => void;
13
10
  type InferDraw<T, FallbackParams> = T extends SimModule<infer D, infer P> ? CanvasDrawFn<D, P> : CanvasDrawFn<T, FallbackParams>;
14
11
  /**
15
- * Subscribe to simulation snapshots and draw to a canvas without React re-renders.
12
+ * Subscribe to simulation state and draw to a canvas without React re-renders.
16
13
  *
17
14
  * Returns a ref to attach to a `<canvas>` element. The `draw` callback is called
18
15
  * directly from the engine's subscription — it bypasses React's state/reconciliation
@@ -21,11 +21,7 @@ function useSimulationCanvas(draw) {
21
21
  if (ctx) {
22
22
  const snap = engineCtx.getSnapshot();
23
23
  const t0 = performance.now();
24
- drawRef.current(ctx, {
25
- data: snap.data,
26
- params: snap.params,
27
- tick: snap.tick
28
- });
24
+ drawRef.current(ctx, snap);
29
25
  engineCtx.recordDrawTime(snap.tick, performance.now() - t0);
30
26
  }
31
27
  }
@@ -36,11 +32,7 @@ function useSimulationCanvas(draw) {
36
32
  const ctx = canvas2.getContext("2d");
37
33
  if (!ctx) return;
38
34
  const t0 = performance.now();
39
- drawRef.current(ctx, {
40
- data: snap.data,
41
- params: snap.params,
42
- tick: snap.tick
43
- });
35
+ drawRef.current(ctx, snap);
44
36
  engineCtx.recordDrawTime(snap.tick, performance.now() - t0);
45
37
  }
46
38
  );
package/dist/sim.cjs CHANGED
@@ -20,13 +20,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/sim.ts
21
21
  var sim_exports = {};
22
22
  __export(sim_exports, {
23
- defineSim: () => defineSim
23
+ defineSim: () => defineSim,
24
+ isInitFn: () => isInitFn
24
25
  });
25
26
  module.exports = __toCommonJS(sim_exports);
27
+ function isInitFn(init) {
28
+ return typeof init === "function";
29
+ }
26
30
  function defineSim(sim) {
27
31
  return sim;
28
32
  }
29
33
  // Annotate the CommonJS export names for ESM import in node:
30
34
  0 && (module.exports = {
31
- defineSim
35
+ defineSim,
36
+ isInitFn
32
37
  });
package/dist/sim.d.cts CHANGED
@@ -1,15 +1,31 @@
1
- /** Arguments passed to the `step` function on each tick. */
2
- type StepArgs<Data, Params> = {
3
- data: Data;
4
- params: Params;
5
- tick: number;
6
- };
1
+ import { State } from './state.cjs';
2
+
3
+ /**
4
+ * Initial state for a simulation. Either a value of type `Data`, or a function
5
+ * `(params) => Data`. The function form is required when the initial state
6
+ * depends on params; otherwise pass the value directly.
7
+ *
8
+ * `Data` itself must not be a function — it has to be structured-cloneable so
9
+ * it can cross the worker boundary and be safely re-emitted on resetWith.
10
+ * That's what makes this union unambiguous: a `function` value is always the
11
+ * `(params) => Data` branch.
12
+ */
13
+ type SimInit<Data, Params> = ((params: Params) => Data) | Data;
14
+ /**
15
+ * Type guard that narrows a `SimInit` to its function branch. Defined as a
16
+ * user-typed predicate so we can do the narrowing without an `as` cast.
17
+ */
18
+ declare function isInitFn<Data, Params>(init: SimInit<Data, Params>): init is (params: Params) => Data;
7
19
  /** A simulation module: the pure business logic that automatick drives. */
8
20
  type SimModule<Data, Params> = {
9
- /** Create initial simulation state from params. Called on engine creation and on resetWith(). */
10
- init: (params: Params) => Data;
21
+ /**
22
+ * Initial simulation state — value or `(params) => Data`. When a value is
23
+ * passed, the engine takes a fresh `structuredClone` on every (re)init so
24
+ * mutations inside `step` never leak across resets.
25
+ */
26
+ init: SimInit<Data, Params>;
11
27
  /** Advance the simulation by one tick. Must be pure and synchronous. */
12
- step: (args: StepArgs<Data, Params>) => Data;
28
+ step: (state: State<Data, Params>) => Data;
13
29
  /** Optional termination predicate. Checked after each step. If it returns true, the simulation stops. */
14
30
  shouldStop?: (data: Data, params: Params) => boolean;
15
31
  /** Default parameter values. Used at engine creation if no params override is provided. */
@@ -37,4 +53,4 @@ type SimData<M> = M extends SimModule<infer D, infer _P> ? D : never;
37
53
  /** Extract the Params type from a SimModule. */
38
54
  type SimParams<M> = M extends SimModule<infer _D, infer P> ? P : never;
39
55
 
40
- export { type SimData, type SimModule, type SimParams, type StepArgs, defineSim };
56
+ export { type SimData, type SimInit, type SimModule, type SimParams, defineSim, isInitFn };
package/dist/sim.d.ts CHANGED
@@ -1,15 +1,31 @@
1
- /** Arguments passed to the `step` function on each tick. */
2
- type StepArgs<Data, Params> = {
3
- data: Data;
4
- params: Params;
5
- tick: number;
6
- };
1
+ import { State } from './state.js';
2
+
3
+ /**
4
+ * Initial state for a simulation. Either a value of type `Data`, or a function
5
+ * `(params) => Data`. The function form is required when the initial state
6
+ * depends on params; otherwise pass the value directly.
7
+ *
8
+ * `Data` itself must not be a function — it has to be structured-cloneable so
9
+ * it can cross the worker boundary and be safely re-emitted on resetWith.
10
+ * That's what makes this union unambiguous: a `function` value is always the
11
+ * `(params) => Data` branch.
12
+ */
13
+ type SimInit<Data, Params> = ((params: Params) => Data) | Data;
14
+ /**
15
+ * Type guard that narrows a `SimInit` to its function branch. Defined as a
16
+ * user-typed predicate so we can do the narrowing without an `as` cast.
17
+ */
18
+ declare function isInitFn<Data, Params>(init: SimInit<Data, Params>): init is (params: Params) => Data;
7
19
  /** A simulation module: the pure business logic that automatick drives. */
8
20
  type SimModule<Data, Params> = {
9
- /** Create initial simulation state from params. Called on engine creation and on resetWith(). */
10
- init: (params: Params) => Data;
21
+ /**
22
+ * Initial simulation state — value or `(params) => Data`. When a value is
23
+ * passed, the engine takes a fresh `structuredClone` on every (re)init so
24
+ * mutations inside `step` never leak across resets.
25
+ */
26
+ init: SimInit<Data, Params>;
11
27
  /** Advance the simulation by one tick. Must be pure and synchronous. */
12
- step: (args: StepArgs<Data, Params>) => Data;
28
+ step: (state: State<Data, Params>) => Data;
13
29
  /** Optional termination predicate. Checked after each step. If it returns true, the simulation stops. */
14
30
  shouldStop?: (data: Data, params: Params) => boolean;
15
31
  /** Default parameter values. Used at engine creation if no params override is provided. */
@@ -37,4 +53,4 @@ type SimData<M> = M extends SimModule<infer D, infer _P> ? D : never;
37
53
  /** Extract the Params type from a SimModule. */
38
54
  type SimParams<M> = M extends SimModule<infer _D, infer P> ? P : never;
39
55
 
40
- export { type SimData, type SimModule, type SimParams, type StepArgs, defineSim };
56
+ export { type SimData, type SimInit, type SimModule, type SimParams, defineSim, isInitFn };
package/dist/sim.js CHANGED
@@ -1,7 +1,8 @@
1
- // src/sim.ts
2
- function defineSim(sim) {
3
- return sim;
4
- }
1
+ import {
2
+ defineSim,
3
+ isInitFn
4
+ } from "./chunk-IKR53C2U.js";
5
5
  export {
6
- defineSim
6
+ defineSim,
7
+ isInitFn
7
8
  };
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);
@@ -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 };
@@ -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
@@ -78,6 +78,8 @@ self.onmessage = async (event) => {
78
78
  shouldStop: sim.shouldStop,
79
79
  initialParams: msg.params,
80
80
  maxTime: msg.config.maxTime,
81
+ // Worker host owns its own setTimeout-driven loop; rAF wouldn't exist here anyway.
82
+ autoFrame: false,
81
83
  });
82
84
  postMessage({ kind: 'ready' });
83
85
  emitSnapshot();
@@ -1,6 +1,5 @@
1
1
  import { WorkerConfig } from './protocol.cjs';
2
- import '../engine.cjs';
3
- import '../sim.cjs';
2
+ import '../state.cjs';
4
3
 
5
4
  /**
6
5
  * Creates a Web Worker that loads a sim module and runs the engine.
@@ -1,6 +1,5 @@
1
1
  import { WorkerConfig } from './protocol.js';
2
- import '../engine.js';
3
- import '../sim.js';
2
+ import '../state.js';
4
3
 
5
4
  /**
6
5
  * Creates a Web Worker that loads a sim module and runs the engine.
@@ -54,6 +54,8 @@ self.onmessage = async (event) => {
54
54
  shouldStop: sim.shouldStop,
55
55
  initialParams: msg.params,
56
56
  maxTime: msg.config.maxTime,
57
+ // Worker host owns its own setTimeout-driven loop; rAF wouldn't exist here anyway.
58
+ autoFrame: false,
57
59
  });
58
60
  postMessage({ kind: 'ready' });
59
61
  emitSnapshot();
@@ -1,5 +1,4 @@
1
- import { EngineSnapshot } from '../engine.cjs';
2
- import '../sim.cjs';
1
+ import { State } from '../state.cjs';
3
2
 
4
3
  /**
5
4
  * Wire protocol types for main ↔ worker communication.
@@ -38,7 +37,7 @@ type MainToWorkerMessage<Params> = {
38
37
  /** Messages sent from the worker to the main thread. */
39
38
  type WorkerToMainMessage<Data, Params> = {
40
39
  kind: 'snapshot';
41
- snapshot: EngineSnapshot<Data, Params>;
40
+ snapshot: State<Data, Params>;
42
41
  } | {
43
42
  kind: 'error';
44
43
  error: {
@@ -1,5 +1,4 @@
1
- import { EngineSnapshot } from '../engine.js';
2
- import '../sim.js';
1
+ import { State } from '../state.js';
3
2
 
4
3
  /**
5
4
  * Wire protocol types for main ↔ worker communication.
@@ -38,7 +37,7 @@ type MainToWorkerMessage<Params> = {
38
37
  /** Messages sent from the worker to the main thread. */
39
38
  type WorkerToMainMessage<Data, Params> = {
40
39
  kind: 'snapshot';
41
- snapshot: EngineSnapshot<Data, Params>;
40
+ snapshot: State<Data, Params>;
42
41
  } | {
43
42
  kind: 'error';
44
43
  error: {
@@ -42,7 +42,6 @@ function createWorkerRunner(worker, config) {
42
42
  status: "idle",
43
43
  stepDurationMs: 0
44
44
  };
45
- let errorMessage = null;
46
45
  function send(msg) {
47
46
  worker.postMessage(serializeMainMessage(msg));
48
47
  }
@@ -59,7 +58,6 @@ function createWorkerRunner(worker, config) {
59
58
  emit();
60
59
  break;
61
60
  case "error":
62
- errorMessage = msg.error.message;
63
61
  currentSnapshot = { ...currentSnapshot, status: "stopped" };
64
62
  emit();
65
63
  break;
@@ -67,8 +65,7 @@ function createWorkerRunner(worker, config) {
67
65
  break;
68
66
  }
69
67
  };
70
- worker.onerror = (event) => {
71
- errorMessage = event.message ?? "Unknown worker error";
68
+ worker.onerror = () => {
72
69
  currentSnapshot = { ...currentSnapshot, status: "stopped" };
73
70
  emit();
74
71
  };
@@ -1,6 +1,5 @@
1
- import { EngineSnapshot } from '../engine.cjs';
1
+ import { State } from '../state.cjs';
2
2
  import { WorkerConfig } from './protocol.cjs';
3
- import '../sim.cjs';
4
3
 
5
4
  /**
6
5
  * Main-thread side of the worker runner.
@@ -14,8 +13,8 @@ type WorkerRunnerConfig<Params> = {
14
13
  config: WorkerConfig;
15
14
  };
16
15
  type WorkerRunner<Data, Params> = {
17
- getSnapshot: () => EngineSnapshot<Data, Params>;
18
- subscribe: (listener: (snapshot: EngineSnapshot<Data, Params>) => void) => () => void;
16
+ getSnapshot: () => State<Data, Params>;
17
+ subscribe: (listener: (snapshot: State<Data, Params>) => void) => () => void;
19
18
  play: () => void;
20
19
  pause: () => void;
21
20
  stop: () => void;
@@ -1,6 +1,5 @@
1
- import { EngineSnapshot } from '../engine.js';
1
+ import { State } from '../state.js';
2
2
  import { WorkerConfig } from './protocol.js';
3
- import '../sim.js';
4
3
 
5
4
  /**
6
5
  * Main-thread side of the worker runner.
@@ -14,8 +13,8 @@ type WorkerRunnerConfig<Params> = {
14
13
  config: WorkerConfig;
15
14
  };
16
15
  type WorkerRunner<Data, Params> = {
17
- getSnapshot: () => EngineSnapshot<Data, Params>;
18
- subscribe: (listener: (snapshot: EngineSnapshot<Data, Params>) => void) => () => void;
16
+ getSnapshot: () => State<Data, Params>;
17
+ subscribe: (listener: (snapshot: State<Data, Params>) => void) => () => void;
19
18
  play: () => void;
20
19
  pause: () => void;
21
20
  stop: () => void;
@@ -16,7 +16,6 @@ function createWorkerRunner(worker, config) {
16
16
  status: "idle",
17
17
  stepDurationMs: 0
18
18
  };
19
- let errorMessage = null;
20
19
  function send(msg) {
21
20
  worker.postMessage(serializeMainMessage(msg));
22
21
  }
@@ -33,7 +32,6 @@ function createWorkerRunner(worker, config) {
33
32
  emit();
34
33
  break;
35
34
  case "error":
36
- errorMessage = msg.error.message;
37
35
  currentSnapshot = { ...currentSnapshot, status: "stopped" };
38
36
  emit();
39
37
  break;
@@ -41,8 +39,7 @@ function createWorkerRunner(worker, config) {
41
39
  break;
42
40
  }
43
41
  };
44
- worker.onerror = (event) => {
45
- errorMessage = event.message ?? "Unknown worker error";
42
+ worker.onerror = () => {
46
43
  currentSnapshot = { ...currentSnapshot, status: "stopped" };
47
44
  emit();
48
45
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "automatick",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "State-machine engine for tick-based simulations in React",
5
5
  "author": "jckr",
6
6
  "license": "MIT",
@@ -29,7 +29,7 @@
29
29
  }
30
30
  },
31
31
  "scripts": {
32
- "build": "tsup src/engine.ts src/sim.ts src/worker/workerRunner.ts src/worker/createSimWorker.ts src/worker/protocol.ts src/react/Simulation.tsx src/react/hooks.ts src/react/useSimulationCanvas.ts src/react/PerformanceOverlay.tsx src/react/SimulationContext.tsx src/react/EngineContext.tsx src/react/SimulationControls.tsx src/react/controlPrimitives.tsx src/react/stableCallback.ts --format esm,cjs --dts --clean --external react --external react-dom --external react/jsx-runtime",
32
+ "build": "tsup src/engine.ts src/sim.ts src/state.ts src/worker/workerRunner.ts src/worker/createSimWorker.ts src/worker/protocol.ts src/react/Simulation.tsx src/react/hooks.ts src/react/useSimulationCanvas.ts src/react/PerformanceOverlay.tsx src/react/SimulationContext.tsx src/react/EngineContext.tsx src/react/SimulationControls.tsx src/react/controlPrimitives.tsx src/react/stableCallback.ts --format esm,cjs --dts --clean --external react --external react-dom --external react/jsx-runtime",
33
33
  "test": "vitest run"
34
34
  },
35
35
  "exports": {