Files
vectorcraft/crates/geom/src/bez.rs
T
dotCooCoo d9d33c9e4b Render: the frame after anchors of a long path move updates only the segments that touch them
The renderer keeps the path of a path or compound path of 1,024 anchors
or more by object id, with the bounds of its segments and of its miter
spikes in runs of 128 segments. When anchors move and the subpaths and
anchor counts stay the same, the next frame rewrites only the path
elements of the segments that start or end at a moved anchor, and
measures again only the runs that hold them and the runs of the
segments with length before them. Other edits make the path again as
before. The pixels and the cull bounds are the same.

- StrokeLayer::bounds_on is unchanged. The miter tip of one joint
  (miter_tip) and the stroke box with given tips (bounds_with_tips) are
  new functions beside it, and Appearance::stroked_bounds_by takes the
  box of each shaped stroke, so the cull bounds of a kept path use the
  kept tips.
- geom::bez::has_length is the predicate reach.rs used for segments
  that draw, made public.

Docs: docs/target-app-parity.md.
2026-11-05 06:28:22 +07:00

110 lines
3.9 KiB
Rust

//! Walking a [`BezPath`] as strokes see it: its subpaths (open or closed), their segments, unit
//! tangents and corners. Shared by the stroke geometry (`vectorcraft-effects`) and the visual
//! bounds and hit testing of strokes (`vectorcraft-doc`).
use kurbo::{BezPath, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Vec2};
/// Tangents meeting at more than about 1° (this cosine) make a corner: fitted dashes centre on it,
/// variable-width strokes give it the stroke's join and miter spikes grow from it.
pub const CORNER_COS: f64 = 0.9998;
/// Element ranges of the subpaths of `bp` (each with whether it is closed).
pub fn subpaths(bp: &BezPath) -> Vec<(std::ops::Range<usize>, bool)> {
let els = bp.elements();
let mut out = vec![];
let mut start = 0;
for (i, el) in els.iter().enumerate() {
match el {
PathEl::MoveTo(_) if i > start => {
out.push((start..i, false));
start = i;
}
PathEl::ClosePath => {
out.push((start..i + 1, true));
start = i + 1;
}
_ => {}
}
}
if start < els.len() {
out.push((start..els.len(), false));
}
// A lone MoveTo (or a MoveTo straight after a ClosePath) draws nothing.
out.retain(|(r, _)| r.len() > 1);
out
}
/// The segments of one subpath (`els` starts with its MoveTo; a ClosePath adds the closing line).
pub fn segments(els: &[PathEl]) -> Vec<PathSeg> {
kurbo::segments(els.iter().copied()).collect()
}
/// Whether `s` has length: a segment all of whose points coincide has none, and no direction.
pub fn has_length(s: &PathSeg) -> bool {
match s {
PathSeg::Line(l) => l.p1 != l.p0,
PathSeg::Quad(q) => q.p1 != q.p0 || q.p2 != q.p0,
PathSeg::Cubic(c) => c.p1 != c.p0 || c.p2 != c.p0 || c.p3 != c.p0,
}
}
/// Unit tangent of `s` at `t`, robust at ends where control points coincide with the end point.
pub fn tangent(s: &PathSeg, t: f64) -> Vec2 {
let d = match s {
PathSeg::Line(l) => l.p1 - l.p0,
PathSeg::Quad(q) => {
let v = q.deriv().eval(t).to_vec2();
if v.hypot() > 1e-9 { v } else { q.p2 - q.p0 }
}
PathSeg::Cubic(c) => {
let v = c.deriv().eval(t).to_vec2();
if v.hypot() > 1e-9 {
v
} else if t < 0.5 {
// The first control point that differs from the start gives the direction.
[c.p2, c.p3].into_iter().map(|p| p - c.p0).find(|v| v.hypot() > 1e-9).unwrap_or_default()
} else {
[c.p1, c.p0].into_iter().map(|p| c.p3 - p).find(|v| v.hypot() > 1e-9).unwrap_or_default()
}
}
};
unit(d)
}
/// `v` normalised, or +x when it has no length.
pub fn unit(v: Vec2) -> Vec2 {
let h = v.hypot();
if h > 1e-12 { v / h } else { Vec2::new(1.0, 0.0) }
}
/// Do segments `a` and `b` (`b` following `a`) meet at a corner?
pub fn kink(a: &PathSeg, b: &PathSeg) -> bool {
tangent(a, 1.0).dot(tangent(b, 0.0)) < CORNER_COS
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn subpaths_and_tangents() {
let mut bp = BezPath::new();
bp.move_to((0.0, 0.0));
bp.line_to((10.0, 0.0));
bp.line_to((10.0, 10.0));
bp.close_path();
bp.move_to((20.0, 0.0));
bp.curve_to((20.0, 0.0), (30.0, 0.0), (30.0, 10.0));
let subs = subpaths(&bp);
assert_eq!(subs, vec![(0..4, true), (4..6, false)]);
let segs = segments(&bp.elements()[subs[0].0.clone()]);
assert_eq!(segs.len(), 3, "the close adds a segment");
assert!(kink(&segs[0], &segs[1]));
let c = segments(&bp.elements()[subs[1].0.clone()]);
// A handle on its anchor: the direction comes from the next control point.
assert_eq!(tangent(&c[0], 0.0), Vec2::new(1.0, 0.0));
assert_eq!(tangent(&c[0], 1.0), Vec2::new(0.0, 1.0));
assert_eq!(unit(Vec2::ZERO), Vec2::new(1.0, 0.0));
}
}