use super::{face_set_slot, int_grid, int_list, to_zero_based, TessellatedFaceSet};
use crate::error::GeometryResult;
use crate::slots::Slots;
use ifc_model::{Entity, EntityId};
pub(crate) mod slot {
pub const NORMALS: usize = 1;
pub const CLOSED: usize = 2;
pub const COORD_INDEX: usize = 3;
pub const PN_INDEX: usize = 4;
}
#[derive(Debug, Clone, Copy)]
pub struct TriangulatedFaceSet<'m> {
slots: Slots<'m>,
}
impl<'m> TriangulatedFaceSet<'m> {
pub fn new(id: EntityId, entity: &'m Entity) -> Self {
Self {
slots: Slots::new(id, entity),
}
}
pub fn id(&self) -> EntityId {
self.slots.id()
}
pub fn base(&self) -> TessellatedFaceSet<'m> {
TessellatedFaceSet::from_slots(self.slots)
}
pub fn coordinates(&self) -> GeometryResult<EntityId> {
self.slots
.req_ref(face_set_slot::COORDINATES, "Coordinates")
}
pub fn closed(&self) -> Option<bool> {
self.slots.opt_bool(slot::CLOSED)
}
pub fn normals(&self) -> Option<Vec<[f64; 3]>> {
let value = self.slots.opt(slot::NORMALS)?;
let rows = value.as_list()?;
rows.iter()
.map(|row| {
let v = row.unwrap_typed().as_list()?;
if v.len() != 3 {
return None;
}
Some([
v[0].unwrap_typed().as_f64()?,
v[1].unwrap_typed().as_f64()?,
v[2].unwrap_typed().as_f64()?,
])
})
.collect()
}
pub fn triangles_1based(&self) -> GeometryResult<Vec<[i64; 3]>> {
let value = self.slots.req(slot::COORD_INDEX, "CoordIndex")?;
let rows = int_grid(value).ok_or_else(|| {
self.slots
.degenerate("CoordIndex is not a list of integer lists")
})?;
rows.into_iter()
.map(|row| {
<[i64; 3]>::try_from(row.as_slice()).map_err(|_| {
self.slots.degenerate(format!(
"CoordIndex entry has {} indices, but a triangle needs exactly 3",
row.len()
))
})
})
.collect()
}
pub fn triangles_0based(&self) -> GeometryResult<Vec<[usize; 3]>> {
let pn = self.pn_index_0based()?;
self.triangles_1based()?
.into_iter()
.map(|tri| {
let mut out = [0usize; 3];
for (slot_i, raw) in tri.iter().enumerate() {
let direct = to_zero_based(&self.slots, "CoordIndex", *raw)?;
out[slot_i] = match &pn {
Some(map) => *map.get(direct).ok_or_else(|| {
self.slots.degenerate(format!(
"CoordIndex {raw} is past the end of PnIndex ({} entries)",
map.len()
))
})?,
None => direct,
};
}
Ok(out)
})
.collect()
}
pub fn pn_index_1based(&self) -> Option<Vec<i64>> {
int_list(self.slots.opt(slot::PN_INDEX)?)
}
pub fn pn_index_0based(&self) -> GeometryResult<Option<Vec<usize>>> {
let Some(raw) = self.pn_index_1based() else {
return Ok(None);
};
raw.into_iter()
.map(|i| to_zero_based(&self.slots, "PnIndex", i))
.collect::<GeometryResult<Vec<_>>>()
.map(Some)
}
pub fn triangle_count(&self) -> GeometryResult<usize> {
Ok(self.triangles_1based()?.len())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::solid::testkit::{entity, int_grid as grid, ints, list, n, r};
use ifc_model::Value;
fn face_set(coord_index: Value, pn: Value) -> Entity {
entity(
"IFCTRIANGULATEDFACESET",
vec![r(50), Value::Null, Value::Bool(true), coord_index, pn],
)
}
#[test]
fn coord_index_follows_normals_and_closed_in_the_slot_order() {
let e = face_set(grid(&[&[1, 2, 3]]), Value::Null);
let view = TriangulatedFaceSet::new(EntityId(1), &e);
assert_eq!(view.coordinates().unwrap(), EntityId(50));
assert_eq!(view.closed(), Some(true));
assert_eq!(view.triangles_1based().unwrap(), vec![[1, 2, 3]]);
}
#[test]
fn coord_index_is_one_based_and_converts_to_zero_based_slice_indices() {
let e = face_set(grid(&[&[1, 2, 3], &[1, 3, 4]]), Value::Null);
let view = TriangulatedFaceSet::new(EntityId(1), &e);
assert_eq!(
view.triangles_1based().unwrap(),
vec![[1, 2, 3], [1, 3, 4]],
"the raw form must preserve the file's own numbers"
);
assert_eq!(
view.triangles_0based().unwrap(),
vec![[0, 1, 2], [0, 2, 3]],
"index 1 in the file is element 0 of a Rust slice"
);
}
#[test]
fn the_highest_one_based_index_maps_to_the_last_slice_element() {
let vertex_count = 4usize;
let e = face_set(grid(&[&[2, 3, 4]]), Value::Null);
let view = TriangulatedFaceSet::new(EntityId(1), &e);
let tri = view.triangles_0based().unwrap()[0];
assert_eq!(tri, [1, 2, 3]);
assert!(
tri.iter().all(|i| *i < vertex_count),
"a valid file's converted indices must all be in range"
);
}
#[test]
fn a_zero_coord_index_is_rejected_rather_than_wrapping() {
let e = face_set(grid(&[&[0, 1, 2]]), Value::Null);
let view = TriangulatedFaceSet::new(EntityId(9), &e);
assert!(view.triangles_1based().is_ok(), "raw form stays readable");
let err = view.triangles_0based().unwrap_err();
assert_eq!(err.entity(), Some(EntityId(9)));
}
#[test]
fn pn_index_indirection_is_applied_not_skipped() {
let e = face_set(grid(&[&[1, 2, 3]]), ints(&[4, 3, 2, 1]));
let view = TriangulatedFaceSet::new(EntityId(1), &e);
assert_eq!(view.pn_index_1based().unwrap(), vec![4, 3, 2, 1]);
assert_eq!(view.pn_index_0based().unwrap().unwrap(), vec![3, 2, 1, 0]);
assert_eq!(
view.triangles_0based().unwrap(),
vec![[3, 2, 1]],
"CoordIndex 1,2,3 selects PnIndex entries 4,3,2 -> slice 3,2,1"
);
}
#[test]
fn without_pn_index_the_coord_index_addresses_coordinates_directly() {
let e = face_set(grid(&[&[1, 2, 3]]), Value::Null);
let view = TriangulatedFaceSet::new(EntityId(1), &e);
assert_eq!(view.pn_index_1based(), None);
assert_eq!(view.pn_index_0based().unwrap(), None);
assert_eq!(view.triangles_0based().unwrap(), vec![[0, 1, 2]]);
}
#[test]
fn an_absent_closed_flag_is_distinguishable_from_an_explicit_false() {
let stated = entity(
"IFCTRIANGULATEDFACESET",
vec![r(50), Value::Null, Value::Bool(false), grid(&[&[1, 2, 3]])],
);
assert_eq!(
TriangulatedFaceSet::new(EntityId(1), &stated).closed(),
Some(false)
);
let unstated = entity(
"IFCTRIANGULATEDFACESET",
vec![r(50), Value::Null, Value::Null, grid(&[&[1, 2, 3]])],
);
assert_eq!(
TriangulatedFaceSet::new(EntityId(1), &unstated).closed(),
None
);
}
#[test]
fn a_face_without_exactly_three_indices_is_degenerate() {
let e = face_set(grid(&[&[1, 2, 3, 4]]), Value::Null);
let err = TriangulatedFaceSet::new(EntityId(4), &e)
.triangles_1based()
.unwrap_err();
assert!(err.to_string().contains("exactly 3"));
}
#[test]
fn normals_are_returned_as_written_without_reinterpretation() {
let e = entity(
"IFCTRIANGULATEDFACESET",
vec![
r(50),
list(vec![list(vec![n(0.0), n(0.0), n(1.0)])]),
Value::Bool(true),
grid(&[&[1, 2, 3]]),
],
);
let view = TriangulatedFaceSet::new(EntityId(1), &e);
assert_eq!(view.normals().unwrap(), vec![[0.0, 0.0, 1.0]]);
assert_eq!(view.triangle_count().unwrap(), 1);
}
}