From 1f458a3ab01465e0558c444fb6fcda5334ca3b36 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 11:45:53 -0800 Subject: [PATCH] Transform: setBounds measures miter spikes of dashed strokes at a slant as drawn A miter join on a stroke whose dashes have round or projecting caps still paints the miter spikes the limit keeps, and the visual box includes them. object.setBounds with Use Preview Bounds did not count such a stroke as one whose margins follow its segments' directions, so a closed path at a slant missed the size typed when the width and height were out of proportion. --- crates/doc/src/reach.rs | 24 +++++++++++++++++------- crates/engine/src/tests/previewbounds.rs | 15 +++++++++++---- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/crates/doc/src/reach.rs b/crates/doc/src/reach.rs index 7f9b6a1766..4648f3087e 100644 --- a/crates/doc/src/reach.rs +++ b/crates/doc/src/reach.rs @@ -217,6 +217,14 @@ impl StrokeLayer { (self.cap == LineCap::Butt && !closed) || (self.join != LineJoin::Round && !capped_dashes) } + /// Does this stroke reach past a subpath that is `closed` or open by a margin that follows the + /// direction of its segments at their corners: where it stops at them + /// ([`Self::stops_at_corners`]) or paints miter spikes, which dashes with round or projecting + /// caps also paint? + fn reaches_by_corners(&self, closed: bool) -> bool { + self.stops_at_corners(closed) || self.join == LineJoin::Miter + } + /// The box this stroke's body paints along `segs`, the segments with length of a subpath, short /// of its caps, joined with `within`: each segment's [`swept_box`] and, with round joins on an /// open subpath, the arc each joint between segments paints ([`round_join_box`]), a shallow one @@ -506,16 +514,18 @@ impl Appearance { } /// Does this appearance have a visible stroke that stops at the corners of its segments on an - /// open subpath or a closed one: butt caps, or bevel and miter joins? + /// open subpath or a closed one, or paints miter spikes: butt caps, or bevel and miter joins + /// ([`StrokeLayer::reaches_by_corners`])? pub fn has_cornered_stroke(&self) -> bool { - self.painted_strokes().any(|s| s.stops_at_corners(false)) + self.painted_strokes().any(|s| s.reaches_by_corners(false)) } /// Does a visible stroke of this appearance that stops at the corners of its segments (butt - /// caps on an open subpath, bevel and miter joins) run along a subpath of `bp` where it does so, - /// with a segment that starts or ends at a slant, not square to the axes, once placed by `a`? - /// The visual box's margin at those corners, and at miter tips, depends on their direction, - /// which a scale out of proportion changes. + /// caps on an open subpath, bevel and miter joins) or paints miter spikes, dashed ones with + /// round or projecting caps included, run along a subpath of `bp` where it does so, with a + /// segment that starts or ends at a slant, not square to the axes, once placed by `a`? The + /// visual box's margin at those corners, and at miter tips, depends on their direction, which a + /// scale out of proportion changes. pub fn has_slanted_stroke_corners(&self, bp: &BezPath, a: vectorcraft_geom::Affine) -> bool { if !self.has_cornered_stroke() { return false; @@ -526,7 +536,7 @@ impl Appearance { }; let els = bp.elements(); let subs = subpaths(bp); - let cornered = |closed: bool| self.painted_strokes().any(|s| s.stops_at_corners(closed)); + let cornered = |closed: bool| self.painted_strokes().any(|s| s.reaches_by_corners(closed)); subs.into_iter() .filter(|(_, closed)| cornered(*closed)) .any(|(r, _)| drawn(&els[r]).iter().any(|s| slanted(tangent(s, 0.0)) || slanted(tangent(s, 1.0)))) diff --git a/crates/engine/src/tests/previewbounds.rs b/crates/engine/src/tests/previewbounds.rs index c0ce01cb57..e395c708bc 100644 --- a/crates/engine/src/tests/previewbounds.rs +++ b/crates/engine/src/tests/previewbounds.rs @@ -672,19 +672,26 @@ fn set_bounds_gives_a_butt_path_with_slanted_corners_the_size_out_of_proportion( fn set_bounds_gives_a_closed_path_with_beveled_or_mitered_corners_the_size_out_of_proportion() { // The bevel at the triangle's tip, the miter the limit bevels there and the miter spikes it // keeps reach past the path by margins that follow the slanted segments' directions, which a - // scale out of proportion changes. + // scale out of proportion changes. The miter spikes of a stroke whose dashes have round or + // projecting caps reach past the path too. let mut wrong = vec![]; - for (join, limit) in [("bevel", 10.0), ("miter", 2.0), ("miter", 10.0)] { + for (join, limit, cap, dash) in [ + ("bevel", 10.0, "round", Value::Null), + ("miter", 2.0, "round", Value::Null), + ("miter", 10.0, "round", Value::Null), + ("miter", 10.0, "round", json!([6, 3])), + ("miter", 10.0, "projecting", json!([6, 3])), + ] { let mut s = session(); let anchors = json!([{"x": 100, "y": 150}, {"x": 110, "y": 100}, {"x": 120, "y": 150}]); let id = NodeId(run(&mut s, "path.create", json!({"anchors": anchors, "closed": true}))["id"].as_u64().unwrap()); - run(&mut s, "stroke.set", json!({"weight": 2, "cap": "round", "join": join, "miterLimit": limit})); + run(&mut s, "stroke.set", json!({"weight": 2, "cap": cap, "join": join, "miterLimit": limit, "dash": dash})); preview(&mut s, true); run(&mut s, "select.set", json!({"ids": [id.0]})); run(&mut s, "object.setBounds", json!({"width": 60, "height": 20, "reference": 0, "strokes": false})); let v = s.transform_bounds(&[id]).unwrap(); if (v.width() - 60.0).abs() > 1e-6 || (v.height() - 20.0).abs() > 1e-6 { - wrong.push(format!("{join} {limit}: {v:?}")); + wrong.push(format!("{join} {limit} {cap} {dash}: {v:?}")); } } assert!(wrong.is_empty(), "{wrong:?}");