Merge pull request #6380 from dotCooCoo/fix/preview-bounds-butt-line

Transform: butt-capped lines measure to their end points with Use Preview Bounds
This commit is contained in:
BFlat
2026-11-10 09:59:06 +07:00
committed by GitHub
7 changed files with 434 additions and 45 deletions
+9 -4
View File
@@ -125,15 +125,20 @@ fn bounds_transform(s: &Session, p: &Value, ids: Vec<NodeId>, least: f64, anchor
// The scales and the point above can miss the size or the reference point when the margins of
// the objects differ, the members of groups and the objects in symbols' art included, and with
// strokes that scale, when the sizes are out of proportion. The objects are then measured one
// by one ([`Spans::measure`]), and the scales solved for. Objects with geometry effects are
// measured as drawn after each scale tried instead ([`refit_drawn`]).
// by one ([`Spans::measure`]), and the scales solved for. Objects with geometry effects, and
// objects with a butt-capped stroke along a segment at a slant, whose margins change with the
// segment's direction, are measured as drawn after each scale tried instead ([`refit_drawn`]).
let symbols = d.symbol_boxes();
let drawn = || ids.iter().filter_map(|id| d.node(*id)).any(|n| effects::preview::draws_geometry(&symbols, n));
let to_local = frame.to_doc().inverse();
let drawn = || {
ids.iter()
.filter_map(|id| d.node(*id))
.any(|n| effects::preview::draws_geometry(&symbols, n) || n.has_slanted_butt_stroke(&symbols, to_local))
};
let solved = if !(s.prefs.use_preview_bounds && w >= 0.0 && h >= 0.0) {
None
} else if transforms_objects(p) && !bool_or(p, "typeAreas", false) && drawn() {
let options = scaling_options(s, p);
let to_local = frame.to_doc().inverse();
let measure = |sx: f64, sy: f64| {
let xf = frame.conjugate(about(rp, Affine::scale_non_uniform(sx, sy)));
let sc = if Scaling::factor(xf).is_some() { options } else { Scaling::default() };
+71
View File
@@ -618,6 +618,77 @@ fn set_bounds_leaves_the_height_of_lines_with_no_height() {
assert!(close(geometric(&s, ids[1]), Rect::new(8.3, 100.0, 30.0, 100.0)), "{:?}", geometric(&s, ids[1]));
}
/// A line with a 2 pt stroke and butt caps, selected, with Use Preview Bounds on.
fn butt_line(s: &mut Session, from: (f64, f64), to: (f64, f64)) -> NodeId {
let id = NodeId(run(s, "shape.line", json!({"x1": from.0, "y1": from.1, "x2": to.0, "y2": to.1}))["id"].as_u64().unwrap());
run(s, "stroke.set", json!({"weight": 2, "cap": "butt"}));
preview(s, true);
run(s, "select.set", json!({"ids": [id.0]}));
id
}
#[test]
fn set_bounds_gives_a_butt_line_the_whole_height() {
// A butt cap ends at the anchor, so 10 mm (28.3465 pt) is the length of the line itself.
let mut s = session();
let id = butt_line(&mut s, (100.0, 100.0), (100.0, 140.0));
assert!(close(s.transform_bounds(&[id]).unwrap(), Rect::new(99.0, 100.0, 101.0, 140.0)), "{:?}", s.transform_bounds(&[id]));
run(&mut s, "object.setBounds", json!({"height": 28.3465, "reference": 0, "strokes": false}));
assert!(close(geometric(&s, id), Rect::new(100.0, 100.0, 100.0, 128.3465)), "{:?}", geometric(&s, id));
assert!(close(s.transform_bounds(&[id]).unwrap(), Rect::new(99.0, 100.0, 101.0, 128.3465)), "{:?}", s.transform_bounds(&[id]));
}
#[test]
fn set_bounds_gives_a_diagonal_butt_line_the_size_out_of_proportion() {
// The stroke's corners follow the line's slope, which a scale out of proportion changes.
let mut s = session();
let id = butt_line(&mut s, (100.0, 100.0), (130.0, 140.0));
let before = s.transform_bounds(&[id]).unwrap();
// The stroke's corners reach 0.8 pt across x and 0.6 pt across y at this 3-4-5 slope.
assert!(close(before, Rect::new(99.2, 99.4, 130.8, 140.6)), "{before:?}");
run(&mut s, "object.setBounds", json!({"height": 100, "reference": 0, "strokes": false}));
let v = s.transform_bounds(&[id]).unwrap();
assert!((v.height() - 100.0).abs() < 1e-6, "{v:?}");
}
#[test]
fn set_bounds_gives_a_butt_path_with_slanted_corners_the_size_out_of_proportion() {
// Square ends, but the bevel join at the apex sets the top edge by the slanted segments'
// corners, which a scale out of proportion moves.
let mut s = session();
let anchors = json!([{"x": 100, "y": 150}, {"x": 100, "y": 140}, {"x": 110, "y": 100}, {"x": 120, "y": 140}, {"x": 120, "y": 150}]);
let id = NodeId(run(&mut s, "path.create", json!({"anchors": anchors}))["id"].as_u64().unwrap());
run(&mut s, "stroke.set", json!({"weight": 2, "cap": "butt", "join": "bevel"}));
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();
assert!((v.width() - 60.0).abs() < 1e-6 && (v.height() - 20.0).abs() < 1e-6, "{v:?}");
}
#[test]
fn set_bounds_gives_a_symbol_instance_of_a_diagonal_butt_line_the_size_out_of_proportion() {
let mut s = session();
butt_line(&mut s, (100.0, 100.0), (130.0, 140.0));
let id = NodeId(run(&mut s, "symbol.new", json!({"name": "Line"}))["id"].as_u64().unwrap());
run(&mut s, "select.set", json!({"ids": [id.0]}));
run(&mut s, "object.setBounds", json!({"width": 20, "height": 100, "reference": 0, "strokes": false}));
let v = s.transform_bounds(&[id]).unwrap();
assert!((v.width() - 20.0).abs() < 1e-6 && (v.height() - 100.0).abs() < 1e-6, "{v:?}");
}
#[test]
fn align_places_a_vertical_butt_line_by_its_end_points() {
// With Use Preview Bounds on, a butt line's ends line up with the top of a rectangle without
// a stroke.
let mut s = session();
rect(&mut s, 20.0, 50.0, 0.0);
let id = butt_line(&mut s, (200.0, 160.0), (200.0, 190.0));
run(&mut s, "select.all", json!({}));
run(&mut s, "object.align", json!({"vertical": "top"}));
assert!(close(geometric(&s, id), Rect::new(200.0, 100.0, 200.0, 130.0)), "{:?}", geometric(&s, id));
}
/// The rectangles of [`thin_and_thick`] in a group, the thick one in a group of its own inside it
/// when `nested`, selected with an unstroked rectangle from x 100 to 110 when `plain`. Returns the
/// selected objects.