mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 13:33:19 +00:00
effects: keep a wide constant stroke over its centre
When a constant profile's half-width reaches the local radius of curvature, the smooth inner offset crosses the centre and the non-zero fill leaves a hole. Cut through the centre at those samples; the cut is inside the painted stroke. Varying profiles keep the offset because their crossings can be real caustics, where a cut would show as a slit. Regression test: a radius-5 circle with a 30 pt stroke contains its centre.
This commit is contained in:
@@ -25,7 +25,15 @@ pub fn width_outline(bp: &BezPath, width: f64, profile: &WidthProfile, st: &Stro
|
||||
/// `spans[i]` of the subpath it was cut from (see [`Dashed::spans`](super::Dashed::spans));
|
||||
/// subpaths without a span are whole.
|
||||
pub(super) fn outline_spans(bp: &BezPath, spans: &[(f64, f64)], width: f64, profile: &WidthProfile, st: &StrokeLayer, tol: f64) -> BezPath {
|
||||
let ctx = Ctx { half: width / 2.0, profile, cap: st.cap, join: st.join, miter_limit: st.miter_limit, tol: tol.max(1e-4) };
|
||||
let ctx = Ctx {
|
||||
half: width / 2.0,
|
||||
profile,
|
||||
cap: st.cap,
|
||||
join: st.join,
|
||||
miter_limit: st.miter_limit,
|
||||
tol: tol.max(1e-4),
|
||||
uniform: profile.points.first().is_some_and(|f| profile.points.iter().all(|p| p.1 == f.1 && p.2 == f.2)),
|
||||
};
|
||||
let mut out = BezPath::new();
|
||||
for (i, (r, closed)) in subpaths(bp).into_iter().enumerate() {
|
||||
let (pts, corner) = flatten(&segments(&bp.elements()[r]), closed, ctx.tol);
|
||||
@@ -159,6 +167,8 @@ struct Ctx<'a> {
|
||||
join: LineJoin,
|
||||
miter_limit: f64,
|
||||
tol: f64,
|
||||
/// The profile has one (left, right) width everywhere.
|
||||
uniform: bool,
|
||||
}
|
||||
|
||||
/// A vertex of a flattened subpath: where it is, the directions in and out, the lengths of the
|
||||
@@ -289,10 +299,22 @@ impl Ctx<'_> {
|
||||
// outline, so it stays sharp: fitting a curve through it would loop.
|
||||
pts.extend([(v.p + na * w, true), (v.p, true), (v.p + nb * w, true)]);
|
||||
} else if !v.corner {
|
||||
// A smooth point's inner offset follows its normal too: a sample added right beside a
|
||||
// vertex must not turn it into a detour to the centre.
|
||||
let n = normal.unwrap_or(v.p);
|
||||
pts.push((n, step));
|
||||
// A smooth point's inner offset follows its normal too. With a constant profile, once
|
||||
// the half-width reaches the local radius of curvature the offset crosses the centre of
|
||||
// curvature and comes back as a reversed contour, which the non-zero fill rules into a
|
||||
// hole. The stroke covers that centre, so cut through it instead: the cut is inside the
|
||||
// painted region and does not show. (With varying widths the crossing usually marks a
|
||||
// real caustic; a cut there would show as a slit, so leave it to the offset.)
|
||||
let turn = 2.0 * c.clamp(-1.0, 1.0).acos();
|
||||
let radius = if turn > 1e-9 { 0.5 * (v.la + v.lb) / turn } else { f64::INFINITY };
|
||||
if self.uniform && w >= radius {
|
||||
pts.push((v.p + na * w, true));
|
||||
pts.push((v.p, true));
|
||||
pts.push((v.p + nb * w, true));
|
||||
} else {
|
||||
let n = normal.unwrap_or(v.p);
|
||||
pts.push((n, step));
|
||||
}
|
||||
} else if w * (1.0 - c * c).sqrt() / c <= v.la.min(v.lb) {
|
||||
// A corner's inner offset is its miter while that stays inside both segments.
|
||||
pts.push((miter.unwrap_or(v.p), true));
|
||||
@@ -645,6 +667,16 @@ mod tests {
|
||||
assert!((a - want).abs() / want < 0.01, "{a} vs {want}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_uniform_circle_narrower_than_its_stroke_has_no_hole() {
|
||||
// Half-width 15 on a radius-5 circle: the inner offset crosses the centre. The constant
|
||||
// stroke still covers the centre, so the outline must not leave a hole there.
|
||||
let c = kurbo::Circle::new((0.0, 0.0), 5.0).to_path(1e-3);
|
||||
let p = WidthProfile { points: vec![(0.0, 1.0, 1.0), (1.0, 1.0, 1.0)] };
|
||||
let o = width_outline(&c, 30.0, &p, &capped(LineCap::Butt), 0.01);
|
||||
assert!(o.contains(Point::new(0.0, 0.0)), "{o:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_coarse_wide_circle_keeps_its_offset() {
|
||||
// A wide stroke flattened coarsely: offsetting each flattened segment by its miter
|
||||
|
||||
Reference in New Issue
Block a user