LMB-hold strokes a line of paints (Auto-Tile: each hovered vertex;
Direct: each hovered cell as an override). RMB-hold strokes a line
of erases (clear_vertex / clear_override). On mouse-press inside the
UI (toolbar / palette / side-panel) the drag never enters paint-mode
so toolbar interaction stays unaffected.
State machine:
- press(button) outside UI -> drag_paint = "paint"|"erase"
- while drag_paint and corresponding button still down -> dispatch
paint at current snap_vertex / mouse_cell each frame
- button release -> drag_paint = nil
Idempotency: maps.set_vertex / set_override / clear_* are no-op when
the target state is unchanged, so the per-frame repaint is cheap and
doesn't grow the JSON on save.
State + actions:
- active_flip (0/1) joins active_rot (0..3). Combined the two cover
all 8 D4 orientations; single Flip button is sufficient (H+V
would be redundant).
- toggle_flip / reset_transform / paint_override_at /
erase_override_at.
Toolbar:
- New Flip (H) + Reset (0) buttons next to Rotate (R).
- Status chip extended with `slot=N rot=N flip=N` indicator.
Paint:
- Direct-mode + LMB on canvas now calls maps.set_override on the
active material-bound terrain layer. Bare slot int when canonical
(rot=0+flip=0), {slot, rot, flip} object otherwise — matches the
v0.5.1 override-format extension. Palette slot 1..14 maps to lib
slot 0..13 (palette is user-friendly 1-based; lib API is 0-based).
- Direct-mode + RMB on canvas calls maps.clear_override.
- Auto-Tile mode unchanged; transform state is ignored there (the
renderer derives slot/rot/flip from the vertex bitmask).
Hotkeys:
- h -> flip (new)
- 0 -> reset_transform (new)
- r continues to cycle rotation CW; Shift+R for CCW is out because
lib-core.input has no modifier-combo support (deferred).
Cheatsheet rows updated.
work.map.json was still schema-v2 with the legacy fa_terrain_v1
atlas — neither has a `material` field on its layers, so the
renderer's vertex/blob path never fired and Auto-Tile painting had
no visible effect (the set_vertex writes landed but the renderer
fell back to the cell-tile path that doesn't consume them).
Migration:
- schema_version 2 -> 3
- size 4x4 -> 16x16 (room to paint)
- atlases: fa_terrain_v1 -> blob_rect_stone (S-V2E2-RM-Blob 14-slot
atlas from lib-asset.prototype-blob-geom)
- layers: surface + wall, both with "material": "blob_rect_stone";
vertices+overrides start empty (lib-core.maps lazy-allocates on
first set_vertex)
Manifest:
- asset_aliases: fa_terrain_v1 -> blob_rect_stone (lib-asset.
prototype-blob-geom)
- deps: drop prototype-fa-starter, add prototype-blob-geom 0.1.0
UI:
- Bottom palette strip (above status, left of side-panel): 14
coloured swatches numbered 1-14. Click selects state.active_tile.
- World-space overlay (Auto-Tile mode only): cell grid + vertex
dots + snapped-vertex highlight ring.
- Side-panel + status repositioned to clear the palette band.
- Cheatsheet updated with the new LMB/RMB canvas + palette rows.
Paint:
- Auto-Tile mode: LMB on canvas -> maps.set_vertex(active_layer,
snap.vx, snap.vy, true); RMB -> set_vertex(..., false). Snap is
computed in update() by rounding world coords to the nearest
(W+1)x(H+1) vertex; off-map cursor publishes nil.
- Direct mode: unchanged from 0.2.0a (paint_cell_at with active
slot + rotation).
Notes:
- Palette swatches are placeholder solid colours, not real tile
thumbnails — that needs a tile-UV getter on lib-core.maps, which
is out of scope for this sub-slice.
- Transform controls, Direct-Override transform writes, and the
Decal/Entity sub-selectors land in 0.2.0c/d.
Layout (per design paper §11):
- Top toolbar with Mode buttons (Auto-Tile / Direct), Save, Erase,
Rotate, Help.
- Right Layers side-panel (8 rows, top-down draw order) as single
source of truth for visibility + active layer — replaces the old
left-edge layer chips.
- Bottom status bar.
State + actions:
- mode = "auto-tile" | "direct" (Tab toggles).
- cheatsheet_open modal toggle (I key, dimmed overlay + key/action
table).
- SPOREL_CI=1 auto-exits after 30 frames (matches vagrant pattern;
ci_frames in manifest is not engine-enforced for kind:"module").
Paint behaviour itself is unchanged from 0.1.0 — bottom palette + the
real Auto-Tile / Direct paint loops land in 0.2.0b/c/d.
deps: lib-core.maps 0.5.1 -> 0.5.2 (consumes set_vertex / get_vertex).
Required by the schema-v3 work in lib-core.maps. The map-editor
v0.1 stays on the legacy tiles[] path and continues to function
unchanged through the auto-upgrade v1->v2->v3 chain on load.
Full migration to v3's vertex-painting + override-UI lands in the
map-editor v0.2 slice
(sporel-meta/docs/superpowers/plans/2026-05-28-map-editor-blob-v2.md).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Other module manifests (spine-prototype, vagrant-skeleton, etc.) use
the short directory name as the id, not the long 'sporel-module-X'
form which is reserved for the Gitea slug. The mismatch caused the
launcher script (which exports SPOREL_MODULE=map-editor) to fail
with ENGINE_ERR_MODULE_NOT_FOUND.
Empty launchable module with deps on lib-core.maps 0.4.0, render,
camera, input, and the fa_terrain_v1 asset alias. Renders the
default 4x4 empty work-map; interactive editing wired in
subsequent commits.