Migrating manta-recon onto tosijs-3d-ensemble

manta-recon is where this format came from — src/prefab.ts and src/prefab-runtime.ts are its ancestors, extracted for the reasons in SPEC.md. This is the guide for putting it back, and it is short because the formats barely diverged.

Tracked as manta-recon#1. The cross-project rule holds: nothing in this document changes a file in that repo — it says what its owner should do, and this copy ships with the package so it is reachable by anyone who only installed it.

Requires tosijs-3d@^0.7.8. 0.1.0 advertised ^0.7.0 and imported a symbol only 0.7.8 exports, so it could not be imported on 0.7.4–0.7.7 — see the changelog. Use 0.1.1 or later.

Where it stands, measured

Run against static/prefabs/*.json as they are today:

prefab validates builds needs
shielded-target.json clean all 4 pieces nothing
ocean-rig.json one warning all 3 pieces a radar registration
dome-facility.json 7 errors all 7 pieces migrate
pyramid-base.json 6 errors all 6 pieces migrate

All four build every piece already. The errors are one cause: 13 pieces with no id.

And nothing of Manta's is lost — subsystems, piece-level values, zones, points and top-level values all survive a load/save round trip. That was the risk worth checking first, because a format that quietly drops a consumer's data cannot be adopted and would not have shown up as an error.

The gate lives at src/manta-mvp.test.ts in this repo and re-runs the whole table; it skips when the sibling checkout is absent.

Four steps

1. Migrate the files

cd ../tosijs-3d-ensemble
bun bin/migrate.ts ../manta-recon/static/prefabs/*.json          # report
bun bin/migrate.ts --write ../manta-recon/static/prefabs/*.json  # apply

Dry by default. It is idempotent, so re-running it is safe and the dry run cannot differ from the write.

It gives each id-less piece an id from its mesh name"Dome Mystery"dome-mystery — and moves hp from the piece into features.destroyable.hp. Ids are not derived from the array index on purpose: that is the fault this format exists to refuse, and baking it into a file people hand-edit would make it permanent.

docs/prefabs/*.json appear to be copies; migrate them too or delete them.

2. Register radar

ocean-rig uses a radar feature nobody has registered, so validate warns and the feature does nothing at build. This should not come from us. Features are a registry open to consumers, and a consumer's feature is meant to be indistinguishable from a built-in — Manta registering its own is that property being exercised, not a gap being filled:

import { registerFeature } from 'tosijs-3d-ensemble';

registerFeature({
  name: 'radar',
  icon: '📡',
  schema: {
    type: 'object',
    title: 'Radar',
    properties: {
      /* … */
    },
  },
  bind: (piece, cfg, ctx) => {
    /* whatever prefab-runtime did */
  },
});

3. Swap the imports

// was
import { validatePrefab, type Prefab } from './prefab';
import { buildPrefab } from './prefab-runtime';

// now
import {
  validate,
  buildEnsemble,
  registerSceneFeatures,
  type Ensemble,
} from 'tosijs-3d-ensemble';
import { registerCombatPreset } from 'tosijs-3d-ensemble/presets/combat';

registerSceneFeatures(); // sun, sky, ground, terrain, water, lamp…
registerCombatPreset(); // destroyable, turret, launcher, protector, blip,
// launchpad, and the roles Manta's files already use

The combat preset registers exactly the roles those prefabs use — structure, target, power, generator, shield, critical — which is unsurprising, since it was written from them.

Building one

import { buildEnsemble, placeMesh } from 'tosijs-3d-ensemble';

const built = buildEnsemble(ensemble, {
  scene, // <tosi-b3d>
  origin, // where the ensemble's local origin sits
  library: 'enemies', // fallback for pieces that name no library of their own
  placePiece: placeMesh, // ⚠️ REQUIRED for any ensemble with meshes
});

⚠️ placePiece does not default. It looks like it should, and this package's own doc comment claimed it did until manta-recon#3 — with it omitted, every piece is recorded and none is placed, which reported "20 of 20 built, zero problems" and put no geometry in the scene.

It cannot default: placeMesh imports tosijs-3d, which needs a DOM at module load, while buildEnsemble imports cleanly under plain Node — which is what lets a generator validate and build headlessly. So the DOM dependency is yours to declare. Omitting it now reports no-placer, and a piece the placer declines reports no-body.

⚠️ Headless means the DEEP import. A generator with no browser writes:

import { buildEnsemble } from 'tosijs-3d-ensemble/runtime/build';
import { validate } from 'tosijs-3d-ensemble/format/validate';

The package's main entry also exports ensembleEditor, a custom element, so evaluating the barrel needs HTMLElement and throws under Node. Tree-shaking saves a bundler; a plain import() evaluates the whole module graph. In a browser or through a bundler, import from 'tosijs-3d-ensemble' as usual.

⚠️ Three problem codes are new, and two are errors. no-placer and no-body report at severity error, and meshes-unchecked as a warning — so a document that validated clean under 0.2.0 can report problems under 0.3.0 without having changed. Each of them is a silence being broken rather than a new rule: the old behaviour is what let a build report "20 of 20 built, zero problems" with nothing in the scene. If your gate is problems.some((p) => p.severity === 'error'), expect it to fire on a build that never passed placePiece.

Two differences worth knowing:

4. Delete prefab.ts and prefab-runtime.ts

754 lines. src/main.ts and src/prefab-editor.ts are the only importers.

prefab-editor.ts, bench-gizmo.ts and bench-view.ts are a separate question — this package has its own editor (ensembleEditor) but it is not yet a drop-in replacement for the bench, so keep them until it is.

What "done" means

bun test in this repo, with the sibling checked out, reports all four prefabs validating clean and building every piece. That is milestone 1, and it is the proof the API is right — which is why it comes before building anything else on top of it.