condor-pathfinding-navmesh 0.4.0

Navmesh pathfinding algorithms and prepared routing structures for Condor.
Documentation
//! Portal chain along a known cell sequence between start and goal.
//!
//! # Role
//!
//! Intermediate representation between discrete cell search and continuous
//! funneling. Pathfinders (channel search, TA*, TRA*, Polyanya) produce a cell
//! sequence, then materialize a [`NavmeshCorridor`] so
//! [`super::funnel::pull_string`] can string-pull against ordered portal
//! crossings. This type does **not** search the mesh; it only records an
//! already-chosen corridor and resolves portals between consecutive cells.

use crate::{Navmesh, NavmeshPortal, Point2};

/// Ordered portal crossings and cells linking two navmesh points.
///
/// Invariant: `portals.len() + 1 == cells.len()` when built via [`Self::from_cells`].
/// `portals[i]` bridges `cells[i]` and `cells[i + 1]`. Start and goal are continuous
/// endpoints that should lie in the first and last cells respectively (callers enforce
/// that contract before funneling).
#[derive(Debug, Clone, PartialEq)]
pub struct NavmeshCorridor {
    /// Continuous path start (expected in the first cell).
    pub start: Point2,
    /// Continuous path goal (expected in the last cell).
    pub goal: Point2,
    /// Ordered portal chain; `portals[i]` bridges `cells[i]` → `cells[i + 1]`.
    pub portals: Vec<NavmeshPortal>,
    /// Ordered cell indices from start cell through goal cell.
    pub cells: Vec<usize>,
}

impl NavmeshCorridor {
    /// Assembles a corridor without checking portal/cell consistency.
    ///
    /// Prefer [`Self::from_cells`] when the cell sequence comes from search over a mesh.
    #[must_use]
    pub fn new(
        start: Point2,
        goal: Point2,
        portals: Vec<NavmeshPortal>,
        cells: Vec<usize>,
    ) -> Self {
        Self {
            start,
            goal,
            portals,
            cells,
        }
    }

    /// Resolves the portal chain for consecutive cells in `cells`.
    ///
    /// Returns `None` when `cells` is empty or any consecutive pair lacks a portal on
    /// `navmesh`. When multiple portals connect the same pair, the first in mesh order wins
    /// (aligned with prepared [`super::prepared::PreparedNavmesh::portal_between`]).
    #[must_use]
    pub fn from_cells(
        navmesh: &Navmesh,
        start: Point2,
        goal: Point2,
        cells: &[usize],
    ) -> Option<Self> {
        if cells.is_empty() {
            return None;
        }

        let mut portals = Vec::with_capacity(cells.len().saturating_sub(1));
        for window in cells.windows(2) {
            let left = window[0];
            let right = window[1];

            // First matching portal wins when multiple portals connect the same pair.
            let portal = *navmesh.portals().iter().find(|p| {
                (p.left_cell == left && p.right_cell == right)
                    || (p.left_cell == right && p.right_cell == left)
            })?;
            portals.push(portal);
        }

        Some(Self::new(start, goal, portals, cells.to_vec()))
    }
}