eunoia 0.7.0

A library for creating area-proportional Euler and Venn diagrams
Documentation
//! Shape implementations for Euler and Venn diagrams.
//!
//! This module defines composable traits for geometric operations and diagram shapes.
//! The trait system is decomposed to allow code reuse across different geometric types:
//!
//! - Basic geometric properties: `Area`, `Centroid`, `Perimeter`, `BoundingBox`
//! - Spatial relations between same-type objects: `Distance`, `Closed`
//! - Diagram-specific operations: `DiagramShape` (composes all of the above)

use crate::geometry::primitives::Point;
use crate::geometry::shapes::Rectangle;

/// Trait for objects that have a computable distance to another object of the same type.
///
/// This can be implemented by both shapes (Circle, Point, LineSegment, etc.) to enable
/// generic distance-based algorithms.
pub trait Distance<Rhs = Self> {
    /// Computes the minimum distance between this object and another.
    /// For shapes, this is the distance between boundaries (0.0 if overlapping).
    /// For points, this is the Euclidean distance.
    fn distance(&self, other: &Rhs) -> f64;
}

/// Trait for objects that have a measurable area.
pub trait Area {
    /// Returns the area of the object.
    fn area(&self) -> f64;
}

/// Trait for objects that have a definable centroid (center of mass).
pub trait Centroid {
    /// Returns the centroid (center point) as a Point.
    fn centroid(&self) -> Point;
}

/// Trait for objects that have a measurable perimeter.
pub trait Perimeter {
    /// Computes the perimeter (boundary length) of the object.
    fn perimeter(&self) -> f64;
}

/// Trait for objects that can be bounded by a rectangle.
pub trait BoundingBox {
    /// Computes the axis-aligned bounding box as a Rectangle.
    fn bounding_box(&self) -> Rectangle;
}

/// Trait for spatial relationships between objects of the same type.
///
/// This covers containment, intersection testing, and computing intersection areas/points.
pub trait Closed: Sized + Area + BoundingBox + Perimeter + Centroid {
    /// Checks if this object contains another object entirely within its boundaries.
    fn contains(&self, other: &Self) -> bool;

    /// Checks if a point is inside the object (on the boundary or interior).
    fn contains_point(&self, point: &Point) -> bool;

    /// Checks if this object intersects with another object.
    fn intersects(&self, other: &Self) -> bool;

    /// Computes the area of intersection between this object and another.
    fn intersection_area(&self, other: &Self) -> f64;

    /// Computes the points where this object's boundary intersects another's.
    fn intersection_points(&self, other: &Self) -> Vec<Point>;
}

/// Trait for shapes that can be used in Euler and Venn diagrams.
///
/// This is a supertrait that combines all the geometric capabilities needed for
/// diagram construction and optimization. Types implementing this trait can be used
/// with the diagram fitter.
///
/// # Type Parameters
///
/// The associated methods for parameter conversion enable the optimization process:
/// 1. Initial layout uses circles (MDS algorithm)
/// 2. Circle parameters are converted to shape-specific parameters via `params_from_circle`
/// 3. Final optimization operates on shape-specific parameters
/// 4. Shapes are constructed from optimized parameters via `from_params`
pub trait DiagramShape: Closed {
    /// Compute all exclusive regions and their areas from a collection of shapes.
    ///
    /// This method should use exact geometric computation for the shape type.
    /// Returns a map from RegionMask (bit representation) to exclusive area.
    ///
    /// This is used during optimization to compute loss functions.
    fn compute_exclusive_regions(
        shapes: &[Self],
    ) -> std::collections::HashMap<crate::geometry::diagram::RegionMask, f64>
    where
        Self: Sized;

    /// Convert initial circle parameters to shape-specific parameters.
    ///
    /// Takes circle parameters (x, y, radius) and converts them to whatever
    /// parameters this shape type needs for optimization.
    ///
    /// For Circle: returns [x, y, r]
    /// For Ellipse: might return [x, y, a, b, angle] where a=b=r initially
    fn params_from_circle(x: f64, y: f64, radius: f64) -> Vec<f64>
    where
        Self: Sized;

    /// Center-to-center distance that would yield `target_overlap` between
    /// two shapes whose set areas are `area_i` and `area_j`. Used by the MDS
    /// initial-layout phase, which optimises 2D positions against a precomputed
    /// scalar distance matrix; this method is the shape-specific overlap-to-
    /// distance inversion that fills that matrix.
    ///
    /// Each shape inverts its own pairwise-overlap formula along a canonical
    /// direction (since axis-aligned squares overlap as a 2D function of
    /// `(|dx|, |dy|)`, the "distance" interpretation is shape-dependent):
    /// - Circle: closed-form lens-area inversion.
    /// - Ellipse: same as Circle (warm-start is a circle of the same area).
    /// - Square (axis-aligned): inverts overlap along the diagonal `|dx| = |dy|`,
    ///   giving `d = √2 · ((s_i+s_j)/2 − √target_overlap)`.
    fn mds_target_distance(
        area_i: f64,
        area_j: f64,
        target_overlap: f64,
    ) -> Result<f64, crate::error::DiagramError>
    where
        Self: Sized;

