mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
* claim: #10011 * Fix snapping at infinite construction-line crossings (#10011)
This commit is contained in:
committed by
GitHub
parent
604515cfb3
commit
e0c1b8adbd
@@ -455,6 +455,27 @@ fn drawing_at_a_curve_crossing_keeps_the_point_on_both() {
|
||||
assert!(st.y > 0.0, "the upper crossing: {st:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drawing_at_an_infinite_line_crossing_keeps_the_point_on_both() {
|
||||
let mut s = new_sketch();
|
||||
let a = ids(&run(&mut s, "sketch.construction_line.vertical", json!({"at": [20, 40]}))["curves"])[0].clone();
|
||||
let b = ids(&run(&mut s, "sketch.construction_line.horizontal", json!({"at": [40, 15]}))["curves"])[0].clone();
|
||||
let r = run(&mut s, "sketch.line", json!({"points": [format!("x:{a}+{b}:20.4,15.4"), [30, 25]], "infer": false, "connect": false}));
|
||||
let line = ids(&r["curves"])[0].clone();
|
||||
let sk = sketch(&s);
|
||||
let start = sk.resolve_point(&format!("{line}.start")).unwrap();
|
||||
assert!(sk.point(start).unwrap().dist(Vec2::new(20.0, 15.0)) < 1e-9);
|
||||
for name in [a, b] {
|
||||
let curve = sk.curve_index(&name).unwrap();
|
||||
assert!(sk.constraints.iter().any(|k| matches!(k.kind, crate::sketch::ConstraintKind::PointOnCurve { p, c } if p == start && c == curve)));
|
||||
}
|
||||
let before_undo = s.doc.to_json();
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(sketch(&s).curves.len(), 2);
|
||||
run(&mut s, "edit.redo", json!({}));
|
||||
assert_eq!(s.doc.to_json(), before_undo);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drawing_at_a_curve_crossing_refuses_hostile_arguments() {
|
||||
let mut s = new_sketch();
|
||||
|
||||
@@ -7,7 +7,7 @@ use serde_json::{Value, json};
|
||||
use solidcraft_doc::project::resolve;
|
||||
use solidcraft_doc::{Document, FeatureKind};
|
||||
use solidcraft_geom::{Plane, Vec2, Vec3};
|
||||
use solidcraft_sketch::{ConstraintKind, LinkKind, LinkSource, Sketch, nearest_crossing};
|
||||
use solidcraft_sketch::{ConstraintKind, LinkKind, LinkSource, Sketch};
|
||||
|
||||
use super::sketch_store;
|
||||
use super::{CommandSpec, in_sketch};
|
||||
@@ -685,9 +685,9 @@ pub(super) fn snap_points(s: &mut Session, p: &Value, cmd: &str) -> Result<Value
|
||||
if ci == cj {
|
||||
return Err(bad(cmd, "`x:` needs two distinct curves"));
|
||||
}
|
||||
let sha = sk.shape(ci).ok_or_else(|| bad(cmd, format!("`{a}` is not a line, circle or arc")))?;
|
||||
let shb = sk.shape(cj).ok_or_else(|| bad(cmd, format!("`{b}` is not a line, circle or arc")))?;
|
||||
let at = nearest_crossing(&sha, &shb, near).ok_or_else(|| bad(cmd, "those curves do not meet"))?;
|
||||
sk.shape(ci).ok_or_else(|| bad(cmd, format!("`{a}` is not a line, circle or arc")))?;
|
||||
sk.shape(cj).ok_or_else(|| bad(cmd, format!("`{b}` is not a line, circle or arc")))?;
|
||||
let at = sk.nearest_crossing(ci, cj, near).ok_or_else(|| bad(cmd, "those curves do not meet"))?;
|
||||
let pi = sk.add_point(at, None)?;
|
||||
sk.add_constraint(ConstraintKind::PointOnCurve { p: pi, c: ci }, None)?;
|
||||
sk.add_constraint(ConstraintKind::PointOnCurve { p: pi, c: cj }, None)?;
|
||||
|
||||
+72
-10
@@ -141,14 +141,24 @@ const ON_CURVE: f64 = 1e-4;
|
||||
/// Curves considered for a snap, so a hostile sketch cannot stall the pointer.
|
||||
const MAX_INTERSECTION_CANDIDATES: usize = 256;
|
||||
|
||||
/// Infinite construction lines use their full support; all other curves keep their bounds.
|
||||
fn snap_distance(shape: &Shape, infinite: bool, p: Vec2) -> f64 {
|
||||
if infinite && matches!(shape, Shape::Line { .. }) { p.dist(shape.project(p)) } else { shape.dist(p) }
|
||||
}
|
||||
|
||||
/// The crossing of `x` and `y` nearest `near` that lies on both bounded curves.
|
||||
pub fn nearest_crossing(x: &Shape, y: &Shape, near: Vec2) -> Option<Vec2> {
|
||||
nearest_crossing_with_extents(x, y, near, [false; 2])
|
||||
}
|
||||
|
||||
fn nearest_crossing_with_extents(x: &Shape, y: &Shape, near: Vec2, infinite: [bool; 2]) -> Option<Vec2> {
|
||||
if !near.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let mut best: Option<(f64, Vec2)> = None;
|
||||
let [infinite_x, infinite_y] = infinite;
|
||||
for q in intersections_within(x, y, 1e-6) {
|
||||
if !q.is_finite() || x.dist(q) > ON_CURVE || y.dist(q) > ON_CURVE {
|
||||
if !(q.is_finite() && snap_distance(x, infinite_x, q) <= ON_CURVE && snap_distance(y, infinite_y, q) <= ON_CURVE) {
|
||||
continue;
|
||||
}
|
||||
let d = q.dist(near);
|
||||
@@ -161,28 +171,29 @@ pub fn nearest_crossing(x: &Shape, y: &Shape, near: Vec2) -> Option<Vec2> {
|
||||
|
||||
/// The nearest crossing of two lines, circles or arcs of `sk` to `p` within `tol`.
|
||||
///
|
||||
/// Skips a crossing that already has a sketch point. Curves whose bounded distance from `p`
|
||||
/// is greater than `tol` cannot meet near the cursor and are ignored.
|
||||
/// Skips a crossing that already has a sketch point. Curves farther than `tol` from `p`
|
||||
/// cannot meet near the cursor and are ignored. Infinite lines use their full support;
|
||||
/// ordinary segments and arcs keep their bounds.
|
||||
pub fn nearest_curve_intersection(sk: &Sketch, p: Vec2, tol: f64) -> Option<(Vec2, usize, usize)> {
|
||||
if !p.is_finite() || !tol.is_finite() || tol <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let mut candidates: Vec<(usize, Shape)> = Vec::new();
|
||||
for i in 0..sk.curves.len() {
|
||||
let mut candidates: Vec<(usize, Shape, bool)> = Vec::new();
|
||||
for (i, curve) in sk.curves.iter().enumerate() {
|
||||
if candidates.len() >= MAX_INTERSECTION_CANDIDATES {
|
||||
break;
|
||||
}
|
||||
let Some(sh) = sk.shape(i) else { continue };
|
||||
if sh.dist(p) <= tol {
|
||||
candidates.push((i, sh));
|
||||
if snap_distance(&sh, curve.infinite, p) <= tol {
|
||||
candidates.push((i, sh, curve.infinite));
|
||||
}
|
||||
}
|
||||
let mut best: Option<(f64, Vec2, usize, usize)> = None;
|
||||
for a in 0..candidates.len() {
|
||||
for b in a + 1..candidates.len() {
|
||||
let Some((ia, sa)) = candidates.get(a) else { continue };
|
||||
let Some((ib, sb)) = candidates.get(b) else { continue };
|
||||
let Some(q) = nearest_crossing(sa, sb, p) else { continue };
|
||||
let Some((ia, sa, infinite_a)) = candidates.get(a) else { continue };
|
||||
let Some((ib, sb, infinite_b)) = candidates.get(b) else { continue };
|
||||
let Some(q) = nearest_crossing_with_extents(sa, sb, p, [*infinite_a, *infinite_b]) else { continue };
|
||||
let d = q.dist(p);
|
||||
if d > tol {
|
||||
continue;
|
||||
@@ -253,6 +264,12 @@ impl Shape {
|
||||
}
|
||||
|
||||
impl Sketch {
|
||||
/// The nearest crossing of two analytic curves, respecting infinite construction lines.
|
||||
pub fn nearest_crossing(&self, a: usize, b: usize, near: Vec2) -> Option<Vec2> {
|
||||
let infinite = [self.curves.get(a)?.infinite, self.curves.get(b)?.infinite];
|
||||
nearest_crossing_with_extents(&self.shape(a)?, &self.shape(b)?, near, infinite)
|
||||
}
|
||||
|
||||
/// The analytic shape of a curve (arcs counter-clockwise from their stored start).
|
||||
pub fn shape(&self, ci: usize) -> Option<Shape> {
|
||||
match self.curves.get(ci)?.kind {
|
||||
@@ -286,6 +303,51 @@ mod intersection_snap_tests {
|
||||
assert_eq!([i.min(j), i.max(j)], [a.min(b), a.max(b)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn infinite_line_crossing_snaps_beyond_both_defining_segments() {
|
||||
for reverse in [false, true] {
|
||||
let mut sk = Sketch::new();
|
||||
let mut lines = [(v(20.0, 40.0), v(20.0, 50.0)), (v(40.0, 15.0), v(50.0, 15.0))];
|
||||
if reverse {
|
||||
lines.reverse();
|
||||
}
|
||||
for (a, b) in lines {
|
||||
let i = sk.add_line(a, b, None, None, None).unwrap();
|
||||
sk.curves[i].construction = true;
|
||||
}
|
||||
let near = v(20.4, 15.4);
|
||||
assert!(nearest_curve_intersection(&sk, near, 1.0).is_none(), "finite construction segments do not cross");
|
||||
for curve in &mut sk.curves {
|
||||
curve.infinite = true;
|
||||
}
|
||||
let (at, a, b) = nearest_curve_intersection(&sk, near, 1.0).expect("infinite lines cross");
|
||||
assert!(at.dist(v(20.0, 15.0)) < 1e-9, "{at:?}");
|
||||
assert_ne!(a, b);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn infinite_line_crossing_keeps_the_other_curves_bounds() {
|
||||
let mut sk = Sketch::new();
|
||||
let line = sk.add_line(v(-6.0, 30.0), v(-6.0, 40.0), None, None, None).unwrap();
|
||||
sk.curves[line].infinite = true;
|
||||
sk.curves[line].construction = true;
|
||||
sk.add_arc(v(0.0, 0.0), v(10.0, 0.0), v(0.0, 10.0), [None; 3], None).unwrap();
|
||||
assert!(nearest_curve_intersection(&sk, v(-6.0, 8.0), 1.0).is_none(), "the missing part of the arc is not a crossing");
|
||||
let CurveKind::Line { a, b } = sk.curves[line].kind else { panic!("line") };
|
||||
sk.points[a].pos.x = 6.0;
|
||||
sk.points[b].pos.x = 6.0;
|
||||
let (at, _, _) = nearest_curve_intersection(&sk, v(6.2, 8.2), 1.0).expect("the drawn arc crosses the infinite line");
|
||||
assert!(at.dist(v(6.0, 8.0)) < 1e-9, "{at:?}");
|
||||
|
||||
let mut sk = Sketch::new();
|
||||
let vertical = sk.add_line(v(20.0, 40.0), v(20.0, 50.0), None, None, None).unwrap();
|
||||
sk.curves[vertical].infinite = true;
|
||||
sk.curves[vertical].construction = true;
|
||||
sk.add_line(v(40.0, 15.0), v(50.0, 15.0), None, None, None).unwrap();
|
||||
assert!(nearest_curve_intersection(&sk, v(20.4, 15.4), 1.0).is_none(), "the finite line must not extend");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nearest_curve_intersection_skips_a_crossing_that_already_has_a_point() {
|
||||
let mut sk = Sketch::new();
|
||||
|
||||
@@ -338,6 +338,31 @@ fn a_corner_starts_a_guide_while_drawing_unless_turned_off() {
|
||||
assert_eq!(h.app.session.doc.to_json(), before, "guides add no geometry or constraints");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rectangle_snaps_to_an_infinite_line_crossing() {
|
||||
use solidcraft_engine::geom::Vec2;
|
||||
let mut h = Harness::new();
|
||||
h.app.preferences.look_at_anim = false;
|
||||
for (command, params) in [
|
||||
("sketch.create", json!({"plane":"XY"})),
|
||||
("sketch.construction_line.vertical", json!({"at":[20,40]})),
|
||||
("sketch.construction_line.horizontal", json!({"at":[40,15]})),
|
||||
] {
|
||||
h.app.run(command, params).unwrap();
|
||||
}
|
||||
h.call("ui.camera", json!({"target":[20,15,0],"yaw":0,"pitch":std::f64::consts::FRAC_PI_2,"distance":120}));
|
||||
step(&mut h, json!({"start":"sketch.rectangle.two_point"}));
|
||||
step(&mut h, json!({"move":{"sketch":[20.4,15.4]}}));
|
||||
step(&mut h, json!({"click":{"sketch":[20.4,15.4]}}));
|
||||
let corner = h.app.tool.as_ref().unwrap().pts[0].0;
|
||||
assert!(corner.dist(Vec2::new(20.0, 15.0)) < 1e-9, "the infinite lines cross beyond their defining segments: {corner:?}");
|
||||
step(&mut h, json!({"click":{"sketch":[30,25]}}));
|
||||
step(&mut h, json!({"key":"Escape"}));
|
||||
let sk = h.app.session.doc.sketch(h.app.session.active_sketch.unwrap()).unwrap();
|
||||
assert_eq!(sk.curves.len(), 6, "two construction lines and the rectangle");
|
||||
assert!(sk.points.iter().any(|p| p.pos.dist(Vec2::new(20.0, 15.0)) < 1e-9), "the committed rectangle keeps the snapped corner");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_recent_midpoints_snap_where_their_guides_meet() {
|
||||
use solidcraft_engine::geom::Vec2;
|
||||
|
||||
Reference in New Issue
Block a user