use crate::mbox::{PathContour, PathSeg};
pub(crate) struct Drawn {
pub(crate) width: f64,
pub(crate) contours: Vec<PathContour>,
}
pub(crate) struct DrawnBand {
pub(crate) height: f64,
pub(crate) contours: Vec<PathContour>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum Stretch {
Hat,
Tilde,
OverBrace,
UnderBrace,
RightArrow,
MapstoArrow,
LeftArrow,
}
#[inline(always)]
fn line(to: (f64, f64)) -> PathSeg {
PathSeg::Line { to }
}
#[inline(always)]
fn quad(ctrl: (f64, f64), to: (f64, f64)) -> PathSeg {
PathSeg::Quad { ctrl, to }
}
#[inline(always)]
fn contour(start: (f64, f64), segments: Vec<PathSeg>) -> PathContour {
PathContour { start, segments }
}
fn rect(x0: f64, y0: f64, x1: f64, y1: f64) -> PathContour {
contour(
(x0, y0),
vec![
line((x1, y0)),
line((x1, y1)),
line((x0, y1)),
line((x0, y0)),
],
)
}
fn mirror_x(c: &PathContour, w: f64) -> PathContour {
let mx = |p: (f64, f64)| (w - p.0, p.1);
PathContour {
start: mx(c.start),
segments: c
.segments
.iter()
.map(|s| match s {
PathSeg::Line { to } => PathSeg::Line { to: mx(*to) },
PathSeg::Quad { ctrl, to } => PathSeg::Quad {
ctrl: mx(*ctrl),
to: mx(*to),
},
})
.collect(),
}
}
fn mirror_y(c: &PathContour, h: f64) -> PathContour {
let my = |p: (f64, f64)| (p.0, h - p.1);
PathContour {
start: my(c.start),
segments: c
.segments
.iter()
.map(|s| match s {
PathSeg::Line { to } => PathSeg::Line { to: my(*to) },
PathSeg::Quad { ctrl, to } => PathSeg::Quad {
ctrl: my(*ctrl),
to: my(*to),
},
})
.collect(),
}
}
pub(crate) fn delimiter(ch: char, total: f64, em: f64) -> Option<Drawn> {
match ch {
'(' | ')' => Some(paren(ch == ')', total, em)),
'[' | ']' => Some(bracket(ch == ']', total, em, true, true)),
'⌈' | '⌉' => Some(bracket(ch == '⌉', total, em, true, false)),
'⌊' | '⌋' => Some(bracket(ch == '⌋', total, em, false, true)),
'{' | '}' => Some(brace(ch == '}', total, em)),
'|' => Some(vert(total, em, false)),
'‖' => Some(vert(total, em, true)),
'⟨' | '⟩' | '〈' | '〉' => Some(angle(matches!(ch, '⟩' | '〉'), total, em)),
'/' | '\\' => Some(slash(ch == '\\', total, em)),
'√' => Some(surd(total, em)),
_ => None,
}
}
fn paren(closing: bool, h: f64, em: f64) -> Drawn {
let w = 0.30 * em + 0.06 * h;
let t_top = 0.035 * em;
let t_mid = 0.062 * em + 0.01 * h;
let bulge = 0.16 * w;
let (x_out, x_in) = (0.0, t_mid);
let start = (w, h);
let c = contour(
start,
vec![
quad((x_out - bulge, h / 2.0), (w, 0.0)),
line((w - t_top, 0.0)),
quad((x_in - bulge, h / 2.0), (w - t_top, h)),
line(start),
],
);
let width = w + 0.04 * em;
let c = if closing { mirror_x(&c, width) } else { c };
Drawn {
width,
contours: vec![c],
}
}
fn bracket(closing: bool, h: f64, em: f64, top_foot: bool, bottom_foot: bool) -> Drawn {
let t = 0.065 * em + 0.004 * h;
let foot = 0.24 * em + 0.01 * h;
let w = t + foot;
let mut pts: Vec<(f64, f64)> = Vec::new();
if top_foot {
pts.push((w, h));
pts.push((0.0, h));
} else {
pts.push((t, h));
pts.push((0.0, h));
}
pts.push((0.0, 0.0));
if bottom_foot {
pts.push((w, 0.0));
pts.push((w, t));
pts.push((t, t));
} else {
pts.push((t, 0.0));
}
if top_foot {
pts.push((t, h - t));
pts.push((w, h - t));
} else {
pts.push((t, h));
}
let start = pts[0];
let segments: Vec<PathSeg> = pts[1..]
.iter()
.copied()
.map(line)
.chain(core::iter::once(line(start)))
.collect();
let c = contour(start, segments);
let width = w + 0.06 * em;
let c = if closing { mirror_x(&c, width) } else { c };
Drawn {
width,
contours: vec![c],
}
}
fn brace(closing: bool, h: f64, em: f64) -> Drawn {
let t = 0.055 * em + 0.006 * h; let reach = 0.16 * em + 0.015 * h; let w = t + 2.0 * reach; let mid = h / 2.0;
let cap = (0.28 * em).min(h * 0.25); let waist = (0.30 * em).min(h * 0.25); let xs = reach; let start = (w, h);
let c = contour(
start,
vec![
quad((xs, h), (xs, h - cap)),
line((xs, mid + waist)),
quad((xs, mid), (0.0, mid)),
quad((xs, mid), (xs, mid - waist)),
line((xs, cap)),
quad((xs, 0.0), (w, 0.0)),
line((w, t)),
quad((xs + t, t), (xs + t, cap)),
line((xs + t, mid - waist)),
quad((xs + t, mid), (t, mid)),
quad((xs + t, mid), (xs + t, mid + waist)),
line((xs + t, h - cap)),
quad((xs + t, h - t), (w, h - t)),
line(start),
],
);
let width = w + 0.05 * em;
let c = if closing { mirror_x(&c, width) } else { c };
Drawn {
width,
contours: vec![c],
}
}
fn vert(h: f64, em: f64, double: bool) -> Drawn {
let t = 0.045 * em;
if double {
let gap = 0.14 * em;
Drawn {
width: 2.0 * t + gap + 0.06 * em,
contours: vec![rect(0.0, 0.0, t, h), rect(t + gap, 0.0, 2.0 * t + gap, h)],
}
} else {
Drawn {
width: t + 0.06 * em,
contours: vec![rect(0.0, 0.0, t, h)],
}
}
}
fn angle(closing: bool, h: f64, em: f64) -> Drawn {
let w = 0.24 * em + 0.10 * h; let mid = h / 2.0;
let shift = 0.085 * em + 0.006 * h; let start = (w + shift, h);
let c = contour(
start,
vec![
line((shift, mid)),
line((w + shift, 0.0)),
line((w, 0.0)),
line((0.0, mid)),
line((w, h)),
line(start),
],
);
let width = w + shift + 0.05 * em;
let c = if closing { mirror_x(&c, width) } else { c };
Drawn {
width,
contours: vec![c],
}
}
fn slash(reverse: bool, h: f64, em: f64) -> Drawn {
let t = 0.06 * em + 0.004 * h; let run = 0.28 * em + 0.18 * h; let w = run + t;
let c = contour(
(0.0, 0.0),
vec![
line((t, 0.0)),
line((w, h)),
line((w - t, h)),
line((0.0, 0.0)),
],
);
let width = w + 0.04 * em;
let c = if reverse { mirror_x(&c, width) } else { c };
Drawn {
width,
contours: vec![c],
}
}
fn surd(total: f64, em: f64) -> Drawn {
let w = 0.58 * em + 0.02 * total;
let heavy = 0.058 * em + 0.004 * total;
let light = 0.056 * em; let vertex_x = 0.30 * em;
let mid_y = total * 0.42;
let start = (0.02 * em, mid_y);
let c = contour(
start,
vec![
line((0.13 * em, mid_y + 0.05 * em)),
line((vertex_x - heavy * 0.6, 0.18 * em)),
line((w - light, total)),
line((w, total)),
line((vertex_x, 0.0)),
line((vertex_x - heavy, 0.0)),
line((0.10 * em, mid_y - 0.02 * em)),
line(start),
],
);
Drawn {
width: w,
contours: vec![c],
}
}
pub(crate) fn stretch(kind: Stretch, width: f64, em: f64) -> DrawnBand {
match kind {
Stretch::Hat => hat(width, em),
Stretch::Tilde => tilde(width, em),
Stretch::OverBrace => hbrace(width, em, false),
Stretch::UnderBrace => hbrace(width, em, true),
Stretch::RightArrow => arrow(width, em, false),
Stretch::LeftArrow => arrow(width, em, true),
Stretch::MapstoArrow => mapsto_arrow(width, em),
}
}
fn hat(w: f64, em: f64) -> DrawnBand {
let t = 0.048 * em + 0.010 * w.min(3.0 * em);
let rise = 0.16 * em + 0.05 * w.min(3.0 * em);
let h = rise + t;
let c = contour(
(0.0, 0.0),
vec![
line((0.0, t)),
line((w / 2.0, h)),
line((w, t)),
line((w, 0.0)),
line((w / 2.0, rise)),
line((0.0, 0.0)),
],
);
DrawnBand {
height: h,
contours: vec![c],
}
}
fn tilde(w: f64, em: f64) -> DrawnBand {
let t = 0.055 * em;
let amp = 0.10 * em + 0.02 * w.min(3.0 * em);
let h = 2.0 * amp + t;
let c = contour(
(0.0, amp * 0.4),
vec![
quad((w * 0.25, amp * 2.0), (w * 0.5, amp)),
quad((w * 0.75, 0.0), (w, amp * 1.6)),
line((w, amp * 1.6 + t)),
quad((w * 0.75, t), (w * 0.5, amp + t)),
quad((w * 0.25, amp * 2.0 + t), (0.0, amp * 0.4 + t)),
line((0.0, amp * 0.4)),
],
);
DrawnBand {
height: h,
contours: vec![c],
}
}
fn hbrace(w: f64, em: f64, under: bool) -> DrawnBand {
let t = 0.050 * em; let reach = (0.16 * em).min(w * 0.12); let h = t + 2.0 * reach;
let cap = (0.30 * em).min(w * 0.20); let waist = (0.32 * em).min(w * 0.20); let mid = w / 2.0;
let ys = reach; let start = (0.0, 0.0);
let c = contour(
start,
vec![
quad((0.0, ys + t), (cap, ys + t)),
line((mid - waist, ys + t)),
quad((mid, ys + t), (mid, h)),
quad((mid, ys + t), (mid + waist, ys + t)),
line((w - cap, ys + t)),
quad((w, ys + t), (w, 0.0)),
line((w - t, 0.0)),
quad((w - t, ys), (w - cap, ys)),
line((mid + waist, ys)),
quad((mid + t, ys), (mid, h - t * 1.2)),
quad((mid - t, ys), (mid - waist, ys)),
line((cap, ys)),
quad((t, ys), (t, 0.0)),
line(start),
],
);
let c = if under { mirror_y(&c, h) } else { c };
DrawnBand {
height: h,
contours: vec![c],
}
}
fn arrow(w: f64, em: f64, left: bool) -> DrawnBand {
let t = 0.048 * em; let head_l = (0.32 * em).min(w * 0.5); let head_h = 0.16 * em; let h = 2.0 * head_h;
let cy = head_h; let c = contour(
(0.0, cy - t / 2.0),
vec![
line((w - head_l, cy - t / 2.0)),
line((w - head_l, cy - head_h)),
line((w, cy)),
line((w - head_l, cy + head_h)),
line((w - head_l, cy + t / 2.0)),
line((0.0, cy + t / 2.0)),
line((0.0, cy - t / 2.0)),
],
);
let c = if left { mirror_x(&c, w) } else { c };
DrawnBand {
height: h,
contours: vec![c],
}
}
fn mapsto_arrow(w: f64, em: f64) -> DrawnBand {
let t = 0.048 * em;
let band = arrow(w, em, false);
let bar = contour(
(0.0, 0.0),
vec![
line((t, 0.0)),
line((t, band.height)),
line((0.0, band.height)),
line((0.0, 0.0)),
],
);
let mut contours = band.contours;
contours.push(bar);
DrawnBand {
height: band.height,
contours,
}
}
pub(crate) struct DrawnSym {
pub(crate) width: f64,
pub(crate) height: f64,
pub(crate) depth: f64,
pub(crate) contours: Vec<PathContour>,
}
pub(crate) fn symbol(ch: char, em: f64) -> Option<DrawnSym> {
match ch {
'\u{2640}' => Some(female(em)),
'\u{2642}' => Some(male(em)),
'\u{2641}' => Some(earth(em)),
_ => None,
}
}
const COS45: f64 = std::f64::consts::FRAC_1_SQRT_2;
const TAN22: f64 = 0.414_213_562_373_095_1;
fn circle_quads(cx: f64, cy: f64, r: f64) -> Vec<PathSeg> {
let b = COS45 * r;
let a = TAN22 * r;
vec![
quad((cx + r, cy + a), (cx + b, cy + b)), quad((cx + a, cy + r), (cx, cy + r)), quad((cx - a, cy + r), (cx - b, cy + b)), quad((cx - r, cy + a), (cx - r, cy)), quad((cx - r, cy - a), (cx - b, cy - b)), quad((cx - a, cy - r), (cx, cy - r)), quad((cx + a, cy - r), (cx + b, cy - b)), quad((cx + r, cy - a), (cx + r, cy)), ]
}
fn ring(cx: f64, cy: f64, r_out: f64, r_in: f64) -> PathContour {
let start = (cx + r_out, cy);
let mut segments = circle_quads(cx, cy, r_out);
let inner_start = (cx + r_in, cy);
segments.push(line(inner_start));
let fwd = circle_quads(cx, cy, r_in);
let mut pts = vec![inner_start];
for s in &fwd {
match s {
PathSeg::Quad { to, .. } | PathSeg::Line { to } => pts.push(*to),
}
}
for (i, s) in fwd.iter().enumerate().rev() {
match s {
PathSeg::Quad { ctrl, .. } => segments.push(quad(*ctrl, pts[i])),
PathSeg::Line { .. } => segments.push(line(pts[i])),
}
}
segments.push(line(start));
contour(start, segments)
}
fn plus(cx: f64, cy: f64, hx: f64, hy: f64, t: f64) -> PathContour {
let h = t / 2.0;
let pts = [
(cx - h, cy - hy),
(cx + h, cy - hy),
(cx + h, cy - h),
(cx + hx, cy - h),
(cx + hx, cy + h),
(cx + h, cy + h),
(cx + h, cy + hy),
(cx - h, cy + hy),
(cx - h, cy + h),
(cx - hx, cy + h),
(cx - hx, cy - h),
(cx - h, cy - h),
];
contour(
pts[0],
pts[1..]
.iter()
.copied()
.map(line)
.chain(core::iter::once(line(pts[0])))
.collect(),
)
}
fn female(em: f64) -> DrawnSym {
let t = 0.055 * em;
let r = 0.27 * em;
let cx = 0.32 * em;
let cy = 0.41 * em; let foot = -0.10 * em;
let join = cy - r + t / 2.0; let cross = plus(cx, (foot + join) / 2.0, 0.13 * em, (join - foot) / 2.0, t);
DrawnSym {
width: 0.64 * em,
height: cy + r,
depth: -foot,
contours: vec![ring(cx, cy, r, r - t), cross],
}
}
fn male(em: f64) -> DrawnSym {
let t = 0.055 * em;
let r = 0.24 * em;
let cx = 0.30 * em;
let cy = 0.26 * em; let b = COS45;
let s0 = r - t / 2.0;
let s1 = r + 0.26 * em;
let p0 = (cx + s0 * b, cy + s0 * b);
let p1 = (cx + s1 * b, cy + s1 * b);
let hw = t / 2.0;
let shaft = contour(
(p0.0 - hw * b, p0.1 + hw * b),
vec![
line((p1.0 - hw * b, p1.1 + hw * b)),
line((p1.0 + hw * b, p1.1 - hw * b)),
line((p0.0 + hw * b, p0.1 - hw * b)),
line((p0.0 - hw * b, p0.1 + hw * b)),
],
);
let hl = 0.15 * em;
let hb = 0.10 * em;
let head = contour(
(p1.0 + hl * b, p1.1 + hl * b),
vec![
line((p1.0 - hb * b, p1.1 + hb * b)),
line((p1.0 + hb * b, p1.1 - hb * b)),
line((p1.0 + hl * b, p1.1 + hl * b)),
],
);
let extent = s1 + hl;
DrawnSym {
width: cx + extent * b + 0.03 * em,
height: cy + extent * b,
depth: 0.0,
contours: vec![ring(cx, cy, r, r - t), shaft, head],
}
}
fn earth(em: f64) -> DrawnSym {
let t = 0.055 * em;
let r = 0.29 * em;
let cx = 0.32 * em;
let cy = 0.34 * em; let reach = r - t / 2.0; DrawnSym {
width: 0.64 * em,
height: cy + r,
depth: 0.0,
contours: vec![ring(cx, cy, r, r - t), plus(cx, cy, reach, reach, t)],
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
fn assert_closed(d: &[PathContour]) {
for c in d {
let last = match c.segments.last().expect("segments") {
PathSeg::Line { to } | PathSeg::Quad { to, .. } => *to,
};
assert!(
(last.0 - c.start.0).abs() < 1e-12 && (last.1 - c.start.1).abs() < 1e-12,
"contour not closed: start {:?}, last {last:?}",
c.start
);
}
}
fn assert_y_range(d: &[PathContour], lo: f64, hi: f64) {
for c in d {
let check = |y: f64| {
assert!(
y >= lo - 1e-9 && y <= hi + 1e-9,
"on-curve y {y} outside [{lo}, {hi}]"
);
};
check(c.start.1);
for s in &c.segments {
match s {
PathSeg::Line { to } | PathSeg::Quad { to, .. } => check(to.1),
}
}
}
}
#[test]
fn every_delimiter_construction_exists_and_is_closed() {
for ch in [
'(', ')', '[', ']', '{', '}', '|', '‖', '⟨', '⟩', '⌈', '⌉', '⌊', '⌋', '/', '\\', '√',
] {
for total in [1.0, 2.5, 7.0, 40.0] {
let d = delimiter(ch, total, 1.0)
.unwrap_or_else(|| panic!("no construction for {ch:?}"));
assert!(d.width > 0.0, "{ch:?} zero width");
assert!(!d.contours.is_empty());
assert_closed(&d.contours);
assert_y_range(&d.contours, 0.0, total);
}
}
}
#[test]
fn unknown_delimiters_have_no_construction() {
assert!(delimiter('↑', 3.0, 1.0).is_none());
assert!(delimiter('x', 3.0, 1.0).is_none());
}
#[test]
fn mirrored_pairs_share_extents() {
for (open, close) in [('(', ')'), ('[', ']'), ('{', '}'), ('⟨', '⟩')] {
let o = delimiter(open, 5.0, 1.0).unwrap();
let c = delimiter(close, 5.0, 1.0).unwrap();
assert!((o.width - c.width).abs() < 1e-12, "{open:?}/{close:?}");
assert_eq!(o.contours.len(), c.contours.len());
}
}
#[test]
fn every_stretch_construction_draws_at_any_width() {
for kind in [
Stretch::Hat,
Stretch::Tilde,
Stretch::OverBrace,
Stretch::UnderBrace,
Stretch::RightArrow,
Stretch::LeftArrow,
Stretch::MapstoArrow,
] {
for w in [0.2, 1.0, 4.0, 25.0] {
let b = stretch(kind, w, 1.0);
assert!(b.height > 0.0);
assert_closed(&b.contours);
assert_y_range(&b.contours, 0.0, b.height);
let max_x = b
.contours
.iter()
.flat_map(|c| {
core::iter::once(c.start.0).chain(c.segments.iter().map(|s| match s {
PathSeg::Line { to } | PathSeg::Quad { to, .. } => to.0,
}))
})
.fold(0.0_f64, f64::max);
assert!(
(max_x - w).abs() < 1e-9,
"{kind:?} at {w}: ink ends at {max_x}"
);
}
}
}
#[test]
fn constructions_are_deterministic() {
let a = delimiter('{', 7.3, 1.0).unwrap();
let b = delimiter('{', 7.3, 1.0).unwrap();
assert_eq!(a.contours, b.contours);
let x = stretch(Stretch::Tilde, 3.7, 1.0);
let y = stretch(Stretch::Tilde, 3.7, 1.0);
assert_eq!(x.contours, y.contours);
for ch in ['\u{2640}', '\u{2642}', '\u{2641}'] {
let s1 = symbol(ch, 1.0).unwrap();
let s2 = symbol(ch, 1.0).unwrap();
assert_eq!(s1.contours, s2.contours, "{ch:?}");
}
}
#[test]
fn every_drawn_symbol_is_closed_and_in_box() {
for ch in ['\u{2640}', '\u{2642}', '\u{2641}'] {
for em in [0.5, 1.0, 2.4] {
let d = symbol(ch, em).unwrap_or_else(|| panic!("no symbol for {ch:?}"));
assert!(d.width > 0.0, "{ch:?} zero width");
assert!(!d.contours.is_empty());
assert_closed(&d.contours);
assert_y_range(&d.contours, -d.depth, d.height);
for c in &d.contours {
let mut xs = vec![c.start.0];
xs.extend(c.segments.iter().map(|s| match s {
PathSeg::Line { to } | PathSeg::Quad { to, .. } => to.0,
}));
for x in xs {
assert!(x >= -1e-9 && x <= d.width + 1e-9, "{ch:?} x {x}");
}
}
}
}
}
#[test]
fn unmapped_symbol_chars_have_no_construction() {
assert!(symbol('x', 1.0).is_none());
assert!(symbol('\u{2643}', 1.0).is_none()); assert!(symbol('\u{00A9}', 1.0).is_none()); }
}