use super::*;
use crate::{KernelStage, OrRefuse};
pub(super) const RULING_INTERIOR_STATIONS: [f64; 7] = [0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875];
pub(in crate::step_import) fn ruling_is_on_surface_at_interior_stations(
mut station_is_on_surface: impl FnMut(f64) -> bool,
) -> bool {
RULING_INTERIOR_STATIONS
.into_iter()
.all(&mut station_is_on_surface)
}
pub(in crate::step_import) fn prefer_exact_general_ruling(analytic: Option<&crate::AnalyticSurface>) -> bool {
matches!(
analytic,
None | Some(crate::AnalyticSurface::Revolution { .. })
)
}
pub(in crate::step_import) fn build_solid(resolver: &Resolver, manifold_ref: usize) -> Result<BrepSolid, String> {
let debug = std::env::var("BREP_DEBUG_STEP_LOOP").is_ok();
let started = debug.then(web_time::Instant::now);
if debug {
eprintln!("SOLID #{manifold_ref} BEGIN");
}
let result = build_solid_inner(resolver, manifold_ref, false)
.map(|(solid, _, _)| solid)
.map_err(|error| error.to_string());
if let Some(started) = started {
eprintln!(
"SOLID #{manifold_ref} END {:?} ok={}",
started.elapsed(),
result.is_ok()
);
}
result
}
fn closed_shell_face_refs(resolver: &Resolver, shell_ref: usize) -> Result<Vec<Value>, String> {
resolver
.get(shell_ref)?
.find("CLOSED_SHELL")
.and_then(|args| args.get(1))
.ok_or_else(|| format!("step_import: #{shell_ref} is not a CLOSED_SHELL"))?
.as_list()
.map(|faces| faces.to_vec())
}
fn build_shell_faces_in_namespace(
builder: &mut SolidBuilder<'_>,
face_refs: &[Value],
) -> Result<Vec<FaceRecord>, String> {
let mut pending = Vec::with_capacity(face_refs.len());
for face_value in face_refs {
let face_ref = face_value.as_ref_id()?;
pending.push(
builder
.collect_face(face_ref)
.map_err(|error| format!("step_import: face #{face_ref}: {error}"))?,
);
}
builder.relocate_periodic_rim_seams(&pending)?;
builder.split_seam_crossing_edges(&mut pending)?;
builder.split_off_plane_planar_faces(&mut pending)?;
builder.reconcile_edges_onto_surfaces(&pending)?;
let mut faces = Vec::with_capacity(pending.len());
for pending_face in pending {
let face_ref = pending_face.face_ref;
faces.push(
builder
.finish_face(pending_face)
.map_err(|error| format!("step_import: face #{face_ref}: {error}"))?,
);
}
builder.report_supplied_pcurves();
Ok(faces)
}
fn brep_with_voids_volume_tolerance(solid: &BrepSolid) -> f64 {
let mut lo = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
let mut hi = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
let mut coordinate_scale = 0.0f64;
for vertex in &solid.vertices {
lo = Vec3::new(
lo.x.min(vertex.point.x),
lo.y.min(vertex.point.y),
lo.z.min(vertex.point.z),
);
hi = Vec3::new(
hi.x.max(vertex.point.x),
hi.y.max(vertex.point.y),
hi.z.max(vertex.point.z),
);
coordinate_scale = coordinate_scale
.max(vertex.point.x.abs())
.max(vertex.point.y.abs())
.max(vertex.point.z.abs());
}
let extent = if solid.vertices.is_empty() {
0.0
} else {
hi.sub(lo).length()
};
let face_count = solid
.shells
.iter()
.map(|shell| shell.faces.len())
.sum::<usize>()
.max(1) as f64;
256.0 * f64::EPSILON * face_count * coordinate_scale.max(extent) * extent * extent
}
const BREP_WITH_VOIDS_INPUT: &str = "brep_with_voids";
fn brep_with_voids_input(message: String) -> KernelRefusal {
KernelRefusal::input(KernelStage::Validate, BREP_WITH_VOIDS_INPUT, message)
}
fn validation_refusal(issues: usize, message: String) -> KernelRefusal {
KernelRefusal::new(
crate::RefusalClass::InvalidResultTopology { issues: issues as u32 },
KernelStage::Validate,
message,
)
}
fn certify_brep_with_voids_topology(solid: &BrepSolid) -> Result<(), KernelRefusal> {
let edges: HashMap<u64, &EdgeRecord> = solid.edges.iter().map(|edge| (edge.id, edge)).collect();
let mut claimed_edges = HashSet::default();
let mut claimed_vertices = HashSet::default();
for (shell_index, shell) in solid.shells.iter().enumerate() {
let mut uses: HashMap<u64, Vec<bool>> = HashMap::default();
for face in &shell.faces {
for face_loop in &face.loops {
for coedge in &face_loop.coedges {
uses.entry(coedge.edge_id).or_default().push(coedge.forward);
}
}
}
let shell_edges: HashSet<u64> = uses.keys().copied().collect();
if shell_edges
.iter()
.any(|edge_id| claimed_edges.contains(edge_id))
{
return Err(brep_with_voids_input(format!(
"step_import: BREP_WITH_VOIDS shell {shell_index} shares an edge reference with another shell"
)));
}
let mut shell_vertices = HashSet::default();
for (edge_id, senses) in &uses {
let edge = edges.get(edge_id).ok_or_else(|| {
brep_with_voids_input(format!("step_import: BREP_WITH_VOIDS shell references missing edge {edge_id}"))
})?;
if !edge.degenerate && (senses.len() != 2 || senses[0] == senses[1]) {
return Err(brep_with_voids_input(format!(
"step_import: BREP_WITH_VOIDS shell {shell_index} edge {edge_id} has incidence {senses:?}"
)));
}
shell_vertices.insert(edge.start_vertex_id);
shell_vertices.insert(edge.end_vertex_id);
}
if shell_vertices
.iter()
.any(|vertex_id| claimed_vertices.contains(vertex_id))
{
return Err(brep_with_voids_input(format!(
"step_import: BREP_WITH_VOIDS shell {shell_index} shares a vertex reference with another shell"
)));
}
claimed_edges.extend(shell_edges);
claimed_vertices.extend(shell_vertices);
}
Ok(())
}
pub(in crate::step_import) fn certify_brep_with_voids_containment(solid: &BrepSolid) -> Result<(), KernelRefusal> {
let edge_map: HashMap<u64, &EdgeRecord> =
solid.edges.iter().map(|edge| (edge.id, edge)).collect();
let vertex_map: HashMap<u64, Vec3> = solid
.vertices
.iter()
.map(|vertex| (vertex.id, vertex.point))
.collect();
let shell_vertex_ids = |shell: &ShellRecord| -> Result<HashSet<u64>, String> {
let mut ids = HashSet::default();
for edge_id in shell
.faces
.iter()
.flat_map(|face| &face.loops)
.flat_map(|face_loop| &face_loop.coedges)
.map(|coedge| coedge.edge_id)
{
let edge = edge_map.get(&edge_id).ok_or_else(|| {
format!("step_import: BREP_WITH_VOIDS shell references missing edge {edge_id}")
})?;
ids.insert(edge.start_vertex_id);
ids.insert(edge.end_vertex_id);
}
Ok(ids)
};
let bounds = |ids: &HashSet<u64>| -> Result<(Vec3, Vec3), String> {
if ids.is_empty() {
return Err("step_import: BREP_WITH_VOIDS shell has no vertices".into());
}
let mut lo = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
let mut hi = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
for id in ids {
let point = vertex_map.get(id).ok_or_else(|| {
format!("step_import: BREP_WITH_VOIDS shell references missing vertex {id}")
})?;
lo = Vec3::new(lo.x.min(point.x), lo.y.min(point.y), lo.z.min(point.z));
hi = Vec3::new(hi.x.max(point.x), hi.y.max(point.y), hi.z.max(point.z));
}
Ok((lo, hi))
};
let outer_ids = shell_vertex_ids(&solid.shells[0]).map_err(brep_with_voids_input)?;
let (outer_lo, outer_hi) = bounds(&outer_ids).map_err(brep_with_voids_input)?;
let extent = outer_hi.sub(outer_lo).length();
let tolerance = (extent * 1e-9).max(1e-12);
let all_faces_planar = solid.shells.iter().all(|shell| {
shell.faces.iter().all(|face| {
matches!(
face.surface.analytic(),
Some(crate::AnalyticSurface::Plane { .. })
)
})
});
let all_edges_lines = solid.edges.iter().all(|edge| {
edge.degenerate || (edge.curve.degree == 1 && edge.curve.control_points.len() == 2)
});
let polyhedral = all_faces_planar && all_edges_lines;
if polyhedral {
for face in &solid.shells[0].faces {
let Some(crate::AnalyticSurface::Plane {
origin,
u_dir,
v_dir,
..
}) = face.surface.analytic()
else {
unreachable!("all faces planar in the polyhedral lane");
};
let mut outward = u_dir.cross(*v_dir).normalized().or_refuse(KernelStage::Validate, "brep_with_voids_normal")?;
if !face.same_sense {
outward = outward.scale(-1.0);
}
for vertex_id in &outer_ids {
let signed_distance = vertex_map[vertex_id].sub(*origin).dot(outward);
if signed_distance > tolerance {
return Err(brep_with_voids_input(format!(
"step_import: BREP_WITH_VOIDS outer shell is not convex at face {} vertex {vertex_id} (outside halfspace by {signed_distance})",
face.id
)));
}
}
}
}
let outer = BrepSolid {
mass_properties_cache: Default::default(),
id: solid.id,
vertices: solid.vertices.clone(),
edges: solid.edges.clone(),
shells: vec![solid.shells[0].clone()],
genus: 0,
kind: crate::BodyKind::Solid,
};
let outer_classifier = crate::SolidClassifier::new(&outer, tolerance).or_refuse(KernelStage::Validate, "brep_with_voids_classifier")?;
let mut void_bounds = Vec::new();
for (shell_index, shell) in solid.shells.iter().enumerate().skip(1) {
let ids = shell_vertex_ids(shell).map_err(brep_with_voids_input)?;
let (lo, hi) = bounds(&ids).map_err(brep_with_voids_input)?;
let bbox_clearances = [
lo.x - outer_lo.x,
outer_hi.x - hi.x,
lo.y - outer_lo.y,
outer_hi.y - hi.y,
lo.z - outer_lo.z,
outer_hi.z - hi.z,
];
if polyhedral {
let strictly_inside_box = lo.x > outer_lo.x + tolerance
&& lo.y > outer_lo.y + tolerance
&& lo.z > outer_lo.z + tolerance
&& hi.x < outer_hi.x - tolerance
&& hi.y < outer_hi.y - tolerance
&& hi.z < outer_hi.z - tolerance;
if !strictly_inside_box {
return Err(brep_with_voids_input(format!(
"step_import: BREP_WITH_VOIDS void {shell_index} is not strictly inside the outer bounds"
)));
}
}
for vertex_id in &ids {
let point = vertex_map[vertex_id];
let class = outer_classifier.classify(point).or_refuse(KernelStage::Validate, "brep_with_voids_classify")?.class;
if class != crate::PointClass::In {
let projection_distance = outer.shells[0]
.faces
.iter()
.filter_map(|face| {
crate::project_point_to_surface(&face.surface, point)
.ok()
.map(|projection| projection.distance)
})
.fold(f64::INFINITY, f64::min);
return Err(brep_with_voids_input(format!(
"step_import: BREP_WITH_VOIDS void {shell_index} vertex {vertex_id} at {point:?} is {class:?} relative to outer shell (classifier tolerance {tolerance}, nearest carrier projection {projection_distance}, bbox clearances {bbox_clearances:?})"
)));
}
}
if !polyhedral {
const VOID_EDGE_SAMPLES: usize = 5;
let mut sampled: HashSet<u64> = HashSet::default();
for face in &shell.faces {
for face_loop in &face.loops {
for coedge in &face_loop.coedges {
if !sampled.insert(coedge.edge_id) {
continue;
}
let edge = edge_map[&coedge.edge_id];
if edge.degenerate {
continue;
}
for step in 1..VOID_EDGE_SAMPLES {
let fraction = step as f64 / VOID_EDGE_SAMPLES as f64;
let t = edge.t0 + (edge.t1 - edge.t0) * fraction;
let Ok(point) = edge.curve.evaluate(t) else {
continue;
};
if outer_classifier.classify(point).or_refuse(KernelStage::Validate, "brep_with_voids_classify")?.class == crate::PointClass::Out {
return Err(brep_with_voids_input(format!(
"step_import: BREP_WITH_VOIDS void {shell_index} edge {} bows outside the outer shell",
coedge.edge_id
)));
}
}
}
}
}
}
void_bounds.push((lo, hi));
}
if polyhedral {
for first in 0..void_bounds.len() {
for second in (first + 1)..void_bounds.len() {
let (a_lo, a_hi) = void_bounds[first];
let (b_lo, b_hi) = void_bounds[second];
let separated = a_hi.x + tolerance < b_lo.x
|| b_hi.x + tolerance < a_lo.x
|| a_hi.y + tolerance < b_lo.y
|| b_hi.y + tolerance < a_lo.y
|| a_hi.z + tolerance < b_lo.z
|| b_hi.z + tolerance < a_lo.z;
if !separated {
return Err(brep_with_voids_input(format!(
"step_import: BREP_WITH_VOIDS void bounds {} and {} overlap",
first + 1,
second + 1
)));
}
}
}
}
Ok(())
}
pub(in crate::step_import) fn build_brep_with_voids(resolver: &Resolver, body_ref: usize) -> Result<BrepSolid, String> {
build_brep_with_voids_captured(resolver, body_ref, false)
.map(|(solid, _, _)| solid)
.map_err(|error| error.to_string())
}
pub(in crate::step_import) fn build_brep_with_voids_captured(
resolver: &Resolver,
body_ref: usize,
capture: bool,
) -> Result<(BrepSolid, Option<readings::TrimCapture>, readings::BodyReadings), StepBodyError> {
let args = resolver
.get(body_ref)?
.find("BREP_WITH_VOIDS")
.ok_or("step_import: body is not BREP_WITH_VOIDS")?;
let outer_ref = args
.get(1)
.ok_or("step_import: BREP_WITH_VOIDS missing outer shell")?
.as_ref_id()?;
let void_values = args
.get(2)
.ok_or("step_import: BREP_WITH_VOIDS missing void shell list")?
.as_list()?;
if void_values.is_empty() {
return Err(brep_with_voids_input("step_import: BREP_WITH_VOIDS has no void shells".into()).into());
}
let mut shell_specs = vec![(outer_ref, true)];
let mut shell_refs = HashSet::default();
shell_refs.insert(outer_ref);
for value in void_values {
let oriented_ref = value.as_ref_id()?;
let oriented = resolver
.get(oriented_ref)?
.find("ORIENTED_CLOSED_SHELL")
.ok_or_else(|| {
format!("step_import: void #{oriented_ref} is not ORIENTED_CLOSED_SHELL")
})?;
let base_ref = oriented
.get(2)
.ok_or("step_import: ORIENTED_CLOSED_SHELL missing base shell")?
.as_ref_id()?;
let orientation = oriented
.get(3)
.ok_or("step_import: ORIENTED_CLOSED_SHELL missing orientation")?;
let same_sense = if orientation.enum_is("T") {
true
} else if orientation.enum_is("F") {
false
} else {
return Err(brep_with_voids_input("step_import: ORIENTED_CLOSED_SHELL orientation is not .T./.F.".into()).into());
};
if !shell_refs.insert(base_ref) {
return Err(brep_with_voids_input(format!(
"step_import: BREP_WITH_VOIDS repeats CLOSED_SHELL #{base_ref}"
)).into());
}
shell_specs.push((base_ref, same_sense));
}
let mut all_face_refs = HashSet::default();
let mut face_lists = Vec::with_capacity(shell_specs.len());
for (shell_ref, _) in &shell_specs {
let faces = closed_shell_face_refs(resolver, *shell_ref)?;
for face in &faces {
let face_ref = face.as_ref_id()?;
if !all_face_refs.insert(face_ref) {
return Err(brep_with_voids_input(format!(
"step_import: BREP_WITH_VOIDS face #{face_ref} belongs to multiple shells"
)).into());
}
}
face_lists.push(faces);
}
let mut builder = SolidBuilder {
resolver,
next_id: 1,
vertices: Vec::new(),
edges: Vec::new(),
vertex_of_ref: HashMap::default(),
edge_of_ref: HashMap::default(),
edge_of_vertex_pair: HashMap::default(),
vertex_index: HashMap::default(),
edge_index: HashMap::default(),
curve_ref_of_edge: HashMap::default(),
supplied_report: SuppliedReport::default(),
bounded: Vec::new(),
readings: capture.then(readings::TrimCapture::default),
face_refs: HashMap::default(),
reconciled_edges: HashSet::default(),
};
let mut shells = Vec::with_capacity(face_lists.len());
for ((_, same_sense), faces) in shell_specs.iter().zip(&face_lists) {
let shell_faces = build_shell_faces_in_namespace(&mut builder, faces)?;
let mut shell = ShellRecord {
id: builder.fresh(),
faces: shell_faces,
};
if !same_sense {
crate::offset_shell::flip_shell_faces(&mut shell)?;
}
shells.push(shell);
}
let solid_id = builder.fresh();
let captured = builder.readings.take();
let bounded = std::mem::take(&mut builder.bounded);
let closure_context = builder.closure_context();
let mut solid = BrepSolid {
mass_properties_cache: Default::default(),
id: solid_id,
vertices: builder.vertices,
edges: builder.edges,
shells,
genus: 0,
kind: crate::BodyKind::Solid,
};
solid = finalize_imported_solid(solid)?;
solid.genus = euler_genus(&solid);
certify_brep_with_voids_topology(&solid)?;
let issues = solid.validate();
if !issues.is_empty() {
dump_invalid_solid_for_triage(&solid);
return Err(validation_refusal(
issues.len(),
format!("step_import: reconstructed BREP_WITH_VOIDS failed validation: {issues:?}"),
)
.into());
}
let tolerance = brep_with_voids_volume_tolerance(&solid);
let _caller = crate::mass_caller("step_import.brep_with_voids");
let mut shell_volumes = solid
.shells
.iter()
.map(crate::mass_properties::shell_signed_volume)
.collect::<Result<Vec<_>, _>>()?;
if shell_volumes
.iter()
.any(|volume| !volume.is_finite() || volume.abs() <= tolerance)
{
return Err(brep_with_voids_input(format!(
"step_import: BREP_WITH_VOIDS shell volume is below tolerance {tolerance}: {shell_volumes:?}"
)).into());
}
let authored_roles = shell_volumes[0] > tolerance
&& shell_volumes
.iter()
.skip(1)
.all(|volume| *volume < -tolerance);
let globally_reversed = shell_volumes[0] < -tolerance
&& shell_volumes
.iter()
.skip(1)
.all(|volume| *volume > tolerance);
if globally_reversed {
crate::offset_shell::flip_all_faces(&mut solid)?;
for volume in &mut shell_volumes {
*volume = -*volume;
}
} else if !authored_roles {
return Err(brep_with_voids_input(format!(
"step_import: BREP_WITH_VOIDS has mixed outer/void orientations {shell_volumes:?}"
)).into());
}
certify_brep_with_voids_containment(&solid)?;
let outer_volume = shell_volumes[0];
let net_volume = shell_volumes.iter().sum::<f64>();
if outer_volume <= tolerance || !net_volume.is_finite() || net_volume <= tolerance {
return Err(brep_with_voids_input(format!(
"step_import: BREP_WITH_VOIDS invalid outer/net volume {outer_volume}/{net_volume} (tolerance {tolerance})"
)).into());
}
solid.genus = euler_genus(&solid);
let closure = closure_context.certify_shell_closure(&solid, outer_ref, capture)?;
Ok((solid, captured, readings::BodyReadings { bounded, closure }))
}
pub(in crate::step_import) fn build_solid_inner(
resolver: &Resolver,
manifold_ref: usize,
capture: bool,
) -> Result<(BrepSolid, Option<readings::TrimCapture>, readings::BodyReadings), StepBodyError> {
let entity = resolver.get(manifold_ref)?;
let shell_ref = entity
.find("MANIFOLD_SOLID_BREP")
.or_else(|| entity.find("FACETED_BREP"))
.and_then(|args| args.get(1))
.ok_or("step_import: solid brep missing shell")?
.as_ref_id()?;
build_solid_from_shell_captured(resolver, shell_ref, capture)
}
pub(in crate::step_import) fn build_solid_from_shell(resolver: &Resolver, shell_ref: usize) -> Result<BrepSolid, String> {
build_solid_from_shell_captured(resolver, shell_ref, false)
.map(|(solid, _, _)| solid)
.map_err(|error| error.to_string())
}
pub(in crate::step_import) fn build_solid_from_shell_captured(
resolver: &Resolver,
shell_ref: usize,
capture: bool,
) -> Result<(BrepSolid, Option<readings::TrimCapture>, readings::BodyReadings), StepBodyError> {
let shell_entity = resolver.get(shell_ref)?;
let face_refs = shell_entity
.find("CLOSED_SHELL")
.or_else(|| shell_entity.find("OPEN_SHELL"))
.and_then(|args| args.get(1))
.ok_or("step_import: shell is not CLOSED_SHELL/OPEN_SHELL")?
.as_list()?
.to_vec();
let mut builder = SolidBuilder {
resolver,
next_id: 1,
vertices: Vec::new(),
edges: Vec::new(),
vertex_of_ref: HashMap::default(),
edge_of_ref: HashMap::default(),
edge_of_vertex_pair: HashMap::default(),
vertex_index: HashMap::default(),
edge_index: HashMap::default(),
curve_ref_of_edge: HashMap::default(),
supplied_report: SuppliedReport::default(),
bounded: Vec::new(),
readings: capture.then(readings::TrimCapture::default),
face_refs: HashMap::default(),
reconciled_edges: HashSet::default(),
};
let profile = std::env::var("BREP_PROFILE").is_ok();
let mut lap_started = web_time::Instant::now();
let mut laps: Vec<(&str, f64)> = Vec::new();
let mut lap = |name: &'static str, started: &mut web_time::Instant, laps: &mut Vec<(&str, f64)>| {
if profile {
let now = web_time::Instant::now();
laps.push((name, (now - *started).as_secs_f64() * 1_000.0));
*started = now;
}
};
let mut pending = Vec::with_capacity(face_refs.len());
for face_value in &face_refs {
let face_ref = face_value.as_ref_id()?;
pending.push(
builder
.collect_face(face_ref)
.map_err(|error| format!("step_import: face #{face_ref}: {error}"))?,
);
}
lap("collect_faces", &mut lap_started, &mut laps);
builder.relocate_periodic_rim_seams(&pending)?;
builder.split_seam_crossing_edges(&mut pending)?;
lap("seams", &mut lap_started, &mut laps);
builder.split_off_plane_planar_faces(&mut pending)?;
lap("planar_split", &mut lap_started, &mut laps);
builder.reconcile_edges_onto_surfaces(&pending)?;
lap("reconcile", &mut lap_started, &mut laps);
let mut faces = Vec::with_capacity(pending.len());
for pending_face in pending {
let face_ref = pending_face.face_ref;
faces.push(
builder
.finish_face(pending_face)
.map_err(|error| format!("step_import: face #{face_ref}: {error}"))?,
);
}
lap("finish_faces", &mut lap_started, &mut laps);
builder.report_supplied_pcurves();
let shell_id = builder.fresh();
let solid_id = builder.fresh();
let captured = builder.readings.take();
let bounded = std::mem::take(&mut builder.bounded);
let closure_context = builder.closure_context();
let mut solid = BrepSolid {
mass_properties_cache: Default::default(),
id: solid_id,
vertices: builder.vertices,
edges: builder.edges,
shells: vec![ShellRecord {
id: shell_id,
faces,
}],
genus: 0,
kind: crate::BodyKind::Solid,
};
solid = finalize_imported_solid(solid)?;
solid.genus = euler_genus(&solid);
lap("finalize", &mut lap_started, &mut laps);
let issues = solid.validate();
lap("validate", &mut lap_started, &mut laps);
if !issues.is_empty() {
dump_invalid_solid_for_triage(&solid);
return Err(validation_refusal(
issues.len(),
format!("step_import: reconstructed solid failed validation: {:?}", issues),
)
.into());
}
let _caller = crate::mass_caller("step_import.face_sense_flip");
if let Ok(volume) = crate::solid_signed_volume(&solid) {
if volume < 0.0 {
crate::offset_shell::flip_all_faces(&mut solid)?;
}
}
lap("signed_volume", &mut lap_started, &mut laps);
let closure = closure_context.certify_shell_closure(&solid, shell_ref, capture)?;
lap("closure", &mut lap_started, &mut laps);
if profile {
let line: Vec<String> = laps.iter().map(|(name, ms)| format!("{name}={ms:.2}")).collect();
eprintln!("step_import.profile shell=#{shell_ref} faces={} {}", face_refs.len(), line.join(" "));
}
Ok((solid, captured, readings::BodyReadings { bounded, closure }))
}
fn dump_invalid_solid_for_triage(solid: &BrepSolid) {
use std::sync::atomic::{AtomicBool, Ordering};
static DUMPED: AtomicBool = AtomicBool::new(false);
let Ok(path) = std::env::var("BREP_STEP_DUMP_INVALID") else {
return;
};
if DUMPED.swap(true, Ordering::Relaxed) {
return;
}
if let Ok(json) = serde_json::to_string(solid) {
let _ = std::fs::write(&path, json);
}
}
fn finalize_imported_solid(mut solid: BrepSolid) -> Result<BrepSolid, StepBodyError> {
if crate::split_pinched_vertices(&mut solid)? > 0 {
solid.genus = euler_genus(&solid);
}
Ok(solid)
}