The existing `disabled = function() return not mode_is("direct")
end` predicate already covers Cell-Tile mode (which is also
non-direct). Self-test t11 enumerates the three transform items
across all three modes and confirms each is disabled in
auto-tile AND cell-tile, enabled only in direct.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
draw_world_overlay gains a cell-tile branch that renders a faint
render-cell grid and a 1-px hover-cell outline around the cursor
cell. The existing G-toggle continues to gate the entire overlay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
paint_cell_material_at and erase_cell_material_at wrap
maps.set_cell_material with a mark_dirty. The canvas-click
dispatch in M.handle_click and the per-frame drag handler both
route through these actions when state.mode == "cell-tile". Self-
test t10 confirms a paint round-trips through maps.get_cell_material.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MODE_PILL_W widens 130 -> 195 (3 x 65). mode_pill_layout returns
the new cell segment between auto and direct; draw_mode_pill
iterates the new triple; the click-handler dispatches the cell
segment to actions.set_mode("cell-tile"). Self-test t9 confirms
the three segments sit at consecutive SEG_W offsets.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
state.set_mode now accepts "cell-tile"; toggle_mode is renamed
cycle_mode (with a backwards-compat alias) and now advances
auto-tile -> cell-tile -> direct -> auto-tile. Tools > Mode
submenu gains a third entry between Auto-Tile and Direct.
Self-test gains t8 (set/get round-trip) and t12 (cycle order).
Startup banner updated to "Tab=cycle mode". Manifest dep pin
bumped to lib-core.maps 0.5.7 to match installed version.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a self_test() entry-point that the editor invokes once when
SPOREL_CI=1 is set, exercising the seven §7.1 acceptance checks
from the menu-UI spec: open/close, toggle flip, modal absorb,
disabled-item no-op, mode switch, category switch, and layer
visibility submenu round-trip. Any failed engine.test assertion
exits with non-zero.
Fixes cheatsheet modal on_click to close only on inside-bounds
click (was closing on any click, breaking t3 outside-click-absorb
assertion).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates the in-editor Cheatsheet to reflect the new G binding and
the Esc cascade; bumps manifest.module and the file header to
v0.3.0; adds a Menu UI section to README.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the eight-button top toolbar, its layout helpers, click
dispatcher and all TOOLBAR_* constants. Side-panel and palette
offsets now anchor to MENU_BAR_H. Mode-pill no longer overlaps a
parallel toolbar element.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both render and click handlers are now gated by the
is_layers_panel_shown / is_palette_shown predicates, so the new
Window menu toggles produce visible effects in the editor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three new modal entries plugged into the unified modal stack.
save_as accepts a typed filename and writes via
maps.save_to_disk; map_properties shows current map id, size and
atlas list as a read-only inspector; about is static text only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
One level of submenu nesting. View > Layers exposes all eight
terrain-stack layers as toggles, sharing state with the right-side
Layers panel. Tools > Mode mirrors the Mode-pill's auto/direct
toggle with a Tab hotkey shown on Direct. Submenu opens by clicking
the parent item; clicking elsewhere closes everything.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Items in File / Edit / View / Map / Tools / Window / Help. New Map
and Open Map appear as disabled placeholders pending a v3-aware
maps.create and engine.asset.list_dir. Undo/Redo are disabled
pending an edit-stack. Cycle Rotation / Toggle Flip / Reset
Transform disable themselves in Auto-Tile mode via a mode_is()
callback. Submenus for View > Layers and Tools > Mode are
declared but their items array stays nil until the submenu render
slice.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Routes clicks on dropdown items to their per-type handler:
action.fire, open_modal for modal items, toggle.fire for toggles.
Disabled items absorb without closing; separators are inert.
Clicks outside the open dropdown close the menu and fall through to
canvas handling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renders the open dropdown for the active menu category with a
three-column item layout (indicator | label | hotkey). Toggle items
show their bound is_on() state with a [v]/[ ] indicator, modal
items append "...", separators draw as thin rules, disabled items
render in a muted colour. Submenu chevron is wired but submenus
themselves land in a later slice.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the seven category labels (File, Edit, View, Map, Tools,
Window, Help) at the top of the screen with hover highlight and
click-to-open / click-to-close behaviour. Item arrays are still
empty stubs; dropdown rendering lands in the next slice.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sticky two-segment pill anchored to the menu bar's right edge.
Click on a segment calls actions.set_mode; Tab hotkey continues to
toggle and the pill reflects the new state next frame. Pill
visually overlaps the legacy toolbar in this slice; the toolbar is
removed in a later commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Esc no longer immediately exits the editor; it first dismisses any
open modal, then any open dropdown menu, and only falls through to
engine.exit() when no overlay is up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces a single-stack modal table where each entry declares
title, dimensions, render fn and on_click fn. The existing
Cheatsheet becomes the framework's first entry; behaviour is
preserved exactly. Dispatcher absorbs clicks while any modal is
open and routes them to the modal's on_click for close-decision.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds menu_open, modal_open, show_layers_panel, show_palette to the
editor's state singleton; removes the now-redundant cheatsheet_open
flag. All cheatsheet-toggle paths route through the new generic
modal_open API. Visible behaviour unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a G key that toggles the world-space overlay on and off
(default on). The overlay adapts to the active painting mode:
- Auto-Tile mode shows the map-grid (paint-tile boundaries, which
sit at half-tile offsets from the render-cell boundaries in
dual-grid), the per-map-tile vertex dots, and the snapped
paint-target highlight.
- Direct mode shows only a subtle render-cell grid, since LMB
strokes write directly to render-cells in that mode.
Bumps the lib-core.maps dep pin to 0.5.6 to pick up the dual-grid
bitmask fix that makes the auto-tile output match the dual-grid
mental model.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diagnostic for the dual-grid + blob-14 painting model: D-key toggles
a per-cell text annotation rendered over the map. For each material
cell on the active layer, draws "S<slot>r<rot>" (and "f" when flip=1)
in green at the cell's top-left, sourced from the new lib-core.maps
0.5.5 cell_material_slot getter.
Use case: confirm that the renderer actually picks distinct slots
(corner_full / tee_full / cross / etc.) for painted regions rather
than always picking slot 13 solid — the rectilinear style fills enough
of each cell that visually adjacent slots 3/7/13 are hard to
distinguish without numerical labels.
Inline today; plan-stub
`docs/superpowers/plans/2026-05-29-debug-overlay-extract-to-lib.md`
deferred until a 2nd consumer (vagrant debug-render, puppet-editor,
etc.) appears.
Also bumps lib-core.maps dep 0.5.4 -> 0.5.5.
Replaces the placeholder colour swatches with actual atlas sub-tex
draws via the new lib-core.maps 0.5.4 getters:
- atlas_diffuse_handle(atlas_idx) -> raylib Texture2D handle
- atlas_tile_uv(atlas_idx, slot) -> {x, y, w, h} or nil
Renders each slot 1..14 (lib slot 0..13) scaled to fit the swatch
rect with draw_sprite_transform. Falls back to the legacy colour
square + slot-number label when the atlas isn't loaded (headless,
material missing, etc.).
Drives off state.get_active_atlas() (defaults to 0 — fine for the
single-atlas work map; multi-atlas selector is a future slice).
Also bumps lib-core.maps dep 0.5.3 -> 0.5.4.
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>
Without the camera scope, maps.draw_map() rendered the map at
screen coords starting at (0,0) regardless of camera.set_target,
and screen_to_world's inverse produced bogus cell coords (mouse
in screen-center mapped to top-left tile). Spine-prototype's
render() shows the right pattern: world-space draws inside
camera.begin/finish, screen-space overlays after.
The engine module-lifecycle looks up HOOK_RENDER by the global
name 'render' (engine_module_lifecycle.c:166). The previous draw()
function was a global with no caller, so the editor's UI never
rendered — the engine fell back to its 'no render hook defined'
placeholder.
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.
The engine's Lua sandbox rejects require() for non-declared-dep
module names like 'src.state'. Multi-file modules in this codebase
either inline everything (spine-prototype, vagrant-skeleton) or
use dofile (lib-sporel.launcher). For three modules that share a
state singleton, dofile would cause state divergence. Cleanest
fix is to inline all three into init.lua via IIFE-wrapped local
module tables, preserving the file-internal namespacing while
satisfying the sandbox.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
init.lua loads the work map, binds hotkeys (E/R/S/Esc), tracks
mouse-cell per frame, dispatches clicks to the UI hit-tester
and falls through to paint-cell for unconsumed map-area clicks.
run-map-editor.sh handles copy-in/copy-out for a real target map
or runs in-place against the module's work slot.
Vertical layer-chip column on the left edge (Roof up top with
divider, entity-slot dashed line between Lw and Ts), collapsible
atlas+tile picker bottom-left, status chip bottom-right. Each
frame redraws everything and dispatches mouse clicks via priority
hit-tests. Tile thumbnails are placeholder gray rects in v0.1.0
because lib-core.maps does not yet expose per-tile UVs to Lua.
paint_cell_at, erase_cell_at, cycle_rotation, save plus thin
wrappers for the UI hit-tests (set_active_layer/_atlas/_tile,
toggle_layer_visible/_roof_mode/_picker). Each action updates
state and delegates the actual map mutation to lib-core.maps.
Singleton table covering map id+path, active layer/atlas/tile/rot,
roof-mode flag, per-layer visibility flags, dirty flag, and picker
open/close state. Mutations go through setters so the test module
can verify state transitions without poking the internal table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.