nucleation 0.10.14

A high-performance Minecraft schematic parser and utility library
Documentation
# TickSimulation in the browser — import recipe

Verified in headless Chromium (main thread + module Web Worker), 2026-07-28.

## Build the package

```sh
# What the apps actually need — mc-tick AND meshing:
RUSTFLAGS='-C codegen-units=1' NUCLEATION_WASM_FEATURES=bridge,mc-tick,meshing \
    tools/package-npm.sh dist/npm-mctick
```

**Do not drop `meshing` from that list.** Both wasm apps render with
nucleation's real mesher (`Schematic.create` + `MeshResult.create` →
GLB → three.js); a package built `bridge,mc-tick` alone still loads, still
simulates, and then fails only where a model is meshed — a silent break of
door-cert's replay. The simulation-only artifact is ~11.0 MB and the one with
meshing ~12.5 MB, so the size is also the tell.

`tools/package-npm.sh` takes `NUCLEATION_WASM_FEATURES` (default is the
published set `bridge,simulation,meshing`; `mc-tick` adds TickSimulation) and
emits an isomorphic `diplomat.config.mjs` (`new URL("./nucleation.wasm",
import.meta.url)`) that works in Node, the browser main thread, and workers.
Serve the `.wasm` with `Content-Type: application/wasm` so
`instantiateStreaming` works (Vite does this out of the box for public/ files).

## Vite frontends

Copy (or symlink) `dist/npm-mctick/` into the app's `public/engine/` and:

```js
const { TickSimulation, TickSettleMode } = await import("/engine/index.mjs");
const sim = TickSimulation.fromSnbt(snbt, TickSettleMode.Quiet, 0, 0, 0, "");
sim.setRngSeed(12345n);          // BigInt
sim.step();                       // or sim.run(80)
sim.nonAirMinX();                 // scalar queries — no JSON in hot loops
```

Don't `import` the package through Vite's dependency pipeline — the runtime
`fetch` of the wasm wants a stable URL; `public/` gives it one.

## Web Workers (the GA's parallelism)

Each worker instantiates its own wasm module (~1 s cold, then full speed —
4.9k flying-machine evals/sec/worker in Chromium). Use a **dynamic import
inside try/catch** in the worker:

```js
self.onmessage = async ({ data }) => {
  try {
    const { TickSimulation, TickSettleMode } = await import("/engine/index.mjs");
    // ... evaluate, postMessage results
  } catch (e) { self.postMessage({ error: String(e) }); }
};
```

A top-level `import` that fails in a module worker fires **neither**
`onmessage` nor `onerror` — the page just hangs. The dynamic form reports.

## Scalar queries (added for the GA)

`nonAirCount(): number` · `nonAirCenterX(): number` · `nonAirMinX(): number` ·
`nonAirMaxX(): number` · `changesCount(): number` — displacement metrics
without `worldSnapshotJson()` round-trips (~19% faster evals).

## Benchmarks (flying-machine eval: construct + quiet settle + kick + 80 ticks)

| where | evals/sec |
|---|---|
| Node wasm, JSON query | 4,072 |
| Node wasm, scalar query | 4,864 |
| Node wasm, scalar + cgu=1 build | 5,185 |
| Chromium main thread | 4,298 |
| Chromium module worker | 4,934 |

Construction ~35% / stepping ~62% of an eval; both dominated by the engine
itself, not the FFI. Native Python pool comparison: ~15k/sec across 6 procs.