From 110d00d07eb18b28c1a525c7dde0c26a44b6ed57 Mon Sep 17 00:00:00 2001 From: Thodoris Tsagkogeorgas Date: Wed, 14 Oct 2026 18:06:11 +0300 Subject: [PATCH] A fillet of a vanished edge says the name is gone (#775) (#1066) A fillet or chamfer whose picked edge no longer exists now says the edge's name is gone instead of failing on an unrelated edge (#775). --- crates/doc/src/eval.rs | 35 +++++++++++++++++++---- crates/engine/src/naming_history_tests.rs | 28 ++++++++++++++++++ 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/crates/doc/src/eval.rs b/crates/doc/src/eval.rs index 51302bf2..53d2e195 100644 --- a/crates/doc/src/eval.rs +++ b/crates/doc/src/eval.rs @@ -1970,6 +1970,24 @@ pub fn edge_names_for(st: &ModelState, body: &Option, pts: &[Vec3]) -> V } } +/// The stored pick sits on the axis of a round, not on an edge. The nearest edge within a +/// quarter of the body is that round's tangency seam, which is a different edge (#775). +fn pick_lies_on_a_round_axis(body: &Body, p: Vec3) -> bool { + let Some(cyl) = kernel::cylinder_face_at(body, p) else { return false }; + let Some(axis) = cyl.axis.normalized() else { return false }; + let radial = p - cyl.axis_point; + let dist = (radial - axis * radial.dot(axis)).len(); + if !(cyl.radius > 1e-6 && dist < cyl.radius * 0.35) { + return false; + } + let tight = (body.size() * 2e-3).max(1e-3); + // A pick on the wall itself is the seam. Only an axial pick is the vanished corner. + match body.nearest_edge(p, cyl.radius * 2.0) { + Ok(Some((_, d))) => d > tight, + _ => true, + } +} + /// Edge reference points re-found on the body: points on an edge stay; a point that moved off /// (an upstream edit) is carried with the body (its place in the body's bounding box then and /// now) to an edge running the same way; failing that it goes to the nearest edge with a @@ -4245,23 +4263,28 @@ fn eval_feature(doc: &Document, vals: &BTreeMap, f: &Feature, st: Some((_, Err(_))) | None => { let mut w = None; let refs: Vec = f.edge_refs.get(i).cloned().into_iter().collect(); - if let Ok(q) = resolve_edges(&mb.body, &[*p], &refs, &mut w) { + let skipped = + format!("edge `{}` no longer exists and was skipped", f.edge_names.get(i).map(String::as_str).unwrap_or("?")); + // The name is gone. A pick on the axis of a round must not fall + // through to the tangency seam (#775). A sharp edge that moved + // still resolves from the point, which an edit relies on. + if pick_lies_on_a_round_axis(&mb.body, *p) { + notes.push(skipped); + } else if let Ok(q) = resolve_edges(&mb.body, &[*p], &refs, &mut w) { out.extend(q); notes.push(match f.edge_names.get(i) { Some(n) if !n.is_empty() => format!("edge `{n}` no longer exists; using the edge at the picked point"), _ => "an edge reference without a name was found by its point".into(), }); } else { - notes.push(format!( - "edge `{}` no longer exists and was skipped", - f.edge_names.get(i).map(String::as_str).unwrap_or("?") - )); + notes.push(skipped); } } } } if out.is_empty() { - return Err(DocError::Invalid("none of the selected edges exist any more".into())); + let why = if notes.is_empty() { String::new() } else { format!(" ({})", notes.join("; ")) }; + return Err(DocError::Invalid(format!("none of the selected edges exist any more{why}"))); } if !notes.is_empty() { *warning = Some(notes.join("; ")); diff --git a/crates/engine/src/naming_history_tests.rs b/crates/engine/src/naming_history_tests.rs index 2932a6c7..dc3685de 100644 --- a/crates/engine/src/naming_history_tests.rs +++ b/crates/engine/src/naming_history_tests.rs @@ -271,3 +271,31 @@ fn a_sketch_on_a_certified_face_survives_a_fillet_and_a_cut_upstream_of_it() { } assert!((top(&s) - 20.0).abs() < 1e-6, "{}", top(&s)); } + +/// A fillet of a shell's inner corners names those edges. Thinning the wall past the outer +/// round removes them. The fillet must say the names are gone, not blend the seam nearest +/// the old pick, which now sits on the axis of the inner round (#775). +#[test] +fn a_shell_fillet_does_not_follow_a_gone_edge_onto_a_smooth_seam() { + let mut s = Session::default(); + run(&mut s, "sketch.create", json!({"plane": "XY"})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [-27.5, -40], "p1": [27.5, 40]})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"distance": 25, "name": "Extrude1"})); + run( + &mut s, + "solid.fillet", + json!({"radius": 5, "name": "Fillet1", "edges": [[-27.5, -40, 12.5], [27.5, -40, 12.5], [27.5, 40, 12.5], [-27.5, 40, 12.5]]}), + ); + run(&mut s, "solid.shell", json!({"faces": [[0, 0, 25]], "thickness": 5, "direction": "inside", "name": "Shell1"})); + run( + &mut s, + "solid.fillet", + json!({"radius": 5, "name": "Fillet2", "edges": [[-22.5, 35, 15], [22.5, 35, 15], [22.5, -35, 15], [-22.5, -35, 15]]}), + ); + assert_eq!(error(&s, "Fillet2"), None); + let edited = s.execute("timeline.edit", &json!({"feature": "Shell1", "set": {"thickness": "2"}})); + let message = error(&s, "Fillet2").or_else(|| edited.err().map(|e| e.to_string())).unwrap_or_default(); + assert!(message.contains("no longer exists"), "{message}"); + assert!(!message.contains("smooth edge"), "{message}"); +}