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.jsonandschema/world-intent-directives.schema.jsondeclare (both closeadditionalProperties: 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/enumerror inline.
Module map
| Module | Role |
|---|---|
viewer/src/ui/world-lab/worldLabShell.ts | Tab nav, toolbar, draft persistence, launch button, runs the dispatch graph |
viewer/src/ui/world-lab/recipeEditor.ts | Structured editor for a world-recipe.schema.json draft with textarea fallback |
viewer/src/ui/world-lab/intentEditor.ts | Structured editor for a world-intent-directives.schema.json draft with textarea fallback |
viewer/src/ui/world-lab/runLauncher.ts | POST /mapv10/runs/launch + EventSource consumer |
viewer/src/ui/world-lab/runLibrary.ts | Card grid from GET /mapv10/runs/; click-to-load via direct renderer.loadRun(manifestUrl) |
viewer/src/ui/world-lab/compareRuns.ts | Two-pane run diff with synced scroll |
viewer/src/ui/world-lab/ajvPipeline.ts | Compiles the 8 committed schemas under Ajv2020 strict mode |
viewer/src/ui/world-lab/vocabularyPickers.ts | Shared token-picker widgets reading WorldVocabularyResolver |
viewer/src/ui/world-lab/worldLabTypes.ts | Local type aliases mirroring schema enums |
viewer/server/runLaunchMiddleware.ts | Vite middleware exposing run-launch + SSE-stream endpoints |
viewer/server/mapv10ArtifactMiddleware.ts | Extends 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:
- 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). - Allocate a launch id and a temp directory
examples/map/mapv10/recipes/.tmp/launch-<launchId>/. - Write
recipe.json, optionallyintents.jsonandvocabulary.json, to the temp directory. - Spawn the generator. Discovery:
- if
process.env.MAPV10_GENERATOR_BINis set and the path exists, spawn that binary directly with no prefix args; - otherwise spawn
cargo run --release --insideexamples/map/mapv10/generator/.
- if
- Stream
stdout/stderr/exitover SSE onGET /mapv10/runs/launch/<launchId>/stream. Theexitevent carriescode,signal, the manifest URL on success (code === 0), and an error string list on spawn failure. - 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 ifmanifest.recipeRefis set, otherwisenull;generatedAtUnixMillis— generator-recorded timestamp;thumbnailPath— relative path to the firstpreviews/*-preview.pngartifact, ornull.
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.