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
}
fn degenerate_path(points: &[Point]) -> Option<String> {
match points.len() {
0 => Some(String::new()),
1 => {
let mut d = String::new();
push_move(&mut d, points[0].x, points[0].y);
Some(d)
}
_ => None,
}
}
fn push_move(d: &mut String, x: f64, y: f64) {
d.push_str(&format!("M{},{}", num(x), num(y)));
}
fn push_line(d: &mut String, x: f64, y: f64) {
d.push_str(&format!("L{},{}", num(x), num(y)));
}
fn push_bezier(d: &mut String, x1: f64, y1: f64, x2: f64, y2: f64, x: f64, y: f64) {
d.push_str(&format!(
"C{},{} {},{} {},{}",
num(x1),
num(y1),
num(x2),
num(y2),
num(x),
num(y)
));
}
fn push_quad(d: &mut String, x1: f64, y1: f64, x: f64, y: f64) {
d.push_str(&format!("Q{},{} {},{}", num(x1), num(y1), num(x), num(y)));
}
pub fn curve_natural_path(points: &[Point]) -> String {
if let Some(d) = degenerate_path(points) {
return d;
}
let mut d = String::new();
push_move(&mut d, points[0].x, points[0].y);
if points.len() == 2 {
push_line(&mut d, points[1].x, points[1].y);
return d;
}
let xs: Vec<f64> = points.iter().map(|p| p.x).collect();
let ys: Vec<f64> = points.iter().map(|p| p.y).collect();
let (cx0, cx1) = natural_control_points(&xs);
let (cy0, cy1) = natural_control_points(&ys);
for i in 1..points.len() {
push_bezier(
&mut d,
cx0[i - 1],
cy0[i - 1],
cx1[i - 1],
cy1[i - 1],
points[i].x,
points[i].y,
);
}
d
}
fn natural_control_points(x: &[f64]) -> (Vec<f64>, Vec<f64>) {
let n = x.len() - 1;
let mut a = vec![0.0; n];
let mut b = vec![0.0; n];
let mut r = vec![0.0; n];
a[0] = 0.0;
b[0] = 2.0;
r[0] = x[0] + 2.0 * x[1];
for i in 1..n - 1 {
a[i] = 1.0;
b[i] = 4.0;
r[i] = 4.0 * x[i] + 2.0 * x[i + 1];
}
a[n - 1] = 2.0;
b[n - 1] = 7.0;
r[n - 1] = 8.0 * x[n - 1] + x[n];
for i in 1..n {
let m = a[i] / b[i - 1];
b[i] -= m;
r[i] -= m * r[i - 1];
}
a[n - 1] = r[n - 1] / b[n - 1];
for i in (0..n - 1).rev() {
a[i] = (r[i] - a[i + 1]) / b[i];
}
b[n - 1] = (x[n] + a[n - 1]) / 2.0;
for i in 0..n - 1 {
b[i] = 2.0 * x[i + 1] - a[i + 1];
}
(a, b)
}
struct SplineState {
x0: f64,
y0: f64,
x1: f64,
y1: f64,
x2: f64,
y2: f64,
point: u8,
}
impl SplineState {
fn new() -> Self {
Self {
x0: f64::NAN,
y0: f64::NAN,
x1: f64::NAN,
y1: f64::NAN,
x2: f64::NAN,
y2: f64::NAN,
point: 0,
}
}
fn shift(&mut self, x: f64, y: f64) {
self.x0 = self.x1;
self.x1 = self.x2;
self.x2 = x;
self.y0 = self.y1;
self.y1 = self.y2;
self.y2 = y;
}
}
pub fn curve_cardinal_path(points: &[Point]) -> String {
curve_cardinal_like(points, 1.0 / 6.0)
}
fn curve_cardinal_like(points: &[Point], k: f64) -> String {
if let Some(d) = degenerate_path(points) {
return d;
}
let mut d = String::new();
let mut st = SplineState::new();
let emit = |st: &SplineState, d: &mut String, x: f64, y: f64| {
push_bezier(
d,
st.x1 + k * (st.x2 - st.x0),
st.y1 + k * (st.y2 - st.y0),
st.x2 + k * (st.x1 - x),
st.y2 + k * (st.y1 - y),
st.x2,
st.y2,
);
};
for p in points {
match st.point {
0 => {
st.point = 1;
push_move(&mut d, p.x, p.y);
}
1 => {
st.point = 2;
st.x1 = p.x;
st.y1 = p.y;
}
2 => {
st.point = 3;
emit(&st, &mut d, p.x, p.y);
}
_ => emit(&st, &mut d, p.x, p.y),
}
st.shift(p.x, p.y);
}
match st.point {
2 => push_line(&mut d, st.x2, st.y2),
3 => emit(&st, &mut d, st.x1, st.y1),
_ => {}
}
d
}
const D3_EPSILON: f64 = 1e-12;
pub fn curve_catmull_rom_path(points: &[Point]) -> String {
if let Some(d) = degenerate_path(points) {
return d;
}
let mut d = String::new();
let mut st = SplineState::new();
let mut l01_a = 0.0_f64;
let mut l12_a = 0.0_f64;
let mut l23_a = 0.0_f64;
let mut l01_2a = 0.0_f64;
let mut l12_2a = 0.0_f64;
let mut l23_2a = 0.0_f64;
let alpha = 0.5;
let emit = |st: &SplineState,
l01_a: f64,
l12_a: f64,
l23_a: f64,
l01_2a: f64,
l12_2a: f64,
l23_2a: f64,
d: &mut String,
x: f64,
y: f64| {
let (mut x1, mut y1) = (st.x1, st.y1);
let (mut x2, mut y2) = (st.x2, st.y2);
if l01_a > D3_EPSILON {
let a = 2.0 * l01_2a + 3.0 * l01_a * l12_a + l12_2a;
let n = 3.0 * l01_a * (l01_a + l12_a);
x1 = (x1 * a - st.x0 * l12_2a + st.x2 * l01_2a) / n;
y1 = (y1 * a - st.y0 * l12_2a + st.y2 * l01_2a) / n;
}
if l23_a > D3_EPSILON {
let b = 2.0 * l23_2a + 3.0 * l23_a * l12_a + l12_2a;
let m = 3.0 * l23_a * (l23_a + l12_a);
x2 = (x2 * b + st.x1 * l23_2a - x * l12_2a) / m;
y2 = (y2 * b + st.y1 * l23_2a - y * l12_2a) / m;
}
push_bezier(d, x1, y1, x2, y2, st.x2, st.y2);
};
for p in points {
if st.point != 0 {
let x23 = st.x2 - p.x;
let y23 = st.y2 - p.y;
l23_2a = (x23 * x23 + y23 * y23).powf(alpha);
l23_a = l23_2a.sqrt();
}
match st.point {
0 => {
st.point = 1;
push_move(&mut d, p.x, p.y);
}
1 => st.point = 2,
2 => {
st.point = 3;
emit(
&st, l01_a, l12_a, l23_a, l01_2a, l12_2a, l23_2a, &mut d, p.x, p.y,
);
}
_ => emit(
&st, l01_a, l12_a, l23_a, l01_2a, l12_2a, l23_2a, &mut d, p.x, p.y,
),
}
l01_a = l12_a;
l12_a = l23_a;
l01_2a = l12_2a;
l12_2a = l23_2a;
st.shift(p.x, p.y);
}
match st.point {
2 => push_line(&mut d, st.x2, st.y2),
3 => emit(
&st, l01_a, l12_a, 0.0, l01_2a, l12_2a, l23_2a, &mut d, st.x2, st.y2,
),
_ => {}
}
d
}
fn js_sign(x: f64) -> f64 {
if x < 0.0 {
-1.0
} else {
1.0
}
}
fn signed_zero_or(h0: f64, h1: f64) -> f64 {
if h0 != 0.0 {
h0
} else if h1 < 0.0 {
-0.0
} else {
0.0
}
}
fn js_min(a: f64, b: f64) -> f64 {
if a.is_nan() || b.is_nan() {
f64::NAN
} else {
a.min(b)
}
}
fn slope3(x0: f64, y0: f64, x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
let h0 = x1 - x0;
let h1 = x2 - x1;
let s0 = (y1 - y0) / signed_zero_or(h0, h1);
let s1 = (y2 - y1) / signed_zero_or(h1, h0);
let p = (s0 * h1 + s1 * h0) / (h0 + h1);
let m = (js_sign(s0) + js_sign(s1)) * js_min(js_min(s0.abs(), s1.abs()), 0.5 * p.abs());
if m.is_nan() || m == 0.0 {
0.0
} else {
m
}
}
fn slope2(x0: f64, y0: f64, x1: f64, y1: f64, t: f64) -> f64 {
let h = x1 - x0;
if h != 0.0 {
(3.0 * (y1 - y0) / h - t) / 2.0
} else {
t
}
}
struct Monotone {
reflect: bool,
a0: f64,
b0: f64,
a1: f64,
b1: f64,
t0: f64,
point: u8,
}
impl Monotone {
fn new(reflect: bool) -> Self {
Self {
reflect,
a0: f64::NAN,
b0: f64::NAN,
a1: f64::NAN,
b1: f64::NAN,
t0: f64::NAN,
point: 0,
}
}
fn push_move(&self, d: &mut String, a: f64, b: f64) {
if self.reflect {
push_move(d, b, a);
} else {
push_move(d, a, b);
}
}
fn push_line(&self, d: &mut String, a: f64, b: f64) {
if self.reflect {
push_line(d, b, a);
} else {
push_line(d, a, b);
}
}
fn emit_hermite(&self, d: &mut String, t0: f64, t1: f64) {
let dx = (self.a1 - self.a0) / 3.0;
let (c1a, c1b) = (self.a0 + dx, self.b0 + dx * t0);
let (c2a, c2b) = (self.a1 - dx, self.b1 - dx * t1);
let (ea, eb) = (self.a1, self.b1);
if self.reflect {
push_bezier(d, c1b, c1a, c2b, c2a, eb, ea);
} else {
push_bezier(d, c1a, c1b, c2a, c2b, ea, eb);
}
}
fn point(&mut self, d: &mut String, x: f64, y: f64) {
let (a, b) = if self.reflect { (y, x) } else { (x, y) };
if a == self.a1 && b == self.b1 {
return; }
let mut t1 = f64::NAN;
match self.point {
0 => {
self.point = 1;
self.push_move(d, a, b);
}
1 => self.point = 2,
2 => {
self.point = 3;
t1 = slope3(self.a0, self.b0, self.a1, self.b1, a, b);
let t0 = slope2(self.a0, self.b0, self.a1, self.b1, t1);
self.emit_hermite(d, t0, t1);
}
_ => {
t1 = slope3(self.a0, self.b0, self.a1, self.b1, a, b);
self.emit_hermite(d, self.t0, t1);
}
}
self.a0 = self.a1;
self.a1 = a;
self.b0 = self.b1;
self.b1 = b;
self.t0 = t1;
}
fn line_end(&mut self, d: &mut String) {
match self.point {
2 => self.push_line(d, self.a1, self.b1),
3 => {
let t1 = slope2(self.a0, self.b0, self.a1, self.b1, self.t0);
self.emit_hermite(d, self.t0, t1);
}
_ => {}
}
}
}
fn curve_monotone_path(points: &[Point], reflect: bool) -> String {
if let Some(d) = degenerate_path(points) {
return d;
}
let mut d = String::new();
let mut st = Monotone::new(reflect);
for p in points {
st.point(&mut d, p.x, p.y);
}
st.line_end(&mut d);
d
}
pub fn curve_monotone_x_path(points: &[Point]) -> String {
curve_monotone_path(points, false)
}
pub fn curve_monotone_y_path(points: &[Point]) -> String {
curve_monotone_path(points, true)
}
fn curve_bump_path(points: &[Point], is_x: bool) -> String {
if let Some(d) = degenerate_path(points) {
return d;
}
let mut d = String::new();
push_move(&mut d, points[0].x, points[0].y);
let (mut x0, mut y0) = (points[0].x, points[0].y);
for p in &points[1..] {
if is_x {
let mx = (x0 + p.x) / 2.0;
push_bezier(&mut d, mx, y0, mx, p.y, p.x, p.y);
} else {
let my = (y0 + p.y) / 2.0;
push_bezier(&mut d, x0, my, p.x, my, p.x, p.y);
}
x0 = p.x;
y0 = p.y;
}
d
}
pub fn curve_bump_x_path(points: &[Point]) -> String {
curve_bump_path(points, true)
}
pub fn curve_bump_y_path(points: &[Point]) -> String {
curve_bump_path(points, false)
}
pub fn rounded_path(points: &[Point], radius: f64) -> String {
if points.len() < 2 {
return degenerate_path(points).unwrap_or_default();
}
const EPSILON: f64 = 1e-5;
let mut d = String::new();
let last = points.len() - 1;
for i in 0..points.len() {
let curr = &points[i];
if i == 0 {
push_move(&mut d, curr.x, curr.y);
continue;
}
if i == last {
push_line(&mut d, curr.x, curr.y);
continue;
}
let prev = &points[i - 1];
let next = &points[i + 1];
let (dx1, dy1) = (curr.x - prev.x, curr.y - prev.y);
let (dx2, dy2) = (next.x - curr.x, next.y - curr.y);
let len1 = dx1.hypot(dy1);
let len2 = dx2.hypot(dy2);
if len1 < EPSILON || len2 < EPSILON {
push_line(&mut d, curr.x, curr.y);
continue;
}
let (nx1, ny1) = (dx1 / len1, dy1 / len1);
let (nx2, ny2) = (dx2 / len2, dy2 / len2);
let dot = (nx1 * nx2 + ny1 * ny2).clamp(-1.0, 1.0);
let angle = dot.acos();
if angle < EPSILON || (std::f64::consts::PI - angle).abs() < EPSILON {
push_line(&mut d, curr.x, curr.y);
continue;
}
let cut_len = (radius / (angle / 2.0).sin())
.min(len1 / 2.0)
.min(len2 / 2.0);
let (start_x, start_y) = (curr.x - nx1 * cut_len, curr.y - ny1 * cut_len);
let (end_x, end_y) = (curr.x + nx2 * cut_len, curr.y + ny2 * cut_len);
push_line(&mut d, start_x, start_y);
push_quad(&mut d, curr.x, curr.y, end_x, end_y);
}
d
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Curve {
#[default]
Basis,
Linear,
Step,
StepBefore,
StepAfter,
Natural,
Cardinal,
CatmullRom,
MonotoneX,
MonotoneY,
BumpX,
BumpY,
Rounded,
}
impl Curve {
pub fn parse(s: &str) -> Curve {
match s {
"linear" => Curve::Linear,
"step" => Curve::Step,
"stepBefore" => Curve::StepBefore,
"stepAfter" => Curve::StepAfter,
"natural" => Curve::Natural,
"cardinal" => Curve::Cardinal,
"catmullRom" => Curve::CatmullRom,
"monotoneX" => Curve::MonotoneX,
"monotoneY" => Curve::MonotoneY,
"bumpX" => Curve::BumpX,
"bumpY" => Curve::BumpY,
"rounded" => Curve::Rounded,
_ => Curve::Basis,
}
}
pub fn rounds_corners(self) -> bool {
!matches!(self, Curve::Rounded)
}
pub fn path(self, points: &[Point]) -> String {
match self {
Curve::Basis => curve_basis_path(points),
Curve::Linear => polyline_path(points),
Curve::Step => step_path(points, 0.5),
Curve::StepBefore => step_path(points, 0.0),
Curve::StepAfter => step_path(points, 1.0),
Curve::Natural => curve_natural_path(points),
Curve::Cardinal => curve_cardinal_path(points),
Curve::CatmullRom => curve_catmull_rom_path(points),
Curve::MonotoneX => curve_monotone_x_path(points),
Curve::MonotoneY => curve_monotone_y_path(points),
Curve::BumpX => curve_bump_x_path(points),
Curve::BumpY => curve_bump_y_path(points),
Curve::Rounded => rounded_path(points, CORNER_RADIUS),
}
}
}
pub fn step_path(points: &[Point], t: f64) -> String {
let mut d = String::new();
let Some(first) = points.first() else {
return d;
};
d.push_str(&format!("M{},{}", num(first.x), num(first.y)));
for w in points.windows(2) {
let (p0, p1) = (&w[0], &w[1]);
let mid_x = p0.x * (1.0 - t) + p1.x * t;
if (mid_x - p0.x).abs() > EPS {
d.push_str(&format!("L{},{}", num(mid_x), num(p0.y)));
}
d.push_str(&format!("L{},{}", num(mid_x), num(p1.y)));
if (mid_x - p1.x).abs() > EPS {
d.push_str(&format!("L{},{}", num(p1.x), num(p1.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),
]
}