From 0858fdf4011d22ebc650c241f308b55b2c5061b0 Mon Sep 17 00:00:00 2001 From: Axel Meyer Date: Sat, 16 May 2026 16:36:43 +0200 Subject: [PATCH] docs: migrate README to API-Doc-Convention (S2-cascade.libs) Restructured per ADR-0038: Abstract + bold-list Badges + Topology H2 + H3-API-Subsections (one per public function) + Conventions/Consumer pattern/CHANGELOG/References. Topology auto-populated via Sporel.exe --lint --fix. Pre-commit hook installed via --install-hooks. Co-Authored-By: Claude Opus 4.7 (1M context) --- .githooks/pre-commit | 13 ++ README.md | 257 ++++++++++++++++++++++++++++++++++++--- scripts/install-hooks.sh | 3 + 3 files changed, 257 insertions(+), 16 deletions(-) create mode 100644 .githooks/pre-commit create mode 100644 scripts/install-hooks.sh diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100644 index 0000000..1ebdad0 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,13 @@ +#!/bin/sh +# Sporel API-Doc-Convention pre-commit hook. +SPOREL_EXE="${SPOREL_EXE:-$(command -v Sporel.exe 2>/dev/null || command -v sporel 2>/dev/null)}" +if [ -z "$SPOREL_EXE" ]; then + echo "INFO: Sporel.exe not on PATH. Skipping lint." >&2 + exit 0 +fi +"$SPOREL_EXE" --lint="$(pwd)" --fix +RC=$? +if git diff --cached --name-only | grep -qx 'README.md'; then + git add README.md +fi +exit $RC diff --git a/README.md b/README.md index 938b185..67903a6 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,258 @@ # lib-core.camera -P.0 2D camera state + begin/finish wrapper. Holds `target = {x, y}` + `zoom`, -wraps `engine.render.camera_2d_begin/end` with current state. +2D camera state + begin/finish render wrapper, with free-pan (keys/edge/drag), bounds-clamping, and screen-to-world unproject. Wraps `engine.render.camera_2d_begin/end` with current target + zoom. -- Lib-ID: `lib-core.camera` -- Version: `0.1.0` -- Spec: `meta/docs/superpowers/specs/2026-05-09-p0-lib-camera-design.md` +**Version:** 0.3.0 +**Lib-ID:** lib-core.camera +**Requires:** lib-core.input v>=0.4.0 +**Tags:** camera, viewport, transform, pan, bounds -Forward-compat stubs (DEPRECATED-MVP) for follow, set_bounds, lerp_to, shake, -set_rotation. See `meta/docs/architecture/render-pipeline.md` and -`meta/docs/architecture/map-topology.md §7.3` for full target context. +## Topology + + +```mermaid +graph LR + this["lib-core.camera"] + lib_core_input["lib-core.input"] + this --> lib_core_input + engine["engine.*"] + this --> engine +``` + ## API -- `camera.set_target(x, y)` — set world-coords for screen-center -- `camera.set_zoom(z)` — set zoom (positive number) -- `camera.target() → {x, y}` — current target -- `camera.zoom() → number` — current zoom -- `camera.begin()` — push camera transform (must be inside render hook) -- `camera.finish()` — pop camera transform (`end` is Lua keyword) + +### `camera.set_target(x, y)` +**Syntax:** `camera.set_target(x: number, y: number) -> void` + +**Example:** +```lua +camera.set_target(256, 256) -- world-coords for screen-center +``` + +**Description:** Sets the world-coords the camera centers on. Clamps to current bounds (if set). + +### `camera.set_zoom(z)` +**Syntax:** `camera.set_zoom(z: number) -> void` + +**Example:** +```lua +camera.set_zoom(1.0) +``` + +**Description:** Sets zoom factor. Must be a positive number. + +### `camera.target()` +**Syntax:** `camera.target() -> number, number` + +**Example:** +```lua +local x, y = camera.target() +``` + +**Description:** Returns current target world-coords. + +### `camera.zoom()` +**Syntax:** `camera.zoom() -> number` + +**Example:** +```lua +local z = camera.zoom() +``` + +**Description:** Returns current zoom factor. + +### `camera.begin()` +**Syntax:** `camera.begin() -> void` + +**Example:** +```lua +camera.begin() +-- draw world here +camera.finish() +``` + +**Description:** Pushes the camera transform onto the render stack. Must be called inside a render hook. + +### `camera.finish()` +**Syntax:** `camera.finish() -> void` + +**Description:** Pops the camera transform. Named `finish` because `end` is a Lua keyword. + +### `camera.bind_pan_keys(left, right, up, down)` +**Syntax:** `camera.bind_pan_keys(left: string, right: string, up: string, down: string) -> void` + +**Example:** +```lua +input.bind("cam_left", { "a" }) +input.bind("cam_right", { "d" }) +input.bind("cam_up", { "w" }) +input.bind("cam_down", { "s" }) +camera.bind_pan_keys("cam_left", "cam_right", "cam_up", "cam_down") +``` + +**Description:** Binds 4 input-actions for keyboard pan. Per-frame summation happens in `update(dt)`. + +### `camera.bind_pan_drag(action)` +**Syntax:** `camera.bind_pan_drag(action: string) -> void` + +**Example:** +```lua +input.bind("cam_drag", { "mouse_middle" }) +camera.bind_pan_drag("cam_drag") +``` + +**Description:** Binds an input-action that, while held, pans by mouse-delta (zoom-scaled). + +### `camera.enable_pan_edge()` +**Syntax:** `camera.enable_pan_edge() -> void` + +**Description:** Enables edge-scroll panning: cursor near a window-edge pans the camera at `pan_speed`. Threshold via `set_edge_threshold`. + +### `camera.set_pan_speed(n)` +**Syntax:** `camera.set_pan_speed(n: number) -> void` + +**Description:** Sets pan-speed in world-units per second for keys + edge-scroll. + +### `camera.pan_speed()` +**Syntax:** `camera.pan_speed() -> number` + +**Description:** Returns current pan-speed. + +### `camera.set_edge_threshold(n)` +**Syntax:** `camera.set_edge_threshold(n: number) -> void` + +**Description:** Sets edge-scroll threshold in screen-pixels. + +### `camera.edge_threshold()` +**Syntax:** `camera.edge_threshold() -> number` + +**Description:** Returns current edge-scroll threshold. + +### `camera.set_pan_invert(b)` +**Syntax:** `camera.set_pan_invert(b: bool) -> void` + +**Description:** Inverts drag-pan direction. False = "drag world" feel, true = "drag camera" feel. + +### `camera.pan_invert()` +**Syntax:** `camera.pan_invert() -> bool` + +**Description:** Returns current drag-invert setting. + +### `camera.set_pan_enabled(source, b)` +**Syntax:** `camera.set_pan_enabled(source: string, b: bool) -> void` + +**Example:** +```lua +camera.set_pan_enabled("edge", false) -- disable edge-scroll only +``` + +**Description:** Per-source toggle. `source` is one of `"keys"`, `"edge"`, `"drag"`. All sources default to enabled. + +### `camera.pan_enabled(source)` +**Syntax:** `camera.pan_enabled(source: string) -> bool` + +**Description:** Returns current toggle-state for the given source. + +### `camera.set_bounds(xmin, ymin, xmax, ymax)` +**Syntax:** `camera.set_bounds(xmin: number, ymin: number, xmax: number, ymax: number) -> void` + +**Example:** +```lua +camera.set_bounds(0, 0, 4096, 4096) +``` + +**Description:** Sets world-space bounds. Once set, `set_target` and `update` clamp to keep the target inside. `xmin < xmax` and `ymin < ymax` required. + +### `camera.clear_bounds()` +**Syntax:** `camera.clear_bounds() -> void` + +**Description:** Removes any active bounds; camera becomes free-roaming. + +### `camera.bounds()` +**Syntax:** `camera.bounds() -> number, number, number, number | nil` + +**Description:** Returns `xmin, ymin, xmax, ymax` if bounds active, else `nil`. + +### `camera.update(dt)` +**Syntax:** `camera.update(dt: number) -> void` + +**Example:** +```lua +function update(ctx, dt) + camera.update(dt) +end +``` + +**Description:** Per-frame pan summation: applies keys + edge + drag contributions (each gated by `pan_enabled[source]`), then clamps to bounds. + +### `camera.screen_to_world(sx, sy)` +**Syntax:** `camera.screen_to_world(sx: number, sy: number) -> number, number` + +**Example:** +```lua +local mx, my = engine.input.get_mouse_pos() +local wx, wy = camera.screen_to_world(mx, my) +``` + +**Description:** Inverse of the Raylib 2D camera transform (origin = window-center, no rotation). Useful for picking, world-space cursors. v0.3.0+. + +## Conventions + +- Y-down-positive per ADR-0031. +- Pan-sources are additive: keys + edge + drag contributions sum each frame. +- Bounds-clamping is always-on once set; use `clear_bounds` to disable. +- Drag-pan is zoom-scaled so on-screen mouse-distance maps to consistent world-distance. ## Consumer pattern + ```lua +local input = require("lib-core.input") local camera = require("lib-core.camera") -camera.set_target(256, 256) -- world-coords + +camera.set_target(256, 256) camera.set_zoom(1.0) +camera.set_pan_speed(400) +camera.set_bounds(0, 0, 4096, 4096) + +input.bind("cam_left", { "a" }) +input.bind("cam_right", { "d" }) +input.bind("cam_up", { "w" }) +input.bind("cam_down", { "s" }) +input.bind("cam_drag", { "mouse_middle" }) +camera.bind_pan_keys("cam_left", "cam_right", "cam_up", "cam_down") +camera.bind_pan_drag("cam_drag") +camera.enable_pan_edge() + +function update(ctx, dt) camera.update(dt) end + function render(ctx) - engine.render.clear_color(...) + engine.render.clear_color(0, 0, 0) camera.begin() -- draw world here camera.finish() end ``` + +## CHANGELOG + +### v0.3.0 (P.3.5) +- Added `screen_to_world(sx, sy)` — unproject screen-coord to world-coord. + +### v0.2.0 +- Pan-API (keys / edge / drag, additive, per-source toggle). +- Bounds-clamping (always-on once set; `clear_bounds` to disable). +- Drag-invert + zoom-scaling. +- `update(dt)` for per-frame pan-summation. + +### v0.1.0 (P.0) +- Initial release: target/zoom + begin/finish. + +## References + +- Spec v0.3.0 (P.3.5): `meta/docs/superpowers/specs/2026-05-13-p3-5-camera-free-pan-design.md` +- Spec v0.1.0 (P.0): `meta/docs/superpowers/specs/2026-05-09-p0-lib-camera-design.md` +- Architecture: `meta/docs/architecture/render-pipeline.md`, `meta/docs/architecture/map-topology.md` §7.3 +- ADR-0001 (engine knows verbs, libs bring nouns) +- ADR-0031 (pixel-convention: Y-down-positive) +- ADR-0038 (API-Doc-Convention) diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh new file mode 100644 index 0000000..2ff6e41 --- /dev/null +++ b/scripts/install-hooks.sh @@ -0,0 +1,3 @@ +#!/bin/sh +git config core.hooksPath .githooks +echo "Hooks activated."