merman-render 0.8.0-alpha.4

Headless layout + SVG renderer for Mermaid (parity-focused; upstream SVG goldens).
Documentation
use super::config::EPSILON;
use crate::model::LayoutPoint;

#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct Rect {
    pub left: f64,
    pub right: f64,
    pub top: f64,
    pub bottom: f64,
}

impl Rect {
    pub fn from_center(x: f64, y: f64, width: f64, height: f64) -> Self {
        Self {
            left: x - width / 2.0,
            right: x + width / 2.0,
            top: y - height / 2.0,
            bottom: y + height / 2.0,
        }
    }

    pub fn width(self) -> f64 {
        (self.right - self.left).max(0.0)
    }

    pub fn height(self) -> f64 {
        (self.bottom - self.top).max(0.0)
    }

    pub fn center(self) -> LayoutPoint {
        LayoutPoint {
            x: (self.left + self.right) / 2.0,
            y: (self.top + self.bottom) / 2.0,
        }
    }

    pub fn union(&mut self, other: Self) {
        self.left = self.left.min(other.left);
        self.right = self.right.max(other.right);
        self.top = self.top.min(other.top);
        self.bottom = self.bottom.max(other.bottom);
    }

    pub fn inflated(self, amount: f64) -> Self {
        Self {
            left: self.left - amount,
            right: self.right + amount,
            top: self.top - amount,
            bottom: self.bottom + amount,
        }
    }

    pub fn contains_point(self, point: &LayoutPoint, inset: f64) -> bool {
        point.x > self.left + inset
            && point.x < self.right - inset
            && point.y > self.top + inset
            && point.y < self.bottom - inset
    }
}

pub(super) fn segment_blocked(
    from: &LayoutPoint,
    to: &LayoutPoint,
    obstacles: &[(String, Rect)],
    excluded: &[&str],
) -> bool {
    let min_x = from.x.min(to.x);
    let max_x = from.x.max(to.x);
    let min_y = from.y.min(to.y);
    let max_y = from.y.max(to.y);
    obstacles.iter().any(|(id, rect)| {
        if excluded.contains(&id.as_str()) {
            return false;
        }
        if (from.y - to.y).abs() <= EPSILON {
            rect.top < from.y && rect.bottom > from.y && rect.right > min_x && rect.left < max_x
        } else if (from.x - to.x).abs() <= EPSILON {
            rect.left < from.x && rect.right > from.x && rect.bottom > min_y && rect.top < max_y
        } else {
            true
        }
    })
}