1use rand::Rng;
65use rustsim_core::{
66 interaction::{PositionedAgent, SpaceInteraction},
67 space::Space,
68 types::{AgentId, ZoneId},
69};
70use std::collections::{HashMap, HashSet, VecDeque};
71use thiserror::Error;
72
73use crate::continuous3d::ContinuousPos3D;
74use crate::graph::{GraphPos, GraphSpace, NeighborType};
75
76#[derive(Debug, Clone, Copy, PartialEq)]
82pub struct HybridPos {
83 pub node: ZoneId,
85 pub offset: ContinuousPos3D,
87}
88
89impl HybridPos {
90 pub fn new(node: ZoneId, offset: ContinuousPos3D) -> Self {
91 Self { node, offset }
92 }
93
94 pub fn at_node(node: ZoneId) -> Self {
95 Self {
96 node,
97 offset: ContinuousPos3D::zero(),
98 }
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
108pub enum ZoneExtentError {
109 #[error("zone extents must be positive")]
110 InvalidExtent,
111}
112
113#[derive(Debug, Clone, Copy)]
115pub struct ZoneExtent {
116 pub size_x: f64,
117 pub size_y: f64,
118 pub size_z: f64,
119}
120
121impl ZoneExtent {
122 pub fn new(size_x: f64, size_y: f64, size_z: f64) -> Result<Self, ZoneExtentError> {
123 if size_x <= 0.0 || size_y <= 0.0 || size_z <= 0.0 {
124 return Err(ZoneExtentError::InvalidExtent);
125 }
126 Ok(Self {
127 size_x,
128 size_y,
129 size_z,
130 })
131 }
132
133 pub fn cube(size: f64) -> Result<Self, ZoneExtentError> {
134 Self::new(size, size, size)
135 }
136
137 pub fn contains(&self, p: &ContinuousPos3D) -> bool {
138 p.x >= 0.0
139 && p.x < self.size_x
140 && p.y >= 0.0
141 && p.y < self.size_y
142 && p.z >= 0.0
143 && p.z < self.size_z
144 }
145
146 pub fn clamp(&self, p: &ContinuousPos3D) -> ContinuousPos3D {
147 ContinuousPos3D::new(
148 p.x.clamp(0.0, self.size_x - 1e-9),
149 p.y.clamp(0.0, self.size_y - 1e-9),
150 p.z.clamp(0.0, self.size_z - 1e-9),
151 )
152 }
153
154 pub fn center(&self) -> ContinuousPos3D {
155 ContinuousPos3D::new(self.size_x / 2.0, self.size_y / 2.0, self.size_z / 2.0)
156 }
157}
158
159impl Default for ZoneExtent {
160 fn default() -> Self {
161 Self {
162 size_x: 10.0,
163 size_y: 10.0,
164 size_z: 10.0,
165 }
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
175pub enum ZoneWalkmapError {
176 #[error("walkmap dimensions must be positive")]
177 InvalidDimensions,
178 #[error("walkmap size mismatch: expected {expected}, got {actual}")]
179 SizeMismatch { expected: usize, actual: usize },
180}
181
182#[derive(Debug, Clone)]
184pub struct ZoneWalkmap {
185 grid_w: usize,
186 grid_h: usize,
187 grid_d: usize,
188 walkmap: Vec<bool>,
189}
190
191impl ZoneWalkmap {
192 pub fn new(
194 walkmap: Vec<bool>,
195 grid_w: usize,
196 grid_h: usize,
197 grid_d: usize,
198 ) -> Result<Self, ZoneWalkmapError> {
199 if grid_w == 0 || grid_h == 0 || grid_d == 0 {
200 return Err(ZoneWalkmapError::InvalidDimensions);
201 }
202 let expected = grid_w * grid_h * grid_d;
203 if walkmap.len() != expected {
204 return Err(ZoneWalkmapError::SizeMismatch {
205 expected,
206 actual: walkmap.len(),
207 });
208 }
209 Ok(Self {
210 grid_w,
211 grid_h,
212 grid_d,
213 walkmap,
214 })
215 }
216
217 pub fn all_walkable(
219 grid_w: usize,
220 grid_h: usize,
221 grid_d: usize,
222 ) -> Result<Self, ZoneWalkmapError> {
223 Self::new(vec![true; grid_w * grid_h * grid_d], grid_w, grid_h, grid_d)
224 }
225
226 pub fn dimensions(&self) -> (usize, usize, usize) {
228 (self.grid_w, self.grid_h, self.grid_d)
229 }
230
231 pub fn as_slice(&self) -> &[bool] {
233 &self.walkmap
234 }
235
236 pub fn is_walkable_voxel(&self, vx: usize, vy: usize, vz: usize) -> bool {
238 if vx >= self.grid_w || vy >= self.grid_h || vz >= self.grid_d {
239 return false;
240 }
241 self.walkmap[vz * self.grid_h * self.grid_w + vy * self.grid_w + vx]
242 }
243
244 pub fn set_walkable_voxel(&mut self, vx: usize, vy: usize, vz: usize, walkable: bool) {
246 if vx < self.grid_w && vy < self.grid_h && vz < self.grid_d {
247 self.walkmap[vz * self.grid_h * self.grid_w + vy * self.grid_w + vx] = walkable;
248 }
249 }
250
251 fn is_walkable_pos(&self, pos: &ContinuousPos3D, extent: &ZoneExtent) -> bool {
253 let vx = ((pos.x / extent.size_x * self.grid_w as f64).floor() as usize)
254 .min(self.grid_w.saturating_sub(1));
255 let vy = ((pos.y / extent.size_y * self.grid_h as f64).floor() as usize)
256 .min(self.grid_h.saturating_sub(1));
257 let vz = ((pos.z / extent.size_z * self.grid_d as f64).floor() as usize)
258 .min(self.grid_d.saturating_sub(1));
259 self.is_walkable_voxel(vx, vy, vz)
260 }
261}
262
263#[derive(Debug, Clone)]
273struct ZoneSpatialHash {
274 spacing: f64,
275 grid_w: usize,
276 grid_h: usize,
277 grid_d: usize,
278 cells: Vec<Vec<AgentId>>,
279}
280
281impl ZoneSpatialHash {
282 fn new(extent: &ZoneExtent, spacing: f64) -> Result<Self, HybridSpaceError> {
283 if spacing <= 0.0 {
284 return Err(HybridSpaceError::InvalidSpacing(spacing));
285 }
286 let grid_w = (extent.size_x / spacing).ceil() as usize;
287 let grid_h = (extent.size_y / spacing).ceil() as usize;
288 let grid_d = (extent.size_z / spacing).ceil() as usize;
289 Ok(Self {
290 spacing,
291 grid_w,
292 grid_h,
293 grid_d,
294 cells: vec![Vec::new(); grid_w * grid_h * grid_d],
295 })
296 }
297
298 fn pos_to_cell(&self, pos: &ContinuousPos3D) -> (usize, usize, usize) {
299 let cx = ((pos.x / self.spacing).floor() as usize).min(self.grid_w.saturating_sub(1));
300 let cy = ((pos.y / self.spacing).floor() as usize).min(self.grid_h.saturating_sub(1));
301 let cz = ((pos.z / self.spacing).floor() as usize).min(self.grid_d.saturating_sub(1));
302 (cx, cy, cz)
303 }
304
305 fn cell_index(&self, cx: usize, cy: usize, cz: usize) -> usize {
306 (cz * self.grid_h + cy) * self.grid_w + cx
307 }
308
309 fn insert(&mut self, id: AgentId, pos: &ContinuousPos3D) {
310 let (cx, cy, cz) = self.pos_to_cell(pos);
311 let idx = self.cell_index(cx, cy, cz);
312 self.cells[idx].push(id);
313 }
314
315 fn remove(&mut self, id: AgentId, pos: &ContinuousPos3D) {
316 let (cx, cy, cz) = self.pos_to_cell(pos);
317 let idx = self.cell_index(cx, cy, cz);
318 if let Some(i) = self.cells[idx].iter().position(|&a| a == id) {
319 self.cells[idx].swap_remove(i);
320 }
321 }
322
323 fn update(&mut self, id: AgentId, old_pos: &ContinuousPos3D, new_pos: &ContinuousPos3D) {
324 let old_cell = self.pos_to_cell(old_pos);
325 let new_cell = self.pos_to_cell(new_pos);
326 if old_cell != new_cell {
327 self.remove(id, old_pos);
328 self.insert(id, new_pos);
329 }
330 }
331
332 fn nearby_ids(
333 &self,
334 pos: &ContinuousPos3D,
335 radius: f64,
336 all_positions: &HashMap<AgentId, HybridPos>,
337 ) -> Vec<AgentId> {
338 let grid_r = ((radius / self.spacing).ceil() as i32) + 1;
339 let (focal_cx, focal_cy, focal_cz) = self.pos_to_cell(pos);
340 let radius_sq = radius * radius;
341 let mut ids = Vec::new();
342
343 for dz in -grid_r..=grid_r {
344 for dy in -grid_r..=grid_r {
345 for dx in -grid_r..=grid_r {
346 let nx = focal_cx as i32 + dx;
347 let ny = focal_cy as i32 + dy;
348 let nz = focal_cz as i32 + dz;
349
350 if nx >= 0
351 && ny >= 0
352 && nz >= 0
353 && (nx as usize) < self.grid_w
354 && (ny as usize) < self.grid_h
355 && (nz as usize) < self.grid_d
356 {
357 let idx = self.cell_index(nx as usize, ny as usize, nz as usize);
358 for &id in &self.cells[idx] {
359 if let Some(agent_pos) = all_positions.get(&id) {
360 let d = pos.distance_to(&agent_pos.offset);
361 if d * d <= radius_sq {
362 ids.push(id);
363 }
364 }
365 }
366 }
367 }
368 }
369 }
370
371 ids
372 }
373}
374
375#[derive(Debug, Clone, Copy, PartialEq, Error)]
380pub enum HybridSpaceError {
381 #[error("invalid graph node index {0}")]
382 InvalidNode(GraphPos),
383 #[error("position is out of bounds")]
384 OutOfBounds,
385 #[error("no edge from node {from} to node {to}")]
386 NoEdge { from: GraphPos, to: GraphPos },
387 #[error("target position is blocked by an obstacle")]
388 Blocked,
389 #[error("spacing must be positive, got {0}")]
390 InvalidSpacing(f64),
391}
392
393#[derive(Debug, Clone)]
414pub struct HybridSpace {
415 graph: GraphSpace,
416 zones: Vec<ZoneExtent>,
417 walkmaps: Vec<Option<ZoneWalkmap>>,
418 spatial_hashes: Vec<Option<ZoneSpatialHash>>,
419 agent_positions: HashMap<AgentId, HybridPos>,
421 agents_at_node: Vec<Vec<AgentId>>,
423}
424
425impl HybridSpace {
426 pub fn new(zone_extents: Vec<ZoneExtent>) -> Self {
431 let n = zone_extents.len();
432 Self {
433 graph: GraphSpace::new(n),
434 zones: zone_extents,
435 walkmaps: vec![None; n],
436 spatial_hashes: vec![None; n],
437 agent_positions: HashMap::new(),
438 agents_at_node: vec![Vec::new(); n],
439 }
440 }
441
442 pub fn uniform(n: usize, extent: ZoneExtent) -> Self {
444 Self::new(vec![extent; n])
445 }
446
447 pub fn num_nodes(&self) -> usize {
451 self.graph.num_vertices()
452 }
453
454 pub fn num_edges(&self) -> usize {
456 self.graph.num_edges()
457 }
458
459 pub fn add_edge(&mut self, a: GraphPos, b: GraphPos) -> bool {
463 self.graph.add_edge(a, b)
464 }
465
466 pub fn rem_edge(&mut self, a: GraphPos, b: GraphPos) -> bool {
470 self.graph.rem_edge(a, b)
471 }
472
473 pub fn neighbors(&self, node: GraphPos, kind: NeighborType) -> Vec<GraphPos> {
475 self.graph.neighbors(node, kind)
476 }
477
478 pub fn graph(&self) -> &GraphSpace {
480 &self.graph
481 }
482
483 pub fn zone_extent(&self, node: GraphPos) -> &ZoneExtent {
487 &self.zones[node]
488 }
489
490 pub fn set_zone_walkmap(
492 &mut self,
493 node: GraphPos,
494 walkmap: ZoneWalkmap,
495 ) -> Result<(), HybridSpaceError> {
496 if node >= self.num_nodes() {
497 return Err(HybridSpaceError::InvalidNode(node));
498 }
499 self.walkmaps[node] = Some(walkmap);
500 Ok(())
501 }
502
503 pub fn clear_zone_walkmap(&mut self, node: GraphPos) {
505 if node < self.num_nodes() {
506 self.walkmaps[node] = None;
507 }
508 }
509
510 pub fn zone_walkmap(&self, node: GraphPos) -> Option<&ZoneWalkmap> {
512 self.walkmaps.get(node).and_then(|w| w.as_ref())
513 }
514
515 pub fn is_walkable(&self, node: GraphPos, pos: &ContinuousPos3D) -> bool {
520 match self.walkmaps.get(node).and_then(|w| w.as_ref()) {
521 Some(wm) => wm.is_walkable_pos(pos, &self.zones[node]),
522 None => true,
523 }
524 }
525
526 pub fn enable_zone_spatial_hash(
528 &mut self,
529 node: GraphPos,
530 spacing: f64,
531 ) -> Result<(), HybridSpaceError> {
532 if node >= self.num_nodes() {
533 return Err(HybridSpaceError::InvalidNode(node));
534 }
535 let mut hash = ZoneSpatialHash::new(&self.zones[node], spacing)?;
536 for &id in &self.agents_at_node[node] {
537 if let Some(pos) = self.agent_positions.get(&id) {
538 hash.insert(id, &pos.offset);
539 }
540 }
541 self.spatial_hashes[node] = Some(hash);
542 Ok(())
543 }
544
545 pub fn disable_zone_spatial_hash(&mut self, node: GraphPos) {
547 if node < self.num_nodes() {
548 self.spatial_hashes[node] = None;
549 }
550 }
551
552 pub fn has_zone_spatial_hash(&self, node: GraphPos) -> bool {
554 self.spatial_hashes.get(node).is_some_and(|h| h.is_some())
555 }
556
557 pub fn add_node(&mut self, extent: ZoneExtent) -> GraphPos {
559 let idx = self.graph.add_vertex();
560 self.zones.push(extent);
561 self.walkmaps.push(None);
562 self.spatial_hashes.push(None);
563 self.agents_at_node.push(Vec::new());
564 idx
565 }
566
567 pub fn agent_position(&self, id: AgentId) -> Option<&HybridPos> {
571 self.agent_positions.get(&id)
572 }
573
574 pub fn agents_at_node(&self, node: GraphPos) -> &[AgentId] {
576 &self.agents_at_node[node]
577 }
578
579 pub fn add_agent_internal(
586 &mut self,
587 id: AgentId,
588 pos: HybridPos,
589 ) -> Result<(), HybridSpaceError> {
590 if pos.node >= self.num_nodes() {
591 return Err(HybridSpaceError::InvalidNode(pos.node));
592 }
593 let clamped = self.zones[pos.node].clamp(&pos.offset);
594 if !self.is_walkable(pos.node, &clamped) {
595 return Err(HybridSpaceError::Blocked);
596 }
597 let final_pos = HybridPos::new(pos.node, clamped);
598 self.agent_positions.insert(id, final_pos);
599 self.agents_at_node[pos.node].push(id);
600 if let Some(hash) = &mut self.spatial_hashes[pos.node] {
601 hash.insert(id, &clamped);
602 }
603 Ok(())
604 }
605
606 fn remove_agent_internal(&mut self, id: AgentId) {
607 if let Some(pos) = self.agent_positions.remove(&id) {
608 if let Some(i) = self.agents_at_node[pos.node]
609 .iter()
610 .position(|&aid| aid == id)
611 {
612 self.agents_at_node[pos.node].swap_remove(i);
613 }
614 if let Some(hash) = &mut self.spatial_hashes[pos.node] {
615 hash.remove(id, &pos.offset);
616 }
617 }
618 }
619
620 pub fn move_within_zone(
628 &mut self,
629 id: AgentId,
630 displacement: ContinuousPos3D,
631 ) -> Result<HybridPos, HybridSpaceError> {
632 let pos = self
633 .agent_positions
634 .get(&id)
635 .copied()
636 .ok_or(HybridSpaceError::OutOfBounds)?;
637
638 let new_offset = pos.offset + displacement;
639 let clamped = self.zones[pos.node].clamp(&new_offset);
640
641 if !self.is_walkable(pos.node, &clamped) {
642 return Err(HybridSpaceError::Blocked);
643 }
644
645 if let Some(hash) = &mut self.spatial_hashes[pos.node] {
646 hash.update(id, &pos.offset, &clamped);
647 }
648
649 let new_pos = HybridPos::new(pos.node, clamped);
650 self.agent_positions.insert(id, new_pos);
651 Ok(new_pos)
652 }
653
654 pub fn move_to_offset(
659 &mut self,
660 id: AgentId,
661 offset: ContinuousPos3D,
662 ) -> Result<HybridPos, HybridSpaceError> {
663 let pos = self
664 .agent_positions
665 .get(&id)
666 .copied()
667 .ok_or(HybridSpaceError::OutOfBounds)?;
668
669 let clamped = self.zones[pos.node].clamp(&offset);
670
671 if !self.is_walkable(pos.node, &clamped) {
672 return Err(HybridSpaceError::Blocked);
673 }
674
675 if let Some(hash) = &mut self.spatial_hashes[pos.node] {
676 hash.update(id, &pos.offset, &clamped);
677 }
678
679 let new_pos = HybridPos::new(pos.node, clamped);
680 self.agent_positions.insert(id, new_pos);
681 Ok(new_pos)
682 }
683
684 pub fn transition_to_zone(
691 &mut self,
692 id: AgentId,
693 to_node: GraphPos,
694 entry_offset: ContinuousPos3D,
695 ) -> Result<HybridPos, HybridSpaceError> {
696 let pos = self
697 .agent_positions
698 .get(&id)
699 .copied()
700 .ok_or(HybridSpaceError::OutOfBounds)?;
701
702 if to_node >= self.num_nodes() {
703 return Err(HybridSpaceError::InvalidNode(to_node));
704 }
705
706 let out_neighbors = self.graph.neighbors_out(pos.node);
708 if !out_neighbors.contains(&to_node) {
709 return Err(HybridSpaceError::NoEdge {
710 from: pos.node,
711 to: to_node,
712 });
713 }
714
715 let clamped = self.zones[to_node].clamp(&entry_offset);
716
717 if !self.is_walkable(to_node, &clamped) {
718 return Err(HybridSpaceError::Blocked);
719 }
720
721 if let Some(i) = self.agents_at_node[pos.node]
723 .iter()
724 .position(|&aid| aid == id)
725 {
726 self.agents_at_node[pos.node].swap_remove(i);
727 }
728 if let Some(hash) = &mut self.spatial_hashes[pos.node] {
729 hash.remove(id, &pos.offset);
730 }
731
732 let new_pos = HybridPos::new(to_node, clamped);
734 self.agent_positions.insert(id, new_pos);
735 self.agents_at_node[to_node].push(id);
736 if let Some(hash) = &mut self.spatial_hashes[to_node] {
737 hash.insert(id, &clamped);
738 }
739 Ok(new_pos)
740 }
741
742 pub fn transition_to_zone_center(
745 &mut self,
746 id: AgentId,
747 to_node: GraphPos,
748 ) -> Result<HybridPos, HybridSpaceError> {
749 let center = self
750 .zones
751 .get(to_node)
752 .ok_or(HybridSpaceError::InvalidNode(to_node))?
753 .center();
754 self.transition_to_zone(id, to_node, center)
755 }
756
757 pub fn nearby_ids(
771 &self,
772 pos: &HybridPos,
773 graph_radius: usize,
774 euclidean_radius: f64,
775 ) -> Vec<AgentId> {
776 let mut result = Vec::new();
777 let mut visited = HashSet::new();
778 let mut queue = VecDeque::new();
779
780 visited.insert(pos.node);
781 queue.push_back((pos.node, 0usize));
782
783 while let Some((node, depth)) = queue.pop_front() {
784 if node == pos.node {
785 self.collect_nearby_same_zone(pos, euclidean_radius, &mut result);
787 } else {
788 result.extend_from_slice(&self.agents_at_node[node]);
791 }
792
793 if depth < graph_radius {
795 for &neighbor in self.graph.neighbors_out(node) {
796 if visited.insert(neighbor) {
797 queue.push_back((neighbor, depth + 1));
798 }
799 }
800 }
801 }
802
803 result
804 }
805
806 pub fn nearby_ids_by_hops(&self, node: GraphPos, graph_radius: usize) -> Vec<AgentId> {
808 let mut result = Vec::new();
809 let mut visited = HashSet::new();
810 let mut queue = VecDeque::new();
811
812 visited.insert(node);
813 queue.push_back((node, 0usize));
814
815 while let Some((n, depth)) = queue.pop_front() {
816 result.extend_from_slice(&self.agents_at_node[n]);
817 if depth < graph_radius {
818 for &neighbor in self.graph.neighbors_out(n) {
819 if visited.insert(neighbor) {
820 queue.push_back((neighbor, depth + 1));
821 }
822 }
823 }
824 }
825
826 result
827 }
828
829 pub fn nearby_ids_same_zone(&self, pos: &HybridPos, radius: f64) -> Vec<AgentId> {
834 let mut result = Vec::new();
835 self.collect_nearby_same_zone(pos, radius, &mut result);
836 result
837 }
838
839 fn collect_nearby_same_zone(&self, pos: &HybridPos, radius: f64, result: &mut Vec<AgentId>) {
841 if let Some(hash) = self.spatial_hashes.get(pos.node).and_then(|h| h.as_ref()) {
842 let ids = hash.nearby_ids(&pos.offset, radius, &self.agent_positions);
844 result.extend(ids);
845 } else {
846 for &aid in &self.agents_at_node[pos.node] {
848 if let Some(agent_pos) = self.agent_positions.get(&aid) {
849 if pos.offset.distance_to(&agent_pos.offset) <= radius {
850 result.push(aid);
851 }
852 }
853 }
854 }
855 }
856}
857
858impl Space for HybridSpace {}
859
860impl<A> SpaceInteraction<A> for HybridSpace
861where
862 A: PositionedAgent<Position = HybridPos>,
863{
864 type Error = HybridSpaceError;
865
866 fn random_position<R: rand::RngCore>(&self, rng: &mut R) -> A::Position {
867 for _ in 0..100 {
869 let node = rng.gen_range(0..self.num_nodes());
870 let zone = &self.zones[node];
871 let offset = ContinuousPos3D::new(
872 rng.gen_range(0.0..zone.size_x),
873 rng.gen_range(0.0..zone.size_y),
874 rng.gen_range(0.0..zone.size_z),
875 );
876 if self.is_walkable(node, &offset) {
877 return HybridPos::new(node, offset);
878 }
879 }
880 let node = rng.gen_range(0..self.num_nodes());
882 let zone = &self.zones[node];
883 let offset = ContinuousPos3D::new(
884 rng.gen_range(0.0..zone.size_x),
885 rng.gen_range(0.0..zone.size_y),
886 rng.gen_range(0.0..zone.size_z),
887 );
888 HybridPos::new(node, offset)
889 }
890
891 fn add_agent(&mut self, agent: &A) -> Result<(), Self::Error> {
892 self.add_agent_internal(agent.id(), *agent.position())
893 }
894
895 fn remove_agent(&mut self, agent: &A) -> Result<(), Self::Error> {
896 self.remove_agent_internal(agent.id());
897 Ok(())
898 }
899
900 fn nearby_ids(&self, position: &A::Position, radius: usize) -> Vec<AgentId> {
901 self.nearby_ids_by_hops(position.node, radius)
904 }
905}