Compare commits
9 Commits
2734efa1a9
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c9e481a6c | ||
|
|
f1cddc3948 | ||
|
|
75655be72d | ||
|
|
4b8abc8502 | ||
|
|
176305e35b | ||
|
|
a150dd8dc5 | ||
|
|
61e7d5192f | ||
|
|
451da04cbd | ||
|
|
bde1f7acfa |
46
README.md
46
README.md
@@ -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
342
init.lua
@@ -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
21
manifest.launcher.md
Normal 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
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "map-editor",
|
||||
"version": "0.3.1",
|
||||
"version": "0.4.0",
|
||||
"kind": "module",
|
||||
"api": "^0.1",
|
||||
"ci_frames": 30,
|
||||
@@ -10,9 +10,11 @@
|
||||
},
|
||||
"deps": [
|
||||
{"id":"lib-core.maps","version":"0.5.7"},
|
||||
{"id":"lib-core.render","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"}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user