# 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 ```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 ``` ## 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)