pub mod polygonal;
pub mod triangulated;
pub use polygonal::{IndexedPolygonalFace, IndexedPolygonalFaceWithVoids, PolygonalFaceSet};
pub use triangulated::TriangulatedFaceSet;
use crate::error::{GeometryError, GeometryResult};
use crate::resource::point::CartesianPointList3D;
use crate::resource::resolve;
use crate::slots::Slots;
use ifc_model::{Entity, EntityId, Model, 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 fn coordinate_list<'v>(
&self,
model: &'v Model,
) -> GeometryResult<CartesianPointList3D<'v>> {
resolve::cartesian_point_list_3d(model, self.id(), self.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 the_coordinate_list_resolves_and_serves_one_based_indices() {
use crate::solid::testkit::{list, model, n};
let row = |x: f64| list(vec![n(x), n(0.0), n(0.0)]);
let coords = entity(
"IFCCARTESIANPOINTLIST3D",
vec![list(vec![row(1.0), row(2.0)])],
);
let m = model(vec![(50, coords)]);
let e = entity("IFCTRIANGULATEDFACESET", vec![r(50)]);
let list = TessellatedFaceSet::new(EntityId(1), &e)
.coordinate_list(&m)
.unwrap();
assert_eq!(list.point(2).unwrap(), Some([2.0, 0.0, 0.0]));
}
#[test]
fn a_two_dimensional_or_dangling_coordinate_list_is_a_typed_error() {
use crate::solid::testkit::{list, model};
let m = model(vec![(
50,
entity("IFCCARTESIANPOINTLIST2D", vec![list(vec![])]),
)]);
let e = entity("IFCPOLYGONALFACESET", vec![r(50)]);
let view = TessellatedFaceSet::new(EntityId(1), &e);
assert!(matches!(
view.coordinate_list(&m).unwrap_err(),
GeometryError::WrongEntityType {
entity: EntityId(50),
expected: "IfcCartesianPointList3D",
..
}
));
let err = view.coordinate_list(&Model::new()).unwrap_err();
assert_eq!(err.entity(), Some(EntityId(1)));
}
#[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"));
}
}
}