shortestpath 0.10.0

Shortest Path is an experimental library finding the shortest path from A to B.
Documentation
// Copyright (C) 2025 Christian Mauduit <ufoot@ufoot.org>

//! Text-based 3D source implementation.
//!
//! This module provides the ability to parse pathfinding volumes from ASCII text.
//! Text is organized in 2D layers separated by lines of separator characters.

use super::cell_type::*;
use super::source_2d_from_text::*;
use super::source_3d::*;
use crate::errors::*;
use crate::mesh_3d::Shape3D;
use std::str::FromStr;

/// A 3D volume source that reads from text.
///
/// Text is organized as multiple 2D layers separated by lines containing only
/// separator characters (`-`, `_`, `=`). Each layer uses the same character
/// interpretation as [`Source2DFromText`].
///
/// # Example
///
/// ```
/// use shortestpath::mesh_source::{Source3DFromText, Source3D, CellType};
///
/// let map = "\
/// .....
/// .###.
/// .....
/// -----
/// .....
/// ..#..
/// .....";
///
/// let source = Source3DFromText::from_text(map);
/// assert_eq!(source.width(), 5);
/// assert_eq!(source.height(), 3);
/// assert_eq!(source.depth(), 2);
/// assert_eq!(source.get(0, 0, 0).unwrap(), CellType::FLOOR);  // '.' is free
/// assert_eq!(source.get(1, 1, 0).unwrap(), CellType::WALL);  // '#' is wall
/// ```
#[derive(Debug, Clone)]
pub struct Source3DFromText {
    width: usize,
    height: usize,
    data: Vec<Vec<Vec<char>>>,
}

impl Source3DFromText {
    fn width_height_from_data(data: &Vec<Vec<Vec<char>>>) -> (usize, usize) {
        let mut width = 0;
        let mut height = 0;
        for plane in data {
            if plane.len() > height {
                height = plane.len();
            }
            for line in plane {
                if line.len() > width {
                    width = line.len();
                }
            }
        }

        (width, height)
    }

    /// Gets the character at the given 3D coordinates.
    ///
    /// Returns the default free character if the position is beyond the line length.
    fn get_char(&self, x: usize, y: usize, z: usize) -> Result<char> {
        if z >= self.data.len() {
            return Err(Error::invalid_xyz(x, y, z));
        }
        if y >= self.height {
            return Err(Error::invalid_xyz(x, y, z));
        }
        if x >= self.width {
            return Err(Error::invalid_xyz(x, y, z));
        }
        let plane = &self.data[z];
        if y >= plane.len() {
            // If undef, it's not a wall, one can go there
            return Ok(Source2DFromText::DEFAULT_FREE_CHAR);
        }
        let line = &plane[y];
        if x >= line.len() {
            // If undef, it's not a wall, one can go there
            return Ok(Source2DFromText::DEFAULT_FREE_CHAR);
        }
        Ok(line[x])
    }

    /// Checks if a character represents a wall.
    pub fn is_wall_char(chr: char) -> bool {
        Source2DFromText::is_wall_char(chr)
    }

    /// Checks if a line is a separator line (used to delimit 3D layers).
    ///
    /// A line is a separator if it contains only separator characters and spaces.
    pub fn is_sep_line(line: &str) -> bool {
        let mut has_sep_char = false;
        for chr in line.chars() {
            if Source2DFromText::is_sep_char(chr) {
                has_sep_char = true;
            } else {
                // Tolerating spaces, they are easy to forget.
                // Not using DEFAULT_SEP_CHAR here as we're really
                // tracking ' ' as "a typo space" and not our separator.
                if chr != ' ' {
                    return false;
                }
            }
        }
        has_sep_char
    }

    /// Creates a Source3DFromText from a string slice.
    ///
    /// Layers are separated by lines containing only separator characters.
    /// This is equivalent to using the `FromStr` trait's `parse()` method.
    ///
    /// # Example
    ///
    /// ```
    /// use shortestpath::mesh_source::{Source3DFromText, Source3D};
    ///
    /// let source = Source3DFromText::from_text("...\n.#.\n---\n...\n...");
    /// assert_eq!(source.width(), 3);
    /// assert_eq!(source.height(), 2);
    /// assert_eq!(source.depth(), 2);
    /// ```
    pub fn from_text(input: &str) -> Source3DFromText {
        Self::from_lines(input.lines())
    }

    /// Creates a Source3DFromText from a slice of string slices.
    pub fn from_slice(input: &[&str]) -> Source3DFromText {
        Self::from_lines(input.iter().copied())
    }

