Files
sporel-lib-core.selection/README.md
Axel Meyer abe8dfa1bf 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) <noreply@anthropic.com>
2026-05-16 16:42:22 +02:00

8.1 KiB

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

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:

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:

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:

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:

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:

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

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)