    /// Get the number of parameters needed for this shape type.
    ///
    /// For Circle: 3 (x, y, r)
    /// For Ellipse: 5 (x, y, a, b, angle)
    fn n_params() -> usize
    where
        Self: Sized;

    /// Construct a shape from optimized parameters.
    ///
    /// Takes a slice of parameters specific to this shape and constructs the shape.
    /// The parameters should match what params_from_circle produces.
    fn from_params(params: &[f64]) -> Self
    where
        Self: Sized;

    /// Convert the shape back to its parameter representation.
    ///
    /// This is the inverse of `from_params`. Returns the parameters that can
    /// reconstruct this shape.
    fn to_params(&self) -> Vec<f64>;

    /// Optional analytical gradient companion to `compute_exclusive_regions`.
    ///
    /// Returns `Some((exclusive_areas, gradients))` where each `gradients[mask]`
    /// is a length-`n_sets · Self::n_params()` vector of `∂A_excl[mask]/∂θ`,
    /// with `θ` ordered to match the flat parameter vector consumed by
    /// `from_params`. The default implementation returns `None`, signalling to
    /// the optimiser that finite differences should be used instead.
    fn compute_exclusive_regions_with_gradient(
        _shapes: &[Self],
    ) -> Option<ExclusiveRegionsAndGradient>
    where
        Self: Sized,
    {
        None
    }

    /// Canonical Venn-diagram layout for `n` sets, or `None` if no canonical
    /// arrangement exists for this shape at this `n`.
    ///
    /// Implementations should return shapes at unit scale (radius/side ~1),
    /// so callers can rescale the layout to match a spec's footprint.
    /// A returned layout must be a true Venn diagram: every one of the
    /// `2ⁿ − 1` non-empty subsets must have positive area.
    ///
    /// Used by [`crate::venn::VennDiagram`] to produce canonical layouts
    /// without invoking the fitter.
    fn canonical_venn_layout(n: usize) -> Option<Vec<Self>>
    where
        Self: Sized,
    {
        let _ = n;
        None
    }
}

/// Pair of (exclusive areas, exclusive gradients) returned by
/// [`DiagramShape::compute_exclusive_regions_with_gradient`]. The gradient
/// vectors are flat, length `n_sets * Self::n_params()`, mirroring the
/// optimiser's parameter layout.
pub type ExclusiveRegionsAndGradient = (
    std::collections::HashMap<crate::geometry::diagram::RegionMask, f64>,
    std::collections::HashMap<crate::geometry::diagram::RegionMask, Vec<f64>>,
);

/// Trait for converting shapes to polygons for visualization.
///
/// This trait allows analytical shapes (circles, ellipses) to be converted
/// to discrete polygon representations for plotting and export.
pub trait Polygonize {
    /// Convert the shape to a polygon with the specified number of vertices.
    ///
    /// # Arguments
    ///
    /// * `n_vertices` - Number of vertices in the resulting polygon (minimum 3)
    ///
    /// # Examples
    ///
    /// ```
    /// use eunoia::geometry::shapes::Circle;
    /// use eunoia::geometry::primitives::Point;
    /// use eunoia::geometry::traits::Polygonize;
    ///
    /// let circle = Circle::new(Point::new(0.0, 0.0), 5.0);
    /// let polygon = circle.polygonize(64);
    /// assert_eq!(polygon.vertices().len(), 64);
    /// ```
    fn polygonize(&self, n_vertices: usize) -> crate::geometry::shapes::Polygon;
}

/// Compute the bounding box for a collection of shapes.
pub fn bounding_box<S: BoundingBox>(shapes: &[S]) -> Rectangle {
    let mut min_x = f64::INFINITY;
    let mut min_y = f64::INFINITY;

    let mut max_x = f64::NEG_INFINITY;
    let mut max_y = f64::NEG_INFINITY;

    for shape in shapes {
        let points = shape.bounding_box().to_points();
        min_x = min_x.min(points.0.x());
        min_y = min_y.min(points.0.y());
        max_x = max_x.max(points.1.x());
        max_y = max_y.max(points.1.y());
    }

    Rectangle::from_corners(Point::new(min_x, min_y), Point::new(max_x, max_y))
}