8 Commits

Author SHA1 Message Date
calic
7c9e481a6c fix(deps): cascade composition 0.4.0 + crafting 0.5.0 pins
Phase J (2026-08-03) bumped lib-core.composition 0.3.0->0.4.0 and
lib-core.crafting 0.3.0->0.5.0, but only vagrant-skeleton and
lib-core.crafting themselves were updated. Dependent manifests kept the
old pins, so dep-fetcher saw disagreeing pins on the same lib and
refused to start both spine-prototype (ENGINE_ERR_LIB_DEP_MISSING,
rc=19) and vagrant-skeleton ("dep check failed", conflict on
lib-core.composition + lib-core.crafting).

Unify all pins on the on-disk versions. No code changes required:
composition 0.4.0 is purely additive (the quality/condition loud-error
was removed; no signature changed), and crafting-display only consumes
crafting.can_craft + crafting.list_recipes, both still present, and
never referenced the `form` field retired by ADR-0055.

README "Requires:" lines synced to match the manifests (Gate 3).

milestone-check.sh: GREEN (build, ctest, smoke, devwrap-ci,
test-all-modules + anomaly scan). vagrant-skeleton headless run clean
at 60 CI frames.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 11:27:22 +02:00
calic
f1cddc3948 chore(deps): bump lib-core.render to 0.3.0 (cascade — additive sprite_scale)
No code change. v0.3.0 adds an optional sprite_scale property on
draw_entities path-1; map-editor doesn't set it, so default 1.0
preserves v0.2.0 behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-21 11:44:28 +02:00
Calic
75655be72d docs: CR-5 — README+launcher-card auf v0.4.0 (3 Paint-Modi), Requires-Block ergaenzt
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 14:47:57 +00:00
Calic
4b8abc8502 chore: bump lib-core.panel dep to 0.4.0
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>
2026-06-15 03:05:30 +02:00
Calic
176305e35b feat: v0.4.0 migrate to lib-core.panel persistent widgets
Migrate the top toolbar, right-side Layers panel, and bottom palette
strip to lib-core.panel v0.3.0 persistent widgets (W.2 of the
panel-window-manager plan):

  map-editor.toolbar -- top strip (full width, ~290 px tall)
  map-editor.layers  -- right side-panel (160 px wide)
  map-editor.palette -- bottom strip (full width, 64 px tall)

All three 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/X — widgets keep their own
visual style).

Existing draw_menu_bar / draw_side_panel / draw_palette and
handle_click logic is wrapped via thin widget render + handle_input
fns (M.draw_toolbar_widget / M.handle_toolbar_click etc.); no
behavioural change to the widget interiors.

Map-canvas remains module-rendered. Canvas drag-paint is now gated on
panel.point_in_any_panel(mx, my) so widget clicks no longer bleed
through into map paint.

Removed state.show_layers_panel / show_palette and their toggle
helpers (panel-lib manages widget visibility; Window menu items now
flip panel.is_open/toggle on the widget ids directly). Modals stay
module-rendered + module-routed (handled by handle_modal_click
before panel.update).

Manifest bumped 0.3.4 -> 0.4.0; lib-core.panel dep added at 0.3.0.
2026-06-15 02:26:20 +02:00
Calic
a150dd8dc5 chore: bump lib-core.composition dep to 0.3.0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-14 11:57:40 +02:00
Calic
61e7d5192f chore: bump lib-core.composition dep to 0.2.0 2026-06-13 14:07:46 +02:00
Calic
451da04cbd feat: ship manifest.launcher.md for master-detail launcher 2026-06-11 07:57:58 +02:00
4 changed files with 317 additions and 99 deletions

View File

@@ -1,11 +1,19 @@
# sporel-module-map-editor
Interactive map editor for Sporel `.map.json` files. v0.2 series ships
the Rev 4 UX architecture (see design paper
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 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).
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.4.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
@@ -153,3 +161,31 @@ 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).

342
init.lua
View File

