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.
This commit is contained in:
dotCooCoo
2026-11-12 11:45:53 -08:00
parent be50e5328b
commit 1f458a3ab0
2 changed files with 28 additions and 11 deletions
+17 -7
View File
@@ -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))))
+11 -4
View File
@@ -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:?}");