Skip to main content

mapv10 World Lab UX

Status: M11k-edit Wave P implemented. M11k-inspect (16-channel overlay inspector) deferred until M11i typed loaders land.

Scope: browser-side authoring + launching surface for world recipes and intent directives. The World Lab is an editor that writes recipe JSON + intent JSON, posts inline payloads to the dev server's run-launch endpoint, streams the spawned generator's stdout/stderr/exit over SSE, and re-renders the resulting run in the live Three.js viewer without a page reload.

Invariants

  • The World Lab does not invent map truth. The renderer (viewer/src/renderer/Mapv10ThreeRenderer.ts) stays a pure consumer of generator products. The World Lab writes recipe + intent JSON and triggers generation; it never mutates renderer scene geometry. This matches M11 source-of-truth invariant 9.
  • No React, no R3F. The shell is plain DOM. The renderer keeps running in the background while the drawer is open.
  • No invented recipe fields. Every editor surface produces only what schema/world-recipe.schema.json and schema/world-intent-directives.schema.json declare (both close additionalProperties: false).
  • No generator products from the UI. The Launch action POSTs a recipe + intent payload to the dev server; the server spawns the generator binary and the generator writes the run products. The UI never writes raster/mesh/manifest files.
  • No silent vocabulary defaults. Recipe and intent token pickers surface "(select a token…)" rather than a hardcoded default; saving a draft with an unset required token surfaces ajv's required / enum error inline.

Module map

ModuleRole
viewer/src/ui/world-lab/worldLabShell.tsTab nav, toolbar, draft persistence, launch button, runs the dispatch graph
viewer/src/ui/world-lab/recipeEditor.tsStructured editor for a world-recipe.schema.json draft with textarea fallback
viewer/src/ui/world-lab/intentEditor.tsStructured editor for a world-intent-directives.schema.json draft with textarea fallback
viewer/src/ui/world-lab/runLauncher.tsPOST /mapv10/runs/launch + EventSource consumer
viewer/src/ui/world-lab/runLibrary.tsCard grid from GET /mapv10/runs/; click-to-load via direct renderer.loadRun(manifestUrl)
viewer/src/ui/world-lab/compareRuns.tsTwo-pane run diff with synced scroll
viewer/src/ui/world-lab/ajvPipeline.tsCompiles the 8 committed schemas under Ajv2020 strict mode
viewer/src/ui/world-lab/vocabularyPickers.tsShared token-picker widgets reading WorldVocabularyResolver
viewer/src/ui/world-lab/worldLabTypes.tsLocal type aliases mirroring schema enums
viewer/server/runLaunchMiddleware.tsVite middleware exposing run-launch + SSE-stream endpoints
viewer/server/mapv10ArtifactMiddleware.tsExtends artifact server with /mapv10/vocabulary/<file> and /mapv10/recipes/... routes; RunStatus extended with recipeRef, generatedAtUnixMillis, thumbnailPath

Server contract

POST /mapv10/runs/launch

{
"recipe": { /* world-recipe-v1 instance */ },
"intents": null | { /* world-intent-directives-v1 instance */ },
"vocabulary": null | { /* world-vocabulary-v1 override */ },
"seed": 20260516,
"scalePreset": "continent",
"runId": "draft-run",
"force": false
}

Response:

202 Accepted
{ "format": "mapv10_run_launch_accepted", "launchId": "...", "runStreamUrl": "/mapv10/runs/launch/<launchId>/stream", "runId": "draft-run" }

Server-side flow:

  1. Validate the payload shape (recipe required as object; intents and vocabulary may be null; seed is a non-negative integer; scalePreset is one of the four committed presets; runId matches ^[A-Za-z0-9_][A-Za-z0-9._-]*$; force is a boolean).
  2. Allocate a launch id and a temp directory examples/map/mapv10/recipes/.tmp/launch-<launchId>/.
  3. Write recipe.json, optionally intents.json and vocabulary.json, to the temp directory.
  4. Spawn the generator. Discovery:
    • if process.env.MAPV10_GENERATOR_BIN is set and the path exists, spawn that binary directly with no prefix args;
    • otherwise spawn cargo run --release -- inside examples/map/mapv10/generator/.
  5. Stream stdout/stderr/exit over SSE on GET /mapv10/runs/launch/<launchId>/stream. The exit event carries code, signal, the manifest URL on success (code === 0), and an error string list on spawn failure.
  6. On exit the temp directory is removed regardless of outcome.

GET /mapv10/vocabulary/<file>

Serves the bundled vocabulary instance so the editor can render token pickers before any run exists on disk. File names must match [A-Za-z0-9._-]+\.json.

GET /mapv10/recipes/<preset>.json and /mapv10/recipes/intents/<preset>.intents.json

Serves the committed starter presets to the preset-picker UI. Recipe ids must match ^[a-z][a-z0-9-]*$.

GET /mapv10/runs/ extension

The RunStatus payload now carries:

  • recipeRef — relative path to the recipe that drove the run if manifest.recipeRef is set, otherwise null;
  • generatedAtUnixMillis — generator-recorded timestamp;
  • thumbnailPath — relative path to the first previews/*-preview.png artifact, or null.

Browser draft storage

Drafts live in window.localStorage under mapv10:world-lab:drafts:v1. The browser is the only owner — there is no server-side draft store. On Launch the in-memory draft is posted inline; the server spills to temp files and deletes them on exit.

Validation pipeline

ajvPipeline.ts compiles every committed schema (world-recipe, world-vocabulary, world-intent-directives, terrain-genesis, hydrology, route-crossing-candidates, manifest, raster-product) under Ajv2020 strict mode with ajv-formats. The order and registration mirror scripts/validate-recipe-schemas.mjs so the in-browser pipeline matches the CI gate.

Editor surfaces validate on every keystroke. The textarea fallback edits the same draft via JSON round-trip; a parse failure surfaces a single inline issue and leaves the form untouched.

M11k-inspect deferral

Wave P intentionally ships D.7 (16-channel overlay inspector) with only the two channels already loaded by manifestLoader.ts: effectiveBiome and effectiveOverride. The 16 M11c / M11d / M11f / M11g raster channels (upliftField, crustAge, flowDirection, flowAccumulationD8, etc.) need typed loader extensions that land in Wave M11i. The "Inspect" tab in the World Lab renders an explicit placeholder labelled TODO(M11k-inspect) listing the deferred channels so the deferral is visible rather than silent. See the ## M11k ADR section for the formal split.

Shell integration

The viewer top bar gains a "World Lab" button next to the existing "Products" button. The button toggles a world-lab-drawer aside peer of debug-drawer. The renderer keeps running in the background while the drawer is open. The drawer mounts the lab lazily on first open so the ajv pipeline does not compile schemas until the user actually enters the lab.