From f643cddec4c1ffaad52b89e79240a7f2f0f489ad Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 19:25:20 -0800 Subject: [PATCH] Dimension tool: a reflected aligned dimension has its line on the reflected side An unlinked aligned dimension that was reflected on its own kept its offset, which is measured along the left normal of a to b. A reflection turns that normal to the other side, so a line 30 pt above two level points stayed 30 pt above them after a flip top to bottom instead of going below them. The offset now changes sign when the transform reverses orientation, as a reflection or a scale by a negative factor does. Other transforms leave it as before, and linked, horizontal and vertical dimensions keep their own rules. Docs: docs/dimensions.md. --- crates/doc/src/dimension.rs | 4 +++ crates/engine/tests/it/dimensions.rs | 50 ++++++++++++++++++++++++++++ docs/dimensions.md | 3 +- 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/crates/doc/src/dimension.rs b/crates/doc/src/dimension.rs index 89df899743..e803d867fc 100644 --- a/crates/doc/src/dimension.rs +++ b/crates/doc/src/dimension.rs @@ -815,6 +815,10 @@ impl Dimension { if self.line_between_points() && !old.line_between_points() { self.keep_gap(&old); } + } else if self.mode == DimensionMode::Linear && xf.determinant() < 0.0 && (self.b - self.a).hypot() > 1e-9 { + // A reflection turns the left normal of a → b to the other side, so an aligned + // dimension's line stays on the reflected side of its points. + self.offset = -self.offset; } if self.mode == DimensionMode::Linear { self.length = self.measured(); diff --git a/crates/engine/tests/it/dimensions.rs b/crates/engine/tests/it/dimensions.rs index 56ee67be17..240a7fe280 100644 --- a/crates/engine/tests/it/dimensions.rs +++ b/crates/engine/tests/it/dimensions.rs @@ -419,6 +419,56 @@ fn a_reflected_horizontal_dimension_keeps_its_line_on_the_reflected_side() { assert_eq!(text(&s, id), "100.00 pt"); } +/// An unlinked aligned dimension reflected on its own has its line on the reflected side of its +/// points: a line 30 pt above a level pair is 30 pt below it after a flip top to bottom, and a +/// half turn moves it below them as well, by turning it. +#[test] +fn a_reflected_aligned_dimension_keeps_its_line_on_the_reflected_side() { + let mut s = session(); + let id = run(&mut s, "dimension.create", json!({"a":[100,200],"b":[200,200],"offset":-30}))["id"].as_u64().unwrap(); + assert_eq!(path_ends(&s, id)[2], vec![(100.0, 170.0), (200.0, 170.0)]); + run(&mut s, "select.set", json!({"ids":[id]})); + run(&mut s, "object.reflect", json!({"axis":"horizontal","origin":[0,200]})); + assert_eq!(path_ends(&s, id)[2], vec![(100.0, 230.0), (200.0, 230.0)]); + assert_eq!(text(&s, id), "100.00 pt"); + // Reflected left to right, the points swap ends and the line stays below them. + run(&mut s, "object.reflect", json!({"axis":"vertical","origin":[150,0]})); + assert_eq!(path_ends(&s, id)[2], vec![(200.0, 230.0), (100.0, 230.0)]); + run(&mut s, "edit.undo", json!({})); + run(&mut s, "edit.undo", json!({})); + assert_eq!(path_ends(&s, id)[2], vec![(100.0, 170.0), (200.0, 170.0)]); + // A half turn about the points' midpoint puts the line below them by turning it. + run(&mut s, "object.rotate", json!({"angle":180,"origin":[150,200]})); + let ends = path_ends(&s, id)[2].iter().map(|(x, y)| (x.round(), y.round())).collect::>(); + assert_eq!(ends, vec![(200.0, 230.0), (100.0, 230.0)]); +} + +/// An unlinked aligned dimension of a diagonal pair reflected about a mirror line through its +/// points has its line on the other side of them, and a scale by a negative factor reverses the +/// side as a reflection does, while a scale by positive factors keeps it. +#[test] +fn an_aligned_dimension_reflected_about_an_angle_or_scaled_by_a_negative_factor_changes_sides() { + let mut s = session(); + let id = run(&mut s, "dimension.create", json!({"a":[100,200],"b":[200,100],"offset":-30}))["id"].as_u64().unwrap(); + let k = 30.0 / 2f64.sqrt(); + let near = |got: &[(f64, f64)], want: [(f64, f64); 2]| got.iter().zip(want).all(|(g, w)| (g.0 - w.0).abs() < 1e-6 && (g.1 - w.1).abs() < 1e-6); + let ends = path_ends(&s, id)[2].clone(); + assert!(near(&ends, [(100.0 - k, 200.0 - k), (200.0 - k, 100.0 - k)]), "{ends:?}"); + run(&mut s, "select.set", json!({"ids":[id]})); + // The mirror line at 45 degrees through (150, 150) holds both points. + run(&mut s, "object.reflect", json!({"axis":45,"origin":[150,150]})); + let ends = path_ends(&s, id)[2].clone(); + assert!(near(&ends, [(100.0 + k, 200.0 + k), (200.0 + k, 100.0 + k)]), "{ends:?}"); + run(&mut s, "edit.undo", json!({})); + let level = run(&mut s, "dimension.create", json!({"a":[100,200],"b":[200,200],"offset":-30}))["id"].as_u64().unwrap(); + run(&mut s, "select.set", json!({"ids":[level]})); + run(&mut s, "object.scale", json!({"sx":-200,"sy":100,"origin":[150,0]})); + assert_eq!(path_ends(&s, level)[2], vec![(250.0, 170.0), (50.0, 170.0)]); + run(&mut s, "edit.undo", json!({})); + run(&mut s, "object.scale", json!({"sx":200,"sy":200,"origin":[150,200]})); + assert_eq!(path_ends(&s, level)[2], vec![(50.0, 170.0), (250.0, 170.0)]); +} + /// The line of a linked horizontal dimension below both points stays below them when the art /// turns a half turn; a line between the points keeps its distance from the first point. #[test] diff --git a/docs/dimensions.md b/docs/dimensions.md index 9cba8a66dc..377ab9d698 100644 --- a/docs/dimensions.md +++ b/docs/dimensions.md @@ -66,7 +66,8 @@ Measurements follow the art they measure through moves, scaling and anchor and h undo and redo included. When the art is reflected, the line of a linked linear dimension goes to the reflected side, as far past the outermost point as before. This does not happen for art whose anchors and handles all lie on one line, such as a straight line, which a half turn puts in the -same place as a reflection. Deleting the art, changing its number of anchors or whether it is +same place as a reflection. An unlinked aligned dimension that is reflected, or scaled by a +negative factor, has its line on the reflected side of its points. Deleting the art, changing its number of anchors or whether it is closed, turning an angle's arm into a curve, or making a measured circle or arc no longer circular leaves its dimensions where they were, no longer linked, in the same undo step, rather than attaching them to a different edge. Undo links them again. A linked dimension moved on its own