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