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 set_slot {
pub const CLOSED: usize = 1;
pub const FACES: usize = 2;
pub const PN_INDEX: usize = 3;
}
pub(crate) mod face_slot {
pub const COORD_INDEX: usize = 0;
pub const INNER_COORD_INDICES: usize = 1;
}
#[derive(Debug, Clone, Copy)]
pub struct PolygonalFaceSet<'m> {
slots: Slots<'m>,
}
impl<'m> PolygonalFaceSet<'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(set_slot::CLOSED)
}
pub fn faces(&self) -> GeometryResult<Vec<EntityId>> {
self.slots.req_ref_list(set_slot::FACES, "Faces")
}
pub fn pn_index_1based(&self) -> Option<Vec<i64>> {
int_list(self.slots.opt(set_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)
}
}
#[derive(Debug, Clone, Copy)]
pub struct IndexedPolygonalFace<'m> {
slots: Slots<'m>,
}
impl<'m> IndexedPolygonalFace<'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 type_name(&self) -> &'m str {
self.slots.type_name()
}
pub fn has_voids(&self) -> bool {
self.type_name()
.eq_ignore_ascii_case("IFCINDEXEDPOLYGONALFACEWITHVOIDS")
}
pub fn outer_loop_1based(&self) -> GeometryResult<Vec<i64>> {
let value = self.slots.req(face_slot::COORD_INDEX, "CoordIndex")?;
int_list(value).ok_or_else(|| {
self.slots
.degenerate("CoordIndex is not a list of integers")
})
}
pub fn outer_loop_0based(&self) -> GeometryResult<Vec<usize>> {
self.outer_loop_1based()?
.into_iter()
.map(|i| to_zero_based(&self.slots, "CoordIndex", i))
.collect()
}
}
#[derive(Debug, Clone, Copy)]
pub struct IndexedPolygonalFaceWithVoids<'m> {
slots: Slots<'m>,
}
impl<'m> IndexedPolygonalFaceWithVoids<'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) -> IndexedPolygonalFace<'m> {
IndexedPolygonalFace { slots: self.slots }
}
pub fn inner_loops_1based(&self) -> GeometryResult<Vec<Vec<i64>>> {
let value = self
.slots
.req(face_slot::INNER_COORD_INDICES, "InnerCoordIndices")?;
int_grid(value).ok_or_else(|| {
self.slots
.degenerate("InnerCoordIndices is not a list of integer lists")
})
}
pub fn inner_loops_0based(&self) -> GeometryResult<Vec<Vec<usize>>> {
self.inner_loops_1based()?
.into_iter()
.map(|loop_| {
loop_
.into_iter()
.map(|i| to_zero_based(&self.slots, "InnerCoordIndices", i))
.collect()
})
.collect()
}
pub fn void_count(&self) -> GeometryResult<usize> {
Ok(self.inner_loops_1based()?.len())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::solid::testkit::{entity, int_grid as grid, ints, r, refs};
use ifc_model::Value;
#[test]
fn closed_precedes_faces_unlike_the_triangulated_layout() {
let e = entity(
"IFCPOLYGONALFACESET",
vec![r(50), Value::Bool(true), refs(&[10, 11]), Value::Null],
);
let view = PolygonalFaceSet::new(EntityId(1), &e);
assert_eq!(view.coordinates().unwrap(), EntityId(50));
assert_eq!(view.closed(), Some(true));
assert_eq!(view.faces().unwrap(), vec![EntityId(10), EntityId(11)]);
}
#[test]
fn face_set_pn_index_applies_the_same_one_based_rule() {
let e = entity(
"IFCPOLYGONALFACESET",
vec![r(50), Value::Bool(true), refs(&[10]), ints(&[3, 1, 2])],
);
let view = PolygonalFaceSet::new(EntityId(1), &e);
assert_eq!(view.pn_index_1based().unwrap(), vec![3, 1, 2]);
assert_eq!(view.pn_index_0based().unwrap().unwrap(), vec![2, 0, 1]);
}
#[test]
fn a_face_set_without_pn_index_reports_none() {
let e = entity(
"IFCPOLYGONALFACESET",
vec![r(50), Value::Bool(true), refs(&[10])],
);
let view = PolygonalFaceSet::new(EntityId(1), &e);
assert_eq!(view.pn_index_0based().unwrap(), None);
}
#[test]
fn a_face_outer_loop_converts_from_one_based_to_zero_based() {
let e = entity("IFCINDEXEDPOLYGONALFACE", vec![ints(&[1, 2, 3, 4])]);
let view = IndexedPolygonalFace::new(EntityId(1), &e);
assert_eq!(view.outer_loop_1based().unwrap(), vec![1, 2, 3, 4]);
assert_eq!(view.outer_loop_0based().unwrap(), vec![0, 1, 2, 3]);
assert!(!view.has_voids());
}
#[test]
fn a_zero_index_in_a_face_loop_is_rejected() {
let e = entity("IFCINDEXEDPOLYGONALFACE", vec![ints(&[0, 1, 2])]);
let err = IndexedPolygonalFace::new(EntityId(8), &e)
.outer_loop_0based()
.unwrap_err();
assert_eq!(err.entity(), Some(EntityId(8)));
}
#[test]
fn inner_loops_are_never_merged_into_the_outer_loop() {
let e = entity(
"IFCINDEXEDPOLYGONALFACEWITHVOIDS",
vec![
ints(&[1, 2, 3, 4]),
grid(&[&[5, 6, 7, 8], &[9, 10, 11, 12]]),
],
);
let view = IndexedPolygonalFaceWithVoids::new(EntityId(1), &e);
let outer = view.base().outer_loop_0based().unwrap();
let inners = view.inner_loops_0based().unwrap();
assert_eq!(outer, vec![0, 1, 2, 3]);
assert_eq!(inners, vec![vec![4, 5, 6, 7], vec![8, 9, 10, 11]]);
assert_eq!(view.void_count().unwrap(), 2);
for inner in &inners {
for i in inner {
assert!(
!outer.contains(i),
"hole index {i} leaked into the outer loop"
);
}
}
}
#[test]
fn a_face_with_voids_is_distinguishable_from_a_plain_face() {
let plain = entity("IFCINDEXEDPOLYGONALFACE", vec![ints(&[1, 2, 3])]);
let voided = entity(
"IFCINDEXEDPOLYGONALFACEWITHVOIDS",
vec![ints(&[1, 2, 3, 4]), grid(&[&[5, 6, 7]])],
);
assert!(!IndexedPolygonalFace::new(EntityId(1), &plain).has_voids());
assert!(IndexedPolygonalFace::new(EntityId(1), &voided).has_voids());
}
#[test]
fn missing_inner_coord_indices_is_an_error_not_an_empty_hole_list() {
let e = entity("IFCINDEXEDPOLYGONALFACEWITHVOIDS", vec![ints(&[1, 2, 3])]);
let view = IndexedPolygonalFaceWithVoids::new(EntityId(2), &e);
assert!(view.inner_loops_1based().is_err());
assert!(view.base().outer_loop_1based().is_ok());
}
}