Files
cadcraft/crates/constraints/tests/dof.rs
T

385 lines
16 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.
//! Degrees of freedom, redundancy hints, ellipse/spline/block geometry and Smooth (G2).
use std::sync::Arc;
use cadcraft_constraints::{DofStatus, SolveError, SolveOptions, add, all_satisfied, dof, solve};
use cadcraft_doc::{
Arc as DArc, Block, Circle as DCircle, Common, Constraint, ConstraintKind as K, DistAxis, Drawing, Ellipse as DEllipse, Entity, EntityKind,
GeomRef, Handle, Insert, Line as DLine, Point as DPoint, Space, Sub,
};
use cadcraft_geom::{Spline as GSpline, Vec2, Vec3};
fn v(x: f64, y: f64) -> Vec3 {
Vec3::new(x, y, 0.0)
}
fn put(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 {
put(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 {
put(d, EntityKind::Circle(DCircle { center: v(c.0, c.1), radius: r }))
}
fn point(d: &mut Drawing, p: (f64, f64)) -> Handle {
put(d, EntityKind::Point(DPoint { p: v(p.0, p.1), angle: 0.0 }))
}
fn ellipse(d: &mut Drawing, c: (f64, f64), major: (f64, f64), ratio: f64, start: f64, end: f64) -> Handle {
put(d, EntityKind::Ellipse(DEllipse { center: v(c.0, c.1), major: v(major.0, major.1), ratio, start, end }))
}
fn spline(d: &mut Drawing, pts: &[(f64, f64)]) -> Handle {
let s = GSpline::from_control(pts.iter().map(|p| Vec2::new(p.0, p.1)).collect(), 3);
put(d, EntityKind::Spline(s))
}
fn con(kind: K, refs: &[GeomRef]) -> Constraint {
Constraint { id: 0, kind, refs: refs.to_vec(), name: String::new(), expr: String::new() }
}
fn dim(kind: K, refs: &[GeomRef], expr: &str) -> Constraint {
Constraint { id: 0, kind, refs: refs.to_vec(), name: String::new(), expr: expr.into() }
}
fn w(h: Handle) -> GeomRef {
GeomRef::whole(h)
}
fn s(h: Handle, sub: Sub) -> GeomRef {
GeomRef::new(h, sub)
}
fn ok(d: &mut Drawing, c: Constraint) -> u32 {
let k = c.kind;
add(d, c, &SolveOptions::default()).unwrap_or_else(|e| panic!("{k:?}: {e}"))
}
fn ends(d: &Drawing, h: Handle) -> (Vec2, Vec2) {
match &d.entity(h).unwrap().kind {
EntityKind::Line(l) => (l.a.xy(), l.b.xy()),
_ => panic!("not a line"),
}
}
fn ell(d: &Drawing, h: Handle) -> cadcraft_geom::Ellipse {
match &d.entity(h).unwrap().kind {
EntityKind::Ellipse(e) => cadcraft_geom::Ellipse { center: e.center.xy(), major: e.major.xy(), ratio: e.ratio, start: e.start, end: e.end },
_ => panic!("not an ellipse"),
}
}
fn spl(d: &Drawing, h: Handle) -> GSpline {
match &d.entity(h).unwrap().kind {
EntityKind::Spline(s) => s.clone(),
_ => panic!("not a spline"),
}
}
fn obj_dof(d: &Drawing, h: Handle) -> Option<usize> {
let r = dof(d, &[h]).unwrap();
r.objects.iter().find(|o| o.handle == h.hex()).and_then(|o| o.dof)
}
/// Signed curvature by sampling a curve near a parameter (central differences).
fn curvature(f: impl Fn(f64) -> Vec2, t: f64, h: f64) -> f64 {
let (a, b, c) = (f(t - h), f(t), f(t + h));
let d1 = (c - a) * (0.5 / h);
let d2 = (c - b * 2.0 + a) * (1.0 / (h * h));
d1.cross(d2) / d1.len().powi(3)
}
#[test]
fn rectangle_dof_counts_down_to_fully_constrained() {
let mut d = Drawing::default();
let l = [
line(&mut d, (0.0, 0.0), (10.0, 0.2)),
line(&mut d, (10.0, 0.2), (10.1, 5.0)),
line(&mut d, (10.1, 5.0), (0.0, 5.0)),
line(&mut d, (0.0, 5.0), (0.0, 0.0)),
];
let r = dof(&d, &[]).unwrap();
assert_eq!((r.status, r.dof, r.message.as_str()), (DofStatus::Unconstrained, Some(0), "No constraints"));
// An unconstrained line has its four parameters.
assert_eq!(obj_dof(&d, l[0]), Some(4));
for i in 0..4 {
ok(&mut d, con(K::Coincident, &[s(l[i], Sub::End), s(l[(i + 1) % 4], Sub::Start)]));
}
ok(&mut d, con(K::Horizontal, &[w(l[0])]));
ok(&mut d, con(K::Horizontal, &[w(l[2])]));
ok(&mut d, con(K::Vertical, &[w(l[1])]));
ok(&mut d, con(K::Vertical, &[w(l[3])]));
// 16 parameters − 12 equations: translation, width, height.
let r = dof(&d, &[]).unwrap();
assert_eq!((r.dof, r.status, r.redundant), (Some(4), DofStatus::Under, 0));
assert_eq!(r.objects.len(), 4);
assert!(r.objects.iter().all(|o| o.params == 4 && o.dof == Some(3) && o.status == DofStatus::Under), "{r:?}");
ok(&mut d, con(K::Fix, &[s(l[0], Sub::Start)]));
assert_eq!(dof(&d, &[]).unwrap().dof, Some(2));
ok(&mut d, dim(K::Distance(DistAxis::Aligned), &[w(l[0])], "12"));
// The bottom edge is now fixed; the others still stretch with the height.
assert_eq!(obj_dof(&d, l[0]), Some(0));
assert_eq!(obj_dof(&d, l[2]), Some(1));
ok(&mut d, dim(K::Distance(DistAxis::Aligned), &[w(l[1])], "4"));
let r = dof(&d, &[]).unwrap();
assert_eq!((r.dof, r.status, r.message.as_str()), (Some(0), DofStatus::Fully, "Fully constrained"));
assert!(r.objects.iter().all(|o| o.status == DofStatus::Fully));
// A fully constrained rectangle refuses one more dimension, naming the constraints it repeats.
let e = add(&mut d, dim(K::Distance(DistAxis::Aligned), &[w(l[3])], "4"), &SolveOptions::default()).unwrap_err();
let SolveError::Redundant { ids } = e else { panic!("{e:?}") };
assert!(!ids.is_empty());
}
#[test]
fn redundant_hint_names_the_duplicated_constraints() {
let mut d = Drawing::default();
let a = line(&mut d, (0.0, 0.0), (10.0, 0.1));
let b = line(&mut d, (0.0, 5.0), (10.0, 5.2));
let c = line(&mut d, (20.0, 0.0), (20.0, 9.0));
let ha = ok(&mut d, con(K::Horizontal, &[w(a)]));
let hb = ok(&mut d, con(K::Horizontal, &[w(b)]));
let vc = ok(&mut d, con(K::Vertical, &[w(c)]));
ok(&mut d, con(K::Coincident, &[s(b, Sub::End), s(c, Sub::Start)]));
let before = d.clone();
let e = add(&mut d, con(K::Parallel, &[w(a), w(b)]), &SolveOptions::default()).unwrap_err();
let SolveError::Redundant { ids } = &e else { panic!("{e:?}") };
assert!(ids.contains(&ha) && ids.contains(&hb), "{ids:?}");
assert!(!ids.contains(&vc), "{ids:?}");
assert!(e.to_string().contains(&format!("#{ha}")), "{e}");
assert_eq!(d, before);
}
#[test]
fn over_constrained_drawing_is_reported() {
let mut d = Drawing::default();
let a = line(&mut d, (0.0, 0.0), (10.0, 0.0));
ok(&mut d, con(K::Horizontal, &[w(a)]));
// A file can hold a duplicate the command would have refused.
d.constraints.push(Constraint { id: 99, ..con(K::Horizontal, &[w(a)]) });
let r = dof(&d, &[]).unwrap();
assert_eq!((r.status, r.redundant), (DofStatus::Over, 1));
assert!(r.message.starts_with("Over-constrained"), "{}", r.message);
assert_eq!(r.objects[0].status, DofStatus::Over);
}
#[test]
fn ellipse_constraints() {
let mut d = Drawing::default();
let e = ellipse(&mut d, (0.0, 0.0), (6.0, 0.5), 0.5, 0.0, std::f64::consts::TAU);
assert_eq!(obj_dof(&d, e), Some(5));
// Horizontal applies to the major axis.
ok(&mut d, con(K::Horizontal, &[w(e)]));
assert!(ell(&d, e).major.y.abs() < 1e-9);
// A point on the ellipse.
let p = point(&mut d, (0.3, 3.4));
ok(&mut d, con(K::Coincident, &[w(p), w(e)]));
let g = ell(&d, e);
let pp = match &d.entity(p).unwrap().kind {
EntityKind::Point(q) => q.p.xy(),
_ => unreachable!(),
};
let (a, b) = (g.major.len(), g.major.len() * g.ratio);
let q = pp - g.center;
assert!(((q.x / a).powi(2) + (q.y / b).powi(2) - 1.0).abs() < 1e-7);
// Concentric with a circle.
let c = circle(&mut d, (0.5, -0.4), 1.0);
ok(&mut d, con(K::Concentric, &[w(e), w(c)]));
let cc = match &d.entity(c).unwrap().kind {
EntityKind::Circle(k) => k.center.xy(),
_ => unreachable!(),
};
assert!(cc.near(ell(&d, e).center, 1e-7));
// A line tangent to the ellipse.
let l = line(&mut d, (-10.0, 3.6), (10.0, 3.2));
ok(&mut d, con(K::Tangent, &[w(l), w(e)]));
let (la, lb) = ends(&d, l);
let g = ell(&d, e);
let n = (lb - la).normalized().perp();
let support = (n.dot(g.major).powi(2) + n.dot(g.minor()).powi(2)).sqrt();
assert!(((g.center - la).dot(n).abs() - support).abs() < 1e-7);
// An axis end point coincides with a line end.
let m = line(&mut d, (6.2, 0.3), (9.0, 5.0));
ok(&mut d, con(K::Coincident, &[s(e, Sub::Vertex(0)), s(m, Sub::Start)]));
let g = ell(&d, e);
assert!(ends(&d, m).0.near(g.center + g.major, 1e-7));
assert!(all_satisfied(&d));
}
#[test]
fn circle_tangent_to_ellipse_and_elliptical_arc_ends() {
let mut d = Drawing::default();
let e = ellipse(&mut d, (0.0, 0.0), (5.0, 0.0), 0.6, 0.0, std::f64::consts::TAU);
ok(&mut d, con(K::Fix, &[w(e)]));
let c = circle(&mut d, (7.5, 0.4), 2.0);
ok(&mut d, con(K::Tangent, &[w(c), w(e)]));
let (cc, r) = match &d.entity(c).unwrap().kind {
EntityKind::Circle(k) => (k.center.xy(), k.radius),
_ => unreachable!(),
};
// Closest distance from the circle's centre to the ellipse equals the radius.
let g = ell(&d, e);
let near = (0..20000).map(|i| g.at_param(i as f64 * std::f64::consts::TAU / 20000.0).dist(cc)).fold(f64::INFINITY, f64::min);
assert!((near - r).abs() < 1e-3, "{near} vs {r}");
assert!(all_satisfied(&d));
// Elliptical arc ends are points.
let arc = ellipse(&mut d, (20.0, 0.0), (4.0, 0.0), 0.5, 0.0, std::f64::consts::PI);
assert_eq!(obj_dof(&d, arc), Some(7));
let l = line(&mut d, (24.3, 0.2), (30.0, 0.0));
ok(&mut d, con(K::Coincident, &[s(arc, Sub::Start), s(l, Sub::Start)]));
let g = ell(&d, arc);
assert!(ends(&d, l).0.near(g.at_param(g.start), 1e-7));
}
#[test]
fn spline_points_tangent_and_smooth_g2() {
let mut d = Drawing::default();
// Arc to the left of the spline's start.
let a = put(&mut d, EntityKind::Arc(DArc { center: v(0.0, 3.0), radius: 3.0, start: std::f64::consts::PI, end: 1.5 * std::f64::consts::PI }));
ok(&mut d, con(K::Fix, &[w(a)]));
let sp = spline(&mut d, &[(0.1, 0.1), (3.0, 0.4), (6.0, 2.0), (9.0, 2.0)]);
assert_eq!(obj_dof(&d, sp), Some(8));
ok(&mut d, con(K::Smooth, &[w(sp), w(a)]));
let g = spl(&d, sp);
let (lo, _) = g.domain();
// Joined at the arc's end (0, 0), tangent along +x, curvature 1/3 like the arc.
assert!(g.eval(lo).near(Vec2::new(0.0, 0.0), 1e-7), "{:?}", g.eval(lo));
let t = (g.eval(lo + 1e-6) - g.eval(lo)).normalized();
assert!(t.near(Vec2::new(1.0, 0.0), 1e-4), "{t:?}");
let k = curvature(|u| g.eval(u), lo + 1e-3, 1e-4);
assert!((k - 1.0 / 3.0).abs() < 2e-2, "curvature {k}");
assert!(all_satisfied(&d));
// The spline's control points are constraint points.
let p = point(&mut d, (9.5, 2.5));
ok(&mut d, con(K::Coincident, &[s(sp, Sub::Vertex(3)), w(p)]));
let pp = match &d.entity(p).unwrap().kind {
EntityKind::Point(q) => q.p.xy(),
_ => unreachable!(),
};
assert!(spl(&d, sp).control[3].near(pp, 1e-7));
// Smooth to a line: the far end ends straight.
let l = line(&mut d, (9.2, 2.1), (15.0, 3.0));
ok(&mut d, con(K::Smooth, &[w(sp), w(l)]));
let g = spl(&d, sp);
let (_, hi) = g.domain();
assert!(g.eval(hi).near(ends(&d, l).0, 1e-7));
let k = curvature(|u| g.eval(u), hi - 1e-3, 1e-4);
assert!(k.abs() < 2e-2, "curvature at the line {k}");
assert!(all_satisfied(&d));
}
#[test]
fn spline_tangent_to_circle_and_fit_spline_smooth() {
let mut d = Drawing::default();
let c = circle(&mut d, (0.0, 0.0), 2.0);
ok(&mut d, con(K::Fix, &[w(c)]));
let sp = spline(&mut d, &[(2.1, 0.3), (2.5, 3.0), (5.0, 4.0), (8.0, 4.0)]);
ok(&mut d, con(K::Tangent, &[w(sp), w(c)]));
let g = spl(&d, sp);
let (lo, _) = g.domain();
let p = g.eval(lo);
assert!((p.len() - 2.0).abs() < 1e-7);
let t = (g.eval(lo + 1e-7) - p).normalized();
assert!(t.dot(p.normalized()).abs() < 1e-4);
// A fit-point spline keeps its fit points and is rebuilt through them.
let fit: Vec<Vec2> = [(20.0, 0.0), (23.0, 2.0), (27.0, 1.0), (30.0, 3.0)].iter().map(|p| Vec2::new(p.0, p.1)).collect();
let fs = put(&mut d, EntityKind::Spline(GSpline::from_fit_points(&fit)));
let l = line(&mut d, (10.0, -1.0), (19.8, 0.2));
ok(&mut d, con(K::Fix, &[w(l)]));
ok(&mut d, con(K::Smooth, &[w(fs), w(l)]));
let g = spl(&d, fs);
assert_eq!(g.fit.len(), 4);
assert!(g.fit[0].near(Vec2::new(19.8, 0.2), 1e-7));
let (lo, _) = g.domain();
assert!(g.eval(lo).near(g.fit[0], 1e-9));
assert!(all_satisfied(&d));
}
#[test]
fn block_reference_moves_rigidly() {
let mut d = Drawing::default();
let mut b = Block::new("BAR");
let h = d.new_handle();
b.entities.push(Entity { handle: h, common: Common::default(), kind: EntityKind::Line(DLine { a: v(0.0, 0.0), b: v(4.0, 0.0) }) });
d.blocks.insert("BAR".into(), Arc::new(b));
let ins = put(
&mut d,
EntityKind::Insert(Insert {
block: "BAR".into(),
insert: v(1.0, 1.0),
scale: v(1.0, 1.0),
rotation: 1.2,
attribs: Vec::new(),
cols: 1,
rows: 1,
col_spacing: 0.0,
row_spacing: 0.0,
}),
);
assert_eq!(obj_dof(&d, ins), Some(3));
let p0 = point(&mut d, (10.0, 10.0));
let wall = line(&mut d, (10.0, 0.0), (10.0, 20.0));
ok(&mut d, con(K::Fix, &[w(p0)]));
ok(&mut d, con(K::Fix, &[w(wall)]));
// The insertion point onto p0 leaves the turn; the block's far end (rigid point 1) onto the
// wall takes it.
ok(&mut d, con(K::Coincident, &[w(ins), w(p0)]));
assert_eq!(obj_dof(&d, ins), Some(1));
ok(&mut d, con(K::Coincident, &[s(ins, Sub::Vertex(1)), w(wall)]));
let EntityKind::Insert(i) = &d.entity(ins).unwrap().kind else { panic!() };
assert!(i.insert.xy().near(Vec2::new(10.0, 10.0), 1e-7));
assert!((i.rotation - std::f64::consts::FRAC_PI_2).abs() < 1e-7, "{}", i.rotation);
// Both points again would be one equation too many.
let p1 = point(&mut d, (10.0, 14.0));
ok(&mut d, con(K::Fix, &[w(p1)]));
assert!(matches!(add(&mut d, con(K::Coincident, &[s(ins, Sub::Vertex(1)), w(p1)]), &SolveOptions::default()), Err(SolveError::Redundant { .. })));
assert_eq!(dof(&d, &[]).unwrap().status, DofStatus::Fully);
}
#[test]
fn hostile_new_geometry_never_panics() {
let mut d = Drawing::default();
let bad_e = ellipse(&mut d, (0.0, 0.0), (0.0, 0.0), f64::NAN, 0.0, 1.0);
let flat_e = ellipse(&mut d, (0.0, 0.0), (1e300, 0.0), 0.0, 0.0, 1.0);
let broken = put(&mut d, EntityKind::Spline(GSpline { degree: 7, knots: vec![f64::NAN; 3], control: vec![Vec2::ZERO; 2], ..Default::default() }));
let one = put(&mut d, EntityKind::Spline(GSpline::from_control(vec![Vec2::new(1.0, 1.0); 3], 2)));
let missing = put(
&mut d,
EntityKind::Insert(Insert {
block: "NOPE".into(),
insert: v(0.0, 0.0),
scale: v(0.0, f64::INFINITY),
rotation: f64::NAN,
attribs: Vec::new(),
cols: 1,
rows: 1,
col_spacing: 0.0,
row_spacing: 0.0,
}),
);
let l = line(&mut d, (0.0, 0.0), (0.0, 0.0));
let hs = [bad_e, flat_e, broken, one, missing, l];
let kinds = [K::Coincident, K::Concentric, K::Tangent, K::Smooth, K::Horizontal, K::Fix, K::Equal, K::Symmetric, K::Radius];
let subs = [Sub::Whole, Sub::Start, Sub::End, Sub::Center, Sub::Vertex(0), Sub::Vertex(3), Sub::Vertex(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], "1"), &SolveOptions::default());
}
}
}
}
let _ = solve(&mut d, &SolveOptions::default());
let _ = dof(&d, &hs);
let _ = dof(&d, &[Handle(0), Handle(u64::MAX)]);
}
/// Fit-point vertex constraints retain a known freedom count because they do not reconstruct the spline.
#[test]
fn vertex_constraints_on_large_fit_spline_do_not_charge_reconstruction() {
let mut d = Drawing::default();
let pts: Vec<_> = (0..500).map(|i| Vec2::new(i as f64, (i % 3) as f64)).collect();
let mut sp = GSpline::from_control(pts.clone(), 3);
sp.fit = pts;
let h = put(&mut d, EntityKind::Spline(sp));
for i in 0..5 {
let p = point(&mut d, (i as f64, (i % 3) as f64));
let mut c = con(K::Coincident, &[s(h, Sub::Vertex(i)), w(p)]);
c.id = i + 1;
d.constraints.push(c);
}
let report = dof(&d, &[]).unwrap();
assert_eq!(report.status, DofStatus::Under);
assert_eq!(report.dof, Some(1000));
}