1use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
6use std::cmp::Ordering;
7use std::f32;
8
9#[derive(Clone, Copy, Debug, PartialEq)]
12pub struct Vec2 {
13 pub x: f32,
14 pub y: f32,
15}
16
17impl Vec2 {
18 #[inline] pub fn new(x: f32, y: f32) -> Self { Self { x, y } }
19 #[inline] pub fn zero() -> Self { Self { x: 0.0, y: 0.0 } }
20 #[inline] pub fn dot(self, o: Self) -> f32 { self.x * o.x + self.y * o.y }
21 #[inline] pub fn cross(self, o: Self) -> f32 { self.x * o.y - self.y * o.x }
22 #[inline] pub fn len_sq(self) -> f32 { self.dot(self) }
23 #[inline] pub fn len(self) -> f32 { self.len_sq().sqrt() }
24 #[inline] pub fn norm(self) -> Self {
25 let l = self.len();
26 if l < 1e-9 { Self::zero() } else { Self::new(self.x / l, self.y / l) }
27 }
28 #[inline] pub fn sub(self, o: Self) -> Self { Self::new(self.x - o.x, self.y - o.y) }
29 #[inline] pub fn add(self, o: Self) -> Self { Self::new(self.x + o.x, self.y + o.y) }
30 #[inline] pub fn scale(self, s: f32) -> Self { Self::new(self.x * s, self.y * s) }
31 #[inline] pub fn lerp(self, o: Self, t: f32) -> Self {
32 Self::new(self.x + (o.x - self.x) * t, self.y + (o.y - self.y) * t)
33 }
34 #[inline] pub fn dist(self, o: Self) -> f32 { self.sub(o).len() }
35 #[inline] pub fn dist_sq(self, o: Self) -> f32 { self.sub(o).len_sq() }
36 #[inline] pub fn perp(self) -> Self { Self::new(-self.y, self.x) }
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
43pub struct AreaFlags(pub u32);
44
45impl AreaFlags {
46 pub const WALKABLE: Self = Self(1 << 0);
47 pub const WATER: Self = Self(1 << 1);
48 pub const ROAD: Self = Self(1 << 2);
49 pub const GRASS: Self = Self(1 << 3);
50 pub const HAZARD: Self = Self(1 << 4);
51 pub const BLOCKED: Self = Self(1 << 5);
52 pub const ALL: Self = Self(u32::MAX);
53 pub const NONE: Self = Self(0);
54
55 #[inline] pub fn contains(self, other: Self) -> bool { (self.0 & other.0) == other.0 }
56 #[inline] pub fn union(self, other: Self) -> Self { Self(self.0 | other.0) }
57 #[inline] pub fn intersect(self, other: Self) -> Self { Self(self.0 & other.0) }
58}
59
60#[derive(Clone, Debug)]
62pub struct AreaCost {
63 pub costs: HashMap<AreaFlags, f32>,
64}
65
66impl Default for AreaCost {
67 fn default() -> Self {
68 let mut costs = HashMap::new();
69 costs.insert(AreaFlags::WALKABLE, 1.0);
70 costs.insert(AreaFlags::WATER, 3.0);
71 costs.insert(AreaFlags::ROAD, 0.8);
72 costs.insert(AreaFlags::GRASS, 1.2);
73 costs.insert(AreaFlags::HAZARD, 5.0);
74 Self { costs }
75 }
76}
77
78impl AreaCost {
79 pub fn get(&self, flags: AreaFlags) -> f32 {
80 for (k, v) in &self.costs {
81 if flags.contains(*k) { return *v; }
82 }
83 1.0
84 }
85 pub fn set(&mut self, flags: AreaFlags, cost: f32) {
86 self.costs.insert(flags, cost);
87 }
88}
89
90#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
94pub struct NavPolyId(pub u32);
95
96#[derive(Clone, Debug)]
98pub struct NavPoly {
99 pub id: NavPolyId,
100 pub verts: Vec<Vec2>, pub centroid: Vec2,
102 pub area: AreaFlags,
103 pub cost: f32, pub portals: Vec<usize>,
106}
107
108impl NavPoly {
109 pub fn new(id: NavPolyId, verts: Vec<Vec2>, area: AreaFlags, cost: f32) -> Self {
110 let centroid = Self::compute_centroid(&verts);
111 Self { id, verts, centroid, area, cost, portals: Vec::new() }
112 }
113
114 fn compute_centroid(verts: &[Vec2]) -> Vec2 {
115 if verts.is_empty() { return Vec2::zero(); }
116 let sum = verts.iter().fold(Vec2::zero(), |a, &v| a.add(v));
117 sum.scale(1.0 / verts.len() as f32)
118 }
119
120 pub fn contains_point(&self, p: Vec2) -> bool {
122 let n = self.verts.len();
123 if n < 3 { return false; }
124 for i in 0..n {
125 let a = self.verts[i];
126 let b = self.verts[(i + 1) % n];
127 let ab = b.sub(a);
128 let ap = p.sub(a);
129 if ab.cross(ap) < 0.0 { return false; }
130 }
131 true
132 }
133
134 pub fn closest_point(&self, p: Vec2) -> Vec2 {
136 if self.contains_point(p) { return p; }
137 let n = self.verts.len();
138 let mut best = self.verts[0];
139 let mut best_dist = f32::MAX;
140 for i in 0..n {
141 let a = self.verts[i];
142 let b = self.verts[(i + 1) % n];
143 let c = closest_point_on_segment(a, b, p);
144 let d = c.dist_sq(p);
145 if d < best_dist { best_dist = d; best = c; }
146 }
147 best
148 }
149
150 pub fn signed_area(&self) -> f32 {
152 let n = self.verts.len();
153 let mut area = 0.0f32;
154 for i in 0..n {
155 let a = self.verts[i];
156 let b = self.verts[(i + 1) % n];
157 area += a.cross(b);
158 }
159 area * 0.5
160 }
161}
162
163fn closest_point_on_segment(a: Vec2, b: Vec2, p: Vec2) -> Vec2 {
164 let ab = b.sub(a);
165 let ap = p.sub(a);
166 let t = ap.dot(ab) / (ab.len_sq() + 1e-12);
167 let t = t.clamp(0.0, 1.0);
168 a.add(ab.scale(t))
169}
170
171#[derive(Clone, Debug)]
175pub struct NavPortal {
176 pub poly_a: NavPolyId,
177 pub poly_b: NavPolyId,
178 pub left: Vec2, pub right: Vec2, }
181
182impl NavPortal {
183 pub fn midpoint(&self) -> Vec2 {
184 self.left.lerp(self.right, 0.5)
185 }
186 pub fn width(&self) -> f32 {
187 self.left.dist(self.right)
188 }
189 pub fn other(&self, poly: NavPolyId) -> NavPolyId {
191 if poly == self.poly_a { self.poly_b } else { self.poly_a }
192 }
193}
194
195#[derive(Clone, Debug, Default)]
199pub struct NavMesh {
200 pub polys: Vec<NavPoly>,
201 pub portals: Vec<NavPortal>,
202 poly_index: HashMap<NavPolyId, usize>,
204 next_id: u32,
205 pub area_cost: AreaCost,
206}
207
208impl NavMesh {
209 pub fn new() -> Self { Self::default() }
210
211 pub fn add_poly(&mut self, verts: Vec<Vec2>, area: AreaFlags, cost: f32) -> NavPolyId {
214 let id = NavPolyId(self.next_id);
215 self.next_id += 1;
216 let idx = self.polys.len();
217 self.polys.push(NavPoly::new(id, verts, area, cost));
218 self.poly_index.insert(id, idx);
219 id
220 }
221
222 pub fn add_portal(&mut self, poly_a: NavPolyId, poly_b: NavPolyId, left: Vec2, right: Vec2) -> usize {
226 let portal_idx = self.portals.len();
227 self.portals.push(NavPortal { poly_a, poly_b, left, right });
228 if let Some(&ia) = self.poly_index.get(&poly_a) {
229 self.polys[ia].portals.push(portal_idx);
230 }
231 if let Some(&ib) = self.poly_index.get(&poly_b) {
232 self.polys[ib].portals.push(portal_idx);
233 }
234 portal_idx
235 }
236
237 pub fn build_portals_from_edges(&mut self) {
239 let n = self.polys.len();
240 for i in 0..n {
241 for j in (i + 1)..n {
242 let pid_a = self.polys[i].id;
243 let pid_b = self.polys[j].id;
244 if let Some((left, right)) = Self::find_shared_edge(&self.polys[i], &self.polys[j]) {
245 let portal_idx = self.portals.len();
246 self.portals.push(NavPortal { poly_a: pid_a, poly_b: pid_b, left, right });
247 self.polys[i].portals.push(portal_idx);
248 self.polys[j].portals.push(portal_idx);
249 }
250 }
251 }
252 }
253
254 fn find_shared_edge(a: &NavPoly, b: &NavPoly) -> Option<(Vec2, Vec2)> {
255 const EPS: f32 = 1e-4;
256 let na = a.verts.len();
257 let nb = b.verts.len();
258 for i in 0..na {
259 let va0 = a.verts[i];
260 let va1 = a.verts[(i + 1) % na];
261 for j in 0..nb {
262 let vb0 = b.verts[j];
263 let vb1 = b.verts[(j + 1) % nb];
264 let fwd = va0.dist_sq(vb0) < EPS && va1.dist_sq(vb1) < EPS;
266 let rev = va0.dist_sq(vb1) < EPS && va1.dist_sq(vb0) < EPS;
267 if fwd || rev { return Some((va0, va1)); }
268 }
269 }
270 None
271 }
272
273 pub fn poly_at_point(&self, p: Vec2) -> Option<NavPolyId> {
276 for poly in &self.polys {
277 if !poly.area.contains(AreaFlags::BLOCKED) && poly.contains_point(p) {
278 return Some(poly.id);
279 }
280 }
281 None
282 }
283
284 pub fn poly_by_id(&self, id: NavPolyId) -> Option<&NavPoly> {
285 self.poly_index.get(&id).map(|&i| &self.polys[i])
286 }
287
288 fn poly_by_id_mut(&mut self, id: NavPolyId) -> Option<&mut NavPoly> {
289 let i = *self.poly_index.get(&id)?;
290 Some(&mut self.polys[i])
291 }
292
293 pub fn closest_point_on_mesh(&self, p: Vec2) -> NavPoint {
295 let mut best_pt = p;
296 let mut best_poly = NavPolyId(0);
297 let mut best_dist = f32::MAX;
298 for poly in &self.polys {
299 if poly.area.contains(AreaFlags::BLOCKED) { continue; }
300 let cp = poly.closest_point(p);
301 let d = cp.dist_sq(p);
302 if d < best_dist {
303 best_dist = d;
304 best_pt = cp;
305 best_poly = poly.id;
306 }
307 }
308 NavPoint { pos: best_pt, poly: best_poly }
309 }
310
311 pub fn find_path(&self, start: Vec2, end: Vec2, filter: AreaFlags) -> Option<NavPath> {
315 let start_poly = self.poly_at_point(start)
316 .unwrap_or_else(|| self.closest_point_on_mesh(start).poly);
317 let end_poly = self.poly_at_point(end)
318 .unwrap_or_else(|| self.closest_point_on_mesh(end).poly);
319
320 if start_poly == end_poly {
321 return Some(NavPath {
322 polys: vec![start_poly],
323 portals: Vec::new(),
324 waypoints: vec![start, end],
325 });
326 }
327
328 let mut open: BinaryHeap<AStarEntry> = BinaryHeap::new();
330 let mut came_from: HashMap<NavPolyId, (NavPolyId, usize)> = HashMap::new(); let mut g_score: HashMap<NavPolyId, f32> = HashMap::new();
332
333 g_score.insert(start_poly, 0.0);
334 open.push(AStarEntry {
335 poly: start_poly,
336 f: self.heuristic(start_poly, end_poly),
337 });
338
339 while let Some(AStarEntry { poly: current, .. }) = open.pop() {
340 if current == end_poly {
341 let (polys, portal_indices) = self.reconstruct_corridor(start_poly, end_poly, &came_from);
342 let portals: Vec<NavPortal> = portal_indices.iter().map(|&i| self.portals[i].clone()).collect();
343 let waypoints = self.string_pull(start, end, &polys, &portals);
344 return Some(NavPath { polys, portals, waypoints });
345 }
346
347 let current_g = *g_score.get(¤t).unwrap_or(&f32::MAX);
348 let portal_idxs: Vec<usize> = if let Some(p) = self.poly_by_id(current) {
349 p.portals.clone()
350 } else { continue };
351
352 for pidx in portal_idxs {
353 let portal = &self.portals[pidx];
354 let neighbor = portal.other(current);
355 if let Some(npoly) = self.poly_by_id(neighbor) {
356 if !npoly.area.intersect(filter).contains(AreaFlags::WALKABLE) { continue; }
357 if npoly.area.contains(AreaFlags::BLOCKED) { continue; }
358 let cost_mod = self.area_cost.get(npoly.area);
359 let edge_cost = current_g + portal.midpoint().dist(
360 self.poly_by_id(current).map(|p| p.centroid).unwrap_or(Vec2::zero())
361 ) * cost_mod;
362 let ng = current_g + edge_cost.max(0.1);
363 if ng < *g_score.get(&neighbor).unwrap_or(&f32::MAX) {
364 g_score.insert(neighbor, ng);
365 came_from.insert(neighbor, (current, pidx));
366 let h = self.heuristic(neighbor, end_poly);
367 open.push(AStarEntry { poly: neighbor, f: ng + h });
368 }
369 }
370 }
371 }
372 None
373 }
374
375 fn heuristic(&self, a: NavPolyId, b: NavPolyId) -> f32 {
376 let ca = self.poly_by_id(a).map(|p| p.centroid).unwrap_or(Vec2::zero());
377 let cb = self.poly_by_id(b).map(|p| p.centroid).unwrap_or(Vec2::zero());
378 ca.dist(cb)
379 }
380
381 fn reconstruct_corridor(
382 &self,
383 start: NavPolyId,
384 end: NavPolyId,
385 came_from: &HashMap<NavPolyId, (NavPolyId, usize)>,
386 ) -> (Vec<NavPolyId>, Vec<usize>) {
387 let mut polys = Vec::new();
388 let mut portal_indices = Vec::new();
389 let mut cur = end;
390 while cur != start {
391 polys.push(cur);
392 if let Some(&(prev, pidx)) = came_from.get(&cur) {
393 portal_indices.push(pidx);
394 cur = prev;
395 } else { break; }
396 }
397 polys.push(start);
398 polys.reverse();
399 portal_indices.reverse();
400 (polys, portal_indices)
401 }
402
403 pub fn string_pull(&self, start: Vec2, end: Vec2, _polys: &[NavPolyId], portals: &[NavPortal]) -> Vec<Vec2> {
407 if portals.is_empty() { return vec![start, end]; }
408
409 let mut port_lefts: Vec<Vec2> = Vec::new();
411 let mut port_rights: Vec<Vec2> = Vec::new();
412
413 port_lefts.push(start);
414 port_rights.push(start);
415
416 for portal in portals {
417 port_lefts.push(portal.left);
418 port_rights.push(portal.right);
419 }
420 port_lefts.push(end);
421 port_rights.push(end);
422
423 let mut path = vec![start];
425 let mut apex = start;
426 let mut left = port_lefts[1];
427 let mut right = port_rights[1];
428 let mut apex_idx = 0usize;
429 let mut left_idx = 1usize;
430 let mut right_idx = 1usize;
431
432 let n = port_lefts.len();
433 for i in 2..n {
434 let new_left = port_lefts[i];
435 let new_right = port_rights[i];
436
437 if triangle_area2(apex, right, new_right) <= 0.0 {
439 if apex == right || triangle_area2(apex, left, new_right) > 0.0 {
440 right = new_right;
441 right_idx = i;
442 } else {
443 path.push(left);
445 apex = left;
446 apex_idx = left_idx;
447 right = apex;
448 right_idx = apex_idx;
449 if apex_idx + 1 < n { left = port_lefts[apex_idx + 1]; left_idx = apex_idx + 1; }
451 if apex_idx + 1 < n { right = port_rights[apex_idx + 1]; right_idx = apex_idx + 1; }
452 continue;
455 }
456 }
457
458 if triangle_area2(apex, left, new_left) >= 0.0 {
460 if apex == left || triangle_area2(apex, right, new_left) < 0.0 {
461 left = new_left;
462 left_idx = i;
463 } else {
464 path.push(right);
465 apex = right;
466 apex_idx = right_idx;
467 left = apex;
468 left_idx = apex_idx;
469 if apex_idx + 1 < n { right = port_rights[apex_idx + 1]; right_idx = apex_idx + 1; }
470 if apex_idx + 1 < n { left = port_lefts[apex_idx + 1]; left_idx = apex_idx + 1; }
471 continue;
472 }
473 }
474 }
475
476 path.push(end);
477 path.dedup_by(|a, b| a.dist_sq(*b) < 1e-8);
479 path
480 }
481}
482
483#[inline]
485fn triangle_area2(a: Vec2, b: Vec2, c: Vec2) -> f32 {
486 (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y)
487}
488
489#[derive(Clone, Copy, Debug)]
491pub struct NavPoint {
492 pub pos: Vec2,
493 pub poly: NavPolyId,
494}
495
496#[derive(Clone, Debug)]
498pub struct NavPath {
499 pub polys: Vec<NavPolyId>,
500 pub portals: Vec<NavPortal>,
501 pub waypoints: Vec<Vec2>,
502}
503
504#[derive(PartialEq)]
507struct AStarEntry {
508 poly: NavPolyId,
509 f: f32,
510}
511
512impl Eq for AStarEntry {}
513
514impl PartialOrd for AStarEntry {
515 fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
516}
517
518impl Ord for AStarEntry {
519 fn cmp(&self, other: &Self) -> Ordering {
520 other.f.partial_cmp(&self.f).unwrap_or(Ordering::Equal)
521 }
522}
523
524#[derive(Clone, Debug)]
528pub struct Obstacle {
529 pub id: u32,
530 pub center: Vec2,
531 pub radius: f32,
532 affected: Vec<NavPolyId>,
534}
535
536pub struct ObstacleCutter {
538 obstacles: HashMap<u32, Obstacle>,
539 next_id: u32,
540}
541
542impl ObstacleCutter {
543 pub fn new() -> Self {
544 Self { obstacles: HashMap::new(), next_id: 0 }
545 }
546
547 pub fn add_obstacle(&mut self, mesh: &mut NavMesh, center: Vec2, radius: f32) -> u32 {
549 let id = self.next_id;
550 self.next_id += 1;
551 let mut affected = Vec::new();
552
553 for poly in &mut mesh.polys {
554 if poly_overlaps_circle(&poly.verts, center, radius) {
555 poly.area = poly.area.union(AreaFlags::BLOCKED);
556 affected.push(poly.id);
557 }
558 }
559 self.obstacles.insert(id, Obstacle { id, center, radius, affected });
560 id
561 }
562
563 pub fn remove_obstacle(&mut self, mesh: &mut NavMesh, id: u32) {
565 if let Some(obs) = self.obstacles.remove(&id) {
566 for pid in obs.affected {
567 if let Some(idx) = mesh.poly_index.get(&pid) {
568 let poly = &mut mesh.polys[*idx];
569 poly.area = AreaFlags(poly.area.0 & !AreaFlags::BLOCKED.0);
570 }
571 }
572 }
573 }
574
575 pub fn move_obstacle(&mut self, mesh: &mut NavMesh, id: u32, new_center: Vec2) {
577 if let Some(obs) = self.obstacles.get(&id).cloned() {
578 self.remove_obstacle(mesh, id);
579 self.add_obstacle(mesh, new_center, obs.radius);
580 }
581 }
582}
583
584fn poly_overlaps_circle(verts: &[Vec2], center: Vec2, radius: f32) -> bool {
585 let r2 = radius * radius;
586 if verts.len() >= 3 {
588 let mut inside = true;
589 let n = verts.len();
590 for i in 0..n {
591 let a = verts[i];
592 let b = verts[(i + 1) % n];
593 if b.sub(a).cross(center.sub(a)) < 0.0 { inside = false; break; }
594 }
595 if inside { return true; }
596 }
597 let n = verts.len();
599 for i in 0..n {
600 let a = verts[i];
601 let b = verts[(i + 1) % n];
602 let cp = closest_point_on_segment(a, b, center);
603 if cp.dist_sq(center) <= r2 { return true; }
604 }
605 false
606}
607
608pub struct NavMeshQuery<'a> {
612 pub mesh: &'a NavMesh,
613 pub filter: AreaFlags,
614}
615
616impl<'a> NavMeshQuery<'a> {
617 pub fn new(mesh: &'a NavMesh) -> Self {
618 Self { mesh, filter: AreaFlags::WALKABLE }
619 }
620
621 pub fn with_filter(mut self, filter: AreaFlags) -> Self {
622 self.filter = filter;
623 self
624 }
625
626 pub fn find_path(&self, start: Vec2, end: Vec2) -> Vec<Vec2> {
628 self.mesh.find_path(start, end, self.filter)
629 .map(|p| p.waypoints)
630 .unwrap_or_default()
631 }
632
633 pub fn snap(&self, p: Vec2) -> Vec2 {
635 self.mesh.closest_point_on_mesh(p).pos
636 }
637
638 pub fn raycast(&self, start: Vec2, end: Vec2) -> Option<Vec2> {
640 let start_poly = self.mesh.poly_at_point(start)?;
641 let dir = end.sub(start);
642 let len = dir.len();
643 if len < 1e-9 { return None; }
644 let step = dir.scale(1.0 / len);
645 let steps = (len / 0.5) as usize + 1;
646 for i in 1..=steps {
647 let t = (i as f32 * 0.5).min(len);
648 let p = start.add(step.scale(t));
649 if let Some(poly_id) = self.mesh.poly_at_point(p) {
650 if let Some(poly) = self.mesh.poly_by_id(poly_id) {
651 if poly.area.contains(AreaFlags::BLOCKED) { return Some(p); }
652 }
653 } else {
654 return Some(p);
655 }
656 }
657 None
658 }
659}
660
661#[derive(Clone, Debug)]
665pub struct NavRegion {
666 pub id: u32,
667 pub polys: HashSet<NavPolyId>,
668 pub entry_portals: Vec<usize>, }
670
671#[derive(Clone, Debug, Default)]
673pub struct RegionGraph {
674 pub regions: Vec<NavRegion>,
675 pub poly_to_region: HashMap<NavPolyId, u32>,
676}
677
678impl RegionGraph {
679 pub fn build(mesh: &NavMesh, max_cluster_size: usize) -> Self {
681 let mut graph = Self::default();
682 let mut visited: HashSet<NavPolyId> = HashSet::new();
683 let mut region_id = 0u32;
684
685 for poly in &mesh.polys {
686 if visited.contains(&poly.id) { continue; }
687 if poly.area.contains(AreaFlags::BLOCKED) { continue; }
688
689 let mut cluster = HashSet::new();
691 let mut queue = VecDeque::new();
692 queue.push_back(poly.id);
693
694 while let Some(pid) = queue.pop_front() {
695 if visited.contains(&pid) { continue; }
696 if cluster.len() >= max_cluster_size { break; }
697 visited.insert(pid);
698 cluster.insert(pid);
699 if let Some(p) = mesh.poly_by_id(pid) {
700 for &portal_idx in &p.portals {
701 let portal = &mesh.portals[portal_idx];
702 let neighbor = portal.other(pid);
703 if !visited.contains(&neighbor) {
704 queue.push_back(neighbor);
705 }
706 }
707 }
708 }
709
710 for &pid in &cluster {
711 graph.poly_to_region.insert(pid, region_id);
712 }
713 graph.regions.push(NavRegion { id: region_id, polys: cluster, entry_portals: Vec::new() });
714 region_id += 1;
715 }
716 let region_count = graph.regions.len();
718 for (pidx, portal) in mesh.portals.iter().enumerate() {
719 let ra = graph.poly_to_region.get(&portal.poly_a).copied();
720 let rb = graph.poly_to_region.get(&portal.poly_b).copied();
721 if let (Some(ra), Some(rb)) = (ra, rb) {
722 if ra != rb && ra < region_count as u32 && rb < region_count as u32 {
723 graph.regions[ra as usize].entry_portals.push(pidx);
724 graph.regions[rb as usize].entry_portals.push(pidx);
725 }
726 }
727 }
728 graph
729 }
730}
731
732pub fn path_length(waypoints: &[Vec2]) -> f32 {
736 waypoints.windows(2).map(|w| w[0].dist(w[1])).sum()
737}
738
739pub fn path_sample(waypoints: &[Vec2], t: f32) -> Vec2 {
741 if waypoints.is_empty() { return Vec2::zero(); }
742 if waypoints.len() == 1 { return waypoints[0]; }
743 let total = path_length(waypoints);
744 if total < 1e-9 { return waypoints[0]; }
745 let target = (t.clamp(0.0, 1.0) * total).min(total - 1e-9);
746 let mut acc = 0.0f32;
747 for i in 0..waypoints.len() - 1 {
748 let seg = waypoints[i].dist(waypoints[i + 1]);
749 if acc + seg >= target {
750 let local_t = (target - acc) / seg.max(1e-9);
751 return waypoints[i].lerp(waypoints[i + 1], local_t);
752 }
753 acc += seg;
754 }
755 *waypoints.last().unwrap()
756}
757
758pub fn nearest_waypoint_index(waypoints: &[Vec2], p: Vec2) -> usize {
760 waypoints.iter().enumerate()
761 .min_by(|(_, a), (_, b)| a.dist_sq(p).partial_cmp(&b.dist_sq(p)).unwrap_or(Ordering::Equal))
762 .map(|(i, _)| i)
763 .unwrap_or(0)
764}
765
766pub fn simplify_path(waypoints: &[Vec2], tolerance: f32) -> Vec<Vec2> {
768 if waypoints.len() <= 2 { return waypoints.to_vec(); }
769 let tol2 = tolerance * tolerance;
770 let mut result = vec![waypoints[0]];
771 let mut i = 0usize;
772 while i < waypoints.len() - 1 {
773 let mut farthest = i + 1;
774 for j in (i + 1)..waypoints.len() {
775 let cp = closest_point_on_segment(waypoints[i], waypoints[j.min(waypoints.len()-1)], waypoints[j]);
776 if cp.dist_sq(waypoints[j]) > tol2 { break; }
777 farthest = j;
778 }
779 result.push(waypoints[farthest]);
780 i = farthest;
781 }
782 result
783}
784
785#[cfg(test)]
788mod tests {
789 use super::*;
790
791 fn square_poly(id: u32, ox: f32, oy: f32, sz: f32, area: AreaFlags) -> (NavPolyId, Vec<Vec2>) {
792 let verts = vec![
793 Vec2::new(ox, oy),
794 Vec2::new(ox + sz, oy),
795 Vec2::new(ox + sz, oy + sz),
796 Vec2::new(ox, oy + sz),
797 ];
798 (NavPolyId(id), verts)
799 }
800
801 #[test]
802 fn test_point_in_poly() {
803 let verts = vec![
804 Vec2::new(0.0, 0.0),
805 Vec2::new(4.0, 0.0),
806 Vec2::new(4.0, 4.0),
807 Vec2::new(0.0, 4.0),
808 ];
809 let poly = NavPoly::new(NavPolyId(0), verts, AreaFlags::WALKABLE, 1.0);
810 assert!(poly.contains_point(Vec2::new(2.0, 2.0)));
811 assert!(!poly.contains_point(Vec2::new(5.0, 5.0)));
812 }
813
814 #[test]
815 fn test_navmesh_build_and_path() {
816 let mut mesh = NavMesh::new();
817 let a = mesh.add_poly(vec![
818 Vec2::new(0.0,0.0), Vec2::new(4.0,0.0),
819 Vec2::new(4.0,4.0), Vec2::new(0.0,4.0),
820 ], AreaFlags::WALKABLE, 1.0);
821 let b = mesh.add_poly(vec![
822 Vec2::new(4.0,0.0), Vec2::new(8.0,0.0),
823 Vec2::new(8.0,4.0), Vec2::new(4.0,4.0),
824 ], AreaFlags::WALKABLE, 1.0);
825 mesh.build_portals_from_edges();
826 let path = mesh.find_path(Vec2::new(1.0, 2.0), Vec2::new(7.0, 2.0), AreaFlags::WALKABLE);
827 assert!(path.is_some());
828 let wp = path.unwrap().waypoints;
829 assert!(wp.len() >= 2);
830 }
831
832 #[test]
833 fn test_closest_point_on_segment() {
834 let a = Vec2::new(0.0, 0.0);
835 let b = Vec2::new(4.0, 0.0);
836 let p = Vec2::new(2.0, 3.0);
837 let c = closest_point_on_segment(a, b, p);
838 assert!((c.x - 2.0).abs() < 1e-5);
839 assert!((c.y - 0.0).abs() < 1e-5);
840 }
841
842 #[test]
843 fn test_obstacle_cutter() {
844 let mut mesh = NavMesh::new();
845 let id = mesh.add_poly(vec![
846 Vec2::new(0.0,0.0), Vec2::new(4.0,0.0),
847 Vec2::new(4.0,4.0), Vec2::new(0.0,4.0),
848 ], AreaFlags::WALKABLE, 1.0);
849 let mut cutter = ObstacleCutter::new();
850 let oid = cutter.add_obstacle(&mut mesh, Vec2::new(2.0, 2.0), 1.0);
851 assert!(mesh.poly_by_id(id).unwrap().area.contains(AreaFlags::BLOCKED));
852 cutter.remove_obstacle(&mut mesh, oid);
853 assert!(!mesh.poly_by_id(id).unwrap().area.contains(AreaFlags::BLOCKED));
854 }
855
856 #[test]
857 fn test_path_sample() {
858 let pts = vec![Vec2::new(0.0,0.0), Vec2::new(10.0,0.0)];
859 let mid = path_sample(&pts, 0.5);
860 assert!((mid.x - 5.0).abs() < 1e-4);
861 }
862}