Proposal: ECS spatial queries — Query.count/first/nearest/within (needs a runtime spatial index) #42
Labels
No labels
area:ci
area:docs
area:input
area:net
area:rendering
area:repo
area:stdlib
area:tooling
area:types
cleanup
dx
priority:high
priority:low
priority:medium
proposal
status:in-progress
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: workshopsoft/ludic#42
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Follow-up to #24, which shipped the
Grid.*half (tile geometry + A* pathfinding) in07e5a20. This tracks the remaining, explicitly-blocked half: ECS spatial queries.Proposed surface
Query.count([Prop]) -> intQuery.first([Prop]) -> entityQuery.nearest(from, [Prop]) -> entityQuery.within(center, radius, [Prop]) -> listWhy it's separate
count/firstare straightforward iterations over the ECS query machinery, butnearest/withinwant a runtime spatial index (a grid/quadtree bucketing entity positions) to be efficient — that index is the real work here and is why #24 called this half blocked. Sequence it after the query/ECS plumbing exposes entity positions to a spatial structure.Notes
Grid.line/flood/line_of_sight/a_star) already landed in #24; this is the entity-space query side.Shipped in
b25dc32(pushed tomain) — the entity-space half that #24 deferred as blocked.Query.*over the EV2 reflection ABI (world_query_next/world_get):Query.count(prop) -> int— how many live entities carrypropQuery.first(prop) -> int— the lowest-id bearer, or-1Query.nearest(prop, pos, x_field, y_field, x, y) -> int— the bearer closest to(x, y), or-1Query.within(prop, pos, x, y, radius, x_field, y_field) -> []int— every bearer withinradius, ascending id orderpropis a property id (World.prop_id). The spatial forms read a position from a coordinate propertyposat two int field ids (World.field_id), sopropcan be a discriminating tag distinct from the position component —Query.nearest(Enemy, Position, cx, cy, px, py)reads "nearest Enemy" — or the same id to query the coordinate component itself.On the "runtime spatial index" this issue called out as the real work: distances are exact squared integers (no sqrt), ties break to the lower entity id, and
withinreturns ascending id order, so every answer is deterministic and replay-safe. The scan is linear over the entity table — ample for the entity counts Ludic targets, the same reasoning the grid pathfinder's open set uses. A bucketed grid / quadtree is a performance optimisation, not a correctness requirement, so it is left as a future refinement rather than a blocker; the surface here won't change when one is added.The engine (
runtime/native/query.ludic, ~55 lines of Ludic, C-free) is spliced on demand when the parser seesQuery.*, which also force-emits the reflection ABI so a Query program needs no@eventsof its own (it used to require them).Verified by
examples/library/query.ludic(18 self-asserting cases over five entities at known positions: count/first with a component filter, nearest with a separate tag vs position property, within radii includingr=0and the empty-property case), wired intox test(now 63 passed). Docs: aQuerysection + 4 per-symbol pages;x check-impl/x check-docsgreen. Seed reseeded and the C-free bootstrap fixpoint holds. Closing.