[go: up one dir, main page]

zng-unit 0.2.11

Part of the zng project.
Documentation
use crate::{Px, PxBox, PxPoint, PxRect, PxSize, PxVector};

use serde::{Deserialize, Serialize};

/// Orientation of two 2D items.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
pub enum Orientation2D {
    /// Point is above the origin.
    Above,
    /// Point is to the right of the origin.
    Right,
    /// Point is below the origin.
    Below,
    /// Point is to the left of the origin.
    Left,
}
impl Orientation2D {
    /// Check if `point` is orientation from `origin`.
    ///
    /// Returns `true` if the point is hit by a 45º frustum cast from origin in the direction defined by the orientation.
    pub fn point_is(self, origin: PxPoint, point: PxPoint) -> bool {
        let (a, b, c, d) = match self {
            Orientation2D::Above => (point.y, origin.y, point.x, origin.x),
            Orientation2D::Right => (origin.x, point.x, point.y, origin.y),
            Orientation2D::Below => (origin.y, point.y, point.x, origin.x),
            Orientation2D::Left => (point.x, origin.x, point.y, origin.y),
        };

        let mut is = false;

        // for 'Above' this is:
        // is above line?
        if a < b {
            // is to the right?
            if c > d {
                // is in the 45º 'frustum'
                // │?╱
                // │╱__
                is = c <= d + (b - a);
            } else {
                //  ╲?│
                // __╲│
                is = c >= d - (b - a);
            }
        }

        is
    }

    /// Check if `b` is orientation from `origin`.
    ///
    /// Returns `true` if the box `b` collides with the box `origin` in the direction defined by orientation. Also
    /// returns `true` if the boxes already overlap.
    pub fn box_is(self, origin: PxBox, b: PxBox) -> bool {
        fn d_intersects(a_min: Px, a_max: Px, b_min: Px, b_max: Px) -> bool {
            a_min < b_max && a_max > b_min
        }
        match self {
            Orientation2D::Above => b.min.y <= origin.min.y && d_intersects(b.min.x, b.max.x, origin.min.x, origin.max.x),
            Orientation2D::Left => b.min.x <= origin.min.x && d_intersects(b.min.y, b.max.y, origin.min.y, origin.max.y),
            Orientation2D::Below => b.max.y >= origin.max.y && d_intersects(b.min.x, b.max.x, origin.min.x, origin.max.x),
            Orientation2D::Right => b.max.x >= origin.max.x && d_intersects(b.min.y, b.max.y, origin.min.y, origin.max.y),
        }
    }

    /// Iterator that yields quadrants for efficient search in a quad-tree, if a point is inside a quadrant and
    /// passes the [`Orientation2D::point_is`] check it is in the orientation, them if it is within the `max_distance` it is valid.
    pub fn search_bounds(self, origin: PxPoint, max_distance: Px, spatial_bounds: PxBox) -> impl Iterator<Item = PxBox> {
        let mut bounds = PxRect::new(origin, PxSize::splat(max_distance));
        match self {
            Orientation2D::Above => {
                bounds.origin.x -= max_distance / Px(2);
                bounds.origin.y -= max_distance;
            }
            Orientation2D::Right => bounds.origin.y -= max_distance / Px(2),
            Orientation2D::Below => bounds.origin.x -= max_distance / Px(2),
            Orientation2D::Left => {
                bounds.origin.y -= max_distance / Px(2);
                bounds.origin.x -= max_distance;
            }
        }

        // oriented search is a 45º square in the direction specified, so we grow and cut the search quadrant like
        // in the "nearest with bounds" algorithm, but then cut again to only the part that fully overlaps the 45º
        // square, points found are then matched with the `Orientation2D::is` method.

        let max_quad = spatial_bounds.intersection_unchecked(&bounds.to_box2d());
        let mut is_none = max_quad.is_empty();

        let mut source_quad = PxRect::new(origin - PxVector::splat(Px(64)), PxSize::splat(Px(128))).to_box2d();
        let mut search_quad = source_quad.intersection_unchecked(&max_quad);
        is_none |= search_quad.is_empty();

        let max_diameter = max_distance * Px(2);

        let mut is_first = true;

        std::iter::from_fn(move || {
            let source_width = source_quad.width();
            if is_none {
                None
            } else if is_first {
                is_first = false;
                Some(search_quad)
            } else if source_width >= max_diameter {
                is_none = true;
                None
            } else {
                source_quad = source_quad.inflate(source_width, source_width);
                let mut new_search = source_quad.intersection_unchecked(&max_quad);
                if new_search == source_quad || new_search.is_empty() {
                    is_none = true; // filled bounds
                    return None;
                }

                match self {
                    Orientation2D::Above => {
                        new_search.max.y = search_quad.min.y;
                    }
                    Orientation2D::Right => {
                        new_search.min.x = search_quad.max.x;
                    }
                    Orientation2D::Below => {
                        new_search.min.y = search_quad.max.y;
                    }
                    Orientation2D::Left => {
                        new_search.max.x = search_quad.min.x;
                    }
                }

                search_quad = new_search;

                Some(search_quad)
            }
        })
    }
}