192 lines
8.5 KiB
Markdown
192 lines
8.5 KiB
Markdown
# 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).
|