Files
sporel-module-map-editor/README.md
2026-06-16 14:47:57 +00:00

192 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# sporel-module-map-editor
Interactive map editor for Sporel `.map.json` files. Aufsetzend auf der
Rev-4-UX-Architektur (siehe Design-Paper
`design/2026-05-28-autotile-blob-styles-design.md` §11 in `sporel-meta`):
Top-Toolbar mit Mode-Switch + Actions, rechtes Layers-Side-Panel, untere
Palette-Strip mit Material-Slot-Thumbnails, Maus-Drag-Paint und drei
Paint-Modi (Auto-Tile / Direct / Cell-Tile, mit Tab umschaltbar). Die
v0.3.0- bis v0.4.0-Iterationen sind eingeflossen: v0.3.0 ersetzte die
Toolbar durch eine Menu-Bar, v0.3.1 ergänzte den Cell-Tile-Modus, und
v0.4.0 migrierte Toolbar, Layers-Panel und Palette auf
`lib-core.panel`-Persistent-Widgets (Details siehe unten).
**Version:** 0.4.0
**Module-ID:** map-editor
**Requires:** lib-core.maps v0.5.7, lib-core.render v0.2.0, lib-core.composition v0.3.0, lib-core.camera v0.3.0, lib-core.input v0.4.0, lib-core.panel v0.4.0, lib-asset.prototype-blob-geom v0.1.0
## 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
bash scripts/run-map-editor.sh
```
With a target — copy a real map in, edit, copy back out:
```bash
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`):
```text
Doubleclick C:/Games/Sporel/bin/Sporel.exe → launcher → map-editor
```
## UI
- **Top toolbar** (full-width): `Auto-Tile`/`Direct` mode 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.
```bash
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=0` only.
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 `flip`
field; combined with `rot` (0-3) the single flip covers all 8 D4
orientations, so the second button would be redundant.
- **No modifier hotkeys**: `lib-core.input` doesn'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_color` re-bake trigger
+ tint editor are deferred (also `0.2.0d`).
- **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 in `sporel-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).