Merge pull request #2053 from brett19/fix/2050-crossing-rails-query

fix: crossing-rails check treats a closest-point query with no solution as no crossing (#2050)
This commit is contained in:
bherbruck
2026-10-31 13:16:04 -04:00
committed by GitHub
4 changed files with 2335 additions and 7 deletions
@@ -9,7 +9,10 @@
//!
//! Deviations: not upstream. Rail positions are sampled, and the distance to the other
//! round's edge is a 3D distance (exact on planar faces, a lower bound on curved ones
//! where the face between the edges bends).
//! where the face between the edges bends). A closest-point query on an edge with no
//! solution (`SmEdge::ClosestPoint` finding none, #2050) measures nothing: a rail point
//! without one is not inside the other round's strip, and gives its own round no width;
//! a rail with no width at all takes no strip it can be tested against.
// ponytail: sampled 3D distances, no face-interior test; a C-shaped face whose two
// edges face each other across its notch would be refused though they fit. Upgrade to
// the surface distance within the face if that shows up.
@@ -18,7 +21,7 @@ use super::{FilletBody, FilletObject, FilletSession};
use crate::error::{EntityRef, Result, SolidError};
use crate::kernel::Kernel;
use crate::math::Point3;
use crate::query::api_queries::edge_closest_point;
use crate::query::edge::closest_point;
use crate::topology::edge;
use crate::topology::edgeuse::p1 as eu1;
use crate::topology::{EdgeId, FaceId};
@@ -51,9 +54,11 @@ impl FilletSession<'_> {
continue;
}
for &p in &a.points {
if edge_closest_point(k, target, b.edge, p)?.closest.distance + tolerance
< b.width
{
// No closest point on the other round's edge: no crossing found here.
let Some(closest) = closest_point(k, target, b.edge, p)? else {
continue;
};
if closest.distance + tolerance < b.width {
self.executive.error.code = 4; // SM_FILERR_EDGE_PROBLEM
self.executive.error.status = 2; // SM_FIL_FAILURE
self.executive.error.edge = Some(FilletObject::Topology(
@@ -111,9 +116,16 @@ impl FilletSession<'_> {
for i in 0..SAMPLES {
let t = lo + (hi - lo) * (i as f64 + 0.5) / SAMPLES as f64;
let p = curve.evaluate_point(k, t)?;
width = width.min(edge_closest_point(k, target, edge, p)?.closest.distance);
if let Some(closest) = closest_point(k, target, edge, p)? {
width = width.min(closest.distance);
}
points.push(p);
}
if !width.is_finite() {
// No rail point found its edge: the rail's strip is unknown, and
// an infinite width would refuse every point tested against it.
continue;
}
rails.push(Rail {
edge,
face,
+1 -1
View File
@@ -221,7 +221,7 @@ pub struct UpstreamFixes {
pub fillet_refuse_overlapping_bands: bool,
/// `DoFillet` refuses a filleted target that is not a closed manifold solid (an edge that is not `SmEdge::IsManifold`) with `SM_FILERR_BAD_MERGE`, where upstream returns success (WR-212, #422); the public wrappers (`fillet_edges`, `fillet_edges_per_edge`, `variable_radius_fillet`) then put the caller's body back as it was before the call.
pub fillet_refuse_non_manifold_result: bool,
/// The fillet executive refuses two rounds on edges of one face whose rails cross on it (no room between them), before the target is edited, where upstream builds an open body (WR-460, #1073).
/// The fillet executive refuses two rounds on edges of one face whose rails cross on it (no room between them), before the target is edited, where upstream builds an open body (WR-460, #1073). A closest-point query on an edge with no solution finds no crossing there and is not a failure (#2050).
pub fillet_refuse_crossing_rails: bool,
/// The vertex overload of `SmFilletErrorInfo::NoteError` records the vertex, its location, the fillet status, the error code and the comment, as the edge overload does, and `CreateFilletCorners` notes a vertex no corner type handles as `SM_FILERR_CORNER_UNSUPPORTED` with "Corner case not implemented" (WR-513, #1469; tangency-labs/smlib@a73cf5e). The pinned overload records nothing, so the failure reached the caller as the generic 1001 with no reason. Off by default (pinned behaviour). Diagnostics only: the pending vertex entry and the operation's result are unchanged, so this does not make an unsupported corner succeed. The whole-operation wrappers (e.g. `fillet_edges`) drop their session and return only the later failure; the retained note is readable through [`crate::blend::fillet_executive::session::FilletSession::error_info`]. The C API's `SMLIB_ERR_CORNER_UNSUPPORTED` (-1111) is not mapped here.
pub fillet_vertex_error_diagnostics: bool,
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,116 @@
//! tangency-labs/solidcore#2050: `fixes.fillet_refuse_crossing_rails` (WR-460, #1073)
//! failed a sound round when a closest-point query on the other round's edge had no
//! solution (`edge_closest_point`'s 1001): a query with no solution finds no crossing
//! there, so it no longer fails the fillet.
//!
//! The body (`data/issue_2050_two_radii_y.smd`, written by `write_brep_to_file`) is a
//! 50 x 40 x 30 box with its vertical edges rounded R8. Its top loop, in the issue's edge
//! order, is rounded R4.
#![allow(clippy::unwrap_used, clippy::indexing_slicing)] // test helpers
use std::f64::consts::PI;
use solidcore::blend::fillet_executive::session::FilletSession;
use solidcore::geom::attribute::{AttributeStore, ObjectKey};
use solidcore::geom::object::ObjectRef;
use solidcore::mass::integrator::properties::{brep_volume, PropertyAttributes};
use solidcore::topology::brep::p10::TopologyEnv;
use solidcore::topology::brep::p3::brep_copy;
use solidcore::topology::brep::p7::read_from_file;
use solidcore::topology::database_io::FileType;
use solidcore::topology::topology::NotifyRecords;
use solidcore::topology::{edge, vertex, EdgeId};
use solidcore::{Brep, EntityRef, Kernel, Result};
/// The top loop's edges, in the order the issue passes them.
const TOP_LOOP: [u32; 8] = [20, 15, 1, 21, 23, 11, 7, 22];
/// `SmBrep::ReadFromFile` of the issue's body.
fn body(k: &mut Kernel) -> Brep {
let bytes = include_bytes!("data/issue_2050_two_radii_y.smd").to_vec();
let mut b = Brep::new_with_infinite_region().unwrap();
let mut records = NotifyRecords::new();
let mut attributes = AttributeStore::default();
let key = |_: EntityRef| ObjectRef {
key: ObjectKey(0),
type_code: 0,
};
let mut env = TopologyEnv {
all_attributes: &[],
attributes: &mut attributes,
key_of: &key,
records: &mut records,
next_key: 1 << 40,
};
read_from_file(k, &mut b, bytes, FileType::Ascii, false, &mut env).unwrap();
b.refresh_cached_topology();
b
}
fn volume(k: &Kernel, b: &mut Brep) -> f64 {
let store = AttributeStore::default();
let attrs = PropertyAttributes {
store: &store,
key_of: &|_| ObjectKey(0),
};
brep_volume(k, b, 1e-9, &attrs).unwrap()
}
/// Round the top loop R4 with the check as given; the copy and the fillet's result.
fn round(refuse_crossing_rails: bool) -> (Kernel, Brep, Result<()>) {
let mut k = Kernel::default();
k.fixes.fillet_refuse_crossing_rails = refuse_crossing_rails;
let read = body(&mut k);
let mut b = brep_copy(&mut k, &read).unwrap();
let ids: Vec<EdgeId> = TOP_LOOP.iter().map(|&i| EdgeId::from_index(i)).collect();
for &e in &ids {
let (s, t) = edge::start_end_vertices(&b, e).unwrap();
for v in [s, t] {
let z = vertex::point(&b, v.unwrap()).unwrap().z;
assert!((z - 30.0).abs() < 1e-6, "edge {e:?} not on the top");
}
}
let result = {
let mut session = FilletSession::new(&mut k, &mut b).unwrap();
session
.prepare_constant_radius_edges(&mut k, &ids, 4.0, 1, 1, 1.0)
.unwrap();
session.do_fillet(&mut k)
};
(k, b, result)
}
/// V = (2000 - (4 - pi) 64) 30 - 116 s - 2 pi (8 - 4 (10 - 3 pi)/(12 - 3 pi)) s with
/// s = (1 - pi/4) 16: the R8-cornered box less the R4 round's cross-section swept along
/// the straight edges (116 = 2 (34 + 24)) and round the four corners (Pappus).
fn expected() -> f64 {
let s = (1.0 - PI / 4.0) * 16.0;
(2000.0 - (4.0 - PI) * 64.0) * 30.0
- 116.0 * s
- 2.0 * PI * (8.0 - 4.0 * (10.0 - 3.0 * PI) / (12.0 - 3.0 * PI)) * s
}
#[test]
fn issue_2050_round_with_crossing_check_builds() {
let (k, mut b, result) = round(true);
result.unwrap();
let v = volume(&k, &mut b);
let want = expected();
assert!((want - 57800.239724).abs() < 1e-6, "closed form {want}");
assert!(
(v - want).abs() <= 1e-9 * want.abs(),
"volume {v}, expected {want}"
);
}
#[test]
fn issue_2050_round_without_check_builds() {
let (k, mut b, result) = round(false);
result.unwrap();
let v = volume(&k, &mut b);
let want = expected();
assert!(
(v - want).abs() <= 1e-9 * want.abs(),
"volume {v}, expected {want}"
);
}