From 0e730e7c45955d4e88ca7838fdb7f2c3bd007412 Mon Sep 17 00:00:00 2001 From: Axel Meyer Date: Wed, 13 May 2026 01:59:08 +0200 Subject: [PATCH] =?UTF-8?q?feat(P.3.5):=20v0.1.0=20->=20v0.2.0=20=E2=80=94?= =?UTF-8?q?=20additive=20Free-Pan=20+=20Bounds-Clamping=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- init.lua | 210 ++++++++++++++++++++++++++++++++++++++++++++++++--- manifest.lib | 2 +- 2 files changed, 200 insertions(+), 12 deletions(-) diff --git a/init.lua b/init.lua index a23206c..6e0218f 100644 --- a/init.lua +++ b/init.lua @@ -1,28 +1,77 @@ -- ===================================================================== --- lib-core.camera — 2D Camera State (P.0) --- See: meta/docs/superpowers/specs/2026-05-09-p0-lib-camera-design.md +-- lib-core.camera v0.2.0 — 2D Camera State + Free-Pan + Bounds-Clamping +-- See: meta/docs/superpowers/specs/2026-05-13-p3-5-camera-free-pan-design.md -- --- Scope: target + zoom state + begin/finish wrapper. --- Forward-compat stubs (DEPRECATED-MVP): --- - follow(entity) -- lib-core.actor slice --- - set_bounds(map_id) -- when map-bounds matter --- - lerp_to(target, dt) -- smooth-follow / animation slice --- - shake(intensity, dur) -- effects/juice slice --- - set_rotation(deg) -- closed-topology slice +-- v0.2.0 additions (additiv über v0.1.0): +-- - Pan-API (keys / edge / drag, additiv, per-source toggle-bar) +-- - Bounds-Clamping (always-on once set; clear_bounds to disable) +-- - Drag-Invert + zoom-scaling +-- - update(dt) for per-frame pan-summation +-- +-- v0.1.0 API unverändert: set_target/set_zoom/target/zoom/begin/finish. +-- DEPRECATED-MVPs siehe Spec §7. -- ===================================================================== +local input = require("lib-core.input") + +-- v0.1.0 state (unverändert) local target_x = 0 local target_y = 0 local zoom_v = 1.0 +-- v0.2.0 NEW state +local pan_speed_v = 300 -- px/s, shared keys + edge +local edge_thresh_v = 20 -- px from window edge +local pan_invert_v = false -- drag-direction (false = "Map grabs") + +local pan_enabled_v = { keys = false, edge = false, drag = false } +local pan_actions = { + left = nil, right = nil, up = nil, down = nil, + drag = nil, +} + +local bounds_v = nil -- nil OR {xmin, ymin, xmax, ymax} + +-- ---------- internal helpers ---------- + +local function clamp(v, lo, hi) + if v < lo then return lo end + if v > hi then return hi end + return v +end + +local function clamp_to_bounds() + if bounds_v == nil then return end + target_x = clamp(target_x, bounds_v.xmin, bounds_v.xmax) + target_y = clamp(target_y, bounds_v.ymin, bounds_v.ymax) +end + +local function check_source(source) + if source ~= "keys" and source ~= "edge" and source ~= "drag" then + error(string.format("camera: unknown source '%s' (expected 'keys' | 'edge' | 'drag')", + tostring(source))) + end +end + +local function check_action_name(arg_name, value) + if type(value) ~= "string" or value == "" then + error(string.format("camera.%s: must be non-empty string", arg_name)) + end +end + +-- ---------- public API ---------- + local M = {} +-- v0.1.0 (unverändert außer: set_target ruft clamp_to_bounds nach) + function M.set_target(x, y) if type(x) ~= "number" or type(y) ~= "number" then error("camera.set_target: x and y must be numbers") end target_x = x target_y = y + clamp_to_bounds() end function M.set_zoom(z) @@ -49,11 +98,150 @@ function M.finish() engine.render.camera_2d_end() end --- DEPRECATED-MVP: forward-compat stubs for later slices +-- v0.2.0 NEW — Bindings + +function M.bind_pan_keys(left, right, up, down) + check_action_name("bind_pan_keys.left", left) + check_action_name("bind_pan_keys.right", right) + check_action_name("bind_pan_keys.up", up) + check_action_name("bind_pan_keys.down", down) + pan_actions.left = left + pan_actions.right = right + pan_actions.up = up + pan_actions.down = down + pan_enabled_v.keys = true +end + +function M.bind_pan_drag(action) + check_action_name("bind_pan_drag.action", action) + pan_actions.drag = action + pan_enabled_v.drag = true +end + +function M.enable_pan_edge() + pan_enabled_v.edge = true +end + +-- v0.2.0 NEW — Numeric Settings + +function M.set_pan_speed(n) + if type(n) ~= "number" or n <= 0 then + error(string.format("camera.set_pan_speed: must be positive number, got %s", + tostring(n))) + end + pan_speed_v = n +end + +function M.pan_speed() return pan_speed_v end + +function M.set_edge_threshold(n) + if type(n) ~= "number" or n < 0 then + error(string.format("camera.set_edge_threshold: must be non-negative number, got %s", + tostring(n))) + end + edge_thresh_v = n +end + +function M.edge_threshold() return edge_thresh_v end + +function M.set_pan_invert(b) + if type(b) ~= "boolean" then + error("camera.set_pan_invert: must be boolean") + end + pan_invert_v = b +end + +function M.pan_invert() return pan_invert_v end + +-- v0.2.0 NEW — Per-Source Runtime-Toggle + +function M.set_pan_enabled(source, b) + check_source(source) + if type(b) ~= "boolean" then + error("camera.set_pan_enabled: bool arg must be boolean") + end + pan_enabled_v[source] = b +end + +function M.pan_enabled(source) + check_source(source) + return pan_enabled_v[source] +end + +-- v0.2.0 NEW — Bounds + +function M.set_bounds(xmin, ymin, xmax, ymax) + if type(xmin) ~= "number" or type(ymin) ~= "number" + or type(xmax) ~= "number" or type(ymax) ~= "number" then + error("camera.set_bounds: all 4 args must be numbers") + end + if xmin > xmax or ymin > ymax then + error(string.format("camera.set_bounds: min > max (%s,%s vs %s,%s)", + tostring(xmin), tostring(ymin), tostring(xmax), tostring(ymax))) + end + bounds_v = { xmin = xmin, ymin = ymin, xmax = xmax, ymax = ymax } + clamp_to_bounds() +end + +function M.clear_bounds() + bounds_v = nil +end + +function M.bounds() + if bounds_v == nil then return nil end + return { xmin = bounds_v.xmin, ymin = bounds_v.ymin, + xmax = bounds_v.xmax, ymax = bounds_v.ymax } +end + +-- v0.2.0 NEW — Per-Frame Pan-Summation + +function M.update(dt) + local dx, dy = 0, 0 + + if pan_enabled_v.keys + and pan_actions.left and pan_actions.right + and pan_actions.up and pan_actions.down then + if input.is_action_down(pan_actions.left) then dx = dx - pan_speed_v * dt end + if input.is_action_down(pan_actions.right) then dx = dx + pan_speed_v * dt end + if input.is_action_down(pan_actions.up) then dy = dy - pan_speed_v * dt end + if input.is_action_down(pan_actions.down) then dy = dy + pan_speed_v * dt end + end + + if pan_enabled_v.edge then + local mx, my = engine.input.get_mouse_pos() + local w, h = engine.window.size() + if mx < edge_thresh_v then dx = dx - pan_speed_v * dt end + if mx > w - edge_thresh_v then dx = dx + pan_speed_v * dt end + if my < edge_thresh_v then dy = dy - pan_speed_v * dt end + if my > h - edge_thresh_v then dy = dy + pan_speed_v * dt end + end + + if pan_enabled_v.drag and pan_actions.drag then + if input.is_action_down(pan_actions.drag) then + local mdx, mdy = engine.input.get_mouse_delta() + local sign = pan_invert_v and 1 or -1 + dx = dx + (sign * mdx) / zoom_v + dy = dy + (sign * mdy) / zoom_v + end + end + + if dx ~= 0 or dy ~= 0 then + target_x = target_x + dx + target_y = target_y + dy + clamp_to_bounds() + end +end + +-- DEPRECATED-MVP: forward-compat stubs (siehe Spec §7): -- DEPRECATED-MVP: function M.follow(entity) -- lib-core.actor slice --- DEPRECATED-MVP: function M.set_bounds(map_id) -- bounds-clamping slice -- DEPRECATED-MVP: function M.lerp_to(target, dt) -- smooth-follow slice -- DEPRECATED-MVP: function M.shake(intensity, dur) -- effects/juice slice -- DEPRECATED-MVP: function M.set_rotation(deg) -- closed-topology slice +-- DEPRECATED-MVP: function M.set_bounds_from_map(id) -- lib-core.maps bridge +-- DEPRECATED-MVP: per-source speeds (key_speed vs edge_speed split) +-- DEPRECATED-MVP: edge-scroll speed-ramping (proximity-to-edge scaling) +-- DEPRECATED-MVP: window-focus-check (Raylib IsWindowFocused for edge-scroll) +-- DEPRECATED-MVP: pan-velocity-smoothing (easing on key-press/release) +-- DEPRECATED-MVP: mouse-delta-jump-detection (filter large deltas from tab-out) return M diff --git a/manifest.lib b/manifest.lib index fc57e22..05b4914 100644 --- a/manifest.lib +++ b/manifest.lib @@ -1 +1 @@ -{"id":"lib-core.camera","version":"0.1.0","api_min":"0.1"} +{"id":"lib-core.camera","version":"0.2.0","api_min":"0.1","deps":[{"id":"lib-core.input","version":"0.3.0"}]} \ No newline at end of file