use crate::preview::mermaid::flowchart::Arrow;
use crate::preview::mermaid::layout::Point;
use super::clusters;
use super::shapes::{self, Glyph, Size};
use super::svg::num;
pub const CORNER_RADIUS: f64 = 5.0;
pub const ARROW_LENGTH: f64 = 9.0;
pub const ARROW_HALF_WIDTH: f64 = 4.0;
pub const CIRCLE_RADIUS: f64 = 4.0;
pub const CROSS_HALF: f64 = 4.5;
const EPS: f64 = 1e-6;
#[derive(Debug, Clone, PartialEq)]
pub enum End {
Node(Glyph, Point, Size),
Cluster(clusters::Rect),
}
pub fn route(points: &[Point], tail: &End, head: &End) -> Vec<Point> {
if let (End::Node(ts, tc, tz), End::Node(hs, hc, hz)) = (tail, head) {
return clip(points, (*ts, tc.clone(), *tz), (*hs, hc.clone(), *hz));
}
let mut pts: Vec<Point> = points.to_vec();
dedupe(&mut pts);
if pts.is_empty() {
return Vec::new();
}
if let End::Cluster(rect) = tail {
pts = clusters::cut_start(&pts, rect);
}
if let End::Cluster(rect) = head {
pts = clusters::cut_end(&pts, rect);
}
if matches!(head, End::Node(..)) && pts.len() >= 2 {
pts.pop();
}
if matches!(tail, End::Node(..)) && pts.len() >= 2 {
pts.remove(0);
}
if let End::Node(shape, center, size) = tail {
let target = pts.first().cloned().unwrap_or_else(|| center.clone());
pts.insert(0, shapes::intersect(*shape, center.clone(), *size, &target));
}
if let End::Node(shape, center, size) = head {
let target = pts.last().cloned().unwrap_or_else(|| center.clone());
pts.push(shapes::intersect(*shape, center.clone(), *size, &target));
}
dedupe(&mut pts);
pts
}
pub fn clip(
points: &[Point],
tail: (Glyph, Point, Size),
head: (Glyph, Point, Size),
) -> Vec<Point> {
let (tail_shape, tail_center, tail_size) = tail;
let (head_shape, head_center, head_size) = head;
let mut inner: Vec<Point> = if points.len() >= 3 {
points[1..points.len() - 1].to_vec()
} else {
Vec::new()
};
dedupe(&mut inner);
let first_target = inner
.first()
.cloned()
.unwrap_or_else(|| head_center.clone());
let last_target = inner.last().cloned().unwrap_or_else(|| tail_center.clone());
let start = shapes::intersect(tail_shape, tail_center, tail_size, &first_target);
let end = shapes::intersect(head_shape, head_center, head_size, &last_target);
let mut out = Vec::with_capacity(inner.len() + 2);
out.push(start);
out.append(&mut inner);
out.push(end);
dedupe(&mut out);
out
}
pub fn dedupe(points: &mut Vec<Point>) {
points.dedup_by(|a, b| (a.x - b.x).abs() < EPS && (a.y - b.y).abs() < EPS);
}
fn corner_positions(points: &[Point]) -> Vec<usize> {
let mut out = Vec::new();
if points.len() < 3 {
return out;
}
for i in 1..points.len() - 1 {
let (prev, curr, next) = (&points[i - 1], &points[i], &points[i + 1]);
let vertical_then_horizontal = prev.x == curr.x
&& curr.y == next.y
&& (curr.x - next.x).abs() > 5.0
&& (curr.y - prev.y).abs() > 5.0;
let horizontal_then_vertical = prev.y == curr.y
&& curr.x == next.x
&& (curr.x - prev.x).abs() > 5.0
&& (curr.y - next.y).abs() > 5.0;
if vertical_then_horizontal || horizontal_then_vertical {
out.push(i);
}
}
out
}
fn adjacent_point(a: &Point, b: &Point, distance: f64) -> Point {
let dx = b.x - a.x;
let dy = b.y - a.y;
let len = (dx * dx + dy * dy).sqrt();
if len == 0.0 {
return b.clone();
}
let ratio = distance / len;
Point::new(b.x - ratio * dx, b.y - ratio * dy)
}
pub fn fix_corners(line: &[Point]) -> Vec<Point> {
let corners = corner_positions(line);
if corners.is_empty() {
return line.to_vec();
}
let mut out = Vec::with_capacity(line.len() + corners.len() * 2);
for (i, point) in line.iter().enumerate() {
if !corners.contains(&i) {
out.push(point.clone());
continue;
}
let prev = &line[i - 1];
let next = &line[i + 1];
let corner = point;
let new_prev = adjacent_point(prev, corner, CORNER_RADIUS);
let new_next = adjacent_point(next, corner, CORNER_RADIUS);
let x_diff = new_next.x - new_prev.x;
let y_diff = new_next.y - new_prev.y;
out.push(new_prev.clone());
let a = std::f64::consts::SQRT_2 * 2.0;
let mut new_corner = corner.clone();
if (next.x - prev.x).abs() > 10.0 && (next.y - prev.y).abs() >= 10.0 {
let r = CORNER_RADIUS;
if corner.x == new_prev.x {
new_corner = Point::new(
if x_diff < 0.0 {
new_prev.x - r + a
} else {
new_prev.x + r - a
},
if y_diff < 0.0 {
new_prev.y - a
} else {
new_prev.y + a
},
);
} else {
new_corner = Point::new(
if x_diff < 0.0 {
new_prev.x - a
} else {
new_prev.x + a
},
if y_diff < 0.0 {
new_prev.y - r + a
} else {
new_prev.y + r - a
},
);
}
}
out.push(new_corner);
out.push(new_next);
}
out
}
pub fn length(points: &[Point]) -> f64 {
points
.windows(2)
.map(|w| (w[1].x - w[0].x).hypot(w[1].y - w[0].y))
.sum()
}
pub fn arc_midpoint(points: &[Point]) -> Option<Point> {
match points.len() {
0 => None,
1 => Some(points[0].clone()),
_ => {
let mut remaining = length(points) / 2.0;
for w in points.windows(2) {
let d = (w[1].x - w[0].x).hypot(w[1].y - w[0].y);
if d == 0.0 {
continue;
}
if d < remaining {
remaining -= d;
continue;
}
let t = remaining / d;
return Some(Point::new(
w[0].x + t * (w[1].x - w[0].x),
w[0].y + t * (w[1].y - w[0].y),
));
}
points.last().cloned()
}
}
}
pub fn trim_end(points: &[Point], by: f64) -> Vec<Point> {
if by <= 0.0 || points.len() < 2 || length(points) <= by + 1.0 {
return points.to_vec();
}
let mut out = points.to_vec();
let mut budget = by;
while out.len() >= 2 {
let n = out.len();
let (a, b) = (out[n - 2].clone(), out[n - 1].clone());
let d = (b.x - a.x).hypot(b.y - a.y);
if d <= budget {
budget -= d;
out.pop();
continue;
}
let t = (d - budget) / d;
out[n - 1] = Point::new(a.x + t * (b.x - a.x), a.y + t * (b.y - a.y));
break;
}
out
}
pub fn trim_start(points: &[Point], by: f64) -> Vec<Point> {
let mut reversed: Vec<Point> = points.iter().rev().cloned().collect();
reversed = trim_end(&reversed, by);
reversed.reverse();
reversed
}
pub const TRIANGLE_LENGTH: f64 = 12.0;
pub const TRIANGLE_HALF_WIDTH: f64 = 7.0;
pub const DIAMOND_LENGTH: f64 = 16.0;
pub const DIAMOND_HALF_WIDTH: f64 = 5.5;
pub const LOLLIPOP_RADIUS: f64 = 5.0;
pub const ER_NEAR: f64 = 8.0;
pub const ER_FOOT_LENGTH: f64 = 10.0;
pub const ER_FAR: f64 = 18.0;
pub const ER_BAR_HALF: f64 = 6.0;
pub const ER_FOOT_HALF: f64 = 9.0;
pub const ER_RING_RADIUS: f64 = 4.0;
pub const ASYNC_LENGTH: f64 = 10.0;
pub const ASYNC_HALF_WIDTH: f64 = 5.0;
pub const ASYNC_NOTCH: f64 = 4.5;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Tip {
#[default]
None,
Arrow,
Cross,
Circle,
HollowTriangle,
FilledDiamond,
HollowDiamond,
Lollipop,
ErOnlyOne,
ErZeroOrOne,
ErOneOrMore,
ErZeroOrMore,
Async,
}
impl Tip {
pub fn of_arrow(arrow: Arrow) -> (Tip, Tip) {
match arrow {
Arrow::None | Arrow::Invalid => (Tip::None, Tip::None),
Arrow::Point => (Tip::None, Tip::Arrow),
Arrow::Cross => (Tip::None, Tip::Cross),
Arrow::Circle => (Tip::None, Tip::Circle),
Arrow::DoublePoint => (Tip::Arrow, Tip::Arrow),
Arrow::DoubleCross => (Tip::Cross, Tip::Cross),
Arrow::DoubleCircle => (Tip::Circle, Tip::Circle),
}
}
pub fn room(self) -> f64 {
match self {
Tip::None => 0.0,
Tip::Arrow => ARROW_LENGTH,
Tip::Cross => CROSS_HALF * 2.0,
Tip::Circle => CIRCLE_RADIUS * 2.0,
Tip::HollowTriangle => TRIANGLE_LENGTH,
Tip::FilledDiamond | Tip::HollowDiamond => DIAMOND_LENGTH,
Tip::Lollipop => LOLLIPOP_RADIUS * 2.0,
Tip::ErOnlyOne | Tip::ErZeroOrOne => 0.0,
Tip::ErOneOrMore | Tip::ErZeroOrMore => ER_FOOT_LENGTH,
Tip::Async => ASYNC_NOTCH,
}
}
}
pub fn terminator_lengths(start: Tip, end: Tip) -> (f64, f64) {
(start.room(), end.room())
}
pub fn diamond(from: &Point, tip: &Point) -> [Point; 4] {
let (ux, uy) = unit(from, tip);
let (nx, ny) = (-uy * DIAMOND_HALF_WIDTH, ux * DIAMOND_HALF_WIDTH);
let mid = Point::new(
tip.x - ux * DIAMOND_LENGTH / 2.0,
tip.y - uy * DIAMOND_LENGTH / 2.0,
);
let back = Point::new(tip.x - ux * DIAMOND_LENGTH, tip.y - uy * DIAMOND_LENGTH);
[
tip.clone(),
Point::new(mid.x + nx, mid.y + ny),
back,
Point::new(mid.x - nx, mid.y - ny),
]
}
pub fn async_head(from: &Point, tip: &Point) -> [Point; 4] {
let (ux, uy) = unit(from, tip);
let back = Point::new(tip.x - ux * ASYNC_LENGTH, tip.y - uy * ASYNC_LENGTH);
let (nx, ny) = (-uy * ASYNC_HALF_WIDTH, ux * ASYNC_HALF_WIDTH);
[
tip.clone(),
Point::new(back.x + nx, back.y + ny),
Point::new(tip.x - ux * ASYNC_NOTCH, tip.y - uy * ASYNC_NOTCH),
Point::new(back.x - nx, back.y - ny),
]
}
pub fn triangle(from: &Point, tip: &Point) -> [Point; 3] {
let (ux, uy) = unit(from, tip);
let base = Point::new(tip.x - ux * TRIANGLE_LENGTH, tip.y - uy * TRIANGLE_LENGTH);
let (nx, ny) = (-uy * TRIANGLE_HALF_WIDTH, ux * TRIANGLE_HALF_WIDTH);
[
tip.clone(),
Point::new(base.x + nx, base.y + ny),
Point::new(base.x - nx, base.y - ny),
]
}
pub fn cross_bar(from: &Point, tip: &Point, distance: f64, half: f64) -> (Point, Point) {
let (ux, uy) = unit(from, tip);
let c = Point::new(tip.x - ux * distance, tip.y - uy * distance);
let (nx, ny) = (-uy * half, ux * half);
(
Point::new(c.x + nx, c.y + ny),
Point::new(c.x - nx, c.y - ny),
)
}
pub fn back_along(from: &Point, tip: &Point, distance: f64) -> Point {
let (ux, uy) = unit(from, tip);
Point::new(tip.x - ux * distance, tip.y - uy * distance)
}
pub fn crows_foot(from: &Point, tip: &Point) -> [(Point, Point); 3] {
let (ux, uy) = unit(from, tip);
let apex = Point::new(tip.x - ux * ER_FOOT_LENGTH, tip.y - uy * ER_FOOT_LENGTH);
let (nx, ny) = (-uy * ER_FOOT_HALF, ux * ER_FOOT_HALF);
[
(apex.clone(), tip.clone()),
(apex.clone(), Point::new(tip.x + nx, tip.y + ny)),
(apex, Point::new(tip.x - nx, tip.y - ny)),
]
}
pub const CARDINALITY_SET_BACK: f64 = 18.0;
pub const CARDINALITY_GAP: f64 = 3.0;
pub fn end_label_anchor(points: &[Point], at_start: bool, w: f64, h: f64) -> Option<Point> {
if points.len() < 2 {
return None;
}
let n = points.len();
let (tip, inward) = if at_start {
(&points[0], &points[1])
} else {
(&points[n - 1], &points[n - 2])
};
let (ux, uy) = unit(tip, inward);
let side = if at_start { 1.0 } else { -1.0 };
let (nx, ny) = (side * -uy, side * ux);
let along = CARDINALITY_SET_BACK.min(length(points) / 3.0);
let across = nx.abs() * w / 2.0 + ny.abs() * h / 2.0 + CARDINALITY_GAP;
Some(Point::new(
tip.x + ux * along + nx * across,
tip.y + uy * along + ny * across,
))
}
fn unit(from: &Point, tip: &Point) -> (f64, f64) {
let (dx, dy) = (tip.x - from.x, tip.y - from.y);
let len = dx.hypot(dy);
if len < EPS {
(1.0, 0.0)
} else {
(dx / len, dy / len)
}
}
pub fn polyline_path(points: &[Point]) -> String {
let mut d = String::new();
for (i, p) in points.iter().enumerate() {
d.push_str(&format!(
"{}{},{}",
if i == 0 { "M" } else { "L" },
num(p.x),
num(p.y)
));
if i + 1 < points.len() {
d.push(' ');
}
}
d
}
pub fn curve_basis_path(points: &[Point]) -> String {
let mut d = String::new();
match points.len() {
0 => return d,
1 => {
d.push_str(&format!("M{},{}", num(points[0].x), num(points[0].y)));
return d;
}
2 => {
d.push_str(&format!(
"M{},{}L{},{}",
num(points[0].x),
num(points[0].y),
num(points[1].x),
num(points[1].y)
));
return d;
}
_ => {}
}
let bezier = |p0: &Point, p1: &Point, p: &Point| {
format!(
"C{},{} {},{} {},{}",
num((2.0 * p0.x + p1.x) / 3.0),
num((2.0 * p0.y + p1.y) / 3.0),
num((p0.x + 2.0 * p1.x) / 3.0),
num((p0.y + 2.0 * p1.y) / 3.0),
num((p0.x + 4.0 * p1.x + p.x) / 6.0),
num((p0.y + 4.0 * p1.y + p.y) / 6.0),
)
};
d.push_str(&format!("M{},{}", num(points[0].x), num(points[0].y)));
d.push_str(&format!(
"L{},{}",
num((5.0 * points[0].x + points[1].x) / 6.0),
num((5.0 * points[0].y + points[1].y) / 6.0)
));
for i in 2..points.len() {
d.push_str(&bezier(&points[i - 2], &points[i - 1], &points[i]));
}
let last = points.len() - 1;
d.push_str(&bezier(&points[last - 1], &points[last], &points[last]));
d.push_str(&format!("L{},{}", num(points[last].x), num(points[last].y)));
d
}
pub fn arrow_head(from: &Point, tip: &Point) -> [Point; 3] {
let dx = tip.x - from.x;
let dy = tip.y - from.y;
let len = dx.hypot(dy);
let (ux, uy) = if len < EPS {
(1.0, 0.0)
} else {
(dx / len, dy / len)
};
let base = Point::new(tip.x - ux * ARROW_LENGTH, tip.y - uy * ARROW_LENGTH);
let (nx, ny) = (-uy * ARROW_HALF_WIDTH, ux * ARROW_HALF_WIDTH);
[
tip.clone(),
Point::new(base.x + nx, base.y + ny),
Point::new(base.x - nx, base.y - ny),
]
}