use crate::{Error, Mesh, Result};
use ifc_lite_core::{AttributeValue, DecodedEntity, EntityDecoder, IfcSchema, IfcType};
use crate::router::GeometryProcessor;
pub struct TriangulatedFaceSetProcessor;
impl TriangulatedFaceSetProcessor {
pub fn new() -> Self {
Self
}
}
impl GeometryProcessor for TriangulatedFaceSetProcessor {
#[inline]
fn process(
&self,
entity: &DecodedEntity,
decoder: &mut EntityDecoder,
_schema: &IfcSchema,
) -> Result<Mesh> {
let coords_attr = entity.get(0).ok_or_else(|| {
Error::geometry("TriangulatedFaceSet missing Coordinates".to_string())
})?;
let coord_entity_id = coords_attr.as_entity_ref().ok_or_else(|| {
Error::geometry("Expected entity reference for Coordinates".to_string())
})?;
use ifc_lite_core::{extract_coordinate_list_from_entity, parse_indices_direct};
let positions = if let Some(raw_bytes) = decoder.get_raw_bytes(coord_entity_id) {
extract_coordinate_list_from_entity(raw_bytes).unwrap_or_default()
} else {
let coords_entity = decoder.decode_by_id(coord_entity_id)?;
let coord_list_attr = coords_entity.get(0).ok_or_else(|| {
Error::geometry("CartesianPointList3D missing CoordList".to_string())
})?;
let coord_list = coord_list_attr
.as_list()
.ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
use ifc_lite_core::AttributeValue;
AttributeValue::parse_coordinate_list_3d(coord_list)
};
let indices_attr = entity
.get(3)
.ok_or_else(|| Error::geometry("TriangulatedFaceSet missing CoordIndex".to_string()))?;
let indices = if let Some(raw_entity_bytes) = decoder.get_raw_bytes(entity.id) {
if let Some(coord_index_bytes) = super::extract_coord_index_bytes(raw_entity_bytes) {
parse_indices_direct(coord_index_bytes)
} else {
let face_list = indices_attr
.as_list()
.ok_or_else(|| Error::geometry("Expected face index list".to_string()))?;
use ifc_lite_core::AttributeValue;
AttributeValue::parse_index_list(face_list)
}
} else {
let face_list = indices_attr
.as_list()
.ok_or_else(|| Error::geometry("Expected face index list".to_string()))?;
use ifc_lite_core::AttributeValue;
AttributeValue::parse_index_list(face_list)
};
Ok(Mesh {
positions,
normals: Vec::new(),
indices,
})
}
fn supported_types(&self) -> Vec<IfcType> {
vec![IfcType::IfcTriangulatedFaceSet]
}
}
impl Default for TriangulatedFaceSetProcessor {
fn default() -> Self {
Self::new()
}
}
pub struct PolygonalFaceSetProcessor;
impl PolygonalFaceSetProcessor {
pub fn new() -> Self {
Self
}
#[inline]
fn parse_index_loop(indices: &[AttributeValue], pn_index: Option<&[u32]>) -> Vec<u32> {
indices
.iter()
.filter_map(|value| {
let idx = value.as_int()?;
if idx <= 0 {
return None;
}
let idx = idx as usize;
if let Some(remap) = pn_index {
remap.get(idx - 1).copied().filter(|mapped| *mapped > 0)
} else {
Some(idx as u32)
}
})
.collect()
}
fn triangulate_polygon(
outer_indices: &[u32],
inner_indices: &[Vec<u32>],
positions: &[f32],
output: &mut Vec<u32>,
) {
if outer_indices.len() < 3 {
return;
}
let get_pos = |idx: u32| -> Option<(f32, f32, f32)> {
if idx == 0 {
return None;
}
let base = ((idx - 1) * 3) as usize;
if base + 2 < positions.len() {
Some((positions[base], positions[base + 1], positions[base + 2]))
} else {
None
}
};
if outer_indices.is_empty() {
return;
}
let mut sum_x = 0.0f64;
let mut sum_y = 0.0f64;
let mut sum_z = 0.0f64;
for i in 0..outer_indices.len() {
let v0 = match get_pos(outer_indices[i]) {
Some(p) => p,
None => {
let first = outer_indices[0] - 1;
for j in 1..outer_indices.len() - 1 {
output.push(first);
output.push(outer_indices[j] - 1);
output.push(outer_indices[j + 1] - 1);
}
return;
}
};
let v1 = match get_pos(outer_indices[(i + 1) % outer_indices.len()]) {
Some(p) => p,
None => {
let first = outer_indices[0] - 1;
for j in 1..outer_indices.len() - 1 {
output.push(first);
output.push(outer_indices[j] - 1);
output.push(outer_indices[j + 1] - 1);
}
return;
}
};
sum_x += (v0.1 - v1.1) as f64 * (v0.2 + v1.2) as f64;
sum_y += (v0.2 - v1.2) as f64 * (v0.0 + v1.0) as f64;
sum_z += (v0.0 - v1.0) as f64 * (v0.1 + v1.1) as f64;
}
let expected_normal = (sum_x, sum_y, sum_z);
let mut push_oriented_triangle = |a: u32, b: u32, c: u32| {
if a == 0 || b == 0 || c == 0 {
return;
}
let i0 = a - 1;
let mut i1 = b - 1;
let mut i2 = c - 1;
if expected_normal.0.abs() + expected_normal.1.abs() + expected_normal.2.abs() > 1e-12 {
if let (Some(p0), Some(p1), Some(p2)) = (get_pos(a), get_pos(b), get_pos(c)) {
let e1 = (
(p1.0 - p0.0) as f64,
(p1.1 - p0.1) as f64,
(p1.2 - p0.2) as f64,
);
let e2 = (
(p2.0 - p0.0) as f64,
(p2.1 - p0.1) as f64,
(p2.2 - p0.2) as f64,
);
let tri_normal = (
e1.1 * e2.2 - e1.2 * e2.1,
e1.2 * e2.0 - e1.0 * e2.2,
e1.0 * e2.1 - e1.1 * e2.0,
);
let dot = tri_normal.0 * expected_normal.0
+ tri_normal.1 * expected_normal.1
+ tri_normal.2 * expected_normal.2;
if dot < 0.0 {
std::mem::swap(&mut i1, &mut i2);
}
}
}
output.push(i0);
output.push(i1);
output.push(i2);
};
if inner_indices.is_empty() && outer_indices.len() == 3 {
push_oriented_triangle(outer_indices[0], outer_indices[1], outer_indices[2]);
return;
}
if inner_indices.is_empty() && outer_indices.len() == 4 {
push_oriented_triangle(outer_indices[0], outer_indices[1], outer_indices[2]);
push_oriented_triangle(outer_indices[0], outer_indices[2], outer_indices[3]);
return;
}
let abs_x = sum_x.abs();
let abs_y = sum_y.abs();
let abs_z = sum_z.abs();
let valid_holes: Vec<&[u32]> = inner_indices
.iter()
.filter(|loop_indices| loop_indices.len() >= 3)
.map(|loop_indices| loop_indices.as_slice())
.collect();
let total_vertices =
outer_indices.len() + valid_holes.iter().map(|loop_indices| loop_indices.len()).sum::<usize>();
let mut coords_2d: Vec<f64> = Vec::with_capacity(total_vertices * 2);
let mut flattened_indices: Vec<u32> = Vec::with_capacity(total_vertices);
let mut hole_starts: Vec<usize> = Vec::with_capacity(valid_holes.len());
for &idx in outer_indices {
let Some(p) = get_pos(idx) else {
let first = outer_indices[0];
for i in 1..outer_indices.len() - 1 {
push_oriented_triangle(first, outer_indices[i], outer_indices[i + 1]);
}
return;
};
flattened_indices.push(idx);
if abs_z >= abs_x && abs_z >= abs_y {
coords_2d.push(p.0 as f64);
coords_2d.push(p.1 as f64);
} else if abs_y >= abs_x {
coords_2d.push(p.0 as f64);
coords_2d.push(p.2 as f64);
} else {
coords_2d.push(p.1 as f64);
coords_2d.push(p.2 as f64);
}
}
for hole in valid_holes {
hole_starts.push(flattened_indices.len());
for &idx in hole {
let Some(p) = get_pos(idx) else {
let first = outer_indices[0];
for i in 1..outer_indices.len() - 1 {
push_oriented_triangle(first, outer_indices[i], outer_indices[i + 1]);
}
return;
};
flattened_indices.push(idx);
if abs_z >= abs_x && abs_z >= abs_y {
coords_2d.push(p.0 as f64);
coords_2d.push(p.1 as f64);
} else if abs_y >= abs_x {
coords_2d.push(p.0 as f64);
coords_2d.push(p.2 as f64);
} else {
coords_2d.push(p.1 as f64);
coords_2d.push(p.2 as f64);
}
}
}
if flattened_indices.len() < 3 {
return;
}
match earcutr::earcut(&coords_2d, &hole_starts, 2) {
Ok(tri_indices) => {
for tri in tri_indices.chunks(3) {
if tri.len() != 3
|| tri[0] >= flattened_indices.len()
|| tri[1] >= flattened_indices.len()
|| tri[2] >= flattened_indices.len()
{
continue;
}
push_oriented_triangle(
flattened_indices[tri[0]],
flattened_indices[tri[1]],
flattened_indices[tri[2]],
);
}
}
Err(_) => {
let first = outer_indices[0];
for i in 1..outer_indices.len() - 1 {
push_oriented_triangle(first, outer_indices[i], outer_indices[i + 1]);
}
}
}
}
#[inline]
fn parse_face_inner_indices(face_entity: &DecodedEntity, pn_index: Option<&[u32]>) -> Vec<Vec<u32>> {
if face_entity.ifc_type != IfcType::IfcIndexedPolygonalFaceWithVoids {
return Vec::new();
}
let Some(inner_attr) = face_entity.get(1).and_then(|a| a.as_list()) else {
return Vec::new();
};
let mut result = Vec::with_capacity(inner_attr.len());
for loop_attr in inner_attr {
let Some(loop_values) = loop_attr.as_list() else {
continue;
};
let parsed = Self::parse_index_loop(loop_values, pn_index);
if parsed.len() >= 3 {
result.push(parsed);
}
}
result
}
#[inline]
fn orient_closed_shell_outward(positions: &[f32], indices: &mut [u32]) {
if indices.len() < 3 || positions.len() < 9 {
return;
}
let vertex_count = positions.len() / 3;
if vertex_count == 0 {
return;
}
let mut cx = 0.0f64;
let mut cy = 0.0f64;
let mut cz = 0.0f64;
for p in positions.chunks_exact(3) {
cx += p[0] as f64;
cy += p[1] as f64;
cz += p[2] as f64;
}
let inv_n = 1.0 / vertex_count as f64;
cx *= inv_n;
cy *= inv_n;
cz *= inv_n;
let mut sign_accum = 0.0f64;
for tri in indices.chunks_exact(3) {
let i0 = tri[0] as usize;
let i1 = tri[1] as usize;
let i2 = tri[2] as usize;
if i0 >= vertex_count || i1 >= vertex_count || i2 >= vertex_count {
continue;
}
let p0 = (
positions[i0 * 3] as f64,
positions[i0 * 3 + 1] as f64,
positions[i0 * 3 + 2] as f64,
);
let p1 = (
positions[i1 * 3] as f64,
positions[i1 * 3 + 1] as f64,
positions[i1 * 3 + 2] as f64,
);
let p2 = (
positions[i2 * 3] as f64,
positions[i2 * 3 + 1] as f64,
positions[i2 * 3 + 2] as f64,
);
let e1 = (p1.0 - p0.0, p1.1 - p0.1, p1.2 - p0.2);
let e2 = (p2.0 - p0.0, p2.1 - p0.1, p2.2 - p0.2);
let n = (
e1.1 * e2.2 - e1.2 * e2.1,
e1.2 * e2.0 - e1.0 * e2.2,
e1.0 * e2.1 - e1.1 * e2.0,
);
let tc = (
(p0.0 + p1.0 + p2.0) / 3.0,
(p0.1 + p1.1 + p2.1) / 3.0,
(p0.2 + p1.2 + p2.2) / 3.0,
);
let out = (tc.0 - cx, tc.1 - cy, tc.2 - cz);
sign_accum += n.0 * out.0 + n.1 * out.1 + n.2 * out.2;
}
if sign_accum < 0.0 {
for tri in indices.chunks_exact_mut(3) {
tri.swap(1, 2);
}
}
}
#[inline]
fn build_flat_shaded_mesh(positions: &[f32], indices: &[u32]) -> Mesh {
let mut flat_positions: Vec<f32> = Vec::with_capacity(indices.len() * 3);
let mut flat_normals: Vec<f32> = Vec::with_capacity(indices.len() * 3);
let mut flat_indices: Vec<u32> = Vec::with_capacity(indices.len());
let vertex_count = positions.len() / 3;
let mut next_index: u32 = 0;
for tri in indices.chunks_exact(3) {
let i0 = tri[0] as usize;
let i1 = tri[1] as usize;
let i2 = tri[2] as usize;
if i0 >= vertex_count || i1 >= vertex_count || i2 >= vertex_count {
continue;
}
let p0 = (
positions[i0 * 3] as f64,
positions[i0 * 3 + 1] as f64,
positions[i0 * 3 + 2] as f64,
);
let p1 = (
positions[i1 * 3] as f64,
positions[i1 * 3 + 1] as f64,
positions[i1 * 3 + 2] as f64,
);
let p2 = (
positions[i2 * 3] as f64,
positions[i2 * 3 + 1] as f64,
positions[i2 * 3 + 2] as f64,
);
let e1 = (p1.0 - p0.0, p1.1 - p0.1, p1.2 - p0.2);
let e2 = (p2.0 - p0.0, p2.1 - p0.1, p2.2 - p0.2);
let nx = e1.1 * e2.2 - e1.2 * e2.1;
let ny = e1.2 * e2.0 - e1.0 * e2.2;
let nz = e1.0 * e2.1 - e1.1 * e2.0;
let len = (nx * nx + ny * ny + nz * nz).sqrt();
let (nx, ny, nz) = if len > 1e-12 {
(nx / len, ny / len, nz / len)
} else {
(0.0, 0.0, 1.0)
};
for &idx in &[i0, i1, i2] {
flat_positions.push(positions[idx * 3]);
flat_positions.push(positions[idx * 3 + 1]);
flat_positions.push(positions[idx * 3 + 2]);
flat_normals.push(nx as f32);
flat_normals.push(ny as f32);
flat_normals.push(nz as f32);
flat_indices.push(next_index);
next_index += 1;
}
}
Mesh {
positions: flat_positions,
normals: flat_normals,
indices: flat_indices,
}
}
}
impl GeometryProcessor for PolygonalFaceSetProcessor {
fn process(
&self,
entity: &DecodedEntity,
decoder: &mut EntityDecoder,
_schema: &IfcSchema,
) -> Result<Mesh> {
let coords_attr = entity.get(0).ok_or_else(|| {
Error::geometry("PolygonalFaceSet missing Coordinates".to_string())
})?;
let coord_entity_id = coords_attr.as_entity_ref().ok_or_else(|| {
Error::geometry("Expected entity reference for Coordinates".to_string())
})?;
use ifc_lite_core::extract_coordinate_list_from_entity;
let positions = if let Some(raw_bytes) = decoder.get_raw_bytes(coord_entity_id) {
extract_coordinate_list_from_entity(raw_bytes).unwrap_or_default()
} else {
let coords_entity = decoder.decode_by_id(coord_entity_id)?;
let coord_list_attr = coords_entity.get(0).ok_or_else(|| {
Error::geometry("CartesianPointList3D missing CoordList".to_string())
})?;
let coord_list = coord_list_attr
.as_list()
.ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
AttributeValue::parse_coordinate_list_3d(coord_list)
};
if positions.is_empty() {
return Ok(Mesh::new());
}
let faces_attr = entity.get(2).ok_or_else(|| {
Error::geometry("PolygonalFaceSet missing Faces".to_string())
})?;
let face_refs = faces_attr
.as_list()
.ok_or_else(|| Error::geometry("Expected faces list".to_string()))?;
let pn_index = entity
.get(3)
.and_then(|attr| attr.as_list())
.map(|list| {
list.iter()
.filter_map(|value| value.as_int())
.filter(|v| *v > 0)
.map(|v| v as u32)
.collect::<Vec<u32>>()
});
let mut indices = Vec::with_capacity(face_refs.len() * 6);
for face_ref in face_refs {
let face_id = face_ref.as_entity_ref().ok_or_else(|| {
Error::geometry("Expected entity reference for face".to_string())
})?;
let face_entity = decoder.decode_by_id(face_id)?;
let coord_index_attr = face_entity.get(0).ok_or_else(|| {
Error::geometry("IndexedPolygonalFace missing CoordIndex".to_string())
})?;
let coord_indices = coord_index_attr
.as_list()
.ok_or_else(|| Error::geometry("Expected coord index list".to_string()))?;
let face_indices = Self::parse_index_loop(coord_indices, pn_index.as_deref());
if face_indices.len() < 3 {
continue;
}
let inner_indices = Self::parse_face_inner_indices(&face_entity, pn_index.as_deref());
Self::triangulate_polygon(&face_indices, &inner_indices, &positions, &mut indices);
}
let is_closed = entity
.get(1)
.and_then(|a| a.as_enum())
.map(|v| v == "T")
.unwrap_or(false);
if is_closed {
Self::orient_closed_shell_outward(&positions, &mut indices);
}
Ok(Self::build_flat_shaded_mesh(&positions, &indices))
}
fn supported_types(&self) -> Vec<IfcType> {
vec![IfcType::IfcPolygonalFaceSet]
}
}
impl Default for PolygonalFaceSetProcessor {
fn default() -> Self {
Self::new()
}
}