Angular constraint between two lines holds the angle it draws, whichever way the lines run (part of #1134) (#1140)

This commit is contained in:
Marco Arnulfo
2026-10-19 02:30:05 -04:00
committed by GitHub
parent cdc61f32b5
commit 15b5395c06
2 changed files with 50 additions and 7 deletions
+21 -7
View File
@@ -40,6 +40,11 @@ pub(crate) enum Form {
C1,
/// Arc sweep (angular on an arc).
ArcSweep,
/// Two lines measured along the rays from their intersection to each line's far end, the
/// angle the dimension draws; `rev` marks a line whose far end is its start point.
Rays {
rev: [bool; 2],
},
/// Fix of a derived point (residual to an anchor).
Anchor(Vec2),
/// Fix handled by locking parameters (no rows).
@@ -92,6 +97,10 @@ fn dir_n(a: Vec2, b: Vec2) -> Vec2 {
if l > 1e-12 { d * (1.0 / l) } else { d }
}
fn ray(a: Vec2, b: Vec2, rev: bool) -> Vec2 {
if rev { dir_n(b, a) } else { dir_n(a, b) }
}
fn sgn(v: f64) -> f64 {
if v < 0.0 { -1.0 } else { 1.0 }
}
@@ -284,10 +293,15 @@ impl Built {
}
K::Angular => {
b.rows = 1;
if n == 2 && ln(0).is_some() && ln(1).is_some() {
b.form = Form::LL;
let (Some((a1, b1)), Some((a2, b2))) = (ln(0), ln(1)) else { return need("two lines") };
let (d1, d2) = (dir_n(a1, b1), dir_n(a2, b2));
if let (2, Some((a1, b1)), Some((a2, b2))) = (n, ln(0), ln(1)) {
// Start-to-end directions would measure the supplement when a line is drawn
// towards the vertex, so use the rays the dimension draws.
let rev = match cadcraft_geom::line_line_infinite(a1, b1, a2, b2) {
Some((_, t, u)) => [t > 0.5, u > 0.5],
None => [false, false],
};
b.form = Form::Rays { rev };
let (d1, d2) = (ray(a1, b1, rev[0]), ray(a2, b2, rev[1]));
b.sign = sgn(d1.cross(d2).atan2(d1.dot(d2)));
} else if n == 1 && m.arc_sweep(x, r_at(c, 0).ok_or_else(|| "Angular needs references".to_string())?).is_some() {
b.form = Form::ArcSweep;
@@ -297,7 +311,7 @@ impl Built {
b.target = target.to_radians();
// Two lines measure a signed angle in [-180, 180] and the sign is re-read from the geometry, so a larger
// target would flip the sign once solved and never be satisfied; only an arc sweep can reach a full turn.
let max = if b.form == Form::LL { std::f64::consts::PI } else { std::f64::consts::TAU };
let max = if matches!(b.form, Form::Rays { .. }) { std::f64::consts::PI } else { std::f64::consts::TAU };
if !b.target.is_finite() || b.target.abs() > max {
return Err("angle out of range".into());
}
@@ -461,9 +475,9 @@ impl Built {
(K::Angular, Form::ArcSweep) => {
out.push(m.arc_sweep(x, r(0)?)? - self.target);
}
(K::Angular, _) => {
(K::Angular, Form::Rays { rev }) => {
let ((a1, b1), (a2, b2)) = (ln(0)?, ln(1)?);
let (d1, d2) = (dir_n(a1, b1), dir_n(a2, b2));
let (d1, d2) = (ray(a1, b1, rev[0]), ray(a2, b2, rev[1]));
out.push(wrap(d1.cross(d2).atan2(d1.dot(d2)) - self.sign * self.target));
}
(K::Radius, _) => out.push(ci(0)?.1 - self.target),
@@ -231,3 +231,32 @@ fn delconstraint_rejects_referenced_dimension() {
s.execute("delconstraint", &json!({ "ids": ids })).unwrap();
assert!(s.doc().unwrap().constraints.is_empty());
}
/// The angle between the two rays from the vertex to the far end of each line: the angle
/// DCANGULAR draws.
fn drawn_angle(s: &Session, a: &str, b: &str) -> f64 {
let ((a0, a1), (b0, b1)) = (ends(s, a), ends(s, b));
let (o, _, _) = cadcraft_geom::line_line_infinite(a0, a1, b0, b1).unwrap();
let far = |p: Vec2, q: Vec2| if p.dist(o) > q.dist(o) { p } else { q };
let (u, v) = (far(a0, a1) - o, far(b0, b1) - o);
u.cross(v).atan2(u.dot(v)).abs().to_degrees()
}
#[test]
fn dcangular_constrains_the_drawn_angle_whichever_way_the_lines_run() {
// #1134: the second line drawn towards the shared vertex got the supplement of the value.
for b_pts in [[[2.0, 10.0], [0.0, 0.0]], [[0.0, 0.0], [2.0, 10.0]]] {
let mut s = Session::new();
s.viewport_px = (1000.0, 1000.0);
let a = line(&mut s, [0.0, 0.0], [10.0, 0.0]);
let b = line(&mut s, b_pts[0], b_pts[1]);
s.cmdline("dcangular").unwrap();
s.input(Input::Point(Vec2::new(3.0, 0.0))).unwrap();
s.input(Input::Point(Vec2::new(0.6, 3.0))).unwrap();
assert!(s.prompt_text().contains("78.69"), "{b_pts:?}: {}", s.prompt_text());
s.cmdline("100").unwrap();
assert!(s.running.is_none());
let got = drawn_angle(&s, &a, &b);
assert!((got - 100.0).abs() < 1e-6, "{b_pts:?}: drawn angle {got}");
}
}