@@ -1,4 +1,4 @@
-- sporel-module-map-editor v0.3.1
-- sporel-module-map-editor v0.4.0
-- Interactive map editor — Rev 4 UX architecture (see design paper
-- 2026-05-28-autotile-blob-styles-design.md §11 + plan stub
-- 2026-05-28-map-editor-blob-v2.md). 0.2.0b adds the bottom palette
@@ -22,7 +22,14 @@
-- active layer renders "S<slot>r<rot>" (and "f" when flip=1) via the
-- new lib-core.maps 0.5.5 cell_material_slot getter. Inline today;
-- extract to lib-sporel.debug-overlay when a 2nd consumer appears.
-- Decal/Entity sub-selectors + Material-Properties modal land in 0.2.0d.
-- 0.4.0 migrates the top-toolbar / right side-panel / bottom palette
-- to lib-core.panel v0.3.0 persistent widgets (z_tier="normal",
-- chromeless, input_block="self"). Canvas + status + modals stay
-- module-rendered. Canvas-paint is gated on
-- panel.point_in_any_panel(mx, my) so panel-widget clicks no longer
-- bleed through to drag-paint. Removed Window > Layers Panel + Palette
-- toggle items (panel-lib manages visibility; persistent widgets are
-- always on).
--
-- All editor logic in this single file: engine sandbox only allows
-- require() for declared lib-deps; multi-file modules either use
@@ -33,6 +40,7 @@
local maps = require("lib-core.maps")
local camera = require("lib-core.camera")
local input = require("lib-core.input")
local panel = require("lib-core.panel")
-- CI-mode auto-exit (matches vagrant-skeleton pattern). Manifest declares
-- ci_frames=30 but engine does not auto-enforce for kind:"module"; we count
@@ -70,8 +78,9 @@ local state = (function()
submenu_open = nil, -- 0.3.0: label of the open submenu item within menu_open
modal_open = nil, -- 0.3.0: nil | <modal_id> (single-stack)
modal_text_input = "", -- 0.3.0: scratch buffer for modal text inputs
show_layers_panel = true, -- 0.3.0: Window > Layers Panel toggle
show_palette = true, -- 0.3.0: Window > Palette toggle
-- 0.4.0: visibility of layers-panel + palette is now managed by
-- lib-core.panel (persistent widgets). The Window-menu items
-- flip panel.is_open("map-editor.{layers,palette}") directly.
mouse_cell = nil, -- {x, y} or nil if mouse off-map
snap_vertex = nil, -- 0.2.0b: {vx, vy} or nil (auto-tile mode snap target)
drag_paint = nil, -- 0.2.0c.1: nil | "paint" | "erase" (active drag-stroke mode)
@@ -179,11 +188,9 @@ local state = (function()
function M.get_submenu_open() return state.submenu_open end
function M.close_submenu() state.submenu_open = nil end
function M.is_layers_panel_shown() return state.show_layers_panel end
function M.toggle_layers_panel() state.show_layers_panel = not state.show_layers_panel end
function M.is_palette_shown() return state.show_palette end
function M.toggle_palette() state.show_palette = not state.show_palette end
-- 0.4.0: layers-panel + palette visibility moved to lib-core.panel
-- (persistent widgets managed via panel.is_open/toggle on the
-- "map-editor.layers" + "map-editor.palette" widget ids).
function M.set_mouse_cell(x, y)
if x == nil then
@@ -231,8 +238,6 @@ local state = (function()
state.submenu_open = nil
state.modal_open = nil
state.modal_text_input = ""
state.show_layers_panel = true
state.show_palette = true
state.debug_overlay = false
state.world_overlay = true
for name in pairs(state.layer_visible) do
@@ -586,13 +591,15 @@ local ui = (function()
end
end
-- 0.4.0: panel-lib manages widget visibility via panel.is_open/toggle.
-- The Window-menu items now flip the persistent widgets' open-state.
local WINDOW_ITEMS = {
{ type = "toggle", label = "Layers Panel",
is_on = function() return state.is_layers_panel_shown() end,
fire = function() state.toggle_layers_panel() end },
is_on = function() return panel.is_open("map-editor.layers") end,
fire = function() panel.toggle("map-editor.layers") end },
{ type = "toggle", label = "Palette",
is_on = function() return state.is_palette_shown() end,
fire = function() state.toggle_palette() end },
is_on = function() return panel.is_open("map-editor.palette") end,
fire = function() panel.toggle("map-editor.palette") end },
}
local HELP_ITEMS = {
@@ -1211,10 +1218,18 @@ local ui = (function()
end
-- =====================================================================
-- Public API
-- Public API (v0.4.0 split for panel-lib persistent widgets)
-- =====================================================================
--
-- The 3 persistent widgets (toolbar / layers / palette) get their own
-- render + handle_input pair. M.draw() now only paints the chrome that
-- is NOT a panel-widget: the bottom-left status chip and any open
-- modal overlay. Modals stay module-rendered + module-routed (they
-- are top-tier transient and need to fully absorb input via the
-- existing dispatch_modal_click path).
function M.draw()
-- Toolbar widget: menu-bar + mode-pill + open-dropdown render.
function M.draw_toolbar_widget(_ctx)
local mx, my = engine.input.get_mouse_pos()
draw_menu_bar(mx, my)
local open_name = state.get_menu_open()
@@ -1227,12 +1242,25 @@ local ui = (function()
end
end
draw_mode_pill()
if state.is_layers_panel_shown() then
draw_side_panel(mx, my)
end
if state.is_palette_shown() then
draw_palette(mx, my)
end
end
-- Layers widget: right-side panel with per-layer rows + roof toggle.
function M.draw_layers_widget(_ctx)
local mx, my = engine.input.get_mouse_pos()
draw_side_panel(mx, my)
end
-- Palette widget: bottom strip with tile-thumbnail swatches.
function M.draw_palette_widget(_ctx)
local mx, my = engine.input.get_mouse_pos()
draw_palette(mx, my)
end
-- M.draw() draws non-widget chrome: status chip + modal overlay.
-- Called by init.lua's render() AFTER panel.render(), so modals
-- correctly appear on top of all widget panels.
function M.draw()
local mx, my = engine.input.get_mouse_pos()
draw_status(mx, my)
local mid = state.get_modal_open()
if mid and MODALS[mid] then
@@ -1334,14 +1362,22 @@ local ui = (function()
end
end
-- Hit-test mouse click. Returns true if the click was consumed by UI.
-- button: "left" | "right"
function M.handle_click(mx, my, button)
-- Modal absorbs all clicks while open.
if dispatch_modal_click(mx, my, button) then
return true
end
-- v0.4.0: hit-test split into per-widget handlers + a module-level
-- modal dispatcher. The 3 widget panels (toolbar / layers / palette)
-- get their own handle_input fns; the modal-click flow stays at
-- the M.update level since modals are NOT panel widgets.
--- M.handle_modal_click(mx, my, button) — returns true if modal
--- consumed the click. Called BEFORE panel.update() so a modal can
--- swallow clicks before they reach widget dispatch.
function M.handle_modal_click(mx, my, button)
return dispatch_modal_click(mx, my, button)
end
--- M.handle_toolbar_click(mx, my, button) — handles menu-bar +
--- mode-pill + dropdown clicks. Wired into the toolbar widget's
--- handle_input via panel-lib. Returns true if consumed.
function M.handle_toolbar_click(mx, my, button)
-- Mode-pill
local pill = mode_pill_layout()
for _, seg in ipairs({ pill.auto, pill.cell, pill.direct }) do
@@ -1426,52 +1462,58 @@ local ui = (function()
end
end
-- Click outside the dropdown closes it (no return — fall through so
-- the click can also hit other UI like canvas).
-- Click outside the dropdown closes it (no return — fall
-- through so other UI can still hit-test the click).
state.close_menu()
end
-- Bottom palette swatches: left = select slot
if state.is_palette_shown() then
for _, sw in ipairs(palette_swatches_layout()) do
if in_rect(mx, my, sw.x, sw.y, sw.w, sw.h) then
if button == "left" then
actions.set_active_tile(sw.slot_id)
end
return true
end
end
end
-- Side-panel layer rows: left = set active; right = toggle visibility
if state.is_layers_panel_shown() then
for _, row in ipairs(side_panel_rows_layout()) do
if in_rect(mx, my, row.eye_x, row.eye_y, row.eye_w, row.eye_h) then
if button == "left" then
actions.toggle_layer_visible(row.name)
end
return true
end
if in_rect(mx, my, row.x, row.y, row.w, row.h) then
if button == "left" then
actions.set_active_layer(row.name)
elseif button == "right" then
actions.toggle_layer_visible(row.name)
end
return true
end
end
end
-- Status chip — no-op fallback to claim the click area
local s = status_layout()
if in_rect(mx, my, s.x, s.y, s.w, s.h) then
return true
end
return false -- click falls through to map-area handler in init.lua
return false
end
--- M.handle_layers_click(mx, my, button) — left = set active layer;
--- right = toggle visibility. Wired into the layers widget.
function M.handle_layers_click(mx, my, button)
for _, row in ipairs(side_panel_rows_layout()) do
if in_rect(mx, my, row.eye_x, row.eye_y, row.eye_w, row.eye_h) then
if button == "left" then
actions.toggle_layer_visible(row.name)
end
return true
end
if in_rect(mx, my, row.x, row.y, row.w, row.h) then
if button == "left" then
actions.set_active_layer(row.name)
elseif button == "right" then
actions.toggle_layer_visible(row.name)
end
return true
end
end
return false
end
--- M.handle_palette_click(mx, my, button) — left = select slot.
--- Wired into the palette widget.
function M.handle_palette_click(mx, my, button)
for _, sw in ipairs(palette_swatches_layout()) do
if in_rect(mx, my, sw.x, sw.y, sw.w, sw.h) then
if button == "left" then
actions.set_active_tile(sw.slot_id)
end
return true
end
end
return false
end
-- Expose layout fns for the widget custom layout-fns registered in
-- the Setup section. Each widget needs its bounds to match the
-- absolute screen coords the existing draw_* helpers use, so we
-- expose the layout fns directly.
M._side_panel_layout = side_panel_layout
M._palette_layout = palette_layout
M._MENU_BAR_H_const = MENU_BAR_H
-- Expose internal tables needed by self_test (test-only; not part of
-- the public draw/click API).
M._MENU_BAR_H = MENU_BAR_H
@@ -1513,7 +1555,88 @@ input.bind("reset_transform", { "0" }) -- 0.2.0c
input.bind("toggle_debug_overlay", { "d" }) -- 0.2.0c.3
input.bind("toggle_world_overlay", { "g" }) -- 0.2.0c.4
engine.print("map-editor v0.3.1: ready. Tab=cycle mode (Auto/Cell/Direct), I=help, G=grid, D=debug, S=save, E=erase, R=rotate, H=flip, 0=reset, ESC=quit / close")
-- =====================================================================
-- 0.4.0: panel-lib persistent widget registration (W.2)
-- =====================================================================
-- Three persistent + chromeless + input_block="self" widgets cover the
-- top toolbar, right-side layers panel, and bottom palette strip. Each
-- widget's bounds match what the existing draw_* helpers expect — they
-- still draw at absolute screen coords via engine.window.size(). The
-- panel-lib's chrome (bg/border/title/X) is suppressed via
-- chromeless=true; the widgets draw their own visual style.
--
-- Map-canvas is NOT a widget. Module renders it directly inside
-- camera.begin/finish; module's drag-paint dispatch is gated on
-- panel.point_in_any_panel(mx, my) so widget clicks never bleed
-- through into canvas paint.
-- Toolbar bounds = top strip (menu-bar height + room for open
-- dropdowns). Use a custom layout fn that covers the union of
-- menu-bar + worst-case dropdown extent.
local TOOLBAR_MAX_H = 290 -- menu-bar (24) + max dropdown rows (12 * 22 + pad)
local toolbar_widget = {
title = "Toolbar",
pause_on_open = false,
render = function(ctx) ui.draw_toolbar_widget(ctx) end,
handle_input = function(_ctx, event)
if event.kind == "click" then
ui.handle_toolbar_click(event.x, event.y, event.button)
end
end,
}
panel.register("map-editor.toolbar", toolbar_widget, {
layout = function(sw, _sh)
return { x = 0, y = 0, w = sw, h = TOOLBAR_MAX_H }
end,
z_tier = "normal",
persistent = true,
input_block = "self",
chromeless = true,
})
local layers_widget = {
title = "Layers",
pause_on_open = false,
render = function(ctx) ui.draw_layers_widget(ctx) end,
handle_input = function(_ctx, event)
if event.kind == "click" then
ui.handle_layers_click(event.x, event.y, event.button)
end
end,
}
panel.register("map-editor.layers", layers_widget, {
layout = function(_sw, _sh)
local p = ui._side_panel_layout()
return { x = p.x, y = p.y, w = p.w, h = p.h }
end,
z_tier = "normal",
persistent = true,
input_block = "self",
chromeless = true,
})
local palette_widget = {
title = "Palette",
pause_on_open = false,
render = function(ctx) ui.draw_palette_widget(ctx) end,
handle_input = function(_ctx, event)
if event.kind == "click" then
ui.handle_palette_click(event.x, event.y, event.button)
end
end,
}
panel.register("map-editor.palette", palette_widget, {
layout = function(_sw, _sh)
local p = ui._palette_layout()
return { x = p.x, y = p.y, w = p.w, h = p.h }
end,
z_tier = "normal",
persistent = true,
input_block = "self",
chromeless = true,
})
engine.print("map-editor v0.4.0: ready. Tab=cycle mode (Auto/Cell/Direct), I=help, G=grid, D=debug, S=save, E=erase, R=rotate, H=flip, 0=reset, ESC=quit / close")
-- =====================================================================
-- 0.3.0: self-tests (Spec §7.1). Run once on the first CI frame.
@@ -1527,7 +1650,9 @@ local function self_test()
-- t1: opening a menu then clicking outside closes it.
state.set_menu_open("File")
engine.test.equals(state.get_menu_open(), "File", "t1: menu opened by setter")
ui.handle_click(2, MENU_BAR_H + 10, "left")
-- 0.4.0: handle_toolbar_click + handle_modal_click split out of
-- handle_click; call the toolbar-specific path here.
ui.handle_toolbar_click(2, MENU_BAR_H + 10, "left")
engine.test.equals(state.get_menu_open(), nil, "t1: outside click closes menu")
-- t2: toggle item flips bound state.
@@ -1549,7 +1674,12 @@ local function self_test()
"t3: cheatsheet modal id set")
engine.test.equals(state.get_menu_open(), nil,
"t3: menu closed when modal opens")
ui.handle_click(2, MENU_BAR_H + 10, "left")
-- 0.4.0: modal-priority is enforced by handle_modal_click (called
-- before panel.update() in M.update). Verify it returns true so
-- a click would be swallowed before reaching the toolbar widget.
local modal_consumed = ui.handle_modal_click(2, MENU_BAR_H + 10, "left")
engine.test.equals(modal_consumed, true,
"t3: modal swallows clicks while open")
engine.test.equals(state.get_modal_open(), "cheatsheet",
"t3: outside click does not close modal")
state.close_modal()
@@ -1772,23 +1902,47 @@ function update(ctx, dt)
actions.toggle_world_overlay()
end
-- Mouse clicks + drag: hit-test UI on press; if consumed we never
-- enter drag-mode for this stroke. Otherwise the press initiates a
-- drag-stroke whose paint/erase action repeats for every subsequent
-- snap_vertex (or cell) while the button stays down. set_vertex /
-- set_override are idempotent + no-op when state is unchanged so
-- repeating per-frame is cheap.
if engine.input.was_mouse_pressed(engine.input.MOUSE_LEFT) then
local consumed = ui.handle_click(mx, my, "left")
if not consumed then
state.set_drag_paint("paint")
end
-- 0.4.0: input dispatch is split between module-level + panel-lib:
-- 1. handle_modal_click: module-level, ALWAYS runs first (modals
-- are top-most overlays and not panel widgets).
-- 2. panel.update(dt): routes clicks to persistent widgets via
-- their input_block="self" hit-tests. Widget handle_input
-- delegates to ui.handle_toolbar_click / handle_layers_click /
-- handle_palette_click.
-- 3. Canvas drag-paint: gated on (no modal AND point not inside
-- any panel widget) to prevent widget-clicks bleeding through
-- to the map canvas underneath.
--
-- Modal precedence: when a modal is open, we still consume the
-- click here (so it doesn't reach panel widgets), but we DON'T
-- start a drag-paint. panel.update still runs but its dispatch
-- routes the press as normal (mostly a no-op for modals since the
-- modal isn't a widget).
local lmb_pressed = engine.input.was_mouse_pressed(engine.input.MOUSE_LEFT)
local rmb_pressed = engine.input.was_mouse_pressed(engine.input.MOUSE_RIGHT)
local modal_open = state.is_modal_open()
if lmb_pressed and modal_open then
ui.handle_modal_click(mx, my, "left")
end
if engine.input.was_mouse_pressed(engine.input.MOUSE_RIGHT) then
local consumed = ui.handle_click(mx, my, "right")
if not consumed then
state.set_drag_paint("erase")
end
if rmb_pressed and modal_open then
ui.handle_modal_click(mx, my, "right")
end
-- Panel-lib dispatch: reads engine.input mouse state directly,
-- emits click + wheel events to widget handle_input fns. Widgets
-- routed by tier (top → normal → hud) with input_block="self".
panel.update(dt)
-- Canvas drag-paint trigger. Only initiate when no modal open AND
-- the click did not land within any panel widget area. Drag-paint
-- still continues per-frame as long as the mouse button stays
-- down (no per-frame panel-bounds check — the originating press
-- already established the click as a canvas-stroke).
if lmb_pressed and not modal_open and not panel.point_in_any_panel(mx, my) then
state.set_drag_paint("paint")
end
if rmb_pressed and not modal_open and not panel.point_in_any_panel(mx, my) then
state.set_drag_paint("erase")
end
-- Per-frame drag-paint dispatch (Auto-Tile uses snap_vertex; Direct
@@ -1837,5 +1991,11 @@ function render(ctx)
ui.draw_world_overlay(m_size.w, m_size.h, t_size, state.get_snap_vertex())
ui.draw_debug_overlay(m_size.w, m_size.h, t_size)
camera.finish()
ui.draw() -- screen-space overlays after camera ends
-- 0.4.0: panel.render() draws the 3 persistent widgets (toolbar +
-- layers + palette) on top of the canvas. ui.draw() then draws the
-- remaining non-widget chrome (status chip + open modal overlay)
-- ON TOP of the widgets — modals are last so they appear above any
-- widget panel.
panel.render()
ui.draw()
end

21
manifest.launcher.md Normal file
View File

@@ -0,0 +1,21 @@
---
{"summary":"Map editor — menu-bar toolbar, three paint modes, Layers-Panel + Palette","author":"calic","tags":["tool","map-editor"],"teaser_images":[]}
---
# Map Editor
Click-to-paint tile editor with a top menu-bar (File / Edit / View / Map / Tools / Window / Help) plus Mode-pill, a right-side Layers-Panel and a bottom Palette-Strip with atlas-tile thumbnails. Three paint modes — Auto-Tile, Direct and Cell-Tile — cycle with Tab.
## Controls
- Mouse-left on canvas: paint (mode-dependent)
- Mouse-right on canvas: clear/erase
- Mouse-left on layer row: switch active layer
- Mouse-right on layer row: toggle visibility
- Mouse-left on palette swatch: select active slot
- Tab: cycle paint mode (Auto-Tile / Direct / Cell-Tile)
- E: erase tile at cursor
- R: rotate · H: flip · 0: reset transform
- S: save
- I: cheatsheet
- ESC: close modal / menu, else quit to launcher

View File

@@ -1,6 +1,6 @@
{
"id": "map-editor",
"version": "0.3.2",
"version": "0.4.0",
"kind": "module",
"api": "^0.1",
"ci_frames": 30,
@@ -10,10 +10,11 @@
},
"deps": [
{"id":"lib-core.maps","version":"0.5.7"},
{"id":"lib-core.render","version":"0.2.0"},
{"id":"lib-core.composition","version":"0.1.0"},
{"id":"lib-core.render","version":"0.3.0"},
{"id":"lib-core.composition","version":"0.4.0"},
{"id":"lib-core.camera","version":"0.3.0"},
{"id":"lib-core.input","version":"0.4.0"},
{"id":"lib-core.panel","version":"0.4.0"},
{"id":"lib-asset.prototype-blob-geom","version":"0.1.0"}
]
}