hex_renderer 0.2.6

Renderer for patterns in hexcasting (a mod for minecraft)
Documentation
use std::collections::{HashMap, HashSet};

use tiny_skia::{LineCap, LineJoin, Pixmap, Stroke};

use crate::{
    options::{Intersections, Lines, Point, Color},
    pattern_utils::{
        Angle, AngleParseError, ConnectionPoint, Coord, Direction, DirectionParseError,
        DynamicList, HexCoord,
    },
};

use super::{
    draw_gradient::draw_gradient_lines, draw_monocolor::draw_monocolor_lines,
    draw_segments::draw_segment_lines, point::draw_points,
};
#[derive(Debug, Clone, PartialEq, PartialOrd)]
///Wrapper around Pattern to specify special cases
pub enum PatternVariant {
    ///Draws the pattern normally
    Normal(Pattern),
    ///Draws the color as monocolor without bends
    /// For Segment and Gradient renderers, it simply draws it in the starting color.
    /// For monocolor, it takes out the bends (if applicable)
    Monocolor(Pattern),
}

#[derive(Debug, Clone, PartialEq)]
///Represents a pattern to be drawn on a grid
pub struct Pattern {
    pub(crate) path: Vec<Coord>,
    pub(crate) top_left: Coord,
    pub(crate) bottom_right: Coord,

    pub(crate) top_left_bound: HexCoord,
    pub(crate) bottom_right_bound: HexCoord,

    pub(crate) left_perimiter: Vec<Coord>,
    pub(crate) right_perimiter: Vec<Coord>,

    pub(crate) points: Vec<Coord>,
    pub(crate) angles: Vec<Angle>,

    pub(crate) collisions: HashMap<ConnectionPoint, i32>,
}

impl PartialOrd for Pattern {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        self.path.partial_cmp(&other.path) 
    }
}
impl std::hash::Hash for Pattern {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.path.hash(state);
    }
}
impl Pattern {
    ///Creates a new pattern with a given start direction and angle_sigs (links)
    pub fn new(rotation: Direction, links: Vec<Angle>) -> Self {
        let mut path = vec![Coord(0, 0), Coord(0, 0) + rotation];
        let mut top_left = path[0].min_components(path[1]);
        let mut bottom_right = path[0].max_components(path[1]);

        let mut top_left_bound = HexCoord::from(path[0]).min_components(path[1].into());
        let mut bottom_right_bound = HexCoord::from(path[0]).max_components(path[1].into());

        let mut rotation = rotation;

        let mut left_perimiter = DynamicList::new();
        let mut right_perimiter = DynamicList::new();

        Self::add_to_perimiter(&mut left_perimiter, &mut right_perimiter, path[0]);
        Self::add_to_perimiter(&mut left_perimiter, &mut right_perimiter, path[1]);

        let mut collisions = HashMap::new();
        let mut connections = HashSet::new();

        connections.insert(ConnectionPoint::new(path[0], path[1]));

        for link in &links {
            rotation = rotation + *link;

            let last_point = *path.last().unwrap();
            let next_point = last_point + rotation;

            top_left = top_left.min_components(next_point);
            bottom_right = bottom_right.max_components(next_point);

            top_left_bound = top_left_bound.min_components(next_point.into());
            bottom_right_bound = bottom_right_bound.max_components(next_point.into());

            Self::add_to_perimiter(&mut left_perimiter, &mut right_perimiter, next_point);
            path.push(next_point);

            let connection_point = ConnectionPoint::new(next_point, last_point);

            if let Some(collisions) = collisions.get_mut(&connection_point) {
                *collisions += 1;
            } else if connections.contains(&connection_point) {
                collisions.insert(connection_point, 1);
            } else {
                connections.insert(connection_point);
            }
        }

        let mut points = path.clone();
        points.sort();
        points.dedup();
        Pattern {
            path,
            top_left,
            bottom_right,
            top_left_bound,
            bottom_right_bound,
            left_perimiter: left_perimiter.into_vector(),
            right_perimiter: right_perimiter.into_vector(),
            points,
            angles: links,
            collisions,
        }
    }
    fn add_to_perimiter(
        left_perimiter: &mut DynamicList<Coord>,
        right_perimiter: &mut DynamicList<Coord>,
        point: Coord,
    ) {
        if let Some(val) = left_perimiter.get(point.1) {
            if point.0 < val.0 {
                left_perimiter.set(point.1, point);
            }
        } else {
            left_perimiter.set(point.1, point);
        }

        if let Some(val) = right_perimiter.get(point.1) {
            if point.0 > val.0 {
                right_perimiter.set(point.1, point);
            }
        } else {
            right_perimiter.set(point.1, point);
        }
    }
    #[allow(clippy::too_many_arguments)]
    pub(crate) fn draw_pattern(
        &self,
        pixmap: &mut Pixmap,
        origin: HexCoord,
        scale: f32,
        line_thickness: f32,
        line_options: &Lines,
        point_options: &Intersections,
        center_dot: &Point,
    ) {
        let stroke = Stroke {
            width: line_thickness * scale,
            line_cap: LineCap::Round,
            line_join: LineJoin::Round,
            ..Default::default()
        };
        //stroke.width = line_thickness * scale;
        //stroke.line_cap = LineCap::Round;
        //stroke.line_join = LineJoin::Round;

        let end_colors;

        match line_options {
            Lines::Monocolor { color, bent } => {
                draw_monocolor_lines(self, pixmap, &stroke, origin, scale, *color, *bent);
                end_colors = (*color, *color);
            }
            Lines::Gradient {
                colors,
                segments_per_color,
                bent,
            } => {
                if colors.len() < 2 {
                    let col = *colors.first().unwrap_or(&Color::WHITE);
                    end_colors = (col, col);
                    draw_monocolor_lines(self, pixmap, &stroke, origin, scale, col, *bent);
                } else {
                    end_colors = (
                        colors[0],
                        draw_gradient_lines(
                            self,
                            pixmap,
                            &stroke,
                            origin,
                            scale,
                            colors,
                            *segments_per_color,
                            *bent,
                        ),
                    );
                }
            }
            Lines::SegmentColors {
                colors,
                triangles: arrows,
                collisions,
            } => {
                end_colors = (
                    colors[0],
                    draw_segment_lines(
                        self,
                        pixmap,
                        &stroke,
                        origin,
                        scale,
                        colors,
                        arrows,
                        point_options.get_max_radius(),
                        collisions,
                    ),
                );
            }
        }

        match point_options {
            Intersections::Nothing => (),
            Intersections::UniformPoints(point) => {
                draw_points(&self.points, pixmap, origin, scale, point);
            }
            Intersections::EndsAndMiddle { start, end, middle } => {
                let start_point = self.path[0];
                let end_point = self.path[self.path.len() - 1];

                let start = start.into_point(end_colors.0);
                let end = end.into_point(end_colors.1);

                draw_points(&vec![start_point], pixmap, origin, scale, &start);
                if start_point != end_point {
                    draw_points(&vec![end_point], pixmap, origin, scale, &end);
                }
                let middle_points: Vec<Coord> = self
                    .points
                    .clone()
                    .into_iter()
                    .filter(|&point| point != start_point && point != end_point)
                    .collect();

                draw_points(&middle_points, pixmap, origin, scale, middle);
            }
        }

        let center = (self.bottom_right_bound + self.top_left_bound) / 2.0;
        let y_factor = 0.866_025_4;

        let y_coord = (center.1 / y_factor).round() as i32;
        let x_coord = (center.0 - 0.5 * y_coord as f32) as i32;

        let coord = Coord(x_coord, y_coord);
        let index = (coord.1 - self.top_left.1) as usize;
        if !self.points.contains(&coord)
            && self.left_perimiter[index].0 < coord.0
            && coord.0 < self.right_perimiter[index].0
        {
            draw_points(
                &vec![Coord(x_coord, y_coord)],
                pixmap,
                origin,
                scale,
                center_dot,
            );
        }
    }
}

