From 77832c07b2dc0ad699836a49a92ae2e9318207af Mon Sep 17 00:00:00 2001 From: calic Date: Fri, 29 May 2026 01:05:39 +0200 Subject: [PATCH] =?UTF-8?q?feat(map-editor):=20v0.2.0c.1=20=E2=80=94=20dra?= =?UTF-8?q?g-paint=20strokes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- init.lua | 65 ++++++++++++++++++++++++++++++++++--------------- manifest.module | 2 +- 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/init.lua b/init.lua index f0c0d68..28f92fd 100644 --- a/init.lua +++ b/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 diff --git a/manifest.module b/manifest.module index 39bf213..1f0e7c0 100644 --- a/manifest.module +++ b/manifest.module @@ -1,6 +1,6 @@ { "id": "map-editor", - "version": "0.2.0c", + "version": "0.2.0c.1", "kind": "module", "api": "^0.1", "ci_frames": 30,