mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-12 11:41:56 +00:00
STEP export: find a region's outer shell by its mates, not list order (#15168)
A region's shell list puts the outer shell first only with the opt-in boolean_outer_shell_first fix; on the default kernel a union that seals a void leaves the cavity's shell first, and the file's outer shell was the cavity, inside out, with the cube as its void. brep_to_model now walks the region tree from the infinite region through the shells' mates: a region's outer shell is the one mated to the region it was reached from. The tests now check each shell's own signed volume (outer positive, each void negative after its turn) and add the cup and lid on the default kernel, in both operand orders (488000). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SdcgEtwSrdwZxqTQp5MfnU
This commit is contained in:
+55
-5
@@ -287,6 +287,44 @@ fn reverses_normal(surface: &solidcore::surface::surface::Surface) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Each region's outer shell, the one that bounds it from outside: the region tree is
|
||||
/// walked from the infinite region through the shells' mates, and a region's outer shell
|
||||
/// is the one mated to the region it was reached from (the region enclosing it). Its other
|
||||
/// shells bound regions nested inside it. The kernel's own list order says nothing here:
|
||||
/// a union that seals a void can leave the void's shell first.
|
||||
fn outer_shells(
|
||||
brep: &Brep,
|
||||
) -> Result<BTreeMap<solidcore::topology::RegionId, solidcore::topology::ShellId>, ExportModelError>
|
||||
{
|
||||
let mut kernel = Kernel::default();
|
||||
let mut outer = BTreeMap::new();
|
||||
let Some(infinite) = brep.infinite_region() else {
|
||||
return Ok(outer);
|
||||
};
|
||||
let mut reached = vec![infinite];
|
||||
let mut next = 0;
|
||||
while let Some(®ion_id) = reached.get(next) {
|
||||
next += 1;
|
||||
let region = brep.region(region_id).map_err(|e| err(e.to_string()))?;
|
||||
for shell_id in ®ion.shells {
|
||||
let shell = brep.shell(*shell_id).map_err(|e| err(e.to_string()))?;
|
||||
for mate in shell
|
||||
.mated_shells(&mut kernel, brep)
|
||||
.map_err(|e| err(e.to_string()))?
|
||||
{
|
||||
let Some(beyond) = brep.shell(mate).map_err(|e| err(e.to_string()))?.region else {
|
||||
continue;
|
||||
};
|
||||
if !reached.contains(&beyond) {
|
||||
reached.push(beyond);
|
||||
outer.insert(beyond, mate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(outer)
|
||||
}
|
||||
|
||||
/// Maps one B-rep to the neutral STEP model.
|
||||
pub fn brep_to_model(brep: &Brep) -> Result<Model, ExportModelError> {
|
||||
let vertex_ids = brep.vertex_list().to_vec();
|
||||
@@ -351,13 +389,13 @@ pub fn brep_to_model(brep: &Brep) -> Result<Model, ExportModelError> {
|
||||
// ISO 10303-42 wants a solid's faces written from outside its material: each face's
|
||||
// normal (the surface's, reversed where `same_sense` is false) pointing out of the
|
||||
// material, and its loops anticlockwise about that normal. A faceuse in a shell of a
|
||||
// material region faces into that region. A region's first shell is its outer one (the
|
||||
// kernel keeps it first); each face of it is written from its mate, the faceuse on the
|
||||
// outer side. Its other shells bound voids, each written as a BREP_WITH_VOIDS void: a
|
||||
// material region faces into that region. A region's outer shell ([`outer_shells`]):
|
||||
// each face of it is written from its mate, the faceuse on the outer side. Its other shells bound voids, each written as a BREP_WITH_VOIDS void: a
|
||||
// closed shell whose faces point out of the void's own volume (the material faceuses
|
||||
// themselves, pointing into the material), under an ORIENTED_CLOSED_SHELL of
|
||||
// orientation .F., which turns them out of the material.
|
||||
let mut written_faceuses = BTreeMap::new();
|
||||
let outer_of = outer_shells(brep)?;
|
||||
// each material region's shells, outer first
|
||||
let mut solids: Vec<Vec<solidcore::topology::ShellId>> = Vec::new();
|
||||
for region_id in brep.regions().ids() {
|
||||
@@ -365,7 +403,19 @@ pub fn brep_to_model(brep: &Brep) -> Result<Model, ExportModelError> {
|
||||
if region.is_void() != 0 {
|
||||
continue;
|
||||
}
|
||||
for (position, shell_id) in region.shells.iter().enumerate() {
|
||||
let mut shells = region.shells.clone();
|
||||
if shells.len() > 1 {
|
||||
let outer = *outer_of.get(®ion_id).ok_or_else(|| {
|
||||
err("a solid region with voids is not reached from the infinite region")
|
||||
})?;
|
||||
if !shells.contains(&outer) {
|
||||
return Err(err("a region's outer shell is not one of its shells"));
|
||||
}
|
||||
shells = std::iter::once(outer)
|
||||
.chain(shells.into_iter().filter(|s| *s != outer))
|
||||
.collect();
|
||||
}
|
||||
for (position, shell_id) in shells.iter().enumerate() {
|
||||
let shell = brep.shell(*shell_id).map_err(|e| err(e.to_string()))?;
|
||||
for faceuse_id in shell.get_faceuses().map_err(|e| err(e.to_string()))? {
|
||||
let faceuse = brep.faceuse(faceuse_id).map_err(|e| err(e.to_string()))?;
|
||||
@@ -381,7 +431,7 @@ pub fn brep_to_model(brep: &Brep) -> Result<Model, ExportModelError> {
|
||||
}
|
||||
}
|
||||
}
|
||||
solids.push(region.shells.clone());
|
||||
solids.push(shells);
|
||||
}
|
||||
for id in brep.face_list() {
|
||||
let face = brep.face(*id).map_err(|e| err(e.to_string()))?;
|
||||
|
||||
+54
-4
@@ -557,6 +557,8 @@ struct FaceMeasure {
|
||||
same_sense: bool,
|
||||
/// On a void's shell (turned over by its `ORIENTED_CLOSED_SHELL`).
|
||||
void: bool,
|
||||
/// Its shell's place in the solid: 0 the outer, then the voids.
|
||||
shell: usize,
|
||||
/// Its loops alone don't say which side the face is (a seam loop round a closed
|
||||
/// surface traces its whole (u, v) box either way): the side is the one `same_sense`
|
||||
/// gives, as ISO readers settle it.
|
||||
@@ -722,17 +724,18 @@ fn measure(text: &str) -> Vec<FaceMeasure> {
|
||||
shells.push((rid(&oriented[2]), sign));
|
||||
}
|
||||
}
|
||||
let faces: Vec<(u64, f64)> = shells
|
||||
let faces: Vec<(u64, f64, usize)> = shells
|
||||
.iter()
|
||||
.flat_map(|&(shell, sign)| {
|
||||
.enumerate()
|
||||
.flat_map(|(index, &(shell, sign))| {
|
||||
list(&file.params(shell, "CLOSED_SHELL")[1])
|
||||
.iter()
|
||||
.map(move |face| (rid(face), sign))
|
||||
.map(move |face| (rid(face), sign, index))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect();
|
||||
let mut out = Vec::new();
|
||||
for (face, shell_sign) in faces {
|
||||
for (face, shell_sign, shell) in faces {
|
||||
let p = file.params(face, "ADVANCED_FACE");
|
||||
let surface_id = rid(&p[2]);
|
||||
let same_sense = flag(&p[3]);
|
||||
@@ -829,6 +832,7 @@ fn measure(text: &str) -> Vec<FaceMeasure> {
|
||||
kind,
|
||||
same_sense,
|
||||
void: shell_sign < 0.0,
|
||||
shell,
|
||||
settled_by_sense,
|
||||
by_loops: shell_sign * by_loops / 3.0,
|
||||
by_sense: shell_sign * by_sense / 3.0,
|
||||
@@ -877,6 +881,24 @@ fn assert_right_side_out(name: &str, brep: &Brep, expected: f64, rel: f64) {
|
||||
silent.is_empty(),
|
||||
"{name}: faces that add nothing: {silent:?}"
|
||||
);
|
||||
// each shell bounds a volume of its own the right way round: the outer shell's is
|
||||
// positive, each void's negative once its ORIENTED_CLOSED_SHELL turns it over
|
||||
let shells = faces.iter().map(|f| f.shell).max().map_or(0, |m| m + 1);
|
||||
for shell in 0..shells {
|
||||
let of = |by: fn(&FaceMeasure) -> f64| {
|
||||
faces
|
||||
.iter()
|
||||
.filter(|f| f.shell == shell)
|
||||
.map(by)
|
||||
.sum::<f64>()
|
||||
};
|
||||
let (loops, sense) = (of(|f| f.by_loops), of(|f| f.by_sense));
|
||||
assert!(
|
||||
(shell == 0) == (loops > 0.0) && (shell == 0) == (sense > 0.0),
|
||||
"{name}: shell {shell} ({}) bounds {loops} by loops, {sense} by same_sense",
|
||||
if shell == 0 { "outer" } else { "void" }
|
||||
);
|
||||
}
|
||||
let inconsistent: Vec<_> = faces
|
||||
.iter()
|
||||
.filter(|f| (f.by_loops - f.by_sense).abs() > rel * scale)
|
||||
@@ -961,6 +983,34 @@ fn a_lofted_b_spline_solid_is_written_right_side_out() {
|
||||
assert_right_side_out("loft", &skin.brep, 20.0 + 4.0 * 3.0_f64.sqrt(), 1e-9);
|
||||
}
|
||||
|
||||
/// The cup and lid: a 100-cube hollowed to an open cup (walls and floor 10), joined with a
|
||||
/// lid over its top 10, which seals an 80-cube cavity, 1000000 − 512000. On the default
|
||||
/// kernel the union leaves the cavity's shell first in the region's list; the file's outer
|
||||
/// shell is still the cube's.
|
||||
#[test]
|
||||
fn a_cavity_sealed_by_a_union_is_written_as_a_void() {
|
||||
for lid_first in [false, true] {
|
||||
let mut kernel = Kernel::default();
|
||||
let cube =
|
||||
api::create_box(&mut kernel, Point3::new(3.0, 4.0, 5.0), 100.0, 100.0, 100.0).unwrap();
|
||||
let pocket = api::create_box(
|
||||
&mut kernel,
|
||||
Point3::new(13.0, 14.0, 15.0),
|
||||
80.0,
|
||||
80.0,
|
||||
100.0,
|
||||
)
|
||||
.unwrap();
|
||||
let lid =
|
||||
api::create_box(&mut kernel, Point3::new(3.0, 4.0, 95.0), 100.0, 100.0, 10.0).unwrap();
|
||||
let cup = boolean_difference(&mut kernel, cube, pocket).unwrap();
|
||||
let (a, b) = if lid_first { (lid, cup) } else { (cup, lid) };
|
||||
let solid = boolean_union(&mut kernel, a, b).unwrap();
|
||||
let name = format!("cup and lid (lid first: {lid_first})");
|
||||
assert_right_side_out(&name, &solid, 488_000.0, 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
/// A 10-cube with a 3-cube cavity: one `BREP_WITH_VOIDS`, the cavity a void (its shell
|
||||
/// under an `ORIENTED_CLOSED_SHELL` turning it over), 1000 − 27.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user