cadcraft/crates/constraints/src/tests.rs

492 lines
21 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use cadcraft_doc::{Arc as DArc, Circle as DCircle, Common, DistAxis, EntityKind, Line as DLine, LwPolyline, Point as DPoint, Space, Sub};
use cadcraft_geom::{PolyVertex, Vec3};
use super::*;
fn v(x: f64, y: f64) -> Vec3 {
Vec3::new(x, y, 0.0)
}
fn add_e(d: &mut Drawing, k: EntityKind) -> Handle {
d.add(&Space::Model, Common::default(), k).unwrap()
}
fn line(d: &mut Drawing, a: (f64, f64), b: (f64, f64)) -> Handle {
add_e(d, EntityKind::Line(DLine { a: v(a.0, a.1), b: v(b.0, b.1) }))
}
fn circle(d: &mut Drawing, c: (f64, f64), r: f64) -> Handle {
add_e(d, EntityKind::Circle(DCircle { center: v(c.0, c.1), radius: r }))
}
fn arc(d: &mut Drawing, c: (f64, f64), r: f64, s: f64, e: f64) -> Handle {
add_e(d, EntityKind::Arc(DArc { center: v(c.0, c.1), radius: r, start: s, end: e }))
}
fn con(kind: ConstraintKind, refs: &[GeomRef]) -> Constraint {
Constraint { id: 0, kind, refs: refs.to_vec(), name: String::new(), expr: String::new() }
}
fn dim(kind: ConstraintKind, refs: &[GeomRef], name: &str, expr: &str) -> Constraint {
Constraint { id: 0, kind, refs: refs.to_vec(), name: name.into(), expr: expr.into() }
}
fn w(h: Handle) -> GeomRef {
GeomRef::whole(h)
}
fn s(h: Handle, sub: Sub) -> GeomRef {
GeomRef::new(h, sub)
}
fn ln(d: &Drawing, h: Handle) -> (Vec2, Vec2) {
match &d.entity(h).unwrap().kind {
EntityKind::Line(l) => (l.a.xy(), l.b.xy()),
_ => panic!(),
}
}
fn circ(d: &Drawing, h: Handle) -> (Vec2, f64) {
match &d.entity(h).unwrap().kind {
EntityKind::Circle(c) => (c.center.xy(), c.radius),
EntityKind::Arc(c) => (c.center.xy(), c.radius),
_ => panic!(),
}
}
fn ok(d: &mut Drawing, c: Constraint) -> u32 {
add(d, c, &SolveOptions::default()).unwrap()
}
fn near(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-6
}
#[test]
fn coincident_points_and_point_on_curve() {
let mut d = Drawing::default();
let a = line(&mut d, (0.0, 0.0), (10.0, 0.0));
let b = line(&mut d, (10.3, 0.4), (10.0, 8.0));
ok(&mut d, con(ConstraintKind::Coincident, &[s(a, Sub::End), s(b, Sub::Start)]));
let (la, lb) = (ln(&d, a), ln(&d, b));
assert!(la.1.near(lb.0, 1e-7));
// The second object moved, the first stayed (minimum movement prefers equal split, but both
// ends ended up together).
let c = circle(&mut d, (5.0, 6.0), 2.0);
let p = add_e(&mut d, EntityKind::Point(DPoint { p: v(5.0, 9.0), angle: 0.0 }));
ok(&mut d, con(ConstraintKind::Coincident, &[w(p), w(c)]));
let (cc, r) = circ(&d, c);
let pp = match &d.entity(p).unwrap().kind {
EntityKind::Point(q) => q.p.xy(),
_ => panic!(),
};
assert!(near(pp.dist(cc), r));
}
#[test]
fn horizontal_vertical_parallel_perpendicular() {
let mut d = Drawing::default();
let a = line(&mut d, (0.0, 0.0), (10.0, 0.5));
let b = line(&mut d, (0.0, 2.0), (0.3, 9.0));
let c = line(&mut d, (5.0, 5.0), (9.0, 6.0));
let e = line(&mut d, (5.0, -5.0), (6.0, -1.0));
ok(&mut d, con(ConstraintKind::Horizontal, &[w(a)]));
ok(&mut d, con(ConstraintKind::Vertical, &[w(b)]));
ok(&mut d, con(ConstraintKind::Parallel, &[w(a), w(c)]));
ok(&mut d, con(ConstraintKind::Perpendicular, &[w(a), w(e)]));
let (a0, a1) = ln(&d, a);
assert!(near(a0.y, a1.y));
let (b0, b1) = ln(&d, b);
assert!(near(b0.x, b1.x));
let (c0, c1) = ln(&d, c);
assert!(near(c0.y, c1.y));
let (e0, e1) = ln(&d, e);
assert!(near(e0.x, e1.x));
}
#[test]
fn collinear_concentric_equal() {
let mut d = Drawing::default();
let a = line(&mut d, (0.0, 0.0), (10.0, 0.0));
let b = line(&mut d, (12.0, 0.5), (20.0, -0.3));
ok(&mut d, con(ConstraintKind::Collinear, &[w(a), w(b)]));
let (a0, a1) = ln(&d, a);
let (b0, b1) = ln(&d, b);
let dir = (a1 - a0).normalized();
assert!(dir.cross(b0 - a0).abs() < 1e-6 && dir.cross(b1 - a0).abs() < 1e-6);
let c1 = circle(&mut d, (0.0, 20.0), 3.0);
let c2 = arc(&mut d, (0.5, 20.4), 5.0, 0.0, 2.0);
ok(&mut d, con(ConstraintKind::Concentric, &[w(c1), w(c2)]));
assert!(circ(&d, c1).0.near(circ(&d, c2).0, 1e-7));
ok(&mut d, con(ConstraintKind::Equal, &[w(c1), w(c2)]));
assert!(near(circ(&d, c1).1, circ(&d, c2).1));
ok(&mut d, con(ConstraintKind::Equal, &[w(a), w(b)]));
let (a0, a1) = ln(&d, a);
let (b0, b1) = ln(&d, b);
assert!(near(a0.dist(a1), b0.dist(b1)));
}
#[test]
fn tangent_and_smooth() {
let mut d = Drawing::default();
let l = line(&mut d, (-10.0, 0.0), (10.0, 0.0));
let c = circle(&mut d, (0.0, 3.5), 3.0);
ok(&mut d, con(ConstraintKind::Tangent, &[w(l), w(c)]));
let (cc, r) = circ(&d, c);
let (a, b) = ln(&d, l);
assert!(near((b - a).normalized().cross(cc - a).abs(), r));
// Circle–circle external.
let c2 = circle(&mut d, (8.0, 3.5), 4.0);
ok(&mut d, con(ConstraintKind::Tangent, &[w(c), w(c2)]));
let ((p, r1), (q, r2)) = (circ(&d, c), circ(&d, c2));
assert!(near(p.dist(q), r1 + r2));
// Smooth behaves as tangent.
let l2 = line(&mut d, (20.0, 0.0), (30.0, 1.0));
let a2 = arc(&mut d, (25.0, 3.0), 2.5, 0.0, 3.0);
ok(&mut d, con(ConstraintKind::Smooth, &[w(l2), w(a2)]));
let ((ca, ra), (x0, x1)) = (circ(&d, a2), ln(&d, l2));
assert!(near((x1 - x0).normalized().cross(ca - x0).abs(), ra));
}
#[test]
fn symmetric_fix() {
let mut d = Drawing::default();
let axis = line(&mut d, (0.0, -10.0), (0.0, 10.0));
ok(&mut d, con(ConstraintKind::Fix, &[w(axis)]));
let a = line(&mut d, (-5.0, 0.0), (-3.0, 4.0));
let b = line(&mut d, (5.5, 0.3), (2.0, 4.5));
ok(&mut d, con(ConstraintKind::Symmetric, &[w(a), w(b), w(axis)]));
assert_eq!(ln(&d, axis), (Vec2::new(0.0, -10.0), Vec2::new(0.0, 10.0)));
let (a0, a1) = ln(&d, a);
let (b0, b1) = ln(&d, b);
assert!(a0.near(Vec2::new(-b0.x, b0.y), 1e-6) && a1.near(Vec2::new(-b1.x, b1.y), 1e-6));
// Symmetric points.
let p = add_e(&mut d, EntityKind::Point(DPoint { p: v(-2.0, 7.0), angle: 0.0 }));
let q = add_e(&mut d, EntityKind::Point(DPoint { p: v(2.5, 6.0), angle: 0.0 }));
ok(&mut d, con(ConstraintKind::Symmetric, &[w(p), w(q), w(axis)]));
}
#[test]
fn two_line_angle_over_180_is_rejected() {
let mut d = Drawing::default();
let a = line(&mut d, (0.0, 0.0), (10.0, 0.0));
let b = line(&mut d, (0.0, 0.0), (0.0, 5.0));
let before = (ln(&d, a), ln(&d, b));
assert!(add(&mut d, dim(ConstraintKind::Angular, &[w(a), w(b)], "", "200"), &SolveOptions::default()).is_err());
assert!(d.constraints.is_empty());
assert_eq!((ln(&d, a), ln(&d, b)), before);
ok(&mut d, dim(ConstraintKind::Angular, &[w(a), w(b)], "", "180"));
assert!(all_satisfied(&d));
}
#[test]
fn dimensional_constraints() {
let mut d = Drawing::default();
let a = line(&mut d, (0.0, 0.0), (10.0, 1.0));
ok(&mut d, dim(ConstraintKind::Distance(DistAxis::Aligned), &[w(a)], "", "12"));
let (a0, a1) = ln(&d, a);
assert!(near(a0.dist(a1), 12.0));
let b = line(&mut d, (0.0, 5.0), (7.0, 9.0));
ok(&mut d, dim(ConstraintKind::Distance(DistAxis::Horizontal), &[w(b)], "", "4"));
ok(&mut d, dim(ConstraintKind::Distance(DistAxis::Vertical), &[w(b)], "", "d1/4"));
let (b0, b1) = ln(&d, b);
assert!(near(b1.x - b0.x, 4.0) && near(b1.y - b0.y, 3.0));
let c = line(&mut d, (20.0, 0.0), (30.0, 2.0));
let e = line(&mut d, (20.0, 0.0), (25.0, 6.0));
ok(&mut d, dim(ConstraintKind::Angular, &[w(c), w(e)], "", "45"));
let ((c0, c1), (e0, e1)) = (ln(&d, c), ln(&d, e));
let (u, t) = ((c1 - c0).normalized(), (e1 - e0).normalized());
assert!(near(u.cross(t).atan2(u.dot(t)).to_degrees(), 45.0));
let k = circle(&mut d, (0.0, 30.0), 3.0);
ok(&mut d, dim(ConstraintKind::Radius, &[w(k)], "", "5"));
assert!(near(circ(&d, k).1, 5.0));
let k2 = arc(&mut d, (20.0, 30.0), 3.0, 0.0, 1.0);
ok(&mut d, dim(ConstraintKind::Diameter, &[w(k2)], "", "rad1*2+2"));
assert!(near(circ(&d, k2).1, 6.0));
let names: Vec<String> = d.constraints.iter().map(|c| c.name.clone()).collect();
assert_eq!(names, ["d1", "d2", "d3", "ang1", "rad1", "dia1"]);
// Measured default value.
let f = line(&mut d, (0.0, 50.0), (3.0, 54.0));
let id = ok(&mut d, dim(ConstraintKind::Distance(DistAxis::Aligned), &[w(f)], "", ""));
assert_eq!(d.constraints.iter().find(|c| c.id == id).unwrap().expr, "5");
assert!(all_satisfied(&d));
}
/// A rectangle of four lines: coincident corners, horizontal/vertical sides, a fixed corner,
/// width and height parameters. Changing a parameter moves the geometry.
#[test]
fn rectangle_chain_with_parameters() {
let mut d = Drawing::default();
let bottom = line(&mut d, (0.0, 0.0), (10.2, 0.3));
let right = line(&mut d, (10.0, 0.1), (9.8, 5.0));
let top = line(&mut d, (10.1, 5.2), (0.2, 4.9));
let left = line(&mut d, (0.0, 5.0), (0.1, -0.2));
for (p, q) in [(bottom, right), (right, top), (top, left), (left, bottom)] {
ok(&mut d, con(ConstraintKind::Coincident, &[s(p, Sub::End), s(q, Sub::Start)]));
}
ok(&mut d, con(ConstraintKind::Horizontal, &[w(bottom)]));
ok(&mut d, con(ConstraintKind::Horizontal, &[w(top)]));
ok(&mut d, con(ConstraintKind::Vertical, &[w(left)]));
ok(&mut d, con(ConstraintKind::Vertical, &[w(right)]));
ok(&mut d, con(ConstraintKind::Fix, &[s(bottom, Sub::Start)]));
set_parameter(&mut d, "width", "10", &SolveOptions::default()).unwrap();
ok(&mut d, dim(ConstraintKind::Distance(DistAxis::Horizontal), &[w(bottom)], "w", "width"));
ok(&mut d, dim(ConstraintKind::Distance(DistAxis::Vertical), &[w(right)], "h", "w/2"));
// Equal opposite sides are implied, so adding one is redundant.
let e = add(&mut d, con(ConstraintKind::Equal, &[w(bottom), w(top)]), &SolveOptions::default());
assert!(matches!(e, Err(SolveError::Redundant { .. })), "{e:?}");
let corners = |d: &Drawing| [ln(d, bottom).0, ln(d, bottom).1, ln(d, top).0, ln(d, top).1];
let c = corners(&d);
let o = c[0];
assert!((c[1] - o).near(Vec2::new(10.0, 0.0), 1e-6), "{c:?}");
assert!((c[2] - o).near(Vec2::new(10.0, 5.0), 1e-6), "{c:?}");
assert!((c[3] - o).near(Vec2::new(0.0, 5.0), 1e-6), "{c:?}");
set_parameter(&mut d, "width", "20", &SolveOptions::default()).unwrap();
let c = corners(&d);
assert!(c[0].near(o, 1e-12), "fixed corner moved");
assert!((c[2] - o).near(Vec2::new(20.0, 10.0), 1e-6), "{c:?}");
assert!(all_satisfied(&d));
// A bad expression is refused and leaves the drawing alone.
let before = d.clone();
assert!(set_parameter(&mut d, "width", "w*2", &SolveOptions::default()).is_err());
assert!(set_parameter(&mut d, "width", "1/0", &SolveOptions::default()).is_err());
assert!(set_parameter(&mut d, "width", "(", &SolveOptions::default()).is_err());
assert_eq!(d, before);
let table = parameter_table(&d);
assert_eq!(table.len(), 3);
assert!(table.iter().any(|r| r.name == "h" && r.value == Some(10.0)));
}
#[test]
fn polyline_rectangle() {
let mut d = Drawing::default();
let vs = [(0.0, 0.0), (8.0, 0.4), (8.3, 3.0), (-0.2, 3.1)].iter().map(|(x, y)| PolyVertex::new(Vec2::new(*x, *y))).collect();
let p = add_e(&mut d, EntityKind::LwPolyline(LwPolyline { vertices: vs, closed: true, const_width: 0.0, elevation: 0.0, plinegen: false }));
for i in [0, 2] {
ok(&mut d, con(ConstraintKind::Horizontal, &[s(p, Sub::Segment(i))]));
}
for i in [1, 3] {
ok(&mut d, con(ConstraintKind::Vertical, &[s(p, Sub::Segment(i))]));
}
ok(&mut d, con(ConstraintKind::Fix, &[s(p, Sub::Vertex(0))]));
ok(&mut d, dim(ConstraintKind::Distance(DistAxis::Aligned), &[s(p, Sub::Segment(0))], "", "6"));
ok(&mut d, dim(ConstraintKind::Distance(DistAxis::Aligned), &[s(p, Sub::Segment(1))], "", "2"));
let EntityKind::LwPolyline(pl) = &d.entity(p).unwrap().kind else { panic!() };
let pts: Vec<Vec2> = pl.vertices.iter().map(|v| v.p).collect();
// Vertex 0 was fixed where the earlier constraints left it.
assert!((pts[2] - pts[0]).near(Vec2::new(6.0, 2.0), 1e-6), "{pts:?}");
}
#[test]
fn conflicts_are_reported_and_nothing_moves() {
let mut d = Drawing::default();
let a = line(&mut d, (0.0, 0.0), (10.0, 1.0));
let h = ok(&mut d, con(ConstraintKind::Horizontal, &[w(a)]));
let before = d.clone();
// Horizontal and vertical on the same (non-degenerate) line, with a length: inconsistent.
let l = ok(&mut d, dim(ConstraintKind::Distance(DistAxis::Aligned), &[w(a)], "", "10"));
let before2 = d.clone();
let e = add(&mut d, con(ConstraintKind::Vertical, &[w(a)]), &SolveOptions::default());
match e {
Err(SolveError::Conflict { ids }) => {
assert_eq!(ids[0], 3);
assert!(ids.contains(&h) || ids.contains(&l), "{ids:?}");
}
other => panic!("{other:?}"),
}
assert_eq!(d, before2);
// Redundant: horizontal twice.
assert!(matches!(add(&mut d, con(ConstraintKind::Horizontal, &[w(a)]), &SolveOptions::default()), Err(SolveError::Redundant { .. })));
// Fixed line can't be made vertical.
let mut d2 = before.clone();
let b = line(&mut d2, (0.0, 0.0), (3.0, 4.0));
ok(&mut d2, con(ConstraintKind::Fix, &[w(b)]));
assert!(matches!(add(&mut d2, con(ConstraintKind::Vertical, &[w(b)]), &SolveOptions::default()), Err(SolveError::Conflict { .. })));
// A direct solve of an inconsistent stored system fails without writing.
let mut d3 = before.clone();
d3.constraints.push(Constraint { id: 9, kind: ConstraintKind::Vertical, refs: vec![w(a)], name: String::new(), expr: String::new() });
d3.constraints.push(dim(ConstraintKind::Distance(DistAxis::Aligned), &[w(a)], "d9", "10"));
let snapshot = d3.clone();
assert!(matches!(solve(&mut d3, &SolveOptions::default()), Err(SolveError::Conflict { .. })));
assert_eq!(d3, snapshot);
}
#[test]
fn keep_option_moves_the_other_entity() {
let mut d = Drawing::default();
let a = line(&mut d, (0.0, 0.0), (10.0, 0.0));
let b = line(&mut d, (0.0, 2.0), (10.0, 4.0));
add(&mut d, con(ConstraintKind::Parallel, &[w(a), w(b)]), &SolveOptions { keep: vec![a], prefer_move: vec![b] }).unwrap();
let (a0, a1) = ln(&d, a);
assert!(a0.near(Vec2::new(0.0, 0.0), 1e-3) && a1.near(Vec2::new(10.0, 0.0), 1e-3), "{a0:?} {a1:?}");
}
#[test]
fn invalid_references_and_purge() {
let mut d = Drawing::default();
let a = line(&mut d, (0.0, 0.0), (10.0, 0.0));
let c = circle(&mut d, (0.0, 5.0), 1.0);
let t = add_e(
&mut d,
EntityKind::Text(cadcraft_doc::Text {
insert: v(0.0, 0.0),
align_pt: None,
height: 1.0,
value: "x".into(),
rotation: 0.0,
width_factor: 1.0,
oblique: 0.0,
style: "Standard".into(),
halign: Default::default(),
valign: Default::default(),
}),
);
let bad = |d: &mut Drawing, c: Constraint| assert!(add(d, c, &SolveOptions::default()).is_err());
bad(&mut d, con(ConstraintKind::Horizontal, &[w(c)]));
bad(&mut d, con(ConstraintKind::Parallel, &[w(a)]));
bad(&mut d, con(ConstraintKind::Horizontal, &[w(t)]));
bad(&mut d, con(ConstraintKind::Horizontal, &[w(Handle(0xFFFF))]));
bad(&mut d, con(ConstraintKind::Coincident, &[s(a, Sub::Vertex(99)), s(a, Sub::Start)]));
bad(&mut d, con(ConstraintKind::Radius, &[w(a)]));
bad(&mut d, dim(ConstraintKind::Radius, &[w(c)], "", "-3"));
bad(&mut d, dim(ConstraintKind::Radius, &[w(c)], "1bad", "3"));
bad(&mut d, dim(ConstraintKind::Radius, &[w(c)], "", "nope*2"));
bad(&mut d, con(ConstraintKind::Symmetric, &[w(a), w(c)]));
bad(&mut d, con(ConstraintKind::Fix, &[]));
assert!(d.constraints.is_empty());
ok(&mut d, con(ConstraintKind::Horizontal, &[w(a)]));
d.remove_entity(a);
assert_eq!(purge_invalid(&mut d), vec![1]);
assert!(d.constraints.is_empty());
}
#[test]
fn hostile_geometry_never_panics() {
let mut d = Drawing::default();
let zero = line(&mut d, (1.0, 1.0), (1.0, 1.0));
let huge = line(&mut d, (1e300, -1e300), (-1e300, 1e300));
let nan = line(&mut d, (f64::NAN, 0.0), (1.0, f64::INFINITY));
let c0 = circle(&mut d, (0.0, 0.0), 0.0);
let kinds = [
ConstraintKind::Coincident,
ConstraintKind::Collinear,
ConstraintKind::Concentric,
ConstraintKind::Fix,
ConstraintKind::Parallel,
ConstraintKind::Perpendicular,
ConstraintKind::Horizontal,
ConstraintKind::Vertical,
ConstraintKind::Tangent,
ConstraintKind::Smooth,
ConstraintKind::Symmetric,
ConstraintKind::Equal,
ConstraintKind::Distance(DistAxis::Aligned),
ConstraintKind::Distance(DistAxis::Horizontal),
ConstraintKind::Angular,
ConstraintKind::Radius,
ConstraintKind::Diameter,
];
let hs = [zero, huge, nan, c0, Handle(0), Handle(u64::MAX)];
let subs = [Sub::Whole, Sub::Start, Sub::Mid, Sub::Center, Sub::Vertex(u32::MAX), Sub::Segment(u32::MAX)];
for k in kinds {
for (i, h) in hs.iter().enumerate() {
for sub in subs {
let refs = [s(*h, sub), w(hs[(i + 1) % hs.len()]), w(hs[(i + 2) % hs.len()])];
for n in 0..=3 {
let _ = add(&mut d, dim(k, &refs[..n], "", "1e308"), &SolveOptions::default());
let _ = add(&mut d, dim(k, &refs[..n], "", ""), &SolveOptions::default());
}
}
}
}
let _ = solve(&mut d, &SolveOptions::default());
let _ = glyphs(&d);
let _ = residuals(&d);
let _ = parameter_table(&d);
let _ = purge_invalid(&mut d);
}
#[test]
fn glyphs_and_residuals() {
let mut d = Drawing::default();
let a = line(&mut d, (0.0, 0.0), (10.0, 0.0));
ok(&mut d, con(ConstraintKind::Horizontal, &[w(a)]));
let g = glyphs(&d);
assert_eq!(g.len(), 1);
assert_eq!(g[0].kind, "Horizontal");
assert!(g[0].satisfied);
assert!(g[0].anchors[0].near(Vec2::new(5.0, 0.0), 1e-12));
d.modify_entity(a, |e| {
if let EntityKind::Line(l) = &mut e.kind {
l.b.y = 3.0;
}
})
.unwrap();
assert!(!all_satisfied(&d));
let r = solve(&mut d, &SolveOptions { keep: vec![], prefer_move: vec![] }).unwrap();
assert_eq!(r.moved, vec![a]);
assert!(all_satisfied(&d));
}
#[test]
fn autoconstrain_rectangle_of_lines() {
let mut d = Drawing::default();
let hs = vec![
line(&mut d, (0.0, 0.0), (10.0, 0.01)),
line(&mut d, (10.01, 0.0), (10.0, 5.0)),
line(&mut d, (10.0, 5.0), (0.0, 5.02)),
line(&mut d, (0.0, 5.0), (0.0, 0.0)),
];
let ids = autoconstrain(&mut d, &hs, &InferOptions::default());
let kinds: Vec<&str> = d.constraints.iter().map(|c| c.kind.name()).collect();
assert_eq!(kinds.iter().filter(|k| **k == "Coincident").count(), 4, "{kinds:?}");
assert_eq!(kinds.iter().filter(|k| **k == "Horizontal").count(), 2, "{kinds:?}");
assert_eq!(kinds.iter().filter(|k| **k == "Vertical").count(), 2, "{kinds:?}");
// Perpendicular/parallel/equal are implied and were skipped as redundant.
assert_eq!(ids.len(), 8, "{kinds:?}");
assert!(all_satisfied(&d));
let (a, b) = ln(&d, hs[0]);
assert!(near(a.y, b.y));
// Circles: concentric and equal.
let c1 = circle(&mut d, (30.0, 0.0), 2.0);
let c2 = circle(&mut d, (30.01, 0.0), 2.01);
autoconstrain(&mut d, &[c1, c2], &InferOptions::default());
assert!(circ(&d, c1).0.near(circ(&d, c2).0, 1e-7) && near(circ(&d, c1).1, circ(&d, c2).1));
// Type filter.
let l = line(&mut d, (50.0, 0.0), (60.0, 0.001));
let only_v = InferOptions { types: vec!["Vertical".into()], ..Default::default() };
assert!(autoconstrain(&mut d, &[l], &only_v).is_empty());
}
#[test]
fn set_parameter_rejects_out_of_domain_dimension() {
let opts = SolveOptions::default();
let mut d = Drawing::default();
let l = line(&mut d, (0.0, 0.0), (10.0, 0.0));
ok(&mut d, dim(ConstraintKind::Distance(DistAxis::Aligned), &[w(l)], "d1", "8"));
let c = circle(&mut d, (0.0, 20.0), 5.0);
ok(&mut d, dim(ConstraintKind::Radius, &[w(c)], "rad1", "3"));
let before = d.clone();
assert!(set_parameter(&mut d, "d1", "-5", &opts).is_err());
assert!(set_parameter(&mut d, "rad1", "0", &opts).is_err());
assert_eq!(d, before);
set_parameter(&mut d, "d1", "6", &opts).unwrap();
set_parameter(&mut d, "rad1", "2", &opts).unwrap();
assert!(all_satisfied(&d));
}
/// The solver may meet a coincidence with a negative arc radius; the stored arc must still put
/// the referenced point where the solver did (issue #703), through `add` and through `solve`.
#[test]
fn arc_solved_with_a_negative_radius_keeps_its_points() {
use std::f64::consts::FRAC_PI_2;
// (arc centre, radius, start, end, point): a semicircle with the point behind it, and the
// asymmetric case from the issue.
let cases = [((0.0, 0.0), 1.0, -FRAC_PI_2, FRAC_PI_2, (-5.0, 0.0)), ((6.694, -0.686), 0.62075, 3.4824, 1.1766, (-7.562, 4.946))];
for (c, r, a0, a1, p) in cases {
for through_solve in [false, true] {
let mut d = Drawing::default();
let a = arc(&mut d, c, r, a0, a1);
let q = add_e(&mut d, EntityKind::Point(DPoint { p: v(p.0, p.1), angle: 0.0 }));
let k = con(ConstraintKind::Coincident, &[s(a, Sub::Mid), w(q)]);
if through_solve {
d.constraints.push(Constraint { id: 1, ..k });
solve(&mut d, &SolveOptions::default()).unwrap();
} else {
ok(&mut d, k);
}
assert!(all_satisfied(&d), "{c:?} solve {through_solve}: {:?}", residuals(&d));
assert!(circ(&d, a).1 > 0.0);
}
}
}