use super::*;
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);
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.reconcile_edges_onto_surfaces(&pending)?;
let mut faces = Vec::with_capacity(pending.len());
for (pending_face, face_value) in pending.into_iter().zip(face_refs) {
let face_ref = face_value.as_ref_id()?;
faces.push(
builder
.finish_face(pending_face)
.map_err(|error| format!("step_import: face #{face_ref}: {error}"))?,
);
}
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
}
fn certify_brep_with_voids_topology(solid: &BrepSolid) -> Result<(), String> {
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(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(|| {
format!("step_import: BREP_WITH_VOIDS shell references missing edge {edge_id}")
})?;
if !edge.degenerate && (senses.len() != 2 || senses[0] == senses[1]) {
return Err(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(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<(), String> {
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])?;
let (outer_lo, outer_hi) = bounds(&outer_ids)?;
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()?;
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(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 {
id: solid.id,
vertices: solid.vertices.clone(),
edges: solid.edges.clone(),
shells: vec![solid.shells[0].clone()],
genus: 0,
};
let outer_classifier = crate::SolidClassifier::new(&outer, tolerance)?;
let mut void_bounds = Vec::new();
for (shell_index, shell) in solid.shells.iter().enumerate().skip(1) {
let ids = shell_vertex_ids(shell)?;
let (lo, hi) = bounds(&ids)?;
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(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)?.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(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)?.class == crate::PointClass::Out {
return Err(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(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> {
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("step_import: BREP_WITH_VOIDS has no void shells".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("step_import: ORIENTED_CLOSED_SHELL orientation is not .T./.F.".into());
};
if !shell_refs.insert(base_ref) {
return Err(format!(
"step_import: BREP_WITH_VOIDS repeats CLOSED_SHELL #{base_ref}"
));
}
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(format!(
"step_import: BREP_WITH_VOIDS face #{face_ref} belongs to multiple shells"
));
}
}
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(),
};
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 mut solid = BrepSolid {
id: solid_id,
vertices: builder.vertices,
edges: builder.edges,
shells,
genus: 0,
};
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(format!(
"step_import: reconstructed BREP_WITH_VOIDS failed validation: {issues:?}"
));
}
let tolerance = brep_with_voids_volume_tolerance(&solid);
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(format!(
"step_import: BREP_WITH_VOIDS shell volume is below tolerance {tolerance}: {shell_volumes:?}"
));
}
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(format!(
"step_import: BREP_WITH_VOIDS has mixed outer/void orientations {shell_volumes:?}"
));
}
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(format!(
"step_import: BREP_WITH_VOIDS invalid outer/net volume {outer_volume}/{net_volume} (tolerance {tolerance})"
));
}
solid.genus = euler_genus(&solid);
Ok(solid)
}
fn build_solid_inner(resolver: &Resolver, manifold_ref: usize) -> Result<BrepSolid, String> {
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(resolver, shell_ref)
}
pub(in crate::step_import) fn build_solid_from_shell(resolver: &Resolver, shell_ref: usize) -> Result<BrepSolid, String> {
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(),
};
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.reconcile_edges_onto_surfaces(&pending)?;
let mut faces = Vec::with_capacity(pending.len());
for (pending_face, face_value) in pending.into_iter().zip(&face_refs) {
let face_ref = face_value.as_ref_id()?;
faces.push(
builder
.finish_face(pending_face)
.map_err(|error| format!("step_import: face #{face_ref}: {error}"))?,
);
}
let shell_id = builder.fresh();
let solid_id = builder.fresh();
let mut solid = BrepSolid {
id: solid_id,
vertices: builder.vertices,
edges: builder.edges,
shells: vec![ShellRecord {
id: shell_id,
faces,
}],
genus: 0,
};
solid = finalize_imported_solid(solid)?;
solid.genus = euler_genus(&solid);
let issues = solid.validate();
if !issues.is_empty() {
dump_invalid_solid_for_triage(&solid);
return Err(format!(
"step_import: reconstructed solid failed validation: {:?}",
issues
));
}
if let Ok(volume) = crate::solid_signed_volume(&solid) {
if volume < 0.0 {
crate::offset_shell::flip_all_faces(&mut solid)?;
}
}
Ok(solid)
}
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, String> {
if crate::split_pinched_vertices(&mut solid)? > 0 {
solid.genus = euler_genus(&solid);
}
Ok(solid)
}