1use std::collections::{HashMap, HashSet};
7use std::f32;
8
9use super::tile_cache_data::{TileCacheBuilderConfig, TileCacheLayer};
10use super::tile_cache_integration::TileCacheNavMeshIntegration;
11use crate::error::TileCacheError;
12use waymark::nav_mesh::TileHeader;
13use waymark::{NavMesh, PolyRef};
14
15const MAX_LAYERS: usize = 32;
17
18#[derive(Debug)]
20#[cfg_attr(
21 feature = "serialization",
22 derive(serde::Serialize, serde::Deserialize)
23)]
24pub struct TileCache {
25 params: TileCacheParams,
27 tile_width: f32,
29 tile_height: f32,
31 tiles: Vec<Option<TileCacheEntry>>,
33 compressed_tiles: Vec<Vec<u8>>,
35 next_free: Option<usize>,
37 pos_lookup: HashMap<(i32, i32, i32), usize>,
39 obstacles: Vec<Option<Obstacle>>,
41 next_free_obstacle: Option<usize>,
43 salt: u32,
45 #[cfg_attr(feature = "serialization", serde(skip))]
47 nav_mesh_integration: Option<TileCacheNavMeshIntegration>,
48}
49
50#[derive(Debug, Clone)]
52#[non_exhaustive]
53#[cfg_attr(
54 feature = "serialization",
55 derive(serde::Serialize, serde::Deserialize)
56)]
57pub struct TileCacheParams {
58 pub origin: [f32; 3],
60 pub cs: f32,
62 pub ch: f32,
64 pub width: i32,
66 pub height: i32,
68 pub max_obstacles: i32,
70}
71
72impl Default for TileCacheParams {
73 fn default() -> Self {
74 Self {
75 origin: [0.0, 0.0, 0.0],
76 cs: 0.3,
77 ch: 0.2,
78 width: 0,
79 height: 0,
80 max_obstacles: 0,
81 }
82 }
83}
84
85#[derive(Debug, Clone)]
87#[cfg_attr(
88 feature = "serialization",
89 derive(serde::Serialize, serde::Deserialize)
90)]
91pub struct TileCacheEntry {
92 pub header: TileHeader,
94 pub data: usize,
96 pub obstacles: Vec<usize>,
98 pub next: Option<usize>,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq)]
104#[cfg_attr(
105 feature = "serialization",
106 derive(serde::Serialize, serde::Deserialize)
107)]
108pub enum ObstacleType {
109 Cylinder,
111 Box,
113 OrientedBox,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq)]
119#[cfg_attr(
120 feature = "serialization",
121 derive(serde::Serialize, serde::Deserialize)
122)]
123pub enum ObstacleState {
124 Empty,
126 Processing,
128 Processed,
130 Removing,
132}
133
134#[derive(Debug, Clone)]
136#[cfg_attr(
137 feature = "serialization",
138 derive(serde::Serialize, serde::Deserialize)
139)]
140pub enum ObstacleData {
141 Cylinder {
143 pos: [f32; 3],
145 radius: f32,
147 height: f32,
149 },
150 Box {
152 bmin: [f32; 3],
154 bmax: [f32; 3],
156 },
157 OrientedBox {
159 center: [f32; 3],
161 half_extents: [f32; 3],
163 rot_aux: [f32; 2],
165 },
166}
167
168#[derive(Debug, Clone)]
170#[cfg_attr(
171 feature = "serialization",
172 derive(serde::Serialize, serde::Deserialize)
173)]
174pub struct Obstacle {
175 pub data: ObstacleData,
177 pub state: ObstacleState,
179 pub salt: u16,
181 pub touched: Vec<u32>,
183 pub pending: Vec<u32>,
185 pub next: Option<usize>,
187}
188
189impl TileCache {
190 pub fn new(params: TileCacheParams) -> Result<Self, TileCacheError> {
192 if params.origin[0].is_infinite()
193 || params.origin[0].is_nan()
194 || params.origin[1].is_infinite()
195 || params.origin[1].is_nan()
196 || params.origin[2].is_infinite()
197 || params.origin[2].is_nan()
198 {
199 return Err(TileCacheError::InvalidParam);
200 }
201
202 if params.cs <= 0.0 || params.ch <= 0.0 {
203 return Err(TileCacheError::InvalidParam);
204 }
205
206 if params.width <= 0 || params.height <= 0 {
207 return Err(TileCacheError::InvalidParam);
208 }
209
210 let max_tiles = params.width * params.height * MAX_LAYERS as i32;
211
212 let mut tiles = Vec::with_capacity(max_tiles as usize);
213 for _i in 0..max_tiles as usize {
214 tiles.push(None);
215 }
216
217 let mut obstacles = Vec::with_capacity(params.max_obstacles as usize);
218 for _i in 0..params.max_obstacles as usize {
219 obstacles.push(None);
220 }
221
222 let next_free = Some(0);
223 let next_free_obstacle = Some(0);
224
225 for (i, tile) in tiles.iter_mut().enumerate().take(max_tiles as usize) {
226 *tile = Some(TileCacheEntry {
227 header: TileHeader::new(0, 0, 0),
228 data: 0,
229 obstacles: Vec::new(),
230 next: if i < max_tiles as usize - 1 {
231 Some(i + 1)
232 } else {
233 None
234 },
235 });
236 }
237
238 for (i, obstacle) in obstacles
239 .iter_mut()
240 .enumerate()
241 .take(params.max_obstacles as usize)
242 {
243 *obstacle = Some(Obstacle {
244 data: ObstacleData::Cylinder {
245 pos: [0.0, 0.0, 0.0],
246 radius: 0.0,
247 height: 0.0,
248 },
249 state: ObstacleState::Empty,
250 salt: 0,
251 touched: Vec::new(),
252 pending: Vec::new(),
253 next: if i < params.max_obstacles as usize - 1 {
254 Some(i + 1)
255 } else {
256 None
257 },
258 });
259 }
260
261 Ok(Self {
262 params,
263 tile_width: 0.0, tile_height: 0.0, tiles,
266 compressed_tiles: Vec::new(),
267 next_free,
268 pos_lookup: HashMap::new(),
269 obstacles,
270 next_free_obstacle,
271 salt: 1,
272 nav_mesh_integration: None,
273 })
274 }
275
276 pub fn init(&mut self) -> Result<(), TileCacheError> {
278 self.tile_width = self.params.cs * self.params.width as f32;
279 self.tile_height = self.params.cs * self.params.height as f32;
280
281 Ok(())
282 }
283
284 pub fn attach_to_nav_mesh(
286 &mut self,
287 builder_config: TileCacheBuilderConfig,
288 ) -> Result<(), TileCacheError> {
289 let integration = TileCacheNavMeshIntegration::new(builder_config);
290 self.nav_mesh_integration = Some(integration);
291
292 Ok(())
293 }
294
295 pub fn add_tile(
297 &mut self,
298 data: &[u8],
299 flags: u8,
300 result: &mut PolyRef,
301 ) -> Result<(), TileCacheError> {
302 let compressed_data = if (flags & 0x01) != 0 {
303 data.to_vec()
304 } else {
305 self.compress_tile(data)?
306 };
307
308 let free_idx = self
309 .next_free
310 .ok_or(TileCacheError::OutOfMemory { resource: "tiles" })?;
311 let mut tile_entry = self.tiles[free_idx]
312 .take()
313 .ok_or(TileCacheError::InvalidParam)?;
314 self.next_free = tile_entry.next;
315
316 let header = self.parse_tile_header(&compressed_data)?;
317 let header_key = (header.x(), header.y(), header.layer());
318
319 if self.pos_lookup.contains_key(&header_key) {
320 tile_entry.next = self.next_free;
321 self.next_free = Some(free_idx);
322 self.tiles[free_idx] = Some(tile_entry);
323 return Err(TileCacheError::InvalidParam);
324 }
325
326 let data_idx = self.compressed_tiles.len();
327 self.compressed_tiles.push(compressed_data);
328 tile_entry.data = data_idx;
329 tile_entry.header = header;
330
331 self.pos_lookup.insert(header_key, free_idx);
332 self.tiles[free_idx] = Some(tile_entry);
333
334 *result = PolyRef::new((self.salt << 16) | (free_idx as u32));
335 self.salt += 1;
336
337 Ok(())
338 }
339
340 pub fn remove_tile(&mut self, ref_val: PolyRef) -> Result<(), TileCacheError> {
342 let tile_idx = (ref_val.id() & 0xFFFF) as usize;
343
344 if tile_idx >= self.tiles.len() {
345 return Err(TileCacheError::InvalidParam);
346 }
347
348 let mut tile_entry = self.tiles[tile_idx]
349 .take()
350 .ok_or(TileCacheError::InvalidParam)?;
351
352 self.pos_lookup.remove(&(
353 tile_entry.header.x(),
354 tile_entry.header.y(),
355 tile_entry.header.layer(),
356 ));
357
358 tile_entry.obstacles.clear();
359 tile_entry.next = self.next_free;
360 self.next_free = Some(tile_idx);
361
362 self.tiles[tile_idx] = Some(tile_entry);
363
364 Ok(())
365 }
366
367 pub fn add_obstacle(
369 &mut self,
370 pos: [f32; 3],
371 radius: f32,
372 height: f32,
373 ) -> Result<u32, TileCacheError> {
374 let free_idx = self.next_free_obstacle.ok_or(TileCacheError::OutOfMemory {
375 resource: "obstacles",
376 })?;
377 let mut obstacle = self.obstacles[free_idx]
378 .take()
379 .ok_or(TileCacheError::InvalidParam)?;
380 self.next_free_obstacle = obstacle.next;
381
382 obstacle.data = ObstacleData::Cylinder {
383 pos,
384 radius,
385 height,
386 };
387 obstacle.state = ObstacleState::Processing;
388 obstacle.salt += 1;
389 obstacle.touched.clear();
390 obstacle.pending.clear();
391
392 let affected_tiles = self.find_tiles_affected_by_obstacle(pos, radius)?;
393 for &tile_idx in &affected_tiles {
394 obstacle.touched.push(tile_idx as u32);
395 obstacle.pending.push(tile_idx as u32);
396 }
397
398 let salt = obstacle.salt;
399 self.obstacles[free_idx] = Some(obstacle);
400
401 Ok(self.encode_obstacle_ref(salt, free_idx))
402 }
403
404 pub fn add_box_obstacle(
406 &mut self,
407 bmin: [f32; 3],
408 bmax: [f32; 3],
409 ) -> Result<u32, TileCacheError> {
410 let free_idx = self.next_free_obstacle.ok_or(TileCacheError::OutOfMemory {
411 resource: "obstacles",
412 })?;
413 let mut obstacle = self.obstacles[free_idx]
414 .take()
415 .ok_or(TileCacheError::InvalidParam)?;
416 self.next_free_obstacle = obstacle.next;
417
418 obstacle.data = ObstacleData::Box { bmin, bmax };
419 obstacle.state = ObstacleState::Processing;
420 obstacle.salt += 1;
421 obstacle.touched.clear();
422 obstacle.pending.clear();
423
424 let center = [
425 (bmin[0] + bmax[0]) * 0.5,
426 (bmin[1] + bmax[1]) * 0.5,
427 (bmin[2] + bmax[2]) * 0.5,
428 ];
429 let radius = ((bmax[0] - bmin[0]).powi(2) + (bmax[2] - bmin[2]).powi(2)).sqrt() * 0.5;
430
431 let affected_tiles = self.find_tiles_affected_by_obstacle(center, radius)?;
432 for &tile_idx in &affected_tiles {
433 obstacle.touched.push(tile_idx as u32);
434 obstacle.pending.push(tile_idx as u32);
435 }
436
437 let salt = obstacle.salt;
438 self.obstacles[free_idx] = Some(obstacle);
439
440 Ok(self.encode_obstacle_ref(salt, free_idx))
441 }
442
443 pub fn add_oriented_box_obstacle(
445 &mut self,
446 center: [f32; 3],
447 half_extents: [f32; 3],
448 y_radians: f32,
449 ) -> Result<u32, TileCacheError> {
450 let free_idx = self.next_free_obstacle.ok_or(TileCacheError::OutOfMemory {
451 resource: "obstacles",
452 })?;
453 let mut obstacle = self.obstacles[free_idx]
454 .take()
455 .ok_or(TileCacheError::InvalidParam)?;
456 self.next_free_obstacle = obstacle.next;
457
458 let angle = y_radians;
459 let cos_half = (angle * 0.5).cos();
460 let sin_half = (angle * 0.5).sin();
461 let rot_aux = [cos_half * -sin_half, cos_half * cos_half - 0.5];
462
463 obstacle.data = ObstacleData::OrientedBox {
464 center,
465 half_extents,
466 rot_aux,
467 };
468 obstacle.state = ObstacleState::Processing;
469 obstacle.salt += 1;
470 obstacle.touched.clear();
471 obstacle.pending.clear();
472
473 let radius = (half_extents[0].powi(2) + half_extents[2].powi(2)).sqrt();
475
476 let affected_tiles = self.find_tiles_affected_by_obstacle(center, radius)?;
477 for &tile_idx in &affected_tiles {
478 obstacle.touched.push(tile_idx as u32);
479 obstacle.pending.push(tile_idx as u32);
480 }
481
482 let salt = obstacle.salt;
483 self.obstacles[free_idx] = Some(obstacle);
484
485 Ok(self.encode_obstacle_ref(salt, free_idx))
486 }
487
488 pub fn remove_obstacle(&mut self, obstacle_ref: u32) -> Result<(), TileCacheError> {
490 let obstacle_idx = self.decode_obstacle_ref_idx(obstacle_ref);
491 let salt = self.decode_obstacle_ref_salt(obstacle_ref);
492
493 if obstacle_idx >= self.obstacles.len() {
494 return Err(TileCacheError::InvalidParam);
495 }
496
497 let stored_salt = self.obstacles[obstacle_idx]
498 .as_ref()
499 .ok_or(TileCacheError::ObstacleNotFound)?
500 .salt;
501 if stored_salt != salt {
502 return Err(TileCacheError::InvalidParam);
503 }
504
505 let mut obstacle = self.obstacles[obstacle_idx]
506 .take()
507 .ok_or(TileCacheError::ObstacleNotFound)?;
508
509 obstacle.state = ObstacleState::Removing;
510 obstacle.pending = obstacle.touched.clone();
511
512 self.obstacles[obstacle_idx] = Some(obstacle);
514
515 Ok(())
516 }
517
518 pub fn update(&mut self) -> Result<(), TileCacheError> {
520 self.update_with_status(0.0).map(|_| ())
521 }
522
523 pub fn update_with_status(&mut self, _dt: f32) -> Result<bool, TileCacheError> {
525 let mut tiles_to_update = HashSet::new();
526 let mut has_pending_work = false;
527
528 for obstacle_idx in 0..self.obstacles.len() {
529 if let Some(obstacle) = &mut self.obstacles[obstacle_idx] {
530 match obstacle.state {
531 ObstacleState::Processing => {
532 has_pending_work = true;
533 for &tile_idx in &obstacle.pending {
534 tiles_to_update.insert(tile_idx as usize);
535 }
536 obstacle.pending.clear();
537 obstacle.state = ObstacleState::Processed;
538 }
539 ObstacleState::Removing => {
540 has_pending_work = true;
541 for &tile_idx in &obstacle.pending {
542 tiles_to_update.insert(tile_idx as usize);
543 }
544 obstacle.state = ObstacleState::Empty;
545 obstacle.touched.clear();
546 obstacle.pending.clear();
547 }
548 _ => {}
549 }
550 }
551 }
552
553 for obstacle_idx in 0..self.obstacles.len() {
554 let is_empty = self.obstacles[obstacle_idx]
555 .as_ref()
556 .is_some_and(|o| o.state == ObstacleState::Empty);
557 if is_empty {
558 if let Some(mut obstacle) = self.obstacles[obstacle_idx].take() {
559 obstacle.next = self.next_free_obstacle;
560 self.next_free_obstacle = Some(obstacle_idx);
561 self.obstacles[obstacle_idx] = Some(obstacle);
562 }
563 }
564 }
565
566 for tile_idx in tiles_to_update {
567 self.rebuild_tile(tile_idx)?;
568 }
569
570 Ok(!has_pending_work)
571 }
572
573 pub fn update_with_nav_mesh(
575 &mut self,
576 _dt: f32,
577 nav_mesh: &mut NavMesh,
578 ) -> Result<bool, TileCacheError> {
579 let mut tiles_to_update = HashSet::new();
580 let mut has_pending_work = false;
581
582 for obstacle_idx in 0..self.obstacles.len() {
583 if let Some(obstacle) = &mut self.obstacles[obstacle_idx] {
584 match obstacle.state {
585 ObstacleState::Processing => {
586 has_pending_work = true;
587 for &tile_idx in &obstacle.pending {
588 tiles_to_update.insert(tile_idx as usize);
589 }
590 obstacle.pending.clear();
591 obstacle.state = ObstacleState::Processed;
592 }
593 ObstacleState::Removing => {
594 has_pending_work = true;
595 for &tile_idx in &obstacle.pending {
596 tiles_to_update.insert(tile_idx as usize);
597 }
598 obstacle.state = ObstacleState::Empty;
599 obstacle.touched.clear();
600 obstacle.pending.clear();
601 }
602 _ => {}
603 }
604 }
605 }
606
607 for obstacle_idx in 0..self.obstacles.len() {
608 let is_empty = self.obstacles[obstacle_idx]
609 .as_ref()
610 .is_some_and(|o| o.state == ObstacleState::Empty);
611 if is_empty {
612 if let Some(mut obstacle) = self.obstacles[obstacle_idx].take() {
613 obstacle.next = self.next_free_obstacle;
614 self.next_free_obstacle = Some(obstacle_idx);
615 self.obstacles[obstacle_idx] = Some(obstacle);
616 }
617 }
618 }
619
620 for tile_idx in tiles_to_update {
621 if let Some(Some(_tile_entry)) = self.tiles.get(tile_idx) {
622 let tile_ref = PolyRef::new((self.salt << 16) | (tile_idx as u32));
623 self.build_nav_mesh_tile(tile_ref, nav_mesh)?;
624 }
625 }
626
627 Ok(!has_pending_work)
628 }
629
630 pub fn build_nav_mesh_tiles_at(
632 &mut self,
633 tx: i32,
634 ty: i32,
635 nav_mesh: &mut NavMesh,
636 ) -> Result<(), TileCacheError> {
637 let tiles = self.get_tiles_at(tx, ty)?;
638
639 for tile_ref in tiles {
640 self.build_nav_mesh_tile(tile_ref, nav_mesh)?;
641 }
642
643 Ok(())
644 }
645
646 pub fn build_nav_mesh_tile(
648 &mut self,
649 tile_ref: PolyRef,
650 nav_mesh: &mut NavMesh,
651 ) -> Result<(), TileCacheError> {
652 let integration = self
653 .nav_mesh_integration
654 .as_ref()
655 .ok_or(TileCacheError::InvalidParam)?;
656
657 let tile_idx = (tile_ref.id() & 0xFFFF) as usize;
658 let _salt = tile_ref.id() >> 16;
659
660 if tile_idx >= self.tiles.len() {
661 return Err(TileCacheError::InvalidParam);
662 }
663
664 let tile_entry = match &self.tiles[tile_idx] {
668 Some(entry) => entry,
669 None => return Err(TileCacheError::InvalidParam),
670 };
671
672 let compressed_data = match self.compressed_tiles.get(tile_entry.data) {
673 Some(data) => data,
674 None => return Err(TileCacheError::InvalidParam),
675 };
676
677 let decompressed_data = self.decompress_tile(compressed_data, None)?;
678 let tile_layer = TileCacheLayer::from_bytes(&decompressed_data)?;
679
680 integration.build_nav_mesh_tile_from_layer(self, nav_mesh, &tile_layer, tile_idx)?;
681
682 Ok(())
683 }
684
685 #[allow(dead_code)]
687 fn find_tile_index(&self, x: i32, y: i32, layer: i32) -> Option<usize> {
688 self.pos_lookup.get(&(x, y, layer)).copied()
689 }
690
691 pub fn rebuild_tile_in_nav_mesh(
693 &mut self,
694 nav_mesh: &mut NavMesh,
695 tile_idx: usize,
696 ) -> Result<Option<PolyRef>, TileCacheError> {
697 if tile_idx >= self.tiles.len() {
698 return Err(TileCacheError::InvalidParam);
699 }
700
701 if self.tiles[tile_idx].is_none() {
702 return Err(TileCacheError::InvalidParam);
703 }
704
705 let integration = self
706 .nav_mesh_integration
707 .as_ref()
708 .ok_or(TileCacheError::InvalidParam)?;
709
710 integration.rebuild_tile_in_nav_mesh(self, nav_mesh, tile_idx)
711 }
712
713 fn rebuild_tile(&mut self, tile_idx: usize) -> Result<(), TileCacheError> {
715 if tile_idx >= self.tiles.len() {
716 return Err(TileCacheError::InvalidParam);
717 }
718
719 if self.tiles[tile_idx].is_none() {
720 return Err(TileCacheError::InvalidParam);
721 }
722
723 let tile_entry = match &self.tiles[tile_idx] {
724 Some(entry) => entry,
725 None => return Err(TileCacheError::InvalidParam),
726 };
727
728 let compressed_data = &self.compressed_tiles[tile_entry.data];
729
730 log::debug!(
733 "Rebuilding tile {} with {} bytes of data",
734 tile_idx,
735 compressed_data.len()
736 );
737
738 Ok(())
739 }
740
741 pub fn get_tile(&self, tile_idx: usize) -> Option<&TileCacheEntry> {
743 if tile_idx >= self.tiles.len() {
744 return None;
745 }
746
747 match &self.tiles[tile_idx] {
748 Some(entry) => Some(entry),
749 None => None,
750 }
751 }
752
753 pub fn get_tile_at(&self, x: i32, y: i32, layer: i32) -> Option<&TileCacheEntry> {
755 if let Some(&tile_idx) = self.pos_lookup.get(&(x, y, layer)) {
756 self.get_tile(tile_idx)
757 } else {
758 None
759 }
760 }
761
762 pub fn get_obstacle(&self, obstacle_idx: usize) -> Option<&Obstacle> {
764 if obstacle_idx >= self.obstacles.len() {
765 return None;
766 }
767
768 match &self.obstacles[obstacle_idx] {
769 Some(obstacle) => Some(obstacle),
770 None => None,
771 }
772 }
773
774 pub fn get_obstacle_count(&self) -> usize {
776 self.obstacles
777 .iter()
778 .filter_map(|obs| obs.as_ref())
779 .filter(|obs| obs.state != ObstacleState::Empty)
780 .count()
781 }
782
783 pub fn get_params(&self) -> &TileCacheParams {
785 &self.params
786 }
787
788 pub fn get_tile_compressed_data(&self, tile_idx: usize) -> Option<&[u8]> {
790 if let Some(Some(tile)) = self.tiles.get(tile_idx) {
791 self.compressed_tiles.get(tile.data).map(|v| v.as_slice())
792 } else {
793 None
794 }
795 }
796
797 pub fn get_tile_data(&self, tile_idx: usize) -> Result<Vec<u8>, TileCacheError> {
799 if let Some(compressed_data) = self.get_tile_compressed_data(tile_idx) {
800 self.decompress_tile(compressed_data, None)
801 } else {
802 Err(TileCacheError::InvalidParam)
803 }
804 }
805
806 pub fn compress_tile(&self, data: &[u8]) -> Result<Vec<u8>, TileCacheError> {
808 Ok(lz4_flex::compress_prepend_size(data))
809 }
810
811 pub fn decompress_tile(
813 &self,
814 compressed_data: &[u8],
815 _uncompressed_size: Option<usize>,
816 ) -> Result<Vec<u8>, TileCacheError> {
817 if compressed_data.is_empty() {
818 return Ok(Vec::new());
819 }
820
821 match lz4_flex::decompress_size_prepended(compressed_data) {
822 Ok(decompressed) => Ok(decompressed),
823 Err(e) => {
824 log::error!("LZ4 decompression failed: {:?}", e);
825 Err(TileCacheError::InvalidParam)
826 }
827 }
828 }
829
830 fn parse_tile_header(&self, compressed_data: &[u8]) -> Result<TileHeader, TileCacheError> {
832 let decompressed = self.decompress_tile(compressed_data, None)?;
833 let tile_layer = TileCacheLayer::from_bytes(&decompressed)?;
834
835 let mut header = TileHeader::new(
836 tile_layer.header.tx,
837 tile_layer.header.ty,
838 tile_layer.header.tlayer,
839 );
840 header.set_data_size(compressed_data.len()); Ok(header)
843 }
844
845 fn find_tiles_affected_by_obstacle(
847 &self,
848 pos: [f32; 3],
849 radius: f32,
850 ) -> Result<Vec<usize>, TileCacheError> {
851 let mut affected_tiles = Vec::new();
852
853 let min_x = ((pos[0] - radius - self.params.origin[0]) / self.tile_width).floor() as i32;
854 let max_x = ((pos[0] + radius - self.params.origin[0]) / self.tile_width).floor() as i32;
855 let min_z = ((pos[2] - radius - self.params.origin[2]) / self.tile_height).floor() as i32;
856 let max_z = ((pos[2] + radius - self.params.origin[2]) / self.tile_height).floor() as i32;
857
858 for z in min_z..=max_z {
859 for x in min_x..=max_x {
860 for layer in 0..MAX_LAYERS as i32 {
861 if let Some(&tile_idx) = self.pos_lookup.get(&(x, z, layer)) {
862 affected_tiles.push(tile_idx);
863 }
864 }
865 }
866 }
867
868 Ok(affected_tiles)
869 }
870
871 pub fn get_salt(&self) -> u32 {
873 self.salt
874 }
875
876 pub fn encode_obstacle_ref(&self, salt: u16, idx: usize) -> u32 {
878 ((salt as u32) << 16) | (idx as u32)
879 }
880
881 pub fn decode_obstacle_ref_salt(&self, ref_val: u32) -> u16 {
883 ((ref_val >> 16) & 0xFFFF) as u16
884 }
885
886 pub fn decode_obstacle_ref_idx(&self, ref_val: u32) -> usize {
888 (ref_val & 0xFFFF) as usize
889 }
890
891 pub fn get_obstacle_by_ref(&self, ref_val: u32) -> Option<&Obstacle> {
893 let idx = self.decode_obstacle_ref_idx(ref_val);
894 let salt = self.decode_obstacle_ref_salt(ref_val);
895
896 if let Some(obstacle) = self.get_obstacle(idx) {
897 if obstacle.salt == salt {
898 return Some(obstacle);
899 }
900 }
901 None
902 }
903
904 pub fn get_obstacle_ref(&self, obstacle_idx: usize) -> Option<u32> {
906 self.get_obstacle(obstacle_idx)
907 .map(|obstacle| self.encode_obstacle_ref(obstacle.salt, obstacle_idx))
908 }
909
910 pub fn query_tiles(
912 &self,
913 bmin: &[f32; 3],
914 bmax: &[f32; 3],
915 max_tiles: usize,
916 ) -> Result<Vec<PolyRef>, TileCacheError> {
917 let mut results = Vec::new();
918
919 let tw = self.params.width as f32 * self.params.cs;
920 let th = self.params.height as f32 * self.params.cs;
921
922 let tx0 = ((bmin[0] - self.params.origin[0]) / tw).floor() as i32;
923 let tx1 = ((bmax[0] - self.params.origin[0]) / tw).floor() as i32;
924 let ty0 = ((bmin[2] - self.params.origin[2]) / th).floor() as i32;
925 let ty1 = ((bmax[2] - self.params.origin[2]) / th).floor() as i32;
926
927 for ty in ty0..=ty1 {
928 for tx in tx0..=tx1 {
929 let tiles = self.get_tiles_at(tx, ty)?;
930
931 for &tile_ref in &tiles {
932 if results.len() >= max_tiles {
933 break;
934 }
935
936 let tile_idx = (tile_ref.id() & 0xFFFF) as usize;
937 let salt = tile_ref.id() >> 16;
938
939 if let Some(Some(_tile)) = self.tiles.get(tile_idx) {
940 if salt != self.salt {
941 continue;
942 }
943
944 if let Some(compressed_data) = self.get_tile_compressed_data(tile_idx) {
945 let decompressed = self.decompress_tile(compressed_data, None)?;
946 if let Ok(tile_layer) = TileCacheLayer::from_bytes(&decompressed) {
947 let (tbmin, tbmax) =
948 self.calc_tight_tile_bounds(&tile_layer.header);
949
950 if Self::overlap_bounds(bmin, bmax, &tbmin, &tbmax) {
951 results.push(tile_ref);
952 }
953 }
954 }
955 }
956 }
957 }
958 }
959
960 Ok(results)
961 }
962
963 pub fn get_tiles_at(&self, tx: i32, ty: i32) -> Result<Vec<PolyRef>, TileCacheError> {
965 let mut tiles = Vec::new();
966
967 for layer in 0..MAX_LAYERS as i32 {
968 if let Some(&tile_idx) = self.pos_lookup.get(&(tx, ty, layer)) {
969 if let Some(Some(_tile)) = self.tiles.get(tile_idx) {
970 let tile_ref = PolyRef::new((self.salt << 16) | (tile_idx as u32));
971 tiles.push(tile_ref);
972 }
973 }
974 }
975
976 Ok(tiles)
977 }
978
979 pub fn calc_tight_tile_bounds(
981 &self,
982 header: &crate::tile_cache_data::TileCacheLayerHeader,
983 ) -> ([f32; 3], [f32; 3]) {
984 let cs = self.params.cs;
985
986 let bmin = [
987 header.bmin[0] + header.minx as f32 * cs,
988 header.bmin[1],
989 header.bmin[2] + header.miny as f32 * cs,
990 ];
991
992 let bmax = [
993 header.bmin[0] + (header.maxx + 1) as f32 * cs,
994 header.bmax[1],
995 header.bmin[2] + (header.maxy + 1) as f32 * cs,
996 ];
997
998 (bmin, bmax)
999 }
1000
1001 pub fn get_obstacle_bounds(&self, obstacle: &Obstacle) -> ([f32; 3], [f32; 3]) {
1003 match &obstacle.data {
1004 ObstacleData::Cylinder {
1005 pos,
1006 radius,
1007 height,
1008 } => {
1009 let bmin = [pos[0] - radius, pos[1], pos[2] - radius];
1010 let bmax = [pos[0] + radius, pos[1] + height, pos[2] + radius];
1011 (bmin, bmax)
1012 }
1013 ObstacleData::Box { bmin, bmax } => (*bmin, *bmax),
1014 ObstacleData::OrientedBox {
1015 center,
1016 half_extents,
1017 ..
1018 } => {
1019 let max_r = 1.41 * half_extents[0].max(half_extents[2]);
1021 let bmin = [
1022 center[0] - max_r,
1023 center[1] - half_extents[1],
1024 center[2] - max_r,
1025 ];
1026 let bmax = [
1027 center[0] + max_r,
1028 center[1] + half_extents[1],
1029 center[2] + max_r,
1030 ];
1031 (bmin, bmax)
1032 }
1033 }
1034 }
1035
1036 fn overlap_bounds(amin: &[f32; 3], amax: &[f32; 3], bmin: &[f32; 3], bmax: &[f32; 3]) -> bool {
1038 !(amin[0] > bmax[0]
1039 || amax[0] < bmin[0]
1040 || amin[1] > bmax[1]
1041 || amax[1] < bmin[1]
1042 || amin[2] > bmax[2]
1043 || amax[2] < bmin[2])
1044 }
1045
1046 #[cfg(feature = "serialization")]
1048 pub fn save_to_json<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), TileCacheError> {
1049 let json = serde_json::to_string_pretty(self)
1050 .map_err(|e| TileCacheError::Serialization(Box::new(e)))?;
1051
1052 std::fs::write(path, json).map_err(TileCacheError::Io)?;
1053
1054 Ok(())
1055 }
1056
1057 #[cfg(feature = "serialization")]
1059 pub fn load_from_json<P: AsRef<std::path::Path>>(path: P) -> Result<Self, TileCacheError> {
1060 let json = std::fs::read_to_string(path).map_err(TileCacheError::Io)?;
1061
1062 let tile_cache =
1063 serde_json::from_str(&json).map_err(|e| TileCacheError::Serialization(Box::new(e)))?;
1064
1065 Ok(tile_cache)
1066 }
1067
1068 #[cfg(feature = "serialization")]
1070 pub fn save_to_binary<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), TileCacheError> {
1071 let encoded =
1072 postcard::to_allocvec(self).map_err(|e| TileCacheError::Serialization(Box::new(e)))?;
1073
1074 std::fs::write(path, encoded).map_err(TileCacheError::Io)?;
1075
1076 Ok(())
1077 }
1078
1079 #[cfg(feature = "serialization")]
1081 pub fn load_from_binary<P: AsRef<std::path::Path>>(path: P) -> Result<Self, TileCacheError> {
1082 let data = std::fs::read(path).map_err(TileCacheError::Io)?;
1083
1084 let tile_cache =
1085 postcard::from_bytes(&data).map_err(|e| TileCacheError::Serialization(Box::new(e)))?;
1086
1087 Ok(tile_cache)
1088 }
1089
1090 #[cfg(feature = "serialization")]
1092 pub fn to_json_bytes(&self) -> Result<Vec<u8>, TileCacheError> {
1093 let json =
1094 serde_json::to_vec(self).map_err(|e| TileCacheError::Serialization(Box::new(e)))?;
1095 Ok(json)
1096 }
1097
1098 #[cfg(feature = "serialization")]
1100 pub fn from_json_bytes(data: &[u8]) -> Result<Self, TileCacheError> {
1101 let tile_cache =
1102 serde_json::from_slice(data).map_err(|e| TileCacheError::Serialization(Box::new(e)))?;
1103 Ok(tile_cache)
1104 }
1105
1106 #[cfg(feature = "serialization")]
1108 pub fn to_binary_bytes(&self) -> Result<Vec<u8>, TileCacheError> {
1109 let data =
1110 postcard::to_allocvec(self).map_err(|e| TileCacheError::Serialization(Box::new(e)))?;
1111 Ok(data)
1112 }
1113
1114 #[cfg(feature = "serialization")]
1116 pub fn from_binary_bytes(data: &[u8]) -> Result<Self, TileCacheError> {
1117 let tile_cache =
1118 postcard::from_bytes(data).map_err(|e| TileCacheError::Serialization(Box::new(e)))?;
1119 Ok(tile_cache)
1120 }
1121}
1122
1123#[cfg(test)]
1124mod tests {
1125 use super::*;
1126 use crate::tile_cache_data::TileCacheLayerHeader;
1127
1128 #[test]
1129 fn test_create_tile_cache() {
1130 let params = TileCacheParams {
1131 origin: [0.0, 0.0, 0.0],
1132 cs: 0.3,
1133 ch: 0.2,
1134 width: 100,
1135 height: 100,
1136 max_obstacles: 1024,
1137 };
1138
1139 let result = TileCache::new(params.clone());
1140
1141 assert!(result.is_ok());
1142
1143 let tile_cache = result.unwrap();
1144
1145 assert_eq!(tile_cache.get_params().width, params.width);
1146 assert_eq!(tile_cache.get_params().height, params.height);
1147 assert_eq!(tile_cache.get_params().cs, params.cs);
1148 assert_eq!(tile_cache.get_params().ch, params.ch);
1149 assert_eq!(tile_cache.get_params().max_obstacles, params.max_obstacles);
1150 assert_eq!(tile_cache.get_obstacle_count(), 0);
1151 }
1152
1153 #[test]
1154 fn test_invalid_params() {
1155 let params = TileCacheParams {
1157 origin: [f32::INFINITY, 0.0, 0.0], cs: 0.3,
1159 ch: 0.2,
1160 width: 100,
1161 height: 100,
1162 max_obstacles: 1024,
1163 };
1164
1165 let result = TileCache::new(params);
1166 assert!(result.is_err());
1167
1168 let params = TileCacheParams {
1170 origin: [0.0, 0.0, 0.0],
1171 cs: -0.3, ch: 0.2,
1173 width: 100,
1174 height: 100,
1175 max_obstacles: 1024,
1176 };
1177
1178 let result = TileCache::new(params);
1179 assert!(result.is_err());
1180
1181 let params = TileCacheParams {
1183 origin: [0.0, 0.0, 0.0],
1184 cs: 0.3,
1185 ch: 0.2,
1186 width: 0, height: 100,
1188 max_obstacles: 1024,
1189 };
1190
1191 let result = TileCache::new(params);
1192 assert!(result.is_err());
1193 }
1194
1195 #[test]
1196 fn test_obstacle_management() {
1197 let params = TileCacheParams {
1198 origin: [0.0, 0.0, 0.0],
1199 cs: 0.3,
1200 ch: 0.2,
1201 width: 10,
1202 height: 10,
1203 max_obstacles: 64,
1204 };
1205
1206 let mut tile_cache = TileCache::new(params).unwrap();
1207 tile_cache.init().unwrap();
1208
1209 let obstacle_ref = tile_cache.add_obstacle([5.0, 1.0, 5.0], 2.0, 3.0).unwrap();
1211 assert_eq!(tile_cache.get_obstacle_count(), 1);
1212
1213 let obstacle = tile_cache.get_obstacle_by_ref(obstacle_ref).unwrap();
1215 match &obstacle.data {
1216 ObstacleData::Cylinder {
1217 pos,
1218 radius,
1219 height,
1220 } => {
1221 assert_eq!(*pos, [5.0, 1.0, 5.0]);
1222 assert_eq!(*radius, 2.0);
1223 assert_eq!(*height, 3.0);
1224 }
1225 _ => panic!("Expected cylinder obstacle"),
1226 }
1227 assert_eq!(obstacle.state, ObstacleState::Processing);
1228
1229 tile_cache.remove_obstacle(obstacle_ref).unwrap();
1231 assert_eq!(tile_cache.get_obstacle_count(), 1);
1233
1234 tile_cache.update().unwrap();
1236 assert_eq!(tile_cache.get_obstacle_count(), 0);
1237 }
1238
1239 #[test]
1240 #[cfg(feature = "serialization")]
1241 fn test_tile_cache_serialization() {
1242 use tempfile::NamedTempFile;
1243
1244 let params = TileCacheParams {
1245 origin: [1.0, 2.0, 3.0],
1246 cs: 0.5,
1247 ch: 0.3,
1248 width: 20,
1249 height: 20,
1250 max_obstacles: 128,
1251 };
1252
1253 let mut tile_cache = TileCache::new(params.clone()).unwrap();
1254 tile_cache.init().unwrap();
1255
1256 let _obstacle_ref = tile_cache
1258 .add_obstacle([10.0, 2.0, 10.0], 1.5, 2.0)
1259 .unwrap();
1260
1261 let json_bytes = tile_cache.to_json_bytes().unwrap();
1263 let restored_from_json = TileCache::from_json_bytes(&json_bytes).unwrap();
1264 assert_eq!(restored_from_json.get_params().cs, params.cs);
1265 assert_eq!(restored_from_json.get_params().width, params.width);
1266
1267 let binary_bytes = tile_cache.to_binary_bytes().unwrap();
1269 let restored_from_binary = TileCache::from_binary_bytes(&binary_bytes).unwrap();
1270 assert_eq!(restored_from_binary.get_params().cs, params.cs);
1271 assert_eq!(restored_from_binary.get_params().height, params.height);
1272
1273 let json_file = NamedTempFile::new().unwrap();
1275 tile_cache.save_to_json(json_file.path()).unwrap();
1276 let restored_from_json_file = TileCache::load_from_json(json_file.path()).unwrap();
1277 assert_eq!(
1278 restored_from_json_file.get_params().max_obstacles,
1279 params.max_obstacles
1280 );
1281
1282 let binary_file = NamedTempFile::new().unwrap();
1283 tile_cache.save_to_binary(binary_file.path()).unwrap();
1284 let restored_from_binary_file = TileCache::load_from_binary(binary_file.path()).unwrap();
1285 assert_eq!(restored_from_binary_file.get_params().origin, params.origin);
1286 }
1287
1288 #[test]
1289 fn test_tile_management() {
1290 let params = TileCacheParams {
1291 origin: [0.0, 0.0, 0.0],
1292 cs: 1.0,
1293 ch: 0.5,
1294 width: 5,
1295 height: 5,
1296 max_obstacles: 32,
1297 };
1298
1299 let mut tile_cache = TileCache::new(params).unwrap();
1300 tile_cache.init().unwrap();
1301
1302 let mut header = TileCacheLayerHeader::new();
1304 header.tx = 1;
1305 header.ty = 2;
1306 header.tlayer = 0;
1307 header.bmin = [0.0, 0.0, 0.0];
1308 header.bmax = [5.0, 5.0, 5.0];
1309 header.width = 5;
1310 header.height = 5;
1311
1312 let layer = TileCacheLayer::new(header);
1313 let tile_data = layer.to_bytes();
1314
1315 let mut tile_ref = PolyRef::new(0);
1317 tile_cache.add_tile(&tile_data, 0, &mut tile_ref).unwrap();
1318 assert!(tile_ref.is_valid());
1319
1320 let salt = tile_cache.get_salt();
1322 assert!(salt > 1);
1323
1324 let tile = tile_cache.get_tile_at(1, 2, 0);
1326 assert!(tile.is_some());
1327
1328 tile_cache.remove_tile(tile_ref).unwrap();
1330 let tile_after_removal = tile_cache.get_tile_at(1, 2, 0);
1331 assert!(tile_after_removal.is_none());
1332 }
1333
1334 #[test]
1335 fn test_update_process() {
1336 let params = TileCacheParams {
1337 origin: [0.0, 0.0, 0.0],
1338 cs: 1.0,
1339 ch: 0.5,
1340 width: 10,
1341 height: 10,
1342 max_obstacles: 64,
1343 };
1344
1345 let mut tile_cache = TileCache::new(params).unwrap();
1346 tile_cache.init().unwrap();
1347
1348 let _obstacle_idx = tile_cache.add_obstacle([5.0, 1.0, 5.0], 2.0, 3.0).unwrap();
1350
1351 tile_cache.update().unwrap();
1353
1354 }
1358}