STEP import: a void reads as a cavity whichever way its shell flag points, again

94bd8719a (#3730) turned a BREP_WITH_VOIDS void inside out when it enclosed volume of the
outer shell's sign: SOLIDWORKS writes .F. over a void shell already facing into its cavity,
SolidCraft .T. That lived in the truck reader; the schema-4 reader (crates/step) that replaced
it took the flag alone, and the block with a 20 x 6 x 5 cavity read as 15600 (the cavity added)
instead of 15000 - 600 = 14400.

The reader now takes each void's sign from the faces themselves before it orders the record:
the divergence theorem on the record's surfaces and pcurves (sew's face_flux of p / 3, moved
to geom's brep::eval so both use it). A void enclosing volume of the outer shell's sign has its
faces turned over. Flipping on the finished record is not an option: its edges are ordered by
the faces' sides.

Fixes kernel step_in::tests::a_void_reads_as_a_cavity_whichever_way_its_shell_flag_points.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-29 22:10:42 -04:00
committed by Irakli Kourtis
parent e5476706b1
commit f8b45f2772
3 changed files with 86 additions and 50 deletions
+48 -1
View File
@@ -9,7 +9,7 @@
//! Call on validated records. Evaluation of a well-formed record never panics: a point that
//! cannot be had (a vanishing weight, a Newton step that does not converge) is `None`.
use super::pcurve::Error;
use super::{Curve, Frame, Surface};
use super::{Curve, Curve2, Frame, Surface};
use crate::Vec3;
use crate::bspline::MAX_SPLINE_DEGREE;
use crate::tess::partials::{Partials, tabulate, trig_derivative};
@@ -482,3 +482,50 @@ fn solve3(rows: [Vec3; 3], rhs: [f64; 3]) -> Option<Vec3> {
let x = (bc * rhs[0] + ca * rhs[1] + ab * rhs[2]) * (1.0 / det);
x.is_finite().then_some(x)
}
/// Four-point Gauss–Legendre nodes and weights on `[−1, 1]`.
const GAUSS4: [(f64, f64); 4] = [
(-0.861_136_311_594_052_6, 0.347_854_845_137_453_8),
(-0.339_981_043_584_856_3, 0.652_145_154_862_546_2),
(0.339_981_043_584_856_3, 0.652_145_154_862_546_2),
(0.861_136_311_594_052_6, 0.347_854_845_137_453_8),
];
/// Pieces each pcurve is integrated in, and how many Gauss panels cover a v range.
const FLUX_PIECES: usize = 32;
const FLUX_PANELS: usize = 4;
/// The flux of `field` through a face, `∫∫ field(S)·(S_u × S_v) du dv` over the (u, v) region its
/// loops (record convention, each use its pcurve and direction) bound, by Green's theorem:
/// `∮ −G du` with `G(u, v) = ∫ field·N ds` from `v0` to `v`. A quadrature for the sign of a
/// shell's volume (`field = p / 3`) or a winding number, which only has to be right to well
/// under a half: no geometry is made from it. Loops round a periodic direction need no seam
/// edge, as a seam's two runs at constant u add nothing to `∮ G du`.
pub fn face_flux(s: &SurfaceEval, v0: f64, loops: &[Vec<(&Curve2, bool)>], field: &dyn Fn(Vec3) -> Vec3) -> Option<f64> {
let g = |u: f64, v: f64| -> Option<f64> {
let (mut sum, h) = (0.0, (v - v0) / FLUX_PANELS as f64);
for panel in 0..FLUX_PANELS {
let a = v0 + h * panel as f64;
for (x, w) in GAUSS4 {
let t = a + 0.5 * h * (x + 1.0);
let (p, su, sv) = s.derivs(u, t)?;
sum += 0.5 * h * w * field(p).dot(su.cross(sv));
}
}
sum.is_finite().then_some(sum)
};
let mut total = 0.0;
for (c, forward) in loops.iter().flatten() {
let [t0, t1] = c.interval()?;
let at = |k: f64| {
let f = k / FLUX_PIECES as f64;
// Exactly the ends at f = 0 and 1, whatever the interval's rounding.
c.eval(if *forward { t0 * (1.0 - f) + t1 * f } else { t1 * (1.0 - f) + t0 * f })
};
for k in 0..FLUX_PIECES {
let (a, m, z) = (at(k as f64)?, at(k as f64 + 0.5)?, at(k as f64 + 1.0)?);
total -= g(m[0], m[1])? * (z[0] - a[0]);
}
}
total.is_finite().then_some(total)
}
+1 -48
View File
@@ -13,7 +13,7 @@
use std::collections::HashMap;
use std::sync::Arc;
use solidcraft_geom::brep::eval::SurfaceEval;
use solidcraft_geom::brep::eval::{SurfaceEval, face_flux};
use solidcraft_geom::brep::{self as b, Curve2, pcurve};
use solidcraft_geom::{Ring, Vec3};
@@ -1650,53 +1650,6 @@ fn domain_of(surface: &b::Surface, pcs: &[Vec<Curve2>]) -> Option<[[f64; 2]; 2]>
Some(out)
}
/// Four-point Gauss–Legendre nodes and weights on `[−1, 1]`.
const GAUSS4: [(f64, f64); 4] = [
(-0.861_136_311_594_052_6, 0.347_854_845_137_453_8),
(-0.339_981_043_584_856_3, 0.652_145_154_862_546_2),
(0.339_981_043_584_856_3, 0.652_145_154_862_546_2),
(0.861_136_311_594_052_6, 0.347_854_845_137_453_8),
];
/// Pieces each pcurve is integrated in, and how many Gauss panels cover a v range.
const FLUX_PIECES: usize = 32;
const FLUX_PANELS: usize = 4;
/// The flux of `field` through a face, `∫∫ field(S)·(S_u × S_v) du dv` over the (u, v) region its
/// loops (record convention, each use its pcurve and direction) bound, by Green's theorem:
/// `∮ −G du` with `G(u, v) = ∫ field·N ds` from `v0` to `v`. A quadrature for the sign of a
/// shell's volume (`field = p / 3`) or a winding number, which only has to be right to well
/// under a half: no geometry is made from it. Loops round a periodic direction need no seam
/// edge, as a seam's two runs at constant u add nothing to `∮ G du`.
fn face_flux(s: &SurfaceEval, v0: f64, loops: &[Vec<(&Curve2, bool)>], field: &dyn Fn(Vec3) -> Vec3) -> Option<f64> {
let g = |u: f64, v: f64| -> Option<f64> {
let (mut sum, h) = (0.0, (v - v0) / FLUX_PANELS as f64);
for panel in 0..FLUX_PANELS {
let a = v0 + h * panel as f64;
for (x, w) in GAUSS4 {
let t = a + 0.5 * h * (x + 1.0);
let (p, su, sv) = s.derivs(u, t)?;
sum += 0.5 * h * w * field(p).dot(su.cross(sv));
}
}
sum.is_finite().then_some(sum)
};
let mut total = 0.0;
for (c, forward) in loops.iter().flatten() {
let [t0, t1] = c.interval()?;
let at = |k: f64| {
let f = k / FLUX_PIECES as f64;
// Exactly the ends at f = 0 and 1, whatever the interval's rounding.
c.eval(if *forward { t0 * (1.0 - f) + t1 * f } else { t1 * (1.0 - f) + t0 * f })
};
for k in 0..FLUX_PIECES {
let (a, m, z) = (at(k as f64)?, at(k as f64 + 0.5)?, at(k as f64 + 1.0)?);
total -= g(m[0], m[1])? * (z[0] - a[0]);
}
}
total.is_finite().then_some(total)
}
#[cfg(test)]
mod tests {
use super::*;
+37 -1
View File
@@ -38,7 +38,7 @@ use std::collections::{HashMap, HashSet};
use std::f64::consts::TAU;
use solidcraft_geom::Vec3;
use solidcraft_geom::brep::eval::{CurveEval, SurfaceEval};
use solidcraft_geom::brep::eval::{CurveEval, SurfaceEval, face_flux};
use solidcraft_geom::brep::pcurve::{self, EdgeUse, TrimEdge, TrimFace, TrimUse, Trims};
use solidcraft_geom::brep::{
self, Brep, Curve as BCurve, Curve2, Edge, EdgeUseRef, Face, Frame, Loop, Natural, OrientedEdge, OrientedFace, Region, Shell,
@@ -1033,6 +1033,42 @@ impl<'b, 'a> Builder<'b, 'a> {
// Regions: which shells are closed (every edge used at least twice among its faces).
let closed: Vec<bool> = shells.iter().map(|s| self.closed(s)).collect();
// A void's faces face into the cavity, out of the material: it encloses volume of the
// opposite sign to the outer shell's. The ORIENTED_CLOSED_SHELL flag alone doesn't say
// so: some exporters write .F. over a shell already facing into the cavity (#3730),
// SolidCraft .T.; read by the flag, such a void added its cavity's volume to the solid's.
// A void enclosing volume of the outer shell's sign is turned inside out, the signs the
// faces' own surfaces and pcurves give (`face_flux` of p / 3, the divergence theorem).
let shells = if kind == Kind::Voids && closed.iter().all(|c| *c) {
let volume = |s: &[(u32, bool)]| -> Option<f64> {
let mut sum = 0.0;
for &(f, forward) in s {
let face = self.faces.get(f as usize)?;
let on = face_forms.get(f as usize)?.as_ref().unwrap_or(&face.surface);
let eval = SurfaceEval::new(on).ok()?;
let edge_loops = face.loops.iter().filter_map(|l| match l {
LoopB::Edges(es) => Some(es),
LoopB::Vertex(_) => None,
});
let uses: Vec<Vec<(&Curve2, bool)>> =
edge_loops.zip(pcs.get(f as usize)?).map(|(es, pc)| pc.iter().zip(es).map(|(c, (_, fwd))| (c, *fwd)).collect()).collect();
let flux = face_flux(&eval, domains.get(f as usize)?[1][0], &uses, &|p| p * (1.0 / 3.0))?;
sum += if forward { flux } else { -flux };
}
sum.is_finite().then_some(sum)
};
let outside = shells.first().and_then(|s| volume(s));
shells
.into_iter()
.enumerate()
.map(|(k, s)| match (outside, k > 0) {
(Some(o), true) if volume(&s).is_some_and(|v| v * o > 0.0) => s.into_iter().map(|(f, fwd)| (f, !fwd)).collect(),
_ => s,
})
.collect()
} else {
shells
};
let mut regions = vec![Region { shells: Vec::new(), void: false }];
match kind {
Kind::Solid | Kind::Voids => {