feat(P.3.5): v0.1.0 -> v0.2.0 — additive Free-Pan + Bounds-Clamping API

This commit is contained in:
Axel Meyer
2026-05-13 01:59:08 +02:00
parent 28fb3bf4e9
commit 0e730e7c45
2 changed files with 200 additions and 12 deletions

210
init.lua
View File

@@ -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