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