use std::f64::consts::{FRAC_PI_2, PI}; use solidcraft_geom::Vec2; use crate::*; pub(crate) fn v(x: f64, y: f64) -> Vec2 { Vec2::new(x, y) } /// Deterministic pseudo-random numbers for the solver harnesses. pub(crate) struct Lcg(pub u64); impl Lcg { pub(crate) fn next(&mut self) -> f64 { self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); (self.0 >> 11) as f64 / (1u64 << 53) as f64 } pub(crate) fn range(&mut self, lo: f64, hi: f64) -> f64 { lo + self.next() * (hi - lo) } /// ±1, evenly. pub(crate) fn sign(&mut self) -> f64 { if self.next() < 0.5 { 1.0 } else { -1.0 } } } /// Run `f` with the constructive seed on or off, without the seed-on/seed-off comparison (for /// tests that measure the difference). pub(crate) fn seeded(on: bool, f: impl FnOnce() -> T) -> T { use crate::construct::switch; switch::without_differential(|| if on { f() } else { switch::without_seed(f) }) } /// The points that are constants for a plain solve. pub(crate) fn given(sk: &Sketch) -> Vec { crate::solver::held_points(sk, &Hold::default()) } /// The constructive seed of a plain solve. pub(crate) fn seed_of(sk: &Sketch) -> crate::construct::Seed { crate::construct::seed(sk, &given(sk), None, &crate::solver::points_on_curves(sk), &mut crate::decompose::Budget::full()) } /// Rough rectangle from four lines sharing corner points. fn rect(sk: &mut Sketch, p: [Vec2; 4]) -> [usize; 4] { let a = sk.add_point(p[0], None).unwrap(); let b = sk.add_point(p[1], None).unwrap(); let c = sk.add_point(p[2], None).unwrap(); let d = sk.add_point(p[3], None).unwrap(); [ sk.add_line_pts(a, b, None).unwrap(), sk.add_line_pts(b, c, None).unwrap(), sk.add_line_pts(c, d, None).unwrap(), sk.add_line_pts(d, a, None).unwrap(), ] } fn line_pts(sk: &Sketch, l: usize) -> (Vec2, Vec2) { match sk.curves[l].kind { CurveKind::Line { a, b } => (sk.points[a].pos, sk.points[b].pos), _ => panic!(), } } #[test] fn fully_constrained_rectangle() { let mut sk = Sketch::new(); let l = rect(&mut sk, [v(0.3, -0.2), v(38.0, 1.0), v(41.0, 29.0), v(-1.0, 31.0)]); use ConstraintKind::*; sk.add_constraint(Horizontal { l: l[0] }, None).unwrap(); sk.add_constraint(Horizontal { l: l[2] }, None).unwrap(); sk.add_constraint(Vertical { l: l[1] }, None).unwrap(); sk.add_constraint(Vertical { l: l[3] }, None).unwrap(); let (a, _) = match sk.curves[l[0]].kind { CurveKind::Line { a, b } => (a, b), _ => panic!(), }; sk.add_constraint(Coincident { p: a, q: 0 }, None).unwrap(); sk.add_constraint(Length { l: l[0], value: 40.0 }, Some("d1".into())).unwrap(); sk.add_constraint(Length { l: l[1], value: 30.0 }, Some("d2".into())).unwrap(); let rep = solve(&mut sk); assert!(rep.ok(), "{rep:?}"); assert_eq!(rep.dof, 0, "{rep:?}"); assert!(rep.point_determined.iter().all(|d| *d)); let (p0, p1) = line_pts(&sk, l[0]); assert!(p0.dist(v(0.0, 0.0)) < 1e-7, "{p0:?}"); assert!(p1.dist(v(40.0, 0.0)) < 1e-7, "{p1:?}"); let (_, p2) = line_pts(&sk, l[1]); assert!(p2.dist(v(40.0, 30.0)) < 1e-7, "{p2:?}"); // Change a dimension and re-solve. sk.constraints[5].kind.set_value(55.0); assert!(solve(&mut sk).ok()); let (_, p1) = line_pts(&sk, l[0]); assert!(p1.dist(v(55.0, 0.0)) < 1e-7); } #[test] fn under_constrained_reports_dof() { let mut sk = Sketch::new(); let l = sk.add_line(v(0.0, 0.0), v(10.0, 1.0), None, None, None).unwrap(); sk.add_constraint(ConstraintKind::Horizontal { l }, None).unwrap(); let rep = solve(&mut sk); assert!(rep.ok()); assert_eq!(rep.dof, 3); let (a, b) = line_pts(&sk, l); assert!((a.y - b.y).abs() < 1e-9); // Minimal movement: the line moved by about half a unit each. assert!((a.y - 0.5).abs() < 0.05 && (b.y - 0.5).abs() < 0.05, "{a:?} {b:?}"); } #[test] fn conflicting_constraints_fail_and_restore() { let mut sk = Sketch::new(); let l = sk.add_line(v(0.0, 0.0), v(10.0, 0.0), None, None, None).unwrap(); sk.add_constraint(ConstraintKind::Horizontal { l }, None).unwrap(); sk.add_constraint(ConstraintKind::Vertical { l }, None).unwrap(); sk.add_constraint(ConstraintKind::Length { l, value: 10.0 }, None).unwrap(); let before = sk.clone(); let rep = solve(&mut sk); assert_eq!(rep.status, SolveStatus::Failed); assert!(!rep.failing.is_empty()); assert_eq!(sk.points, before.points); } #[test] fn circles_tangent_equal_concentric() { let mut sk = Sketch::new(); let c1 = sk.add_circle(v(0.0, 0.0), 5.0, Some(0), None).unwrap(); let c2 = sk.add_circle(v(12.0, 1.0), 3.0, None, None).unwrap(); use ConstraintKind::*; sk.add_constraint(Radius { c: c1, value: 10.0 }, None).unwrap(); sk.add_constraint(Equal { a: c1, b: c2 }, None).unwrap(); sk.add_constraint(Tangent { a: c1, b: c2 }, None).unwrap(); let rep = solve(&mut sk); assert!(rep.ok(), "{rep:?}"); assert!((sk.radius(c2).unwrap() - 10.0).abs() < 1e-7); assert!((sk.center(c2).unwrap().len() - 20.0).abs() < 1e-7); let c3 = sk.add_circle(v(3.0, 3.0), 2.0, None, None).unwrap(); sk.add_constraint(Concentric { a: c3, b: c2 }, None).unwrap(); assert!(solve(&mut sk).ok()); assert!(sk.center(c3).unwrap().dist(sk.center(c2).unwrap()) < 1e-7); } #[test] fn line_tangent_to_circle_and_angles() { let mut sk = Sketch::new(); let c = sk.add_circle(v(0.0, 0.0), 10.0, Some(0), None).unwrap(); use ConstraintKind::*; sk.add_constraint(Radius { c, value: 10.0 }, None).unwrap(); let l = sk.add_line(v(-20.0, 12.0), v(20.0, 11.0), None, None, None).unwrap(); sk.add_constraint(Tangent { a: l, b: c }, None).unwrap(); sk.add_constraint(Horizontal { l }, None).unwrap(); assert!(solve(&mut sk).ok()); let (a, _) = line_pts(&sk, l); assert!((a.y.abs() - 10.0).abs() < 1e-7, "{a:?}"); let m = sk.add_line(v(0.0, 0.0), v(10.0, 3.0), Some(0), None, None).unwrap(); sk.add_constraint(Angle { a: l, b: m, value: PI / 6.0, flip: false }, None).unwrap(); sk.add_constraint(Length { l: m, value: 8.0 }, None).unwrap(); assert!(solve(&mut sk).ok()); let (p, q) = line_pts(&sk, m); let (la, lb) = line_pts(&sk, l); assert!(((q - p).angle() - (lb - la).angle() - PI / 6.0).abs() < 1e-7, "{la:?} {lb:?} {p:?} {q:?}"); let n2 = sk.add_line(v(1.0, 1.0), v(2.0, 5.0), None, None, None).unwrap(); sk.add_constraint(Perpendicular { a: m, b: n2 }, None).unwrap(); sk.add_constraint(Parallel { a: n2, b: n2 }, None).unwrap(); assert!(solve(&mut sk).ok()); let (p2, q2) = line_pts(&sk, n2); assert!(((q2 - p2).angle() - (PI / 6.0 + FRAC_PI_2)).abs() < 1e-6 || ((q2 - p2).angle() - (PI / 6.0 - FRAC_PI_2)).abs() < 1e-6); } #[test] fn midpoint_symmetry_point_on_curve_arc() { let mut sk = Sketch::new(); use ConstraintKind::*; let l = sk.add_line(v(-5.0, 0.0), v(5.0, 0.0), None, None, None).unwrap(); let p = sk.add_point(v(1.0, 1.0), None).unwrap(); sk.add_constraint(Midpoint { p, l }, None).unwrap(); let a = sk.add_arc(v(0.0, 0.0), v(4.0, 0.0), v(0.0, 4.0), [None, None, None], None).unwrap(); sk.add_constraint(Radius { c: a, value: 6.0 }, None).unwrap(); let q = sk.add_point(v(3.0, 3.0), None).unwrap(); sk.add_constraint(PointOnCurve { p: q, c: a }, None).unwrap(); let s1 = sk.add_point(v(-2.0, 1.0), None).unwrap(); let s2 = sk.add_point(v(3.0, -1.5), None).unwrap(); let axis = sk.add_line(v(0.0, -10.0), v(0.0, 10.0), None, None, None).unwrap(); sk.add_constraint(Vertical { l: axis }, None).unwrap(); sk.add_constraint(Symmetric { p: s1, q: s2, l: axis }, None).unwrap(); let rep = solve(&mut sk); assert!(rep.ok(), "{rep:?}"); let (la, lb) = line_pts(&sk, l); assert!(sk.points[p].pos.dist((la + lb) * 0.5) < 1e-7); assert!((sk.radius(a).unwrap() - 6.0).abs() < 1e-7); let c = sk.center(a).unwrap(); assert!((sk.points[q].pos.dist(c) - 6.0).abs() < 1e-7); let (ax, _) = line_pts(&sk, axis); assert!((sk.points[s1].pos.x + sk.points[s2].pos.x - 2.0 * ax.x).abs() < 1e-7); assert!((sk.points[s1].pos.y - sk.points[s2].pos.y).abs() < 1e-7); } #[test] fn profiles_rectangle_with_hole_and_split() { let mut sk = Sketch::new(); rect(&mut sk, [v(0.0, 0.0), v(40.0, 0.0), v(40.0, 30.0), v(0.0, 30.0)]); sk.add_circle(v(20.0, 15.0), 5.0, None, None).unwrap(); let ps = find_profiles(&sk); assert_eq!(ps.len(), 2, "{ps:?}"); let big = ps.iter().find(|p| p.region.holes.len() == 1).unwrap(); assert!((big.area - (1200.0 - PI * 25.0)).abs() < 1e-6); let disc = ps.iter().find(|p| p.region.holes.is_empty()).unwrap(); assert!((disc.area - PI * 25.0).abs() < 1e-6); assert_eq!(big.hole_curves.len(), 1); // A line across the rectangle splits it into two regions (with the hole in one). let mut sk2 = Sketch::new(); let l = rect(&mut sk2, [v(0.0, 0.0), v(40.0, 0.0), v(40.0, 30.0), v(0.0, 30.0)]); let (b0, _) = line_pts(&sk2, l[0]); let _ = b0; let pa = sk2.add_point(v(10.0, 0.0), None).unwrap(); let pb = sk2.add_point(v(10.0, 30.0), None).unwrap(); // Re-split bottom and top edges at x = 10 by rebuilding the sketch as 7 lines. let mut s3 = Sketch::new(); let pts = [v(0.0, 0.0), v(10.0, 0.0), v(40.0, 0.0), v(40.0, 30.0), v(10.0, 30.0), v(0.0, 30.0)]; let ids: Vec = pts.iter().map(|p| s3.add_point(*p, None).unwrap()).collect(); for i in 0..6 { s3.add_line_pts(ids[i], ids[(i + 1) % 6], None).unwrap(); } s3.add_line_pts(ids[1], ids[4], None).unwrap(); let _ = (pa, pb); let ps = find_profiles(&s3); assert_eq!(ps.len(), 2); let mut areas: Vec = ps.iter().map(|p| p.area).collect(); areas.sort_by(f64::total_cmp); assert!((areas[0] - 300.0).abs() < 1e-9 && (areas[1] - 900.0).abs() < 1e-9, "{areas:?}"); } #[test] fn profile_with_arcs_and_construction_ignored() { // Slot: two lines and two half arcs. let mut sk = Sketch::new(); let p = [v(0.0, 0.0), v(20.0, 0.0), v(20.0, 10.0), v(0.0, 10.0)]; let ids: Vec = p.iter().map(|q| sk.add_point(*q, None).unwrap()).collect(); sk.add_line_pts(ids[0], ids[1], None).unwrap(); sk.add_arc(v(20.0, 5.0), p[1], p[2], [None, Some(ids[1]), Some(ids[2])], None).unwrap(); sk.add_line_pts(ids[2], ids[3], None).unwrap(); sk.add_arc(v(0.0, 5.0), p[3], p[0], [None, Some(ids[3]), Some(ids[0])], None).unwrap(); let c = sk.add_line(v(-5.0, -5.0), v(30.0, 20.0), None, None, None).unwrap(); sk.curves[c].construction = true; let ps = find_profiles(&sk); assert_eq!(ps.len(), 1, "{ps:?}"); assert!((ps[0].area - (200.0 + PI * 25.0)).abs() < 1e-6, "{}", ps[0].area); assert_eq!(ps[0].outer_curves.len(), 4); } #[test] fn removal_and_references() { let mut sk = Sketch::new(); let l1 = sk.add_line(v(0.0, 0.0), v(1.0, 0.0), None, None, Some("l1")).unwrap(); let e = sk.resolve_point("l1.end").unwrap(); let l2 = sk.add_line(v(1.0, 0.0), v(1.0, 1.0), Some(e), None, Some("l2")).unwrap(); sk.add_constraint(ConstraintKind::Horizontal { l: l1 }, None).unwrap(); sk.add_constraint(ConstraintKind::Vertical { l: l2 }, None).unwrap(); assert!(sk.add_line(v(0.0, 0.0), v(1.0, 0.0), None, None, Some("l1")).is_err()); sk.remove_curves(&[l1]); assert_eq!(sk.curves.len(), 1); assert_eq!(sk.constraints.len(), 1); assert!(sk.resolve_point("l2.start").is_some()); assert!(sk.resolve_point("l1.start").is_none()); assert_eq!(sk.points.len(), 3, "origin + l2's two points"); assert!(solve(&mut sk).ok()); let json = serde_json::to_string(&sk).unwrap(); let back: Sketch = serde_json::from_str(&json).unwrap(); assert_eq!(back, sk); } #[test] fn hostile_values_rejected() { let mut sk = Sketch::new(); assert!(sk.add_point(v(f64::NAN, 0.0), None).is_err()); assert!(sk.add_circle(v(0.0, 0.0), -1.0, None, None).is_err()); assert!(sk.add_circle(v(0.0, 0.0), f64::INFINITY, None, None).is_err()); assert!(sk.add_constraint(ConstraintKind::Horizontal { l: 99 }, None).is_err()); let c = sk.add_circle(v(0.0, 0.0), 1.0, None, None).unwrap(); assert!(sk.add_constraint(ConstraintKind::Horizontal { l: c }, None).is_err()); assert!(sk.add_constraint(ConstraintKind::Radius { c, value: f64::NAN }, None).is_err()); assert!(sk.add_constraint(ConstraintKind::Radius { c, value: -2.0 }, None).is_err()); } #[test] fn profiles_split_at_crossings_and_touch_points() { // A line across a circle: two half discs. let mut sk = Sketch::new(); sk.add_circle(v(0.0, 0.0), 10.0, None, None).unwrap(); sk.add_line(v(-15.0, 0.0), v(15.0, 0.0), None, None, None).unwrap(); let ps = find_profiles(&sk); assert_eq!(ps.len(), 2, "{ps:?}"); for p in &ps { assert!((p.area - PI * 50.0).abs() < 1e-6, "{}", p.area); } // Belt: two circles joined by two tangent lines whose ends sit on the circles. let mut b = Sketch::new(); let c1 = b.add_circle(v(0.0, 0.0), 20.0, Some(0), None).unwrap(); let c2 = b.add_circle(v(50.0, 0.0), 10.0, None, None).unwrap(); let l1 = b.add_line(v(3.0, 19.0), v(47.0, 13.0), None, None, None).unwrap(); let l2 = b.add_line(v(1.0, -22.0), v(44.0, -8.0), None, None, None).unwrap(); use ConstraintKind::*; for (l, end, c) in [(l1, "start", c1), (l1, "end", c2), (l2, "start", c1), (l2, "end", c2)] { let p = b.resolve_point(&format!("{}.{end}", b.curves[l].id)).unwrap(); b.add_constraint(PointOnCurve { p, c }, None).unwrap(); } for (l, c) in [(l1, c1), (l1, c2), (l2, c1), (l2, c2)] { b.add_constraint(Tangent { a: l, b: c }, None).unwrap(); } b.add_constraint(Radius { c: c1, value: 20.0 }, None).unwrap(); b.add_constraint(Radius { c: c2, value: 10.0 }, None).unwrap(); let r = solve(&mut b); assert!(r.ok(), "{r:?}"); let ps = find_profiles(&b); assert_eq!(ps.len(), 3, "two discs and the band between them: {:?}", ps.iter().map(|p| p.area).collect::>()); let total: f64 = ps.iter().map(|p| p.area).sum(); assert!(total > PI * 500.0, "{total}"); } #[test] fn merging_adjacent_regions() { let a = solidcraft_geom::Region2 { outer: solidcraft_geom::Loop2::polygon(&[v(0.0, 0.0), v(10.0, 0.0), v(10.0, 10.0), v(0.0, 10.0)]), holes: vec![] }; let b = solidcraft_geom::Region2 { outer: solidcraft_geom::Loop2::polygon(&[v(10.0, 0.0), v(20.0, 0.0), v(20.0, 10.0), v(10.0, 10.0)]), holes: vec![], }; let c = solidcraft_geom::Region2 { outer: solidcraft_geom::Loop2::polygon(&[v(50.0, 0.0), v(60.0, 0.0), v(60.0, 10.0)]), holes: vec![] }; let m = merge_regions(&[a, b, c]); assert_eq!(m.len(), 2); let mut areas: Vec = m.iter().map(|r| r.area()).collect(); areas.sort_by(f64::total_cmp); assert!((areas[0] - 50.0).abs() < 1e-9 && (areas[1] - 200.0).abs() < 1e-9, "{areas:?}"); // The two squares make one rectangle: the outline isn't left split where the shared edge was. let sides = m.iter().map(|r| r.outer.segs.len()).collect::>(); assert!(sides.contains(&4) && sides.contains(&3), "{sides:?}"); } /// A sketch holding `text` the way `sketch.text` makes it: one text link of the built-in font's /// outlines. fn text_sketch(text: &str, at: Vec2, height: f64) -> Sketch { let mut sk = Sketch::new(); let source = LinkSource::Text { text: text.into(), at, height, angle: 0.0, font: None }; sk.add_link(LinkKind::Text, source, &text_geometry(text, at, height, 0.0).unwrap()).unwrap(); sk } fn sorted_areas(ps: &[Profile]) -> Vec { let mut a: Vec = ps.iter().map(|p| p.area).collect(); a.sort_by(f64::total_cmp); a } fn assert_areas(got: &[f64], mut want: Vec) { want.sort_by(f64::total_cmp); assert_eq!(got.len(), want.len(), "{got:?} against {want:?}"); for (g, w) in got.iter().zip(&want) { assert!((g - w).abs() < 2e-3, "{got:?} against {want:?}"); } } #[test] fn text_counters_are_holes_of_their_letters_not_profiles_of_their_own() { // Areas in mm² at a capital height of 10 mm, summed from the Liberation Sans contours (shoelace // formula, quadratic pieces flattened finely): a glyph's outer contour, then its counters. // O: 83.3133 - 47.4584 A: 39.2159 - 9.0647 B: 68.9283 - 12.9336 - 16.6277 // 8: 56.8394 - 10.6451 - 13.7347 let (o, o_counter) = (83.3133 - 47.4584, 47.4584); let (a, a_counter) = (39.2159 - 9.0647, 9.0647); let (b, b_counters) = (68.9283 - 12.9336 - 16.6277, [12.9336, 16.6277]); let (eight, eight_counters) = (56.8394 - 10.6451 - 13.7347, [10.6451, 13.7347]); let check = |text: &str, letters: Vec, counters: Vec| { let sk = text_sketch(text, Vec2::ZERO, 10.0); let all = find_drawn_profiles(&sk); // Every counter is a closed region of its own, next to its letter with the hole in it... assert_areas(&sorted_areas(&all), letters.iter().chain(&counters).copied().collect()); // ...and "all profiles" of text means the letters. assert_areas(&sorted_areas(&without_text_counters(&sk, all)), letters); }; check("O", vec![o], vec![o_counter]); check("8", vec![eight], eight_counters.to_vec()); check("AB", vec![a, b], [vec![a_counter], b_counters.to_vec()].concat()); // A letter without a counter is untouched. check("I", vec![13.5557], vec![]); } #[test] fn text_beside_other_curves_is_drawn_geometry() { // A 30 x 20 rectangle with an "I" (a 10 mm tall bar, 13.5557 mm² in Liberation Sans) beside it. let mut sk = text_sketch("I", v(40.0, 0.0), 10.0); for (p, q) in [((0.0, 0.0), (30.0, 0.0)), ((30.0, 0.0), (30.0, 20.0)), ((30.0, 20.0), (0.0, 20.0)), ((0.0, 20.0), (0.0, 0.0))] { sk.add_line(v(p.0, p.1), v(q.0, q.1), None, None, None).unwrap(); } assert_areas(&sorted_areas(&find_drawn_profiles(&sk)), vec![13.5557, 600.0]); // Projected reference geometry still doesn't count as drawn. let square = |s: f64| { vec![ LinkGeom::Line(v(100.0, 0.0), v(100.0 + s, 0.0)), LinkGeom::Line(v(100.0 + s, 0.0), v(100.0 + s, s)), LinkGeom::Line(v(100.0 + s, s), v(100.0, s)), LinkGeom::Line(v(100.0, s), v(100.0, 0.0)), ] }; sk.add_link(LinkKind::Project, LinkSource::Origin, &square(5.0)).unwrap(); assert_eq!(find_profiles(&sk).len(), 3); assert_areas(&sorted_areas(&find_drawn_profiles(&sk)), vec![13.5557, 600.0]); } #[test] fn a_letter_merged_with_its_counter_is_the_filled_letter() { // The ring of an O and its counter share the counter's two conics; merged, they cancel along it. let sk = text_sketch("O", Vec2::ZERO, 10.0); let regions: Vec<_> = find_profiles(&sk).into_iter().map(|p| p.region).collect(); assert_eq!(regions.len(), 2); let merged = merge_regions(®ions); assert_eq!(merged.len(), 1, "{} regions", merged.len()); assert!(merged[0].holes.is_empty(), "{} holes", merged[0].holes.len()); assert!((merged[0].area() - 83.3133).abs() < 2e-3, "{}", merged[0].area()); } #[test] fn links_update_in_place_rebuild_and_break() { use crate::{LinkGeom, LinkKind, LinkSource}; let mut sk = Sketch::new(); let src = LinkSource::Origin; let sq = |s: f64| { vec![ LinkGeom::Line(v(0.0, 0.0), v(s, 0.0)), LinkGeom::Line(v(s, 0.0), v(s, s)), LinkGeom::Line(v(s, s), v(0.0, s)), LinkGeom::Line(v(0.0, s), v(0.0, 0.0)), ] }; let j = sk.add_link(LinkKind::Project, src, &sq(10.0)).unwrap(); assert_eq!(sk.link_curves(&j).len(), 4); // Corners are shared: 4 points, all fixed. let pts = sk.link_points(&j); assert_eq!(pts.len(), 4); assert!(pts.iter().all(|p| sk.points[*p].fixed)); // A free line dimensioned to a projected corner keeps its constraint across an update. let l = sk.add_line(v(3.0, 3.0), v(5.0, 7.0), None, None, None).unwrap(); let (la, _) = match sk.curves[l].kind { CurveKind::Line { a, b } => (a, b), _ => panic!(), }; sk.add_constraint(ConstraintKind::Coincident { p: la, q: pts[2] }, None).unwrap(); let ids: Vec = sk.link_curves(&j).iter().map(|c| sk.curves[*c].id.clone()).collect(); assert!(!sk.update_link(&j, &sq(20.0)).unwrap(), "same shape updates in place"); let ids2: Vec = sk.link_curves(&j).iter().map(|c| sk.curves[*c].id.clone()).collect(); assert_eq!(ids, ids2); assert!(solve(&mut sk).ok()); assert!(sk.points[la].pos.dist(v(20.0, 20.0)) < 1e-9); assert_eq!(find_profiles(&sk).len(), 1); // A different shape rebuilds (the coincidence on the old corner goes). assert!(sk.update_link(&j, &[LinkGeom::Circle(v(1.0, 1.0), 4.0)]).unwrap()); assert_eq!(sk.link_curves(&j).len(), 1); assert_eq!(sk.constraints.len(), 0); // Linked circles keep their radius in the solver. let c = sk.link_curves(&j)[0]; let rep = solve(&mut sk); assert!(rep.curve_determined[c], "{rep:?}"); // Breaking the link frees it. sk.break_link(&j).unwrap(); assert!(sk.links.is_empty()); assert!(sk.curves[c].link.is_none()); let rep = solve(&mut sk); assert!(!rep.curve_determined[c]); // Deleting all of a link's curves removes the link. let j2 = sk.add_link(LinkKind::Project, LinkSource::Origin, &[LinkGeom::Line(v(0.0, 0.0), v(1.0, 0.0))]).unwrap(); let cs = sk.link_curves(&j2); sk.remove_curves(&cs); assert!(sk.link(&j2).is_none()); // Nothing to project is an error, not an empty link. assert!(sk.add_link(LinkKind::Project, LinkSource::Origin, &[LinkGeom::Line(v(0.0, 0.0), v(0.0, 0.0))]).is_err()); // Round trip through JSON. let j3 = sk.add_link(LinkKind::Intersect, LinkSource::Axis { name: "X".into() }, &[LinkGeom::Point(v(2.0, 2.0))]).unwrap(); let back: Sketch = serde_json::from_str(&serde_json::to_string(&sk).unwrap()).unwrap(); assert_eq!(back, sk); assert_eq!(back.link(&j3).unwrap().kind, LinkKind::Intersect); } /// Many independent constrained rectangles solve quickly (per-component work). #[test] fn large_sketch_solves_fast() { let mut sk = Sketch::new(); for i in 0..150 { let o = v(i as f64 * 50.0, 0.0); let l = rect(&mut sk, [o, o + v(40.0, 0.5), o + v(40.5, 30.0), o + v(-0.3, 30.2)]); use ConstraintKind::*; sk.add_constraint(Horizontal { l: l[0] }, None).unwrap(); sk.add_constraint(Horizontal { l: l[2] }, None).unwrap(); sk.add_constraint(Vertical { l: l[1] }, None).unwrap(); sk.add_constraint(Vertical { l: l[3] }, None).unwrap(); sk.add_constraint(Length { l: l[0], value: 40.0 }, None).unwrap(); sk.add_constraint(Length { l: l[1], value: 30.0 }, None).unwrap(); } let t = std::time::Instant::now(); let rep = solve(&mut sk); let ms = t.elapsed().as_millis(); assert!(rep.ok()); assert_eq!(rep.dof, 300); assert!(ms < 10_000, "{ms} ms"); eprintln!("large sketch: {ms} ms"); } #[test] fn long_connected_chain_solves() { let mut sk = Sketch::new(); let n = 300; let mut prev = 0usize; for i in 0..n { let p = sk.add_point(v((i + 1) as f64 * 10.0 + 0.3, if i % 2 == 0 { 0.4 } else { -0.2 }), None).unwrap(); let l = sk.add_line_pts(prev, p, None).unwrap(); sk.add_constraint(ConstraintKind::Horizontal { l }, None).unwrap(); sk.add_constraint(ConstraintKind::Length { l, value: 10.0 }, None).unwrap(); prev = p; } let t = std::time::Instant::now(); let rep = solve(&mut sk); let ms = t.elapsed().as_millis(); eprintln!("chain: {ms} ms, iters {}", rep.iterations); assert!(rep.ok(), "{rep:?}"); assert_eq!(rep.dof, 0); assert!(ms < 10_000, "{ms} ms"); } /// Free-form curves stay exact in profiles: a degree-5 spline closed by a line, and a fit spline /// cut in two by a line through it (each side gets an exact sub-curve ending on the line). #[test] fn exact_profiles_for_high_degree_and_cut_free_form_curves() { use solidcraft_geom::Seg2; let exact_loop = |p: &Profile| p.region.outer.segs.iter().all(|s| !matches!(s, Seg2::Line { .. }) || s.length() > 1.0); let closed = |p: &Profile| { let s = &p.region.outer.segs; s.iter().zip(s.iter().cycle().skip(1)).all(|(a, b)| a.end().dist(b.start()) < 1e-9) }; // Degree 5 over six points, closed by a line. let mut sk = Sketch::default(); let ids: Vec = [v(0.0, 0.0), v(4.0, 10.0), v(10.0, 12.0), v(16.0, 12.0), v(22.0, 10.0), v(26.0, 0.0)] .iter() .map(|p| sk.add_point(*p, None).unwrap()) .collect(); sk.add_curve(CurveKind::Spline { pts: ids.clone(), control: true, degree: 5, periodic: false }, None).unwrap(); sk.add_line_pts(ids[5], ids[0], None).unwrap(); let pr = find_profiles(&sk); assert_eq!(pr.len(), 1); assert!(pr[0].region.outer.segs.iter().any(|s| matches!(s, Seg2::Bezier { n: 5, .. })), "{:?}", pr[0].region.outer.segs); assert!(closed(&pr[0])); // A fit spline arch over a base line, cut by a vertical line at x = 10. let mut sk = Sketch::default(); let ids: Vec = [v(0.0, 0.0), v(6.0, 9.0), v(14.0, 9.0), v(20.0, 0.0)].iter().map(|p| sk.add_point(*p, None).unwrap()).collect(); sk.add_curve(CurveKind::Spline { pts: ids.clone(), control: false, degree: 3, periodic: false }, None).unwrap(); sk.add_line_pts(ids[3], ids[0], None).unwrap(); let (a, b) = (sk.add_point(v(10.0, -5.0), None).unwrap(), sk.add_point(v(10.0, 20.0), None).unwrap()); sk.add_line_pts(a, b, None).unwrap(); let pr = find_profiles(&sk); assert_eq!(pr.len(), 2, "{pr:?}"); let whole: f64 = pr.iter().map(|p| p.area).sum(); for p in &pr { assert!(closed(p), "{:?}", p.region.outer.segs); assert!(exact_loop(p), "polyline pieces left: {:?}", p.region.outer.segs); // The crossing lies on the line x = 10. assert!( p.region .outer .segs .iter() .any(|s| matches!(s, Seg2::Cubic { .. }) && ((s.start().x - 10.0).abs() < 1e-9 || (s.end().x - 10.0).abs() < 1e-9)) ); } // Same total area as the uncut arch. sk.curves.pop(); let uncut: f64 = find_profiles(&sk).iter().map(|p| p.area).sum(); assert!((whole - uncut).abs() < 1e-6 * uncut, "{whole} vs {uncut}"); }