mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user