    /// Creates a Source3DFromText from a slice of Strings.
    pub fn from_strings(input: &[String]) -> Source3DFromText {
        Self::from_lines(input.iter().map(|s| s.as_str()))
    }

    /// Creates a Source3DFromText from an iterator of string slices.
    pub fn from_lines<'a, I>(input: I) -> Source3DFromText
    where
        I: Iterator<Item = &'a str>,
    {
        let mut data: Vec<Vec<Vec<char>>> = Vec::new();
        let mut plane_data: Vec<Vec<char>> = Vec::new();
        for line in input {
            if Self::is_sep_line(line) {
                data.push(plane_data);
                plane_data = Vec::new();
            } else {
                let line_data = String::from(line).chars().collect();
                plane_data.push(line_data);
            }
        }
        if !plane_data.is_empty() {
            data.push(plane_data);
        }
        let (width, height) = Self::width_height_from_data(&data);
        Source3DFromText {
            width,
            height,
            data,
        }
    }
}

impl FromStr for Source3DFromText {
    type Err = std::convert::Infallible;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        Ok(Self::from_text(s))
    }
}

impl Source3D for Source3DFromText {
    fn get(&self, x: usize, y: usize, z: usize) -> Result<CellType> {
        let chr = self.get_char(x, y, z)?;
        Ok(if Self::is_wall_char(chr) {
            CellType::WALL
        } else {
            CellType::FLOOR
        })
    }

    fn width(&self) -> usize {
        self.width
    }

    fn height(&self) -> usize {
        self.height
    }

    fn depth(&self) -> usize {
        self.data.len()
    }
}

impl Shape3D for Source3DFromText {
    fn shape(&self) -> (usize, usize, usize) {
        (self.width, self.height, self.data.len())
    }
}

impl std::fmt::Display for Source3DFromText {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "Source3DFromText ({}x{}x{}):", self.width(), self.height(), self.depth())?;
        write!(f, "{}", crate::mesh_source::repr::repr_source_3d(self))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_is_sep_line() {
        assert!(Source3DFromText::is_sep_line("-"));
        assert!(Source3DFromText::is_sep_line("__"));
        assert!(Source3DFromText::is_sep_line("== = "));
        assert!(!Source3DFromText::is_sep_line(""));
        assert!(!Source3DFromText::is_sep_line("abc"));
        assert!(!Source3DFromText::is_sep_line("_x_"));
        assert!(!Source3DFromText::is_sep_line("."));
        assert!(!Source3DFromText::is_sep_line("  "));
    }

    #[test]
    fn test_from_text_basic() {
        let source =
            Source3DFromText::from_text("aaaaaa\nb b b \n      \n123456\n------\n\n xyz\n");
        assert_eq!(source.width(), 6);
        assert_eq!(source.height(), 4);
        assert_eq!(source.depth(), 2);
        assert_eq!(source.get_char(0, 0, 0).unwrap(), 'a');
        assert_eq!(source.get(0, 0, 0).unwrap(), CellType::WALL);
        assert_eq!(source.get_char(2, 1, 0).unwrap(), 'b');
        assert_eq!(source.get(2, 1, 0).unwrap(), CellType::WALL);
        assert_eq!(source.get_char(3, 1, 0).unwrap(), ' ');
        assert_eq!(source.get(3, 1, 0).unwrap(), CellType::FLOOR);
        assert_eq!(source.get_char(4, 2, 0).unwrap(), ' ');
        assert_eq!(source.get(4, 2, 0).unwrap(), CellType::FLOOR);
        assert_eq!(source.get_char(5, 3, 0).unwrap(), '6');
        assert_eq!(source.get(5, 3, 0).unwrap(), CellType::WALL);
        assert_eq!(source.get_char(0, 0, 1).unwrap(), ' ');
        assert_eq!(source.get(0, 0, 1).unwrap(), CellType::FLOOR);
        assert_eq!(source.get_char(1, 1, 1).unwrap(), 'x');
        assert_eq!(source.get(1, 1, 1).unwrap(), CellType::WALL);
        assert!(source.get_char(6, 4, 2).is_err());
        assert!(source.get(6, 4, 2).is_err());
    }

    #[test]
    fn test_from_text_eof() {
        let source = Source3DFromText::from_text("a\nabc\n====================\n");
        assert_eq!(source.width(), 3);
        assert_eq!(source.height(), 2);
        assert_eq!(source.depth(), 1);
    }
}