use super::GeometryRouter;
use crate::csg::{ClippingProcessor, Plane, Triangle, TriangleVec};
use crate::mesh::{SubMesh, SubMeshCollection};
use crate::{Error, Mesh, Point3, Result, TessellationQuality, Vector3};
use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcType};
use nalgebra::Matrix4;
use rustc_hash::{FxHashMap, FxHashSet};
const NORMALIZE_EPSILON: f64 = 1e-12;
const MIN_OPENING_VOLUME: f64 = 0.0001;
const CSG_TRIANGLE_RETENTION_DIVISOR: usize = 4;
const MIN_VALID_TRIANGLES: usize = 4;
const MAX_EXTRUSION_EXTRACT_DEPTH: usize = 32;
fn extract_rotation_columns(m: &Matrix4<f64>) -> (Vector3<f64>, Vector3<f64>, Vector3<f64>) {
(
Vector3::new(m[(0, 0)], m[(1, 0)], m[(2, 0)]),
Vector3::new(m[(0, 1)], m[(1, 1)], m[(2, 1)]),
Vector3::new(m[(0, 2)], m[(1, 2)], m[(2, 2)]),
)
}
fn rotate_and_normalize(
rot: &(Vector3<f64>, Vector3<f64>, Vector3<f64>),
dir: &Vector3<f64>,
) -> Result<Vector3<f64>> {
(rot.0 * dir.x + rot.1 * dir.y + rot.2 * dir.z)
.try_normalize(NORMALIZE_EPSILON)
.ok_or_else(|| Error::geometry("Zero-length direction vector".to_string()))
}
#[inline]
fn determine_extrusion_axis(
extrusion_dir: Option<&Vector3<f64>>,
wall_min: &Point3<f64>,
wall_max: &Point3<f64>,
) -> usize {
if let Some(dir) = extrusion_dir {
let ax = dir.x.abs();
let ay = dir.y.abs();
let az = dir.z.abs();
if ax >= ay && ax >= az {
0
} else if ay >= az {
1
} else {
2
}
} else {
let dx = (wall_max.x - wall_min.x).abs();
let dy = (wall_max.y - wall_min.y).abs();
let dz = (wall_max.z - wall_min.z).abs();
if dx <= dy && dx <= dz {
0
} else if dy <= dz {
1
} else {
2
}
}
}
#[inline(always)]
fn axis_val(p: &Point3<f64>, axis: usize) -> f64 {
match axis {
0 => p.x,
1 => p.y,
_ => p.z,
}
}
#[inline(always)]
fn point_from_axes(a: usize, va: f64, b: usize, vb: f64, c: usize, vc: f64) -> Point3<f64> {
let mut coords = [0.0_f64; 3];
coords[a] = va;
coords[b] = vb;
coords[c] = vc;
Point3::new(coords[0], coords[1], coords[2])
}
#[inline(always)]
fn vec_along_axis(axis: usize, sign: f64) -> Vector3<f64> {
let mut coords = [0.0_f64; 3];
coords[axis] = sign;
Vector3::new(coords[0], coords[1], coords[2])
}
#[inline]
fn add_reveal_quad(
mesh: &mut Mesh,
p0: Point3<f64>,
p1: Point3<f64>,
p2: Point3<f64>,
p3: Point3<f64>,
desired_normal: Vector3<f64>,
) {
let edge1 = p1 - p0;
let edge2 = p2 - p0;
let computed = edge1.cross(&edge2);
let base = mesh.vertex_count() as u32;
mesh.add_vertex(p0, desired_normal);
mesh.add_vertex(p1, desired_normal);
mesh.add_vertex(p2, desired_normal);
mesh.add_vertex(p3, desired_normal);
if computed.dot(&desired_normal) >= 0.0 {
mesh.add_triangle(base, base + 1, base + 2);
mesh.add_triangle(base, base + 2, base + 3);
} else {
mesh.add_triangle(base, base + 2, base + 1);
mesh.add_triangle(base, base + 3, base + 2);
}
}
fn generate_reveal_quads(
mesh: &mut Mesh,
open_min: &Point3<f64>,
open_max: &Point3<f64>,
wall_min: &Point3<f64>,
wall_max: &Point3<f64>,
extrusion_dir: Option<&Vector3<f64>>,
) {
let ea = determine_extrusion_axis(extrusion_dir, wall_min, wall_max);
let d_min = axis_val(wall_min, ea).max(axis_val(open_min, ea));
let d_max = axis_val(wall_max, ea).min(axis_val(open_max, ea));
if d_max - d_min < 1e-4 {
return; }
let cross: [usize; 2] = match ea {
0 => [1, 2],
1 => [0, 2],
_ => [0, 1],
};
for &ax in &cross {
let ov_min = axis_val(open_min, ax).max(axis_val(wall_min, ax));
let ov_max = axis_val(open_max, ax).min(axis_val(wall_max, ax));
if ov_max - ov_min < 1e-4 {
return;
}
}
for (i, &ca) in cross.iter().enumerate() {
let oa = cross[1 - i]; let o_min = axis_val(open_min, oa).max(axis_val(wall_min, oa));
let o_max = axis_val(open_max, oa).min(axis_val(wall_max, oa));
let face_lo = axis_val(open_min, ca);
if face_lo > axis_val(wall_min, ca) + 1e-4 {
add_reveal_quad(
mesh,
point_from_axes(ea, d_min, ca, face_lo, oa, o_min),
point_from_axes(ea, d_max, ca, face_lo, oa, o_min),
point_from_axes(ea, d_max, ca, face_lo, oa, o_max),
point_from_axes(ea, d_min, ca, face_lo, oa, o_max),
vec_along_axis(ca, 1.0),
);
}
let face_hi = axis_val(open_max, ca);
if face_hi < axis_val(wall_max, ca) - 1e-4 {
add_reveal_quad(
mesh,
point_from_axes(ea, d_min, ca, face_hi, oa, o_max),
point_from_axes(ea, d_max, ca, face_hi, oa, o_max),
point_from_axes(ea, d_max, ca, face_hi, oa, o_min),
point_from_axes(ea, d_min, ca, face_hi, oa, o_min),
vec_along_axis(ca, -1.0),
);
}
}
}
fn generate_recess_cap(
mesh: &mut Mesh,
open_min: &Point3<f64>,
open_max: &Point3<f64>,
wall_min: &Point3<f64>,
wall_max: &Point3<f64>,
extrusion_dir: Option<&Vector3<f64>>,
) {
let ea = determine_extrusion_axis(extrusion_dir, wall_min, wall_max);
let host_extent = (axis_val(wall_max, ea) - axis_val(wall_min, ea)).abs();
let face_align_tol = host_extent * 1e-5;
let open_lo = axis_val(open_min, ea);
let open_hi = axis_val(open_max, ea);
let wall_lo = axis_val(wall_min, ea);
let wall_hi = axis_val(wall_max, ea);
let near_at_min_face = (open_lo - wall_lo).abs() < face_align_tol;
let near_at_max_face = (open_hi - wall_hi).abs() < face_align_tol;
let far_inside_max = open_hi < wall_hi - face_align_tol;
let far_inside_min = open_lo > wall_lo + face_align_tol;
let (cap_value, normal_sign) = if near_at_min_face && far_inside_max {
(open_hi, -1.0)
} else if near_at_max_face && far_inside_min {
(open_lo, 1.0)
} else {
return;
};
let cross: [usize; 2] = match ea {
0 => [1, 2],
1 => [0, 2],
_ => [0, 1],
};
let c0 = cross[0];
let c1 = cross[1];
let c0_lo = axis_val(open_min, c0).max(axis_val(wall_min, c0));
let c0_hi = axis_val(open_max, c0).min(axis_val(wall_max, c0));
let c1_lo = axis_val(open_min, c1).max(axis_val(wall_min, c1));
let c1_hi = axis_val(open_max, c1).min(axis_val(wall_max, c1));
if c0_hi - c0_lo < 1e-4 || c1_hi - c1_lo < 1e-4 {
return;
}
let (a, b, c, d) = if normal_sign > 0.0 {
(
point_from_axes(ea, cap_value, c0, c0_lo, c1, c1_lo),
point_from_axes(ea, cap_value, c0, c0_hi, c1, c1_lo),
point_from_axes(ea, cap_value, c0, c0_hi, c1, c1_hi),
point_from_axes(ea, cap_value, c0, c0_lo, c1, c1_hi),
)
} else {
(
point_from_axes(ea, cap_value, c0, c0_lo, c1, c1_lo),
point_from_axes(ea, cap_value, c0, c0_lo, c1, c1_hi),
point_from_axes(ea, cap_value, c0, c0_hi, c1, c1_hi),
point_from_axes(ea, cap_value, c0, c0_hi, c1, c1_lo),
)
};
add_reveal_quad(mesh, a, b, c, d, vec_along_axis(ea, normal_sign));
}
fn is_body_representation(rep_type: &str) -> bool {
matches!(
rep_type,
"Body"
| "SweptSolid"
| "Brep"
| "CSG"
| "Clipping"
| "Tessellation"
| "MappedRepresentation"
| "SolidModel"
| "SurfaceModel"
| "AdvancedSweptSolid"
| "AdvancedBrep"
)
}
#[inline]
fn wall_thinnest_axis_dir(wall_min: &Point3<f64>, wall_max: &Point3<f64>) -> Vector3<f64> {
let ext = [
(wall_max.x - wall_min.x).abs(),
(wall_max.y - wall_min.y).abs(),
(wall_max.z - wall_min.z).abs(),
];
let mut axis = 0;
for i in 1..3 {
if ext[i] < ext[axis] {
axis = i;
}
}
match axis {
0 => Vector3::new(1.0, 0.0, 0.0),
1 => Vector3::new(0.0, 1.0, 0.0),
_ => Vector3::new(0.0, 0.0, 1.0),
}
}
#[derive(Clone)]
enum OpeningType {
Rectangular(Point3<f64>, Point3<f64>, Option<Vector3<f64>>),
DiagonalRectangular(Mesh, OpeningFrame),
NonRectangular(Mesh, Point3<f64>, Point3<f64>, Option<Vector3<f64>>),
}
#[derive(Clone, Copy)]
struct OpeningFrame {
depth: Vector3<f64>,
cross_a: Vector3<f64>,
cross_b: Vector3<f64>,
}
impl OpeningFrame {
fn from_depth(depth: Vector3<f64>) -> Option<Self> {
let depth = depth.try_normalize(NORMALIZE_EPSILON)?;
let seed = if depth.z.abs() < 0.9 {
Vector3::new(0.0, 0.0, 1.0)
} else {
Vector3::new(0.0, 1.0, 0.0)
};
let cross_a = seed.cross(&depth).try_normalize(NORMALIZE_EPSILON)?;
let cross_b = depth.cross(&cross_a).try_normalize(NORMALIZE_EPSILON)?;
Some(Self {
depth,
cross_a,
cross_b,
})
}
#[inline]
fn to_local_point(&self, p: Point3<f64>) -> Point3<f64> {
let v = p.coords;
Point3::new(
v.dot(&self.depth),
v.dot(&self.cross_a),
v.dot(&self.cross_b),
)
}
#[inline]
fn to_world_point(&self, p: Point3<f64>) -> Point3<f64> {
let v = self.depth * p.x + self.cross_a * p.y + self.cross_b * p.z;
Point3::new(v.x, v.y, v.z)
}
#[inline]
fn to_local_vector(&self, v: Vector3<f64>) -> Vector3<f64> {
Vector3::new(
v.dot(&self.depth),
v.dot(&self.cross_a),
v.dot(&self.cross_b),
)
}
#[inline]
fn to_world_vector(&self, v: Vector3<f64>) -> Vector3<f64> {
self.depth * v.x + self.cross_a * v.y + self.cross_b * v.z
}
fn is_axis_aligned(&self) -> bool {
is_axis_aligned_direction(&self.depth)
&& is_axis_aligned_direction(&self.cross_a)
&& is_axis_aligned_direction(&self.cross_b)
}
}
#[inline]
fn is_axis_aligned_direction(dir: &Vector3<f64>) -> bool {
const AXIS_THRESHOLD: f64 = 0.95;
dir.x.abs().max(dir.y.abs()).max(dir.z.abs()) > AXIS_THRESHOLD
}
#[inline]
fn mesh_point(mesh: &Mesh, index: u32) -> Option<Point3<f64>> {
let base = index as usize * 3;
Some(Point3::new(
*mesh.positions.get(base)? as f64,
*mesh.positions.get(base + 1)? as f64,
*mesh.positions.get(base + 2)? as f64,
))
}
fn ray_triangle_param(
origin: Point3<f64>,
dir: &Vector3<f64>,
a: Point3<f64>,
b: Point3<f64>,
c: Point3<f64>,
) -> Option<f64> {
const EPS: f64 = 1e-9;
let e1 = b - a;
let e2 = c - a;
let pvec = dir.cross(&e2);
let det = e1.dot(&pvec);
if det.abs() < EPS {
return None; }
let inv_det = 1.0 / det;
let tvec = origin - a;
let u = tvec.dot(&pvec) * inv_det;
if !(-EPS..=1.0 + EPS).contains(&u) {
return None;
}
let qvec = tvec.cross(&e1);
let v = dir.dot(&qvec) * inv_det;
if v < -EPS || u + v > 1.0 + EPS {
return None;
}
Some(e2.dot(&qvec) * inv_det)
}
fn axis_line_crosses_mesh(mesh: &Mesh, point: Point3<f64>, axis: &Vector3<f64>) -> bool {
for tri in mesh.indices.chunks_exact(3) {
let (Some(a), Some(b), Some(c)) = (
mesh_point(mesh, tri[0]),
mesh_point(mesh, tri[1]),
mesh_point(mesh, tri[2]),
) else {
continue;
};
if ray_triangle_param(point, axis, a, b, c).is_some() {
return true;
}
}
false
}
fn opening_redundant_with_host(host: &Mesh, opening: &Mesh, axis: &Vector3<f64>) -> bool {
let Some(axis) = axis.try_normalize(NORMALIZE_EPSILON) else {
return false;
};
let Some(centroid) = mesh_vertex_centroid(opening) else {
return false;
};
const PULL_TO_CENTROID: f64 = 0.1;
if axis_line_crosses_mesh(host, centroid, &axis) {
return false;
}
for v in opening.positions.chunks_exact(3) {
let vertex = Point3::new(v[0] as f64, v[1] as f64, v[2] as f64);
let sample = vertex + (centroid - vertex) * PULL_TO_CENTROID;
if axis_line_crosses_mesh(host, sample, &axis) {
return false;
}
}
true
}
fn mesh_vertex_centroid(mesh: &Mesh) -> Option<Point3<f64>> {
let n = mesh.positions.len() / 3;
if n == 0 {
return None;
}
let (mut sx, mut sy, mut sz) = (0.0f64, 0.0f64, 0.0f64);
for chunk in mesh.positions.chunks_exact(3) {
sx += chunk[0] as f64;
sy += chunk[1] as f64;
sz += chunk[2] as f64;
}
let inv = 1.0 / n as f64;
Some(Point3::new(sx * inv, sy * inv, sz * inv))
}
fn extent_along_axis(mesh: &Mesh, axis: &Vector3<f64>) -> Option<f64> {
let mut min = f64::INFINITY;
let mut max = f64::NEG_INFINITY;
for chunk in mesh.positions.chunks_exact(3) {
let p = Vector3::new(chunk[0] as f64, chunk[1] as f64, chunk[2] as f64);
let projection = p.dot(axis);
min = min.min(projection);
max = max.max(projection);
}
min.is_finite().then_some(max - min)
}
fn is_rectangular_box_mesh(mesh: &Mesh) -> bool {
let mut axes: Vec<Vector3<f64>> = Vec::with_capacity(4);
let mut tri_axes: Vec<(usize, f64)> = Vec::with_capacity(mesh.indices.len() / 3);
for tri in mesh.indices.chunks_exact(3) {
let (Some(p0), Some(p1), Some(p2)) = (
mesh_point(mesh, tri[0]),
mesh_point(mesh, tri[1]),
mesh_point(mesh, tri[2]),
) else {
continue;
};
let Some(normal) = (p1 - p0).cross(&(p2 - p0)).try_normalize(NORMALIZE_EPSILON) else {
continue;
};
let axis_index = match axes
.iter()
.position(|axis| normal.dot(axis).abs() > 0.98)
{
Some(idx) => idx,
None => {
if axes.len() >= 3 {
return false;
}
axes.push(normal);
axes.len() - 1
}
};
let offset = p0.coords.dot(&axes[axis_index]);
tri_axes.push((axis_index, offset));
}
if axes.len() != 3 {
return false;
}
const ORTHOGONAL_DOT_TOL: f64 = 0.02;
for i in 0..3 {
for j in (i + 1)..3 {
if axes[i].dot(&axes[j]).abs() > ORTHOGONAL_DOT_TOL {
return false;
}
}
}
const PLANE_TOL: f64 = 1e-3;
for axis_index in 0..3 {
let mut planes: Vec<f64> = Vec::with_capacity(3);
for (idx, offset) in &tri_axes {
if *idx != axis_index {
continue;
}
if !planes.iter().any(|p| (p - offset).abs() < PLANE_TOL) {
planes.push(*offset);
if planes.len() > 2 {
return false;
}
}
}
if planes.len() != 2 {
return false;
}
}
true
}
fn infer_opening_frame(mesh: &Mesh, extrusion_dir: Option<&Vector3<f64>>) -> Option<OpeningFrame> {
let mut axes: Vec<(Vector3<f64>, f64)> = Vec::new();
for tri in mesh.indices.chunks_exact(3) {
let (Some(p0), Some(p1), Some(p2)) = (
mesh_point(mesh, tri[0]),
mesh_point(mesh, tri[1]),
mesh_point(mesh, tri[2]),
) else {
continue;
};
let normal_raw = (p1 - p0).cross(&(p2 - p0));
let weight = normal_raw.norm();
let Some(mut normal) = normal_raw.try_normalize(NORMALIZE_EPSILON) else {
continue;
};
if let Some((axis, axis_weight)) = axes
.iter_mut()
.find(|(axis, _)| normal.dot(axis).abs() > 0.98)
{
if normal.dot(axis) < 0.0 {
normal = -normal;
}
if let Some(merged) =
(*axis * *axis_weight + normal * weight).try_normalize(NORMALIZE_EPSILON)
{
*axis = merged;
*axis_weight += weight;
}
} else {
axes.push((normal, weight));
}
}
if axes.len() < 3 {
return extrusion_dir.and_then(|dir| OpeningFrame::from_depth(*dir));
}
let depth_index =
if let Some(dir) = extrusion_dir.and_then(|d| d.try_normalize(NORMALIZE_EPSILON)) {
axes.iter()
.enumerate()
.max_by(|(_, (a, _)), (_, (b, _))| a.dot(&dir).abs().total_cmp(&b.dot(&dir).abs()))
.map(|(index, _)| index)?
} else {
axes.iter()
.enumerate()
.filter_map(|(index, (axis, _))| extent_along_axis(mesh, axis).map(|e| (index, e)))
.min_by(|(_, a), (_, b)| a.total_cmp(b))
.map(|(index, _)| index)?
};
let mut depth = axes[depth_index].0;
if let Some(dir) = extrusion_dir {
if depth.dot(dir) < 0.0 {
depth = -depth;
}
}
let mut cross_candidates: Vec<Vector3<f64>> = axes
.iter()
.enumerate()
.filter_map(|(index, (axis, _))| {
(index != depth_index && axis.dot(&depth).abs() < 0.25).then_some(*axis)
})
.collect();
if cross_candidates.len() < 2 {
return OpeningFrame::from_depth(depth);
}
let mut cross_a = cross_candidates.remove(0);
cross_a = (cross_a - depth * cross_a.dot(&depth)).try_normalize(NORMALIZE_EPSILON)?;
let mut cross_b = depth.cross(&cross_a).try_normalize(NORMALIZE_EPSILON)?;
if cross_b.dot(&cross_candidates[0]) < 0.0 {
cross_b = -cross_b;
}
Some(OpeningFrame {
depth,
cross_a,
cross_b,
})
}
struct ClipBuffers {
result: TriangleVec,
remaining: TriangleVec,
next_remaining: TriangleVec,
}
impl ClipBuffers {
fn new() -> Self {
Self {
result: TriangleVec::new(),
remaining: TriangleVec::new(),
next_remaining: TriangleVec::new(),
}
}
#[inline]
fn clear(&mut self) {
self.result.clear();
self.remaining.clear();
self.next_remaining.clear();
}
}
pub(super) struct VoidContext {
openings: Vec<OpeningType>,
merged_openings: Vec<OpeningType>,
}
impl VoidContext {
fn is_noop(&self) -> bool {
self.openings.is_empty()
}
}
impl GeometryRouter {
fn extract_extrusion_direction_from_solid(
&self,
solid: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Option<(Vector3<f64>, Option<Matrix4<f64>>)> {
let direction_attr = solid.get(2)?;
let direction_entity = decoder.resolve_ref(direction_attr).ok()??;
let local_dir = self.parse_direction(&direction_entity).ok()?;
let position_transform = if let Some(pos_attr) = solid.get(1) {
if !pos_attr.is_null() {
if let Ok(Some(pos_entity)) = decoder.resolve_ref(pos_attr) {
if pos_entity.ifc_type == IfcType::IfcAxis2Placement3D {
self.parse_axis2_placement_3d(&pos_entity, decoder).ok()
} else {
None
}
} else {
None
}
} else {
None
}
} else {
None
};
Some((local_dir, position_transform))
}
fn extract_extrusion_direction_recursive(
&self,
item: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Option<(Vector3<f64>, Option<Matrix4<f64>>)> {
let mut current = item.clone();
let mut visited = FxHashSet::default();
let mut mapping_chain: Option<Matrix4<f64>> = None;
for _depth in 0..MAX_EXTRUSION_EXTRACT_DEPTH {
if !visited.insert(current.id) {
return None;
}
match current.ifc_type {
IfcType::IfcExtrudedAreaSolid => {
let (dir, position_transform) =
self.extract_extrusion_direction_from_solid(¤t, decoder)?;
let combined = match (mapping_chain.as_ref(), position_transform) {
(Some(chain), Some(pos)) => Some(chain * pos),
(Some(chain), None) => Some(chain.clone()),
(None, Some(pos)) => Some(pos),
(None, None) => None,
};
return Some((dir, combined));
}
IfcType::IfcBooleanClippingResult | IfcType::IfcBooleanResult => {
let first_attr = current.get(1)?;
current = decoder.resolve_ref(first_attr).ok()??;
}
IfcType::IfcMappedItem => {
let source_attr = current.get(0)?;
let source = decoder.resolve_ref(source_attr).ok()??;
let rep_attr = source.get(1)?;
let rep = decoder.resolve_ref(rep_attr).ok()??;
if let Some(target_attr) = current.get(1) {
if !target_attr.is_null() {
if let Ok(Some(target)) = decoder.resolve_ref(target_attr) {
if let Ok(map) =
self.parse_cartesian_transformation_operator(&target, decoder)
{
mapping_chain = Some(match mapping_chain.take() {
Some(chain) => chain * map,
None => map,
});
}
}
}
}
let items_attr = rep.get(3)?;
let items = decoder.resolve_ref_list(items_attr).ok()?;
current = items.first()?.clone();
}
_ => return None,
}
}
None
}
pub fn get_opening_item_meshes_world(
&self,
element: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Vec<Mesh>> {
let representation_attr = element.get(6).ok_or_else(|| {
Error::geometry("Element has no representation attribute".to_string())
})?;
if representation_attr.is_null() {
return Ok(vec![]);
}
let representation = decoder
.resolve_ref(representation_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve representation".to_string()))?;
let representations_attr = representation.get(2).ok_or_else(|| {
Error::geometry("ProductDefinitionShape missing Representations".to_string())
})?;
let representations = decoder.resolve_ref_list(representations_attr)?;
let mut placement_transform = self
.get_placement_transform_from_element(element, decoder)
.unwrap_or_else(|_| Matrix4::identity());
self.scale_transform(&mut placement_transform);
let mut item_meshes = Vec::new();
for shape_rep in representations {
if shape_rep.ifc_type != IfcType::IfcShapeRepresentation {
continue;
}
if let Some(rep_type_attr) = shape_rep.get(2) {
if let Some(rep_type) = rep_type_attr.as_string() {
if !is_body_representation(rep_type) {
continue;
}
}
}
let items_attr = match shape_rep.get(3) {
Some(attr) => attr,
None => continue,
};
let items = match decoder.resolve_ref_list(items_attr) {
Ok(items) => items,
Err(_) => continue,
};
for item in items {
let mut mesh = match self.process_representation_item(&item, decoder) {
Ok(m) if !m.is_empty() => m,
_ => continue,
};
self.transform_mesh_world(&mut mesh, &placement_transform);
item_meshes.push(mesh);
}
}
Ok(item_meshes)
}
pub fn get_opening_item_bounds_with_direction(
&self,
element: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Vec<(Point3<f64>, Point3<f64>, Option<Vector3<f64>>)>> {
let representation_attr = element.get(6).ok_or_else(|| {
Error::geometry("Element has no representation attribute".to_string())
})?;
if representation_attr.is_null() {
return Ok(vec![]);
}
let representation = decoder
.resolve_ref(representation_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve representation".to_string()))?;
let representations_attr = representation.get(2).ok_or_else(|| {
Error::geometry("ProductDefinitionShape missing Representations".to_string())
})?;
let representations = decoder.resolve_ref_list(representations_attr)?;
let mut placement_transform = self
.get_placement_transform_from_element(element, decoder)
.unwrap_or_else(|_| Matrix4::identity());
self.scale_transform(&mut placement_transform);
let mut bounds_list = Vec::new();
for shape_rep in representations {
if shape_rep.ifc_type != IfcType::IfcShapeRepresentation {
continue;
}
if let Some(rep_type_attr) = shape_rep.get(2) {
if let Some(rep_type) = rep_type_attr.as_string() {
if !is_body_representation(rep_type) {
continue;
}
}
}
let items_attr = match shape_rep.get(3) {
Some(attr) => attr,
None => continue,
};
let items = match decoder.resolve_ref_list(items_attr) {
Ok(items) => items,
Err(_) => continue,
};
for item in items {
let extrusion_direction = if let Some((local_dir, position_transform)) =
self.extract_extrusion_direction_recursive(&item, decoder)
{
if let Some(pos_transform) = position_transform {
let pos_rot = extract_rotation_columns(&pos_transform);
let world_dir = rotate_and_normalize(&pos_rot, &local_dir)?;
let element_rot = extract_rotation_columns(&placement_transform);
let final_dir = rotate_and_normalize(&element_rot, &world_dir)?;
Some(final_dir)
} else {
let element_rot = extract_rotation_columns(&placement_transform);
let final_dir = rotate_and_normalize(&element_rot, &local_dir)?;
Some(final_dir)
}
} else {
None
};
let mesh = match self.process_representation_item(&item, decoder) {
Ok(m) if !m.is_empty() => m,
_ => continue,
};
let (mesh_min, mesh_max) = mesh.bounds();
let corners = [
Point3::new(mesh_min.x as f64, mesh_min.y as f64, mesh_min.z as f64),
Point3::new(mesh_max.x as f64, mesh_min.y as f64, mesh_min.z as f64),
Point3::new(mesh_min.x as f64, mesh_max.y as f64, mesh_min.z as f64),
Point3::new(mesh_max.x as f64, mesh_max.y as f64, mesh_min.z as f64),
Point3::new(mesh_min.x as f64, mesh_min.y as f64, mesh_max.z as f64),
Point3::new(mesh_max.x as f64, mesh_min.y as f64, mesh_max.z as f64),
Point3::new(mesh_min.x as f64, mesh_max.y as f64, mesh_max.z as f64),
Point3::new(mesh_max.x as f64, mesh_max.y as f64, mesh_max.z as f64),
];
let transformed: Vec<Point3<f64>> = corners
.iter()
.map(|p| placement_transform.transform_point(p))
.collect();
let world_min = Point3::new(
transformed
.iter()
.map(|p| p.x)
.fold(f64::INFINITY, f64::min),
transformed
.iter()
.map(|p| p.y)
.fold(f64::INFINITY, f64::min),
transformed
.iter()
.map(|p| p.z)
.fold(f64::INFINITY, f64::min),
);
let world_max = Point3::new(
transformed
.iter()
.map(|p| p.x)
.fold(f64::NEG_INFINITY, f64::max),
transformed
.iter()
.map(|p| p.y)
.fold(f64::NEG_INFINITY, f64::max),
transformed
.iter()
.map(|p| p.z)
.fold(f64::NEG_INFINITY, f64::max),
);
let rtc = self.rtc_offset;
let rtc_min = Point3::new(
world_min.x - rtc.0,
world_min.y - rtc.1,
world_min.z - rtc.2,
);
let rtc_max = Point3::new(
world_max.x - rtc.0,
world_max.y - rtc.1,
world_max.z - rtc.2,
);
bounds_list.push((rtc_min, rtc_max, extrusion_direction));
}
}
Ok(bounds_list)
}
#[inline]
pub fn process_element_with_voids(
&self,
element: &DecodedEntity,
decoder: &mut EntityDecoder,
void_index: &FxHashMap<u32, Vec<u32>>,
) -> Result<Mesh> {
let opening_ids = match void_index.get(&element.id) {
Some(ids) if !ids.is_empty() => ids,
_ => {
return self.process_element(element, decoder);
}
};
let wall_mesh = match self.process_element(element, decoder) {
Ok(m) => m,
Err(_) => {
return self.process_element(element, decoder);
}
};
Ok(self.apply_voids_to_mesh(wall_mesh, element, opening_ids, decoder))
}
pub(super) fn apply_voids_to_mesh(
&self,
mesh: Mesh,
element: &DecodedEntity,
opening_ids: &[u32],
decoder: &mut EntityDecoder,
) -> Mesh {
let ctx = self.build_void_context(element, opening_ids, decoder);
self.apply_void_context(mesh, &ctx, element.id)
}
pub(super) fn build_void_context(
&self,
element: &DecodedEntity,
opening_ids: &[u32],
decoder: &mut EntityDecoder,
) -> VoidContext {
let openings = self.classify_openings(element, opening_ids, decoder);
let merged_openings = Self::merge_rectangular_openings(&openings);
VoidContext {
openings,
merged_openings,
}
}
pub(super) fn apply_void_context(
&self,
mesh: Mesh,
ctx: &VoidContext,
element_id: u32,
) -> Mesh {
let tris_before = mesh.triangle_count();
let host_bounds_capture = {
let (mn, mx) = mesh.bounds();
((mn.x, mn.y, mn.z), (mx.x, mx.y, mx.z))
};
if ctx.is_noop() {
return mesh;
}
let clipper = ClippingProcessor::new();
let mut result = mesh;
let (wall_min_f32, wall_max_f32) = result.bounds();
let wall_min = Point3::new(
wall_min_f32.x as f64,
wall_min_f32.y as f64,
wall_min_f32.z as f64,
);
let wall_max = Point3::new(
wall_max_f32.x as f64,
wall_max_f32.y as f64,
wall_max_f32.z as f64,
);
let wall_valid = !result.is_empty()
&& result.positions.iter().all(|&v| v.is_finite())
&& result.triangle_count() >= 4;
if !wall_valid {
return result;
}
let mut csg_operation_count = 0;
const MAX_CSG_OPERATIONS: usize = 10;
self.apply_diagonal_openings(&mut result, &ctx.openings);
let mut rect_boxes: Vec<(Point3<f64>, Point3<f64>)> = Vec::new();
let mut rect_dirs: Vec<Option<Vector3<f64>>> = Vec::new();
let mut non_rect_openings: Vec<&OpeningType> = Vec::new();
for opening in &ctx.merged_openings {
match opening {
OpeningType::Rectangular(open_min, open_max, extrusion_dir) => {
let (final_min, final_max) = if let Some(dir) = extrusion_dir {
self.extend_opening_along_direction(
*open_min, *open_max, wall_min, wall_max, *dir,
)
} else {
(*open_min, *open_max)
};
rect_boxes.push((final_min, final_max));
rect_dirs.push(*extrusion_dir);
}
other => {
non_rect_openings.push(other);
}
}
}
if !rect_boxes.is_empty() {
let (new_result, processed) =
self.cut_multiple_rectangular_openings(&result, &rect_boxes);
result = new_result;
for (i, (open_min, open_max)) in rect_boxes.iter().enumerate().take(processed) {
generate_reveal_quads(
&mut result,
open_min,
open_max,
&wall_min,
&wall_max,
rect_dirs[i].as_ref(),
);
generate_recess_cap(
&mut result,
open_min,
open_max,
&wall_min,
&wall_max,
rect_dirs[i].as_ref(),
);
}
}
for opening in &non_rect_openings {
match *opening {
OpeningType::Rectangular(..) | OpeningType::DiagonalRectangular(..) => {}
OpeningType::NonRectangular(
ref opening_mesh,
open_min_pt,
open_max_pt,
extrusion_dir,
) => {
if csg_operation_count >= MAX_CSG_OPERATIONS {
continue;
}
let opening_valid = !opening_mesh.is_empty()
&& opening_mesh.positions.iter().all(|&v| v.is_finite())
&& opening_mesh.positions.len() >= 9;
if !opening_valid {
continue;
}
let (result_min, result_max) = result.bounds();
let (open_min_f32, open_max_f32) = opening_mesh.bounds();
let no_overlap = open_max_f32.x < result_min.x
|| open_min_f32.x > result_max.x
|| open_max_f32.y < result_min.y
|| open_min_f32.y > result_max.y
|| open_max_f32.z < result_min.z
|| open_min_f32.z > result_max.z;
if no_overlap {
continue;
}
let open_vol = (open_max_f32.x - open_min_f32.x)
* (open_max_f32.y - open_min_f32.y)
* (open_max_f32.z - open_min_f32.z);
let min_open_vol = match self.tessellation_quality {
TessellationQuality::High | TessellationQuality::Highest => 1e-9_f32,
_ => MIN_OPENING_VOLUME as f32,
};
if open_vol < min_open_vol {
continue;
}
let tri_before = result.triangle_count();
let failures_before = clipper.failure_count();
let mut csg_succeeded = false;
let mut csg_unchanged = false;
match clipper.subtract_mesh(&result, opening_mesh) {
Ok(csg_result) => {
let min_tris = (tri_before / CSG_TRIANGLE_RETENTION_DIVISOR)
.max(MIN_VALID_TRIANGLES);
let changed = csg_result.triangle_count() != tri_before;
csg_unchanged = !changed;
if !csg_result.is_empty()
&& csg_result.triangle_count() >= min_tris
&& changed
{
result = csg_result;
csg_succeeded = true;
}
}
Err(_) => {}
}
csg_operation_count += 1;
if !csg_succeeded {
let dir = extrusion_dir.or_else(|| {
Some(wall_thinnest_axis_dir(&wall_min, &wall_max))
});
let (final_min, final_max) = if let Some(dir) = dir {
self.extend_opening_along_direction(
*open_min_pt,
*open_max_pt,
wall_min,
wall_max,
dir,
)
} else {
(*open_min_pt, *open_max_pt)
};
let engulfs_host = {
let tol = 0.03_f64;
let covers = |omin: f64, omax: f64, wmin: f64, wmax: f64| {
let slack = (wmax - wmin).abs().max(1.0e-9) * tol;
omin <= wmin + slack && omax >= wmax - slack
};
covers(final_min.x, final_max.x, wall_min.x, wall_max.x)
&& covers(final_min.y, final_max.y, wall_min.y, wall_max.y)
&& covers(final_min.z, final_max.z, wall_min.z, wall_max.z)
};
let capped = clipper.has_operand_too_large_since(failures_before);
let probe_axis = dir.unwrap_or_else(|| {
wall_thinnest_axis_dir(&wall_min, &wall_max)
});
let redundant_void =
opening_redundant_with_host(&result, opening_mesh, &probe_axis);
let suppress_fallback =
redundant_void || (csg_unchanged && engulfs_host && !capped);
if !suppress_fallback {
#[cfg(any(debug_assertions, test))]
{
eprintln!(
"[issue-635] AABB fallback used: opening={} tris (over MAX_CSG_POLYGONS_PER_MESH or no change)",
opening_mesh.triangle_count()
);
}
let aabb_cut =
self.cut_rectangular_opening(&result, final_min, final_max);
if !aabb_cut.is_empty() && aabb_cut.triangle_count() != tri_before {
result = aabb_cut;
}
}
}
}
}
}
let kernel_failures = clipper.take_failures();
if !kernel_failures.is_empty() {
self.record_host_failure_summary(element_id, &kernel_failures);
self.record_csg_failures(element_id, kernel_failures);
}
self.record_host_cut_effect(
element_id,
tris_before,
result.triangle_count(),
rect_boxes.len(),
host_bounds_capture,
);
result
}
pub fn process_element_with_submeshes_and_voids(
&self,
element: &DecodedEntity,
decoder: &mut EntityDecoder,
void_index: &FxHashMap<u32, Vec<u32>>,
) -> Result<SubMeshCollection> {
if let Some(layered) = self.try_layered_sub_meshes(element, decoder, Some(void_index)) {
return Ok(layered);
}
let opening_ids = match void_index.get(&element.id) {
Some(ids) if !ids.is_empty() => ids.clone(),
_ => return Ok(SubMeshCollection::new()),
};
let sub_meshes = self.process_element_with_submeshes(element, decoder)?;
if sub_meshes.is_empty() {
return Ok(SubMeshCollection::new());
}
let ctx = self.build_void_context(element, &opening_ids, decoder);
let mut voided = SubMeshCollection::new();
for sub in sub_meshes.sub_meshes {
let geometry_id = sub.geometry_id;
let voided_mesh = self.apply_void_context(sub.mesh, &ctx, element.id);
if !voided_mesh.is_empty() {
voided
.sub_meshes
.push(SubMesh::new(geometry_id, voided_mesh));
}
}
Ok(voided)
}
fn fallback_aabb_for_opening(
&self,
opening_entity: &DecodedEntity,
opening_mesh: &Mesh,
decoder: &mut EntityDecoder,
) -> (Point3<f64>, Point3<f64>, Option<Vector3<f64>>) {
let dir = self
.get_opening_item_bounds_with_direction(opening_entity, decoder)
.ok()
.and_then(|items| items.into_iter().find_map(|(_, _, d)| d));
let (mn, mx) = opening_mesh.bounds();
(
Point3::new(mn.x as f64, mn.y as f64, mn.z as f64),
Point3::new(mx.x as f64, mx.y as f64, mx.z as f64),
dir,
)
}
fn classify_openings(
&self,
host: &DecodedEntity,
opening_ids: &[u32],
decoder: &mut EntityDecoder,
) -> Vec<OpeningType> {
use super::{ClassificationKind, OpeningDiagnostic, OpeningKindDiag};
let host_is_horizontal_surface = matches!(
host.ifc_type,
IfcType::IfcSlab | IfcType::IfcRoof | IfcType::IfcCovering
);
let mut host_diag: Vec<OpeningDiagnostic> = Vec::with_capacity(opening_ids.len());
let mut openings: Vec<OpeningType> = Vec::new();
for &opening_id in opening_ids.iter() {
let opening_entity = match decoder.decode_by_id(opening_id) {
Ok(e) => e,
Err(_) => continue,
};
let opening_mesh = match self.process_element(&opening_entity, decoder) {
Ok(m) if !m.is_empty() => m,
_ => continue,
};
let vertex_count = opening_mesh.positions.len() / 3;
let mut bump = |router: &Self,
ck: ClassificationKind,
kind: OpeningKindDiag,
guard_saved: bool| {
router.bump_classification(ck);
host_diag.push(OpeningDiagnostic {
opening_id,
kind,
vertex_count,
guard_saved,
});
};
if vertex_count > 100 {
let (fallback_min, fallback_max, fallback_dir) =
self.fallback_aabb_for_opening(&opening_entity, &opening_mesh, decoder);
bump(
self,
ClassificationKind::NonRectangular,
OpeningKindDiag::NonRectangular,
false,
);
openings.push(OpeningType::NonRectangular(
opening_mesh,
fallback_min,
fallback_max,
fallback_dir,
));
} else {
let item_bounds_with_dir = self
.get_opening_item_bounds_with_direction(&opening_entity, decoder)
.unwrap_or_default();
if !item_bounds_with_dir.is_empty() {
let _host_is_horizontal = host_is_horizontal_surface;
let item_meshes = self
.get_opening_item_meshes_world(&opening_entity, decoder)
.unwrap_or_default();
if item_meshes.len() == item_bounds_with_dir.len() {
for ((min_pt, max_pt, extrusion_dir), item_mesh) in item_bounds_with_dir
.into_iter()
.zip(item_meshes.into_iter())
{
let frame = infer_opening_frame(&item_mesh, extrusion_dir.as_ref());
let direction_is_diagonal = extrusion_dir
.map(|d| !is_axis_aligned_direction(&d))
.unwrap_or(false);
let is_clean_box = is_rectangular_box_mesh(&item_mesh);
if let Some(frame) = frame {
if !is_clean_box {
bump(
self,
ClassificationKind::NonRectangular,
OpeningKindDiag::NonRectangular,
false,
);
openings.push(OpeningType::NonRectangular(
item_mesh,
min_pt,
max_pt,
extrusion_dir,
));
} else if direction_is_diagonal || !frame.is_axis_aligned() {
bump(
self,
ClassificationKind::Diagonal,
OpeningKindDiag::Diagonal,
false,
);
openings.push(OpeningType::DiagonalRectangular(
item_mesh, frame,
));
} else {
bump(
self,
ClassificationKind::Rectangular,
OpeningKindDiag::Rectangular,
false,
);
openings.push(OpeningType::Rectangular(
min_pt,
max_pt,
extrusion_dir,
));
}
} else if is_clean_box {
bump(
self,
ClassificationKind::Rectangular,
OpeningKindDiag::Rectangular,
false,
);
openings.push(OpeningType::Rectangular(
min_pt,
max_pt,
extrusion_dir,
));
} else {
bump(
self,
ClassificationKind::NonRectangular,
OpeningKindDiag::NonRectangular,
false,
);
openings.push(OpeningType::NonRectangular(
item_mesh,
min_pt,
max_pt,
extrusion_dir,
));
}
}
} else {
for (min_pt, max_pt, extrusion_dir) in item_bounds_with_dir {
bump(
self,
ClassificationKind::Rectangular,
OpeningKindDiag::Rectangular,
false,
);
openings.push(OpeningType::Rectangular(
min_pt, max_pt, extrusion_dir,
));
}
}
} else {
let (open_min, open_max) = opening_mesh.bounds();
let min_f64 =
Point3::new(open_min.x as f64, open_min.y as f64, open_min.z as f64);
let max_f64 =
Point3::new(open_max.x as f64, open_max.y as f64, open_max.z as f64);
bump(
self,
ClassificationKind::Rectangular,
OpeningKindDiag::Rectangular,
false,
);
openings.push(OpeningType::Rectangular(min_f64, max_f64, None));
}
}
}
if !host_diag.is_empty() {
self.record_host_opening_diagnostic(
host.id,
&format!("{}", host.ifc_type),
host_diag,
);
}
openings
}
fn merge_rectangular_openings(openings: &[OpeningType]) -> Vec<OpeningType> {
const MERGE_TOLERANCE: f64 = 0.01;
let mut rects: Vec<(Point3<f64>, Point3<f64>, Option<Vector3<f64>>)> = Vec::new();
let mut others: Vec<OpeningType> = Vec::new();
for opening in openings {
match opening {
OpeningType::Rectangular(min, max, dir) => {
rects.push((*min, *max, *dir));
}
other => others.push(other.clone()),
}
}
let mut merged = true;
while merged {
merged = false;
let mut i = 0;
while i < rects.len() {
let mut j = i + 1;
while j < rects.len() {
let (a_min, a_max, _) = &rects[i];
let (b_min, b_max, _) = &rects[j];
let overlaps_x = a_min.x <= b_max.x + MERGE_TOLERANCE
&& a_max.x >= b_min.x - MERGE_TOLERANCE;
let overlaps_y = a_min.y <= b_max.y + MERGE_TOLERANCE
&& a_max.y >= b_min.y - MERGE_TOLERANCE;
let overlaps_z = a_min.z <= b_max.z + MERGE_TOLERANCE
&& a_max.z >= b_min.z - MERGE_TOLERANCE;
let dirs_compatible = match (&rects[i].2, &rects[j].2) {
(Some(a), Some(b)) => {
let dot = a.x * b.x + a.y * b.y + a.z * b.z;
dot.abs() > 0.99 }
(None, None) => true,
_ => false, };
if overlaps_x && overlaps_y && overlaps_z && dirs_compatible {
let dir = rects[i].2;
rects[i] = (
Point3::new(
a_min.x.min(b_min.x),
a_min.y.min(b_min.y),
a_min.z.min(b_min.z),
),
Point3::new(
a_max.x.max(b_max.x),
a_max.y.max(b_max.y),
a_max.z.max(b_max.z),
),
dir,
);
rects.remove(j);
merged = true;
} else {
j += 1;
}
}
i += 1;
}
}
let mut result: Vec<OpeningType> = rects
.into_iter()
.map(|(min, max, dir)| OpeningType::Rectangular(min, max, dir))
.collect();
result.extend(others);
result
}
fn apply_diagonal_openings(&self, result: &mut Mesh, openings: &[OpeningType]) {
let diagonal_openings: Vec<(&Mesh, &OpeningFrame)> = openings
.iter()
.filter_map(|o| match o {
OpeningType::DiagonalRectangular(mesh, frame) => Some((mesh, frame)),
_ => None,
})
.collect();
if diagonal_openings.is_empty() {
return;
}
for (opening_mesh, frame) in diagonal_openings {
for chunk in result.positions.chunks_exact_mut(3) {
let p = frame.to_local_point(Point3::new(
chunk[0] as f64,
chunk[1] as f64,
chunk[2] as f64,
));
chunk[0] = p.x as f32;
chunk[1] = p.y as f32;
chunk[2] = p.z as f32;
}
for chunk in result.normals.chunks_exact_mut(3) {
let n = frame.to_local_vector(Vector3::new(
chunk[0] as f64,
chunk[1] as f64,
chunk[2] as f64,
));
chunk[0] = n.x as f32;
chunk[1] = n.y as f32;
chunk[2] = n.z as f32;
}
let (rot_wall_min_f32, rot_wall_max_f32) = result.bounds();
let rot_wall_min = Point3::new(
rot_wall_min_f32.x as f64,
rot_wall_min_f32.y as f64,
rot_wall_min_f32.z as f64,
);
let rot_wall_max = Point3::new(
rot_wall_max_f32.x as f64,
rot_wall_max_f32.y as f64,
rot_wall_max_f32.z as f64,
);
let mut rot_min = Point3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
let mut rot_max = Point3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
for chunk in opening_mesh.positions.chunks_exact(3) {
let p = frame.to_local_point(Point3::new(
chunk[0] as f64,
chunk[1] as f64,
chunk[2] as f64,
));
rot_min.x = rot_min.x.min(p.x);
rot_min.y = rot_min.y.min(p.y);
rot_min.z = rot_min.z.min(p.z);
rot_max.x = rot_max.x.max(p.x);
rot_max.y = rot_max.y.max(p.y);
rot_max.z = rot_max.z.max(p.z);
}
rot_min.x = rot_min.x.min(rot_wall_min.x);
rot_max.x = rot_max.x.max(rot_wall_max.x);
*result = self.cut_rectangular_opening_no_faces(result, rot_min, rot_max);
let x_dir = Vector3::new(1.0, 0.0, 0.0);
generate_reveal_quads(
result,
&rot_min,
&rot_max,
&rot_wall_min,
&rot_wall_max,
Some(&x_dir),
);
generate_recess_cap(
result,
&rot_min,
&rot_max,
&rot_wall_min,
&rot_wall_max,
Some(&x_dir),
);
for chunk in result.positions.chunks_exact_mut(3) {
let p = frame.to_world_point(Point3::new(
chunk[0] as f64,
chunk[1] as f64,
chunk[2] as f64,
));
chunk[0] = p.x as f32;
chunk[1] = p.y as f32;
chunk[2] = p.z as f32;
}
for chunk in result.normals.chunks_exact_mut(3) {
let n = frame.to_world_vector(Vector3::new(
chunk[0] as f64,
chunk[1] as f64,
chunk[2] as f64,
));
chunk[0] = n.x as f32;
chunk[1] = n.y as f32;
chunk[2] = n.z as f32;
}
}
}
fn extend_opening_along_direction(
&self,
open_min: Point3<f64>,
open_max: Point3<f64>,
wall_min: Point3<f64>,
wall_max: Point3<f64>,
extrusion_direction: Vector3<f64>, ) -> (Point3<f64>, Point3<f64>) {
let open_center = Point3::new(
(open_min.x + open_max.x) * 0.5,
(open_min.y + open_max.y) * 0.5,
(open_min.z + open_max.z) * 0.5,
);
let wall_corners = [
Point3::new(wall_min.x, wall_min.y, wall_min.z),
Point3::new(wall_max.x, wall_min.y, wall_min.z),
Point3::new(wall_min.x, wall_max.y, wall_min.z),
Point3::new(wall_max.x, wall_max.y, wall_min.z),
Point3::new(wall_min.x, wall_min.y, wall_max.z),
Point3::new(wall_max.x, wall_min.y, wall_max.z),
Point3::new(wall_min.x, wall_max.y, wall_max.z),
Point3::new(wall_max.x, wall_max.y, wall_max.z),
];
let mut wall_min_proj = f64::INFINITY;
let mut wall_max_proj = f64::NEG_INFINITY;
for corner in &wall_corners {
let proj = (corner - open_center).dot(&extrusion_direction);
wall_min_proj = wall_min_proj.min(proj);
wall_max_proj = wall_max_proj.max(proj);
}
let open_corners = [
Point3::new(open_min.x, open_min.y, open_min.z),
Point3::new(open_max.x, open_min.y, open_min.z),
Point3::new(open_min.x, open_max.y, open_min.z),
Point3::new(open_max.x, open_max.y, open_min.z),
Point3::new(open_min.x, open_min.y, open_max.z),
Point3::new(open_max.x, open_min.y, open_max.z),
Point3::new(open_min.x, open_max.y, open_max.z),
Point3::new(open_max.x, open_max.y, open_max.z),
];
let mut open_min_proj = f64::INFINITY;
let mut open_max_proj = f64::NEG_INFINITY;
for corner in &open_corners {
let proj = (corner - open_center).dot(&extrusion_direction);
open_min_proj = open_min_proj.min(proj);
open_max_proj = open_max_proj.max(proj);
}
let opening_proj_extent = (open_max_proj - open_min_proj).abs();
let wall_extent_x = (wall_max.x - wall_min.x).abs();
let wall_extent_y = (wall_max.y - wall_min.y).abs();
let wall_extent_z = (wall_max.z - wall_min.z).abs();
let wall_min_extent = wall_extent_x.min(wall_extent_y).min(wall_extent_z);
if opening_proj_extent > wall_min_extent * 1.05 {
return (open_min, open_max);
}
let opening_max_dim = (open_max.x - open_min.x)
.abs()
.max((open_max.y - open_min.y).abs())
.max((open_max.z - open_min.z).abs());
let wall_proj_extent = (wall_max_proj - wall_min_proj).abs();
if wall_proj_extent > opening_max_dim {
return (open_min, open_max);
}
const POKE_TOL: f64 = 1e-6;
let opening_pokes_past_wall = open_min_proj < wall_min_proj - POKE_TOL
|| open_max_proj > wall_max_proj + POKE_TOL;
if opening_pokes_past_wall {
return (open_min, open_max);
}
let face_align_tol = (wall_max_proj - wall_min_proj).abs() * 1e-5;
let near_at_min_face = (open_min_proj - wall_min_proj).abs() < face_align_tol;
let near_at_max_face = (open_max_proj - wall_max_proj).abs() < face_align_tol;
let far_inside_min = open_min_proj > wall_min_proj + face_align_tol;
let far_inside_max = open_max_proj < wall_max_proj - face_align_tol;
let is_recess = (near_at_min_face && far_inside_max) || (near_at_max_face && far_inside_min);
if is_recess {
return (open_min, open_max);
}
let extend_backward = (open_min_proj - wall_min_proj).max(0.0); let extend_forward = (wall_max_proj - open_max_proj).max(0.0);
let wall_extent_along_dir = (wall_max_proj - wall_min_proj).abs();
let coplanarity_pad = (wall_extent_along_dir * 1e-5).max(1e-5);
let extend_backward = extend_backward + coplanarity_pad;
let extend_forward = extend_forward + coplanarity_pad;
let extended_min = open_min - extrusion_direction * extend_backward;
let extended_max = open_max + extrusion_direction * extend_forward;
let all_points = [open_min, open_max, extended_min, extended_max];
let new_min = Point3::new(
all_points.iter().map(|p| p.x).fold(f64::INFINITY, f64::min),
all_points.iter().map(|p| p.y).fold(f64::INFINITY, f64::min),
all_points.iter().map(|p| p.z).fold(f64::INFINITY, f64::min),
);
let new_max = Point3::new(
all_points
.iter()
.map(|p| p.x)
.fold(f64::NEG_INFINITY, f64::max),
all_points
.iter()
.map(|p| p.y)
.fold(f64::NEG_INFINITY, f64::max),
all_points
.iter()
.map(|p| p.z)
.fold(f64::NEG_INFINITY, f64::max),
);
(new_min, new_max)
}
fn cut_multiple_rectangular_openings(
&self,
mesh: &Mesh,
boxes: &[(Point3<f64>, Point3<f64>)],
) -> (Mesh, usize) {
let mut current = mesh.clone();
const MAX_TRIANGLES: usize = 500_000;
let mut processed = 0;
for (open_min, open_max) in boxes.iter() {
if current.indices.len() / 3 > MAX_TRIANGLES {
break;
}
current = self.cut_rectangular_opening(¤t, *open_min, *open_max);
processed += 1;
}
(current, processed)
}
pub(super) fn cut_rectangular_opening(
&self,
mesh: &Mesh,
open_min: Point3<f64>,
open_max: Point3<f64>,
) -> Mesh {
self.cut_rectangular_opening_no_faces(mesh, open_min, open_max)
}
fn cut_rectangular_opening_no_faces(
&self,
mesh: &Mesh,
open_min: Point3<f64>,
open_max: Point3<f64>,
) -> Mesh {
use nalgebra::Vector3;
const EPSILON: f64 = 1e-6;
let mut result = Mesh::with_capacity(mesh.positions.len() / 3, mesh.indices.len() / 3);
let mut clip_buffers = ClipBuffers::new();
let num_vertices = mesh.positions.len() / 3;
for chunk in mesh.indices.chunks_exact(3) {
let i0 = chunk[0] as usize;
let i1 = chunk[1] as usize;
let i2 = chunk[2] as usize;
if i0 >= num_vertices || i1 >= num_vertices || i2 >= num_vertices {
continue;
}
let v0 = Point3::new(
mesh.positions[i0 * 3] as f64,
mesh.positions[i0 * 3 + 1] as f64,
mesh.positions[i0 * 3 + 2] as f64,
);
let v1 = Point3::new(
mesh.positions[i1 * 3] as f64,
mesh.positions[i1 * 3 + 1] as f64,
mesh.positions[i1 * 3 + 2] as f64,
);
let v2 = Point3::new(
mesh.positions[i2 * 3] as f64,
mesh.positions[i2 * 3 + 1] as f64,
mesh.positions[i2 * 3 + 2] as f64,
);
let n0 = if mesh.normals.len() >= mesh.positions.len() {
Vector3::new(
mesh.normals[i0 * 3] as f64,
mesh.normals[i0 * 3 + 1] as f64,
mesh.normals[i0 * 3 + 2] as f64,
)
} else {
let edge1 = v1 - v0;
let edge2 = v2 - v0;
edge1
.cross(&edge2)
.try_normalize(1e-10)
.unwrap_or(Vector3::new(0.0, 0.0, 1.0))
};
let tri_min_x = v0.x.min(v1.x).min(v2.x);
let tri_max_x = v0.x.max(v1.x).max(v2.x);
let tri_min_y = v0.y.min(v1.y).min(v2.y);
let tri_max_y = v0.y.max(v1.y).max(v2.y);
let tri_min_z = v0.z.min(v1.z).min(v2.z);
let tri_max_z = v0.z.max(v1.z).max(v2.z);
let eps_x = EPSILON.max(open_min.x.abs().max(open_max.x.abs()) * 1e-6);
let eps_y = EPSILON.max(open_min.y.abs().max(open_max.y.abs()) * 1e-6);
let eps_z = EPSILON.max(open_min.z.abs().max(open_max.z.abs()) * 1e-6);
if tri_max_x <= open_min.x - eps_x
|| tri_min_x >= open_max.x + eps_x
|| tri_max_y <= open_min.y - eps_y
|| tri_min_y >= open_max.y + eps_y
|| tri_max_z <= open_min.z - eps_z
|| tri_min_z >= open_max.z + eps_z
{
let base = result.vertex_count() as u32;
result.add_vertex(v0, n0);
result.add_vertex(v1, n0);
result.add_vertex(v2, n0);
result.add_triangle(base, base + 1, base + 2);
continue;
}
if tri_min_x >= open_min.x + EPSILON
&& tri_max_x <= open_max.x - EPSILON
&& tri_min_y >= open_min.y + EPSILON
&& tri_max_y <= open_max.y - EPSILON
&& tri_min_z >= open_min.z + EPSILON
&& tri_max_z <= open_max.z - EPSILON
{
continue;
}
if self.triangle_intersects_box(&v0, &v1, &v2, &open_min, &open_max) {
self.clip_triangle_against_box(
&mut result,
&mut clip_buffers,
&v0,
&v1,
&v2,
&n0,
&open_min,
&open_max,
);
} else {
let base = result.vertex_count() as u32;
result.add_vertex(v0, n0);
result.add_vertex(v1, n0);
result.add_vertex(v2, n0);
result.add_triangle(base, base + 1, base + 2);
}
}
result
}
fn triangle_intersects_box(
&self,
v0: &Point3<f64>,
v1: &Point3<f64>,
v2: &Point3<f64>,
box_min: &Point3<f64>,
box_max: &Point3<f64>,
) -> bool {
use nalgebra::Vector3;
const SAT_EPSILON: f64 = 1e-6;
let box_center = Point3::new(
(box_min.x + box_max.x) * 0.5,
(box_min.y + box_max.y) * 0.5,
(box_min.z + box_max.z) * 0.5,
);
let box_half_extents = Vector3::new(
(box_max.x - box_min.x) * 0.5,
(box_max.y - box_min.y) * 0.5,
(box_max.z - box_min.z) * 0.5,
);
let t0 = v0 - box_center;
let t1 = v1 - box_center;
let t2 = v2 - box_center;
let e0 = t1 - t0;
let e1 = t2 - t1;
let e2 = t0 - t2;
for axis_idx in 0..3 {
let axis = match axis_idx {
0 => Vector3::new(1.0, 0.0, 0.0),
1 => Vector3::new(0.0, 1.0, 0.0),
2 => Vector3::new(0.0, 0.0, 1.0),
_ => unreachable!(),
};
let p0 = t0.dot(&axis);
let p1 = t1.dot(&axis);
let p2 = t2.dot(&axis);
let tri_min = p0.min(p1).min(p2);
let tri_max = p0.max(p1).max(p2);
let box_extent = box_half_extents[axis_idx];
let axis_eps =
SAT_EPSILON.max(box_center[axis_idx].abs().max(box_extent.abs()) * 1e-6);
if tri_max < -box_extent - axis_eps || tri_min > box_extent + axis_eps {
return false; }
}
let triangle_normal = e0.cross(&e2);
let triangle_offset = t0.dot(&triangle_normal);
let mut box_projection = 0.0;
for i in 0..3 {
let axis = match i {
0 => Vector3::new(1.0, 0.0, 0.0),
1 => Vector3::new(0.0, 1.0, 0.0),
2 => Vector3::new(0.0, 0.0, 1.0),
_ => unreachable!(),
};
box_projection += box_half_extents[i] * triangle_normal.dot(&axis).abs();
}
let phys_slack = SAT_EPSILON
.max(box_center.x.abs().max(box_center.y.abs()).max(box_center.z.abs()) * 1e-6);
let normal_magnitude = triangle_normal.norm();
let t2_epsilon = phys_slack * normal_magnitude.max(1.0);
if triangle_offset.abs() > box_projection + t2_epsilon {
return false; }
let box_axes = [
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
];
let tri_edges = [e0, e1, e2];
for box_axis in &box_axes {
for tri_edge in &tri_edges {
let axis = box_axis.cross(tri_edge);
if axis.norm_squared() < 1e-10 {
continue;
}
let axis_normalized = axis.normalize();
let p0 = t0.dot(&axis_normalized);
let p1 = t1.dot(&axis_normalized);
let p2 = t2.dot(&axis_normalized);
let tri_min = p0.min(p1).min(p2);
let tri_max = p0.max(p1).max(p2);
let mut box_projection = 0.0;
for i in 0..3 {
let box_axis_vec = box_axes[i];
box_projection +=
box_half_extents[i] * axis_normalized.dot(&box_axis_vec).abs();
}
if tri_max < -box_projection - phys_slack
|| tri_min > box_projection + phys_slack
{
return false; }
}
}
true
}
fn clip_triangle_against_box(
&self,
result: &mut Mesh,
buffers: &mut ClipBuffers,
v0: &Point3<f64>,
v1: &Point3<f64>,
v2: &Point3<f64>,
normal: &Vector3<f64>,
open_min: &Point3<f64>,
open_max: &Point3<f64>,
) {
let clipper = ClippingProcessor::new();
let coord_mag = open_min
.x
.abs()
.max(open_max.x.abs())
.max(open_min.y.abs())
.max(open_max.y.abs())
.max(open_min.z.abs())
.max(open_max.z.abs());
let epsilon = clipper.epsilon.max(coord_mag * 1e-6);
buffers.clear();
let planes = [
Plane::new(
Point3::new(open_min.x, 0.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
),
Plane::new(
Point3::new(open_max.x, 0.0, 0.0),
Vector3::new(-1.0, 0.0, 0.0),
),
Plane::new(
Point3::new(0.0, open_min.y, 0.0),
Vector3::new(0.0, 1.0, 0.0),
),
Plane::new(
Point3::new(0.0, open_max.y, 0.0),
Vector3::new(0.0, -1.0, 0.0),
),
Plane::new(
Point3::new(0.0, 0.0, open_min.z),
Vector3::new(0.0, 0.0, 1.0),
),
Plane::new(
Point3::new(0.0, 0.0, open_max.z),
Vector3::new(0.0, 0.0, -1.0),
),
];
if !v0.x.is_finite()
|| !v0.y.is_finite()
|| !v0.z.is_finite()
|| !v1.x.is_finite()
|| !v1.y.is_finite()
|| !v1.z.is_finite()
|| !v2.x.is_finite()
|| !v2.y.is_finite()
|| !v2.z.is_finite()
{
let base = result.vertex_count() as u32;
result.add_vertex(*v0, *normal);
result.add_vertex(*v1, *normal);
result.add_vertex(*v2, *normal);
result.add_triangle(base, base + 1, base + 2);
return;
}
buffers.remaining.push(Triangle::new(*v0, *v1, *v2));
for plane in &planes {
buffers.next_remaining.clear();
for tri in &buffers.remaining {
let d0 = plane.signed_distance(&tri.v0);
let d1 = plane.signed_distance(&tri.v1);
let d2 = plane.signed_distance(&tri.v2);
if !d0.is_finite() || !d1.is_finite() || !d2.is_finite() {
buffers.result.push(tri.clone()); continue;
}
let f0 = d0 >= -epsilon;
let f1 = d1 >= -epsilon;
let f2 = d2 >= -epsilon;
let front_count = f0 as u8 + f1 as u8 + f2 as u8;
match front_count {
3 => {
buffers.next_remaining.push(tri.clone());
}
0 => {
buffers.result.push(tri.clone());
}
1 => {
let (front, back1, back2, d_f, d_b1, d_b2) = if f0 {
(tri.v0, tri.v1, tri.v2, d0, d1, d2)
} else if f1 {
(tri.v1, tri.v2, tri.v0, d1, d2, d0)
} else {
(tri.v2, tri.v0, tri.v1, d2, d0, d1)
};
let denom1 = d_f - d_b1;
let denom2 = d_f - d_b2;
if denom1.abs() < 1e-12 || denom2.abs() < 1e-12 {
buffers.next_remaining.push(tri.clone());
continue;
}
let t1 = (d_f / denom1).clamp(0.0, 1.0);
let t2 = (d_f / denom2).clamp(0.0, 1.0);
let p1 = front + (back1 - front) * t1;
let p2 = front + (back2 - front) * t2;
if !p1.x.is_finite()
|| !p1.y.is_finite()
|| !p1.z.is_finite()
|| !p2.x.is_finite()
|| !p2.y.is_finite()
|| !p2.z.is_finite()
{
buffers.next_remaining.push(tri.clone());
continue;
}
buffers.next_remaining.push(Triangle::new(front, p1, p2));
buffers.result.push(Triangle::new(p1, back1, back2));
buffers.result.push(Triangle::new(p1, back2, p2));
}
2 => {
let (front1, front2, back, d_f1, d_f2, d_b) = if !f0 {
(tri.v1, tri.v2, tri.v0, d1, d2, d0)
} else if !f1 {
(tri.v2, tri.v0, tri.v1, d2, d0, d1)
} else {
(tri.v0, tri.v1, tri.v2, d0, d1, d2)
};
let denom1 = d_f1 - d_b;
let denom2 = d_f2 - d_b;
if denom1.abs() < 1e-12 || denom2.abs() < 1e-12 {
buffers.next_remaining.push(tri.clone());
continue;
}
let t1 = (d_f1 / denom1).clamp(0.0, 1.0);
let t2 = (d_f2 / denom2).clamp(0.0, 1.0);
let p1 = front1 + (back - front1) * t1;
let p2 = front2 + (back - front2) * t2;
if !p1.x.is_finite()
|| !p1.y.is_finite()
|| !p1.z.is_finite()
|| !p2.x.is_finite()
|| !p2.y.is_finite()
|| !p2.z.is_finite()
{
buffers.next_remaining.push(tri.clone());
continue;
}
buffers
.next_remaining
.push(Triangle::new(front1, front2, p1));
buffers.next_remaining.push(Triangle::new(front2, p2, p1));
buffers.result.push(Triangle::new(p1, p2, back));
}
_ => {
buffers.result.push(tri.clone());
}
}
}
std::mem::swap(&mut buffers.remaining, &mut buffers.next_remaining);
}
for tri in &buffers.result {
let base = result.vertex_count() as u32;
result.add_vertex(tri.v0, *normal);
result.add_vertex(tri.v1, *normal);
result.add_vertex(tri.v2, *normal);
result.add_triangle(base, base + 1, base + 2);
}
}
}
#[cfg(test)]
mod reveal_tests {
use super::*;
use crate::Mesh;
#[allow(dead_code)]
fn make_box_mesh(min: Point3<f64>, max: Point3<f64>) -> Mesh {
let mut m = Mesh::with_capacity(24, 36);
let corners = [
Point3::new(min.x, min.y, min.z), Point3::new(max.x, min.y, min.z), Point3::new(max.x, max.y, min.z), Point3::new(min.x, max.y, min.z), Point3::new(min.x, min.y, max.z), Point3::new(max.x, min.y, max.z), Point3::new(max.x, max.y, max.z), Point3::new(min.x, max.y, max.z), ];
let faces: [(Vector3<f64>, [usize; 4]); 6] = [
(Vector3::new(0.0, 0.0, -1.0), [0, 2, 1, 3]), (Vector3::new(0.0, 0.0, 1.0), [4, 5, 6, 7]), (Vector3::new(0.0, -1.0, 0.0), [0, 1, 5, 4]), (Vector3::new(0.0, 1.0, 0.0), [2, 3, 7, 6]), (Vector3::new(-1.0, 0.0, 0.0), [0, 4, 7, 3]), (Vector3::new(1.0, 0.0, 0.0), [1, 2, 6, 5]), ];
for (n, idx) in &faces {
let b = m.vertex_count() as u32;
m.add_vertex(corners[idx[0]], *n);
m.add_vertex(corners[idx[1]], *n);
m.add_vertex(corners[idx[2]], *n);
m.add_vertex(corners[idx[3]], *n);
m.add_triangle(b, b + 1, b + 2);
m.add_triangle(b, b + 2, b + 3);
}
m
}
fn make_oriented_box_mesh(
origin: Point3<f64>,
length_axis: Vector3<f64>,
thickness_axis: Vector3<f64>,
length: (f64, f64),
thickness: (f64, f64),
height: (f64, f64),
) -> Mesh {
let z_axis = Vector3::new(0.0, 0.0, 1.0);
let point =
|l: f64, t: f64, z: f64| origin + length_axis * l + thickness_axis * t + z_axis * z;
let corners = [
point(length.0, thickness.0, height.0),
point(length.1, thickness.0, height.0),
point(length.1, thickness.1, height.0),
point(length.0, thickness.1, height.0),
point(length.0, thickness.0, height.1),
point(length.1, thickness.0, height.1),
point(length.1, thickness.1, height.1),
point(length.0, thickness.1, height.1),
];
let mut m = Mesh::with_capacity(24, 36);
let faces: [[usize; 4]; 6] = [
[0, 2, 1, 3],
[4, 5, 6, 7],
[0, 1, 5, 4],
[2, 3, 7, 6],
[0, 4, 7, 3],
[1, 2, 6, 5],
];
for idx in &faces {
let edge1 = corners[idx[1]] - corners[idx[0]];
let edge2 = corners[idx[2]] - corners[idx[0]];
let normal = edge1
.cross(&edge2)
.try_normalize(1e-10)
.unwrap_or(Vector3::new(0.0, 0.0, 1.0));
let b = m.vertex_count() as u32;
m.add_vertex(corners[idx[0]], normal);
m.add_vertex(corners[idx[1]], normal);
m.add_vertex(corners[idx[2]], normal);
m.add_vertex(corners[idx[3]], normal);
m.add_triangle(b, b + 1, b + 2);
m.add_triangle(b, b + 2, b + 3);
}
m
}
fn make_framed_box_mesh(
origin: Point3<f64>,
depth_axis: Vector3<f64>,
cross_a: Vector3<f64>,
cross_b: Vector3<f64>,
depth: (f64, f64),
a: (f64, f64),
b: (f64, f64),
) -> Mesh {
let point =
|d: f64, av: f64, bv: f64| origin + depth_axis * d + cross_a * av + cross_b * bv;
let corners = [
point(depth.0, a.0, b.0),
point(depth.1, a.0, b.0),
point(depth.1, a.1, b.0),
point(depth.0, a.1, b.0),
point(depth.0, a.0, b.1),
point(depth.1, a.0, b.1),
point(depth.1, a.1, b.1),
point(depth.0, a.1, b.1),
];
let mut m = Mesh::with_capacity(24, 36);
let faces: [[usize; 4]; 6] = [
[0, 2, 1, 3],
[4, 5, 6, 7],
[0, 1, 5, 4],
[2, 3, 7, 6],
[0, 4, 7, 3],
[1, 2, 6, 5],
];
for idx in &faces {
let edge1 = corners[idx[1]] - corners[idx[0]];
let edge2 = corners[idx[2]] - corners[idx[0]];
let normal = edge1
.cross(&edge2)
.try_normalize(1e-10)
.unwrap_or(Vector3::new(0.0, 0.0, 1.0));
let b = m.vertex_count() as u32;
m.add_vertex(corners[idx[0]], normal);
m.add_vertex(corners[idx[1]], normal);
m.add_vertex(corners[idx[2]], normal);
m.add_vertex(corners[idx[3]], normal);
m.add_triangle(b, b + 1, b + 2);
m.add_triangle(b, b + 2, b + 3);
}
m
}
fn make_l_shape_prism_mesh() -> Mesh {
let z0 = 0.0;
let z1 = 1.0;
let footprint = [
(0.0_f64, 0.0_f64),
(4.0, 0.0),
(4.0, 2.0),
(2.0, 2.0),
(2.0, 4.0),
(0.0, 4.0),
];
let mut m = Mesh::new();
let n = footprint.len();
for i in 0..n {
let (x0, y0) = footprint[i];
let (x1, y1) = footprint[(i + 1) % n];
let edge = Vector3::new(x1 - x0, y1 - y0, 0.0);
let z_up = Vector3::new(0.0, 0.0, 1.0);
let normal = edge
.cross(&z_up)
.try_normalize(1e-10)
.unwrap_or(Vector3::new(1.0, 0.0, 0.0));
let p0 = Point3::new(x0, y0, z0);
let p1 = Point3::new(x1, y1, z0);
let p2 = Point3::new(x1, y1, z1);
let p3 = Point3::new(x0, y0, z1);
let b = m.vertex_count() as u32;
m.add_vertex(p0, normal);
m.add_vertex(p1, normal);
m.add_vertex(p2, normal);
m.add_vertex(p3, normal);
m.add_triangle(b, b + 1, b + 2);
m.add_triangle(b, b + 2, b + 3);
}
let bottom_n = Vector3::new(0.0, 0.0, -1.0);
let top_n = Vector3::new(0.0, 0.0, 1.0);
let bottom_base = m.vertex_count() as u32;
for &(x, y) in &footprint {
m.add_vertex(Point3::new(x, y, z0), bottom_n);
}
let top_base = m.vertex_count() as u32;
for &(x, y) in &footprint {
m.add_vertex(Point3::new(x, y, z1), top_n);
}
for i in 1..(n as u32 - 1) {
m.add_triangle(bottom_base, bottom_base + i + 1, bottom_base + i);
m.add_triangle(top_base, top_base + i, top_base + i + 1);
}
m
}
fn reveal_normals(mesh: &Mesh, pre_tri_count: usize) -> Vec<Vector3<f64>> {
let mut normals = Vec::new();
let indices = &mesh.indices[pre_tri_count * 3..];
for tri in indices.chunks_exact(3) {
let i = tri[0] as usize;
let nx = mesh.normals[i * 3] as f64;
let ny = mesh.normals[i * 3 + 1] as f64;
let nz = mesh.normals[i * 3 + 2] as f64;
normals.push(Vector3::new(nx, ny, nz));
}
normals
}
#[test]
fn test_reveals_generated_for_axis_aligned_opening() {
let wall_min = Point3::new(0.0, -0.15, 0.0);
let wall_max = Point3::new(10.0, 0.15, 3.0);
let open_min = Point3::new(4.0, -0.3, 1.0);
let open_max = Point3::new(6.0, 0.3, 2.5);
let mut mesh = Mesh::new();
let extrusion_dir = Vector3::new(0.0, 1.0, 0.0);
generate_reveal_quads(
&mut mesh,
&open_min,
&open_max,
&wall_min,
&wall_max,
Some(&extrusion_dir),
);
assert_eq!(
mesh.triangle_count(),
8,
"Expected 4 reveal quads (8 triangles)"
);
assert_eq!(mesh.vertex_count(), 16, "Expected 16 vertices (4 per quad)");
}
#[test]
fn test_reveal_normals_point_inward() {
let wall_min = Point3::new(0.0, -0.15, 0.0);
let wall_max = Point3::new(10.0, 0.15, 3.0);
let open_min = Point3::new(4.0, -0.3, 1.0);
let open_max = Point3::new(6.0, 0.3, 2.5);
let mut mesh = Mesh::new();
let dir = Vector3::new(0.0, 1.0, 0.0);
generate_reveal_quads(
&mut mesh,
&open_min,
&open_max,
&wall_min,
&wall_max,
Some(&dir),
);
let normals = reveal_normals(&mesh, 0);
let has_pos_x = normals.iter().any(|n| n.x > 0.5);
let has_neg_x = normals.iter().any(|n| n.x < -0.5);
let has_pos_z = normals.iter().any(|n| n.z > 0.5);
let has_neg_z = normals.iter().any(|n| n.z < -0.5);
assert!(has_pos_x, "Should have +X normal (left reveal)");
assert!(has_neg_x, "Should have −X normal (right reveal)");
assert!(has_pos_z, "Should have +Z normal (bottom reveal)");
assert!(has_neg_z, "Should have −Z normal (top reveal)");
}
#[test]
fn test_no_reveals_when_opening_at_wall_boundary() {
let wall_min = Point3::new(0.0, -0.15, 0.0);
let wall_max = Point3::new(10.0, 0.15, 3.0);
let open_min = Point3::new(0.0, -0.3, 0.0);
let open_max = Point3::new(10.0, 0.3, 2.1);
let mut mesh = Mesh::new();
let dir = Vector3::new(0.0, 1.0, 0.0);
generate_reveal_quads(
&mut mesh,
&open_min,
&open_max,
&wall_min,
&wall_max,
Some(&dir),
);
assert_eq!(
mesh.triangle_count(),
2,
"Only top reveal expected (1 quad = 2 tris)"
);
}
#[test]
fn test_reveals_with_extrusion_along_x() {
let wall_min = Point3::new(-0.15, 0.0, 0.0);
let wall_max = Point3::new(0.15, 10.0, 3.0);
let open_min = Point3::new(-0.3, 4.0, 1.0);
let open_max = Point3::new(0.3, 6.0, 2.5);
let mut mesh = Mesh::new();
let dir = Vector3::new(1.0, 0.0, 0.0);
generate_reveal_quads(
&mut mesh,
&open_min,
&open_max,
&wall_min,
&wall_max,
Some(&dir),
);
assert_eq!(mesh.triangle_count(), 8, "4 reveal quads for X-extrusion");
}
#[test]
fn test_reveals_with_extrusion_along_z() {
let wall_min = Point3::new(0.0, 0.0, -0.15);
let wall_max = Point3::new(10.0, 10.0, 0.15);
let open_min = Point3::new(3.0, 3.0, -0.3);
let open_max = Point3::new(5.0, 5.0, 0.3);
let mut mesh = Mesh::new();
let dir = Vector3::new(0.0, 0.0, 1.0);
generate_reveal_quads(
&mut mesh,
&open_min,
&open_max,
&wall_min,
&wall_max,
Some(&dir),
);
assert_eq!(mesh.triangle_count(), 8, "4 reveal quads for Z-extrusion");
}
#[test]
fn test_reveals_clamp_to_wall_depth() {
let wall_min = Point3::new(0.0, 0.0, 0.0);
let wall_max = Point3::new(10.0, 0.3, 3.0);
let open_min = Point3::new(4.0, -1.0, 1.0);
let open_max = Point3::new(6.0, 1.3, 2.5);
let mut mesh = Mesh::new();
let dir = Vector3::new(0.0, 1.0, 0.0);
generate_reveal_quads(
&mut mesh,
&open_min,
&open_max,
&wall_min,
&wall_max,
Some(&dir),
);
for chunk in mesh.positions.chunks_exact(3) {
let y = chunk[1] as f64;
assert!(
y >= -1e-3 && y <= 0.3 + 1e-3,
"Reveal vertex Y={y} should be within wall bounds [0.0, 0.3]"
);
}
}
#[test]
fn test_no_reveals_when_no_wall_thickness() {
let wall_min = Point3::new(0.0, 0.0, 0.0);
let wall_max = Point3::new(10.0, 0.0, 3.0);
let open_min = Point3::new(4.0, -0.1, 1.0);
let open_max = Point3::new(6.0, 0.1, 2.5);
let mut mesh = Mesh::new();
let dir = Vector3::new(0.0, 1.0, 0.0);
generate_reveal_quads(
&mut mesh,
&open_min,
&open_max,
&wall_min,
&wall_max,
Some(&dir),
);
assert_eq!(
mesh.triangle_count(),
0,
"No reveals for zero-thickness wall"
);
}
#[test]
fn test_no_reveals_when_opening_misses_submesh_on_cross_axis() {
let wall_min = Point3::new(0.0, -0.15, 0.0);
let wall_max = Point3::new(10.0, 0.15, 0.5);
let open_min = Point3::new(4.0, -0.3, 1.0);
let open_max = Point3::new(6.0, 0.3, 2.0);
let mut mesh = Mesh::new();
let dir = Vector3::new(0.0, 1.0, 0.0);
generate_reveal_quads(
&mut mesh,
&open_min,
&open_max,
&wall_min,
&wall_max,
Some(&dir),
);
assert_eq!(
mesh.triangle_count(),
0,
"No reveals when opening lies outside sub-mesh on a cross-axis"
);
}
#[test]
fn test_diagonal_reveals_do_not_expand_mesh_bounds() {
let origin = Point3::new(211.0, 124.0, 8.6);
let length_axis = Vector3::new(0.930469718224507, 0.36636880798889876, 0.0);
let thickness_axis = Vector3::new(0.36636880798889876, -0.930469718224507, 0.0);
let wall = make_oriented_box_mesh(
origin,
length_axis,
thickness_axis,
(0.0, 14.0),
(-0.15, 0.15),
(0.0, 2.88),
);
let opening = make_oriented_box_mesh(
origin,
length_axis,
thickness_axis,
(4.0, 6.0),
(-0.4, 0.4),
(0.9, 2.7),
);
let (before_min, before_max) = wall.bounds();
let mut result = wall;
let frame = infer_opening_frame(&opening, Some(&thickness_axis)).unwrap();
GeometryRouter::new().apply_diagonal_openings(
&mut result,
&[OpeningType::DiagonalRectangular(opening, frame)],
);
let (after_min, after_max) = result.bounds();
assert!(after_min.x >= before_min.x - 1e-3);
assert!(after_min.y >= before_min.y - 1e-3);
assert!(after_min.z >= before_min.z - 1e-3);
assert!(after_max.x <= before_max.x + 1e-3);
assert!(after_max.y <= before_max.y + 1e-3);
assert!(after_max.z <= before_max.z + 1e-3);
}
#[test]
fn test_rectangular_box_detector_accepts_clean_box() {
let opening = make_framed_box_mesh(
Point3::new(0.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
(-0.15, 0.15),
(-1.0, 1.0),
(0.0, 2.0),
);
assert!(is_rectangular_box_mesh(&opening));
}
#[test]
fn test_rectangular_box_detector_rejects_l_shape() {
let opening = make_l_shape_prism_mesh();
assert!(
!is_rectangular_box_mesh(&opening),
"rectilinear non-box footprints must fall through to NonRectangular CSG"
);
}
#[test]
fn test_rectangular_box_detector_rejects_trapezoid_extrusion() {
let mut positions: Vec<f32> = Vec::new();
let mut indices: Vec<u32> = Vec::new();
let push_v = |positions: &mut Vec<f32>, x: f32, y: f32, z: f32| {
positions.extend_from_slice(&[x, y, z]);
};
push_v(&mut positions, -0.3, 0.0, 0.0); push_v(&mut positions, 0.3, 0.0, 0.0); push_v(&mut positions, 0.5, 0.0, 2.0); push_v(&mut positions, -0.5, 0.0, 2.0); push_v(&mut positions, -0.3, 0.6, 0.0); push_v(&mut positions, 0.3, 0.6, 0.0); push_v(&mut positions, 0.5, 0.6, 2.0); push_v(&mut positions, -0.5, 0.6, 2.0); indices.extend_from_slice(&[0, 1, 2, 0, 2, 3]);
indices.extend_from_slice(&[5, 4, 7, 5, 7, 6]);
indices.extend_from_slice(&[4, 5, 1, 4, 1, 0]);
indices.extend_from_slice(&[3, 2, 6, 3, 6, 7]);
indices.extend_from_slice(&[1, 5, 6, 1, 6, 2]);
indices.extend_from_slice(&[4, 0, 3, 4, 3, 7]);
let mut mesh = Mesh::new();
mesh.positions = positions;
mesh.indices = indices;
assert!(
!is_rectangular_box_mesh(&mesh),
"trapezoid extrusion must be rejected — its slanted-side axis is \
not perpendicular to the top/bottom axis, so the AABB cutter would \
over-cut the host"
);
}
#[test]
fn test_rectangular_box_detector_accepts_rotated_box() {
let opening = make_framed_box_mesh(
Point3::new(0.0, 0.0, 0.0),
Vector3::new(0.7071067811865476, 0.7071067811865476, 0.0),
Vector3::new(-0.7071067811865476, 0.7071067811865476, 0.0),
Vector3::new(0.0, 0.0, 1.0),
(-0.15, 0.15),
(-1.0, 1.0),
(0.0, 2.0),
);
assert!(
is_rectangular_box_mesh(&opening),
"axis-rotated boxes must still be detected — rotation alone does \
not make them non-rectangular"
);
}
#[test]
fn test_infers_sloped_brep_opening_frame() {
let depth_axis = Vector3::new(0.0, -0.5, 0.8660254037844386);
let cross_a = Vector3::new(1.0, 0.0, 0.0);
let cross_b = depth_axis.cross(&cross_a).normalize();
let opening = make_framed_box_mesh(
Point3::new(10.0, 20.0, 5.0),
depth_axis,
cross_a,
cross_b,
(-0.2, 0.2),
(-0.8, 0.8),
(-0.4, 0.4),
);
let frame = infer_opening_frame(&opening, None).unwrap();
assert!(
frame.depth.dot(&depth_axis).abs() > 0.99,
"shortest inferred axis should be the sloped roof-window depth"
);
assert!(
frame.cross_a.dot(&cross_a).abs() > 0.99 || frame.cross_b.dot(&cross_a).abs() > 0.99,
"inferred frame should preserve the opening roll axis"
);
assert!(
!frame.is_axis_aligned(),
"sloped BRep opening should use the diagonal frame path"
);
}
#[test]
fn test_reveals_clamp_to_wall_on_orthogonal_cross_axis() {
let wall_min = Point3::new(0.0, -0.15, 0.0);
let wall_max = Point3::new(10.0, 0.15, 2.0);
let open_min = Point3::new(4.0, -0.3, 1.0);
let open_max = Point3::new(6.0, 0.3, 3.0);
let mut mesh = Mesh::new();
let dir = Vector3::new(0.0, 1.0, 0.0);
generate_reveal_quads(
&mut mesh,
&open_min,
&open_max,
&wall_min,
&wall_max,
Some(&dir),
);
for chunk in mesh.positions.chunks_exact(3) {
let z = chunk[2] as f64;
assert!(
z >= -1e-3 && z <= 2.0 + 1e-3,
"Reveal vertex Z={z} should stay within sub-mesh [0.0, 2.0]"
);
}
}
#[test]
fn test_extend_opening_pads_past_wall_on_exact_match() {
let router = crate::router::GeometryRouter::new();
let wall_min = Point3::new(0.0, 0.0, 0.0);
let wall_max = Point3::new(10.0, 0.2, 3.0);
let open_min = Point3::new(4.0, 0.0, 1.0);
let open_max = Point3::new(6.0, 0.2, 2.5);
let dir = Vector3::new(0.0, 1.0, 0.0);
let (new_min, new_max) =
router.extend_opening_along_direction(open_min, open_max, wall_min, wall_max, dir);
assert!(
new_min.y < wall_min.y,
"extended opening min Y {} must be strictly below wall min Y {}",
new_min.y,
wall_min.y,
);
assert!(
new_max.y > wall_max.y,
"extended opening max Y {} must be strictly above wall max Y {}",
new_max.y,
wall_max.y,
);
let back_pad = wall_min.y - new_min.y;
let fwd_pad = new_max.y - wall_max.y;
assert!(back_pad > 0.0 && back_pad < 1e-3);
assert!(fwd_pad > 0.0 && fwd_pad < 1e-3);
assert_eq!(new_min.x, open_min.x);
assert_eq!(new_max.x, open_max.x);
assert_eq!(new_min.z, open_min.z);
assert_eq!(new_max.z, open_max.z);
}
#[test]
fn test_extend_opening_skipped_when_opening_pokes_past_wall() {
let router = crate::router::GeometryRouter::new();
let wall_min = Point3::new(7.9, 0.0, 0.0);
let wall_max = Point3::new(8.1, 3.0, 3.0);
let open_min = Point3::new(8.0, 0.5, 1.0);
let open_max = Point3::new(8.2, 1.5, 2.0);
let dir = Vector3::new(1.0, 0.0, 0.0);
let (new_min, new_max) =
router.extend_opening_along_direction(open_min, open_max, wall_min, wall_max, dir);
assert_eq!(new_min, open_min, "X-poke-out: extension must not change min");
assert_eq!(new_max, open_max, "X-poke-out: extension must not change max");
let wall_min = Point3::new(5.9, 0.0, 0.0);
let wall_max = Point3::new(6.1, 3.0, 3.0);
let open_min = Point3::new(5.8, 0.5, 1.0);
let open_max = Point3::new(6.0, 1.5, 2.0);
let dir = Vector3::new(-1.0, 0.0, 0.0);
let (new_min, new_max) =
router.extend_opening_along_direction(open_min, open_max, wall_min, wall_max, dir);
assert_eq!(new_min, open_min, "-X-poke-out: extension must not change min");
assert_eq!(new_max, open_max, "-X-poke-out: extension must not change max");
}
#[test]
fn test_determine_extrusion_axis() {
let wmin = Point3::new(0.0, 0.0, 0.0);
let wmax = Point3::new(10.0, 0.3, 3.0);
assert_eq!(
determine_extrusion_axis(Some(&Vector3::new(1.0, 0.0, 0.0)), &wmin, &wmax),
0
);
assert_eq!(
determine_extrusion_axis(Some(&Vector3::new(0.0, 1.0, 0.0)), &wmin, &wmax),
1
);
assert_eq!(
determine_extrusion_axis(Some(&Vector3::new(0.0, 0.0, 1.0)), &wmin, &wmax),
2
);
assert_eq!(
determine_extrusion_axis(Some(&Vector3::new(0.7, 0.7, 0.0)), &wmin, &wmax),
0 );
assert_eq!(determine_extrusion_axis(None, &wmin, &wmax), 1);
}
}