1use crate::storage::data_structures::NodeRec;
27use anyhow::{bail, Result};
28use bytemuck::{bytes_of, from_bytes, Pod, Zeroable};
29use std::collections::HashMap;
30
31pub const SPATIAL_PAGE_MAGIC: u32 = 0x5347_5041; pub const SPATIAL_PAGE_VERSION: u32 = 1;
33
34pub const DEFAULT_MAX_NODES_PER_PAGE: usize = 56;
36
37#[derive(Debug, Clone, Copy, PartialEq)]
39pub struct BoundingBox {
40 pub min_x: f32,
41 pub max_x: f32,
42 pub min_y: f32,
43 pub max_y: f32,
44 pub min_z: f32,
45 pub max_z: f32,
46}
47
48impl BoundingBox {
49 pub fn new(min_x: f32, max_x: f32, min_y: f32, max_y: f32, min_z: f32, max_z: f32) -> Self {
50 Self {
51 min_x,
52 max_x,
53 min_y,
54 max_y,
55 min_z,
56 max_z,
57 }
58 }
59
60 pub fn from_node(node: &NodeRec) -> Self {
61 Self {
62 min_x: node.x,
63 max_x: node.x,
64 min_y: node.y,
65 max_y: node.y,
66 min_z: node.z,
67 max_z: node.z,
68 }
69 }
70
71 pub fn expand(&mut self, other: &BoundingBox) {
72 self.min_x = self.min_x.min(other.min_x);
73 self.max_x = self.max_x.max(other.max_x);
74 self.min_y = self.min_y.min(other.min_y);
75 self.max_y = self.max_y.max(other.max_y);
76 self.min_z = self.min_z.min(other.min_z);
77 self.max_z = self.max_z.max(other.max_z);
78 }
79
80 pub fn intersects(&self, other: &BoundingBox) -> bool {
81 self.min_x <= other.max_x
82 && self.max_x >= other.min_x
83 && self.min_y <= other.max_y
84 && self.max_y >= other.min_y
85 && self.min_z <= other.max_z
86 && self.max_z >= other.min_z
87 }
88
89 pub fn contains_point(&self, x: f32, y: f32, z: f32) -> bool {
90 x >= self.min_x
91 && x <= self.max_x
92 && y >= self.min_y
93 && y <= self.max_y
94 && z >= self.min_z
95 && z <= self.max_z
96 }
97}
98
99#[repr(C)]
101#[derive(Clone, Copy, Debug, Pod, Zeroable)]
102pub struct SpatialPageHeader {
103 pub magic: u32,
104 pub version: u32,
105 pub morton_prefix: u64,
106 pub node_count: u16, pub morton_shift: u8, pub _pad1: [u8; 5],
109 pub min_x: f32,
111 pub max_x: f32,
112 pub min_y: f32,
113 pub max_y: f32,
114 pub min_z: f32,
115 pub max_z: f32,
116 pub _pad2: [u8; 8],
117 pub property_block_offset: u32,
119 pub property_block_len: u32,
121 pub edge_cluster_offset: u32,
123 pub edge_cluster_len: u32,
125 pub _reserved: [u8; 16],
126}
127
128impl SpatialPageHeader {
129 pub const LEN: usize = std::mem::size_of::<Self>();
130
131 pub fn validate(&self) -> Result<()> {
132 if self.magic != SPATIAL_PAGE_MAGIC {
133 bail!(
134 "SpatialPageHeader: bad magic (expected 0x{:08x}, got 0x{:08x})",
135 SPATIAL_PAGE_MAGIC,
136 self.magic
137 );
138 }
139 if self.version != SPATIAL_PAGE_VERSION {
140 bail!(
141 "SpatialPageHeader: unsupported version {} (expected {})",
142 self.version,
143 SPATIAL_PAGE_VERSION
144 );
145 }
146 Ok(())
147 }
148
149 pub fn bbox(&self) -> BoundingBox {
150 BoundingBox::new(
151 self.min_x, self.max_x, self.min_y, self.max_y, self.min_z, self.max_z,
152 )
153 }
154}
155
156#[repr(C)]
159#[derive(Clone, Copy, Debug, Pod, Zeroable)]
160pub struct PageLocalEdge {
161 pub src_node_idx: u16,
162 pub _pad1: [u8; 6],
163 pub dst_node_id: u64,
164 pub weight: f32,
165 pub flags: u32,
166}
167
168#[derive(Debug, Clone)]
170pub struct SpatialPage {
171 pub header: SpatialPageHeader,
172 pub nodes: Vec<NodeRec>,
173 pub properties: HashMap<u64, HashMap<String, String>>,
175 pub edges: Vec<PageLocalEdge>,
177}
178
179impl SpatialPage {
180 pub fn new(morton_prefix: u64, morton_shift: u8) -> Self {
182 Self {
183 header: SpatialPageHeader {
184 magic: SPATIAL_PAGE_MAGIC,
185 version: SPATIAL_PAGE_VERSION,
186 morton_prefix,
187 morton_shift,
188 node_count: 0,
189 _pad1: [0; 5],
190 min_x: f32::MAX,
191 max_x: f32::MIN,
192 min_y: f32::MAX,
193 max_y: f32::MIN,
194 min_z: f32::MAX,
195 max_z: f32::MIN,
196 _pad2: [0; 8],
197 property_block_offset: 0,
198 property_block_len: 0,
199 edge_cluster_offset: 0,
200 edge_cluster_len: 0,
201 _reserved: [0; 16],
202 },
203 nodes: vec![],
204 properties: HashMap::new(),
205 edges: vec![],
206 }
207 }
208
209 pub fn pack(&self) -> Result<Vec<u8>> {
211 let node_bytes = self.nodes.len() * std::mem::size_of::<NodeRec>();
213 let node_bytes_aligned = align_up(node_bytes, 8);
214
215 let mut prop_buf = Vec::new();
217 for (node_id, props) in &self.properties {
218 prop_buf.extend_from_slice(&node_id.to_le_bytes());
219 let entry_bytes = encode_property_entry(props)?;
220 prop_buf.extend_from_slice(&(entry_bytes.len() as u32).to_le_bytes());
221 prop_buf.extend_from_slice(&entry_bytes);
222 }
223 let prop_bytes_aligned = align_up(prop_buf.len(), 8);
224
225 let edge_bytes = self.edges.len() * std::mem::size_of::<PageLocalEdge>();
226
227 let total = SpatialPageHeader::LEN + node_bytes_aligned + prop_bytes_aligned + edge_bytes;
228 let mut buf = Vec::with_capacity(total);
229
230 let mut header = self.header;
232 header.node_count = self.nodes.len() as u16;
233 header.edge_cluster_len = self.edges.len() as u32;
234
235 let mut offset = SpatialPageHeader::LEN;
237
238 offset += node_bytes_aligned;
240
241 header.property_block_offset = offset as u32;
242 header.property_block_len = prop_buf.len() as u32;
243 offset += prop_bytes_aligned;
244
245 header.edge_cluster_offset = offset as u32;
246
247 buf.extend_from_slice(bytes_of(&header));
248
249 for node in &self.nodes {
251 buf.extend_from_slice(bytes_of(node));
252 }
253 pad_to_alignment(&mut buf, 8);
254
255 buf.extend_from_slice(&prop_buf);
257 pad_to_alignment(&mut buf, 8);
258
259 for edge in &self.edges {
261 buf.extend_from_slice(bytes_of(edge));
262 }
263
264 Ok(buf)
265 }
266
267 pub fn unpack(data: &[u8]) -> Result<Self> {
269 if data.len() < SpatialPageHeader::LEN {
270 bail!(
271 "SpatialPage unpack: data too short for header ({} < {})",
272 data.len(),
273 SpatialPageHeader::LEN
274 );
275 }
276 let header: &SpatialPageHeader = from_bytes(&data[0..SpatialPageHeader::LEN]);
277 header.validate()?;
278
279 let node_count = header.node_count as usize;
280 let edge_count = header.edge_cluster_len as usize;
281
282 let node_start = SpatialPageHeader::LEN;
284 let node_bytes = node_count * std::mem::size_of::<NodeRec>();
285 let node_end = node_start + node_bytes;
286 if data.len() < node_end {
287 bail!("SpatialPage unpack: data too short for nodes");
288 }
289 let nodes: &[NodeRec] = bytemuck::cast_slice(&data[node_start..node_end]);
290 let nodes = nodes.to_vec(); let prop_offset = header.property_block_offset as usize;
294 let prop_len = header.property_block_len as usize;
295 let mut properties = HashMap::new();
296 if prop_len > 0 {
297 let prop_end = prop_offset + prop_len;
298 if data.len() < prop_end {
299 bail!("SpatialPage unpack: data too short for properties");
300 }
301 let mut cursor = prop_offset;
302 while cursor < prop_end {
303 if cursor + 12 > data.len() {
304 bail!("SpatialPage unpack: truncated property entry");
305 }
306 let node_id = u64::from_le_bytes([
307 data[cursor],
308 data[cursor + 1],
309 data[cursor + 2],
310 data[cursor + 3],
311 data[cursor + 4],
312 data[cursor + 5],
313 data[cursor + 6],
314 data[cursor + 7],
315 ]);
316 cursor += 8;
317 let entry_len = u32::from_le_bytes([
318 data[cursor],
319 data[cursor + 1],
320 data[cursor + 2],
321 data[cursor + 3],
322 ]) as usize;
323 cursor += 4;
324 if cursor + entry_len > data.len() {
325 bail!("SpatialPage unpack: truncated property data");
326 }
327 let entry = decode_property_entry(&data[cursor..cursor + entry_len])?;
328 cursor += entry_len;
329 properties.insert(node_id, entry);
330 }
331 }
332
333 let edge_offset = header.edge_cluster_offset as usize;
335 let edge_bytes = edge_count * std::mem::size_of::<PageLocalEdge>();
336 let edge_end = edge_offset + edge_bytes;
337 if data.len() < edge_end {
338 bail!("SpatialPage unpack: data too short for edges");
339 }
340 let edges: &[PageLocalEdge] = bytemuck::cast_slice(&data[edge_offset..edge_end]);
341 let edges = edges.to_vec();
342
343 Ok(SpatialPage {
344 header: *header,
345 nodes,
346 properties,
347 edges,
348 })
349 }
350
351 pub fn find_node_by_id(&self, id: u64) -> Option<usize> {
354 self.nodes.binary_search_by_key(&id, |n| n.id).ok()
355 }
356
357 pub fn get_properties(&self, node_id: u64) -> Option<&HashMap<String, String>> {
359 self.properties.get(&node_id)
360 }
361
362 pub fn get_edges_for_node(&self, node_id: u64) -> Vec<&PageLocalEdge> {
364 if let Some(idx) = self.find_node_by_id(node_id) {
365 let idx = idx as u16;
366 self.edges
367 .iter()
368 .filter(|e| e.src_node_idx == idx)
369 .collect()
370 } else {
371 vec![]
372 }
373 }
374}
375
376pub fn build_spatial_pages(
382 mut nodes: Vec<NodeRec>,
383 properties: &HashMap<u64, HashMap<String, String>>,
384 edges: &[(u64, u64, f32, u32)], max_nodes_per_page: usize,
386) -> Vec<SpatialPage> {
387 if nodes.is_empty() {
388 return vec![];
389 }
390
391 nodes.sort_by_key(|n| n.morton_code);
392
393 let mut pages = vec![];
394 let mut start = 0;
395
396 while start < nodes.len() {
397 let base_morton = nodes[start].morton_code;
398 let mut shift: u8 = 64;
399
400 let (end, final_shift) = loop {
401 let prefix = if shift == 0 || shift >= 64 {
402 0u64
403 } else {
404 (base_morton >> shift) << shift
405 };
406
407 let mut end = start;
408 while end < nodes.len() {
409 let candidate = if shift == 0 || shift >= 64 {
410 0u64
411 } else {
412 (nodes[end].morton_code >> shift) << shift
413 };
414 if candidate != prefix || end - start >= max_nodes_per_page {
415 break;
416 }
417 end += 1;
418 }
419
420 if end - start <= max_nodes_per_page || shift == 0 {
421 break (end, shift);
422 }
423 shift = shift.saturating_sub(1);
424 };
425
426 let page_nodes = nodes[start..end].to_vec();
427 let bbox = compute_bbox(&page_nodes);
428
429 let mut page_edges = vec![];
431 for &(src_id, dst_id, weight, flags) in edges {
432 if let Some(src_idx) = page_nodes.iter().position(|n| n.id == src_id) {
433 page_edges.push(PageLocalEdge {
434 src_node_idx: src_idx as u16,
435 _pad1: [0; 6],
436 dst_node_id: dst_id,
437 weight,
438 flags,
439 });
440 }
441 }
442
443 let mut page_props = HashMap::new();
445 for node in &page_nodes {
446 if let Some(props) = properties.get(&node.id) {
447 page_props.insert(node.id, props.clone());
448 }
449 }
450
451 let prefix = if final_shift == 0 || final_shift >= 64 {
452 0u64
453 } else {
454 (base_morton >> final_shift) << final_shift
455 };
456
457 pages.push(SpatialPage {
458 header: SpatialPageHeader {
459 magic: SPATIAL_PAGE_MAGIC,
460 version: SPATIAL_PAGE_VERSION,
461 morton_prefix: prefix,
462 morton_shift: final_shift,
463 node_count: page_nodes.len() as u16,
464 _pad1: [0; 5],
465 min_x: bbox.min_x,
466 max_x: bbox.max_x,
467 min_y: bbox.min_y,
468 max_y: bbox.max_y,
469 min_z: bbox.min_z,
470 max_z: bbox.max_z,
471 _pad2: [0; 8],
472 property_block_offset: 0,
473 property_block_len: 0,
474 edge_cluster_offset: 0,
475 edge_cluster_len: page_edges.len() as u32,
476 _reserved: [0; 16],
477 },
478 nodes: page_nodes,
479 properties: page_props,
480 edges: page_edges,
481 });
482
483 start = end;
484 }
485
486 pages
487}
488
489fn compute_bbox(nodes: &[NodeRec]) -> BoundingBox {
491 let mut bbox = BoundingBox::from_node(&nodes[0]);
492 for node in &nodes[1..] {
493 let nbb = BoundingBox::from_node(node);
494 bbox.expand(&nbb);
495 }
496 bbox
497}
498
499pub fn overlaps(a: &BoundingBox, b: &BoundingBox) -> usize {
505 (if a.max_x > b.min_x && a.min_x < b.max_x {
506 1
507 } else {
508 0
509 } + if a.max_y > b.min_y && a.min_y < b.max_y {
510 1
511 } else {
512 0
513 } + if a.max_z > b.min_z && a.min_z < b.max_z {
514 1
515 } else {
516 0
517 })
518}
519
520fn encode_property_entry(props: &HashMap<String, String>) -> Result<Vec<u8>> {
521 let count = props.len();
522 let mut size = 4; for (k, v) in props {
524 if k.len() > u16::MAX as usize || v.len() > u16::MAX as usize {
525 bail!("property key/value exceeds 64KB");
526 }
527 size += 2 + k.len() + 2 + v.len();
528 }
529 let mut buf = Vec::with_capacity(size);
530 buf.extend_from_slice(&(count as u32).to_le_bytes());
531 for (k, v) in props {
532 buf.extend_from_slice(&(k.len() as u16).to_le_bytes());
533 buf.extend_from_slice(k.as_bytes());
534 buf.extend_from_slice(&(v.len() as u16).to_le_bytes());
535 buf.extend_from_slice(v.as_bytes());
536 }
537 Ok(buf)
538}
539
540fn decode_property_entry(data: &[u8]) -> Result<HashMap<String, String>> {
541 if data.len() < 4 {
542 bail!("property entry too short");
543 }
544 let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
545 let mut props = HashMap::with_capacity(count);
546 let mut cursor = 4usize;
547
548 for _ in 0..count {
549 if cursor + 2 > data.len() {
550 bail!("truncated property key length");
551 }
552 let klen = u16::from_le_bytes([data[cursor], data[cursor + 1]]) as usize;
553 cursor += 2;
554 if cursor + klen > data.len() {
555 bail!("truncated property key");
556 }
557 let key = String::from_utf8(data[cursor..cursor + klen].to_vec())?;
558 cursor += klen;
559
560 if cursor + 2 > data.len() {
561 bail!("truncated property value length");
562 }
563 let vlen = u16::from_le_bytes([data[cursor], data[cursor + 1]]) as usize;
564 cursor += 2;
565 if cursor + vlen > data.len() {
566 bail!("truncated property value");
567 }
568 let val = String::from_utf8(data[cursor..cursor + vlen].to_vec())?;
569 cursor += vlen;
570
571 props.insert(key, val);
572 }
573
574 Ok(props)
575}
576
577#[inline]
582fn align_up(n: usize, align: usize) -> usize {
583 (n + align - 1) & !(align - 1)
584}
585
586fn pad_to_alignment(buf: &mut Vec<u8>, align: usize) {
587 let aligned = align_up(buf.len(), align);
588 buf.resize(aligned, 0);
589}
590
591#[cfg(test)]
596mod tests {
597 use super::*;
598
599 fn make_node(id: u64, morton: u64, x: f32, y: f32, z: f32) -> NodeRec {
600 NodeRec {
601 id,
602 morton_code: morton,
603 x,
604 y,
605 z,
606 edge_off: 0,
607 edge_len: 0,
608 flags: 0,
609 begin_ts: 1,
610 end_ts: 0,
611 tx_id: 1,
612 visibility: 1,
613 _padding: [0; 7],
614 }
615 }
616
617 #[test]
618 fn test_pack_unpack_empty() {
619 let page = SpatialPage::new(0, 64);
620 let packed = page.pack().unwrap();
621 let unpacked = SpatialPage::unpack(&packed).unwrap();
622 assert_eq!(unpacked.header.magic, SPATIAL_PAGE_MAGIC);
623 assert_eq!(unpacked.nodes.len(), 0);
624 assert_eq!(unpacked.edges.len(), 0);
625 }
626
627 #[test]
628 fn test_pack_unpack_with_nodes() {
629 let mut page = SpatialPage::new(0, 64);
630 page.nodes.push(make_node(1, 0b0000, 0.0, 0.0, 0.0));
631 page.nodes.push(make_node(2, 0b0001, 1.0, 1.0, 1.0));
632
633 let mut props = HashMap::new();
634 props.insert(1u64, {
635 let mut p = HashMap::new();
636 p.insert("kind".to_string(), "sensor".to_string());
637 p
638 });
639
640 page.properties = props;
641
642 page.edges.push(PageLocalEdge {
643 src_node_idx: 0,
644 _pad1: [0; 6],
645 dst_node_id: 2,
646 weight: 1.5,
647 flags: 0,
648 });
649
650 let packed = page.pack().unwrap();
651 let unpacked = SpatialPage::unpack(&packed).unwrap();
652
653 assert_eq!(unpacked.nodes.len(), 2);
654 assert_eq!(unpacked.nodes[0].id, 1);
655 assert_eq!(unpacked.nodes[1].id, 2);
656 assert_eq!(
657 unpacked.properties.get(&1).unwrap().get("kind").unwrap(),
658 "sensor"
659 );
660 assert_eq!(unpacked.edges.len(), 1);
661 assert_eq!(unpacked.edges[0].dst_node_id, 2);
662 assert_eq!(unpacked.edges[0].weight, 1.5);
663 }
664
665 #[test]
666 fn test_build_spatial_pages_grouping() {
667 let nodes = vec![
668 make_node(1, 0b0000_0000, 0.0, 0.0, 0.0),
669 make_node(2, 0b0000_0001, 1.0, 0.0, 0.0),
670 make_node(3, 0b1111_0000, 10.0, 10.0, 10.0),
671 make_node(4, 0b1111_0001, 11.0, 10.0, 10.0),
672 ];
673
674 let pages = build_spatial_pages(nodes, &HashMap::new(), &[], 2);
675 assert_eq!(pages.len(), 2, "Should split into 2 pages by morton prefix");
676 assert_eq!(pages[0].nodes.len(), 2);
677 assert_eq!(pages[1].nodes.len(), 2);
678 }
679
680 #[test]
681 fn test_bounding_box_intersects() {
682 let a = BoundingBox::new(0.0, 10.0, 0.0, 10.0, 0.0, 10.0);
683 let b = BoundingBox::new(5.0, 15.0, 5.0, 15.0, 5.0, 15.0);
684 assert!(a.intersects(&b));
685
686 let c = BoundingBox::new(20.0, 30.0, 0.0, 10.0, 0.0, 10.0);
687 assert!(!a.intersects(&c));
688 }
689
690 #[test]
691 fn test_binary_search_in_page() {
692 let mut page = SpatialPage::new(0, 64);
693 page.nodes.push(make_node(10, 0b0010, 0.0, 0.0, 0.0));
694 page.nodes.push(make_node(20, 0b0100, 1.0, 1.0, 1.0));
695 page.nodes.push(make_node(30, 0b1000, 2.0, 2.0, 2.0));
696
697 assert_eq!(page.find_node_by_id(20), Some(1));
698 assert_eq!(page.find_node_by_id(99), None);
699 }
700}