feat(map-editor): v0.2.0c.1 — drag-paint strokes
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.
This commit is contained in:
65
init.lua
65
init.lua
@@ -1,4 +1,4 @@
|
||||
-- sporel-module-map-editor v0.2.0c
|
||||
-- sporel-module-map-editor v0.2.0c.1
|
||||
-- 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
|
||||
@@ -12,6 +12,9 @@
|
||||
-- {slot, rot, flip} on a material-bound terrain layer. Single
|
||||
-- `flip` flag (0/1) combined with rot (0..3) covers all 8 D4
|
||||
-- orientations; H+V buttons are redundant so we ship just one.
|
||||
-- 0.2.0c.1 adds drag-paint: LMB-hold strokes a line of paints, RMB-hold
|
||||
-- a line of erases (both in Auto-Tile and Direct mode). UI clicks on
|
||||
-- toolbar / palette / side-panel never enter drag-mode.
|
||||
-- Decal/Entity sub-selectors + Material-Properties modal land in 0.2.0d.
|
||||
--
|
||||
-- All editor logic in this single file: engine sandbox only allows
|
||||
@@ -59,6 +62,7 @@ local state = (function()
|
||||
cheatsheet_open = false, -- 0.2.0a: ?-modal toggled?
|
||||
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)
|
||||
}
|
||||
|
||||
-- =====================================================================
|
||||
@@ -147,6 +151,9 @@ local state = (function()
|
||||
end
|
||||
function M.get_snap_vertex() return state.snap_vertex end
|
||||
|
||||
function M.set_drag_paint(mode) state.drag_paint = mode end -- 0.2.0c.1
|
||||
function M.get_drag_paint() return state.drag_paint end
|
||||
|
||||
-- Used by tests to reset between asserts
|
||||
function M.reset()
|
||||
state.map_id = nil
|
||||
@@ -162,6 +169,7 @@ local state = (function()
|
||||
state.cheatsheet_open = false
|
||||
state.mouse_cell = nil
|
||||
state.snap_vertex = nil
|
||||
state.drag_paint = nil
|
||||
for name in pairs(state.layer_visible) do
|
||||
state.layer_visible[name] = true
|
||||
end
|
||||
@@ -400,9 +408,10 @@ local ui = (function()
|
||||
{ key="H", action="Toggle tile flip 0<->1 (Direct-mode override)" },
|
||||
{ key="0", action="Reset transform (rot=0, flip=0)" },
|
||||
{ key="Esc", action="Quit editor" },
|
||||
{ key="LMB on canvas (Auto-Tile)", action="Paint vertex on active layer (any-corner fill)" },
|
||||
{ key="RMB on canvas (Auto-Tile)", action="Clear vertex on active layer" },
|
||||
{ key="LMB on canvas (Direct)", action="Paint cell with active slot + rotation" },
|
||||
{ key="LMB-drag canvas (Auto-Tile)", action="Stroke-paint vertices (any-corner fill)" },
|
||||
{ key="RMB-drag canvas (Auto-Tile)", action="Stroke-clear vertices" },
|
||||
{ key="LMB-drag canvas (Direct)", action="Stroke-paint cells (override with slot+rot+flip)" },
|
||||
{ key="RMB-drag canvas (Direct)", action="Stroke-clear overrides" },
|
||||
{ key="LMB on palette swatch", action="Select slot (1-14) as active tile" },
|
||||
{ key="RMB on Layer row", action="Toggle layer visibility" },
|
||||
{ key="LMB on Layer row", action="Set as active layer" },
|
||||
@@ -817,7 +826,7 @@ input.bind("toggle_cheatsheet", { "i" }) -- 0.2.0a — `i` for Info/help (no
|
||||
input.bind("flip", { "h" }) -- 0.2.0c
|
||||
input.bind("reset_transform", { "0" }) -- 0.2.0c
|
||||
|
||||
engine.print("map-editor v0.2.0c: ready. Tab=mode, I=help, S=save, E=erase, R=rotate, H=flip, 0=reset, ESC=quit")
|
||||
engine.print("map-editor v0.2.0c.1: ready. Tab=mode, I=help, S=save, E=erase, R=rotate, H=flip, 0=reset, ESC=quit")
|
||||
|
||||
-- =====================================================================
|
||||
-- Frame hooks
|
||||
@@ -897,31 +906,49 @@ function update(ctx, dt)
|
||||
actions.reset_transform()
|
||||
end
|
||||
|
||||
-- Mouse clicks: hit-test UI first, then map area
|
||||
-- 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
|
||||
if state.get_mode() == "auto-tile" then
|
||||
local snap = state.get_snap_vertex()
|
||||
if snap then actions.paint_vertex_at(snap.vx, snap.vy, true) end
|
||||
elseif state.get_mouse_cell() then
|
||||
-- 0.2.0c: Direct-mode on a material-bound terrain layer
|
||||
-- writes an override per cell, honouring active rot+flip.
|
||||
local cell = state.get_mouse_cell()
|
||||
actions.paint_override_at(cell.x, cell.y)
|
||||
end
|
||||
state.set_drag_paint("paint")
|
||||
end
|
||||
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
|
||||
end
|
||||
|
||||
-- Per-frame drag-paint dispatch (Auto-Tile uses snap_vertex; Direct
|
||||
-- uses mouse_cell). Stops as soon as the originating button releases.
|
||||
local dp = state.get_drag_paint()
|
||||
if dp then
|
||||
local lmb_down = engine.input.is_mouse_down(engine.input.MOUSE_LEFT)
|
||||
local rmb_down = engine.input.is_mouse_down(engine.input.MOUSE_RIGHT)
|
||||
local active = (dp == "paint" and lmb_down) or (dp == "erase" and rmb_down)
|
||||
if not active then
|
||||
state.set_drag_paint(nil)
|
||||
else
|
||||
if state.get_mode() == "auto-tile" then
|
||||
local snap = state.get_snap_vertex()
|
||||
if snap then actions.paint_vertex_at(snap.vx, snap.vy, false) end
|
||||
elseif state.get_mouse_cell() then
|
||||
-- 0.2.0c: Direct + RMB on canvas clears the override.
|
||||
if snap then
|
||||
actions.paint_vertex_at(snap.vx, snap.vy, dp == "paint")
|
||||
end
|
||||
else
|
||||
local cell = state.get_mouse_cell()
|
||||
actions.erase_override_at(cell.x, cell.y)
|
||||
if cell then
|
||||
if dp == "paint" then
|
||||
actions.paint_override_at(cell.x, cell.y)
|
||||
else
|
||||
actions.erase_override_at(cell.x, cell.y)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user