From 3501fa2c54073af603c24df6a3d55c06295329e3 Mon Sep 17 00:00:00 2001 From: Mateusz Sych <151755914+SychPL@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:26:20 +0200 Subject: [PATCH] Emboss: say how far the picked point is from a face (#392) (#1733) Thanks @SychPL. --- crates/engine/src/cmd/features_more.rs | 30 ++++++++++++++++---- crates/engine/src/cmd/features_more_tests.rs | 17 +++++++++++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/cmd/features_more.rs b/crates/engine/src/cmd/features_more.rs index 556a0319..055b56c9 100644 --- a/crates/engine/src/cmd/features_more.rs +++ b/crates/engine/src/cmd/features_more.rs @@ -161,6 +161,8 @@ fn emboss(s: &mut Session, p: &Value) -> Result { let at = solidcraft_doc::apply_point(&s.active_component_frame(), local); let state = s.world_state(); let mut candidates = Vec::new(); + // The nearest face off the point: (distance, body), for the error when none holds it. + let mut nearest: Option<(f64, &str)> = None; for body in &state.bodies { let definition = body.definition_name(); if !targets.is_empty() && !targets.iter().any(|n| n == definition || n == &body.name) { @@ -179,14 +181,30 @@ fn emboss(s: &mut Session, p: &Value) -> Result { inside.then_some(((at - a).dot(n).abs(), *face as usize)) }) .min_by(|a, b| a.0.total_cmp(&b.0)); - if let Some((distance, index)) = closest - && distance < (body.body.size() * 1e-3).max(1e-3) - { - candidates.push((body, index)); + match closest { + Some((distance, index)) if distance < (body.body.size() * 1e-3).max(1e-3) => candidates.push((body, index)), + Some((distance, _)) if nearest.is_none_or(|(d, _)| distance < d) => nearest = Some((distance, body.name.as_str())), + _ => {} } } - if candidates.len() != 1 { - return Err(bad(cmd, "`face` must identify exactly one target body surface")); + // #392: say what is wrong with the point, not only that it picks no single face. + let point = format!("[{}, {}, {}]", local.x, local.y, local.z); + match (candidates.as_slice(), nearest) { + ([], Some((d, body))) => { + return Err(bad( + cmd, + format!("`face` {point} lies on no face: the nearest, on `{body}`, is {d:.3} mm away; give a point on the face"), + )); + } + ([], None) => return Err(bad(cmd, format!("`face` {point} lies on no face of the target bodies"))), + ([_], _) => {} + (more, _) => { + let names: Vec<&str> = more.iter().map(|(b, _)| b.name.as_str()).collect(); + return Err(bad( + cmd, + format!("`face` {point} lies on faces of {} bodies ({}): name one in `targets`", names.len(), names.join(", ")), + )); + } } let (selected, index) = candidates.first().copied().ok_or_else(|| bad(cmd, "no target face"))?; let name = selected.definition_name(); diff --git a/crates/engine/src/cmd/features_more_tests.rs b/crates/engine/src/cmd/features_more_tests.rs index 7852fc00..ac076d1e 100644 --- a/crates/engine/src/cmd/features_more_tests.rs +++ b/crates/engine/src/cmd/features_more_tests.rs @@ -92,6 +92,23 @@ fn emboss_and_deboss_on_a_face() { assert!(s.execute("solid.emboss", &json!({"sketch": "Logo", "depth": 1, "mode": "sideways"})).is_err()); } +#[test] +fn emboss_face_off_the_part_says_how_far() { + // #392: a point beside the face was refused with "`face` must identify exactly one target + // body surface", which didn't say the point was off the face. + let mut s = Session::default(); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 10, "body_name": "A"})); + run(&mut s, "sketch.create", json!({"plane": {"face": [20, 15, 10]}, "name": "Logo"})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [5, 5], "p1": [15, 15]})); + run(&mut s, "sketch.finish", json!({})); + let e = s.execute("solid.emboss", &json!({"sketch": "Logo", "depth": 1, "face": [20, 15, 11.25]})).unwrap_err().to_string(); + assert!(e.contains("[20, 15, 11.25] lies on no face: the nearest, on `A`, is 1.250 mm away"), "{e}"); + run(&mut s, "solid.box", json!({"length": 10, "width": 30, "height": 10, "corner": [40, 0, 0], "operation": "new_body", "body_name": "B"})); + let e = s.execute("solid.emboss", &json!({"sketch": "Logo", "depth": 1, "face": [40, 15, 5]})).unwrap_err().to_string(); + assert!(e.contains("lies on faces of 2 bodies (A, B): name one in `targets`"), "{e}"); + run(&mut s, "solid.emboss", json!({"sketch": "Logo", "depth": 1, "face": [20, 15, 10]})); +} + fn cylinder_badge() -> Session { let mut s = Session::default(); run(&mut s, "parameters.add", json!({"name":"badge_depth","expression":"1 mm"}));