impl PatternVariant {
    pub fn get_inner(&self) -> &Pattern {
        match self {
            PatternVariant::Normal(pattern) => pattern,
            PatternVariant::Monocolor(pattern) => pattern,
        }
    }
}

#[derive(Debug, Clone)]
pub enum PatternParseError {
    InvalidParts(String),
    HangingHexPattern(String),
    InvalidStartDirection { input: String, direction: String },
    InvalidAngle { input: String, angle: char },
}
impl TryFrom<&str> for Pattern {
    type Error = PatternParseError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        let mut parts: Vec<&str> = value.trim().split(' ').collect();

        if parts.len() != 2 {
            return Err(Self::Error::InvalidParts(value.to_string()));
        }

        if parts[0].to_lowercase().starts_with("hexpattern(") {
            if parts[1].ends_with(')') {
                parts[0] = &parts[0]["hexpattern(".len()..];
                parts[1] = &parts[1][..parts[1].len() - 1];
            } else {
                return Err(Self::Error::HangingHexPattern(value.to_string()));
            }
        }

        let direction: Direction =
            parts[0]
                .try_into()
                .map_err(|err| PatternParseError::InvalidStartDirection {
                    input: value.to_string(),
                    direction: match err {
                        DirectionParseError::InvalidNumber(_) => unreachable!(),
                        DirectionParseError::InvalidStr(str) => str,
                    },
                })?;

        let angles: Vec<Angle> = parts[1]
            .chars()
            .map(Angle::try_from)
            .collect::<Result<Vec<Angle>, AngleParseError>>()
            .map_err(|err| PatternParseError::InvalidAngle {
                input: value.to_string(),
                angle: err.0,
            })?;

        Ok(Pattern::new(direction, angles))
    }
}