1#![doc = include_str!("../README.md")]
2#![warn(
3 missing_debug_implementations,
4 missing_copy_implementations,
5 trivial_casts,
6 trivial_numeric_casts,
7 unsafe_code,
8 unstable_features,
9 unused_import_braces,
10 unused_qualifications,
11 missing_docs
12)]
13#![cfg_attr(docsrs, feature(doc_cfg))]
14
15use std::sync::Arc;
16
17#[cfg(feature = "debug-with-gizmos")]
18use bevy::{
19 app::Update,
20 asset::Assets,
21 color::Color,
22 prelude::{Component, Gizmos, Query, Res, Resource},
23};
24use bevy::{
25 app::{App, Plugin},
26 asset::{Asset, AssetApp, RenderAssetUsages},
27 log::{debug, warn},
28 math::{Affine3A, Quat, Vec2, Vec3, Vec3Swizzles},
29 mesh::{Indices, MeshVertexAttributeId, PrimitiveTopology, VertexAttributeValues},
30 prelude::{Mesh, Transform, TransformPoint},
31 reflect::TypePath,
32};
33use itertools::Itertools;
34
35pub mod asset_loaders;
36mod obstacles;
37mod updater;
38
39pub mod prelude {
41 pub use crate::obstacles::{
42 ObstacleSource, cached::CachedObstacle, primitive::PrimitiveObstacle,
43 };
44 pub use crate::updater::{
45 CachableObstacle, ManagedNavMesh, NAVMESH_BUILD_DURATION, NavMeshSettings, NavMeshStatus,
46 NavMeshUpdateMode, NavMeshUpdateModeBlocking, NavmeshUpdaterPlugin,
47 };
48 pub use crate::{NavMesh, Triangulation, VleueNavigatorPlugin};
49 #[cfg(feature = "debug-with-gizmos")]
50 pub use crate::{NavMeshDebug, NavMeshesDebug};
51}
52
53#[derive(Debug, Clone, Copy)]
57pub struct VleueNavigatorPlugin;
58
59#[cfg(feature = "debug-with-gizmos")]
63#[derive(Resource, Clone, Copy, Debug)]
64pub struct NavMeshesDebug(
65 pub Color,
67);
68
69#[cfg(feature = "debug-with-gizmos")]
73#[derive(Component, Clone, Copy, Debug)]
74pub struct NavMeshDebug(
75 pub Color,
77);
78
79impl Plugin for VleueNavigatorPlugin {
80 fn build(&self, app: &mut App) {
81 app.register_asset_loader(asset_loaders::NavMeshPolyanyaLoader)
82 .init_asset::<NavMesh>();
83
84 #[cfg(feature = "debug-with-gizmos")]
85 app.add_systems(Update, display_navmesh);
86 }
87}
88
89#[derive(Debug, PartialEq)]
91pub struct TransformedPath {
92 pub length: f32,
94 pub path: Vec<Vec3>,
96 #[cfg(feature = "detailed-layers")]
99 #[cfg_attr(docsrs, doc(cfg(feature = "detailed-layers")))]
100 pub path_with_layers: Vec<(Vec3, u8)>,
101}
102
103use polyanya::Trimesh;
104pub use polyanya::{Path, Triangulation};
105
106#[derive(Debug, Clone)]
107pub(crate) struct BuildingMesh {
108 pub(crate) mesh: polyanya::Mesh,
109 pub(crate) failed_stitches: Vec<(u8, u8)>,
110}
111
112#[derive(Debug, TypePath, Clone, Asset)]
114pub struct NavMesh {
115 mesh: Arc<polyanya::Mesh>,
116 building: Option<BuildingMesh>,
117 transform: Transform,
118}
119
120impl NavMesh {
121 pub fn from_polyanya_mesh(mesh: polyanya::Mesh) -> NavMesh {
123 NavMesh {
124 mesh: Arc::new(mesh),
125 building: None,
126 transform: Transform::IDENTITY,
127 }
128 }
129
130 pub fn from_bevy_mesh_and_then(
136 mesh: &Mesh,
137 callback: impl Fn(&mut polyanya::Mesh),
138 ) -> Option<NavMesh> {
139 if mesh.primitive_topology() != PrimitiveTopology::TriangleList {
140 return None;
141 }
142 let normal = get_vectors(mesh, Mesh::ATTRIBUTE_NORMAL)
143 .and_then(|mut i| i.next())
144 .unwrap_or(Vec3::Z);
145 let rotation = Quat::from_rotation_arc(normal, Vec3::Z);
146 let rotation_reverse = rotation.inverse();
147
148 let vertices = get_vectors(mesh, Mesh::ATTRIBUTE_POSITION)
149 .expect("can't extract a navmesh from a mesh without `Mesh::ATTRIBUTE_POSITION`")
150 .map(|vertex| rotation_reverse.mul_vec3(vertex))
151 .map(|coords| coords.xy())
152 .collect();
153
154 let triangles = mesh
155 .indices()
156 .expect("No polygon indices found in mesh")
157 .iter()
158 .tuples::<(_, _, _)>()
159 .map(|(a, b, c)| [c, b, a])
160 .collect();
161
162 let mut polyanya_mesh = Trimesh {
163 vertices,
164 triangles,
165 }
166 .try_into()
167 .unwrap();
168 callback(&mut polyanya_mesh);
169
170 let mut navmesh = Self::from_polyanya_mesh(polyanya_mesh);
171 navmesh.transform = Transform::from_rotation(rotation);
172 Some(navmesh)
173 }
174
175 pub fn from_bevy_mesh(mesh: &Mesh) -> Option<NavMesh> {
180 Self::from_bevy_mesh_and_then(mesh, |_| {})
181 }
182
183 pub fn from_edge_and_obstacles(edges: Vec<Vec2>, obstacles: Vec<Vec<Vec2>>) -> NavMesh {
191 let mut triangulation = Triangulation::from_outer_edges(&edges);
192 triangulation.add_obstacles(obstacles);
193
194 let mut mesh: polyanya::Mesh = triangulation.as_navmesh();
195 triangulation.simplify(0.001);
196 for _i in 0..3 {
197 if mesh.merge_polygons() {
198 break;
199 }
200 }
201 mesh.set_search_delta(0.01);
202
203 Self::from_polyanya_mesh(mesh)
204 }
205
206 pub fn get(&self) -> Arc<polyanya::Mesh> {
208 self.mesh.clone()
209 }
210
211 pub fn set_search_delta(&mut self, delta: f32) -> bool {
215 if let Some(mesh) = Arc::get_mut(&mut self.mesh) {
216 debug!("setting mesh delta to {}", delta);
217 mesh.set_search_delta(delta);
218 true
219 } else {
220 warn!("failed setting mesh delta to {}", delta);
221 false
222 }
223 }
224
225 pub fn search_delta(&self) -> f32 {
227 self.mesh.search_delta()
228 }
229
230 pub fn set_search_steps(&mut self, steps: u32) -> bool {
234 if let Some(mesh) = Arc::get_mut(&mut self.mesh) {
235 debug!("setting mesh steps to {}", steps);
236 mesh.set_search_steps(steps);
237 true
238 } else {
239 warn!("failed setting mesh steps to {}", steps);
240 false
241 }
242 }
243
244 pub fn search_steps(&self) -> u32 {
246 self.mesh.search_steps()
247 }
248
249 #[inline]
251 pub async fn get_path(&self, from: Vec2, to: Vec2) -> Option<Path> {
252 self.mesh.get_path(from, to).await
253 }
254
255 pub async fn get_transformed_path(&self, from: Vec3, to: Vec3) -> Option<TransformedPath> {
259 let inner_from = self.world_to_mesh().transform_point(from).xy();
260 let inner_to = self.world_to_mesh().transform_point(to).xy();
261 let path = self.mesh.get_path(inner_from, inner_to).await;
262 path.map(|path| self.transform_path(path))
263 }
264
265 #[inline]
267 pub fn path(&self, from: Vec2, to: Vec2) -> Option<Path> {
268 self.mesh.path(from, to)
269 }
270
271 pub fn transformed_path(&self, from: Vec3, to: Vec3) -> Option<TransformedPath> {
275 let inner_from = self.world_to_mesh().transform_point(from).xy();
276 let inner_to = self.world_to_mesh().transform_point(to).xy();
277 let path = self.mesh.path(inner_from, inner_to);
278 path.map(|path| self.transform_path(path))
279 }
280
281 fn transform_path(&self, path: Path) -> TransformedPath {
282 let transform = self.transform();
283 TransformedPath {
284 length: path.length,
286 path: path
287 .path
288 .into_iter()
289 .map(|coords| transform.transform_point(coords.extend(0.0)))
290 .collect(),
291 #[cfg(feature = "detailed-layers")]
292 path_with_layers: path
293 .path_with_layers
294 .into_iter()
295 .map(|(coords, layer)| (transform.transform_point(coords.extend(0.0)), layer))
296 .collect(),
297 }
298 }
299
300 pub fn transformed_is_in_mesh(&self, point: Vec3) -> bool {
302 let point_in_navmesh = self.world_to_mesh().transform_point(point).xy();
303 self.mesh.point_in_mesh(point_in_navmesh)
304 }
305
306 pub fn is_in_mesh(&self, point: Vec2) -> bool {
308 self.mesh.point_in_mesh(point)
309 }
310
311 pub fn transform(&self) -> Transform {
315 self.transform
316 }
317
318 pub fn set_transform(&mut self, transform: Transform) {
322 self.transform = transform;
323 }
324
325 pub fn to_mesh(&self) -> Mesh {
329 let mut new_mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::all());
330 let mesh_to_world = self.transform();
331 new_mesh.insert_attribute(
332 Mesh::ATTRIBUTE_POSITION,
333 self.mesh.layers[0]
334 .vertices
335 .iter()
336 .map(|v| v.coords.extend(0.0))
337 .map(|coords| mesh_to_world.transform_point(coords).into())
338 .collect::<Vec<[f32; 3]>>(),
339 );
340 new_mesh.insert_indices(Indices::U32(
341 self.mesh.layers[0]
342 .polygons
343 .iter()
344 .flat_map(|p| {
345 (2..p.vertices.len())
346 .flat_map(|i| [p.vertices[0], p.vertices[i - 1], p.vertices[i]])
347 })
348 .collect(),
349 ));
350 new_mesh
351 }
352
353 pub fn to_wireframe_mesh(&self) -> Mesh {
355 let mut new_mesh = Mesh::new(PrimitiveTopology::LineList, RenderAssetUsages::all());
356 let mesh_to_world = self.transform();
357 new_mesh.insert_attribute(
358 Mesh::ATTRIBUTE_POSITION,
359 self.mesh.layers[0]
360 .vertices
361 .iter()
362 .map(|v| [v.coords.x, v.coords.y, 0.0])
363 .map(|coords| mesh_to_world.transform_point(coords.into()).into())
364 .collect::<Vec<[f32; 3]>>(),
365 );
366 new_mesh.insert_indices(Indices::U32(
367 self.mesh.layers[0]
368 .polygons
369 .iter()
370 .flat_map(|p| {
371 (0..p.vertices.len())
372 .map(|i| [p.vertices[i], p.vertices[(i + 1) % p.vertices.len()]])
373 })
374 .unique_by(|[a, b]| if a < b { (*a, *b) } else { (*b, *a) })
375 .flatten()
376 .collect(),
377 ));
378 new_mesh
379 }
380
381 #[inline]
383 pub fn world_to_mesh(&self) -> Affine3A {
384 world_to_mesh(&self.transform())
385 }
386}
387
388pub(crate) fn world_to_mesh(navmesh_transform: &Transform) -> Affine3A {
389 navmesh_transform.compute_affine().inverse()
390}
391
392fn get_vectors(
393 mesh: &Mesh,
394 id: impl Into<MeshVertexAttributeId>,
395) -> Option<impl Iterator<Item = Vec3> + '_> {
396 let vectors = match mesh.attribute(id) {
397 Some(VertexAttributeValues::Float32x3(values)) => values,
398 _ => return None,
400 };
401 Some(vectors.iter().cloned().map(Vec3::from))
402}
403
404#[cfg(feature = "debug-with-gizmos")]
405pub fn display_navmesh(
407 live_navmeshes: Query<(
408 &updater::ManagedNavMesh,
409 Option<&NavMeshDebug>,
410 &bevy::prelude::GlobalTransform,
411 &updater::NavMeshSettings,
412 )>,
413 mut gizmos: Gizmos,
414 navmeshes: Res<Assets<NavMesh>>,
415 controls: Option<Res<NavMeshesDebug>>,
416) {
417 for (mesh, debug, mesh_to_world, settings) in &live_navmeshes {
418 let Some(color) = debug
419 .map(|debug| debug.0)
420 .or_else(|| controls.as_ref().map(|c| c.0))
421 else {
422 continue;
423 };
424 if let Some(navmesh) = navmeshes.get(mesh) {
425 let navmesh = navmesh.get();
426 let Some(layer) = &navmesh.layers.get(settings.layer.unwrap_or(0) as usize) else {
427 continue;
428 };
429 display_layer_gizmo(layer, mesh_to_world, color, &mut gizmos);
430 }
431 }
432}
433
434#[cfg(feature = "debug-with-gizmos")]
435pub fn display_mesh_gizmo<T: bevy::gizmos::config::GizmoConfigGroup>(
437 mesh: &polyanya::Mesh,
438 mesh_to_world: &bevy::prelude::GlobalTransform,
439 colors: &[Color],
440 gizmos: &mut Gizmos<T>,
441) {
442 for (layer, color) in mesh.layers.iter().zip(colors.iter().cycle()) {
443 display_layer_gizmo(layer, mesh_to_world, *color, gizmos);
444 }
445}
446
447#[cfg(feature = "debug-with-gizmos")]
448pub fn display_layer_gizmo<T: bevy::gizmos::config::GizmoConfigGroup>(
450 layer: &polyanya::Layer,
451 mesh_to_world: &bevy::prelude::GlobalTransform,
452 color: Color,
453 gizmos: &mut Gizmos<T>,
454) {
455 #[cfg(feature = "detailed-layers")]
456 let scale = layer.scale;
457 #[cfg(not(feature = "detailed-layers"))]
458 let scale = Vec2::ONE;
459 for polygon in &layer.polygons {
460 let mut v = polygon
461 .vertices
462 .iter()
463 .filter(|i| **i != u32::MAX)
464 .map(|i| {
465 (layer.vertices[*i as usize].coords * scale)
466 .extend(-layer.height.get(*i as usize).cloned().unwrap_or_default())
467 })
468 .map(|v| mesh_to_world.transform_point(v))
469 .collect::<Vec<_>>();
470 if !v.is_empty() {
471 let first_index = polygon.vertices[0] as usize;
472 let first = &layer.vertices[first_index];
473 v.push(
474 mesh_to_world.transform_point(
475 (first.coords * scale)
476 .extend(-layer.height.get(first_index).cloned().unwrap_or_default()),
477 ),
478 );
479 gizmos.linestrip(v, color);
480 }
481 }
482}
483
484#[cfg(feature = "debug-with-gizmos")]
485pub fn display_polygon_gizmo<T: bevy::gizmos::config::GizmoConfigGroup>(
487 layer: &polyanya::Layer,
488 polygon: u32,
489 mesh_to_world: &bevy::prelude::GlobalTransform,
490 color: Color,
491 gizmos: &mut Gizmos<T>,
492) {
493 #[cfg(feature = "detailed-layers")]
494 let scale = layer.scale;
495 #[cfg(not(feature = "detailed-layers"))]
496 let scale = Vec2::ONE;
497 let polygon = &layer.polygons[polygon as usize];
498 let mut v = polygon
499 .vertices
500 .iter()
501 .filter(|i| **i != u32::MAX)
502 .map(|i| {
503 (layer.vertices[*i as usize].coords * scale)
504 .extend(-layer.height.get(*i as usize).cloned().unwrap_or_default())
505 })
506 .map(|v| mesh_to_world.transform_point(v))
507 .collect::<Vec<_>>();
508 if !v.is_empty() {
509 let first_index = polygon.vertices[0] as usize;
510 let first = &layer.vertices[first_index];
511 v.push(
512 mesh_to_world.transform_point(
513 (first.coords * scale)
514 .extend(-layer.height.get(first_index).cloned().unwrap_or_default()),
515 ),
516 );
517 gizmos.linestrip(v, color);
518 }
519}
520
521#[cfg(test)]
522mod tests {
523 use polyanya::Trimesh;
524
525 use super::*;
526
527 #[test]
528 fn generating_from_existing_navmesh_results_in_same_navmesh() {
529 let expected_navmesh = NavMesh::from_polyanya_mesh(
531 Trimesh {
532 vertices: vec![
533 Vec2::new(1., 1.),
534 Vec2::new(5., 1.),
535 Vec2::new(5., 4.),
536 Vec2::new(1., 4.),
537 Vec2::new(2., 2.),
538 Vec2::new(4., 3.),
539 ],
540 triangles: vec![[4, 1, 0], [5, 2, 1], [3, 2, 5], [3, 5, 1], [3, 4, 0]],
541 }
542 .try_into()
543 .unwrap(),
544 );
545 let initial_navmesh = NavMesh::from_polyanya_mesh(
546 Trimesh {
547 vertices: vec![
548 Vec2::new(1., 1.),
549 Vec2::new(5., 1.),
550 Vec2::new(5., 4.),
551 Vec2::new(1., 4.),
552 Vec2::new(2., 2.),
553 Vec2::new(4., 3.),
554 ],
555 triangles: vec![[0, 1, 4], [1, 2, 5], [5, 2, 3], [1, 5, 3], [0, 4, 3]],
556 }
557 .try_into()
558 .unwrap(),
559 );
560 let mut bevy_mesh = initial_navmesh.to_mesh();
561 bevy_mesh.insert_attribute(
563 Mesh::ATTRIBUTE_NORMAL,
564 (0..6).map(|_| [0.0, 0.0, 1.0]).collect::<Vec<_>>(),
565 );
566 let actual_navmesh = NavMesh::from_bevy_mesh(&bevy_mesh).unwrap();
567
568 assert_same_navmesh(expected_navmesh, actual_navmesh);
569 }
570
571 #[test]
572 fn rotated_mesh_generates_expected_navmesh() {
573 let expected_navmesh = NavMesh::from_polyanya_mesh(
574 Trimesh {
575 vertices: vec![
576 Vec2::new(-1., 1.),
577 Vec2::new(1., 1.),
578 Vec2::new(-1., -1.),
579 Vec2::new(1., -1.),
580 ],
581 triangles: vec![[3, 1, 0], [2, 3, 0]],
582 }
583 .try_into()
584 .unwrap(),
585 );
586 let mut bevy_mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::all());
587 bevy_mesh.insert_attribute(
588 Mesh::ATTRIBUTE_POSITION,
589 vec![
590 [-1.0, 0.0, 1.0],
591 [1.0, 0.0, 1.0],
592 [-1.0, 0.0, -1.0],
593 [1.0, 0.0, -1.0],
594 ],
595 );
596 bevy_mesh.insert_attribute(
597 Mesh::ATTRIBUTE_NORMAL,
598 vec![
599 [0.0, 1.0, -0.0],
600 [0.0, 1.0, -0.0],
601 [0.0, 1.0, -0.0],
602 [0.0, 1.0, -0.0],
603 ],
604 );
605 bevy_mesh.insert_indices(Indices::U32(vec![0, 1, 3, 0, 3, 2]));
606
607 let actual_navmesh = NavMesh::from_bevy_mesh(&bevy_mesh).unwrap();
608
609 assert_same_navmesh(expected_navmesh, actual_navmesh);
610 }
611
612 fn assert_same_navmesh(expected: NavMesh, actual: NavMesh) {
613 let expected_mesh = expected.mesh;
614 let actual_mesh = actual.mesh;
615
616 for i in 0..expected_mesh.layers.len() {
617 assert_eq!(
618 expected_mesh.layers[i].polygons,
619 actual_mesh.layers[i].polygons
620 );
621 for (index, (expected_vertex, actual_vertex)) in expected_mesh.layers[i]
622 .vertices
623 .iter()
624 .zip(actual_mesh.layers[i].vertices.iter())
625 .enumerate()
626 {
627 let nearly_same_coords =
628 (expected_vertex.coords - actual_vertex.coords).length_squared() < 1e-8;
629 assert!(
630 nearly_same_coords,
631 "\nvertex {index} does not have the expected coords.\nExpected vertices: {0:?}\nGot vertices: {1:?}",
632 expected_mesh.layers[i].vertices, actual_mesh.layers[i].vertices
633 );
634
635 let adjusted_actual = wrap_to_first(&actual_vertex.polygons, |index| *index != u32::MAX).unwrap_or_else(||
636 panic!("vertex {index}: Found only surrounded by obstacles.\nExpected vertices: {0:?}\nGot vertices: {1:?}",
637 expected_mesh.layers[i].vertices, actual_mesh.layers[i].vertices));
638
639 let adjusted_expectation= wrap_to_first(&expected_vertex.polygons, |polygon| {
640 *polygon == adjusted_actual[0]
641 })
642 .unwrap_or_else(||
643 panic!("vertex {index}: Failed to expected polygons.\nExpected vertices: {0:?}\nGot vertices: {1:?}",
644 expected_mesh.layers[i].vertices, actual_mesh.layers[i].vertices));
645
646 assert_eq!(
647 adjusted_expectation, adjusted_actual,
648 "\nvertex {index} does not have the expected polygons.\nExpected vertices: {0:?}\nGot vertices: {1:?}",
649 expected_mesh.layers[i].vertices, actual_mesh.layers[i].vertices
650 );
651 }
652 }
653 }
654
655 fn wrap_to_first(polygons: &[u32], pred: impl Fn(&u32) -> bool) -> Option<Vec<u32>> {
656 let offset = polygons.iter().position(pred)?;
657 Some(
658 polygons
659 .iter()
660 .skip(offset)
661 .chain(polygons.iter().take(offset))
662 .cloned()
663 .collect(),
664 )
665 }
666}