Skip to main content

condor_navmesh/algorithms/
tra_star.rs

1//! Prepared-routing lane: TRA* builders and waypoint-database policy surfaces.
2//!
3//! # Surface (prepared, not static pathfinder)
4//!
5//! TRA* is **not** a [`crate::NavmeshPathfinder`]. Every public entry is a
6//! [`PreparedNavmeshBuilder`] that clones a validated static [`Navmesh`], indexes
7//! adjacency (via [`StaticPreparedNavmesh`]), and returns an
8//! immutable map with a local `search` method. Use this lane when many queries
9//! share one mesh; use channel search / TA* / Polyanya for independent one-shots.
10//!
11//! **Dynamic availability**: builders never read
12//! [`DynamicNavmeshState`](crate::DynamicNavmeshState). When cells or portals
13//! toggle, materialize a new static mesh and re-`preprocess` (or run
14//! [`DynamicPreparedNavmeshQuery`](crate::DynamicPreparedNavmeshQuery) for the
15//! connectivity rebuild helper). Prepared waypoint caches are discarded with the
16//! old map—there is no incremental repair.
17//!
18//! # Builder lifecycle
19//!
20//! 1. Hold a validated static [`Navmesh`] (or materialization).
21//! 2. Call `Builder::preprocess` → `Prepared*` map (or `Prepared*::builder()`).
22//! 3. Call `prepared.search(query)` repeatedly until the underlying mesh changes.
23//! 4. On mesh or availability change, drop the map and preprocess again.
24//!
25//! # Routing core (shared)
26//!
27//! All variants run the same geometric pipeline: connectivity precheck → prepared
28//! neighbor BFS corridor → portal chain → midpoint **seed policy** → funnel
29//! string-pull → walkability. Cost is Euclidean segment length over the funnelled
30//! [`PolygonPath`] (no per-cell weights). Seed policy is the
31//! only intentional difference between variants.
32//!
33//! # Waypoint-database policy ladder
34//!
35//! | Surface | Seed / cache policy |
36//! | --- | --- |
37//! | [`TRAStarBuilder`] / [`PreparedTRAStar`] | Default midpoints each query (no DB) |
38//! | [`TRAStarPortalTransitionCacheBuilder`] | Eager directed `(from,to)` midpoint table |
39//! | [`TRAStarWaypointDatabaseStaticBuilder`] | Eager per-cell waypoint lists |
40//! | [`TRAStarWaypointDatabaseLazyQueryBuilder`] | Scratch map per query; no retention |
41//! | [`TRAStarWaypointDatabaseAdaptiveLruBuilder`] | Single-tier recency LRU across queries |
42//! | [`TRAStarWaypointDatabaseTwoTierLruBuilder`] | Probation → protected segmented LRU |
43//! | [`TRAStarWaypointDatabaseCostAwareEvictionBuilder`] | Cost-weighted eviction signals |
44//! | Fixed admission / promotion / demotion builders | Threshold + hit-count + overflow rules |
45//! | [`TRAStarWaypointDatabasePolicyProfileBuilder`] | Named curated bundle (v1) |
46//!
47//! Portfolio recommendation for exact prepared navmesh routing points at base
48//! [`TRAStarBuilder`]; policy variants are for cache-behavior evidence and knobs.
49//!
50//! # Examples
51//!
52//! ```
53//! use condor_navmesh::{
54//!     Navmesh, NavmeshCell, Point2, PreparedNavmesh, PreparedNavmeshBuilder, TRAStarBuilder,
55//! };
56//!
57//! let navmesh = Navmesh::new(
58//!     vec![NavmeshCell::new(
59//!         "cell-0",
60//!         vec![
61//!             Point2::new(0.0, 0.0),
62//!             Point2::new(2.0, 0.0),
63//!             Point2::new(0.0, 2.0),
64//!         ],
65//!     )],
66//!     vec![],
67//! );
68//! let builder = TRAStarBuilder;
69//! let prepared = builder.preprocess(&navmesh).expect("prep fails only with invalid navmesh");
70//! assert_eq!(prepared.name(), "tra-star");
71//! ```
72use 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/// Base TRA* preprocess: adjacency snapshot + corridor/funnel search, no waypoint cache.
88///
89/// Default prepared surface for multi-query exact-style routing on a static mesh.
90/// Prefer this unless a specific midpoint-cache policy is required.
91#[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/// Immutable prepared TRA* map wrapping a [`StaticPreparedNavmesh`].
108///
109/// Preprocess once, then [`Self::search`] per query. Drop and rebuild when the
110/// source mesh or availability overlay changes; this type is not patched in place.
111#[derive(Debug, Clone, PartialEq)]
112pub struct PreparedTRAStar {
113    prepared: StaticPreparedNavmesh,
114}
115
116impl PreparedTRAStar {
117    /// Returns a [`TRAStarBuilder`] for preprocess entry.
118    #[must_use]
119    pub fn builder() -> TRAStarBuilder {
120        TRAStarBuilder
121    }
122
123    /// Borrowed static prepared navmesh (adjacency / portal tables).
124    #[must_use]
125    pub fn prepared_navmesh(&self) -> &StaticPreparedNavmesh {
126        &self.prepared
127    }
128
129    /// Routes with default portal-midpoint seeds and funnel string-pull.
130    ///
131    /// Same connectivity and walkability errors as static pathfinders; found vs
132    /// no-path are both successful outcomes with visit stats.
133    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/// Eager directed portal-midpoint table built at preprocess (no eviction).
161///
162/// Materializes every `(from_cell, to_cell)` midpoint once; query seeds the funnel
163/// from that table. Use when the full transition set fits memory and must stay hot.
164#[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/// Prepared TRA* with an eager map from directed cell transitions to portal midpoints.
183///
184/// Preprocess materializes every portal midpoint; query seeds the funnel from that table.
185#[derive(Debug, Clone, PartialEq)]
186pub struct PreparedTRAStarPortalTransitionCache {
187    prepared: PreparedTRAStar,
188    portal_transition_midpoints: BTreeMap<(usize, usize), Point2>,
189}
190
191impl PreparedTRAStarPortalTransitionCache {
192    /// Returns a [`TRAStarPortalTransitionCacheBuilder`] for preprocess entry.
193    #[must_use]
194    pub fn builder() -> TRAStarPortalTransitionCacheBuilder {
195        TRAStarPortalTransitionCacheBuilder
196    }
197
198    /// Underlying base TRA* prepared surface.
199    #[must_use]
200    pub fn prepared_tra_star(&self) -> &PreparedTRAStar {
201        &self.prepared
202    }
203
204    /// Number of directed `(from_cell, to_cell)` midpoint entries retained after preprocess.
205    #[must_use]
206    pub fn portal_transition_count(&self) -> usize {
207        self.portal_transition_midpoints.len()
208    }
209
210    /// Routes with precomputed portal-midpoint seeds and funnel string-pull.
211    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/// Eager per-cell portal-midpoint waypoint lists (full static database, no eviction).
266#[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/// Per-query lazy midpoints only—scratch storage discarded after each `search`.
285///
286/// Useful as a no-retention baseline against LRU / static DB policies.
287#[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/// Capacity-bounded single-tier LRU of portal midpoints across queries (recency only).
306#[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/// Two-tier probation/protected midpoint LRU; first hit promotes into protected.
326#[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/// Cost-weighted eviction of cached midpoints (recency blended with low-cost bias).
346#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
347pub struct TRAStarWaypointDatabaseCostAwareEvictionBuilder;
348
349/// Retain midpoints only when portal segment cost meets a fixed admission threshold.
350///
351/// Below-threshold midpoints still seed the current query; they are not stored.
352#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
353pub struct TRAStarWaypointDatabaseFixedAdmissionThresholdBuilder;
354
355/// Fixed admission threshold plus multi-hit promotion from probation to protected.
356#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
357pub struct TRAStarWaypointDatabaseFixedPromotionRuleBuilder;
358
359/// Fixed admission + promotion, with protected overflow that **evicts** (not demotes).
360///
361/// Differs from two-tier demotion: least-recent protected entries leave the cache.
362#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
363pub struct TRAStarWaypointDatabaseFixedDemotionRuleBuilder;
364
365/// Named admission / promotion / demotion parameter bundle for the curated policy lane.
366///
367/// Prefer [`TRAStarWaypointDatabasePolicyProfileBuilder`] over composing fixed-rule
368/// builders by hand when the portfolio needs a single stable algorithm id.
369#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
370pub enum TRAStarWaypointDatabasePolicyProfile {
371    /// v1 fixed thresholds: admission cost `2.5`, promotion after `2` probation hits,
372    /// protected overflow evicts least-recent protected.
373    #[default]
374    V1,
375}
376
377impl TRAStarWaypointDatabasePolicyProfile {
378    /// Stable algorithm name string for this profile (matches builder `name()`).
379    #[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    /// Minimum portal segment cost required to admit a midpoint into the cache.
387    #[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    /// Probation hits required before promoting a midpoint into the protected tier.
395    #[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/// Preprocess builder that materializes a named [`TRAStarWaypointDatabasePolicyProfile`].
404///
405/// Stable `name()` comes from the profile (e.g. policy-profile-v1), not a generic builder id.
406#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
407pub struct TRAStarWaypointDatabasePolicyProfileBuilder {
408    profile: TRAStarWaypointDatabasePolicyProfile,
409}
410
411impl TRAStarWaypointDatabasePolicyProfileBuilder {
412    /// Builds with the given named policy profile.
413    #[must_use]
414    pub const fn new(profile: TRAStarWaypointDatabasePolicyProfile) -> Self {
415        Self { profile }
416    }
417
418    /// Policy profile this builder will materialize.
419    #[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/// Prepared TRA* with an eager per-cell portal-midpoint waypoint database.
525///
526/// All directed midpoints materialize at preprocess; no eviction across queries.
527#[derive(Debug, Clone, PartialEq)]
528pub struct PreparedTRAStarWaypointDatabaseStatic {
529    prepared: PreparedTRAStar,
530    waypoint_database: BTreeMap<usize, Vec<CellWaypointEntry>>,
531}
532
533/// Prepared TRA* that computes midpoints on demand without cross-query retention.
534///
535/// Each `search` builds a scratch map discarded at query end (no durable cache policy).
536#[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/// Prepared TRA* with a capacity-bounded single-tier LRU of portal midpoints.
583///
584/// Midpoints fill on first use and survive across queries until capacity eviction (recency only).
585#[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/// Prepared TRA* with probation/protected two-tier LRU midpoint cache.
712///
713/// First hit promotes probation → protected; protected overflow demotes to probation.
714#[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/// Prepared TRA* that evicts cached midpoints using cost-weighted signals (not recency alone).
903#[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/// Prepared TRA* that gates cache admission on a fixed transition-cost threshold.
912///
913/// Midpoints below the threshold are still used for the current seed but are not retained.
914#[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/// Prepared TRA* that promotes probation midpoints after a fixed hit count.
924///
925/// Combines fixed admission threshold with multi-hit promotion into the protected tier.
926#[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/// Prepared TRA* that demotes protected midpoints under a fixed overflow policy.
937///
938/// Protected overflow evicts the least-recent protected entry rather than demoting to probation.
939#[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/// Prepared TRA* bound to a named [`TRAStarWaypointDatabasePolicyProfile`].
950///
951/// Curated policy bundle; v1 delegates to fixed-admission + promotion + demotion rules.
952#[derive(Debug)]
953pub struct PreparedTRAStarWaypointDatabasePolicyProfile {
954    profile: TRAStarWaypointDatabasePolicyProfile,
955    waypoint_policy: PreparedTRAStarWaypointDatabaseFixedDemotionRule,
956}
957
958impl PreparedTRAStarWaypointDatabaseStatic {
959    /// Returns a [`TRAStarWaypointDatabaseStaticBuilder`] for preprocess entry.
960    #[must_use]
961    pub fn builder() -> TRAStarWaypointDatabaseStaticBuilder {
962        TRAStarWaypointDatabaseStaticBuilder
963    }
964
965    /// Underlying base TRA* prepared surface.
966    #[must_use]
967    pub fn prepared_tra_star(&self) -> &PreparedTRAStar {
968        &self.prepared
969    }
970
971    /// Total directed midpoint entries in the eager waypoint database.
972    #[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    /// Routes using the static waypoint database for funnel midpoint seeds.
981    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    /// Returns a [`TRAStarWaypointDatabaseLazyQueryBuilder`] for preprocess entry.
1052    #[must_use]
1053    pub fn builder() -> TRAStarWaypointDatabaseLazyQueryBuilder {
1054        TRAStarWaypointDatabaseLazyQueryBuilder
1055    }
1056
1057    /// Underlying base TRA* prepared surface.
1058    #[must_use]
1059    pub fn prepared_tra_star(&self) -> &PreparedTRAStar {
1060        &self.prepared
1061    }
1062
1063    /// Routes with per-query lazy midpoint materialization (no cross-query cache).
1064    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    /// Returns a [`TRAStarWaypointDatabaseAdaptiveLruBuilder`] for preprocess entry.
1126    #[must_use]
1127    pub fn builder() -> TRAStarWaypointDatabaseAdaptiveLruBuilder {
1128        TRAStarWaypointDatabaseAdaptiveLruBuilder
1129    }
1130
1131    /// Underlying base TRA* prepared surface.
1132    #[must_use]
1133    pub fn prepared_tra_star(&self) -> &PreparedTRAStar {
1134        &self.prepared
1135    }
1136
1137    /// Maximum midpoint entries retained in the adaptive LRU.
1138    #[must_use]
1139    pub fn lru_capacity(&self) -> usize {
1140        self.capacity
1141    }
1142
1143    /// Midpoints currently held in the adaptive LRU (across queries).
1144    #[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    /// Routes while updating the adaptive LRU midpoint cache.
1153    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    /// Returns a [`TRAStarWaypointDatabaseTwoTierLruBuilder`] for preprocess entry.
1208    #[must_use]
1209    pub fn builder() -> TRAStarWaypointDatabaseTwoTierLruBuilder {
1210        TRAStarWaypointDatabaseTwoTierLruBuilder
1211    }
1212
1213    /// Underlying base TRA* prepared surface.
1214    #[must_use]
1215    pub fn prepared_tra_star(&self) -> &PreparedTRAStar {
1216        &self.prepared
1217    }
1218
1219    /// Total midpoint capacity across probation and protected segments.
1220    #[must_use]
1221    pub fn lru_capacity(&self) -> usize {
1222        self.capacity
1223    }
1224
1225    /// Capacity reserved for the protected LRU segment.
1226    #[must_use]
1227    pub fn protected_segment_capacity(&self) -> usize {
1228        self.protected_capacity
1229    }
1230
1231    /// Midpoints currently retained in either tier.
1232    #[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    /// Midpoints currently in the probation segment.
1241    #[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    /// Midpoints currently in the protected segment.
1250    #[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    /// Routes while updating the two-tier midpoint LRU.
1259    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    /// Returns a [`TRAStarWaypointDatabaseCostAwareEvictionBuilder`] for preprocess entry.
1316    #[must_use]
1317    pub fn builder() -> TRAStarWaypointDatabaseCostAwareEvictionBuilder {
1318        TRAStarWaypointDatabaseCostAwareEvictionBuilder
1319    }
1320
1321    /// Underlying base TRA* prepared surface.
1322    #[must_use]
1323    pub fn prepared_tra_star(&self) -> &PreparedTRAStar {
1324        &self.prepared
1325    }
1326
1327    /// Total midpoint capacity before cost-weighted eviction.
1328    #[must_use]
1329    pub fn eviction_capacity(&self) -> usize {
1330        self.capacity
1331    }
1332
1333    /// Capacity reserved for the protected segment.
1334    #[must_use]
1335    pub fn protected_segment_capacity(&self) -> usize {
1336        self.protected_capacity
1337    }
1338
1339    /// Midpoints currently retained under the cost-aware policy.
1340    #[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    /// Midpoints currently in probation.
1349    #[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    /// Midpoints currently in the protected segment.
1358    #[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    /// Routes while updating the cost-aware midpoint cache.
1367    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    /// Returns a [`TRAStarWaypointDatabaseFixedAdmissionThresholdBuilder`] for preprocess entry.
1434    #[must_use]
1435    pub fn builder() -> TRAStarWaypointDatabaseFixedAdmissionThresholdBuilder {
1436        TRAStarWaypointDatabaseFixedAdmissionThresholdBuilder
1437    }
1438
1439    /// Underlying base TRA* prepared surface.
1440    #[must_use]
1441    pub fn prepared_tra_star(&self) -> &PreparedTRAStar {
1442        &self.prepared
1443    }
1444
1445    /// Minimum portal segment cost required to retain a midpoint in cache.
1446    #[must_use]
1447    pub fn admission_cost_threshold(&self) -> f64 {
1448        self.admission_threshold
1449    }
1450
1451    /// Total midpoint capacity before eviction.
1452    #[must_use]
1453    pub fn eviction_capacity(&self) -> usize {
1454        self.capacity
1455    }
1456
1457    /// Capacity reserved for the protected segment.
1458    #[must_use]
1459    pub fn protected_segment_capacity(&self) -> usize {
1460        self.protected_capacity
1461    }
1462
1463    /// Midpoints currently retained (above the admission threshold).
1464    #[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    /// Midpoints currently in probation.
1473    #[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    /// Midpoints currently in the protected segment.
1482    #[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    /// Routes while applying the fixed admission-threshold cache policy.
1491    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    /// Returns a [`TRAStarWaypointDatabaseFixedPromotionRuleBuilder`] for preprocess entry.
1563    #[must_use]
1564    pub fn builder() -> TRAStarWaypointDatabaseFixedPromotionRuleBuilder {
1565        TRAStarWaypointDatabaseFixedPromotionRuleBuilder
1566    }
1567
1568    /// Underlying base TRA* prepared surface.
1569    #[must_use]
1570    pub fn prepared_tra_star(&self) -> &PreparedTRAStar {
1571        &self.prepared
1572    }
1573
1574    /// Minimum portal segment cost required to retain a midpoint in cache.
1575    #[must_use]
1576    pub fn admission_cost_threshold(&self) -> f64 {
1577        self.admission_threshold
1578    }
1579
1580    /// Probation hits required before promoting into the protected tier.
1581    #[must_use]
1582    pub fn promotion_hits_required(&self) -> usize {
1583        self.promotion_hits_required
1584    }
1585
1586    /// Total midpoint capacity before eviction.
1587    #[must_use]
1588    pub fn eviction_capacity(&self) -> usize {
1589        self.capacity
1590    }
1591
1592    /// Capacity reserved for the protected segment.
1593    #[must_use]
1594    pub fn protected_segment_capacity(&self) -> usize {
1595        self.protected_capacity
1596    }
1597
1598    /// Midpoints currently retained under the promotion rule.
1599    #[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    /// Midpoints currently in probation.
1608    #[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    /// Midpoints currently in the protected segment.
1617    #[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    /// Routes while applying fixed admission + multi-hit promotion.
1626    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    /// Returns a [`TRAStarWaypointDatabaseFixedDemotionRuleBuilder`] for preprocess entry.
1704    #[must_use]
1705    pub fn builder() -> TRAStarWaypointDatabaseFixedDemotionRuleBuilder {
1706        TRAStarWaypointDatabaseFixedDemotionRuleBuilder
1707    }
1708
1709    /// Underlying base TRA* prepared surface.
1710    #[must_use]
1711    pub fn prepared_tra_star(&self) -> &PreparedTRAStar {
1712        &self.prepared
1713    }
1714
1715    /// Minimum portal segment cost required to retain a midpoint in cache.
1716    #[must_use]
1717    pub fn admission_cost_threshold(&self) -> f64 {
1718        self.admission_threshold
1719    }
1720
1721    /// Probation hits required before promoting into the protected tier.
1722    #[must_use]
1723    pub fn promotion_hits_required(&self) -> usize {
1724        self.promotion_hits_required
1725    }
1726
1727    /// Total midpoint capacity before eviction.
1728    #[must_use]
1729    pub fn eviction_capacity(&self) -> usize {
1730        self.capacity
1731    }
1732
1733    /// Capacity reserved for the protected segment.
1734    #[must_use]
1735    pub fn protected_segment_capacity(&self) -> usize {
1736        self.protected_capacity
1737    }
1738
1739    /// Midpoints currently retained under the demotion rule.
1740    #[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    /// Midpoints currently in probation.
1749    #[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    /// Midpoints currently in the protected segment.
1758    #[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    /// Routes while applying fixed demotion on protected overflow.
1767    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    /// Returns a default-profile [`TRAStarWaypointDatabasePolicyProfileBuilder`].
1845    #[must_use]
1846    pub fn builder() -> TRAStarWaypointDatabasePolicyProfileBuilder {
1847        TRAStarWaypointDatabasePolicyProfileBuilder::default()
1848    }
1849
1850    /// Named policy profile bound into this prepared map.
1851    #[must_use]
1852    pub fn policy_profile(&self) -> TRAStarWaypointDatabasePolicyProfile {
1853        self.profile
1854    }
1855
1856    /// Underlying base TRA* prepared surface.
1857    #[must_use]
1858    pub fn prepared_tra_star(&self) -> &PreparedTRAStar {
1859        self.waypoint_policy.prepared_tra_star()
1860    }
1861
1862    /// Profile admission cost threshold (see [`TRAStarWaypointDatabasePolicyProfile`]).
1863    #[must_use]
1864    pub fn admission_cost_threshold(&self) -> f64 {
1865        self.waypoint_policy.admission_cost_threshold()
1866    }
1867
1868    /// Profile promotion hit count.
1869    #[must_use]
1870    pub fn promotion_hits_required(&self) -> usize {
1871        self.waypoint_policy.promotion_hits_required()
1872    }
1873
1874    /// Total midpoint capacity before eviction.
1875    #[must_use]
1876    pub fn eviction_capacity(&self) -> usize {
1877        self.waypoint_policy.eviction_capacity()
1878    }
1879
1880    /// Capacity reserved for the protected segment.
1881    #[must_use]
1882    pub fn protected_segment_capacity(&self) -> usize {
1883        self.waypoint_policy.protected_segment_capacity()
1884    }
1885
1886    /// Midpoints currently retained under the bound profile.
1887    #[must_use]
1888    pub fn retained_waypoint_count(&self) -> usize {
1889        self.waypoint_policy.retained_waypoint_count()
1890    }
1891
1892    /// Midpoints currently in probation.
1893    #[must_use]
1894    pub fn retained_probation_count(&self) -> usize {
1895        self.waypoint_policy.retained_probation_count()
1896    }
1897
1898    /// Midpoints currently in the protected segment.
1899    #[must_use]
1900    pub fn retained_protected_count(&self) -> usize {
1901        self.waypoint_policy.retained_protected_count()
1902    }
1903
1904    /// Routes under the bound policy profile's midpoint cache rules.
1905    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}