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.
This commit is contained in:
dotCooCoo
2026-11-13 19:48:30 -08:00
parent 446bf59bc2
commit f643cddec4
3 changed files with 56 additions and 1 deletions
+4
View File
@@ -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();
+50
View File
@@ -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::<Vec<_>>();
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]
+2 -1
View File
@@ -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