mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 10:17:02 -06:00
Arrowhead gains 30 original shapes generated in code, appended after the first ten so saved names stay stable: Barbed, Half Arrow (left/right), Concave, Double Arrow, Chevron, Feather, Swallowtail, Reverse Triangle, Triangle to Bar, Kite, Leaf, Drop, Half Circle, Oval, Target, Tag (+ open), Diamond (open), Hexagon (+ open), Star, Cross, Plus, Double Bar, Dot on Bar, Slash, Double Slash, Bracket and Fork. Each is drawn in local units of the head's weight inside the usual 4 x 4 box, with its own stop for the stroke under it. Arrowhead::label() names them in the menus. The Stroke panel's arrowhead dropdowns render every head as the canvas draws it (cached textures, a shared widgets::doc_preview that the Brushes panel now uses too) in a scrolling list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
408 lines
18 KiB
Rust
408 lines
18 KiB
Rust
//! Arrowheads. A head of weight `hw` (stroke weight × scale) fits a box `4·hw` long and `4·hw`
|
||
//! wide. Hollow kinds are rings (`hw/2` walls) or, for the open arrow, a chevron with `hw` arms.
|
||
//! The stroke ends under the head at the head's *inset* from the tip: halfway into solid heads,
|
||
//! in the back wall of hollow ones (with the cap kept out of the hole) and at the chevron's inner
|
||
//! corner. With [`ArrowAlign::Tip`] the tip sits on the end point and the stroke is shortened by
|
||
//! the inset; with [`ArrowAlign::Extend`] the path keeps its length and the tip sits the inset
|
||
//! past the end point. Heads added after the first ten are drawn in local units ([`local`]).
|
||
|
||
use std::borrow::Cow;
|
||
|
||
use std::f64::consts::{FRAC_PI_2, PI};
|
||
|
||
use kurbo::{Affine, BezPath, ParamCurve, ParamCurveArclen, PathSeg, Point, Shape, Vec2};
|
||
use vectorcraft_doc::{ArrowAlign, Arrowhead, StrokeLayer};
|
||
|
||
use super::{ARCLEN_ACCURACY, StrokePieces, cap_extent, push_seg, segments, subpaths, tangent, unit};
|
||
|
||
/// One arrowhead.
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub struct Arrow {
|
||
pub kind: Arrowhead,
|
||
/// The filled outline (non-zero rule; hollow kinds are rings).
|
||
pub outline: BezPath,
|
||
pub tip: Point,
|
||
/// Unit direction the head points in.
|
||
pub dir: Vec2,
|
||
/// Distance from the tip back to where the stroke ends under the head.
|
||
pub inset: f64,
|
||
}
|
||
|
||
/// [`super::stroke_pieces`] for a stroke with at least one arrowhead.
|
||
pub(super) fn pieces<'a>(bp: &'a BezPath, st: &StrokeLayer) -> StrokePieces<'a> {
|
||
let subs = subpaths(bp);
|
||
let els = bp.elements();
|
||
// Heads go on the start of the first subpath and the end of the last one, when open.
|
||
let first = subs.first().filter(|s| !s.1).map(|s| s.0.clone());
|
||
let last = subs.last().filter(|s| !s.1).map(|s| s.0.clone());
|
||
let cap = cap_extent(st.cap, st.width);
|
||
let (hw0, hw1) = (st.arrow_weight(false), st.arrow_weight(true));
|
||
let start = st.start_arrow.zip(first).map(|(k, r)| (k, r, inset(k, hw0, cap)));
|
||
let end = st.end_arrow.zip(last).map(|(k, r)| (k, r, inset(k, hw1, cap)));
|
||
if start.is_none() && end.is_none() {
|
||
return StrokePieces { line: Cow::Borrowed(bp), heads: vec![] };
|
||
}
|
||
let tip_mode = st.arrow_align == ArrowAlign::Tip;
|
||
let mut heads = vec![];
|
||
// Trims per subpath index (start, end), applied in tip mode only.
|
||
let mut trims: Vec<(f64, f64)> = vec![(0.0, 0.0); subs.len()];
|
||
let mut place = |kind: Arrowhead, range: std::ops::Range<usize>, at_start: bool, inset: f64, hw: f64| {
|
||
let segs = segments(&els[range]);
|
||
let (end_pt, outward) = end_of(&segs, at_start);
|
||
let (tip, dir) = if tip_mode {
|
||
// Point along the chord from where the stroke now ends to the end point.
|
||
let dir = cut_point(&segs, at_start, inset).map(|p| end_pt - p).filter(|v| v.hypot() > 1e-9).map(unit).unwrap_or(outward);
|
||
(end_pt, dir)
|
||
} else {
|
||
(end_pt + outward * inset, outward)
|
||
};
|
||
heads.push(Arrow { kind, outline: shape(kind, tip, dir, hw), tip, dir, inset });
|
||
};
|
||
if let Some((k, r, i)) = start {
|
||
place(k, r, true, i, hw0);
|
||
if tip_mode {
|
||
trims[0].0 = i;
|
||
}
|
||
}
|
||
if let Some((k, r, i)) = end {
|
||
place(k, r, false, i, hw1);
|
||
if tip_mode {
|
||
trims[subs.len() - 1].1 = i;
|
||
}
|
||
}
|
||
if !tip_mode {
|
||
return StrokePieces { line: Cow::Borrowed(bp), heads };
|
||
}
|
||
let mut line = BezPath::new();
|
||
for ((range, _), &(s0, s1)) in subs.iter().zip(&trims) {
|
||
if s0 == 0.0 && s1 == 0.0 {
|
||
line.extend(els[range.clone()].iter().copied());
|
||
continue;
|
||
}
|
||
// Only open subpaths get heads, so a trimmed subpath stays open.
|
||
let segs = trim(&segments(&els[range.clone()]), s0, s1);
|
||
if let Some(first) = segs.first() {
|
||
line.move_to(first.start());
|
||
for s in &segs {
|
||
push_seg(&mut line, s);
|
||
}
|
||
}
|
||
}
|
||
StrokePieces { line: Cow::Owned(line), heads }
|
||
}
|
||
|
||
/// The end point of a subpath and the unit direction pointing out of it there.
|
||
fn end_of(segs: &[PathSeg], at_start: bool) -> (Point, Vec2) {
|
||
let long = |s: &&PathSeg| s.arclen(ARCLEN_ACCURACY) > 1e-9;
|
||
if at_start {
|
||
let p = segs.first().map_or(Point::ORIGIN, |s| s.start());
|
||
let d = segs.iter().find(long).map_or(Vec2::new(-1.0, 0.0), |s| -tangent(s, 0.0));
|
||
(p, d)
|
||
} else {
|
||
let p = segs.last().map_or(Point::ORIGIN, |s| s.end());
|
||
let d = segs.iter().rev().find(long).map_or(Vec2::new(1.0, 0.0), |s| tangent(s, 1.0));
|
||
(p, d)
|
||
}
|
||
}
|
||
|
||
/// The point `dist` along the subpath from its start (or back from its end), if it is that long.
|
||
fn cut_point(segs: &[PathSeg], from_start: bool, dist: f64) -> Option<Point> {
|
||
let (i, t) = if from_start { cut_from_start(segs, dist)? } else { cut_from_end(segs, dist)? };
|
||
Some(segs[i].eval(t))
|
||
}
|
||
|
||
/// (segment, parameter) at arc length `dist` from the start.
|
||
fn cut_from_start(segs: &[PathSeg], dist: f64) -> Option<(usize, f64)> {
|
||
let mut acc = 0.0;
|
||
for (i, s) in segs.iter().enumerate() {
|
||
let len = s.arclen(ARCLEN_ACCURACY);
|
||
if acc + len >= dist {
|
||
return Some((i, if len > 0.0 { s.inv_arclen(dist - acc, ARCLEN_ACCURACY) } else { 0.0 }));
|
||
}
|
||
acc += len;
|
||
}
|
||
None
|
||
}
|
||
|
||
/// (segment, parameter) at arc length `dist` back from the end.
|
||
fn cut_from_end(segs: &[PathSeg], dist: f64) -> Option<(usize, f64)> {
|
||
let mut acc = 0.0;
|
||
for (i, s) in segs.iter().enumerate().rev() {
|
||
let len = s.arclen(ARCLEN_ACCURACY);
|
||
if acc + len >= dist {
|
||
return Some((i, if len > 0.0 { s.inv_arclen(len - (dist - acc), ARCLEN_ACCURACY) } else { 1.0 }));
|
||
}
|
||
acc += len;
|
||
}
|
||
None
|
||
}
|
||
|
||
/// The segments with `s0` of arc length removed from the start and `s1` from the end (empty when
|
||
/// nothing is left).
|
||
fn trim(segs: &[PathSeg], s0: f64, s1: f64) -> Vec<PathSeg> {
|
||
let (Some((i0, t0)), Some((i1, t1))) = (cut_from_start(segs, s0), cut_from_end(segs, s1)) else { return vec![] };
|
||
if (i0, t0) >= (i1, t1) {
|
||
return vec![];
|
||
}
|
||
if i0 == i1 {
|
||
return vec![segs[i0].subsegment(t0..t1)];
|
||
}
|
||
let mut out = Vec::with_capacity(i1 - i0 + 1);
|
||
out.push(segs[i0].subsegment(t0..1.0));
|
||
out.extend_from_slice(&segs[i0 + 1..i1]);
|
||
out.push(segs[i1].subsegment(0.0..t1));
|
||
out
|
||
}
|
||
|
||
/// Head length (and width) for weight `hw`.
|
||
fn len(hw: f64) -> f64 {
|
||
4.0 * hw
|
||
}
|
||
/// Wall of the hollow ring heads.
|
||
fn ring(hw: f64) -> f64 {
|
||
0.5 * hw
|
||
}
|
||
/// Arm thickness of the open arrow.
|
||
fn arm(hw: f64) -> f64 {
|
||
hw
|
||
}
|
||
/// Thickness of the bar.
|
||
fn bar(hw: f64) -> f64 {
|
||
0.75 * hw
|
||
}
|
||
/// Where the notch of the (open) arrow sits, as a fraction of the length.
|
||
const NOTCH: f64 = 0.7;
|
||
|
||
/// Distance from the tip where the stroke ends, for a stroke whose cap reaches `cap` past its end.
|
||
fn inset(kind: Arrowhead, hw: f64, cap: f64) -> f64 {
|
||
let l = len(hw);
|
||
// Solid heads: halfway in, far enough that the cap stays behind the tip.
|
||
let solid = |back: f64| (back / 2.0).max(cap);
|
||
// Hollow heads: in the back wall, with the cap kept out of the hole.
|
||
let hollow = |back: f64| (back - ring(hw) / 2.0).max(back - ring(hw) + cap);
|
||
match kind {
|
||
Arrowhead::Triangle | Arrowhead::Circle | Arrowhead::Square | Arrowhead::Diamond => solid(l),
|
||
Arrowhead::Arrow => solid(NOTCH * l),
|
||
Arrowhead::Bar => solid(bar(hw)),
|
||
Arrowhead::TriangleOpen | Arrowhead::CircleOpen | Arrowhead::SquareOpen => hollow(l),
|
||
// The inner corner of the chevron: arm / sin(half angle).
|
||
Arrowhead::ArrowOpen => (arm(hw) * (l * l + l * l / 4.0).sqrt() / (l / 2.0)).max(cap),
|
||
_ => match local_stop(kind) {
|
||
Stop::At(u) => (u * hw).max(cap),
|
||
Stop::Wall(back) => hollow(back * hw),
|
||
},
|
||
}
|
||
}
|
||
|
||
/// Outline of a `kind` head of weight `hw` with its tip at `tip`, pointing along `dir`.
|
||
fn shape(kind: Arrowhead, tip: Point, dir: Vec2, hw: f64) -> BezPath {
|
||
let n = Vec2::new(-dir.y, dir.x);
|
||
let l = len(hw);
|
||
let half = l / 2.0;
|
||
let base = tip - dir * l;
|
||
let mut p = BezPath::new();
|
||
let triangle = [tip, base + n * half, base - n * half];
|
||
let square = [tip + n * half, base + n * half, base - n * half, tip - n * half];
|
||
let centre = tip - dir * half;
|
||
match kind {
|
||
Arrowhead::Triangle => polygon(&mut p, &triangle),
|
||
Arrowhead::TriangleOpen => ring_polygon(&mut p, &triangle, ring(hw)),
|
||
Arrowhead::Arrow => polygon(&mut p, &[tip, base + n * half, tip - dir * (NOTCH * l), base - n * half]),
|
||
Arrowhead::ArrowOpen => {
|
||
// Arms of thickness `arm` from the tip to the base corners, cut square at their ends.
|
||
let t = arm(hw);
|
||
let (a, b) = (base + n * half, base - n * half);
|
||
let (ua, ub) = (unit(a - tip), unit(b - tip));
|
||
// Inward (towards the axis) normals of the two arms.
|
||
let (na, nb) = (Vec2::new(-ua.y, ua.x), Vec2::new(ub.y, -ub.x));
|
||
let inner = tip - dir * (t * (a - tip).hypot() / half);
|
||
polygon(&mut p, &[tip, a, a + na * t, inner, b + nb * t, b]);
|
||
}
|
||
Arrowhead::Circle => p.extend(kurbo::Circle::new(centre, half).path_elements(0.01)),
|
||
Arrowhead::CircleOpen => {
|
||
p.extend(kurbo::Circle::new(centre, half).path_elements(0.01));
|
||
p.extend(kurbo::Circle::new(centre, half - ring(hw)).to_path(0.01).reverse_subpaths());
|
||
}
|
||
Arrowhead::Square => polygon(&mut p, &square),
|
||
Arrowhead::SquareOpen => ring_polygon(&mut p, &square, ring(hw)),
|
||
Arrowhead::Diamond => polygon(&mut p, &[tip, centre + n * half, base, centre - n * half]),
|
||
Arrowhead::Bar => {
|
||
let t = bar(hw);
|
||
polygon(&mut p, &[tip + n * half, tip + n * half - dir * t, tip - n * half - dir * t, tip - n * half]);
|
||
}
|
||
_ => {
|
||
// Local x runs back from the tip, y across to the right of `dir`; a unit is `hw`.
|
||
p = local(kind);
|
||
p.apply_affine(Affine::new([-dir.x * hw, -dir.y * hw, n.x * hw, n.y * hw, tip.x, tip.y]));
|
||
}
|
||
}
|
||
p
|
||
}
|
||
|
||
/// Where the stroke ends under a head drawn in local units.
|
||
enum Stop {
|
||
/// This many units back from the tip (or as far as the cap needs).
|
||
At(f64),
|
||
/// In the back wall of a ring whose back is this many units from the tip.
|
||
Wall(f64),
|
||
}
|
||
|
||
/// Flat-to-flat length of the hexagons (their corners touch the sides of the 4-unit box).
|
||
const HEX: f64 = 2.0 * 1.732_050_807_568_877_2;
|
||
/// Flattening tolerance of the curved local heads, in units.
|
||
const LOCAL_TOL: f64 = 1e-3;
|
||
|
||
fn local_stop(kind: Arrowhead) -> Stop {
|
||
match kind {
|
||
Arrowhead::Target | Arrowhead::TagOpen | Arrowhead::DiamondOpen => Stop::Wall(4.0),
|
||
Arrowhead::HexagonOpen => Stop::Wall(HEX),
|
||
Arrowhead::DoubleBar | Arrowhead::DotOnBar => Stop::At(0.375),
|
||
Arrowhead::Bracket => Stop::At(0.3),
|
||
Arrowhead::Fork => Stop::At(2.7),
|
||
Arrowhead::HalfArrowLeft | Arrowhead::HalfArrowRight | Arrowhead::Feather => Stop::At(1.3),
|
||
Arrowhead::Swallowtail => Stop::At(1.4),
|
||
Arrowhead::Chevron => Stop::At(1.0),
|
||
Arrowhead::Slash => Stop::At(1.2),
|
||
Arrowhead::DoubleSlash => Stop::At(1.05),
|
||
Arrowhead::DoubleArrow => Stop::At(1.7),
|
||
Arrowhead::HalfCircle => Stop::At(1.0),
|
||
Arrowhead::Concave => Stop::At(2.6),
|
||
Arrowhead::Hexagon => Stop::At(HEX / 2.0),
|
||
_ => Stop::At(2.0),
|
||
}
|
||
}
|
||
|
||
/// The outline of a head added after the first ten, in local units: x from the tip (0) back
|
||
/// along the line (to at most 4), y across it (−2 to 2, positive to the right of the direction
|
||
/// the head points). Every shape is generated here.
|
||
fn local(kind: Arrowhead) -> BezPath {
|
||
let mut p = BezPath::new();
|
||
let poly = |p: &mut BezPath, pts: &[(f64, f64)]| polygon(p, &pts.iter().map(|&q| Point::from(q)).collect::<Vec<_>>());
|
||
let ring_poly = |p: &mut BezPath, pts: &[(f64, f64)]| ring_polygon(p, &pts.iter().map(|&q| Point::from(q)).collect::<Vec<_>>(), ring(1.0));
|
||
let circle = |p: &mut BezPath, c: (f64, f64), r: f64| p.extend(kurbo::Circle::new(c, r).path_elements(LOCAL_TOL));
|
||
// A regular polygon of `n` corners around `c`, the first at angle `a0`, alternating radii.
|
||
let star = |c: (f64, f64), radii: &[f64], n: usize, a0: f64| -> Vec<(f64, f64)> {
|
||
(0..n).map(|i| (a0 + i as f64 * 2.0 * PI / n as f64, radii[i % radii.len()])).map(|(a, r)| (c.0 + r * a.cos(), c.1 + r * a.sin())).collect()
|
||
};
|
||
// A plus of arms `h` long and `k` thick round (2, 0), turned by `turn`.
|
||
let plus = |h: f64, k: f64, turn: f64| -> Vec<(f64, f64)> {
|
||
let arm = [(k, h), (k, k), (h, k)];
|
||
let (sin, cos) = turn.sin_cos();
|
||
(0..4)
|
||
.flat_map(|q| arm.iter().map(move |&(x, y)| [(x, y), (y, -x), (-x, -y), (-y, x)][q]))
|
||
.map(|(x, y)| (2.0 + x * cos - y * sin, x * sin + y * cos))
|
||
.collect()
|
||
};
|
||
let hexagon = star((HEX / 2.0, 0.0), &[2.0], 6, PI / 6.0);
|
||
match kind {
|
||
Arrowhead::Barbed => poly(&mut p, &[(0.0, 0.0), (3.6, 2.0), (2.6, 0.55), (4.0, 0.55), (4.0, -0.55), (2.6, -0.55), (3.6, -2.0)]),
|
||
Arrowhead::HalfArrowLeft => poly(&mut p, &[(0.0, 0.0), (4.0, -2.0), (3.0, -0.5), (3.0, 0.5), (1.0, 0.5)]),
|
||
Arrowhead::HalfArrowRight => poly(&mut p, &[(0.0, 0.0), (4.0, 2.0), (3.0, 0.5), (3.0, -0.5), (1.0, -0.5)]),
|
||
Arrowhead::Concave => {
|
||
p.move_to((0.0, 0.0));
|
||
p.quad_to((2.4, 0.4), (4.0, 2.0));
|
||
p.line_to((4.0, -2.0));
|
||
p.quad_to((2.4, -0.4), (0.0, 0.0));
|
||
p.close_path();
|
||
}
|
||
Arrowhead::DoubleBar => {
|
||
poly(&mut p, &[(0.0, -2.0), (0.75, -2.0), (0.75, 2.0), (0.0, 2.0)]);
|
||
poly(&mut p, &[(1.5, -2.0), (2.25, -2.0), (2.25, 2.0), (1.5, 2.0)]);
|
||
}
|
||
Arrowhead::Feather => poly(&mut p, &[(0.0, 0.5), (1.4, 2.0), (4.0, 2.0), (2.6, 0.5), (2.6, -0.5), (4.0, -2.0), (1.4, -2.0), (0.0, -0.5)]),
|
||
Arrowhead::DotOnBar => {
|
||
poly(&mut p, &[(0.0, -2.0), (0.75, -2.0), (0.75, 2.0), (0.0, 2.0)]);
|
||
circle(&mut p, (2.5, 0.0), 1.25);
|
||
}
|
||
Arrowhead::Chevron => poly(&mut p, &[(0.0, 0.0), (2.0, 2.0), (4.0, 2.0), (2.0, 0.0), (4.0, -2.0), (2.0, -2.0)]),
|
||
Arrowhead::DoubleArrow => poly(&mut p, &[(0.0, 0.0), (2.2, 1.8), (1.9, 0.5), (4.0, 2.0), (3.4, 0.0), (4.0, -2.0), (1.9, -0.5), (2.2, -1.8)]),
|
||
Arrowhead::Target => {
|
||
circle(&mut p, (2.0, 0.0), 2.0);
|
||
p.extend(kurbo::Circle::new((2.0, 0.0), 2.0 - ring(1.0)).to_path(LOCAL_TOL).reverse_subpaths());
|
||
circle(&mut p, (2.0, 0.0), 0.75);
|
||
}
|
||
Arrowhead::Star => poly(&mut p, &star((2.0, 0.0), &[2.0, 0.85], 10, PI)),
|
||
Arrowhead::Cross => poly(&mut p, &plus(2.2, 0.4, PI / 4.0)),
|
||
Arrowhead::Plus => poly(&mut p, &plus(1.9, 0.4, 0.0)),
|
||
Arrowhead::Hexagon => poly(&mut p, &hexagon),
|
||
Arrowhead::HexagonOpen => ring_poly(&mut p, &hexagon),
|
||
Arrowhead::Tag => poly(&mut p, &[(0.0, 0.0), (1.6, 2.0), (4.0, 2.0), (4.0, -2.0), (1.6, -2.0)]),
|
||
Arrowhead::TagOpen => ring_poly(&mut p, &[(0.0, 0.0), (1.6, 2.0), (4.0, 2.0), (4.0, -2.0), (1.6, -2.0)]),
|
||
Arrowhead::HalfCircle => {
|
||
p.move_to((2.0, 2.0));
|
||
p.extend(kurbo::Arc::new((2.0, 0.0), (2.0, 2.0), FRAC_PI_2, PI, 0.0).append_iter(LOCAL_TOL));
|
||
p.close_path();
|
||
}
|
||
Arrowhead::Drop => {
|
||
// Straight sides from the tip, tangent to a round back.
|
||
let (c, r) = (Point::new(2.6, 0.0), 1.4);
|
||
let a = PI - (r / c.x).acos();
|
||
p.move_to((0.0, 0.0));
|
||
p.line_to(c + Vec2::from_angle(a) * r);
|
||
p.extend(kurbo::Arc::new(c, (r, r), a, -2.0 * a, 0.0).append_iter(LOCAL_TOL));
|
||
p.close_path();
|
||
}
|
||
Arrowhead::Slash => poly(&mut p, &[(0.0, 2.0), (0.8, 2.0), (2.4, -2.0), (1.6, -2.0)]),
|
||
Arrowhead::DoubleSlash => {
|
||
poly(&mut p, &[(0.0, 2.0), (0.7, 2.0), (2.1, -2.0), (1.4, -2.0)]);
|
||
poly(&mut p, &[(1.6, 2.0), (2.3, 2.0), (3.7, -2.0), (3.0, -2.0)]);
|
||
}
|
||
Arrowhead::DiamondOpen => ring_poly(&mut p, &[(0.0, 0.0), (2.0, 2.0), (4.0, 0.0), (2.0, -2.0)]),
|
||
Arrowhead::TriangleReverse => poly(&mut p, &[(0.0, 2.0), (4.0, 0.0), (0.0, -2.0)]),
|
||
Arrowhead::Swallowtail => poly(&mut p, &[(0.0, 2.0), (4.0, 2.0), (2.8, 0.0), (4.0, -2.0), (0.0, -2.0)]),
|
||
Arrowhead::Bracket => poly(&mut p, &[(0.0, 2.0), (2.0, 2.0), (2.0, 1.4), (0.6, 1.4), (0.6, -1.4), (2.0, -1.4), (2.0, -2.0), (0.0, -2.0)]),
|
||
Arrowhead::Fork => poly(&mut p, &[(0.0, 2.0), (3.0, 2.0), (3.0, -2.0), (0.0, -2.0), (0.0, -1.4), (2.4, -1.4), (2.4, 1.4), (0.0, 1.4)]),
|
||
Arrowhead::Leaf => {
|
||
p.move_to((0.0, 0.0));
|
||
p.quad_to((2.0, 3.2), (4.0, 0.0));
|
||
p.quad_to((2.0, -3.2), (0.0, 0.0));
|
||
p.close_path();
|
||
}
|
||
Arrowhead::Kite => poly(&mut p, &[(0.0, 0.0), (1.3, 1.7), (4.0, 0.0), (1.3, -1.7)]),
|
||
Arrowhead::TriangleBar => {
|
||
poly(&mut p, &[(0.0, 2.0), (0.6, 2.0), (0.6, 0.25), (4.0, 1.8), (4.0, -1.8), (0.6, -0.25), (0.6, -2.0), (0.0, -2.0)])
|
||
}
|
||
Arrowhead::Oval => p.extend(kurbo::Ellipse::new((2.0, 0.0), (2.0, 1.2), 0.0).path_elements(LOCAL_TOL)),
|
||
// The first ten are drawn by `shape`.
|
||
Arrowhead::Triangle
|
||
| Arrowhead::TriangleOpen
|
||
| Arrowhead::Circle
|
||
| Arrowhead::CircleOpen
|
||
| Arrowhead::Square
|
||
| Arrowhead::SquareOpen
|
||
| Arrowhead::Bar
|
||
| Arrowhead::Diamond
|
||
| Arrowhead::Arrow
|
||
| Arrowhead::ArrowOpen => {}
|
||
}
|
||
p
|
||
}
|
||
|
||
fn polygon(p: &mut BezPath, pts: &[Point]) {
|
||
let Some((first, rest)) = pts.split_first() else { return };
|
||
p.move_to(*first);
|
||
for q in rest {
|
||
p.line_to(*q);
|
||
}
|
||
p.close_path();
|
||
}
|
||
|
||
/// A convex polygon with a hole: the polygon inset by `wall`, wound the other way.
|
||
fn ring_polygon(p: &mut BezPath, pts: &[Point], wall: f64) {
|
||
polygon(p, pts);
|
||
let n = pts.len();
|
||
// Signed area: which side of each edge is inside.
|
||
let area: f64 = (0..n).map(|i| pts[i].to_vec2().cross(pts[(i + 1) % n].to_vec2())).sum();
|
||
let inward = |e: Vec2| if area > 0.0 { Vec2::new(-e.y, e.x) } else { Vec2::new(e.y, -e.x) };
|
||
let inner: Vec<Point> = (0..n)
|
||
.map(|i| {
|
||
let (prev, cur, next) = (pts[(i + n - 1) % n], pts[i], pts[(i + 1) % n]);
|
||
let (n1, n2) = (inward(unit(cur - prev)), inward(unit(next - cur)));
|
||
// The corner where both edges, moved in by `wall`, meet.
|
||
cur + (n1 + n2) * (wall / (1.0 + n1.dot(n2)))
|
||
})
|
||
.rev()
|
||
.collect();
|
||
polygon(p, &inner);
|
||
}
|