use std::f64::consts::PI;
use crate::preview::mermaid::flowchart::Shape;
use crate::preview::mermaid::layout::intersect::{
intersect_ellipse, intersect_polygon, intersect_rect,
};
use crate::preview::mermaid::layout::Point;
use crate::preview::mermaid::text_metrics::FONT_SIZE;
use super::labels;
pub const PADDING: f64 = 15.0;
pub const CORNER_RADIUS: f64 = 5.0;
pub const DOUBLE_CIRCLE_GAP: f64 = 5.0;
pub const SUBROUTINE_FRAME: f64 = 8.0;
const CAP_SEGMENTS: usize = 24;
pub const STATE_MARKER_RADIUS: f64 = 7.0;
pub const STATE_END_INNER_RADIUS: f64 = 4.0;
pub const CHOICE_SIZE: f64 = 40.0;
pub const BAR_LENGTH: f64 = 70.0;
pub const BAR_THICKNESS: f64 = 10.0;
pub const NOTE_FOLD: f64 = 10.0;
pub const PARTICIPANT_PAD_X: f64 = 14.0;
pub const PARTICIPANT_PAD_Y: f64 = 10.0;
pub const PARTICIPANT_MIN_WIDTH: f64 = 62.0;
pub const ACTOR_FIGURE_HEIGHT: f64 = 32.0;
pub const ACTOR_HEAD_RADIUS: f64 = 6.0;
pub const BUMP: f64 = 6.0;
pub const UNDERLINE_GAP: f64 = 3.0;
pub const FACE_RADIUS: f64 = 11.0;
pub const ARROW_HEAD: f64 = 10.0;
pub const COMMIT_RADIUS: f64 = 8.0;
pub const TAG_POINT: f64 = 7.0;
pub const TAG_PAD_X: f64 = 5.0;
pub const TAG_PAD_Y: f64 = 3.0;
pub const CHAMFER: f64 = 6.0;
pub const TAG_TIP: f64 = 3.0;
pub const TAG_HOLE_RADIUS: f64 = 1.5;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Glyph {
Flow(Shape),
StateStart,
StateEnd,
Choice,
Bar {
horizontal: bool,
},
Note,
TitledBox,
ClassBox,
ErBox,
Participant,
Actor,
Wedge,
ChartBar,
ChartPoint,
Ribbon,
ChartLabel,
PlotFrame,
Graticule,
Cloud,
Bang,
Underline,
Face,
BlockArrow,
Reverted,
Tag,
ChamferedRect,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Mark {
Wedge {
start: f64,
sweep: f64,
},
Ribbon {
left_top: f64,
left_bottom: f64,
right_top: f64,
right_bottom: f64,
},
Face {
score: f64,
},
BlockArrow {
left: bool,
right: bool,
up: bool,
down: bool,
},
Graticule {
rings: usize,
spokes: usize,
polygon: bool,
},
}
impl From<Shape> for Glyph {
fn from(shape: Shape) -> Glyph {
Glyph::Flow(shape)
}
}
impl Default for Glyph {
fn default() -> Glyph {
Glyph::Flow(Shape::default())
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Size {
pub w: f64,
pub h: f64,
}
impl Size {
pub fn new(w: f64, h: f64) -> Size {
Size { w, h }
}
}
fn flow_size(shape: Shape, label: Size) -> Size {
let (lw, lh) = (label.w, label.h);
match shape {
Shape::Rect => Size::new(lw + PADDING * 4.0, lh + PADDING * 2.0),
Shape::RoundedRect => Size::new(lw + PADDING * 2.0, lh + PADDING * 2.0),
Shape::Stadium => {
let h = lh + PADDING;
Size::new(lw + h / 4.0 + PADDING, h)
}
Shape::Circle => {
let r = lw / 2.0 + PADDING;
Size::new(r * 2.0, r * 2.0)
}
Shape::DoubleCircle => {
let r = lw / 2.0 + PADDING;
Size::new(r * 2.0, r * 2.0)
}
Shape::Diamond => {
let s = (lw + PADDING) + (lh + PADDING);
Size::new(s, s)
}
Shape::Hexagon => {
let h = lh + PADDING;
Size::new(lw + h / 2.0 + PADDING, h)
}
Shape::Subroutine => Size::new(lw + 2.0 * SUBROUTINE_FRAME + PADDING, lh + PADDING),
Shape::Cylinder => {
let w = lw + PADDING;
let ry = cylinder_ry(w);
Size::new(w, lh + PADDING + 3.0 * ry)
}
Shape::Trapezoid | Shape::LeanRight | Shape::LeanLeft => {
let h = lh + PADDING;
Size::new(lw + PADDING + h, h)
}
Shape::InvTrapezoid => {
let h = lh + PADDING * 2.0;
Size::new(lw + PADDING * 2.0 + h, h)
}
Shape::Odd => {
let h = lh + PADDING;
Size::new(lw + PADDING + h / 4.0, h)
}
Shape::Text => Size::new(lw + PADDING, lh + PADDING),
}
}
fn cylinder_ry(w: f64) -> f64 {
let rx = w / 2.0;
rx / (2.5 + w / 50.0)
}
#[derive(Debug, Clone, PartialEq)]
pub enum Outline {
Rect {
w: f64,
h: f64,
r: f64,
},
Circle {
r: f64,
},
DoubleCircle {
outer: f64,
inner: f64,
},
Polygon(Vec<Point>),
Subroutine {
w: f64,
h: f64,
inset: f64,
},
Cylinder {
w: f64,
h: f64,
ry: f64,
},
Cloud {
w: f64,
h: f64,
},
Underline {
w: f64,
h: f64,
},
Face {
r: f64,
score: f64,
},
Crossed {
r: f64,
},
Tag {
w: f64,
h: f64,
point: f64,
tip: f64,
hole: f64,
},
Disc {
r: f64,
},
Target {
outer: f64,
inner: f64,
},
Bar {
w: f64,
h: f64,
},
Note {
w: f64,
h: f64,
fold: f64,
},
Actor {
w: f64,
h: f64,
figure_h: f64,
},
Wedge {
r: f64,
start: f64,
sweep: f64,
},
Ribbon {
w: f64,
left_top: f64,
left_bottom: f64,
right_top: f64,
right_bottom: f64,
},
Graticule {
r: f64,
rings: usize,
spokes: usize,
polygon: bool,
},
None,
}
fn flow_outline(shape: Shape, size: Size) -> Outline {
let (w, h) = (size.w, size.h);
match shape {
Shape::Rect => Outline::Rect { w, h, r: 0.0 },
Shape::RoundedRect => Outline::Rect {
w,
h,
r: CORNER_RADIUS,
},
Shape::Stadium => Outline::Rect { w, h, r: h / 2.0 },
Shape::Circle => Outline::Circle { r: w / 2.0 },
Shape::DoubleCircle => Outline::DoubleCircle {
outer: w / 2.0,
inner: (w / 2.0 - DOUBLE_CIRCLE_GAP).max(0.0),
},
Shape::Subroutine => Outline::Subroutine {
w,
h,
inset: SUBROUTINE_FRAME,
},
Shape::Cylinder => Outline::Cylinder {
w,
h,
ry: cylinder_ry(w),
},
Shape::Text => Outline::None,
_ => Outline::Polygon(flow_polygon(shape, size)),
}
}
fn flow_polygon(shape: Shape, size: Size) -> Vec<Point> {
let (w, h) = (size.w, size.h);
let (hw, hh) = (w / 2.0, h / 2.0);
match shape {
Shape::Diamond => vec![
Point::new(0.0, -hh),
Point::new(hw, 0.0),
Point::new(0.0, hh),
Point::new(-hw, 0.0),
],
Shape::Hexagon => {
let m = h / 4.0;
vec![
Point::new(-hw + m, -hh),
Point::new(hw - m, -hh),
Point::new(hw, 0.0),
Point::new(hw - m, hh),
Point::new(-hw + m, hh),
Point::new(-hw, 0.0),
]
}
Shape::Trapezoid => {
let t = (hw - hh).max(0.0);
vec![
Point::new(-hw, hh),
Point::new(hw, hh),
Point::new(t, -hh),
Point::new(-t, -hh),
]
}
Shape::InvTrapezoid => {
let t = (hw - hh).max(0.0);
vec![
Point::new(-t, hh),
Point::new(t, hh),
Point::new(hw, -hh),
Point::new(-hw, -hh),
]
}
Shape::LeanRight => {
let t = (hw - hh).max(0.0);
vec![
Point::new(-hw, hh),
Point::new(t, hh),
Point::new(hw, -hh),
Point::new(-t, -hh),
]
}
Shape::LeanLeft => {
let t = (hw - hh).max(0.0);
vec![
Point::new(-t, hh),
Point::new(hw, hh),
Point::new(t, -hh),
Point::new(-hw, -hh),
]
}
Shape::Odd => {
let notch = h / 4.0;
vec![
Point::new(-hw, -hh),
Point::new(-hw + notch, 0.0),
Point::new(-hw, hh),
Point::new(hw, hh),
Point::new(hw, -hh),
]
}
Shape::Stadium => {
let r = hh;
let a = (hw - r).max(0.0);
let mut pts = vec![Point::new(-a, -r), Point::new(a, -r)];
for i in 1..CAP_SEGMENTS {
let t = -PI / 2.0 + PI * i as f64 / CAP_SEGMENTS as f64;
pts.push(Point::new(a + r * t.cos(), r * t.sin()));
}
pts.push(Point::new(a, r));
pts.push(Point::new(-a, r));
for i in 1..CAP_SEGMENTS {
let t = PI / 2.0 + PI * i as f64 / CAP_SEGMENTS as f64;
pts.push(Point::new(-a + r * t.cos(), r * t.sin()));
}
pts
}
_ => Vec::new(),
}
}
fn flow_intersect(shape: Shape, center: Point, size: Size, target: &Point) -> Point {
match shape {
Shape::Circle | Shape::DoubleCircle => {
intersect_ellipse(center.x, center.y, size.w / 2.0, size.h / 2.0, target)
}
Shape::Rect | Shape::RoundedRect | Shape::Subroutine | Shape::Cylinder | Shape::Text => {
intersect_rect(center.x, center.y, size.w, size.h, target)
}
_ => {
let verts: Vec<Point> = flow_polygon(shape, size)
.into_iter()
.map(|p| Point::new(center.x + p.x, center.y + p.y))
.collect();
if verts.len() < 3 {
return intersect_rect(center.x, center.y, size.w, size.h, target);
}
intersect_polygon(&verts, ¢er, target)
}
}
}
pub fn size(glyph: Glyph, label: Size) -> Size {
match glyph {
Glyph::Flow(shape) => flow_size(shape, label),
Glyph::StateStart | Glyph::StateEnd => {
Size::new(STATE_MARKER_RADIUS * 2.0, STATE_MARKER_RADIUS * 2.0)
}
Glyph::Choice => Size::new(CHOICE_SIZE, CHOICE_SIZE),
Glyph::Bar { horizontal } => {
if horizontal {
Size::new(BAR_LENGTH, BAR_THICKNESS)
} else {
Size::new(BAR_THICKNESS, BAR_LENGTH)
}
}
Glyph::Note => Size::new(
(label.w + PADDING * 2.0).max(NOTE_FOLD * 3.0),
(label.h + PADDING * 2.0).max(NOTE_FOLD * 2.0),
),
Glyph::TitledBox => Size::new(label.w + PADDING * 2.0, label.h + PADDING * 2.0),
Glyph::ClassBox | Glyph::ErBox => label,
Glyph::Participant => Size::new(
(label.w + PARTICIPANT_PAD_X * 2.0).max(PARTICIPANT_MIN_WIDTH),
label.h + PARTICIPANT_PAD_Y * 2.0,
),
Glyph::Actor => label,
Glyph::Wedge
| Glyph::ChartBar
| Glyph::ChartPoint
| Glyph::Ribbon
| Glyph::PlotFrame
| Glyph::Graticule => label,
Glyph::ChartLabel => label,
Glyph::Cloud | Glyph::Bang => Size::new(
label.w + PADDING * 2.0 + BUMP * 2.0,
label.h + PADDING * 2.0 + BUMP * 2.0,
),
Glyph::Underline => Size::new(label.w + PADDING, label.h + UNDERLINE_GAP * 2.0),
Glyph::Face => Size::new(FACE_RADIUS * 2.0, FACE_RADIUS * 2.0),
Glyph::BlockArrow => Size::new(
label.w + PADDING * 2.0 + ARROW_HEAD * 2.0,
label.h + PADDING * 2.0 + ARROW_HEAD * 2.0,
),
Glyph::Reverted => Size::new(COMMIT_RADIUS * 2.0, COMMIT_RADIUS * 2.0),
Glyph::Tag => Size::new(
label.w + (TAG_PAD_X + TAG_POINT) * 2.0,
label.h + TAG_PAD_Y * 2.0,
),
Glyph::ChamferedRect => flow_size(Shape::Rect, label),
}
}
pub const ORTHO_PAD_X: f64 = 20.0;
pub const ORTHO_PAD_Y: f64 = 11.0;
pub const ORTHO_MIN_WIDTH: f64 = 96.0;
pub const ORTHO_WIDTH_STEP: f64 = 8.0;
pub const ORTHO_MAX_WIDTH: f64 = 240.0;
pub fn orthogonal_covers(glyph: Glyph) -> bool {
matches!(
glyph,
Glyph::Flow(Shape::Rect | Shape::RoundedRect) | Glyph::ChamferedRect | Glyph::TitledBox
)
}
fn ortho_extra_pad_x(glyph: Glyph) -> f64 {
if glyph == Glyph::ChamferedRect {
CHAMFER
} else {
0.0
}
}
pub fn ortho_height(lines: usize) -> f64 {
let k = lines.max(1) as f64;
FONT_SIZE as f64 * k + labels::ORTHO_LINE_GAP * (k - 1.0) + ORTHO_PAD_Y * 2.0
}
pub fn ortho_width(glyph: Glyph, text_width: f64) -> f64 {
let padded = text_width + (ORTHO_PAD_X + ortho_extra_pad_x(glyph)) * 2.0;
let floored = padded.max(ORTHO_MIN_WIDTH);
(floored / ORTHO_WIDTH_STEP).ceil() * ORTHO_WIDTH_STEP
}
pub fn orthogonal_node(glyph: Glyph, text: &str) -> Option<(labels::Label, Size)> {
if !orthogonal_covers(glyph) {
return None;
}
let budget = ORTHO_MAX_WIDTH - (ORTHO_PAD_X + ortho_extra_pad_x(glyph)) * 2.0;
let label = labels::Label::measure_wrapped(text, budget, FONT_SIZE as f64);
let size = orthogonal_label_box(glyph, &label);
Some((label, size))
}
pub fn orthogonal_label_box(glyph: Glyph, label: &labels::Label) -> Size {
Size::new(
ortho_width(glyph, label.width),
ortho_height(label.lines.len()),
)
}
pub fn outline(glyph: Glyph, size: Size, mark: Option<Mark>) -> Outline {
match glyph {
Glyph::Wedge => match mark {
Some(Mark::Wedge { start, sweep }) => Outline::Wedge {
r: size.w / 2.0,
start,
sweep,
},
_ => Outline::None,
},
Glyph::Ribbon => match mark {
Some(Mark::Ribbon {
left_top,
left_bottom,
right_top,
right_bottom,
}) => Outline::Ribbon {
w: size.w,
left_top,
left_bottom,
right_top,
right_bottom,
},
_ => Outline::None,
},
Glyph::Graticule => match mark {
Some(Mark::Graticule {
rings,
spokes,
polygon,
}) => Outline::Graticule {
r: size.w / 2.0,
rings,
spokes,
polygon,
},
_ => Outline::None,
},
Glyph::ChartBar | Glyph::PlotFrame => Outline::Rect {
w: size.w,
h: size.h,
r: 0.0,
},
Glyph::ChartPoint => Outline::Disc {
r: size.w.min(size.h) / 2.0,
},
Glyph::ChartLabel => Outline::None,
Glyph::Face => match mark {
Some(Mark::Face { score }) => Outline::Face {
r: size.w.min(size.h) / 2.0,
score,
},
_ => Outline::None,
},
Glyph::BlockArrow => match mark {
Some(Mark::BlockArrow {
left,
right,
up,
down,
}) => Outline::Polygon(block_arrow_polygon(size, left, right, up, down)),
_ => Outline::None,
},
_ => flow_outline_or_glyph(glyph, size),
}
}
fn block_arrow_polygon(size: Size, left: bool, right: bool, up: bool, down: bool) -> Vec<Point> {
let (hw, hh) = (size.w / 2.0, size.h / 2.0);
let (bw, bh) = (
hw - if left || right { ARROW_HEAD } else { 0.0 },
hh - if up || down { ARROW_HEAD } else { 0.0 },
);
let mut p: Vec<Point> = Vec::new();
p.push(Point::new(-bw, -bh));
if up {
p.push(Point::new(-bw * 0.5, -bh));
p.push(Point::new(0.0, -hh));
p.push(Point::new(bw * 0.5, -bh));
}
p.push(Point::new(bw, -bh));
if right {
p.push(Point::new(bw, -bh * 0.5));
p.push(Point::new(hw, 0.0));
p.push(Point::new(bw, bh * 0.5));
}
p.push(Point::new(bw, bh));
if down {
p.push(Point::new(bw * 0.5, bh));
p.push(Point::new(0.0, hh));
p.push(Point::new(-bw * 0.5, bh));
}
p.push(Point::new(-bw, bh));
if left {
p.push(Point::new(-bw, bh * 0.5));
p.push(Point::new(-hw, 0.0));
p.push(Point::new(-bw, -bh * 0.5));
}
p
}
fn flow_outline_or_glyph(glyph: Glyph, size: Size) -> Outline {
match glyph {
Glyph::Flow(shape) => flow_outline(shape, size),
Glyph::StateStart => Outline::Disc { r: size.w / 2.0 },
Glyph::StateEnd => Outline::Target {
outer: size.w / 2.0,
inner: STATE_END_INNER_RADIUS.min(size.w / 2.0),
},
Glyph::Choice => Outline::Polygon(flow_polygon(Shape::Diamond, size)),
Glyph::Bar { .. } => Outline::Bar {
w: size.w,
h: size.h,
},
Glyph::Note => Outline::Note {
w: size.w,
h: size.h,
fold: NOTE_FOLD.min(size.w / 2.0).min(size.h / 2.0),
},
Glyph::TitledBox => Outline::Rect {
w: size.w,
h: size.h,
r: CORNER_RADIUS,
},
Glyph::ClassBox | Glyph::ErBox => Outline::Rect {
w: size.w,
h: size.h,
r: 0.0,
},
Glyph::Participant => Outline::Rect {
w: size.w,
h: size.h,
r: CORNER_RADIUS,
},
Glyph::Actor => Outline::Actor {
w: size.w,
h: size.h,
figure_h: ACTOR_FIGURE_HEIGHT.min(size.h),
},
Glyph::Cloud => Outline::Cloud {
w: size.w,
h: size.h,
},
Glyph::Bang => Outline::Polygon(bang_polygon(size)),
Glyph::Underline => Outline::Underline {
w: size.w,
h: size.h,
},
Glyph::Reverted => Outline::Crossed {
r: size.w.min(size.h) / 2.0,
},
Glyph::Tag => Outline::Tag {
w: size.w,
h: size.h,
point: TAG_POINT.min(size.w / 2.0),
tip: TAG_TIP.min(size.h / 2.0),
hole: TAG_HOLE_RADIUS,
},
Glyph::ChamferedRect => Outline::Polygon(chamfered_rect_polygon(size)),
Glyph::Wedge
| Glyph::ChartBar
| Glyph::ChartPoint
| Glyph::Ribbon
| Glyph::ChartLabel
| Glyph::PlotFrame
| Glyph::Graticule
| Glyph::Face
| Glyph::BlockArrow => Outline::None,
}
}
fn chamfered_rect_polygon(size: Size) -> Vec<Point> {
let (hw, hh) = (size.w / 2.0, size.h / 2.0);
let c = CHAMFER.min(hw).min(hh);
vec![
Point::new(-hw + c, -hh),
Point::new(hw - c, -hh),
Point::new(hw, -hh + c),
Point::new(hw, hh - c),
Point::new(hw - c, hh),
Point::new(-hw + c, hh),
Point::new(-hw, hh - c),
Point::new(-hw, -hh + c),
]
}
fn bang_polygon(size: Size) -> Vec<Point> {
const TEETH: usize = 16;
let (rx, ry) = (size.w / 2.0, size.h / 2.0);
(0..TEETH * 2)
.map(|i| {
let a = std::f64::consts::TAU * i as f64 / (TEETH * 2) as f64;
let k = if i % 2 == 0 {
1.0
} else {
1.0 - BUMP / rx.max(1.0)
};
Point::new(rx * k * a.cos(), ry * k * a.sin())
})
.collect()
}
pub fn tag_points(w: f64, h: f64, point: f64, tip: f64) -> Vec<Point> {
let (hw, hh) = (w / 2.0, h / 2.0);
vec![
Point::new(-hw, -tip),
Point::new(-hw + point, -hh),
Point::new(hw - point, -hh),
Point::new(hw, -tip),
Point::new(hw, tip),
Point::new(hw - point, hh),
Point::new(-hw + point, hh),
Point::new(-hw, tip),
]
}
pub fn tag_hole_center(w: f64, point: f64) -> Point {
Point::new(-w / 2.0 + point * 0.6, 0.0)
}
pub fn polygon(glyph: Glyph, size: Size) -> Vec<Point> {
match glyph {
Glyph::Flow(shape) => flow_polygon(shape, size),
Glyph::Choice => flow_polygon(Shape::Diamond, size),
Glyph::Bang => bang_polygon(size),
Glyph::Tag => tag_points(
size.w,
size.h,
TAG_POINT.min(size.w / 2.0),
TAG_TIP.min(size.h / 2.0),
),
Glyph::ChamferedRect => chamfered_rect_polygon(size),
_ => Vec::new(),
}
}
pub fn intersect(glyph: Glyph, center: Point, size: Size, target: &Point) -> Point {
match glyph {
Glyph::Flow(shape) => flow_intersect(shape, center, size, target),
Glyph::StateStart | Glyph::StateEnd => {
intersect_ellipse(center.x, center.y, size.w / 2.0, size.h / 2.0, target)
}
Glyph::Choice => flow_intersect(Shape::Diamond, center, size, target),
Glyph::Bar { .. }
| Glyph::Note
| Glyph::TitledBox
| Glyph::ClassBox
| Glyph::ErBox
| Glyph::Participant
| Glyph::Actor
| Glyph::Wedge
| Glyph::ChartBar
| Glyph::ChartPoint
| Glyph::Ribbon
| Glyph::ChartLabel
| Glyph::PlotFrame
| Glyph::Graticule
| Glyph::Cloud
| Glyph::Bang
| Glyph::Underline
| Glyph::BlockArrow
| Glyph::Tag => intersect_rect(center.x, center.y, size.w, size.h, target),
Glyph::Face | Glyph::Reverted => {
intersect_ellipse(center.x, center.y, size.w / 2.0, size.h / 2.0, target)
}
Glyph::ChamferedRect => {
let verts: Vec<Point> = chamfered_rect_polygon(size)
.into_iter()
.map(|p| Point::new(center.x + p.x, center.y + p.y))
.collect();
intersect_polygon(&verts, ¢er, target)
}
}
}