pub mod polygonal;
pub mod triangulated;
pub use polygonal::{IndexedPolygonalFace, IndexedPolygonalFaceWithVoids, PolygonalFaceSet};
pub use triangulated::TriangulatedFaceSet;
use crate::error::{GeometryError, GeometryResult};
use crate::slots::Slots;
use ifc_model::{Entity, EntityId, Value};
pub(super) mod face_set_slot {
pub const COORDINATES: usize = 0;
}
#[derive(Debug, Clone, Copy)]
pub struct TessellatedItem<'m> {
slots: Slots<'m>,
}
impl<'m> TessellatedItem<'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 is_face_set(&self) -> bool {
let n = self.type_name();
n.eq_ignore_ascii_case("IFCTRIANGULATEDFACESET")
|| n.eq_ignore_ascii_case("IFCPOLYGONALFACESET")
}
}
#[derive(Debug, Clone, Copy)]
pub struct TessellatedFaceSet<'m> {
slots: Slots<'m>,
}
impl<'m> TessellatedFaceSet<'m> {
pub fn new(id: EntityId, entity: &'m Entity) -> Self {
Self {
slots: Slots::new(id, entity),
}
}
pub(super) fn from_slots(slots: Slots<'m>) -> Self {
Self { slots }
}
pub fn id(&self) -> EntityId {
self.slots.id()
}
pub fn type_name(&self) -> &'m str {
self.slots.type_name()
}
pub fn coordinates(&self) -> GeometryResult<EntityId> {
self.slots
.req_ref(face_set_slot::COORDINATES, "Coordinates")
}
}
pub(super) fn to_zero_based(
slots: &Slots<'_>,
attribute: &'static str,
index: i64,
) -> GeometryResult<usize> {
if index < 1 {
return Err(GeometryError::Degenerate {
entity: slots.id(),
type_name: slots.type_name().to_string(),
detail: format!(
"{attribute} holds {index}, but IFC indices are 1-based and must be at least 1"
),
});
}
Ok((index - 1) as usize)
}
pub(super) fn int_list(value: &Value) -> Option<Vec<i64>> {
let items = value.as_list()?;
items
.iter()
.map(|v| match v.unwrap_typed() {
Value::Integer(i) => Some(*i),
Value::Real(r) => Some(*r as i64),
_ => None,
})
.collect()
}
pub(super) fn int_grid(value: &Value) -> Option<Vec<Vec<i64>>> {
let rows = value.as_list()?;
rows.iter()
.map(|row| int_list(row.unwrap_typed()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::solid::testkit::{entity, ints, r};
#[test]
fn coordinates_is_slot_zero_for_both_concrete_face_sets() {
for name in ["IFCTRIANGULATEDFACESET", "IFCPOLYGONALFACESET"] {
let e = entity(name, vec![r(50)]);
assert_eq!(
TessellatedFaceSet::new(EntityId(1), &e)
.coordinates()
.unwrap(),
EntityId(50),
"{name}"
);
}
}
#[test]
fn an_indexed_polygonal_face_is_a_tessellated_item_but_not_a_face_set() {
let face = entity("IFCINDEXEDPOLYGONALFACE", vec![ints(&[1, 2, 3])]);
assert!(!TessellatedItem::new(EntityId(1), &face).is_face_set());
let set = entity("IFCPOLYGONALFACESET", vec![r(1)]);
assert!(TessellatedItem::new(EntityId(1), &set).is_face_set());
}
#[test]
fn a_zero_index_is_rejected_instead_of_wrapping_to_usize_max() {
let e = entity("IFCTRIANGULATEDFACESET", vec![]);
let slots = Slots::new(EntityId(3), &e);
assert_eq!(to_zero_based(&slots, "CoordIndex", 1).unwrap(), 0);
assert_eq!(to_zero_based(&slots, "CoordIndex", 7).unwrap(), 6);
for bad in [0, -1] {
let err = to_zero_based(&slots, "CoordIndex", bad).unwrap_err();
assert_eq!(err.entity(), Some(EntityId(3)));
assert!(err.to_string().contains("1-based"));
}
}
}