diff --git a/crates/engine/src/cmd/features.rs b/crates/engine/src/cmd/features.rs index 626d7cba..6c5f3128 100644 --- a/crates/engine/src/cmd/features.rs +++ b/crates/engine/src/cmd/features.rs @@ -751,9 +751,39 @@ fn edge_points(s: &Session, p: &Value, cmd: &str) -> Result> { return Err(bad(cmd, "select 1…1000 edges")); } let st = s.model.state(); + // A picked point must lie on an edge (within the distance the kernel re-finds an edge from) + // of the body the feature names, or of any body: a pick far from every edge would round + // whichever edge is nearest (#783). Each body's edges are worked out once. + let named = str_(p, "body").filter(|n| st.body(n).is_some()); + let mut candidates: Option)>> = None; let mut out = Vec::new(); for e in a { if let Some(pt) = vec3(e).or_else(|| e.get("point").and_then(vec3)) { + let bodies = candidates.get_or_insert_with(|| { + // Mesh bodies have no edges to pick: rounding one is refused later, saying why. + st.bodies + .iter() + .filter(|b| named.is_none_or(|n| b.name == n) && !b.body.is_mesh()) + .filter_map(|b| { + let size = b.body.size(); + b.body.edges((size * 2e-3).max(1e-3)).ok().map(|edges| (size, edges)) + }) + .collect() + }); + let mut nearest = f64::INFINITY; + let on_edge = bodies.iter().any(|(size, edges)| { + let d = edges + .iter() + .flat_map(|e| e.points.windows(2)) + .filter_map(|w| Some(pt.dist_to_segment(*w.first()?, *w.get(1)?))) + .fold(f64::INFINITY, f64::min); + nearest = nearest.min(d); + d <= size * 0.05 + 1e-3 + }); + if !on_edge && !bodies.is_empty() { + let far = if nearest.is_finite() { format!("the nearest edge is {nearest:.3} mm away") } else { "there is no edge".to_string() }; + return Err(bad(cmd, format!("no edge at [{}, {}, {}]: {far}", pt.x, pt.y, pt.z))); + } out.push(pt); continue; } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 99522a98..9e0d1a83 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -33,6 +33,31 @@ fn round_as_tall_as_a_disc_uses_up_its_wall() { assert!(rel(volume(&mut s), expect) < 2e-4, "{} vs {expect}", volume(&mut s)); } +/// #783: an edge pick far from every edge (beside the body, above it, inside it, or on another +/// body than the one named) is refused and adds nothing, instead of rounding the nearest edge; +/// a pick on an edge, or a little off it as a click lands, still works. +#[test] +fn edge_picks_far_from_every_edge_are_refused() { + let mut s = Session::default(); + for (name, x) in [("A", 0), ("B", 40), ("C", 80), ("D", 120)] { + run(&mut s, "solid.box", json!({"length": 20, "width": 20, "height": 10, "corner": [x, 0, 0], "operation": "new", "body_name": name})); + } + run(&mut s, "solid.fillet", json!({"body": "A", "edges": [[10, 0, 10]], "radius": 1})); + run(&mut s, "solid.fillet", json!({"body": "B", "edges": [[50, 0.4, 10.3]], "radius": 1})); + let features = s.doc.features.len(); + for (id, p, far) in [ + ("solid.fillet", json!({"body": "B", "edges": [[50, -15, 10]], "radius": 1}), "15.0"), + ("solid.chamfer", json!({"body": "C", "edges": [[90, 10, 40]], "distance": 1}), "31.623 mm"), + ("solid.fillet", json!({"edges": [[130, 10, 5]], "radius": 1}), "11.180 mm"), + ("solid.fillet", json!({"body": "C", "edges": [[10, 20, 10]], "radius": 1}), "mm away"), + ("solid.press_pull", json!({"edges": [[50, -15, 10]], "distance": 1}), "15.0"), + ] { + let err = s.execute(id, &p).expect_err(&format!("{id} {p}")).to_string(); + assert!(err.contains("no edge at") && err.contains(far), "{id} {p}: {err}"); + } + assert_eq!(s.doc.features.len(), features); +} + #[test] fn fillet_as_wide_as_a_face_rounds_it_off() { // #199: 14 x 6 x 10 box, the four vertical edges at r = 3 (half the 6 mm end faces). diff --git a/examples/bracket.json b/examples/bracket.json index e3ca078a..752df039 100644 --- a/examples/bracket.json +++ b/examples/bracket.json @@ -5,7 +5,7 @@ {"command": "sketch.line", "params": {"points": [[0, 0], [40, 0], [40, 6], [6, 6], [6, 30], [0, 30]], "closed": true, "ids": ["a", "b", "c", "d", "e", "f"]}}, {"command": "sketch.finish", "params": {}}, {"command": "solid.extrude", "params": {"distance": "width", "direction": "negative", "body_name": "Bracket"}}, - {"command": "solid.fillet", "params": {"edges": [[40, 30, 6]], "radius": 2}}, + {"command": "solid.fillet", "params": {"edges": [[40, -30, -6]], "radius": 2}}, {"command": "sketch.create", "params": {"plane": "XY", "name": "Holes"}}, {"command": "sketch.circle.center", "params": {"center": [25, 15], "diameter": 8}}, {"command": "sketch.circle.center", "params": {"center": [25, 45], "diameter": 8}},