Skip to main content

waymark_tilecache/
tile_cache_builder.rs

1//! Tile cache builder for real-time mesh rebuilding
2//!
3//! This module provides functionality to rebuild navigation mesh tiles
4//! from compressed tile cache data, with support for dynamic obstacles.
5
6use super::tile_cache::{Obstacle, ObstacleData, ObstacleState, TileCache};
7use super::tile_cache_data::{TileCacheBuilderConfig, TileCacheLayer};
8use crate::error::TileCacheError;
9use glam::Vec3;
10use landmark::{
11    CompactHeightfield, ContourSet, Heightfield, MESH_NULL_IDX, PolyMesh, PolyMeshDetail,
12    RecastConfig,
13};
14use waymark::nav_mesh::{MeshTile, Poly, PolyDetail, TileHeader};
15use waymark::{MAX_VERTS_PER_POLY, PolyFlags, PolyType};
16
17/// Tile cache builder for converting cached layers to navigation mesh tiles
18#[derive(Debug)]
19pub struct TileCacheBuilder {
20    /// Configuration for building tiles
21    config: TileCacheBuilderConfig,
22    /// Recast configuration derived from builder config
23    recast_config: RecastConfig,
24}
25
26impl TileCacheBuilder {
27    /// Creates a new tile cache builder
28    pub fn new(config: TileCacheBuilderConfig) -> Self {
29        // Convert TileCacheBuilderConfig to RecastConfig
30        let recast_config = {
31            let mut rc = RecastConfig::default();
32            rc.cs = config.cs;
33            rc.ch = config.ch;
34            rc.walkable_height = config.walkable_height;
35            rc.walkable_climb = config.walkable_climb;
36            rc.walkable_radius = config.walkable_radius;
37            rc.max_edge_len = config.max_edge_len as i32;
38            rc.max_simplification_error = config.max_simplification_error;
39            rc.min_region_area = config.min_region_area;
40            rc.merge_region_area = config.merge_region_area;
41            rc.max_vertices_per_polygon = config.max_verts_per_poly;
42            rc
43        };
44
45        Self {
46            config,
47            recast_config,
48        }
49    }
50
51    /// Builds a navigation mesh tile from a tile cache layer
52    pub fn build_tile_from_layer(
53        &self,
54        layer: &TileCacheLayer,
55        obstacles: &[Obstacle],
56        _tile_cache: &TileCache,
57    ) -> Result<MeshTile, TileCacheError> {
58        // Step 1: Create a compact heightfield from the layer data
59        let mut chf = self.layer_to_compact_heightfield(layer)?;
60
61        // Step 2: Apply obstacles to the heightfield
62        self.rasterize_obstacles(&mut chf, obstacles, &layer.header)?;
63
64        // Step 3: Build contours from the modified heightfield
65        let cset = ContourSet::build_from_compact_heightfield(
66            &chf,
67            self.recast_config.max_simplification_error,
68            self.recast_config.max_edge_len,
69            self.recast_config.min_region_area,
70            self.recast_config.merge_region_area,
71        )?;
72
73        // Step 4: Build polygon mesh from contours
74        let pmesh = PolyMesh::build_from_contour_set(
75            &cset,
76            self.recast_config.max_vertices_per_polygon as usize,
77        )?;
78
79        // Step 5: Build detail mesh for height accuracy
80        let dmesh = PolyMeshDetail::build_from_poly_mesh(
81            &pmesh,
82            &chf,
83            self.recast_config.detail_sample_dist,
84            self.recast_config.detail_sample_max_error,
85        )?;
86
87        // Step 6: Convert to navigation mesh tile
88        let tile = self.create_mesh_tile(&pmesh, &dmesh, &layer.header)?;
89
90        Ok(tile)
91    }
92
93    /// Converts a tile cache layer to a compact heightfield
94    fn layer_to_compact_heightfield(
95        &self,
96        layer: &TileCacheLayer,
97    ) -> Result<CompactHeightfield, TileCacheError> {
98        let header = &layer.header;
99
100        // Calculate dimensions
101        let width = header.width as usize;
102        let height = header.height as usize;
103
104        // Create heightfield with proper bounds
105        let mut hf = Heightfield::new(
106            width as i32,
107            height as i32,
108            Vec3::new(header.bmin[0], header.bmin[1], header.bmin[2]),
109            Vec3::new(header.bmax[0], header.bmax[1], header.bmax[2]),
110            self.config.cs,
111            self.config.ch,
112        );
113
114        // Reconstruct spans from layer data
115        // The layer stores compressed heightfield data
116        // We need to decode regions, areas, and connections
117        self.decode_layer_to_heightfield(&mut hf, layer)?;
118
119        // Convert to compact heightfield
120        let chf = CompactHeightfield::build_from_heightfield(
121            &hf,
122            self.config.walkable_height,
123            self.config.walkable_climb,
124        )?;
125
126        Ok(chf)
127    }
128
129    /// Decodes layer data into heightfield spans
130    fn decode_layer_to_heightfield(
131        &self,
132        hf: &mut Heightfield,
133        layer: &TileCacheLayer,
134    ) -> Result<(), TileCacheError> {
135        let width = layer.header.width as usize;
136        let height = layer.header.height as usize;
137
138        // Validate data sizes
139        if layer.regons.len() != width * height {
140            return Err(TileCacheError::InvalidRegionData);
141        }
142        if layer.areas.len() != width * height {
143            return Err(TileCacheError::InvalidAreaData);
144        }
145
146        // Reconstruct spans from layer data
147        for y in 0..height {
148            for x in 0..width {
149                let idx = y * width + x;
150                let region = layer.regons[idx];
151                let area = layer.areas[idx];
152
153                // Skip empty cells
154                if region == 0 {
155                    continue;
156                }
157
158                // Calculate height from layer header
159                let cell_height = layer.header.hmin as i32;
160
161                // Add span to heightfield
162                hf.add_span(
163                    x as i32,
164                    y as i32,
165                    cell_height as i16,
166                    (cell_height + 1) as i16,
167                    area,
168                )?;
169            }
170        }
171
172        Ok(())
173    }
174
175    /// Rasterizes obstacles into the compact heightfield
176    fn rasterize_obstacles(
177        &self,
178        chf: &mut CompactHeightfield,
179        obstacles: &[Obstacle],
180        header: &crate::tile_cache_data::TileCacheLayerHeader,
181    ) -> Result<(), TileCacheError> {
182        for obstacle in obstacles {
183            // Skip empty or removing obstacles
184            if obstacle.state == ObstacleState::Empty || obstacle.state == ObstacleState::Removing {
185                continue;
186            }
187
188            // Check if obstacle is within tile bounds
189            let tile_min = Vec3::new(header.bmin[0], header.bmin[1], header.bmin[2]);
190            let tile_max = Vec3::new(header.bmax[0], header.bmax[1], header.bmax[2]);
191
192            let (obs_min, obs_max) = match &obstacle.data {
193                ObstacleData::Cylinder {
194                    pos,
195                    radius,
196                    height,
197                } => {
198                    let obs_min = Vec3::new(pos[0] - radius, pos[1], pos[2] - radius);
199                    let obs_max = Vec3::new(pos[0] + radius, pos[1] + height, pos[2] + radius);
200                    (obs_min, obs_max)
201                }
202                ObstacleData::Box { bmin, bmax } => {
203                    let obs_min = Vec3::new(bmin[0], bmin[1], bmin[2]);
204                    let obs_max = Vec3::new(bmax[0], bmax[1], bmax[2]);
205                    (obs_min, obs_max)
206                }
207                ObstacleData::OrientedBox {
208                    center,
209                    half_extents,
210                    ..
211                } => {
212                    // Conservative bounds for oriented box
213                    let radius = (half_extents[0].powi(2) + half_extents[2].powi(2)).sqrt();
214                    let obs_min = Vec3::new(
215                        center[0] - radius,
216                        center[1] - half_extents[1],
217                        center[2] - radius,
218                    );
219                    let obs_max = Vec3::new(
220                        center[0] + radius,
221                        center[1] + half_extents[1],
222                        center[2] + radius,
223                    );
224                    (obs_min, obs_max)
225                }
226            };
227
228            // Skip if obstacle doesn't overlap tile
229            if obs_max.x < tile_min.x
230                || obs_min.x > tile_max.x
231                || obs_max.z < tile_min.z
232                || obs_min.z > tile_max.z
233            {
234                continue;
235            }
236
237            // Rasterize obstacle based on type
238            match &obstacle.data {
239                ObstacleData::Cylinder { .. } => {
240                    self.rasterize_cylinder_obstacle(chf, obstacle)?;
241                }
242                ObstacleData::Box { .. } => {
243                    self.rasterize_box_obstacle(chf, obstacle)?;
244                }
245                ObstacleData::OrientedBox { .. } => {
246                    self.rasterize_oriented_box_obstacle(chf, obstacle)?;
247                }
248            }
249        }
250
251        Ok(())
252    }
253
254    /// Rasterizes a cylindrical obstacle into the heightfield
255    fn rasterize_cylinder_obstacle(
256        &self,
257        chf: &mut CompactHeightfield,
258        obstacle: &Obstacle,
259    ) -> Result<(), TileCacheError> {
260        let (center, radius, height) = match &obstacle.data {
261            ObstacleData::Cylinder {
262                pos,
263                radius,
264                height,
265            } => (Vec3::new(pos[0], pos[1], pos[2]), *radius, *height),
266            _ => return Ok(()), // Skip non-cylinder obstacles
267        };
268
269        // Cache read-only values to avoid borrow conflicts with mutable span access
270        let chf_bmin = chf.bmin();
271        let chf_width = chf.width();
272        let chf_height = chf.height();
273
274        // Convert world coordinates to cell coordinates
275        let min_x = ((center.x - radius - chf_bmin.x) / self.config.cs).floor() as i32;
276        let max_x = ((center.x + radius - chf_bmin.x) / self.config.cs).ceil() as i32;
277        let min_z = ((center.z - radius - chf_bmin.z) / self.config.cs).floor() as i32;
278        let max_z = ((center.z + radius - chf_bmin.z) / self.config.cs).ceil() as i32;
279
280        // Clamp to heightfield bounds
281        let min_x = min_x.max(0);
282        let max_x = max_x.min(chf_width - 1);
283        let min_z = min_z.max(0);
284        let max_z = max_z.min(chf_height - 1);
285
286        // Mark cells within the cylinder as unwalkable
287        for z in min_z..=max_z {
288            for x in min_x..=max_x {
289                // Calculate cell center
290                let cell_x = chf_bmin.x + (x as f32 + 0.5) * self.config.cs;
291                let cell_z = chf_bmin.z + (z as f32 + 0.5) * self.config.cs;
292
293                // Check if cell center is within cylinder radius
294                let dx = cell_x - center.x;
295                let dz = cell_z - center.z;
296                let dist_sq = dx * dx + dz * dz;
297
298                if dist_sq <= radius * radius {
299                    // Mark all spans in this column as unwalkable if they overlap the obstacle height
300                    let cell_idx = (z * chf_width + x) as usize;
301                    // Read cell info before borrowing spans mutably
302                    let (first_span_idx, span_count) = {
303                        match chf.cells().get(cell_idx) {
304                            Some(cell) => match cell.index {
305                                Some(idx) => (idx, cell.count),
306                                None => continue,
307                            },
308                            None => continue,
309                        }
310                    };
311
312                    for s in 0..span_count {
313                        let current_span_idx = first_span_idx + s;
314                        if let Some(span) = chf.spans_mut().get_mut(current_span_idx) {
315                            // Check if span overlaps obstacle height
316                            let span_min_y = chf_bmin.y + span.y as f32 * self.config.ch;
317                            let span_max_y = span_min_y + self.config.ch;
318
319                            if span_min_y < center.y + height && span_max_y > center.y {
320                                // Mark span as non-walkable
321                                span.area = 0; // RC_NULL_AREA
322                            }
323                        }
324                    }
325                }
326            }
327        }
328
329        Ok(())
330    }
331
332    /// Creates a navigation mesh tile from polygon and detail meshes
333    fn create_mesh_tile(
334        &self,
335        pmesh: &PolyMesh,
336        dmesh: &PolyMeshDetail,
337        header: &crate::tile_cache_data::TileCacheLayerHeader,
338    ) -> Result<MeshTile, TileCacheError> {
339        // Create tile header
340        let mut tile_header = TileHeader::new(header.tx, header.ty, header.tlayer);
341        tile_header.set_vert_count(pmesh.vert_count() as i32);
342        tile_header.set_poly_count(pmesh.poly_count() as i32);
343        tile_header.set_detail_mesh_count(dmesh.poly_count() as i32);
344        tile_header.set_detail_vert_count(dmesh.vert_count() as i32);
345        tile_header.set_detail_tri_count(dmesh.tri_count() as i32);
346
347        // Copy vertices (convert from i32 to f32)
348        let mut verts = Vec::with_capacity(pmesh.vert_count() * 3);
349        for i in 0..pmesh.vert_count() {
350            verts.push(pmesh.verts()[i * 3] as f32);
351            verts.push(pmesh.verts()[i * 3 + 1] as f32);
352            verts.push(pmesh.verts()[i * 3 + 2] as f32);
353        }
354
355        // Convert polygons
356        let mut polys = Vec::with_capacity(pmesh.poly_count());
357        for i in 0..pmesh.poly_count() {
358            // Extract vertices from the polygon mesh data
359            let mut vert_count = 0;
360            let mut verts = [0u16; MAX_VERTS_PER_POLY];
361            let mut neighbors = [0u16; MAX_VERTS_PER_POLY];
362
363            // Find actual vertex count by looking at polygon data
364            for j in 0..pmesh.max_verts_per_poly() {
365                let poly_idx = i * pmesh.max_verts_per_poly() + j;
366                if poly_idx < pmesh.polys().len() {
367                    let v = pmesh.polys()[poly_idx];
368                    if v != MESH_NULL_IDX {
369                        verts[vert_count] = v;
370                        neighbors[vert_count] = 0; // No neighbor data in PolyMesh
371                        vert_count += 1;
372                    }
373                }
374            }
375
376            let poly = Poly::from_mesh_data(
377                verts,
378                neighbors,
379                vert_count as u8,
380                pmesh.areas()[i],
381                PolyType::Ground,
382                PolyFlags::WALK,
383            );
384
385            polys.push(poly);
386        }
387
388        // Convert detail meshes
389        let mut detail_meshes = Vec::with_capacity(dmesh.poly_count());
390        for i in 0..dmesh.poly_count() {
391            detail_meshes.push(PolyDetail {
392                vert_base: dmesh.poly_start()[i] as u32,
393                tri_base: dmesh.poly_start()[i] as u32,
394                vert_count: 0, // Will be calculated from triangles
395                tri_count: dmesh.poly_tri_count()[i] as u8,
396            });
397        }
398
399        // Copy detail vertices
400        let detail_verts = dmesh.vertices().to_vec();
401
402        // Copy detail triangles (convert from u32 to u8)
403        let mut detail_tris = Vec::with_capacity(dmesh.tri_count() * 3);
404        for i in 0..dmesh.tri_count() {
405            detail_tris.push(dmesh.triangles()[i * 3] as u8);
406            detail_tris.push(dmesh.triangles()[i * 3 + 1] as u8);
407            detail_tris.push(dmesh.triangles()[i * 3 + 2] as u8);
408        }
409
410        // Create the mesh tile
411        let tile = MeshTile::from_tile_data(
412            tile_header,
413            polys,
414            verts,
415            detail_meshes,
416            detail_verts,
417            detail_tris,
418        );
419
420        Ok(tile)
421    }
422
423    /// Rasterizes an axis-aligned box obstacle into the heightfield
424    fn rasterize_box_obstacle(
425        &self,
426        chf: &mut CompactHeightfield,
427        obstacle: &Obstacle,
428    ) -> Result<(), TileCacheError> {
429        let (bmin, bmax) = match &obstacle.data {
430            ObstacleData::Box { bmin, bmax } => (*bmin, *bmax),
431            _ => return Ok(()), // Skip non-box obstacles
432        };
433
434        // Cache read-only values to avoid borrow conflicts with mutable span access
435        let chf_bmin = chf.bmin();
436        let chf_width = chf.width();
437        let chf_height = chf.height();
438
439        // Convert world coordinates to cell coordinates
440        let min_x = ((bmin[0] - chf_bmin.x) / self.config.cs).floor() as i32;
441        let max_x = ((bmax[0] - chf_bmin.x) / self.config.cs).ceil() as i32;
442        let min_z = ((bmin[2] - chf_bmin.z) / self.config.cs).floor() as i32;
443        let max_z = ((bmax[2] - chf_bmin.z) / self.config.cs).ceil() as i32;
444
445        // Clamp to heightfield bounds
446        let min_x = min_x.max(0);
447        let max_x = max_x.min(chf_width - 1);
448        let min_z = min_z.max(0);
449        let max_z = max_z.min(chf_height - 1);
450
451        // Rasterize the box
452        for z in min_z..=max_z {
453            for x in min_x..=max_x {
454                let cell_idx = (x + z * chf_width) as usize;
455                // Read cell info before borrowing spans mutably
456                let (cell_index, cell_count) = {
457                    let cell = &chf.cells()[cell_idx];
458                    match cell.index {
459                        Some(idx) => (idx, cell.count),
460                        None => continue,
461                    }
462                };
463
464                // Mark all spans within the box height range
465                for i in cell_index..(cell_index + cell_count) {
466                    let span = &mut chf.spans_mut()[i];
467                    let span_y_min = span.min as f32 * self.config.ch + chf_bmin.y;
468                    let span_y_max = span.max as f32 * self.config.ch + chf_bmin.y;
469
470                    // Check if span overlaps with box height
471                    if span_y_max > bmin[1] && span_y_min < bmax[1] {
472                        span.area = 0; // Mark as unwalkable
473                    }
474                }
475            }
476        }
477
478        Ok(())
479    }
480
481    /// Rasterizes an oriented box obstacle into the heightfield
482    fn rasterize_oriented_box_obstacle(
483        &self,
484        chf: &mut CompactHeightfield,
485        obstacle: &Obstacle,
486    ) -> Result<(), TileCacheError> {
487        let (center, half_extents, rot_aux) = match &obstacle.data {
488            ObstacleData::OrientedBox {
489                center,
490                half_extents,
491                rot_aux,
492            } => (*center, *half_extents, *rot_aux),
493            _ => return Ok(()), // Skip non-oriented-box obstacles
494        };
495
496        // Reconstruct rotation from auxiliary values
497        // rot_aux[0] = cos(0.5*angle)*sin(-0.5*angle)
498        // rot_aux[1] = cos(0.5*angle)*cos(0.5*angle) - 0.5
499        let cos_half_cos_half = rot_aux[1] + 0.5;
500        let cos_half_sin_half = rot_aux[0];
501
502        // Conservative bounding box for the oriented box
503        let radius = (half_extents[0].powi(2) + half_extents[2].powi(2)).sqrt();
504
505        // Cache read-only values to avoid borrow conflicts with mutable span access
506        let chf_bmin = chf.bmin();
507        let chf_width = chf.width();
508        let chf_height = chf.height();
509
510        // Convert world coordinates to cell coordinates
511        let min_x = ((center[0] - radius - chf_bmin.x) / self.config.cs).floor() as i32;
512        let max_x = ((center[0] + radius - chf_bmin.x) / self.config.cs).ceil() as i32;
513        let min_z = ((center[2] - radius - chf_bmin.z) / self.config.cs).floor() as i32;
514        let max_z = ((center[2] + radius - chf_bmin.z) / self.config.cs).ceil() as i32;
515
516        // Clamp to heightfield bounds
517        let min_x = min_x.max(0);
518        let max_x = max_x.min(chf_width - 1);
519        let min_z = min_z.max(0);
520        let max_z = max_z.min(chf_height - 1);
521
522        // Rasterize cells that are inside the oriented box
523        for z in min_z..=max_z {
524            for x in min_x..=max_x {
525                // Get cell center in world space
526                let cell_x = x as f32 * self.config.cs + chf_bmin.x + self.config.cs * 0.5;
527                let cell_z = z as f32 * self.config.cs + chf_bmin.z + self.config.cs * 0.5;
528
529                // Transform to box-local coordinates
530                let dx = cell_x - center[0];
531                let dz = cell_z - center[2];
532
533                // Apply inverse rotation (using 2D rotation matrix)
534                // For Y-axis rotation: cos(θ) = 2*cos²(θ/2) - 1
535                let cos_angle = 2.0 * cos_half_cos_half - 1.0;
536                let sin_angle = 2.0 * cos_half_sin_half;
537
538                let local_x = cos_angle * dx + sin_angle * dz;
539                let local_z = -sin_angle * dx + cos_angle * dz;
540
541                // Check if point is inside the box
542                if local_x.abs() <= half_extents[0] && local_z.abs() <= half_extents[2] {
543                    let cell_idx = (x + z * chf_width) as usize;
544                    // Read cell info before borrowing spans mutably
545                    let (cell_index, cell_count) = {
546                        let cell = &chf.cells()[cell_idx];
547                        match cell.index {
548                            Some(idx) => (idx, cell.count),
549                            None => continue,
550                        }
551                    };
552
553                    // Mark all spans within the box height range
554                    for i in cell_index..(cell_index + cell_count) {
555                        let span = &mut chf.spans_mut()[i];
556                        let span_y_min = span.min as f32 * self.config.ch + chf_bmin.y;
557                        let span_y_max = span.max as f32 * self.config.ch + chf_bmin.y;
558
559                        // Check if span overlaps with box height
560                        if span_y_max > center[1] - half_extents[1]
561                            && span_y_min < center[1] + half_extents[1]
562                        {
563                            span.area = 0; // Mark as unwalkable
564                        }
565                    }
566                }
567            }
568        }
569
570        Ok(())
571    }
572}
573
574#[cfg(test)]
575mod tests {
576    use super::*;
577
578    #[test]
579    fn test_tile_cache_builder_creation() {
580        let config = TileCacheBuilderConfig::default();
581        let builder = TileCacheBuilder::new(config);
582
583        assert_eq!(builder.recast_config.cs, 0.3);
584        assert_eq!(builder.recast_config.ch, 0.2);
585        assert_eq!(builder.recast_config.walkable_height, 20);
586    }
587}