Skip to main content

clawft_kernel/
spatial_backend.rs

1//! Unified spatial index backend trait (ADR-056 §8 / WEFT-718).
2//!
3//! Mirrors [`crate::vector_backend::VectorBackend`] conventions: epoch
4//! versioning, `backend_name`, `Send + Sync` for `Arc` sharing. Compiled
5//! only when the `ecc` feature is enabled.
6
7use clawft_bvh::{
8    Aabb, BranchId, BranchMeta, DiffEntry, Frustum, Leaf, LeafId, Ray, RayHit, Vec3,
9};
10use serde::{Deserialize, Serialize};
11use thiserror::Error;
12
13// ── Error ────────────────────────────────────────────────────────────────
14
15/// Errors from spatial backend operations.
16#[derive(Debug, Error)]
17pub enum SpatialError {
18    /// Configured leaf capacity exceeded.
19    #[error("capacity exceeded: max {max} leaves (current {current})")]
20    CapacityExceeded {
21        /// Configured max.
22        max: usize,
23        /// Current count.
24        current: usize,
25    },
26
27    /// Optimistic concurrency conflict.
28    #[error("epoch conflict: expected {expected}, actual {actual}")]
29    EpochConflict {
30        /// Caller's epoch.
31        expected: u64,
32        /// Store epoch.
33        actual: u64,
34    },
35
36    /// Unknown branch id.
37    #[error("unknown branch {0}")]
38    UnknownBranch(u64),
39
40    /// I/O during persistence.
41    #[error("io error: {0}")]
42    Io(#[from] std::io::Error),
43
44    /// Generic backend error.
45    #[error("{0}")]
46    Other(String),
47}
48
49/// Result alias for spatial backend operations.
50pub type SpatialResult<T> = Result<T, SpatialError>;
51
52// ── Backend kind (re-export shape for kernel callers) ────────────────────
53
54/// Which spatial backend to construct (from config).
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
56#[serde(rename_all = "lowercase")]
57pub enum SpatialBackendKind {
58    /// In-memory BVH (Phase C only variant).
59    #[default]
60    Bvh,
61}
62
63// ── Trait ────────────────────────────────────────────────────────────────
64
65/// Unified spatial index backend interface (ADR-056 §8).
66///
67/// Implementations must be `Send + Sync` so they can be shared across
68/// async tasks via `Arc`.
69pub trait SpatialBackend: Send + Sync {
70    /// Insert a leaf; returns the assigned id.
71    fn insert(&self, leaf: Leaf) -> SpatialResult<LeafId>;
72
73    /// Remove a leaf by id. Returns `true` if it was present.
74    fn remove(&self, id: LeafId) -> bool;
75
76    /// Fetch a leaf by id.
77    fn get(&self, id: LeafId) -> Option<Leaf>;
78
79    /// Leaves whose AABB contains `p`.
80    fn query_point(&self, p: Vec3) -> Vec<LeafId>;
81
82    /// Leaves intersecting `bb`.
83    fn query_aabb(&self, bb: Aabb) -> Vec<LeafId>;
84
85    /// Leaves intersecting sphere.
86    fn query_sphere(&self, center: Vec3, radius: f32) -> Vec<LeafId>;
87
88    /// Leaves hit by ray (sorted by enter `t`).
89    fn query_ray(&self, ray: Ray, max_t: f32) -> Vec<RayHit>;
90
91    /// Leaves intersecting a frustum.
92    fn query_frustum(&self, frustum: &Frustum) -> Vec<LeafId>;
93
94    /// k nearest leaves by AABB-center distance.
95    fn query_knn(&self, p: Vec3, k: usize) -> Vec<LeafId>;
96
97    /// Derive a child branch (COW in Phase D; full clone in Phase C).
98    fn derive_branch(&self, meta: BranchMeta) -> SpatialResult<BranchId>;
99
100    /// Presence-only branch diff inside `region`.
101    fn branch_diff(&self, a: BranchId, b: BranchId, region: Aabb) -> Vec<DiffEntry>;
102
103    /// Monotonic epoch (bumped on every mutation).
104    fn epoch(&self) -> u64;
105
106    /// Number of leaves on the active branch.
107    fn len(&self) -> usize;
108
109    /// True when the active branch has no leaves.
110    fn is_empty(&self) -> bool {
111        self.len() == 0
112    }
113
114    /// Human-readable backend name (e.g. `"bvh"`).
115    fn backend_name(&self) -> &str;
116
117    /// Optional capacity limit.
118    fn max_leaves(&self) -> Option<usize> {
119        None
120    }
121
122    /// Insert only if store epoch matches `parent_epoch`.
123    fn insert_with_epoch(&self, leaf: Leaf, parent_epoch: u64) -> SpatialResult<LeafId> {
124        let current = self.epoch();
125        if parent_epoch < current {
126            return Err(SpatialError::EpochConflict {
127                expected: parent_epoch,
128                actual: current,
129            });
130        }
131        self.insert(leaf)
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn backend_kind_default_is_bvh() {
141        assert_eq!(SpatialBackendKind::default(), SpatialBackendKind::Bvh);
142    }
143
144    #[test]
145    fn error_display() {
146        let err = SpatialError::CapacityExceeded {
147            max: 10,
148            current: 10,
149        };
150        assert!(format!("{err}").contains("10"));
151        let err = SpatialError::EpochConflict {
152            expected: 1,
153            actual: 2,
154        };
155        assert!(format!("{err}").contains('1'));
156    }
157}