vleue_navigator/
lib.rs

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
39/// Prelude for imports
40pub 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/// Bevy plugin to add support for the [`NavMesh`] asset type.
54///
55/// This plugin doesn't add support for updating them. See [`NavmeshUpdaterPlugin`] for that.
56#[derive(Debug, Clone, Copy)]
57pub struct VleueNavigatorPlugin;
58
59/// Controls wether to display all [`NavMesh`]es with gizmos, and the color used.
60///
61/// When this resource is present, all [`NavMesh`]es will be visible.
62#[cfg(feature = "debug-with-gizmos")]
63#[derive(Resource, Clone, Copy, Debug)]
64pub struct NavMeshesDebug(
65    /// Color to display the [`NavMesh`] with
66    pub Color,
67);
68
69/// Controls wether to display a specific [`NavMesh`] with gizmos, and the color used.
70///
71/// When this component is present on an entity, the [`NavMesh`] will be visible.
72#[cfg(feature = "debug-with-gizmos")]
73#[derive(Component, Clone, Copy, Debug)]
74pub struct NavMeshDebug(
75    /// Color to display the [`NavMesh`] with
76    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/// A path between two points, in 3D space, transformed using [`NavMesh::transform`].
90#[derive(Debug, PartialEq)]
91pub struct TransformedPath {
92    /// Length of the path.
93    pub length: f32,
94    /// Coordinates for each step of the path. The destination is the last step.
95    pub path: Vec<Vec3>,
96    /// Coordinates for each step of the path. The destination is the last step.
97    /// This path also contains the layer of each step, and steps when changing layers even on a straight line.
98    #[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/// A navigation mesh
113#[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    /// Builds a [`NavMesh`] from a Polyanya [`Mesh`](polyanya::Mesh)
122    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    /// Creates a [`NavMesh`] from a Bevy [`Mesh`], assuming it constructs a 2D structure.
131    /// All triangle normals are aligned during the conversion, so the orientation of the [`Mesh`] does not matter.
132    /// The [`polyanya::Mesh`] generated in the process can be modified via `callback`.
133    ///
134    /// Only supports meshes with the [`PrimitiveTopology::TriangleList`].
135    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    /// Creates a [`NavMesh`] from a Bevy [`Mesh`], assuming it constructs a 2D structure.
176    /// All triangle normals are aligned during the conversion, so the orientation of the [`Mesh`] does not matter.
177    ///
178    /// Only supports meshes with the [`PrimitiveTopology::TriangleList`].
179    pub fn from_bevy_mesh(mesh: &Mesh) -> Option<NavMesh> {
180        Self::from_bevy_mesh_and_then(mesh, |_| {})
181    }
182
183    /// Builds a navmesh from its edges and obstacles.
184    ///
185    /// Obstacles will be merged in case some are overlapping, and mesh will be simplified to reduce the number of polygons.
186    ///
187    /// If you want more controls over the simplification process, you can use the [`Self::from_polyanya_mesh`] method.
188    ///
189    /// Depending on the scale of your mesh, you should change the [`Self::search_delta`] value using [`Self::set_search_delta`].
190    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    /// Retrieves the underlying Polyanya navigation mesh.
207    pub fn get(&self) -> Arc<polyanya::Mesh> {
208        self.mesh.clone()
209    }
210
211    /// Sets the [`search_delta`](polyanya::Mesh::search_delta) value of the navmesh.
212    ///
213    /// Returns `true` if the delta was successfully set, `false` otherwise. This can happens if the mesh is shared and already being modified.
214    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    /// Retrieves the [`search_delta`](polyanya::Mesh::search_delta) value of the navmesh.
226    pub fn search_delta(&self) -> f32 {
227        self.mesh.search_delta()
228    }
229
230    /// Sets the [`search_steps`](polyanya::Mesh::search_steps) value of the navmesh.
231    ///
232    /// Returns `true` if the steps value was successfully set, `false` otherwise. This can happens if the mesh is shared and already being modified.
233    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    /// Retrieves the [`search_steps`](polyanya::Mesh::search_steps) value of the navmesh.
245    pub fn search_steps(&self) -> u32 {
246        self.mesh.search_steps()
247    }
248
249    /// Asynchronously finds the shortest path between two points.
250    #[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    /// Asynchronously finds the shortest path between two points.
256    ///
257    /// Inputs and results are transformed using the [`NavMesh::transform`].
258    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    /// Finds the shortest path between two points.
266    #[inline]
267    pub fn path(&self, from: Vec2, to: Vec2) -> Option<Path> {
268        self.mesh.path(from, to)
269    }
270
271    /// Finds the shortest path between two points.
272    ///
273    /// Inputs and results are transformed using the [`NavMesh::transform`]
274    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            // TODO: recompute length
285            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    /// Checks if a 3D point is within a navigable part of the mesh, using the [`NavMesh::transform`].
301    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    /// Checks if a point is within a navigable part of the mesh.
307    pub fn is_in_mesh(&self, point: Vec2) -> bool {
308        self.mesh.point_in_mesh(point)
309    }
310
311    /// Retrieves the transform used to convert world coordinates into mesh coordinates.
312    ///
313    /// After applying this transform, the `z` coordinate is dropped because navmeshes are in 2D space.
314    pub fn transform(&self) -> Transform {
315        self.transform
316    }
317
318    /// Sets the mesh transform.
319    ///
320    /// It will be used to transform a point in 3D space to a point in the NavMesh 2D space by rotating it and ignoring the `z` axis.
321    pub fn set_transform(&mut self, transform: Transform) {
322        self.transform = transform;
323    }
324
325    /// Creates a [`Mesh`] from this [`NavMesh`], suitable for debugging the surface.
326    ///
327    /// This mesh doesn't have normals.
328    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    /// Creates a [`Mesh`] from this [`NavMesh`], showing the wireframe of the polygons.
354    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    /// Returns the transform that would convert world coordinates into mesh coordinates.
382    #[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        // Guaranteed by Bevy for the attributes requested in this context
399        _ => return None,
400    };
401    Some(vectors.iter().cloned().map(Vec3::from))
402}
403
404#[cfg(feature = "debug-with-gizmos")]
405/// System displaying navmeshes using gizmos for debug purposes.
406pub 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")]
435/// Display a mesh with gizmos.
436pub 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")]
448/// Display a layer with gizmos.
449pub 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")]
485/// Display a layer with gizmos.
486pub 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        // TODO: try and find why this is in CW instead of CCW
530        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        // Add back normals as they are used to determine where is up in the mesh
562        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}