diff --git a/crates/engine/src/cmd/sketch.rs b/crates/engine/src/cmd/sketch.rs index 4b41940f..97abb94e 100644 --- a/crates/engine/src/cmd/sketch.rs +++ b/crates/engine/src/cmd/sketch.rs @@ -435,10 +435,17 @@ fn draw_line(s: &mut Session, p: &Value) -> Result { } if infer { let d = b.pos() - a.pos(); - if d.y.abs() <= d.len() * 1e-9 { + let axis = if d.y.abs() <= d.len() * 1e-9 { cons.push(add_c(sk, ConstraintKind::Horizontal { l: c })?); + true } else if d.x.abs() <= d.len() * 1e-9 { cons.push(add_c(sk, ConstraintKind::Vertical { l: c })?); + true + } else { + false + }; + if let Some(k) = infer_at_start(sk, c, axis) { + cons.push(add_c(sk, k)?); } } curves.push(c); @@ -449,6 +456,38 @@ fn draw_line(s: &mut Session, p: &Value) -> Result { Ok(result((curves, cons), info)) } +/// A relation the new line `c` has with the curve it starts from (drawn exactly so): tangent +/// to an arc, or perpendicular to a line (parallel continuations are left alone). `axis`: the +/// line already got horizontal/vertical, so a perpendicular to an axis-aligned line would be +/// redundant. +fn infer_at_start(sk: &Sketch, c: usize, axis: bool) -> Option { + let CurveKind::Line { a, b } = sk.curves.get(c)?.kind else { return None }; + let (pa, pb) = (sk.point(a)?, sk.point(b)?); + let u = (pb - pa).normalized()?; + for (i, o) in sk.curves.iter().enumerate() { + if i == c || !o.kind.uses(a) { + continue; + } + match o.kind { + CurveKind::Arc { c: cc, .. } => { + let r = (pa - sk.point(cc)?).normalized()?; + if r.dot(u).abs() < 1e-9 { + return Some(ConstraintKind::Tangent { a: i, b: c }); + } + } + CurveKind::Line { a: oa, b: ob } => { + let v = (sk.point(ob)? - sk.point(oa)?).normalized()?; + let o_axis = v.x.abs() < 1e-9 || v.y.abs() < 1e-9; + if u.dot(v).abs() < 1e-9 && !(axis && o_axis) { + return Some(ConstraintKind::Perpendicular { a: i, b: c }); + } + } + _ => {} + } + } + None +} + /// Closed polygon of lines through new or referenced corner points. fn polygon_lines(sk: &mut Sketch, corners: &[Vec2]) -> Result<(Vec, Vec)> { let pts: Vec = corners.iter().map(|c| sk.add_point(*c, None)).collect::>()?; diff --git a/crates/engine/src/cmd/sketch_constraints_tests.rs b/crates/engine/src/cmd/sketch_constraints_tests.rs index dae6b894..d144094e 100644 --- a/crates/engine/src/cmd/sketch_constraints_tests.rs +++ b/crates/engine/src/cmd/sketch_constraints_tests.rs @@ -133,3 +133,16 @@ fn auto_constrain_fully_constrains_a_rough_sketch() { assert!(s.active_sketch.is_none()); assert!(r["added"].as_array().unwrap().len() >= 3, "{r}"); } + +#[test] +fn drawing_infers_tangent_and_perpendicular() { + let mut s = new_sketch(); + let l1 = ids(&run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [10, 10]], "infer": true}))["curves"])[0].clone(); + let r = run(&mut s, "DrawPolyline", json!({"points": [format!("{l1}.end"), [20, 0]], "infer": true})); + assert_eq!(r["constraints"].as_array().unwrap().len(), 1, "{r}"); + let a = ids(&run(&mut s, "ArcCenterTwoPoint", json!({"center": [50, 0], "start": [60, 0], "end": [50, 10]}))["curves"])[0].clone(); + let r = run(&mut s, "DrawPolyline", json!({"points": [format!("{a}.end"), [30, 10]], "infer": true})); + let si = run(&mut s, "sketch.inspect", json!({})); + let names: Vec<&str> = si["constraints"].as_array().unwrap().iter().map(|c| c["name"].as_str().unwrap()).collect(); + assert!(names.contains(&"Perpendicular") && names.contains(&"Tangent"), "{names:?} {r}"); +} diff --git a/crates/sketch/src/profiles.rs b/crates/sketch/src/profiles.rs index 78b0c6ba..46bda3f0 100644 --- a/crates/sketch/src/profiles.rs +++ b/crates/sketch/src/profiles.rs @@ -160,9 +160,24 @@ pub fn find_profiles(sk: &Sketch) -> Vec { } } } - if shapes.len() <= 2000 { + // Bounding boxes: only shapes whose boxes overlap can cross. + let boxes: Vec<(Vec2, Vec2)> = shapes + .iter() + .map(|(_, sh)| match *sh { + Shape::Line { a, b } => (Vec2::new(a.x.min(b.x) - tol, a.y.min(b.y) - tol), Vec2::new(a.x.max(b.x) + tol, a.y.max(b.y) + tol)), + Shape::Round { c, r, .. } => (Vec2::new(c.x - r - tol, c.y - r - tol), Vec2::new(c.x + r + tol, c.y + r + tol)), + }) + .collect(); + let overlap = |i: usize, j: usize| match (boxes.get(i), boxes.get(j)) { + (Some((a0, a1)), Some((b0, b1))) => a0.x <= b1.x && b0.x <= a1.x && a0.y <= b1.y && b0.y <= a1.y, + _ => false, + }; + if shapes.len() <= 20_000 { for i in 0..shapes.len() { for j in (i + 1)..shapes.len() { + if !overlap(i, j) { + continue; + } let (Some((_, x)), Some((_, y))) = (shapes.get(i), shapes.get(j)) else { continue }; let mut pts = intersections(x, y); // A tangent touch gives two nearly equal roots: one point. @@ -172,12 +187,12 @@ pub fn find_profiles(sk: &Sketch) -> Vec { pts = vec![(p + q) * 0.5]; } for p in pts { - // Crossings at (or next to) curve ends are already cuts. - if ends.iter().any(|e| e.dist(p) < tol * 100.0) { - continue; - } let on_x = x.param_on(p, tol).or_else(|| end_param(x, p, tol)); let on_y = y.param_on(p, tol).or_else(|| end_param(y, p, tol)); + // Crossings at (or next to) curve ends are already cuts. + if on_x.is_none() || on_y.is_none() || ends.iter().any(|e| e.dist(p) < tol * 100.0) { + continue; + } if let (Some(tx), Some(ty)) = (on_x, on_y) { if x.param_on(p, tol).is_some() && let Some(c) = cuts.get_mut(i) diff --git a/crates/sketch/src/tests.rs b/crates/sketch/src/tests.rs index e162d1ae..cd45b950 100644 --- a/crates/sketch/src/tests.rs +++ b/crates/sketch/src/tests.rs @@ -362,3 +362,48 @@ fn links_update_in_place_rebuild_and_break() { 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"); +}