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) <noreply@anthropic.com>
245 lines
8.1 KiB
Markdown
245 lines
8.1 KiB
Markdown
# lib-core.selection
|
|
|
|
Click-select + drag-box-select + selection-set-state. Handle-registry pattern: modules register AABB-callbacks, the lib polls them during hit-test. State-machine (`idle`/`ambiguous`/`dragging`) disambiguates click-vs-drag via pixel-threshold. Modifier-bindings via action-names (`mod_add`, `mod_toggle`). Render via alpha-tinted draw_rect helpers.
|
|
|
|
**Version:** 0.1.0
|
|
**Lib-ID:** lib-core.selection
|
|
**Requires:** lib-core.input v>=0.4.0, lib-core.camera v>=0.3.0
|
|
**Tags:** selection, rts, click, drag-box, hit-test
|
|
|
|
## Topology
|
|
|
|
<!-- topology:start (auto-generated; do not edit) -->
|
|
```mermaid
|
|
graph LR
|
|
this["lib-core.selection"]
|
|
lib_core_input["lib-core.input"]
|
|
this --> lib_core_input
|
|
lib_core_camera["lib-core.camera"]
|
|
this --> lib_core_camera
|
|
engine["engine.*"]
|
|
this --> engine
|
|
```
|
|
<!-- topology:end -->
|
|
|
|
## API
|
|
|
|
### `selection.register(aabb_fn)`
|
|
**Syntax:** `selection.register(aabb_fn: fun() -> {x: number, y: number, w: number, h: number}) -> handle`
|
|
|
|
**Example:**
|
|
```lua
|
|
local h = selection.register(function()
|
|
return { x = entity.x, y = entity.y, w = entity.w, h = entity.h }
|
|
end)
|
|
```
|
|
|
|
**Description:** Registers a selectable entity. Caller supplies an AABB-callback returning current world-space `{x, y, w, h}`. Returns an opaque handle.
|
|
|
|
### `selection.unregister(handle)`
|
|
**Syntax:** `selection.unregister(handle: handle) -> void`
|
|
|
|
**Description:** Removes a selectable. Also removes from the active selection-set if present. No-op for unknown handles.
|
|
|
|
### `selection.count_registered()`
|
|
**Syntax:** `selection.count_registered() -> integer`
|
|
|
|
**Description:** Returns the number of registered selectables.
|
|
|
|
### `selection.list()`
|
|
**Syntax:** `selection.list() -> handle[]`
|
|
|
|
**Description:** Returns an array of currently-selected handles.
|
|
|
|
### `selection.count()`
|
|
**Syntax:** `selection.count() -> integer`
|
|
|
|
**Description:** Returns the size of the selection-set.
|
|
|
|
### `selection.contains(handle)`
|
|
**Syntax:** `selection.contains(handle: handle) -> bool`
|
|
|
|
**Description:** Returns true iff `handle` is in the selection-set.
|
|
|
|
### `selection.is_empty()`
|
|
**Syntax:** `selection.is_empty() -> bool`
|
|
|
|
**Description:** Returns true iff the selection-set is empty.
|
|
|
|
### `selection.add(handle)`
|
|
**Syntax:** `selection.add(handle: handle) -> void`
|
|
|
|
**Description:** Adds a handle to the selection-set. No-op if already selected or handle unknown.
|
|
|
|
### `selection.remove(handle)`
|
|
**Syntax:** `selection.remove(handle: handle) -> void`
|
|
|
|
**Description:** Removes a handle from the selection-set. No-op if not selected.
|
|
|
|
### `selection.toggle(handle)`
|
|
**Syntax:** `selection.toggle(handle: handle) -> void`
|
|
|
|
**Description:** Adds the handle if absent, removes if present.
|
|
|
|
### `selection.clear()`
|
|
**Syntax:** `selection.clear() -> void`
|
|
|
|
**Description:** Empties the selection-set.
|
|
|
|
### `selection.bind_action(action_name)`
|
|
**Syntax:** `selection.bind_action(action_name: string) -> void`
|
|
|
|
**Example:**
|
|
```lua
|
|
input.bind("lmb", { "mouse_left" })
|
|
selection.bind_action("lmb")
|
|
```
|
|
|
|
**Description:** Binds the primary select-action (typically LMB). Press starts a potential click/drag; release resolves it (click selects the topmost AABB under the cursor; drag selects all AABBs intersecting the box).
|
|
|
|
### `selection.bind_modifier_add(action_name)`
|
|
**Syntax:** `selection.bind_modifier_add(action_name: string) -> void`
|
|
|
|
**Description:** Binds a modifier-action (typically Shift). While held, new selections ADD to the existing set instead of replacing.
|
|
|
|
### `selection.bind_modifier_toggle(action_name)`
|
|
**Syntax:** `selection.bind_modifier_toggle(action_name: string) -> void`
|
|
|
|
**Description:** Binds a modifier-action (typically Ctrl). While held, selections TOGGLE instead of replacing.
|
|
|
|
### `selection.set_drag_threshold(px)`
|
|
**Syntax:** `selection.set_drag_threshold(px: number) -> void`
|
|
|
|
**Description:** Sets the pixel-distance threshold that disambiguates click vs drag. Default ~4 px.
|
|
|
|
### `selection.drag_threshold()`
|
|
**Syntax:** `selection.drag_threshold() -> number`
|
|
|
|
**Description:** Returns the current drag-threshold.
|
|
|
|
### `selection.set_enabled(b)`
|
|
**Syntax:** `selection.set_enabled(b: bool) -> void`
|
|
|
|
**Description:** Master enable/disable.
|
|
|
|
### `selection.enabled()`
|
|
**Syntax:** `selection.enabled() -> bool`
|
|
|
|
**Description:** Returns master-enabled state.
|
|
|
|
### `selection.drag_state()`
|
|
**Syntax:** `selection.drag_state() -> string`
|
|
|
|
**Description:** Returns `"idle"`, `"ambiguous"` (button down but haven't crossed drag-threshold yet), or `"dragging"`.
|
|
|
|
### `selection.box_rect()`
|
|
**Syntax:** `selection.box_rect() -> {x: number, y: number, w: number, h: number} | nil`
|
|
|
|
**Description:** Returns the current drag-box in screen-space, or `nil` if not currently dragging.
|
|
|
|
### `selection.set_drag_box_color(r, g, b, a)`
|
|
**Syntax:** `selection.set_drag_box_color(r: integer, g: integer, b: integer, a: integer) -> void`
|
|
|
|
**Description:** Sets the alpha-tinted fill color of the drag-box overlay.
|
|
|
|
### `selection.drag_box_color()`
|
|
**Syntax:** `selection.drag_box_color() -> integer, integer, integer, integer`
|
|
|
|
**Description:** Returns current drag-box color components.
|
|
|
|
### `selection.set_highlight_color(r, g, b, a)`
|
|
**Syntax:** `selection.set_highlight_color(r: integer, g: integer, b: integer, a: integer) -> void`
|
|
|
|
**Description:** Sets the alpha-tinted color used by `render_highlights` for selected entities.
|
|
|
|
### `selection.highlight_color()`
|
|
**Syntax:** `selection.highlight_color() -> integer, integer, integer, integer`
|
|
|
|
**Description:** Returns current highlight color components.
|
|
|
|
### `selection.update(dt)`
|
|
**Syntax:** `selection.update(dt: number) -> void`
|
|
|
|
**Example:**
|
|
```lua
|
|
function update(ctx, dt) selection.update(dt) end
|
|
```
|
|
|
|
**Description:** Per-frame state-machine: tracks press/release, threshold-crossing, drag-box assembly, hit-test on release, applies add/toggle/replace based on modifiers.
|
|
|
|
### `selection.render_drag_box()`
|
|
**Syntax:** `selection.render_drag_box() -> void`
|
|
|
|
**Example:**
|
|
```lua
|
|
selection.render_drag_box() -- screen-space, draw after camera.finish()
|
|
```
|
|
|
|
**Description:** Draws the alpha-tinted drag-box (screen-space). No-op when not dragging.
|
|
|
|
### `selection.render_highlights()`
|
|
**Syntax:** `selection.render_highlights() -> void`
|
|
|
|
**Example:**
|
|
```lua
|
|
camera.begin()
|
|
-- world entities
|
|
selection.render_highlights() -- world-space
|
|
camera.finish()
|
|
```
|
|
|
|
**Description:** Draws alpha-tinted highlights over all selected AABBs (world-space). Call inside `camera.begin/finish`.
|
|
|
|
## Conventions
|
|
|
|
- Handle-registry pattern (parallel to `lib-core.interaction`).
|
|
- Last-registered = end-of-array; click hit-test picks the last-matching AABB (top-most by registration order).
|
|
- Drag-box is screen-space; highlights are world-space.
|
|
- Click vs drag disambiguated by pixel-threshold from press-position.
|
|
- Modifier semantics: `mod_add` adds to set, `mod_toggle` toggles; no modifier = replace.
|
|
- v0.1.0 (P.3.10 patch): uses `lib-core.input.was_action_released` (no more `prev_click_down` workaround).
|
|
- Y-down-positive per ADR-0031.
|
|
|
|
## Consumer pattern
|
|
|
|
```lua
|
|
local selection = require("lib-core.selection")
|
|
local input = require("lib-core.input")
|
|
local camera = require("lib-core.camera")
|
|
|
|
input.bind("lmb", { "mouse_left" })
|
|
input.bind("mod_add", { "shift" })
|
|
input.bind("mod_toggle", { "ctrl" })
|
|
|
|
selection.bind_action("lmb")
|
|
selection.bind_modifier_add("mod_add")
|
|
selection.bind_modifier_toggle("mod_toggle")
|
|
|
|
local handle = selection.register(function()
|
|
return { x = entity.x, y = entity.y, w = entity.w, h = entity.h }
|
|
end)
|
|
|
|
function update(ctx, dt) selection.update(dt) end
|
|
|
|
function render(ctx)
|
|
camera.begin()
|
|
-- draw entities
|
|
selection.render_highlights() -- world-space
|
|
camera.finish()
|
|
selection.render_drag_box() -- screen-space
|
|
end
|
|
```
|
|
|
|
## CHANGELOG
|
|
|
|
### v0.1.0 (P.3.6 + P.3.10 patch)
|
|
- Initial release: click-select + drag-box-select + selection-set + add/toggle modifiers.
|
|
- P.3.10 patch: uses `lib-core.input.was_action_released`; `prev_click_down` workaround removed.
|
|
|
|
## References
|
|
|
|
- Spec v0.1.0 (P.3.6): `meta/docs/superpowers/specs/2026-05-14-p3-6-lib-selection-design.md`
|
|
- ADR-0001 (engine knows verbs, libs bring nouns)
|
|
- ADR-0031 (pixel-convention: Y-down-positive)
|
|
- ADR-0038 (API-Doc-Convention)
|