feat(P.3.5): v0.1.0 -> v0.2.0 — additive Free-Pan + Bounds-Clamping API
This commit is contained in:
210
init.lua
210
init.lua
@@ -1,28 +1,77 @@
|
|||||||
-- =====================================================================
|
-- =====================================================================
|
||||||
-- lib-core.camera — 2D Camera State (P.0)
|
-- lib-core.camera v0.2.0 — 2D Camera State + Free-Pan + Bounds-Clamping
|
||||||
-- See: meta/docs/superpowers/specs/2026-05-09-p0-lib-camera-design.md
|
-- See: meta/docs/superpowers/specs/2026-05-13-p3-5-camera-free-pan-design.md
|
||||||
--
|
--
|
||||||
-- Scope: target + zoom state + begin/finish wrapper.
|
-- v0.2.0 additions (additiv über v0.1.0):
|
||||||
-- Forward-compat stubs (DEPRECATED-MVP):
|
-- - Pan-API (keys / edge / drag, additiv, per-source toggle-bar)
|
||||||
-- - follow(entity) -- lib-core.actor slice
|
-- - Bounds-Clamping (always-on once set; clear_bounds to disable)
|
||||||
-- - set_bounds(map_id) -- when map-bounds matter
|
-- - Drag-Invert + zoom-scaling
|
||||||
-- - lerp_to(target, dt) -- smooth-follow / animation slice
|
-- - update(dt) for per-frame pan-summation
|
||||||
-- - shake(intensity, dur) -- effects/juice slice
|
--
|
||||||
-- - set_rotation(deg) -- closed-topology slice
|
-- 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_x = 0
|
||||||
local target_y = 0
|
local target_y = 0
|
||||||
local zoom_v = 1.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 = {}
|
local M = {}
|
||||||
|
|
||||||
|
-- v0.1.0 (unverändert außer: set_target ruft clamp_to_bounds nach)
|
||||||
|
|
||||||
function M.set_target(x, y)
|
function M.set_target(x, y)
|
||||||
if type(x) ~= "number" or type(y) ~= "number" then
|
if type(x) ~= "number" or type(y) ~= "number" then
|
||||||
error("camera.set_target: x and y must be numbers")
|
error("camera.set_target: x and y must be numbers")
|
||||||
end
|
end
|
||||||
target_x = x
|
target_x = x
|
||||||
target_y = y
|
target_y = y
|
||||||
|
clamp_to_bounds()
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.set_zoom(z)
|
function M.set_zoom(z)
|
||||||
@@ -49,11 +98,150 @@ function M.finish()
|
|||||||
engine.render.camera_2d_end()
|
engine.render.camera_2d_end()
|
||||||
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.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.lerp_to(target, dt) -- smooth-follow slice
|
||||||
-- DEPRECATED-MVP: function M.shake(intensity, dur) -- effects/juice 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_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
|
return M
|
||||||
|
|||||||
@@ -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"}]}
|
||||||
Reference in New Issue
Block a user