1use std::collections::{BTreeMap, VecDeque};
73
74use crate::navmesh::points_equal;
75use crate::{
76 Navmesh, NavmeshQuery, NavmeshQueryResult, NavmeshSearchResult, Point2, PolygonPath,
77 PreparedNavmesh, PreparedNavmeshBuildError, PreparedNavmeshBuilder, StaticPreparedNavmesh,
78 StaticPreparedNavmeshBuilder,
79};
80
81const EPSILON: f64 = 1e-9;
82const ADAPTIVE_LRU_CAPACITY: usize = 256;
83const COST_AWARE_EVICTION_WEIGHT: f64 = 0.35;
84const FIXED_ADMISSION_COST_THRESHOLD: f64 = 2.5;
85const FIXED_PROMOTION_RULE_HITS: usize = 2;
86
87#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
92pub struct TRAStarBuilder;
93
94impl PreparedNavmeshBuilder for TRAStarBuilder {
95 type Map = PreparedTRAStar;
96
97 fn name(&self) -> &'static str {
98 "tra-star"
99 }
100
101 fn preprocess(&self, navmesh: &Navmesh) -> Result<Self::Map, PreparedNavmeshBuildError> {
102 let prepared = StaticPreparedNavmeshBuilder.preprocess(navmesh)?;
103 Ok(PreparedTRAStar { prepared })
104 }
105}
106
107#[derive(Debug, Clone, PartialEq)]
112pub struct PreparedTRAStar {
113 prepared: StaticPreparedNavmesh,
114}
115
116impl PreparedTRAStar {
117 #[must_use]
119 pub fn builder() -> TRAStarBuilder {
120 TRAStarBuilder
121 }
122
123 #[must_use]
125 pub fn prepared_navmesh(&self) -> &StaticPreparedNavmesh {
126 &self.prepared
127 }
128
129 pub fn search(&self, query: NavmeshQuery) -> NavmeshSearchResult {
134 search_with_midpoint_seed(self, query, default_midpoint_seed)
135 }
136}
137
138impl PreparedNavmesh for PreparedTRAStar {
139 fn name(&self) -> &'static str {
140 "tra-star"
141 }
142
143 fn navmesh(&self) -> &Navmesh {
144 self.prepared.navmesh()
145 }
146
147 fn neighbors(&self, cell_index: usize) -> Option<&[usize]> {
148 self.prepared.neighbors(cell_index)
149 }
150
151 fn portals_from(&self, cell_index: usize) -> Option<&[crate::NavmeshPortal]> {
152 self.prepared.portals_from(cell_index)
153 }
154
155 fn portal_between(&self, left_cell: usize, right_cell: usize) -> Option<crate::NavmeshPortal> {
156 self.prepared.portal_between(left_cell, right_cell)
157 }
158}
159
160#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
165pub struct TRAStarPortalTransitionCacheBuilder;
166
167impl PreparedNavmeshBuilder for TRAStarPortalTransitionCacheBuilder {
168 type Map = PreparedTRAStarPortalTransitionCache;
169
170 fn name(&self) -> &'static str {
171 "tra-star-portal-transition-cache"
172 }
173
174 fn preprocess(&self, navmesh: &Navmesh) -> Result<Self::Map, PreparedNavmeshBuildError> {
175 let prepared = TRAStarBuilder.preprocess(navmesh)?;
176 Ok(PreparedTRAStarPortalTransitionCache::from_prepared(
177 prepared,
178 ))
179 }
180}
181
182#[derive(Debug, Clone, PartialEq)]
186pub struct PreparedTRAStarPortalTransitionCache {
187 prepared: PreparedTRAStar,
188 portal_transition_midpoints: BTreeMap<(usize, usize), Point2>,
189}
190
191impl PreparedTRAStarPortalTransitionCache {
192 #[must_use]
194 pub fn builder() -> TRAStarPortalTransitionCacheBuilder {
195 TRAStarPortalTransitionCacheBuilder
196 }
197
198 #[must_use]
200 pub fn prepared_tra_star(&self) -> &PreparedTRAStar {
201 &self.prepared
202 }
203
204 #[must_use]
206 pub fn portal_transition_count(&self) -> usize {
207 self.portal_transition_midpoints.len()
208 }
209
210 pub fn search(&self, query: NavmeshQuery) -> NavmeshSearchResult {
212 search_with_midpoint_seed(self, query, |corridor, cells| {
213 self.midpoint_seed_from_transitions(cells, corridor)
214 })
215 }
216
217 fn from_prepared(prepared: PreparedTRAStar) -> Self {
218 let mut portal_transition_midpoints = BTreeMap::new();
219 for cell_index in 0..prepared.navmesh().cells().len() {
220 let Some(portals) = prepared.portals_from(cell_index) else {
221 continue;
222 };
223
224 for &portal in portals {
225 let neighbor = if portal.left_cell == cell_index {
226 portal.right_cell
227 } else {
228 portal.left_cell
229 };
230 portal_transition_midpoints
231 .entry((cell_index, neighbor))
232 .or_insert_with(|| crate::algorithms::channel_search::portal_midpoint(&portal));
233 }
234 }
235
236 Self {
237 prepared,
238 portal_transition_midpoints,
239 }
240 }
241
242 fn midpoint_seed_from_transitions(
243 &self,
244 cells: &[usize],
245 corridor: &crate::navmesh::corridor::NavmeshCorridor,
246 ) -> Vec<Point2> {
247 cells
248 .windows(2)
249 .enumerate()
250 .map(|(index, pair)| {
251 self.portal_transition_midpoints
252 .get(&(pair[0], pair[1]))
253 .copied()
254 .or_else(|| {
255 corridor.portals.get(index).copied().map(|portal| {
256 crate::algorithms::channel_search::portal_midpoint(&portal)
257 })
258 })
259 .unwrap_or(corridor.goal)
260 })
261 .collect()
262 }
263}
264
265#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
267pub struct TRAStarWaypointDatabaseStaticBuilder;
268
269impl PreparedNavmeshBuilder for TRAStarWaypointDatabaseStaticBuilder {
270 type Map = PreparedTRAStarWaypointDatabaseStatic;
271
272 fn name(&self) -> &'static str {
273 "tra-star-waypoint-database-static"
274 }
275
276 fn preprocess(&self, navmesh: &Navmesh) -> Result<Self::Map, PreparedNavmeshBuildError> {
277 let prepared = TRAStarBuilder.preprocess(navmesh)?;
278 Ok(PreparedTRAStarWaypointDatabaseStatic::from_prepared(
279 prepared,
280 ))
281 }
282}
283
284#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
288pub struct TRAStarWaypointDatabaseLazyQueryBuilder;
289
290impl PreparedNavmeshBuilder for TRAStarWaypointDatabaseLazyQueryBuilder {
291 type Map = PreparedTRAStarWaypointDatabaseLazyQuery;
292
293 fn name(&self) -> &'static str {
294 "tra-star-waypoint-database-lazy-query"
295 }
296
297 fn preprocess(&self, navmesh: &Navmesh) -> Result<Self::Map, PreparedNavmeshBuildError> {
298 let prepared = TRAStarBuilder.preprocess(navmesh)?;
299 Ok(PreparedTRAStarWaypointDatabaseLazyQuery::from_prepared(
300 prepared,
301 ))
302 }
303}
304
305#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
307pub struct TRAStarWaypointDatabaseAdaptiveLruBuilder;
308
309impl PreparedNavmeshBuilder for TRAStarWaypointDatabaseAdaptiveLruBuilder {
310 type Map = PreparedTRAStarWaypointDatabaseAdaptiveLru;
311
312 fn name(&self) -> &'static str {
313 "tra-star-waypoint-database-adaptive-lru"
314 }
315
316 fn preprocess(&self, navmesh: &Navmesh) -> Result<Self::Map, PreparedNavmeshBuildError> {
317 let prepared = TRAStarBuilder.preprocess(navmesh)?;
318 Ok(PreparedTRAStarWaypointDatabaseAdaptiveLru::from_prepared(
319 prepared,
320 ADAPTIVE_LRU_CAPACITY,
321 ))
322 }
323}
324
325#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
327pub struct TRAStarWaypointDatabaseTwoTierLruBuilder;
328
329impl PreparedNavmeshBuilder for TRAStarWaypointDatabaseTwoTierLruBuilder {
330 type Map = PreparedTRAStarWaypointDatabaseTwoTierLru;
331
332 fn name(&self) -> &'static str {
333 "tra-star-waypoint-database-two-tier-lru"
334 }
335
336 fn preprocess(&self, navmesh: &Navmesh) -> Result<Self::Map, PreparedNavmeshBuildError> {
337 let prepared = TRAStarBuilder.preprocess(navmesh)?;
338 Ok(PreparedTRAStarWaypointDatabaseTwoTierLru::from_prepared(
339 prepared,
340 ADAPTIVE_LRU_CAPACITY,
341 ))
342 }
343}
344
345#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
347pub struct TRAStarWaypointDatabaseCostAwareEvictionBuilder;
348
349#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
353pub struct TRAStarWaypointDatabaseFixedAdmissionThresholdBuilder;
354
355#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
357pub struct TRAStarWaypointDatabaseFixedPromotionRuleBuilder;
358
359#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
363pub struct TRAStarWaypointDatabaseFixedDemotionRuleBuilder;
364
365#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
370pub enum TRAStarWaypointDatabasePolicyProfile {
371 #[default]
374 V1,
375}
376
377impl TRAStarWaypointDatabasePolicyProfile {
378 #[must_use]
380 pub const fn name(self) -> &'static str {
381 match self {
382 Self::V1 => "tra-star-waypoint-database-policy-profile-v1",
383 }
384 }
385
386 #[must_use]
388 pub const fn admission_cost_threshold(self) -> f64 {
389 match self {
390 Self::V1 => FIXED_ADMISSION_COST_THRESHOLD,
391 }
392 }
393
394 #[must_use]
396 pub const fn promotion_hits_required(self) -> usize {
397 match self {
398 Self::V1 => FIXED_PROMOTION_RULE_HITS,
399 }
400 }
401}
402
403#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
407pub struct TRAStarWaypointDatabasePolicyProfileBuilder {
408 profile: TRAStarWaypointDatabasePolicyProfile,
409}
410
411impl TRAStarWaypointDatabasePolicyProfileBuilder {
412 #[must_use]
414 pub const fn new(profile: TRAStarWaypointDatabasePolicyProfile) -> Self {
415 Self { profile }
416 }
417
418 #[must_use]
420 pub const fn policy_profile(self) -> TRAStarWaypointDatabasePolicyProfile {
421 self.profile
422 }
423}
424
425impl PreparedNavmeshBuilder for TRAStarWaypointDatabaseCostAwareEvictionBuilder {
426 type Map = PreparedTRAStarWaypointDatabaseCostAwareEviction;
427
428 fn name(&self) -> &'static str {
429 "tra-star-waypoint-database-cost-aware-eviction"
430 }
431
432 fn preprocess(&self, navmesh: &Navmesh) -> Result<Self::Map, PreparedNavmeshBuildError> {
433 let prepared = TRAStarBuilder.preprocess(navmesh)?;
434 Ok(
435 PreparedTRAStarWaypointDatabaseCostAwareEviction::from_prepared(
436 prepared,
437 ADAPTIVE_LRU_CAPACITY,
438 ),
439 )
440 }
441}
442
443impl PreparedNavmeshBuilder for TRAStarWaypointDatabaseFixedAdmissionThresholdBuilder {
444 type Map = PreparedTRAStarWaypointDatabaseFixedAdmissionThreshold;
445
446 fn name(&self) -> &'static str {
447 "tra-star-waypoint-database-fixed-admission-threshold"
448 }
449
450 fn preprocess(&self, navmesh: &Navmesh) -> Result<Self::Map, PreparedNavmeshBuildError> {
451 let prepared = TRAStarBuilder.preprocess(navmesh)?;
452 Ok(
453 PreparedTRAStarWaypointDatabaseFixedAdmissionThreshold::from_prepared(
454 prepared,
455 ADAPTIVE_LRU_CAPACITY,
456 FIXED_ADMISSION_COST_THRESHOLD,
457 ),
458 )
459 }
460}
461
462impl PreparedNavmeshBuilder for TRAStarWaypointDatabaseFixedPromotionRuleBuilder {
463 type Map = PreparedTRAStarWaypointDatabaseFixedPromotionRule;
464
465 fn name(&self) -> &'static str {
466 "tra-star-waypoint-database-fixed-promotion-rule"
467 }
468
469 fn preprocess(&self, navmesh: &Navmesh) -> Result<Self::Map, PreparedNavmeshBuildError> {
470 let prepared = TRAStarBuilder.preprocess(navmesh)?;
471 Ok(
472 PreparedTRAStarWaypointDatabaseFixedPromotionRule::from_prepared(
473 prepared,
474 ADAPTIVE_LRU_CAPACITY,
475 FIXED_ADMISSION_COST_THRESHOLD,
476 FIXED_PROMOTION_RULE_HITS,
477 ),
478 )
479 }
480}
481
482impl PreparedNavmeshBuilder for TRAStarWaypointDatabaseFixedDemotionRuleBuilder {
483 type Map = PreparedTRAStarWaypointDatabaseFixedDemotionRule;
484
485 fn name(&self) -> &'static str {
486 "tra-star-waypoint-database-fixed-demotion-rule"
487 }
488
489 fn preprocess(&self, navmesh: &Navmesh) -> Result<Self::Map, PreparedNavmeshBuildError> {
490 let prepared = TRAStarBuilder.preprocess(navmesh)?;
491 Ok(
492 PreparedTRAStarWaypointDatabaseFixedDemotionRule::from_prepared(
493 prepared,
494 ADAPTIVE_LRU_CAPACITY,
495 FIXED_ADMISSION_COST_THRESHOLD,
496 FIXED_PROMOTION_RULE_HITS,
497 ),
498 )
499 }
500}
501
502impl PreparedNavmeshBuilder for TRAStarWaypointDatabasePolicyProfileBuilder {
503 type Map = PreparedTRAStarWaypointDatabasePolicyProfile;
504
505 fn name(&self) -> &'static str {
506 self.profile.name()
507 }
508
509 fn preprocess(&self, navmesh: &Navmesh) -> Result<Self::Map, PreparedNavmeshBuildError> {
510 let prepared = TRAStarBuilder.preprocess(navmesh)?;
511 Ok(PreparedTRAStarWaypointDatabasePolicyProfile::from_prepared(
512 prepared,
513 self.profile,
514 ))
515 }
516}
517
518#[derive(Debug, Clone, Copy, PartialEq)]
519struct CellWaypointEntry {
520 to_cell: usize,
521 waypoint: Point2,
522}
523
524#[derive(Debug, Clone, PartialEq)]
528pub struct PreparedTRAStarWaypointDatabaseStatic {
529 prepared: PreparedTRAStar,
530 waypoint_database: BTreeMap<usize, Vec<CellWaypointEntry>>,
531}
532
533#[derive(Debug, Clone, PartialEq)]
537pub struct PreparedTRAStarWaypointDatabaseLazyQuery {
538 prepared: PreparedTRAStar,
539}
540
541#[derive(Debug, Default)]
542struct WaypointLruCache {
543 entries: BTreeMap<(usize, usize), Point2>,
544 order: VecDeque<(usize, usize)>,
545}
546
547impl WaypointLruCache {
548 fn get(&mut self, key: (usize, usize)) -> Option<Point2> {
549 let waypoint = self.entries.get(&key).copied()?;
550 self.promote(key);
551 Some(waypoint)
552 }
553
554 fn insert(&mut self, key: (usize, usize), waypoint: Point2, capacity: usize) {
555 if self.entries.insert(key, waypoint).is_some() {
556 self.promote(key);
557 return;
558 }
559
560 self.order.push_back(key);
561 while self.entries.len() > capacity {
562 if let Some(evicted) = self.order.pop_front() {
563 self.entries.remove(&evicted);
564 } else {
565 break;
566 }
567 }
568 }
569
570 fn len(&self) -> usize {
571 self.entries.len()
572 }
573
574 fn promote(&mut self, key: (usize, usize)) {
575 if let Some(position) = self.order.iter().position(|entry| *entry == key) {
576 let _ = self.order.remove(position);
577 }
578 self.order.push_back(key);
579 }
580}
581
582#[derive(Debug)]
586pub struct PreparedTRAStarWaypointDatabaseAdaptiveLru {
587 prepared: PreparedTRAStar,
588 capacity: usize,
589 waypoint_lru_cache: std::sync::Mutex<WaypointLruCache>,
590}
591
592#[derive(Debug, Clone, Copy, PartialEq, Eq)]
593enum LruTier {
594 Probation,
595 Protected,
596}
597
598#[derive(Debug, Clone, Copy, PartialEq, Eq)]
599enum ProtectedOverflowPolicy {
600 DemoteToProbation,
601 EvictLeastRecentProtected,
602}
603
604#[derive(Debug, Clone, Copy, PartialEq)]
605struct SegmentedWaypointEntry {
606 waypoint: Point2,
607 tier: LruTier,
608}
609
610#[derive(Debug, Default)]
611struct WaypointSegmentedLruCache {
612 entries: BTreeMap<(usize, usize), SegmentedWaypointEntry>,
613 probation_order: VecDeque<(usize, usize)>,
614 protected_order: VecDeque<(usize, usize)>,
615}
616
617impl WaypointSegmentedLruCache {
618 fn get(&mut self, key: (usize, usize), protected_capacity: usize) -> Option<Point2> {
619 let waypoint = self.entries.get(&key)?.waypoint;
620 let tier = self.entries.get(&key)?.tier;
621 match tier {
622 LruTier::Probation => {
623 Self::remove_from_order(&mut self.probation_order, key);
624 if let Some(entry) = self.entries.get_mut(&key) {
625 entry.tier = LruTier::Protected;
626 }
627 self.protected_order.push_back(key);
628 self.rebalance_protected(protected_capacity);
629 }
630 LruTier::Protected => {
631 Self::remove_from_order(&mut self.protected_order, key);
632 self.protected_order.push_back(key);
633 }
634 }
635 Some(waypoint)
636 }
637
638 fn insert(
639 &mut self,
640 key: (usize, usize),
641 waypoint: Point2,
642 capacity: usize,
643 protected_capacity: usize,
644 ) {
645 if let Some(entry) = self.entries.get_mut(&key) {
646 entry.waypoint = waypoint;
647 let _ = self.get(key, protected_capacity);
648 return;
649 }
650
651 self.entries.insert(
652 key,
653 SegmentedWaypointEntry {
654 waypoint,
655 tier: LruTier::Probation,
656 },
657 );
658 self.probation_order.push_back(key);
659
660 self.evict_to_capacity(capacity);
661 self.rebalance_protected(protected_capacity);
662 }
663
664 fn len(&self) -> usize {
665 self.entries.len()
666 }
667
668 fn probation_len(&self) -> usize {
669 self.probation_order.len()
670 }
671
672 fn protected_len(&self) -> usize {
673 self.protected_order.len()
674 }
675
676 fn evict_to_capacity(&mut self, capacity: usize) {
677 while self.entries.len() > capacity {
678 let evicted = self
679 .probation_order
680 .pop_front()
681 .or_else(|| self.protected_order.pop_front());
682 if let Some(key) = evicted {
683 self.entries.remove(&key);
684 } else {
685 break;
686 }
687 }
688 }
689
690 fn rebalance_protected(&mut self, protected_capacity: usize) {
691 while self.protected_order.len() > protected_capacity {
692 let Some(demoted) = self.protected_order.pop_front() else {
693 break;
694 };
695
696 if let Some(entry) = self.entries.get_mut(&demoted) {
697 entry.tier = LruTier::Probation;
698 Self::remove_from_order(&mut self.probation_order, demoted);
699 self.probation_order.push_back(demoted);
700 }
701 }
702 }
703
704 fn remove_from_order(order: &mut VecDeque<(usize, usize)>, key: (usize, usize)) {
705 if let Some(position) = order.iter().position(|entry| *entry == key) {
706 let _ = order.remove(position);
707 }
708 }
709}
710
711#[derive(Debug)]
715pub struct PreparedTRAStarWaypointDatabaseTwoTierLru {
716 prepared: PreparedTRAStar,
717 capacity: usize,
718 protected_capacity: usize,
719 waypoint_lru_cache: std::sync::Mutex<WaypointSegmentedLruCache>,
720}
721
722#[derive(Debug, Clone, Copy, PartialEq)]
723struct CostAwareWaypointEntry {
724 waypoint: Point2,
725 tier: LruTier,
726 cost_signal: f64,
727 probation_hits: usize,
728}
729
730#[derive(Debug, Default)]
731struct WaypointCostAwareCache {
732 entries: BTreeMap<(usize, usize), CostAwareWaypointEntry>,
733 probation_order: VecDeque<(usize, usize)>,
734 protected_order: VecDeque<(usize, usize)>,
735}
736
737impl WaypointCostAwareCache {
738 fn get(
739 &mut self,
740 key: (usize, usize),
741 protected_capacity: usize,
742 promotion_hits_required: usize,
743 overflow_policy: ProtectedOverflowPolicy,
744 ) -> Option<Point2> {
745 let entry = self.entries.get(&key).copied()?;
746 match entry.tier {
747 LruTier::Probation => {
748 Self::remove_from_order(&mut self.probation_order, key);
749 if entry.probation_hits + 1 >= promotion_hits_required {
750 if let Some(stored) = self.entries.get_mut(&key) {
751 stored.tier = LruTier::Protected;
752 stored.probation_hits = 0;
753 }
754 self.protected_order.push_back(key);
755 self.rebalance_protected(protected_capacity, overflow_policy);
756 } else {
757 if let Some(stored) = self.entries.get_mut(&key) {
758 stored.probation_hits += 1;
759 }
760 self.probation_order.push_back(key);
761 }
762 }
763 LruTier::Protected => {
764 Self::remove_from_order(&mut self.protected_order, key);
765 self.protected_order.push_back(key);
766 }
767 }
768 Some(entry.waypoint)
769 }
770
771 fn insert(
772 &mut self,
773 key: (usize, usize),
774 waypoint: Point2,
775 cost_signal: f64,
776 capacity: usize,
777 protected_capacity: usize,
778 ) {
779 if let Some(entry) = self.entries.get_mut(&key) {
780 entry.waypoint = waypoint;
781 entry.cost_signal = cost_signal;
782 let _ = self.get(
783 key,
784 protected_capacity,
785 1,
786 ProtectedOverflowPolicy::DemoteToProbation,
787 );
788 return;
789 }
790
791 self.entries.insert(
792 key,
793 CostAwareWaypointEntry {
794 waypoint,
795 tier: LruTier::Probation,
796 cost_signal,
797 probation_hits: 0,
798 },
799 );
800 self.probation_order.push_back(key);
801
802 self.evict_to_capacity(capacity);
803 self.rebalance_protected(
804 protected_capacity,
805 ProtectedOverflowPolicy::DemoteToProbation,
806 );
807 }
808
809 fn len(&self) -> usize {
810 self.entries.len()
811 }
812
813 fn probation_len(&self) -> usize {
814 self.probation_order.len()
815 }
816
817 fn protected_len(&self) -> usize {
818 self.protected_order.len()
819 }
820
821 fn evict_to_capacity(&mut self, capacity: usize) {
822 while self.entries.len() > capacity {
823 self.evict_one();
824 }
825 }
826
827 fn evict_one(&mut self) {
828 let from_probation = !self.probation_order.is_empty();
829 let order = if from_probation {
830 &self.probation_order
831 } else {
832 &self.protected_order
833 };
834
835 let Some(key) = self.select_eviction_candidate(order) else {
836 return;
837 };
838
839 if from_probation {
840 Self::remove_from_order(&mut self.probation_order, key);
841 } else {
842 Self::remove_from_order(&mut self.protected_order, key);
843 }
844 self.entries.remove(&key);
845 }
846
847 fn select_eviction_candidate(
848 &self,
849 order: &VecDeque<(usize, usize)>,
850 ) -> Option<(usize, usize)> {
851 let len = order.len();
852 order
853 .iter()
854 .copied()
855 .enumerate()
856 .filter_map(|(index, key)| {
857 self.entries.get(&key).map(|entry| {
858 let recency_priority = (len.saturating_sub(index)) as f64 / len.max(1) as f64;
859 let low_cost_priority = 1.0 / (1.0 + entry.cost_signal);
860 let eviction_priority = recency_priority * (1.0 - COST_AWARE_EVICTION_WEIGHT)
861 + low_cost_priority * COST_AWARE_EVICTION_WEIGHT;
862 (key, eviction_priority)
863 })
864 })
865 .max_by(|(_, left), (_, right)| left.total_cmp(right))
866 .map(|(key, _)| key)
867 }
868
869 fn rebalance_protected(
870 &mut self,
871 protected_capacity: usize,
872 overflow_policy: ProtectedOverflowPolicy,
873 ) {
874 while self.protected_order.len() > protected_capacity {
875 let Some(demoted) = self.protected_order.pop_front() else {
876 break;
877 };
878
879 match overflow_policy {
880 ProtectedOverflowPolicy::DemoteToProbation => {
881 if let Some(entry) = self.entries.get_mut(&demoted) {
882 entry.tier = LruTier::Probation;
883 entry.probation_hits = 0;
884 Self::remove_from_order(&mut self.probation_order, demoted);
885 self.probation_order.push_back(demoted);
886 }
887 }
888 ProtectedOverflowPolicy::EvictLeastRecentProtected => {
889 self.entries.remove(&demoted);
890 }
891 }
892 }
893 }
894
895 fn remove_from_order(order: &mut VecDeque<(usize, usize)>, key: (usize, usize)) {
896 if let Some(position) = order.iter().position(|entry| *entry == key) {
897 let _ = order.remove(position);
898 }
899 }
900}
901
902#[derive(Debug)]
904pub struct PreparedTRAStarWaypointDatabaseCostAwareEviction {
905 prepared: PreparedTRAStar,
906 capacity: usize,
907 protected_capacity: usize,
908 waypoint_cost_cache: std::sync::Mutex<WaypointCostAwareCache>,
909}
910
911#[derive(Debug)]
915pub struct PreparedTRAStarWaypointDatabaseFixedAdmissionThreshold {
916 prepared: PreparedTRAStar,
917 capacity: usize,
918 protected_capacity: usize,
919 admission_threshold: f64,
920 waypoint_cost_cache: std::sync::Mutex<WaypointCostAwareCache>,
921}
922
923#[derive(Debug)]
927pub struct PreparedTRAStarWaypointDatabaseFixedPromotionRule {
928 prepared: PreparedTRAStar,
929 capacity: usize,
930 protected_capacity: usize,
931 admission_threshold: f64,
932 promotion_hits_required: usize,
933 waypoint_cost_cache: std::sync::Mutex<WaypointCostAwareCache>,
934}
935
936#[derive(Debug)]
940pub struct PreparedTRAStarWaypointDatabaseFixedDemotionRule {
941 prepared: PreparedTRAStar,
942 capacity: usize,
943 protected_capacity: usize,
944 admission_threshold: f64,
945 promotion_hits_required: usize,
946 waypoint_cost_cache: std::sync::Mutex<WaypointCostAwareCache>,
947}
948
949#[derive(Debug)]
953pub struct PreparedTRAStarWaypointDatabasePolicyProfile {
954 profile: TRAStarWaypointDatabasePolicyProfile,
955 waypoint_policy: PreparedTRAStarWaypointDatabaseFixedDemotionRule,
956}
957
958impl PreparedTRAStarWaypointDatabaseStatic {
959 #[must_use]
961 pub fn builder() -> TRAStarWaypointDatabaseStaticBuilder {
962 TRAStarWaypointDatabaseStaticBuilder
963 }
964
965 #[must_use]
967 pub fn prepared_tra_star(&self) -> &PreparedTRAStar {
968 &self.prepared
969 }
970
971 #[must_use]
973 pub fn waypoint_entry_count(&self) -> usize {
974 self.waypoint_database
975 .values()
976 .map(std::vec::Vec::len)
977 .sum()
978 }
979
980 pub fn search(&self, query: NavmeshQuery) -> NavmeshSearchResult {
982 search_with_midpoint_seed(self, query, |corridor, cells| {
983 self.midpoint_seed_from_waypoint_database(cells, corridor)
984 })
985 }
986
987 fn from_prepared(prepared: PreparedTRAStar) -> Self {
988 let mut waypoint_database = BTreeMap::new();
989
990 for cell_index in 0..prepared.navmesh().cells().len() {
991 let Some(prepared_neighbors) = prepared.neighbors(cell_index) else {
992 continue;
993 };
994
995 let mut neighbors = prepared_neighbors.to_vec();
996 neighbors.sort_unstable();
997
998 let entries = neighbors
999 .into_iter()
1000 .filter_map(|neighbor| {
1001 prepared
1002 .portal_between(cell_index, neighbor)
1003 .map(|portal| CellWaypointEntry {
1004 to_cell: neighbor,
1005 waypoint: crate::algorithms::channel_search::portal_midpoint(&portal),
1006 })
1007 })
1008 .collect::<Vec<_>>();
1009
1010 if !entries.is_empty() {
1011 waypoint_database.insert(cell_index, entries);
1012 }
1013 }
1014
1015 Self {
1016 prepared,
1017 waypoint_database,
1018 }
1019 }
1020
1021 fn lookup_waypoint(&self, from_cell: usize, to_cell: usize) -> Option<Point2> {
1022 self.waypoint_database.get(&from_cell).and_then(|entries| {
1023 entries
1024 .iter()
1025 .find_map(|entry| (entry.to_cell == to_cell).then_some(entry.waypoint))
1026 })
1027 }
1028
1029 fn midpoint_seed_from_waypoint_database(
1030 &self,
1031 cells: &[usize],
1032 corridor: &crate::navmesh::corridor::NavmeshCorridor,
1033 ) -> Vec<Point2> {
1034 cells
1035 .windows(2)
1036 .enumerate()
1037 .map(|(index, pair)| {
1038 self.lookup_waypoint(pair[0], pair[1])
1039 .or_else(|| {
1040 corridor.portals.get(index).copied().map(|portal| {
1041 crate::algorithms::channel_search::portal_midpoint(&portal)
1042 })
1043 })
1044 .unwrap_or(corridor.goal)
1045 })
1046 .collect()
1047 }
1048}
1049
1050impl PreparedTRAStarWaypointDatabaseLazyQuery {
1051 #[must_use]
1053 pub fn builder() -> TRAStarWaypointDatabaseLazyQueryBuilder {
1054 TRAStarWaypointDatabaseLazyQueryBuilder
1055 }
1056
1057 #[must_use]
1059 pub fn prepared_tra_star(&self) -> &PreparedTRAStar {
1060 &self.prepared
1061 }
1062
1063 pub fn search(&self, query: NavmeshQuery) -> NavmeshSearchResult {
1065 search_with_midpoint_seed(self, query, |corridor, cells| {
1066 self.midpoint_seed_from_lazy_waypoint_database(cells, corridor)
1067 })
1068 }
1069
1070 fn from_prepared(prepared: PreparedTRAStar) -> Self {
1071 Self { prepared }
1072 }
1073
1074 fn lookup_or_insert_lazy_waypoint(
1075 &self,
1076 query_waypoint_database: &mut BTreeMap<usize, Vec<CellWaypointEntry>>,
1077 from_cell: usize,
1078 to_cell: usize,
1079 ) -> Option<Point2> {
1080 if let Some(entries) = query_waypoint_database.get(&from_cell)
1081 && let Some(waypoint) = entries
1082 .iter()
1083 .find_map(|entry| (entry.to_cell == to_cell).then_some(entry.waypoint))
1084 {
1085 return Some(waypoint);
1086 }
1087
1088 let waypoint = self
1089 .prepared
1090 .portal_between(from_cell, to_cell)
1091 .map(|portal| crate::algorithms::channel_search::portal_midpoint(&portal))?;
1092
1093 query_waypoint_database
1094 .entry(from_cell)
1095 .or_default()
1096 .push(CellWaypointEntry { to_cell, waypoint });
1097
1098 Some(waypoint)
1099 }
1100
1101 fn midpoint_seed_from_lazy_waypoint_database(
1102 &self,
1103 cells: &[usize],
1104 corridor: &crate::navmesh::corridor::NavmeshCorridor,
1105 ) -> Vec<Point2> {
1106 let mut query_waypoint_database: BTreeMap<usize, Vec<CellWaypointEntry>> = BTreeMap::new();
1107
1108 cells
1109 .windows(2)
1110 .enumerate()
1111 .map(|(index, pair)| {
1112 self.lookup_or_insert_lazy_waypoint(&mut query_waypoint_database, pair[0], pair[1])
1113 .or_else(|| {
1114 corridor.portals.get(index).copied().map(|portal| {
1115 crate::algorithms::channel_search::portal_midpoint(&portal)
1116 })
1117 })
1118 .unwrap_or(corridor.goal)
1119 })
1120 .collect()
1121 }
1122}
1123
1124impl PreparedTRAStarWaypointDatabaseAdaptiveLru {
1125 #[must_use]
1127 pub fn builder() -> TRAStarWaypointDatabaseAdaptiveLruBuilder {
1128 TRAStarWaypointDatabaseAdaptiveLruBuilder
1129 }
1130
1131 #[must_use]
1133 pub fn prepared_tra_star(&self) -> &PreparedTRAStar {
1134 &self.prepared
1135 }
1136
1137 #[must_use]
1139 pub fn lru_capacity(&self) -> usize {
1140 self.capacity
1141 }
1142
1143 #[must_use]
1145 pub fn retained_waypoint_count(&self) -> usize {
1146 self.waypoint_lru_cache
1147 .lock()
1148 .expect("adaptive waypoint cache lock should not be poisoned")
1149 .len()
1150 }
1151
1152 pub fn search(&self, query: NavmeshQuery) -> NavmeshSearchResult {
1154 search_with_midpoint_seed(self, query, |corridor, cells| {
1155 self.midpoint_seed_from_adaptive_lru(cells, corridor)
1156 })
1157 }
1158
1159 fn from_prepared(prepared: PreparedTRAStar, capacity: usize) -> Self {
1160 Self {
1161 prepared,
1162 capacity,
1163 waypoint_lru_cache: std::sync::Mutex::new(WaypointLruCache::default()),
1164 }
1165 }
1166
1167 fn lookup_or_insert_waypoint(&self, from_cell: usize, to_cell: usize) -> Option<Point2> {
1168 let mut cache = self
1169 .waypoint_lru_cache
1170 .lock()
1171 .expect("adaptive waypoint cache lock should not be poisoned");
1172 let key = (from_cell, to_cell);
1173 if let Some(waypoint) = cache.get(key) {
1174 return Some(waypoint);
1175 }
1176
1177 let waypoint = self
1178 .prepared
1179 .portal_between(from_cell, to_cell)
1180 .map(|portal| crate::algorithms::channel_search::portal_midpoint(&portal))?;
1181 cache.insert(key, waypoint, self.capacity);
1182 Some(waypoint)
1183 }
1184
1185 fn midpoint_seed_from_adaptive_lru(
1186 &self,
1187 cells: &[usize],
1188 corridor: &crate::navmesh::corridor::NavmeshCorridor,
1189 ) -> Vec<Point2> {
1190 cells
1191 .windows(2)
1192 .enumerate()
1193 .map(|(index, pair)| {
1194 self.lookup_or_insert_waypoint(pair[0], pair[1])
1195 .or_else(|| {
1196 corridor.portals.get(index).copied().map(|portal| {
1197 crate::algorithms::channel_search::portal_midpoint(&portal)
1198 })
1199 })
1200 .unwrap_or(corridor.goal)
1201 })
1202 .collect()
1203 }
1204}
1205
1206impl PreparedTRAStarWaypointDatabaseTwoTierLru {
1207 #[must_use]
1209 pub fn builder() -> TRAStarWaypointDatabaseTwoTierLruBuilder {
1210 TRAStarWaypointDatabaseTwoTierLruBuilder
1211 }
1212
1213 #[must_use]
1215 pub fn prepared_tra_star(&self) -> &PreparedTRAStar {
1216 &self.prepared
1217 }
1218
1219 #[must_use]
1221 pub fn lru_capacity(&self) -> usize {
1222 self.capacity
1223 }
1224
1225 #[must_use]
1227 pub fn protected_segment_capacity(&self) -> usize {
1228 self.protected_capacity
1229 }
1230
1231 #[must_use]
1233 pub fn retained_waypoint_count(&self) -> usize {
1234 self.waypoint_lru_cache
1235 .lock()
1236 .expect("segmented waypoint cache lock should not be poisoned")
1237 .len()
1238 }
1239
1240 #[must_use]
1242 pub fn retained_probation_count(&self) -> usize {
1243 self.waypoint_lru_cache
1244 .lock()
1245 .expect("segmented waypoint cache lock should not be poisoned")
1246 .probation_len()
1247 }
1248
1249 #[must_use]
1251 pub fn retained_protected_count(&self) -> usize {
1252 self.waypoint_lru_cache
1253 .lock()
1254 .expect("segmented waypoint cache lock should not be poisoned")
1255 .protected_len()
1256 }
1257
1258 pub fn search(&self, query: NavmeshQuery) -> NavmeshSearchResult {
1260 search_with_midpoint_seed(self, query, |corridor, cells| {
1261 self.midpoint_seed_from_two_tier_lru(cells, corridor)
1262 })
1263 }
1264
1265 fn from_prepared(prepared: PreparedTRAStar, capacity: usize) -> Self {
1266 let protected_capacity = capacity / 2;
1267 Self {
1268 prepared,
1269 capacity,
1270 protected_capacity,
1271 waypoint_lru_cache: std::sync::Mutex::new(WaypointSegmentedLruCache::default()),
1272 }
1273 }
1274
1275 fn lookup_or_insert_waypoint(&self, from_cell: usize, to_cell: usize) -> Option<Point2> {
1276 let mut cache = self
1277 .waypoint_lru_cache
1278 .lock()
1279 .expect("segmented waypoint cache lock should not be poisoned");
1280 let key = (from_cell, to_cell);
1281 if let Some(waypoint) = cache.get(key, self.protected_capacity) {
1282 return Some(waypoint);
1283 }
1284
1285 let waypoint = self
1286 .prepared
1287 .portal_between(from_cell, to_cell)
1288 .map(|portal| crate::algorithms::channel_search::portal_midpoint(&portal))?;
1289 cache.insert(key, waypoint, self.capacity, self.protected_capacity);
1290 Some(waypoint)
1291 }
1292
1293 fn midpoint_seed_from_two_tier_lru(
1294 &self,
1295 cells: &[usize],
1296 corridor: &crate::navmesh::corridor::NavmeshCorridor,
1297 ) -> Vec<Point2> {
1298 cells
1299 .windows(2)
1300 .enumerate()
1301 .map(|(index, pair)| {
1302 self.lookup_or_insert_waypoint(pair[0], pair[1])
1303 .or_else(|| {
1304 corridor.portals.get(index).copied().map(|portal| {
1305 crate::algorithms::channel_search::portal_midpoint(&portal)
1306 })
1307 })
1308 .unwrap_or(corridor.goal)
1309 })
1310 .collect()
1311 }
1312}
1313
1314impl PreparedTRAStarWaypointDatabaseCostAwareEviction {
1315 #[must_use]
1317 pub fn builder() -> TRAStarWaypointDatabaseCostAwareEvictionBuilder {
1318 TRAStarWaypointDatabaseCostAwareEvictionBuilder
1319 }
1320
1321 #[must_use]
1323 pub fn prepared_tra_star(&self) -> &PreparedTRAStar {
1324 &self.prepared
1325 }
1326
1327 #[must_use]
1329 pub fn eviction_capacity(&self) -> usize {
1330 self.capacity
1331 }
1332
1333 #[must_use]
1335 pub fn protected_segment_capacity(&self) -> usize {
1336 self.protected_capacity
1337 }
1338
1339 #[must_use]
1341 pub fn retained_waypoint_count(&self) -> usize {
1342 self.waypoint_cost_cache
1343 .lock()
1344 .expect("cost-aware waypoint cache lock should not be poisoned")
1345 .len()
1346 }
1347
1348 #[must_use]
1350 pub fn retained_probation_count(&self) -> usize {
1351 self.waypoint_cost_cache
1352 .lock()
1353 .expect("cost-aware waypoint cache lock should not be poisoned")
1354 .probation_len()
1355 }
1356
1357 #[must_use]
1359 pub fn retained_protected_count(&self) -> usize {
1360 self.waypoint_cost_cache
1361 .lock()
1362 .expect("cost-aware waypoint cache lock should not be poisoned")
1363 .protected_len()
1364 }
1365
1366 pub fn search(&self, query: NavmeshQuery) -> NavmeshSearchResult {
1368 search_with_midpoint_seed(self, query, |corridor, cells| {
1369 self.midpoint_seed_from_cost_aware_eviction(cells, corridor)
1370 })
1371 }
1372
1373 fn from_prepared(prepared: PreparedTRAStar, capacity: usize) -> Self {
1374 let protected_capacity = capacity / 2;
1375 Self {
1376 prepared,
1377 capacity,
1378 protected_capacity,
1379 waypoint_cost_cache: std::sync::Mutex::new(WaypointCostAwareCache::default()),
1380 }
1381 }
1382
1383 fn lookup_or_insert_waypoint(&self, from_cell: usize, to_cell: usize) -> Option<Point2> {
1384 let mut cache = self
1385 .waypoint_cost_cache
1386 .lock()
1387 .expect("cost-aware waypoint cache lock should not be poisoned");
1388 let key = (from_cell, to_cell);
1389 if let Some(waypoint) = cache.get(
1390 key,
1391 self.protected_capacity,
1392 1,
1393 ProtectedOverflowPolicy::DemoteToProbation,
1394 ) {
1395 return Some(waypoint);
1396 }
1397
1398 let portal = self.prepared.portal_between(from_cell, to_cell)?;
1399 let waypoint = crate::algorithms::channel_search::portal_midpoint(&portal);
1400 let cost_signal = segment_cost(portal.start, portal.end);
1401 cache.insert(
1402 key,
1403 waypoint,
1404 cost_signal,
1405 self.capacity,
1406 self.protected_capacity,
1407 );
1408 Some(waypoint)
1409 }
1410
1411 fn midpoint_seed_from_cost_aware_eviction(
1412 &self,
1413 cells: &[usize],
1414 corridor: &crate::navmesh::corridor::NavmeshCorridor,
1415 ) -> Vec<Point2> {
1416 cells
1417 .windows(2)
1418 .enumerate()
1419 .map(|(index, pair)| {
1420 self.lookup_or_insert_waypoint(pair[0], pair[1])
1421 .or_else(|| {
1422 corridor.portals.get(index).copied().map(|portal| {
1423 crate::algorithms::channel_search::portal_midpoint(&portal)
1424 })
1425 })
1426 .unwrap_or(corridor.goal)
1427 })
1428 .collect()
1429 }
1430}
1431
1432impl PreparedTRAStarWaypointDatabaseFixedAdmissionThreshold {
1433 #[must_use]
1435 pub fn builder() -> TRAStarWaypointDatabaseFixedAdmissionThresholdBuilder {
1436 TRAStarWaypointDatabaseFixedAdmissionThresholdBuilder
1437 }
1438
1439 #[must_use]
1441 pub fn prepared_tra_star(&self) -> &PreparedTRAStar {
1442 &self.prepared
1443 }
1444
1445 #[must_use]
1447 pub fn admission_cost_threshold(&self) -> f64 {
1448 self.admission_threshold
1449 }
1450
1451 #[must_use]
1453 pub fn eviction_capacity(&self) -> usize {
1454 self.capacity
1455 }
1456
1457 #[must_use]
1459 pub fn protected_segment_capacity(&self) -> usize {
1460 self.protected_capacity
1461 }
1462
1463 #[must_use]
1465 pub fn retained_waypoint_count(&self) -> usize {
1466 self.waypoint_cost_cache
1467 .lock()
1468 .expect("fixed-threshold waypoint cache lock should not be poisoned")
1469 .len()
1470 }
1471
1472 #[must_use]
1474 pub fn retained_probation_count(&self) -> usize {
1475 self.waypoint_cost_cache
1476 .lock()
1477 .expect("fixed-threshold waypoint cache lock should not be poisoned")
1478 .probation_len()
1479 }
1480
1481 #[must_use]
1483 pub fn retained_protected_count(&self) -> usize {
1484 self.waypoint_cost_cache
1485 .lock()
1486 .expect("fixed-threshold waypoint cache lock should not be poisoned")
1487 .protected_len()
1488 }
1489
1490 pub fn search(&self, query: NavmeshQuery) -> NavmeshSearchResult {
1492 search_with_midpoint_seed(self, query, |corridor, cells| {
1493 self.midpoint_seed_from_fixed_admission_threshold(cells, corridor)
1494 })
1495 }
1496
1497 fn from_prepared(prepared: PreparedTRAStar, capacity: usize, admission_threshold: f64) -> Self {
1498 let protected_capacity = capacity / 2;
1499 Self {
1500 prepared,
1501 capacity,
1502 protected_capacity,
1503 admission_threshold,
1504 waypoint_cost_cache: std::sync::Mutex::new(WaypointCostAwareCache::default()),
1505 }
1506 }
1507
1508 fn lookup_or_insert_waypoint(&self, from_cell: usize, to_cell: usize) -> Option<Point2> {
1509 let mut cache = self
1510 .waypoint_cost_cache
1511 .lock()
1512 .expect("fixed-threshold waypoint cache lock should not be poisoned");
1513 let key = (from_cell, to_cell);
1514 if let Some(waypoint) = cache.get(
1515 key,
1516 self.protected_capacity,
1517 1,
1518 ProtectedOverflowPolicy::DemoteToProbation,
1519 ) {
1520 return Some(waypoint);
1521 }
1522
1523 let portal = self.prepared.portal_between(from_cell, to_cell)?;
1524 let waypoint = crate::algorithms::channel_search::portal_midpoint(&portal);
1525 let cost_signal = segment_cost(portal.start, portal.end);
1526 if cost_signal < self.admission_threshold {
1527 return Some(waypoint);
1528 }
1529
1530 cache.insert(
1531 key,
1532 waypoint,
1533 cost_signal,
1534 self.capacity,
1535 self.protected_capacity,
1536 );
1537 Some(waypoint)
1538 }
1539
1540 fn midpoint_seed_from_fixed_admission_threshold(
1541 &self,
1542 cells: &[usize],
1543 corridor: &crate::navmesh::corridor::NavmeshCorridor,
1544 ) -> Vec<Point2> {
1545 cells
1546 .windows(2)
1547 .enumerate()
1548 .map(|(index, pair)| {
1549 self.lookup_or_insert_waypoint(pair[0], pair[1])
1550 .or_else(|| {
1551 corridor.portals.get(index).copied().map(|portal| {
1552 crate::algorithms::channel_search::portal_midpoint(&portal)
1553 })
1554 })
1555 .unwrap_or(corridor.goal)
1556 })
1557 .collect()
1558 }
1559}
1560
1561impl PreparedTRAStarWaypointDatabaseFixedPromotionRule {
1562 #[must_use]
1564 pub fn builder() -> TRAStarWaypointDatabaseFixedPromotionRuleBuilder {
1565 TRAStarWaypointDatabaseFixedPromotionRuleBuilder
1566 }
1567
1568 #[must_use]
1570 pub fn prepared_tra_star(&self) -> &PreparedTRAStar {
1571 &self.prepared
1572 }
1573
1574 #[must_use]
1576 pub fn admission_cost_threshold(&self) -> f64 {
1577 self.admission_threshold
1578 }
1579
1580 #[must_use]
1582 pub fn promotion_hits_required(&self) -> usize {
1583 self.promotion_hits_required
1584 }
1585
1586 #[must_use]
1588 pub fn eviction_capacity(&self) -> usize {
1589 self.capacity
1590 }
1591
1592 #[must_use]
1594 pub fn protected_segment_capacity(&self) -> usize {
1595 self.protected_capacity
1596 }
1597
1598 #[must_use]
1600 pub fn retained_waypoint_count(&self) -> usize {
1601 self.waypoint_cost_cache
1602 .lock()
1603 .expect("fixed-promotion waypoint cache lock should not be poisoned")
1604 .len()
1605 }
1606
1607 #[must_use]
1609 pub fn retained_probation_count(&self) -> usize {
1610 self.waypoint_cost_cache
1611 .lock()
1612 .expect("fixed-promotion waypoint cache lock should not be poisoned")
1613 .probation_len()
1614 }
1615
1616 #[must_use]
1618 pub fn retained_protected_count(&self) -> usize {
1619 self.waypoint_cost_cache
1620 .lock()
1621 .expect("fixed-promotion waypoint cache lock should not be poisoned")
1622 .protected_len()
1623 }
1624
1625 pub fn search(&self, query: NavmeshQuery) -> NavmeshSearchResult {
1627 search_with_midpoint_seed(self, query, |corridor, cells| {
1628 self.midpoint_seed_from_fixed_promotion_rule(cells, corridor)
1629 })
1630 }
1631
1632 fn from_prepared(
1633 prepared: PreparedTRAStar,
1634 capacity: usize,
1635 admission_threshold: f64,
1636 promotion_hits_required: usize,
1637 ) -> Self {
1638 let protected_capacity = capacity / 2;
1639 Self {
1640 prepared,
1641 capacity,
1642 protected_capacity,
1643 admission_threshold,
1644 promotion_hits_required,
1645 waypoint_cost_cache: std::sync::Mutex::new(WaypointCostAwareCache::default()),
1646 }
1647 }
1648
1649 fn lookup_or_insert_waypoint(&self, from_cell: usize, to_cell: usize) -> Option<Point2> {
1650 let mut cache = self
1651 .waypoint_cost_cache
1652 .lock()
1653 .expect("fixed-promotion waypoint cache lock should not be poisoned");
1654 let key = (from_cell, to_cell);
1655 if let Some(waypoint) = cache.get(
1656 key,
1657 self.protected_capacity,
1658 self.promotion_hits_required,
1659 ProtectedOverflowPolicy::DemoteToProbation,
1660 ) {
1661 return Some(waypoint);
1662 }
1663
1664 let portal = self.prepared.portal_between(from_cell, to_cell)?;
1665 let waypoint = crate::algorithms::channel_search::portal_midpoint(&portal);
1666 let cost_signal = segment_cost(portal.start, portal.end);
1667 if cost_signal < self.admission_threshold {
1668 return Some(waypoint);
1669 }
1670
1671 cache.insert(
1672 key,
1673 waypoint,
1674 cost_signal,
1675 self.capacity,
1676 self.protected_capacity,
1677 );
1678 Some(waypoint)
1679 }
1680
1681 fn midpoint_seed_from_fixed_promotion_rule(
1682 &self,
1683 cells: &[usize],
1684 corridor: &crate::navmesh::corridor::NavmeshCorridor,
1685 ) -> Vec<Point2> {
1686 cells
1687 .windows(2)
1688 .enumerate()
1689 .map(|(index, pair)| {
1690 self.lookup_or_insert_waypoint(pair[0], pair[1])
1691 .or_else(|| {
1692 corridor.portals.get(index).copied().map(|portal| {
1693 crate::algorithms::channel_search::portal_midpoint(&portal)
1694 })
1695 })
1696 .unwrap_or(corridor.goal)
1697 })
1698 .collect()
1699 }
1700}
1701
1702impl PreparedTRAStarWaypointDatabaseFixedDemotionRule {
1703 #[must_use]
1705 pub fn builder() -> TRAStarWaypointDatabaseFixedDemotionRuleBuilder {
1706 TRAStarWaypointDatabaseFixedDemotionRuleBuilder
1707 }
1708
1709 #[must_use]
1711 pub fn prepared_tra_star(&self) -> &PreparedTRAStar {
1712 &self.prepared
1713 }
1714
1715 #[must_use]
1717 pub fn admission_cost_threshold(&self) -> f64 {
1718 self.admission_threshold
1719 }
1720
1721 #[must_use]
1723 pub fn promotion_hits_required(&self) -> usize {
1724 self.promotion_hits_required
1725 }
1726
1727 #[must_use]
1729 pub fn eviction_capacity(&self) -> usize {
1730 self.capacity
1731 }
1732
1733 #[must_use]
1735 pub fn protected_segment_capacity(&self) -> usize {
1736 self.protected_capacity
1737 }
1738
1739 #[must_use]
1741 pub fn retained_waypoint_count(&self) -> usize {
1742 self.waypoint_cost_cache
1743 .lock()
1744 .expect("fixed-demotion waypoint cache lock should not be poisoned")
1745 .len()
1746 }
1747
1748 #[must_use]
1750 pub fn retained_probation_count(&self) -> usize {
1751 self.waypoint_cost_cache
1752 .lock()
1753 .expect("fixed-demotion waypoint cache lock should not be poisoned")
1754 .probation_len()
1755 }
1756
1757 #[must_use]
1759 pub fn retained_protected_count(&self) -> usize {
1760 self.waypoint_cost_cache
1761 .lock()
1762 .expect("fixed-demotion waypoint cache lock should not be poisoned")
1763 .protected_len()
1764 }
1765
1766 pub fn search(&self, query: NavmeshQuery) -> NavmeshSearchResult {
1768 search_with_midpoint_seed(self, query, |corridor, cells| {
1769 self.midpoint_seed_from_fixed_demotion_rule(cells, corridor)
1770 })
1771 }
1772
1773 fn from_prepared(
1774 prepared: PreparedTRAStar,
1775 capacity: usize,
1776 admission_threshold: f64,
1777 promotion_hits_required: usize,
1778 ) -> Self {
1779 let protected_capacity = capacity / 2;
1780 Self {
1781 prepared,
1782 capacity,
1783 protected_capacity,
1784 admission_threshold,
1785 promotion_hits_required,
1786 waypoint_cost_cache: std::sync::Mutex::new(WaypointCostAwareCache::default()),
1787 }
1788 }
1789
1790 fn lookup_or_insert_waypoint(&self, from_cell: usize, to_cell: usize) -> Option<Point2> {
1791 let mut cache = self
1792 .waypoint_cost_cache
1793 .lock()
1794 .expect("fixed-demotion waypoint cache lock should not be poisoned");
1795 let key = (from_cell, to_cell);
1796 if let Some(waypoint) = cache.get(
1797 key,
1798 self.protected_capacity,
1799 self.promotion_hits_required,
1800 ProtectedOverflowPolicy::EvictLeastRecentProtected,
1801 ) {
1802 return Some(waypoint);
1803 }
1804
1805 let portal = self.prepared.portal_between(from_cell, to_cell)?;
1806 let waypoint = crate::algorithms::channel_search::portal_midpoint(&portal);
1807 let cost_signal = segment_cost(portal.start, portal.end);
1808 if cost_signal < self.admission_threshold {
1809 return Some(waypoint);
1810 }
1811
1812 cache.insert(
1813 key,
1814 waypoint,
1815 cost_signal,
1816 self.capacity,
1817 self.protected_capacity,
1818 );
1819 Some(waypoint)
1820 }
1821
1822 fn midpoint_seed_from_fixed_demotion_rule(
1823 &self,
1824 cells: &[usize],
1825 corridor: &crate::navmesh::corridor::NavmeshCorridor,
1826 ) -> Vec<Point2> {
1827 cells
1828 .windows(2)
1829 .enumerate()
1830 .map(|(index, pair)| {
1831 self.lookup_or_insert_waypoint(pair[0], pair[1])
1832 .or_else(|| {
1833 corridor.portals.get(index).copied().map(|portal| {
1834 crate::algorithms::channel_search::portal_midpoint(&portal)
1835 })
1836 })
1837 .unwrap_or(corridor.goal)
1838 })
1839 .collect()
1840 }
1841}
1842
1843impl PreparedTRAStarWaypointDatabasePolicyProfile {
1844 #[must_use]
1846 pub fn builder() -> TRAStarWaypointDatabasePolicyProfileBuilder {
1847 TRAStarWaypointDatabasePolicyProfileBuilder::default()
1848 }
1849
1850 #[must_use]
1852 pub fn policy_profile(&self) -> TRAStarWaypointDatabasePolicyProfile {
1853 self.profile
1854 }
1855
1856 #[must_use]
1858 pub fn prepared_tra_star(&self) -> &PreparedTRAStar {
1859 self.waypoint_policy.prepared_tra_star()
1860 }
1861
1862 #[must_use]
1864 pub fn admission_cost_threshold(&self) -> f64 {
1865 self.waypoint_policy.admission_cost_threshold()
1866 }
1867
1868 #[must_use]
1870 pub fn promotion_hits_required(&self) -> usize {
1871 self.waypoint_policy.promotion_hits_required()
1872 }
1873
1874 #[must_use]
1876 pub fn eviction_capacity(&self) -> usize {
1877 self.waypoint_policy.eviction_capacity()
1878 }
1879
1880 #[must_use]
1882 pub fn protected_segment_capacity(&self) -> usize {
1883 self.waypoint_policy.protected_segment_capacity()
1884 }
1885
1886 #[must_use]
1888 pub fn retained_waypoint_count(&self) -> usize {
1889 self.waypoint_policy.retained_waypoint_count()
1890 }
1891
1892 #[must_use]
1894 pub fn retained_probation_count(&self) -> usize {
1895 self.waypoint_policy.retained_probation_count()
1896 }
1897
1898 #[must_use]
1900 pub fn retained_protected_count(&self) -> usize {
1901 self.waypoint_policy.retained_protected_count()
1902 }
1903
1904 pub fn search(&self, query: NavmeshQuery) -> NavmeshSearchResult {
1906 self.waypoint_policy.search(query)
1907 }
1908
1909 fn from_prepared(
1910 prepared: PreparedTRAStar,
1911 profile: TRAStarWaypointDatabasePolicyProfile,
1912 ) -> Self {
1913 let waypoint_policy = match profile {
1914 TRAStarWaypointDatabasePolicyProfile::V1 => {
1915 PreparedTRAStarWaypointDatabaseFixedDemotionRule::from_prepared(
1916 prepared,
1917 ADAPTIVE_LRU_CAPACITY,
1918 profile.admission_cost_threshold(),
1919 profile.promotion_hits_required(),
1920 )
1921 }
1922 };
1923 Self {
1924 profile,
1925 waypoint_policy,
1926 }
1927 }
1928}
1929
1930impl PreparedNavmesh for PreparedTRAStarWaypointDatabaseStatic {
1931 fn name(&self) -> &'static str {
1932 "tra-star-waypoint-database-static"
1933 }
1934
1935 fn navmesh(&self) -> &Navmesh {
1936 self.prepared.navmesh()
1937 }
1938
1939 fn neighbors(&self, cell_index: usize) -> Option<&[usize]> {
1940 self.prepared.neighbors(cell_index)
1941 }
1942
1943 fn portals_from(&self, cell_index: usize) -> Option<&[crate::NavmeshPortal]> {
1944 self.prepared.portals_from(cell_index)
1945 }
1946
1947 fn portal_between(&self, left_cell: usize, right_cell: usize) -> Option<crate::NavmeshPortal> {
1948 self.prepared.portal_between(left_cell, right_cell)
1949 }
1950}
1951
1952impl PreparedNavmesh for PreparedTRAStarWaypointDatabaseLazyQuery {
1953 fn name(&self) -> &'static str {
1954 "tra-star-waypoint-database-lazy-query"
1955 }
1956
1957 fn navmesh(&self) -> &Navmesh {
1958 self.prepared.navmesh()
1959 }
1960
1961 fn neighbors(&self, cell_index: usize) -> Option<&[usize]> {
1962 self.prepared.neighbors(cell_index)
1963 }
1964
1965 fn portals_from(&self, cell_index: usize) -> Option<&[crate::NavmeshPortal]> {
1966 self.prepared.portals_from(cell_index)
1967 }
1968
1969 fn portal_between(&self, left_cell: usize, right_cell: usize) -> Option<crate::NavmeshPortal> {
1970 self.prepared.portal_between(left_cell, right_cell)
1971 }
1972}
1973
1974impl PreparedNavmesh for PreparedTRAStarWaypointDatabaseAdaptiveLru {
1975 fn name(&self) -> &'static str {
1976 "tra-star-waypoint-database-adaptive-lru"
1977 }
1978
1979 fn navmesh(&self) -> &Navmesh {
1980 self.prepared.navmesh()
1981 }
1982
1983 fn neighbors(&self, cell_index: usize) -> Option<&[usize]> {
1984 self.prepared.neighbors(cell_index)
1985 }
1986
1987 fn portals_from(&self, cell_index: usize) -> Option<&[crate::NavmeshPortal]> {
1988 self.prepared.portals_from(cell_index)
1989 }
1990
1991 fn portal_between(&self, left_cell: usize, right_cell: usize) -> Option<crate::NavmeshPortal> {
1992 self.prepared.portal_between(left_cell, right_cell)
1993 }
1994}
1995
1996impl PreparedNavmesh for PreparedTRAStarWaypointDatabaseTwoTierLru {
1997 fn name(&self) -> &'static str {
1998 "tra-star-waypoint-database-two-tier-lru"
1999 }
2000
2001 fn navmesh(&self) -> &Navmesh {
2002 self.prepared.navmesh()
2003 }
2004
2005 fn neighbors(&self, cell_index: usize) -> Option<&[usize]> {
2006 self.prepared.neighbors(cell_index)
2007 }
2008
2009 fn portals_from(&self, cell_index: usize) -> Option<&[crate::NavmeshPortal]> {
2010 self.prepared.portals_from(cell_index)
2011 }
2012
2013 fn portal_between(&self, left_cell: usize, right_cell: usize) -> Option<crate::NavmeshPortal> {
2014 self.prepared.portal_between(left_cell, right_cell)
2015 }
2016}
2017
2018impl PreparedNavmesh for PreparedTRAStarWaypointDatabaseCostAwareEviction {
2019 fn name(&self) -> &'static str {
2020 "tra-star-waypoint-database-cost-aware-eviction"
2021 }
2022
2023 fn navmesh(&self) -> &Navmesh {
2024 self.prepared.navmesh()
2025 }
2026
2027 fn neighbors(&self, cell_index: usize) -> Option<&[usize]> {
2028 self.prepared.neighbors(cell_index)
2029 }
2030
2031 fn portals_from(&self, cell_index: usize) -> Option<&[crate::NavmeshPortal]> {
2032 self.prepared.portals_from(cell_index)
2033 }
2034
2035 fn portal_between(&self, left_cell: usize, right_cell: usize) -> Option<crate::NavmeshPortal> {
2036 self.prepared.portal_between(left_cell, right_cell)
2037 }
2038}
2039
2040impl PreparedNavmesh for PreparedTRAStarWaypointDatabaseFixedAdmissionThreshold {
2041 fn name(&self) -> &'static str {
2042 "tra-star-waypoint-database-fixed-admission-threshold"
2043 }
2044
2045 fn navmesh(&self) -> &Navmesh {
2046 self.prepared.navmesh()
2047 }
2048
2049 fn neighbors(&self, cell_index: usize) -> Option<&[usize]> {
2050 self.prepared.neighbors(cell_index)
2051 }
2052
2053 fn portals_from(&self, cell_index: usize) -> Option<&[crate::NavmeshPortal]> {
2054 self.prepared.portals_from(cell_index)
2055 }
2056
2057 fn portal_between(&self, left_cell: usize, right_cell: usize) -> Option<crate::NavmeshPortal> {
2058 self.prepared.portal_between(left_cell, right_cell)
2059 }
2060}
2061
2062impl PreparedNavmesh for PreparedTRAStarWaypointDatabaseFixedPromotionRule {
2063 fn name(&self) -> &'static str {
2064 "tra-star-waypoint-database-fixed-promotion-rule"
2065 }
2066
2067 fn navmesh(&self) -> &Navmesh {
2068 self.prepared.navmesh()
2069 }
2070
2071 fn neighbors(&self, cell_index: usize) -> Option<&[usize]> {
2072 self.prepared.neighbors(cell_index)
2073 }
2074
2075 fn portals_from(&self, cell_index: usize) -> Option<&[crate::NavmeshPortal]> {
2076 self.prepared.portals_from(cell_index)
2077 }
2078
2079 fn portal_between(&self, left_cell: usize, right_cell: usize) -> Option<crate::NavmeshPortal> {
2080 self.prepared.portal_between(left_cell, right_cell)
2081 }
2082}
2083
2084impl PreparedNavmesh for PreparedTRAStarWaypointDatabaseFixedDemotionRule {
2085 fn name(&self) -> &'static str {
2086 "tra-star-waypoint-database-fixed-demotion-rule"
2087 }
2088
2089 fn navmesh(&self) -> &Navmesh {
2090 self.prepared.navmesh()
2091 }
2092
2093 fn neighbors(&self, cell_index: usize) -> Option<&[usize]> {
2094 self.prepared.neighbors(cell_index)
2095 }
2096
2097 fn portals_from(&self, cell_index: usize) -> Option<&[crate::NavmeshPortal]> {
2098 self.prepared.portals_from(cell_index)
2099 }
2100
2101 fn portal_between(&self, left_cell: usize, right_cell: usize) -> Option<crate::NavmeshPortal> {
2102 self.prepared.portal_between(left_cell, right_cell)
2103 }
2104}
2105
2106impl PreparedNavmesh for PreparedTRAStarWaypointDatabasePolicyProfile {
2107 fn name(&self) -> &'static str {
2108 self.profile.name()
2109 }
2110
2111 fn navmesh(&self) -> &Navmesh {
2112 self.waypoint_policy.navmesh()
2113 }
2114
2115 fn neighbors(&self, cell_index: usize) -> Option<&[usize]> {
2116 self.waypoint_policy.neighbors(cell_index)
2117 }
2118
2119 fn portals_from(&self, cell_index: usize) -> Option<&[crate::NavmeshPortal]> {
2120 self.waypoint_policy.portals_from(cell_index)
2121 }
2122
2123 fn portal_between(&self, left_cell: usize, right_cell: usize) -> Option<crate::NavmeshPortal> {
2124 self.waypoint_policy.portal_between(left_cell, right_cell)
2125 }
2126}
2127
2128impl PreparedNavmesh for PreparedTRAStarPortalTransitionCache {
2129 fn name(&self) -> &'static str {
2130 "tra-star-portal-transition-cache"
2131 }
2132
2133 fn navmesh(&self) -> &Navmesh {
2134 self.prepared.navmesh()
2135 }
2136
2137 fn neighbors(&self, cell_index: usize) -> Option<&[usize]> {
2138 self.prepared.neighbors(cell_index)
2139 }
2140
2141 fn portals_from(&self, cell_index: usize) -> Option<&[crate::NavmeshPortal]> {
2142 self.prepared.portals_from(cell_index)
2143 }
2144
2145 fn portal_between(&self, left_cell: usize, right_cell: usize) -> Option<crate::NavmeshPortal> {
2146 self.prepared.portal_between(left_cell, right_cell)
2147 }
2148}
2149
2150fn search_with_midpoint_seed(
2151 prepared: &impl PreparedNavmesh,
2152 query: NavmeshQuery,
2153 midpoint_seed_builder: impl Fn(&crate::navmesh::corridor::NavmeshCorridor, &[usize]) -> Vec<Point2>,
2154) -> NavmeshSearchResult {
2155 let (start_cell, goal_cell) = match prepared.query(query) {
2156 NavmeshQueryResult::Connected {
2157 start_cell,
2158 goal_cell,
2159 } => (start_cell, goal_cell),
2160 NavmeshQueryResult::InvalidStart => {
2161 return Err(crate::NavmeshSearchError::InvalidStart { point: query.start });
2162 }
2163 NavmeshQueryResult::InvalidGoal => {
2164 return Err(crate::NavmeshSearchError::InvalidGoal { point: query.goal });
2165 }
2166 NavmeshQueryResult::NoPath { .. } => return crate::navmesh::search_not_found(0),
2167 };
2168
2169 if points_equal(query.start, query.goal) {
2170 return crate::navmesh::search_found(
2171 PolygonPath::from_points(vec![query.start])
2172 .expect("polygon path contains at least one point"),
2173 1,
2174 );
2175 }
2176
2177 let (Some(cells), visited_nodes) =
2178 search_prepared_cell_corridor(prepared, start_cell, goal_cell, query.budget)?
2179 else {
2180 return crate::navmesh::search_not_found(0);
2181 };
2182
2183 let Some(corridor) = prepared.materialize_corridor(query.start, query.goal, &cells) else {
2184 return crate::navmesh::search_not_found(visited_nodes);
2185 };
2186
2187 let midpoint_seed = midpoint_seed_builder(&corridor, &cells);
2188 let baseline_points =
2189 crate::navmesh::funnel::pull_string(prepared.navmesh(), &corridor, midpoint_seed.clone());
2190 if baseline_points.len() >= 2 && !prepared.navmesh().path_is_walkable(&baseline_points) {
2191 return crate::navmesh::search_not_found(visited_nodes);
2192 }
2193
2194 let refined_seed = refine_query_locally(prepared.navmesh(), &corridor, midpoint_seed);
2195 let refined_points =
2196 crate::navmesh::funnel::pull_string(prepared.navmesh(), &corridor, refined_seed);
2197
2198 let chosen_points = if refined_points.len() >= 2
2199 && prepared.navmesh().path_is_walkable(&refined_points)
2200 && path_cost(&refined_points) + EPSILON < path_cost(&baseline_points)
2201 {
2202 refined_points
2203 } else {
2204 baseline_points
2205 };
2206
2207 crate::navmesh::search_found(
2208 PolygonPath::from_points(chosen_points).expect("polygon path contains at least one point"),
2209 visited_nodes,
2210 )
2211}
2212
2213fn default_midpoint_seed(
2214 corridor: &crate::navmesh::corridor::NavmeshCorridor,
2215 _cells: &[usize],
2216) -> Vec<Point2> {
2217 corridor
2218 .portals
2219 .iter()
2220 .map(crate::algorithms::channel_search::portal_midpoint)
2221 .collect()
2222}
2223
2224fn search_prepared_cell_corridor(
2225 prepared: &impl PreparedNavmesh,
2226 start_cell: usize,
2227 goal_cell: usize,
2228 budget: condor_core::SearchBudget,
2229) -> Result<(Option<Vec<usize>>, usize), crate::NavmeshSearchError> {
2230 let cell_count = prepared.navmesh().cells().len();
2231 if start_cell >= cell_count || goal_cell >= cell_count {
2232 return Ok((None, 0));
2233 }
2234
2235 let mut seen = vec![false; cell_count];
2236 let mut parents = vec![None; cell_count];
2237 let mut frontier = std::collections::VecDeque::from([start_cell]);
2238 let mut visited_nodes = 0;
2239 let watch = condor_core::BudgetWatch::start(budget);
2240
2241 seen[start_cell] = true;
2242 parents[start_cell] = Some(start_cell);
2243
2244 while let Some(cell_index) = frontier.pop_front() {
2245 visited_nodes += 1;
2246 if cell_index == goal_cell {
2247 return Ok((
2248 reconstruct_cell_path(&parents, start_cell, goal_cell),
2249 visited_nodes,
2250 ));
2251 }
2252
2253 watch.check(visited_nodes)?;
2254
2255 let Some(prepared_neighbors) = prepared.neighbors(cell_index) else {
2256 continue;
2257 };
2258 let mut neighbors = prepared_neighbors.to_vec();
2259 neighbors.sort_unstable();
2260
2261 for neighbor in neighbors {
2262 if neighbor >= seen.len() || seen[neighbor] {
2263 continue;
2264 }
2265
2266 seen[neighbor] = true;
2267 parents[neighbor] = Some(cell_index);
2268 frontier.push_back(neighbor);
2269 }
2270 }
2271
2272 Ok((None, visited_nodes))
2273}
2274
2275fn reconstruct_cell_path(
2276 parents: &[Option<usize>],
2277 start_cell: usize,
2278 goal_cell: usize,
2279) -> Option<Vec<usize>> {
2280 let mut cells = vec![goal_cell];
2281 let mut current = goal_cell;
2282
2283 while current != start_cell {
2284 let parent = parents[current]?;
2285 cells.push(parent);
2286 current = parent;
2287 }
2288
2289 cells.reverse();
2290 Some(cells)
2291}
2292
2293fn refine_query_locally(
2294 navmesh: &Navmesh,
2295 corridor: &crate::navmesh::corridor::NavmeshCorridor,
2296 seed_points: Vec<Point2>,
2297) -> Vec<Point2> {
2298 if seed_points.is_empty() {
2299 return seed_points;
2300 }
2301
2302 let mut refined = seed_points;
2303 let max_passes = corridor.portals.len().max(1);
2304
2305 for _ in 0..max_passes {
2306 let mut improved = false;
2307
2308 for index in 0..corridor.portals.len() {
2309 let portal = corridor.portals[index];
2310 let prev = if index == 0 {
2311 corridor.start
2312 } else {
2313 refined[index - 1]
2314 };
2315 let next = if index + 1 == refined.len() {
2316 corridor.goal
2317 } else {
2318 refined[index + 1]
2319 };
2320
2321 let current = refined[index];
2322 let current_cost = local_turn_cost(prev, current, next);
2323
2324 let mut best_point = current;
2325 let mut best_cost = current_cost;
2326 for candidate in [portal.start, portal.end] {
2327 if !navmesh.segment_is_walkable(prev, candidate)
2328 || !navmesh.segment_is_walkable(candidate, next)
2329 {
2330 continue;
2331 }
2332
2333 let candidate_cost = local_turn_cost(prev, candidate, next);
2334 if candidate_cost + EPSILON < best_cost {
2335 best_point = candidate;
2336 best_cost = candidate_cost;
2337 }
2338 }
2339
2340 if !points_equal(best_point, current) {
2341 refined[index] = best_point;
2342 improved = true;
2343 }
2344 }
2345
2346 if !improved {
2347 break;
2348 }
2349 }
2350
2351 refined
2352}
2353
2354fn local_turn_cost(prev: Point2, current: Point2, next: Point2) -> f64 {
2355 segment_cost(prev, current) + segment_cost(current, next)
2356}
2357
2358fn path_cost(points: &[Point2]) -> f64 {
2359 points
2360 .windows(2)
2361 .map(|segment| segment_cost(segment[0], segment[1]))
2362 .sum()
2363}
2364
2365fn segment_cost(a: Point2, b: Point2) -> f64 {
2366 ((a.x - b.x).powi(2) + (a.y - b.y).powi(2)).sqrt()
2367}