Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SpatialIndex

Spatial index for Scene items.

Public types

KindName
traitSpatialIndex — A spatial index over ItemIds keyed by axis-aligned scene rectangles
constDEFAULT_CELL_SIZE — Default cell size for GridHashIndex — 256 logical pixels
structGridHashIndex — Uniform grid spatial hash

Public functions

SpatialIndex

ReturnsFunction
Methods
insert(id: ItemId, bounds: Rect)
remove(id: ItemId)
Vec<ItemId>query(scene_rect: Rect)
boolcontains(id: ItemId)
usizelen()
bool { /* default implementation */ }is_empty()

GridHashIndex

ReturnsFunction
Constructors
Selfnew(cell_size: f32)
Methods
f32cell_size()
usizecell_count()

Detailed description

GridHashIndex is the only shipped implementation — a uniform grid hash. The SpatialIndex trait is deliberately small — three mutating operations (insert, remove, query) plus two read methods (contains, len) — so an application that needs different behaviour (e.g. an R-tree) can supply its own implementation in a one-line change via Scene::with_index.

Why grid hash first

  • Cache-friendly: items in the same cell are stored contiguously.

  • Insert / remove / move are amortised O(k) where k is the number of cells the item overlaps (typically 1–4 for items smaller than the cell size).

  • query(rect) returns deduplicated candidates from the cells the rect overlaps; callers can narrow with a per-item AABB check.

  • Oversized items. An item whose AABB would bucket into more than MAX_CELLS_PER_ITEM grid cells (a scene backdrop, a full-document canvas rect, or any item at extreme coordinates with large bounds — all reachable in production, not exotic) is NOT bucketed cell-by-cell at all. It is stored instead in a separate oversized: HashMap<ItemId, Rect> that query always scans in full, in addition to the cell lookup, keeping an exact AABB-intersection test against scene_rect (so it contributes no cell-fan-out false positives of its own).

    This closes what used to be an unconditional, uncapped eager allocation: cells_for_rect computed (width / cell_size) * (height / cell_size) cells and reserved that many (i32, i32) slots before the loop that fills them ran — no upper bound, and using bare i32 arithmetic that could itself overflow for large extents (debug builds panicked, release builds could wrap to a huge or negative usize). A single 1e6 × 1e6 logical-pixel item at the clamped-minimum cell_size of 1.0 asked for (1e6+1)² ≈ 1e12 cells — roughly 8 TB for the Vec<(i32, i32)> alone — before any assertion or even the fill loop ran; this was reachable from a single Scene::add_item call, no adversarial input required. Even at the default 256 px cell_size, a 1e6-square item alone reserved (1e6 / 256)² ≈ 1.5e7 cells (~122 MB) for that one item. The same hazard applied to query/items_in_rect, since a caller can pass an arbitrarily large scene_rect too — see query's own oversized-span fallback.

    A custom SpatialIndex would still handle non-uniform density better — an R-tree, say, for an editor with many overlapping items — but none ships; the trait is the place to add one.

Default cell_size is DEFAULT_CELL_SIZE (256.0 logical pixels) — large enough that typical card-sized items (~200 px) bucket into 1–4 cells and small enough that viewport queries (~800–1200 px) hit a manageable fan-out.

Example

// ItemId values are obtained from Scene::add_item in real code;
// the example uses the crate-internal constructor for illustration.
use teksilo_scene::{GridHashIndex, SpatialIndex, ItemId};
use teksilo_canvas::Rect;

let mut index = GridHashIndex::default();
let id = ItemId(1); // in practice: returned by Scene::add_item
index.insert(id, Rect::new(10.0, 10.0, 80.0, 80.0));
assert!(index.contains(id));

let hits = index.query(Rect::new(0.0, 0.0, 100.0, 100.0));
assert!(hits.contains(&id));

index.remove(id);
assert!(index.is_empty());

API reference

📖 Full rustdoc API for this module

pub trait SpatialIndex

Import: use teksilo_scene::SpatialIndex; — add teksilo-scene to [dependencies]: teksilo does not re-export it.

A spatial index over ItemIds keyed by axis-aligned scene rectangles. Used by Scene for items_in_rect queries and by SceneView for viewport culling.

#![allow(unused)]
fn main() {
pub trait SpatialIndex: Send + std::fmt::Debug { /* associated items below */ }
}

Associated items

fn insert(&mut self, id: ItemId, bounds: Rect);

Insert or update an item's bounds. Calling insert again with the same id replaces the previous bounds (re-buckets the item). Equivalent to remove(id); insert(id, bounds); on implementations that need an explicit update path.

fn remove(&mut self, id: ItemId);

Remove an item. No-op if id is not present.

fn query(&self, scene_rect: Rect) -> Vec<ItemId>;

Items whose bounds intersect scene_rect, in implementation- defined order. The result is deduplicated. May include false positives (items in cells the rect overlaps but whose bounds don't actually intersect) — callers that need exact intersection narrow with a per-item check.

fn contains(&self, id: ItemId) -> bool;

Whether id is currently in the index.

fn len(&self) -> usize;

Total number of items in the index.

fn is_empty(&self) -> bool { /* default implementation */ }

Whether the index is empty.

pub const DEFAULT_CELL_SIZE

Import: none — DEFAULT_CELL_SIZE is public in a private module of teksilo-scene and nothing re-exports it.

Default cell size for GridHashIndex — 256 logical pixels. Item-side typical 200 px cards bucket into 1–4 cells; viewport queries (~800–1200 px) hit a small fan-out.

#![allow(unused)]
fn main() {
pub const DEFAULT_CELL_SIZE: f32 = 256.0;
}

pub struct GridHashIndex

Import: use teksilo_scene::GridHashIndex; — add teksilo-scene to [dependencies]: teksilo does not re-export it.

Uniform grid spatial hash. Each item is bucketed into every cell its AABB overlaps; queries union all items from the cells the query rect overlaps. Items whose AABB would span more than MAX_CELLS_PER_ITEM cells are NOT bucketed — see oversized below and the module doc's "Oversized items" section.

#![allow(unused)]
fn main() {
pub struct GridHashIndex { /* fields */ }
}

Methods

pub fn new(cell_size: f32) -> Self

Create a grid with cell_size logical pixels per cell. Clamped to a minimum of 1.0 to avoid pathological huge bucket counts.

pub fn cell_size(&self) -> f32

The configured cell size in logical pixels.

pub fn cell_count(&self) -> usize

Number of cells currently storing at least one item. Useful for diagnostics; not part of the public SpatialIndex trait. Oversized items (see MAX_CELLS_PER_ITEM) never occupy a cell, so they never contribute to this count.