nucleation 0.10.4 → 0.10.7

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.
@@ -133,6 +133,50 @@ export class TickSimulation {
133
133
  */
134
134
  getBlock(x: number, y: number, z: number): string;
135
135
 
136
+ /**
137
+ * Batched block-state reads: `positions_json` is `[[x,y,z], ...]`,
138
+ * the answer a JSON array of descriptors in the same order
139
+ * (`"minecraft:air"` for empty).
140
+ *
141
+ * A verification sweep over a computational build is thousands of
142
+ * probe reads per settle; one boundary call per sweep instead of one
143
+ * per probe is the difference between the FFI being the throughput
144
+ * ceiling and not.
145
+ */
146
+ readProbes(positionsJson: string): string;
147
+
148
+ /**
149
+ * Who powers this cell and why — a power-source tree from the
150
+ * simulation's current state, as JSON.
151
+ *
152
+ * Each node carries `pos`, `state`, `kind` (`wire` / `conductor` /
153
+ * `source` / `block`), the `power` the cell carries or emits, and
154
+ * `inputs`: the per-side contributions that explain it, each with a
155
+ * `mechanism` (`block_signal`, `wire`, `wire_up`, `wire_down`,
156
+ * `strong`, `signal`), the arriving `power`, and a recursive
157
+ * `source` node. Cycles stop with `"cycle": true`. Coordinates are
158
+ * the same structure-local space `get_block` reads.
159
+ *
160
+ * This is the static complement of running the sim: an open (a dead
161
+ * route that settles quiescent) shows up as an empty `inputs` list
162
+ * exactly where the feed should have been.
163
+ */
164
+ conductionTrace(x: number, y: number, z: number): string;
165
+
166
+ /**
167
+ * Write every settled non-air state back into `schematic` — the bulk
168
+ * form of `{simulate=true}`: settle once, keep the world the engine
169
+ * ended on. Returns how many blocks changed.
170
+ *
171
+ * The schematic's bounding-box minimum corresponds to the
172
+ * simulation's `(0, 0, 0)`, which is exactly how `from_schematic`
173
+ * loaded it. A file baked this way carries real wire connections and
174
+ * power in its palette, loads quiescent under `InWorld`, and renders
175
+ * correctly in any static consumer. Cells the simulation turned into
176
+ * air (a popped-off component) are left as the schematic had them.
177
+ */
178
+ bakeTo(schematic: Schematic): number;
179
+
136
180
  /**
137
181
  * Snapshot the entire simulation; returns a checkpoint id.
138
182
  */
@@ -192,6 +236,112 @@ export class TickSimulation {
192
236
  */
193
237
  clearUpdates(): void;
194
238
 
239
+ /**
240
+ * Start recording a run timeline from the current tick.
241
+ *
242
+ * A timeline is what makes a span of simulation reviewable after the
243
+ * fact: block deltas, the inputs that caused them and the piston
244
+ * strokes they drove, plus one whole-world frame to replay them from.
245
+ * Off by default — a simulation used for timing should not pay for it.
246
+ *
247
+ * Called again, it restarts from the current tick, and the previously
248
+ * stopped span is released.
249
+ *
250
+ * Starting a recording also wipes the plain block-change log that
251
+ * {@link TickSimulation::changes_json} and {@link TickSimulation::changes_count}
252
+ * read back to empty — a separate reset from
253
+ * {@link TickSimulation::record_updates}/{@link TickSimulation::clear_updates},
254
+ * which govern a different log. A host holding a cursor into the
255
+ * change log (the sim lab keeps a cumulative one) must reset that
256
+ * cursor when it calls this, or it will read past the end of a log
257
+ * that is no longer the one it was walking.
258
+ */
259
+ recordTimeline(): void;
260
+
261
+ /**
262
+ * End the recording, keeping the span readable.
263
+ *
264
+ * This is a host's Stop button, and it is not a rewind: the span stays
265
+ * readable and exportable until the next
266
+ * {@link TickSimulation::record_timeline}, while the simulation is free to
267
+ * run on without the recording following it. No-op if nothing was
268
+ * recording.
269
+ */
270
+ stopTimeline(): void;
271
+
272
+ /**
273
+ * Where the recorded run was busy, as JSON:
274
+ * `{"start":T,"end":T,"ticks":[{"tick":T,"changes":N,"inputs":N,
275
+ * "pistons":N}]}`.
276
+ *
277
+ * The strip a host draws to let someone pick a span worth exporting.
278
+ * Only ticks that did something appear: an idle tick is **absent**
279
+ * rather than present with zeroes, so a build that sits still does not
280
+ * advance the strip and a long quiet run stays cheap to send.
281
+ *
282
+ * `{"start":0,"end":0,"ticks":[]}` when nothing has been recorded.
283
+ */
284
+ timelineActivityJson(): string;
285
+
286
+ /**
287
+ * Exact and translated recurrence in the timeline a read query would
288
+ * resolve to (see {@link Self::timeline}), as JSON:
289
+ * `{"exact":{"start":T,"end":T,"period":N,"drift":[x,y,z]}|null,
290
+ * "translated":{...}|null}`.
291
+ *
292
+ * **An absent cycle is `null`, not an error.** Most builds — an
293
+ * adder, a door — never repeat their own state, and that is the
294
+ * ordinary outcome, not a failed search.
295
+ *
296
+ * **O(ticks × blocks): replays the whole recorded span to build one
297
+ * digest per tick boundary**, then rebuilds full frames for the
298
+ * handful of candidates that survive. This is an on-demand "find
299
+ * cycles" action for a host UI button, never something to call per
300
+ * tick or per frame — poll {@link Self::timeline_activity_json} instead.
301
+ *
302
+ * Materialises the whole recorded timeline once to answer this call
303
+ * (an owned `RunTimeline`'s `initial` frame copies every non-air
304
+ * block) — acceptable for one on-demand press, not for a loop.
305
+ *
306
+ * `{"exact":null,"translated":null}` when nothing has been recorded.
307
+ */
308
+ timelineCyclesJson(): string;
309
+
310
+ /**
311
+ * Project `[start_tick, end_tick)` of the timeline a read query would
312
+ * resolve to (see {@link Self::timeline}) into the animated-GLB mesher's
313
+ * `Timeline` JSON — `{"origin":[x,y,z],"tick_ms":F,
314
+ * "events":[{"kind":"set_block"|"piston",...}]}` — via
315
+ * `crate::tick_timeline::mesher_timeline_json`.
316
+ *
317
+ * **Materialises the whole recorded timeline to answer this call**
318
+ * (an owned `RunTimeline`'s `initial` frame copies every non-air
319
+ * block in the world) — this is an on-demand "export this
320
+ * selection" action, not something to call per frame or poll.
321
+ *
322
+ * Fails if no timeline has been recorded, or if `start_tick..
323
+ * end_tick` is empty or outside the recorded span.
324
+ */
325
+ animationTimelineJson(startTick: number, endTick: number, tickMs: number): string;
326
+
327
+ /**
328
+ * The selection's starting scene — `[start_tick, end_tick)` of the
329
+ * timeline a read query would resolve to (see {@link Self::timeline}) —
330
+ * as schematic bytes, base64-encoded.
331
+ *
332
+ * A WASM handle cannot cross a worker boundary, so this exists for a
333
+ * host to hand the bytes to a worker, which rebuilds the schematic
334
+ * with `Schematic.fromData`.
335
+ *
336
+ * **Materialises the whole recorded timeline to answer this call**
337
+ * — see {@link Self::animation_timeline_json}; an on-demand export
338
+ * action, not a per-frame poll.
339
+ *
340
+ * Fails if no timeline has been recorded, or if `start_tick..
341
+ * end_tick` is empty or outside the recorded span.
342
+ */
343
+ selectionSchematicB64(startTick: number, endTick: number): string;
344
+
195
345
  /**
196
346
  * How many updates have been recorded — page before pulling them.
197
347
  */
@@ -234,8 +384,89 @@ export class TickSimulation {
234
384
  */
235
385
  updatesWaveJson(tick: number): string;
236
386
 
387
+ /**
388
+ * Every block a piston currently has in flight, as JSON:
389
+ * `[{"to":[x,y,z],"from":[x,y,z],"state":"...","carried":"...",
390
+ * "carried_short":"..."|null,"remains":"..."|null,"dir":"east",
391
+ * "extending":bool,"started":T,"lands":T,"source_piston":bool}]`.
392
+ *
393
+ * Draw `carried` travelling `from` -> `to`, and `remains` (when it is
394
+ * not null) parked at `to` for the whole move. They differ from
395
+ * `state` — what actually lands — only for a retracting piston, whose
396
+ * body stays put while its head comes home; vanilla's
397
+ * `PistonHeadRenderer` splits exactly these two slots.
398
+ *
399
+ * `carried_short` is the same arm with `short=true`. Draw it while the
400
+ * head is **within half a block of its body** — `progress <= 0.5`
401
+ * extending, `progress >= 0.5` retracting — or the shaft passes
402
+ * visibly through the back of the piston as it comes home. Which form
403
+ * to use is yours; naming the state is the engine's.
404
+ *
405
+ * What a renderer needs to animate a stroke, from the simulator that
406
+ * dispatched it. The block-change stream cannot answer this: it says a
407
+ * cell became a `moving_piston` placeholder, not which block set off,
408
+ * which cell it left, or which tick it arrives — so a host that
409
+ * reconstructs strokes from changes is reimplementing piston mechanics
410
+ * downstream of the engine, and animating on a clock the simulation
411
+ * does not share. That desync is what draws a block twice, leaves a
412
+ * gap where one should be, and shears a piston head off its load.
413
+ *
414
+ * `started` and `lands` are tick numbers in the engine's frame, where
415
+ * {@link Self::tick_count} counts *completed* ticks: after stepping to
416
+ * `tick_count == t`, a flight's progress is
417
+ * `(t - started) / (lands - started)`, clamped to 1. Draw it while it
418
+ * is listed and drop it when it stops being listed — the same call
419
+ * that stops reporting it is the tick the real block is written, so
420
+ * there is no frame with both and none with neither.
421
+ */
422
+ movingBlocksJson(): string;
423
+
424
+ /**
425
+ * Drop the recorded block changes without stopping recording.
426
+ *
427
+ * The log grows for as long as the simulation runs and nothing
428
+ * empties it, so a long-running host — a browser session driving
429
+ * thousands of ticks — accumulates every block change forever. A
430
+ * host that has already consumed {@link TickSimulation::changes_json}
431
+ * can say so here and keep recording on. A host holding a cursor
432
+ * into the change log must reset that cursor when it calls this, or
433
+ * it will read past the end of a log that is no longer the one it
434
+ * was walking — the same hazard {@link TickSimulation::record_timeline}
435
+ * names for its own reset of this log.
436
+ *
437
+ * Refuses — and leaves the log untouched — while
438
+ * {@link TickSimulation::record_timeline} is recording: a run timeline
439
+ * is a seed frame plus this same log, and every timeline reader
440
+ * trusts that the log describes every mutation since recording
441
+ * began. Clearing it out from under a live recording would make
442
+ * replay silently wrong rather than fail loudly. Returns `true` if
443
+ * the log was cleared, `false` if the call was refused — following
444
+ * {@link TickSimulation::run_until_quiescent}'s convention of reporting
445
+ * whether the call achieved what it was asked, rather than swallowing
446
+ * a no-op.
447
+ */
448
+ clearChanges(): boolean;
449
+
237
450
  changesJson(): string;
238
451
 
452
+ /**
453
+ * The same JSON array {@link TickSimulation::changes_json} produces,
454
+ * but only the entries from index `start` onward.
455
+ *
456
+ * Exists for a host draining the log every frame while a run
457
+ * timeline recording refuses {@link TickSimulation::clear_changes}: the
458
+ * log only ever grows in that state, so without this,
459
+ * `changes_json` re-serialises the whole backlog on every single
460
+ * drain — a cost that climbs for as long as the recording runs,
461
+ * which is exactly when a session runs longest. Reading from a
462
+ * cursor keeps a drain's cost to what is actually new.
463
+ *
464
+ * `start` at or past the end of the log yields `[]`, not an error —
465
+ * a host racing a draining cursor against a growing log should not
466
+ * have to special-case "nothing new yet".
467
+ */
468
+ changesJsonFrom(start: number): string;
469
+
239
470
  /**
240
471
  * Live item entities and minecarts, as JSON:
241
472
  * `{"items":[{"id":N,"item":"...","count":N,"pos":[..],"vel":[..],
@@ -133,6 +133,50 @@ export class TickSimulation {
133
133
  */
134
134
  getBlock(x: number, y: number, z: number): string;
135
135
 
136
+ /**
137
+ * Batched block-state reads: `positions_json` is `[[x,y,z], ...]`,
138
+ * the answer a JSON array of descriptors in the same order
139
+ * (`"minecraft:air"` for empty).
140
+ *
141
+ * A verification sweep over a computational build is thousands of
142
+ * probe reads per settle; one boundary call per sweep instead of one
143
+ * per probe is the difference between the FFI being the throughput
144
+ * ceiling and not.
145
+ */
146
+ readProbes(positionsJson: string): string;
147
+
148
+ /**
149
+ * Who powers this cell and why — a power-source tree from the
150
+ * simulation's current state, as JSON.
151
+ *
152
+ * Each node carries `pos`, `state`, `kind` (`wire` / `conductor` /
153
+ * `source` / `block`), the `power` the cell carries or emits, and
154
+ * `inputs`: the per-side contributions that explain it, each with a
155
+ * `mechanism` (`block_signal`, `wire`, `wire_up`, `wire_down`,
156
+ * `strong`, `signal`), the arriving `power`, and a recursive
157
+ * `source` node. Cycles stop with `"cycle": true`. Coordinates are
158
+ * the same structure-local space `get_block` reads.
159
+ *
160
+ * This is the static complement of running the sim: an open (a dead
161
+ * route that settles quiescent) shows up as an empty `inputs` list
162
+ * exactly where the feed should have been.
163
+ */
164
+ conductionTrace(x: number, y: number, z: number): string;
165
+
166
+ /**
167
+ * Write every settled non-air state back into `schematic` — the bulk
168
+ * form of `{simulate=true}`: settle once, keep the world the engine
169
+ * ended on. Returns how many blocks changed.
170
+ *
171
+ * The schematic's bounding-box minimum corresponds to the
172
+ * simulation's `(0, 0, 0)`, which is exactly how `from_schematic`
173
+ * loaded it. A file baked this way carries real wire connections and
174
+ * power in its palette, loads quiescent under `InWorld`, and renders
175
+ * correctly in any static consumer. Cells the simulation turned into
176
+ * air (a popped-off component) are left as the schematic had them.
177
+ */
178
+ bakeTo(schematic: Schematic): number;
179
+
136
180
  /**
137
181
  * Snapshot the entire simulation; returns a checkpoint id.
138
182
  */
@@ -192,6 +236,112 @@ export class TickSimulation {
192
236
  */
193
237
  clearUpdates(): void;
194
238
 
239
+ /**
240
+ * Start recording a run timeline from the current tick.
241
+ *
242
+ * A timeline is what makes a span of simulation reviewable after the
243
+ * fact: block deltas, the inputs that caused them and the piston
244
+ * strokes they drove, plus one whole-world frame to replay them from.
245
+ * Off by default — a simulation used for timing should not pay for it.
246
+ *
247
+ * Called again, it restarts from the current tick, and the previously
248
+ * stopped span is released.
249
+ *
250
+ * Starting a recording also wipes the plain block-change log that
251
+ * {@link TickSimulation::changes_json} and {@link TickSimulation::changes_count}
252
+ * read back to empty — a separate reset from
253
+ * {@link TickSimulation::record_updates}/{@link TickSimulation::clear_updates},
254
+ * which govern a different log. A host holding a cursor into the
255
+ * change log (the sim lab keeps a cumulative one) must reset that
256
+ * cursor when it calls this, or it will read past the end of a log
257
+ * that is no longer the one it was walking.
258
+ */
259
+ recordTimeline(): void;
260
+
261
+ /**
262
+ * End the recording, keeping the span readable.
263
+ *
264
+ * This is a host's Stop button, and it is not a rewind: the span stays
265
+ * readable and exportable until the next
266
+ * {@link TickSimulation::record_timeline}, while the simulation is free to
267
+ * run on without the recording following it. No-op if nothing was
268
+ * recording.
269
+ */
270
+ stopTimeline(): void;
271
+
272
+ /**
273
+ * Where the recorded run was busy, as JSON:
274
+ * `{"start":T,"end":T,"ticks":[{"tick":T,"changes":N,"inputs":N,
275
+ * "pistons":N}]}`.
276
+ *
277
+ * The strip a host draws to let someone pick a span worth exporting.
278
+ * Only ticks that did something appear: an idle tick is **absent**
279
+ * rather than present with zeroes, so a build that sits still does not
280
+ * advance the strip and a long quiet run stays cheap to send.
281
+ *
282
+ * `{"start":0,"end":0,"ticks":[]}` when nothing has been recorded.
283
+ */
284
+ timelineActivityJson(): string;
285
+
286
+ /**
287
+ * Exact and translated recurrence in the timeline a read query would
288
+ * resolve to (see {@link Self::timeline}), as JSON:
289
+ * `{"exact":{"start":T,"end":T,"period":N,"drift":[x,y,z]}|null,
290
+ * "translated":{...}|null}`.
291
+ *
292
+ * **An absent cycle is `null`, not an error.** Most builds — an
293
+ * adder, a door — never repeat their own state, and that is the
294
+ * ordinary outcome, not a failed search.
295
+ *
296
+ * **O(ticks × blocks): replays the whole recorded span to build one
297
+ * digest per tick boundary**, then rebuilds full frames for the
298
+ * handful of candidates that survive. This is an on-demand "find
299
+ * cycles" action for a host UI button, never something to call per
300
+ * tick or per frame — poll {@link Self::timeline_activity_json} instead.
301
+ *
302
+ * Materialises the whole recorded timeline once to answer this call
303
+ * (an owned `RunTimeline`'s `initial` frame copies every non-air
304
+ * block) — acceptable for one on-demand press, not for a loop.
305
+ *
306
+ * `{"exact":null,"translated":null}` when nothing has been recorded.
307
+ */
308
+ timelineCyclesJson(): string;
309
+
310
+ /**
311
+ * Project `[start_tick, end_tick)` of the timeline a read query would
312
+ * resolve to (see {@link Self::timeline}) into the animated-GLB mesher's
313
+ * `Timeline` JSON — `{"origin":[x,y,z],"tick_ms":F,
314
+ * "events":[{"kind":"set_block"|"piston",...}]}` — via
315
+ * `crate::tick_timeline::mesher_timeline_json`.
316
+ *
317
+ * **Materialises the whole recorded timeline to answer this call**
318
+ * (an owned `RunTimeline`'s `initial` frame copies every non-air
319
+ * block in the world) — this is an on-demand "export this
320
+ * selection" action, not something to call per frame or poll.
321
+ *
322
+ * Fails if no timeline has been recorded, or if `start_tick..
323
+ * end_tick` is empty or outside the recorded span.
324
+ */
325
+ animationTimelineJson(startTick: number, endTick: number, tickMs: number): string;
326
+
327
+ /**
328
+ * The selection's starting scene — `[start_tick, end_tick)` of the
329
+ * timeline a read query would resolve to (see {@link Self::timeline}) —
330
+ * as schematic bytes, base64-encoded.
331
+ *
332
+ * A WASM handle cannot cross a worker boundary, so this exists for a
333
+ * host to hand the bytes to a worker, which rebuilds the schematic
334
+ * with `Schematic.fromData`.
335
+ *
336
+ * **Materialises the whole recorded timeline to answer this call**
337
+ * — see {@link Self::animation_timeline_json}; an on-demand export
338
+ * action, not a per-frame poll.
339
+ *
340
+ * Fails if no timeline has been recorded, or if `start_tick..
341
+ * end_tick` is empty or outside the recorded span.
342
+ */
343
+ selectionSchematicB64(startTick: number, endTick: number): string;
344
+
195
345
  /**
196
346
  * How many updates have been recorded — page before pulling them.
197
347
  */
@@ -234,8 +384,89 @@ export class TickSimulation {
234
384
  */
235
385
  updatesWaveJson(tick: number): string;
236
386
 
387
+ /**
388
+ * Every block a piston currently has in flight, as JSON:
389
+ * `[{"to":[x,y,z],"from":[x,y,z],"state":"...","carried":"...",
390
+ * "carried_short":"..."|null,"remains":"..."|null,"dir":"east",
391
+ * "extending":bool,"started":T,"lands":T,"source_piston":bool}]`.
392
+ *
393
+ * Draw `carried` travelling `from` -> `to`, and `remains` (when it is
394
+ * not null) parked at `to` for the whole move. They differ from
395
+ * `state` — what actually lands — only for a retracting piston, whose
396
+ * body stays put while its head comes home; vanilla's
397
+ * `PistonHeadRenderer` splits exactly these two slots.
398
+ *
399
+ * `carried_short` is the same arm with `short=true`. Draw it while the
400
+ * head is **within half a block of its body** — `progress <= 0.5`
401
+ * extending, `progress >= 0.5` retracting — or the shaft passes
402
+ * visibly through the back of the piston as it comes home. Which form
403
+ * to use is yours; naming the state is the engine's.
404
+ *
405
+ * What a renderer needs to animate a stroke, from the simulator that
406
+ * dispatched it. The block-change stream cannot answer this: it says a
407
+ * cell became a `moving_piston` placeholder, not which block set off,
408
+ * which cell it left, or which tick it arrives — so a host that
409
+ * reconstructs strokes from changes is reimplementing piston mechanics
410
+ * downstream of the engine, and animating on a clock the simulation
411
+ * does not share. That desync is what draws a block twice, leaves a
412
+ * gap where one should be, and shears a piston head off its load.
413
+ *
414
+ * `started` and `lands` are tick numbers in the engine's frame, where
415
+ * {@link Self::tick_count} counts *completed* ticks: after stepping to
416
+ * `tick_count == t`, a flight's progress is
417
+ * `(t - started) / (lands - started)`, clamped to 1. Draw it while it
418
+ * is listed and drop it when it stops being listed — the same call
419
+ * that stops reporting it is the tick the real block is written, so
420
+ * there is no frame with both and none with neither.
421
+ */
422
+ movingBlocksJson(): string;
423
+
424
+ /**
425
+ * Drop the recorded block changes without stopping recording.
426
+ *
427
+ * The log grows for as long as the simulation runs and nothing
428
+ * empties it, so a long-running host — a browser session driving
429
+ * thousands of ticks — accumulates every block change forever. A
430
+ * host that has already consumed {@link TickSimulation::changes_json}
431
+ * can say so here and keep recording on. A host holding a cursor
432
+ * into the change log must reset that cursor when it calls this, or
433
+ * it will read past the end of a log that is no longer the one it
434
+ * was walking — the same hazard {@link TickSimulation::record_timeline}
435
+ * names for its own reset of this log.
436
+ *
437
+ * Refuses — and leaves the log untouched — while
438
+ * {@link TickSimulation::record_timeline} is recording: a run timeline
439
+ * is a seed frame plus this same log, and every timeline reader
440
+ * trusts that the log describes every mutation since recording
441
+ * began. Clearing it out from under a live recording would make
442
+ * replay silently wrong rather than fail loudly. Returns `true` if
443
+ * the log was cleared, `false` if the call was refused — following
444
+ * {@link TickSimulation::run_until_quiescent}'s convention of reporting
445
+ * whether the call achieved what it was asked, rather than swallowing
446
+ * a no-op.
447
+ */
448
+ clearChanges(): boolean;
449
+
237
450
  changesJson(): string;
238
451
 
452
+ /**
453
+ * The same JSON array {@link TickSimulation::changes_json} produces,
454
+ * but only the entries from index `start` onward.
455
+ *
456
+ * Exists for a host draining the log every frame while a run
457
+ * timeline recording refuses {@link TickSimulation::clear_changes}: the
458
+ * log only ever grows in that state, so without this,
459
+ * `changes_json` re-serialises the whole backlog on every single
460
+ * drain — a cost that climbs for as long as the recording runs,
461
+ * which is exactly when a session runs longest. Reading from a
462
+ * cursor keeps a drain's cost to what is actually new.
463
+ *
464
+ * `start` at or past the end of the log yields `[]`, not an error —
465
+ * a host racing a draining cursor against a growing log should not
466
+ * have to special-case "nothing new yet".
467
+ */
468
+ changesJsonFrom(start: number): string;
469
+
239
470
  /**
240
471
  * Live item entities and minecarts, as JSON:
241
472
  * `{"items":[{"id":N,"item":"...","count":N,"pos":[..],"vel":[..],