From f070eee2b68e5756003109a00a60f8cb79d03b3f Mon Sep 17 00:00:00 2001 From: jyviko <2501648+jyviko@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:12:50 -0400 Subject: [PATCH] brep, kernel: a face's side is read on its NURBS form (#4149) A shell's `forward` is about the surface the face's (u, v) are on, its NURBS form when it has one, which may run against its cylinder, cone, sphere or torus. The kernel read it against the analytic surface, so a fillet's bore whose form runs the other way was a shaft and an M10 thread was refused in its 8.5 mm hole. `Face::form_turns` says when the form turns the normal. Co-Authored-By: Claude Opus 5.5 --- crates/geom/src/brep.rs | 34 ++++++++++++++++++++++++++++++++-- crates/kernel/src/offset.rs | 3 ++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/crates/geom/src/brep.rs b/crates/geom/src/brep.rs index 2c62760bc7..45f71bd27e 100644 --- a/crates/geom/src/brep.rs +++ b/crates/geom/src/brep.rs @@ -75,8 +75,9 @@ pub enum Shell { #[serde(deny_unknown_fields)] pub struct OrientedFace { pub face: u32, - /// The face's normal in this shell is its surface's natural normal (∂S/∂u × ∂S/∂v). In a - /// closed shell the normals point out of the material. + /// The face's normal in this shell is the natural normal (∂S/∂u × ∂S/∂v) of the surface its + /// (u, v) are on, [`Face::parameters`]: its NURBS form when it has one. In a closed shell the + /// normals point out of the material. pub forward: bool, } @@ -529,6 +530,35 @@ impl Face { pub fn parameters(&self) -> &Surface { self.nurbs.as_ref().unwrap_or(&self.surface) } + + /// Whether the face's NURBS form turns its natural normal against `surface`'s (away from + /// the axis of a cylinder or cone, the centre of a sphere, the tube's centre circle of a + /// torus), at the middle of `domain`. A shell's `forward` is about [`Face::parameters`], so + /// the side `surface` is used on is `forward != form_turns()`. `Some(false)` without a form; + /// `None` where the form does not evaluate there. + pub fn form_turns(&self) -> Option { + let Some(form) = &self.nurbs else { return Some(false) }; + let eval = eval::SurfaceEval::new(form).ok()?; + let [[u0, u1], [v0, v1]] = self.domain; + let (p, su, sv) = eval.derivs(0.5 * (u0 + u1), 0.5 * (v0 + v1))?; + let v3 = |a: [f64; 3]| crate::Vec3::new(a[0], a[1], a[2]); + let radial = |f: &Frame| { + let (o, z) = (v3(f.origin), v3(f.x_axis).cross(v3(f.y_axis))); + let d = p - o; + (o, d - z * (d.dot(z) / z.dot(z))) + }; + let outward = match &self.surface { + Surface::Cylinder { frame, .. } | Surface::Cone { frame, .. } => radial(frame).1, + Surface::Sphere { frame, .. } => p - v3(frame.origin), + Surface::Torus { frame, major_radius, .. } => { + let (o, r) = radial(frame); + p - (o + r.normalized()? * *major_radius) + } + _ => return None, + }; + let along = su.cross(sv).dot(outward); + (along.abs() > 0.0).then_some(along < 0.0) + } } impl Brep { diff --git a/crates/kernel/src/offset.rs b/crates/kernel/src/offset.rs index b2dea0ced7..911c869bcf 100644 --- a/crates/kernel/src/offset.rs +++ b/crates/kernel/src/offset.rs @@ -37,7 +37,8 @@ fn axes(f: &Frame) -> Option<(Vec3, Vec3)> { /// uses its natural side. pub(crate) fn surf_of(b: &Brep, face: usize) -> Option { let f = b.faces.get(face)?; - let out = crate::assemble::natural_out(b, face)?; + // `forward` is about the face's NURBS form, which may run against its surface. + let out = crate::assemble::natural_out(b, face)? != f.form_turns()?; let sign = if out { 1.0 } else { -1.0 }; Some(match &f.surface { Surface::Plane { origin, u_axis, v_axis } => {