Engine resolver does exact strcmp on dep versions, so consumer manifests must pin the new lib-core.panel 0.4.0 to keep load-time resolution green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sporel-module-map-editor
Interactive map editor for Sporel .map.json files. v0.2 series ships
the Rev 4 UX architecture (see design paper
design/2026-05-28-autotile-blob-styles-design.md §11 in sporel-meta):
top toolbar with mode-switch + actions, right Layers side-panel, bottom
palette strip of material slot thumbnails, mouse drag-paint, and two
mutually-exclusive paint modes (Auto-Tile vs Direct).
Run
Default — edit the module's in-place work map (maps/work.map.json,
a 16×16 schema-v3 map bound to the blob_rect_stone material):
bash scripts/run-map-editor.sh
With a target — copy a real map in, edit, copy back out:
bash scripts/run-map-editor.sh ~/Projects/Sporel/sporel-modules/spine-prototype/maps/demo.map.json
Or via the install layer (per install-reference.sh):
Doubleclick C:/Games/Sporel/bin/Sporel.exe → launcher → map-editor
UI
- Top toolbar (full-width):
Auto-Tile/Directmode buttons (mutually exclusive, Tab toggles),Save(S),Erase(E),Rotate(R),Flip(H),Reset(0),Help(I). Each button shows its hotkey label inline. The active mode is highlighted. - Right Layers side-panel (8 rows top-down:
canopy,upper_wall,wall,lower_wall,topsurface,surface,subsurface,foundation). Per row: visibility eye + active marker + layer name.- Left-click name → set active layer
- Left-click eye → toggle layer visibility
- Right-click row → also toggles visibility
- Bottom palette strip: 14 swatches (slots 1-14). Real tile
thumbnails rendered from the active atlas via
maps.atlas_tile_uv. Active slot has a gold border.- Left-click → set active slot
- Status chip (bottom-left of palette band): mouse-cell, dirty flag
(
*when unsaved), and current transform indicator (slot=N rot=N flip=N) for Direct-mode awareness. - Auto-Tile-mode world overlay: cell grid (1 px), vertex dots (4 px squares), and a snapped-vertex highlight ring (green) at the nearest vertex to the cursor.
- Cheatsheet modal (I): centered dim-overlay listing all hotkeys
- LMB/RMB drag semantics.
Modes
| Mode | LMB on canvas | RMB on canvas | Notes |
|---|---|---|---|
| Auto-Tile | Paints vertex at snap target — maps.set_vertex(layer, vx, vy, true). Any-corner rule flips up to 4 surrounding cells to material; renderer derives slot/rot/flip from neighbour bitmask. |
Clears vertex — set_vertex(..., false) |
Transform controls ignored (slot is derived from neighbours). |
| Direct | Writes per-cell override — maps.set_override(layer, x, y, {slot, rot, flip}). Bare slot int when canonical (rot=0+flip=0). |
Clears override — maps.clear_override(layer, x, y) |
Honours active Rot/Flip from toolbar. Use to force-place a specific orientation that the auto-derived slot wouldn't pick. |
Both modes support drag-paint: hold LMB (or RMB) and stroke across the canvas to apply the action at every hovered vertex/cell. UI hits on the toolbar / side-panel / palette never enter drag-mode.
Hotkeys
| Key | Action |
|---|---|
Tab |
Toggle mode (Auto-Tile ↔ Direct) |
I |
Show / hide cheatsheet |
S |
Save current map to work.map.json |
E |
Erase tile at mouse cell |
R |
Cycle rotation 0 → 1 → 2 → 3 → 0 (Direct-mode override) |
H |
Toggle flip 0 ↔ 1 (Direct-mode override) |
0 |
Reset transform (rot=0, flip=0) |
Esc |
Quit |
LMB-drag |
Stroke-paint (mode-dependent) |
RMB-drag |
Stroke-clear |
Headless / smoke
ci_frames = 30 in manifest.module. The editor reads
SPOREL_CI=1 and auto-exits after 30 frames so the smoke harness
can run it without a window-close click.
SPOREL_CI=1 ./Sporel.exe --module=map-editor # exits with rc=0 after ~0.5 s
Limitations (current scope)
- Single-atlas palette: the palette draws from
atlas_idx=0only. Multi-atlas selector is a future slice. Workaround: keep one material atlas per map for now. - Single Flip flag: design paper mentions Flip-H + Flip-V as
separate controls, but the override format only stores one
flipfield; combined withrot(0-3) the single flip covers all 8 D4 orientations, so the second button would be redundant. - No modifier hotkeys:
lib-core.inputdoesn't expose modifier combos, so Shift+R for CCW rotation isn't supported. Workaround: press R three times. - No Decal / Entity sub-selectors: Direct mode on non-terrain
layers is unimplemented (lands in
0.2.0d). - No Material-Properties modal:
base_colorre-bake trigger- tint editor are deferred (also
0.2.0d).
- tint editor are deferred (also
- No multi-vertex brush sizes / procedural fill / undo-redo — see design paper §11 "Out of scope" for the full list.
Plan + spec
- Design paper:
docs/design/2026-05-28-autotile-blob-styles-design.md§11 insporel-meta - Plan:
docs/superpowers/plans/2026-05-28-map-editor-blob-v2.md
Menu UI (v0.3.0)
Top toolbar replaced with a seven-category menu bar (File, Edit, View, Map, Tools, Window, Help) plus a sticky two-segment Mode-pill on the right end. Item types: action, modal, toggle, submenu, separator; items can be conditionally disabled. A single-stack modal framework hosts the existing Cheatsheet (I) and three new modals: Save As, Map Properties (read-only), About. The Esc key cascades modal -> menu -> quit.
New Map and Open Map appear in the File menu as disabled placeholders pending a v3-aware maps.create and an engine.asset.list_dir in a future engine + lib slice.
All previous hotkeys (Tab, S, E, R, H, 0, D, G, I, Esc) continue to work; their bindings are documented in the items where applicable and in the in-editor Cheatsheet modal.
Cell-Tile Mode (v0.3.1)
Third painting mechanic alongside Auto-Tile and Direct. LMB paints a per-cell material flag at the cursor cell; RMB clears it. The renderer uses the classical 47-blob 8-neighbour bitmask for cell-tile cells, unlocking the 9 atlas slots that Auto-Tile's dual-grid 4-corner rule structurally cannot reach (isolated, end, corner_open, straight, tee_open, tee_half, cross_open, cross_q1, cross_q2adj).
Auto-Tile (vertex-paint, 5 reachable slots, dual-grid look) and Direct (manual override placement, all 14 slots via explicit pick) are unchanged. Tab cycles through all three modes.
For clean visuals, use a single painting mode per layer. Mixing Vertex-Tile and Cell-Tile material in the same layer is allowed but produces an asymmetric seam at the boundary because vertex-tile cells are structurally blind to their cell-tile neighbours.
See sporel-meta/docs/superpowers/specs/2026-06-01-map-editor-cell-tile-mode-design.md
for the full design rationale and the Boris-taxonomy background.
Panel-Lib Migration (v0.4.0)
The top toolbar, right-side Layers panel, and bottom palette strip
have been migrated to lib-core.panel v0.3.0 persistent widgets
(W.2 of the panel-window-manager plan):
| Widget ID | Layout slot | Role |
|---|---|---|
map-editor.toolbar |
top strip (full width, 290 px) | Menu bar + mode-pill + open-dropdown |
map-editor.layers |
right side-panel (160 px wide) | Layer rows + roof toggle |
map-editor.palette |
bottom strip (full width, 64 px) | Tile-thumbnail swatches |
All three are registered as persistent=true (open from module-init,
not dismissed by ESC), input_block="self" (hit-test claims clicks
within own bounds; misses fall through to canvas), and chromeless=true
(panel-lib skips its title-bar / border / close-X — the widgets paint
their own existing style). The map-canvas is NOT a panel widget — it
remains module-rendered between camera.begin/finish. Canvas drag-paint
is gated on panel.point_in_any_panel(mx, my) so widget clicks no
longer bleed through into the underlying map cells.
The Window menu's Layers Panel / Palette toggles now flip the
persistent widgets' open-state via panel.toggle(id) directly.
Modals (cheatsheet, save-as, map-properties, about) and the bottom-left
status chip stay module-rendered (drawn AFTER panel.render() so they
appear on top of widget panels).