use super::*;
pub(super) fn build(name: &str, w: f64, h: f64, a: &dyn Fn(usize) -> f64) -> Option<Geometry> {
let ss = w.min(h);
let full = Rect::new(0.0, 0.0, w, h);
let mut b = Pb::new();
let g = match name {
"lineInv" => {
let l = b.m(0.0, h).l(w, 0.0).take();
Geometry {
paths: vec![stroke_only(l)],
text_rect: full,
handles: vec![],
sites: vec![Point::new(0.0, h), Point::new(w, 0.0)],
}
}
"nonIsoscelesTrapezoid" => {
let max = 50000.0 * w / ss.max(1e-9);
let (v1, v2) = (a(0).clamp(0.0, max), a(1).clamp(0.0, max));
let (d1, d2) = (ss * v1 / 1e5, ss * v2 / 1e5);
let mut g = simple(
b.poly(&[(0.0, h), (d1, 0.0), (w - d2, 0.0), (w, h)]).take(),
Rect::new(d1 * 0.66, h * 0.2, w - d2 * 0.66, h),
w,
h,
);
g.handles.push(Handle::new(
0,
Point::ZERO,
Vec2::new(ss / 1e5, 0.0),
v1,
0.0,
max,
));
g.handles.push(Handle::new(
1,
Point::new(w, 0.0),
Vec2::new(-ss / 1e5, 0.0),
v2,
0.0,
max,
));
g
}
"pieWedge" => {
b.m(0.0, h).arc(w, h, 180.0, 90.0).l(w, h).z();
simple(b.take(), Rect::new(w * 0.29289, h * 0.29289, w, h), w, h)
}
"flowChartOfflineStorage" => {
let tri = b.poly(&[(0.0, 0.0), (w, 0.0), (w / 2.0, h)]).take();
let bar = Pb::new().m(w * 0.4, h * 0.8).l(w * 0.6, h * 0.8).take();
Geometry {
paths: vec![fill(tri), stroke_only(bar)],
text_rect: Rect::new(w / 4.0, 0.0, w * 0.75, h * 0.8),
handles: vec![],
sites: vec![
Point::new(w / 2.0, 0.0),
Point::new(w / 4.0, h / 2.0),
Point::new(w / 2.0, h),
Point::new(w * 0.75, h / 2.0),
],
}
}
"chartX" | "chartStar" | "chartPlus" => {
let body = corner_rect(w, h, [Corner::Square; 4]);
let mut l = Pb::new();
if name != "chartPlus" {
l.m(0.0, 0.0).l(w, h).m(0.0, h).l(w, 0.0);
}
if name != "chartX" {
l.m(w / 2.0, 0.0).l(w / 2.0, h);
}
if name == "chartPlus" {
l.m(0.0, h / 2.0).l(w, h / 2.0);
}
Geometry {
paths: vec![fill_only(body), stroke_only(l.take())],
text_rect: full,
handles: vec![],
sites: mid_sites(w, h),
}
}
"cornerTabs" | "squareTabs" | "plaqueTabs" => {
let d = (w.hypot(h) / 20.0).min(w / 2.0).min(h / 2.0);
match name {
"cornerTabs" => {
b.poly(&[(0.0, 0.0), (d, 0.0), (0.0, d)]);
b.poly(&[(w - d, 0.0), (w, 0.0), (w, d)]);
b.poly(&[(w, h - d), (w, h), (w - d, h)]);
b.poly(&[(0.0, h - d), (d, h), (0.0, h)]);
}
"squareTabs" => {
b.poly(&[(0.0, 0.0), (d, 0.0), (d, d), (0.0, d)]);
b.poly(&[(w - d, 0.0), (w, 0.0), (w, d), (w - d, d)]);
b.poly(&[(w - d, h - d), (w, h - d), (w, h), (w - d, h)]);
b.poly(&[(0.0, h - d), (d, h - d), (d, h), (0.0, h)]);
}
_ => {
if d > 0.0 {
b.m(0.0, 0.0).l(d, 0.0).arc(d, d, 0.0, 90.0).z();
b.m(w, 0.0).l(w, d).arc(d, d, 90.0, 90.0).z();
b.m(w, h).l(w - d, h).arc(d, d, 180.0, 90.0).z();
b.m(0.0, h).l(0.0, h - d).arc(d, d, 270.0, 90.0).z();
} else {
b.m(0.0, 0.0).z();
}
}
}
Geometry {
paths: vec![fill(b.take())],
text_rect: full.inset(-d),
handles: vec![],
sites: mid_sites(w, h),
}
}
"callout1"
| "callout2"
| "callout3"
| "accentCallout1"
| "accentCallout2"
| "accentCallout3"
| "borderCallout2"
| "borderCallout3"
| "accentBorderCallout1"
| "accentBorderCallout2"
| "accentBorderCallout3" => {
let n = match name.chars().last() {
Some('2') => 2,
Some('3') => 3,
_ => 1,
};
line_callout(
w,
h,
a,
n + 1,
name.starts_with("accent"),
name.contains("order"),
)
}
"leftRightArrowCallout" | "upDownArrowCallout" => {
let vertical = name == "upDownArrowCallout";
let (l, t) = if vertical { (h, w) } else { (w, h) };
let ss = l.min(t);
let head = (ss * a(1).max(0.0) / 1e5).min(t / 2.0);
let shaft = (ss * a(0).max(0.0) / 1e5).min(head * 2.0);
let hl = (ss * a(2).max(0.0) / 1e5).min(l / 2.0);
let boxl = (l * a(3).max(0.0) / 1e5).min(l - 2.0 * hl);
let (c, b0, b1) = (t / 2.0, (l - boxl) / 2.0, (l + boxl) / 2.0);
let pts = [
(0.0, c),
(hl, c - head),
(hl, c - shaft / 2.0),
(b0, c - shaft / 2.0),
(b0, 0.0),
(b1, 0.0),
(b1, c - shaft / 2.0),
(l - hl, c - shaft / 2.0),
(l - hl, c - head),
(l, c),
(l - hl, c + head),
(l - hl, c + shaft / 2.0),
(b1, c + shaft / 2.0),
(b1, t),
(b0, t),
(b0, c + shaft / 2.0),
(hl, c + shaft / 2.0),
(hl, c + head),
];
let d = if vertical { Dir::Down } else { Dir::Right };
let pts = orient(d, w, h, &pts);
let tr = orient(d, w, h, &[(b0, 0.0), (b1, t)]);
simple(
b.poly(&pts).take(),
Rect::from_points(Point::new(tr[0].0, tr[0].1), Point::new(tr[1].0, tr[1].1)).abs(),
w,
h,
)
}
"quadArrowCallout" => {
let head = (ss * a(1).max(0.0) / 1e5).min(ss / 2.0);
let shaft = (ss * a(0).max(0.0) / 1e5).min(head * 2.0);
let hl = (ss * a(2).max(0.0) / 1e5).min(ss / 2.0);
let k = a(3).clamp(0.0, 100000.0) / 1e5;
let (bw, bh) = (
(w * k).min(w - 2.0 * hl).max(shaft),
(h * k).min(h - 2.0 * hl).max(shaft),
);
let (cx, cy) = (w / 2.0, h / 2.0);
let (bx0, bx1, by0, by1) = (cx - bw / 2.0, cx + bw / 2.0, cy - bh / 2.0, cy + bh / 2.0);
let (s2, hd) = (shaft / 2.0, head);
let pts = [
(0.0, cy),
(hl, cy - hd),
(hl, cy - s2),
(bx0, cy - s2),
(bx0, by0),
(cx - s2, by0),
(cx - s2, hl),
(cx - hd, hl),
(cx, 0.0),
(cx + hd, hl),
(cx + s2, hl),
(cx + s2, by0),
(bx1, by0),
(bx1, cy - s2),
(w - hl, cy - s2),
(w - hl, cy - hd),
(w, cy),
(w - hl, cy + hd),
(w - hl, cy + s2),
(bx1, cy + s2),
(bx1, by1),
(cx + s2, by1),
(cx + s2, h - hl),
(cx + hd, h - hl),
(cx, h),
(cx - hd, h - hl),
(cx - s2, h - hl),
(cx - s2, by1),
(bx0, by1),
(bx0, cy + s2),
(hl, cy + s2),
(hl, cy + hd),
];
simple(b.poly(&pts).take(), Rect::new(bx0, by0, bx1, by1), w, h)
}
"curvedRightArrow" | "curvedLeftArrow" | "curvedUpArrow" | "curvedDownArrow" => {
curved_arrow(name, w, h, a(0), a(1), a(2))
}
"swooshArrow" => swoosh(w, h, a(0), a(1)),
"ellipseRibbon" | "ellipseRibbon2" => {
ellipse_ribbon(name == "ellipseRibbon2", w, h, a(0), a(1), a(2))
}
"leftRightRibbon" => left_right_ribbon(w, h, a(0), a(1), a(2)),
"gear6" | "gear9" => gear(if name == "gear6" { 6 } else { 9 }, w, h, a(0), a(1)),
"funnel" => funnel(w, h),
"circularArrow" | "leftCircularArrow" | "leftRightCircularArrow" => {
circular_arrow(name, w, h, [a(0), a(1), a(2), a(3), a(4)])
}
_ => return None,
};
Some(g)
}
struct Ring {
c: Point,
rx: f64,
ry: f64,
th: f64,
hw: f64,
}
impl Ring {
fn at(&self, deg: f64, d: f64) -> Point {
let (ax, ay) = ((self.rx + d).max(0.0), (self.ry + d).max(0.0));
let t = vis(deg, ax, ay).to_radians();
Point::new(self.c.x + ax * t.cos(), self.c.y + ay * t.sin())
}
fn param(&self, p: Point, d: f64) -> f64 {
let (ax, ay) = ((self.rx + d).max(1e-9), (self.ry + d).max(1e-9));
((p.y - self.c.y) / ay)
.atan2((p.x - self.c.x) / ax)
.to_degrees()
}
fn cross(&self, b: Point, u: Vec2, d: f64, sign: f64) -> Point {
let (ax, ay) = ((self.rx + d).max(1e-9), (self.ry + d).max(1e-9));
let (px, py) = ((b.x - self.c.x) / ax, (b.y - self.c.y) / ay);
let (ux, uy) = (u.x / ax, u.y / ay);
let qa = ux * ux + uy * uy;
let qb = 2.0 * (px * ux + py * uy);
let qc = px * px + py * py - 1.0;
let disc = qb * qb - 4.0 * qa * qc;
if qa < 1e-18 || disc < 0.0 {
return b;
}
let r = disc.sqrt();
let (s0, s1) = ((-qb - r) / (2.0 * qa), (-qb + r) / (2.0 * qa));
let s = if sign > 0.0 {
if s0 >= 0.0 { s0 } else { s1 }
} else if s1 <= 0.0 {
s1
} else {
s0
};
b + u * s
}
fn head(&self, base: f64, sweep: f64) -> [Point; 5] {
let b = self.at(base, 0.0);
let tip = self.at(base + sweep, 0.0);
let d = tip - self.c;
let u = if d.hypot() > 1e-12 {
d / d.hypot()
} else {
Vec2::new(1.0, 0.0)
};
let half = self.th / 2.0;
[
self.cross(b, u, half, 1.0),
b + u * self.hw,
tip,
b - u * self.hw,
self.cross(b, u, -half, -1.0),
]
}
}
fn turn(a: f64, b: f64, dir: f64) -> f64 {
if dir > 0.0 {
(b - a).rem_euclid(360.0)
} else {
-(a - b).rem_euclid(360.0)
}
}
fn circular_arrow(name: &str, w: f64, h: f64, a: [f64; 5]) -> Geometry {
let ss = w.min(h);
if ss <= 0.0 {
return simple(
corner_rect(w, h, [Corner::Square; 4]),
Rect::new(0.0, 0.0, w, h),
w,
h,
);
}
let a5 = a[4].clamp(0.0, 25000.0);
let hw = ss * a5 / 1e5;
let th = ss * a[0].clamp(0.0, 2.0 * a5) / 1e5;
let ring = Ring {
c: Point::new(w / 2.0, h / 2.0),
rx: (w / 2.0 - hw).max(th / 2.0),
ry: (h / 2.0 - hw).max(th / 2.0),
th,
hw,
};
let deg = |v: f64| (v / 60000.0) % 360.0;
let (sweep, end, start) = (deg(a[1]), deg(a[2]), deg(a[3]));
let half = th / 2.0;
let mut b = Pb::new();
let edge_arc = |b: &mut Pb, from: Point, to: Point, d: f64, dir: f64| {
let (ax, ay) = ((ring.rx + d).max(0.0), (ring.ry + d).max(0.0));
let (s, e) = (ring.param(from, d), ring.param(to, d));
b.arc(ax, ay, s, turn(s, e, dir));
};
let outer_arc = |b: &mut Pb, from: Point, to: Point, dir: f64| edge_arc(b, from, to, half, dir);
let inner_arc =
|b: &mut Pb, from: Point, to: Point, dir: f64| edge_arc(b, from, to, -half, dir);
if name == "leftRightCircularArrow" {
let hs = ring.head(start, -sweep);
let he = ring.head(end, sweep);
b.m(hs[0].x, hs[0].y);
outer_arc(&mut b, hs[0], he[0], 1.0);
b.l(he[1].x, he[1].y)
.l(he[2].x, he[2].y)
.l(he[3].x, he[3].y)
.l(he[4].x, he[4].y);
inner_arc(&mut b, he[4], hs[4], -1.0);
b.l(hs[3].x, hs[3].y)
.l(hs[2].x, hs[2].y)
.l(hs[1].x, hs[1].y)
.z();
} else {
let dir = if name == "circularArrow" { 1.0 } else { -1.0 };
let he = ring.head(end, sweep);
let (o0, i0) = (ring.at(start, half), ring.at(start, -half));
b.m(o0.x, o0.y);
outer_arc(&mut b, o0, he[0], dir);
b.l(he[1].x, he[1].y)
.l(he[2].x, he[2].y)
.l(he[3].x, he[3].y)
.l(he[4].x, he[4].y);
inner_arc(&mut b, he[4], i0, -dir);
b.z();
}
simple(b.take(), Rect::new(0.0, 0.0, w, h), w, h)
}
fn funnel(w: f64, h: f64) -> Geometry {
const RIM: f64 = 173.0; const SPOUT: f64 = 163.4; let ss = w.min(h);
let (cx, rim_y) = (w / 2.0, h / 4.0);
let (sx, sy) = (w / 8.0, h / 15.0);
let spout_y = h - sy;
let at = |rx: f64, ry: f64, cy: f64, deg: f64| {
let t = deg.to_radians();
Point::new(cx + rx * t.cos(), cy + ry * t.sin())
};
let p0 = at(cx, rim_y, rim_y, RIM);
let p1 = at(sx, sy, spout_y, 180.0 - SPOUT);
let mut b = Pb::new();
b.m(p0.x, p0.y).arc(cx, rim_y, RIM, 540.0 - 2.0 * RIM);
b.l(p1.x, p1.y)
.arc(sx, sy, 180.0 - SPOUT, 2.0 * SPOUT - 180.0)
.z();
let d = ss / 20.0;
b.ellipse(cx, rim_y, (cx - d).max(0.0), (rim_y - d).max(0.0));
Geometry {
paths: vec![fill_eo(b.take())],
text_rect: Rect::new(w / 4.0, h / 2.0, w * 0.75, h - sy),
handles: vec![],
sites: mid_sites(w, h),
}
}
fn gear(n: usize, w: f64, h: f64, a0: f64, a1: f64) -> Geometry {
let ss = w.min(h);
let th = ss * a0.clamp(0.0, 20000.0) / 1e5; let lean_max = if n == 6 { 5380.0 } else { 2730.0 };
let l = ss * a1.clamp(0.0, a0.clamp(0.0, 20000.0).min(lean_max)) / 1e5; let (cx, cy) = (w / 2.0, h / 2.0);
let (rx, ry) = ((cx - th).max(0.0), (cy - th).max(0.0));
let r = (ss / 2.0 - th).max(1e-9);
let half = ((th + l) / 2.0 / r).atan();
let step = std::f64::consts::TAU / n as f64;
let on_body = |t: f64| Point::new(cx + rx * t.cos(), cy + ry * t.sin());
let mut b = Pb::new();
for k in 0..n {
let t = -std::f64::consts::FRAC_PI_2 + step * k as f64;
let nrm = Vec2::new(t.cos() * ry, t.sin() * rx);
let u = if nrm.hypot() > 1e-12 {
nrm / nrm.hypot()
} else {
Vec2::new(t.cos(), t.sin())
};
let v = Vec2::new(-u.y, u.x);
let (r0, r1) = (on_body(t - half), on_body(t + half));
let (c0, c1) = (r0 + u * th + v * l, r1 + u * th - v * l);
if k == 0 {
b.m(r0.x, r0.y);
} else {
b.arc(
rx,
ry,
(t - step + half).to_degrees(),
(step - 2.0 * half).to_degrees(),
);
}
b.l(c0.x, c0.y).l(c1.x, c1.y).l(r1.x, r1.y);
}
let t_last = -std::f64::consts::FRAC_PI_2 + step * (n - 1) as f64;
b.arc(
rx,
ry,
(t_last + half).to_degrees(),
(step - 2.0 * half).to_degrees(),
)
.z();
let k = std::f64::consts::FRAC_1_SQRT_2;
let text = Rect::new(cx - rx * k, cy - ry * k, cx + rx * k, cy + ry * k);
simple(b.take(), text, w, h)
}
fn left_right_ribbon(w: f64, h: f64, a0: f64, a1: f64, a2: f64) -> Geometry {
let ss = w.min(h);
let sh = h * a2.clamp(0.0, 33333.0) / 1e5; let bt = (h * a0.max(0.0) / 1e5).min(h - sh); let cx = w / 2.0;
let (rx, ry) = (w / 32.0, (sh / 4.0).min(w / 24.0));
let hb = (ss * a1.max(0.0) / 1e5).min(cx - rx); let yc = (h - sh) / 2.0;
let (lt, lb) = (yc - bt / 2.0, yc + bt / 2.0);
let (rt, rb) = (lt + sh, lb + sh);
let mut edge = Pb::new();
edge.m(cx - rx, lb)
.l(hb, lb)
.l(hb, h - sh)
.l(0.0, yc)
.l(hb, 0.0)
.l(hb, lt)
.l(cx, lt);
edge.arc(rx, ry, 270.0, 180.0).arc(rx, ry, 270.0, -90.0);
let edge = edge.take();
let mut left = Pb::new();
left.m(0.0, yc)
.l(hb, 0.0)
.l(hb, lt)
.l(cx, lt)
.arc(rx, ry, 270.0, 180.0)
.arc(rx, ry, 270.0, -180.0);
left.l(cx, lb.max(lt + 4.0 * ry))
.l(hb, lb)
.l(hb, h - sh)
.z();
let left = left.take();
let mut curl = Pb::new();
curl.m(cx + rx, lt + ry)
.l(cx + rx, rt)
.l(cx, lt + 4.0 * ry)
.arc(rx, ry, 90.0, 180.0)
.arc(rx, ry, 90.0, -90.0)
.z();
let mut right = Pb::new();
right
.m(w, yc + sh)
.l(w - hb, h)
.l(w - hb, rb)
.l(cx, rb)
.arc(rx, ry, 90.0, 90.0)
.l(cx - rx, rt - ry)
.arc(rx, ry, 180.0, -90.0);
right.l(w - hb, rt).l(w - hb, sh).z();
Geometry {
paths: vec![
fill_only(left),
stroke_only(edge),
shade(curl.take(), FillMode::DarkenLess),
fill(right.take()),
],
text_rect: Rect::new(hb.min(cx - rx), rt, (w - hb).max(cx + rx), lb.max(rt)),
handles: vec![],
sites: vec![
Point::new(0.0, yc),
Point::new(cx, rt),
Point::new(w, yc + sh),
Point::new(cx, lb),
],
}
}
fn sag(b: &mut Pb, w: f64, d: f64, y0: f64, xa: f64, xb: f64) {
let hw = (w / 2.0).max(1e-9);
let f = |x: f64| y0 + d * (1.0 - ((x - hw) / hw).powi(2));
let slope = -2.0 * d * (xa - hw) / (hw * hw);
b.q((xa + xb) / 2.0, f(xa) + slope * (xb - xa) / 2.0, xb, f(xb));
}
fn ellipse_ribbon(up: bool, w: f64, h: f64, a0: f64, a1: f64, a2: f64) -> Geometry {
let th = h * a0.clamp(0.0, 100000.0) / 1e5; let d = (h * a2.max(0.0) / 1e5)
.max(th - (h - th) / 2.0)
.clamp(0.0, th);
let bw = w * a1.clamp(25000.0, 75000.0) / 1e5;
let (x0, x1) = ((w - bw) / 2.0, (w + bw) / 2.0);
let ear = w / 8.0;
let (xf, xg) = (x0 + ear, x1 - ear);
let e = h - th; let off = th - d; let t = |x: f64| {
let hw = (w / 2.0).max(1e-9);
d * (1.0 - ((x - hw) / hw).powi(2))
};
let mut ends = Pb::new();
ends.m(0.0, 0.0);
sag(&mut ends, w, d, 0.0, 0.0, xf);
ends.l(xf, t(xf) + off).l(x0, t(x0) + off).l(x0, t(x0) + e);
sag(&mut ends, w, d, e, x0, 0.0);
ends.l(ear, t(ear) + e / 2.0).z();
ends.m(w, 0.0).l(w - ear, t(w - ear) + e / 2.0).l(w, e);
sag(&mut ends, w, d, e, w, x1);
ends.l(x1, t(x1) + off).l(xg, t(xg) + off).l(xg, t(xg));
sag(&mut ends, w, d, 0.0, xg, w);
ends.z();
let mut folds = Pb::new();
folds.poly(&[(x0, t(x0) + off), (xf, t(xf)), (xf, t(xf) + off)]);
folds.poly(&[(x1, t(x1) + off), (xg, t(xg)), (xg, t(xg) + off)]);
let mut band = Pb::new();
band.m(x0, t(x0) + off);
sag(&mut band, w, d, off, x0, x1);
band.l(x1, t(x1) + off + e);
sag(&mut band, w, d, off + e, x1, x0);
band.z();
let mut paths = vec![
fill(ends.take()),
shade(folds.take(), FillMode::DarkenLess),
fill(band.take()),
];
let mut text = Rect::new(x0, th, x1, t(x0) + off + e);
if up {
let m = kurbo::Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, h]);
for p in &mut paths {
p.path.apply_affine(m);
}
text = m.transform_rect_bbox(text);
}
Geometry {
paths,
text_rect: text,
handles: vec![],
sites: mid_sites(w, h),
}
}
fn swoosh(w: f64, h: f64, a0: f64, a1: f64) -> Geometry {
const TILT: f64 = 0.1126; const LIFT: f64 = 0.051; let ss = w.min(h);
let e = ss / 8.0;
let th = (h * a0.max(0.0) / 1e5).min((h - 2.0 * e).max(0.0));
let hl = (ss * a1.max(0.0) / 1e5)
.max((e + th) * TILT)
.min((w - e * TILT).max(0.0));
let qx = w - hl;
let mut b = Pb::new();
b.m(0.0, h)
.q(w / 6.0, h / 3.0, qx, e)
.l(qx - e * TILT, 0.0)
.l(w, (e + th / 2.0 - LIFT * h).clamp(0.0, 2.0 * e + th))
.l((qx + (e + th) * TILT).min(w), 2.0 * e + th)
.l((qx + th * TILT).min(w), e + th)
.q(w / 4.0, (th + h / 12.0 + e).min(h), 0.0, h)
.z();
simple(b.take(), Rect::new(0.0, 0.0, w, h), w, h)
}
fn curved_arrow(name: &str, w: f64, h: f64, a0: f64, a1: f64, a2: f64) -> Geometry {
let along_x = matches!(name, "curvedRightArrow" | "curvedLeftArrow");
let (cw, ch) = if along_x { (w, h) } else { (h, w) };
let ss = cw.min(ch);
let head = (ss * a1.max(0.0) / 1e5).min(ch / 2.0);
let th = (ss * a0.max(0.0) / 1e5).min(head);
let hl = (ss * a2.max(0.0) / 1e5).min(cw);
let hh = head / 2.0;
let r = ((ch - hh - th / 2.0) / 2.0).max(0.0);
let te = if cw > 0.0 {
(-(hl / cw)).clamp(-1.0, 1.0).acos().to_degrees()
} else {
90.0
};
let mut back = Pb::new();
back.m(cw, 0.0)
.arc(cw, r, 270.0, -90.0)
.l(0.0, r + th)
.arc(cw, r, 180.0, 90.0)
.z();
let mut front = Pb::new();
front.m(0.0, r).arc(cw, r, 180.0, te - 180.0);
let yb = front.cur.y;
let yc = yb + th / 2.0;
front
.l(cw - hl, yc - hh)
.l(cw, 2.0 * r + th / 2.0)
.l(cw - hl, yc + hh)
.l(cw - hl, yb + th)
.arc(cw, r, te, 180.0 - te)
.z();
let m = match name {
"curvedLeftArrow" => kurbo::Affine::new([-1.0, 0.0, 0.0, 1.0, w, 0.0]),
"curvedUpArrow" => kurbo::Affine::new([0.0, -1.0, 1.0, 0.0, 0.0, h]),
"curvedDownArrow" => kurbo::Affine::new([0.0, 1.0, 1.0, 0.0, 0.0, 0.0]),
_ => kurbo::Affine::IDENTITY,
};
let (mut back, mut front) = (back.take(), front.take());
back.apply_affine(m);
front.apply_affine(m);
Geometry {
paths: vec![shade(back, FillMode::DarkenLess), fill(front)],
text_rect: Rect::new(0.0, 0.0, w, h),
handles: vec![],
sites: mid_sites(w, h),
}
}
fn line_callout(
w: f64,
h: f64,
a: &dyn Fn(usize) -> f64,
pts: usize,
accent: bool,
border: bool,
) -> Geometry {
let body = corner_rect(w, h, [Corner::Square; 4]);
let pt = |i: usize| {
(
w * a(2 * i + 1).clamp(-1e7, 1e7) / 1e5,
h * a(2 * i).clamp(-1e7, 1e7) / 1e5,
)
};
let mut l = Pb::new();
let (x0, y0) = pt(0);
l.m(x0, y0);
for i in 1..pts {
let (x, y) = pt(i);
l.l(x, y);
}
let mut paths = vec![if border { fill(body) } else { fill_only(body) }];
if accent {
paths.push(stroke_only(Pb::new().m(x0, 0.0).l(x0, h).take()));
}
paths.push(stroke_only(l.take()));
Geometry {
paths,
text_rect: Rect::new(0.0, 0.0, w, h),
handles: vec![],
sites: mid_sites(w, h),
}
}
#[cfg(test)]
mod tests {
use super::super::{CATALOG, FillMode, build, info};
use kurbo::{BezPath, PathEl, Point, Shape};
const NEW: [&str; 38] = [
"lineInv",
"nonIsoscelesTrapezoid",
"pieWedge",
"leftRightArrowCallout",
"upDownArrowCallout",
"quadArrowCallout",
"circularArrow",
"leftCircularArrow",
"leftRightCircularArrow",
"curvedRightArrow",
"curvedLeftArrow",
"curvedUpArrow",
"curvedDownArrow",
"swooshArrow",
"callout1",
"callout2",
"callout3",
"accentCallout1",
"accentCallout2",
"accentCallout3",
"borderCallout2",
"borderCallout3",
"accentBorderCallout1",
"accentBorderCallout2",
"accentBorderCallout3",
"ellipseRibbon",
"ellipseRibbon2",
"leftRightRibbon",
"flowChartOfflineStorage",
"gear6",
"gear9",
"funnel",
"cornerTabs",
"squareTabs",
"plaqueTabs",
"chartX",
"chartStar",
"chartPlus",
];
fn outline(name: &str, w: f64, h: f64, adj: &[f64]) -> BezPath {
build(name, w, h, adj)
.map(|g| g.outline())
.unwrap_or_default()
}
fn dist(path: &BezPath, p: Point) -> f64 {
let mut best = f64::MAX;
let mut prev = Point::ZERO;
let mut start = Point::ZERO;
kurbo::flatten(path.iter(), 0.01, |el| {
let (a, b) = match el {
PathEl::MoveTo(q) => {
prev = q;
start = q;
return;
}
PathEl::LineTo(q) => (prev, q),
PathEl::ClosePath => (prev, start),
_ => return,
};
let d = b - a;
let t = if d.hypot2() > 0.0 {
((p - a).dot(d) / d.hypot2()).clamp(0.0, 1.0)
} else {
0.0
};
best = best.min((a + d * t - p).hypot());
prev = b;
});
best
}
fn on(name: &str, w: f64, h: f64, adj: &[f64], pts: &[(f64, f64)]) {
let path = outline(name, w, h, adj);
for &(x, y) in pts {
let d = dist(&path, Point::new(x, y));
assert!(
d < 1.0,
"{name} {w}x{h} {adj:?}: ({x}, {y}) is {d:.2} from the outline"
);
}
}
#[test]
fn new_presets_are_in_the_catalog() {
for n in NEW {
let i = info(n).unwrap_or_else(|| panic!("{n} missing from the catalog"));
assert!(!i.label.is_empty());
}
assert!(CATALOG.len() >= 183);
}
#[test]
fn new_presets_build_within_bounds_at_many_sizes_and_adjusts() {
let sizes = [
(480.0, 300.0),
(300.0, 300.0),
(200.0, 460.0),
(100.0, 460.0),
(1.0, 1.0),
(0.0, 0.0),
(0.0, 50.0),
(60.0, 0.0),
];
let adjusts: [&[f64]; 8] = [
&[],
&[0.0; 8],
&[100000.0; 8],
&[-50000.0; 8],
&[1e12, -1e12, 1e12, -1e12, 1e12, -1e12, 1e12, -1e12],
&[
f64::MAX,
f64::MIN,
f64::MAX,
f64::MIN,
f64::MAX,
f64::MIN,
f64::MAX,
f64::MIN,
],
&[
f64::NAN,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NAN,
5.0,
6.0,
7.0,
8.0,
],
&[
30000.0, 60000.0, 20000.0, 9000000.0, 20000.0, 40000.0, 120000.0, -30000.0,
],
];
for n in NEW {
let callout = n.contains("allout") && !n.contains("Arrow");
for (w, h) in sizes {
for adj in adjusts {
let g = build(n, w, h, adj).unwrap_or_else(|| panic!("{n} did not build"));
assert!(!g.paths.is_empty(), "{n} has no paths");
for s in &g.paths {
let bb = s.path.bounding_box();
assert!(
bb.x0.is_finite()
&& bb.y0.is_finite()
&& bb.x1.is_finite()
&& bb.y1.is_finite(),
"{n} {w}x{h} {adj:?}: {bb:?}"
);
if !callout {
let spill = if n.contains("ircular") { 0.5 } else { 0.01 };
let tol = 0.5 + w.max(h) * spill;
assert!(
bb.x0 >= -tol
&& bb.y0 >= -tol
&& bb.x1 <= w + tol
&& bb.y1 <= h + tol,
"{n} {w}x{h} {adj:?} out of bounds: {bb:?}"
);
}
}
let t = g.text_rect;
assert!(
t.x0.is_finite()
&& t.y0.is_finite()
&& t.x1.is_finite()
&& t.y1.is_finite(),
"{n} text rect {t:?}"
);
}
}
let g = build(n, 480.0, 300.0, &[]).unwrap_or_else(|| panic!("{n}"));
assert!(
g.outline().elements().len() >= 2,
"{n} default outline is empty"
);
}
}
#[test]
fn simple_new_presets_match_reference_points() {
on("lineInv", 480.0, 300.0, &[], &[(0.0, 300.0), (480.0, 0.0)]);
on(
"nonIsoscelesTrapezoid",
480.0,
300.0,
&[],
&[(75.0, 0.0), (405.0, 0.0), (0.0, 300.0)],
);
on(
"nonIsoscelesTrapezoid",
200.0,
460.0,
&[80000.0, 20000.0],
&[(100.0, 0.0), (160.0, 0.0)],
);
on(
"pieWedge",
480.0,
300.0,
&[],
&[(64.3, 150.0), (0.0, 300.0), (480.0, 0.0)],
);
on(
"flowChartOfflineStorage",
480.0,
300.0,
&[],
&[(240.0, 300.0), (192.0, 240.0), (288.0, 240.0)],
);
on(
"squareTabs",
480.0,
300.0,
&[],
&[(28.3, 28.3), (451.7, 271.7)],
);
on("cornerTabs", 480.0, 300.0, &[], &[(23.3, 5.0)]);
on("plaqueTabs", 480.0, 300.0, &[], &[(27.85, 5.0)]);
on(
"chartStar",
480.0,
300.0,
&[],
&[(240.0, 0.0), (240.0, 300.0), (48.0, 30.0)],
);
on(
"chartPlus",
480.0,
300.0,
&[],
&[(0.0, 150.0), (240.0, 0.0)],
);
for n in ["chartX", "callout1", "accentCallout2"] {
let g = build(n, 100.0, 100.0, &[]).unwrap_or_else(|| panic!("{n}"));
assert!(!g.paths[0].stroke, "{n}");
}
}
#[test]
fn line_callouts_follow_their_adjusts() {
on(
"callout3",
480.0,
300.0,
&[],
&[
(-40.0, 56.25),
(-80.0, 56.25),
(-80.0, 300.0),
(-40.0, 338.9),
],
);
let adj = [
50000.0, 110000.0, 20000.0, 120000.0, -20000.0, 80000.0, 30000.0, 40000.0,
];
let g = build("accentBorderCallout3", 480.0, 300.0, &adj)
.unwrap_or_else(|| panic!("accentBorderCallout3"));
assert_eq!(g.paths.len(), 3);
assert!(g.paths[0].stroke);
let accent = g.paths[1].path.bounding_box();
assert!(
(accent.x0 - 528.0).abs() < 1e-6 && accent.y0 == 0.0 && accent.y1 == 300.0,
"{accent:?}"
);
on(
"accentBorderCallout3",
480.0,
300.0,
&adj,
&[(576.0, 60.0), (384.0, -60.0), (192.0, 90.0)],
);
}
#[test]
fn arrow_callouts_match_reference_points() {
on(
"leftRightArrowCallout",
480.0,
300.0,
&[],
&[
(0.0, 150.0),
(75.0, 75.0),
(124.5, 0.0),
(355.5, 300.0),
(480.0, 150.0),
],
);
on(
"leftRightArrowCallout",
200.0,
460.0,
&[],
&[(50.0, 180.0), (52.0, 0.0), (148.0, 460.0)],
);
on(
"upDownArrowCallout",
480.0,
300.0,
&[],
&[(240.0, 0.0), (0.0, 77.8), (480.0, 222.2), (240.0, 300.0)],
);
on(
"quadArrowCallout",
480.0,
300.0,
&[],
&[
(0.0, 150.0),
(124.5, 77.8),
(240.0, 0.0),
(480.0, 150.0),
(240.0, 300.0),
],
);
}
#[test]
fn curved_arrows_match_reference_points() {
on(
"curvedRightArrow",
480.0,
300.0,
&[],
&[
(480.0, 0.0),
(480.0, 75.0),
(480.0, 225.0),
(405.0, 149.0),
(405.0, 299.0),
(100.0, 151.5),
(300.0, 6.7),
],
);
on(
"curvedRightArrow",
200.0,
460.0,
&[30000.0, 60000.0, 40000.0],
&[
(200.0, 400.0),
(120.0, 324.5),
(120.0, 443.5),
(200.0, 60.0),
],
);
on(
"curvedRightArrow",
200.0,
460.0,
&[60000.0, 30000.0, 80000.0],
&[(200.0, 60.0)],
);
on(
"curvedLeftArrow",
480.0,
300.0,
&[],
&[(0.0, 225.0), (0.0, 0.0)],
);
on(
"curvedUpArrow",
480.0,
300.0,
&[],
&[(405.0, 0.0), (0.0, 0.0), (75.0, 0.0)],
);
on(
"curvedDownArrow",
480.0,
300.0,
&[],
&[(405.0, 300.0), (0.0, 300.0)],
);
let g = build("curvedRightArrow", 480.0, 300.0, &[])
.unwrap_or_else(|| panic!("curvedRightArrow"));
assert_eq!(g.paths[0].fill, FillMode::DarkenLess);
}
#[test]
fn swoosh_matches_reference_points() {
on(
"swooshArrow",
480.0,
300.0,
&[],
&[
(0.0, 300.0),
(425.8, 0.0),
(480.0, 59.7),
(442.8, 150.0),
(202.0, 105.5),
(202.0, 159.0),
],
);
on(
"swooshArrow",
480.0,
300.0,
&[40000.0, 16667.0],
&[(480.0, 82.2), (447.8, 195.0)],
);
on(
"swooshArrow",
200.0,
460.0,
&[],
&[(163.9, 0.0), (200.0, 59.4)],
);
}
#[test]
fn ribbons_match_reference_points() {
on(
"ellipseRibbon",
480.0,
300.0,
&[],
&[
(0.0, 0.0),
(120.0, 28.1),
(60.0, 128.9),
(240.0, 300.0),
(120.0, 66.1),
(180.0, 72.2),
(180.0, 35.2),
],
);
on(
"ellipseRibbon",
480.0,
300.0,
&[30000.0, 60000.0, 20000.0],
&[(120.0, 45.0), (240.0, 300.0), (96.0, 68.4)],
);
on(
"ellipseRibbon",
480.0,
300.0,
&[80000.0, 20000.0, 60000.0],
&[(240.0, 240.0), (1.0, 61.75)],
);
on(
"ellipseRibbon2",
480.0,
300.0,
&[],
&[(0.0, 300.0), (240.0, 0.0)],
);
on(
"leftRightRibbon",
480.0,
300.0,
&[],
&[
(0.0, 125.0),
(150.0, 0.0),
(150.0, 50.0),
(255.0, 62.5),
(225.0, 87.5),
(480.0, 175.0),
(330.0, 300.0),
],
);
on(
"leftRightRibbon",
480.0,
300.0,
&[90000.0, 10000.0, 60000.0],
&[(0.0, 100.0), (480.0, 200.0), (240.0, 300.0)],
);
}
#[test]
fn gears_and_funnel_match_reference_points() {
on(
"gear6",
300.0,
300.0,
&[],
&[(150.0, 3.5), (133.7, 3.5), (166.3, 3.5), (45.0, 150.0)],
);
on("gear9", 300.0, 300.0, &[], &[(150.0, 1.0), (30.0, 150.0)]);
on("gear6", 480.0, 300.0, &[], &[(240.0, 3.5), (201.0, 3.5)]);
on(
"funnel",
480.0,
300.0,
&[],
&[
(240.0, 0.0),
(0.0, 75.0),
(240.0, 15.0),
(15.0, 75.0),
(240.0, 300.0),
(60.5, 150.0),
],
);
let g = build("funnel", 480.0, 300.0, &[]).unwrap_or_else(|| panic!("funnel"));
assert!(g.paths[0].even_odd);
}
#[test]
fn circular_arrows_match_reference_points() {
on(
"circularArrow",
300.0,
300.0,
&[],
&[
(19.0, 150.0),
(56.5, 150.0),
(150.0, 19.0),
(262.4, 150.0),
(218.6, 113.4),
(293.6, 113.4),
],
);
on(
"circularArrow",
300.0,
300.0,
&[20000.0, 1800000.0, 18000000.0, 5400000.0, 20000.0],
&[
(150.0, 270.0),
(150.0, 210.0),
(247.0, 42.1),
(143.0, 102.1),
(227.9, 105.0),
],
);
on(
"leftCircularArrow",
300.0,
300.0,
&[],
&[(37.6, 150.0), (262.4, 150.0), (150.0, 281.0)],
);
on(
"leftRightCircularArrow",
300.0,
300.0,
&[],
&[(262.4, 150.0), (37.6, 150.0), (150.0, 19.0)],
);
on(
"circularArrow",
300.0,
300.0,
&[25000.0, 1142319.0, 20457681.0, 10800000.0, 5000.0],
&[(0.0, 150.0), (30.0, 150.0)],
);
}
}