use crate::error::GeometryResult;
use crate::slots::Slots;
use ifc_model::{Entity, EntityId};
pub(crate) mod plane_slot {
pub const BASIS_SURFACE: usize = 0;
pub const OUTER_BOUNDARY: usize = 1;
pub const INNER_BOUNDARIES: usize = 2;
}
pub(crate) mod surface_slot {
pub const BASIS_SURFACE: usize = 0;
pub const BOUNDARIES: usize = 1;
pub const IMPLICIT_OUTER: usize = 2;
}
pub(crate) mod trimmed_slot {
pub const BASIS_SURFACE: usize = 0;
pub const U1: usize = 1;
pub const V1: usize = 2;
pub const U2: usize = 3;
pub const V2: usize = 4;
pub const USENSE: usize = 5;
pub const VSENSE: usize = 6;
}
#[derive(Debug, Clone, Copy)]
pub struct CurveBoundedPlane<'m> {
slots: Slots<'m>,
}
impl<'m> CurveBoundedPlane<'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 basis_surface_ref(&self) -> GeometryResult<EntityId> {
self.slots
.req_ref(plane_slot::BASIS_SURFACE, "BasisSurface")
}
pub fn outer_boundary_ref(&self) -> GeometryResult<EntityId> {
self.slots
.req_ref(plane_slot::OUTER_BOUNDARY, "OuterBoundary")
}
pub fn inner_boundary_refs(&self) -> Vec<EntityId> {
self.slots.opt_ref_list(plane_slot::INNER_BOUNDARIES)
}
}
#[derive(Debug, Clone, Copy)]
pub struct CurveBoundedSurface<'m> {
slots: Slots<'m>,
}
impl<'m> CurveBoundedSurface<'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 basis_surface_ref(&self) -> GeometryResult<EntityId> {
self.slots
.req_ref(surface_slot::BASIS_SURFACE, "BasisSurface")
}
pub fn boundary_refs(&self) -> GeometryResult<Vec<EntityId>> {
let boundaries = self
.slots
.req_ref_list(surface_slot::BOUNDARIES, "Boundaries")?;
if boundaries.is_empty() {
return Err(self
.slots
.degenerate("Boundaries is empty; SET [1:?] requires a member"));
}
Ok(boundaries)
}
pub fn implicit_outer(&self) -> bool {
self.slots
.opt_bool(surface_slot::IMPLICIT_OUTER)
.unwrap_or(false)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TrimRectangle {
pub u1: f64,
pub v1: f64,
pub u2: f64,
pub v2: f64,
pub usense: bool,
pub vsense: bool,
}
impl TrimRectangle {
pub fn u_wraps(&self) -> bool {
self.usense == (self.u1 > self.u2)
}
pub fn v_wraps(&self) -> bool {
self.vsense == (self.v1 > self.v2)
}
}
#[derive(Debug, Clone, Copy)]
pub struct RectangularTrimmedSurface<'m> {
slots: Slots<'m>,
}
impl<'m> RectangularTrimmedSurface<'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 basis_surface_ref(&self) -> GeometryResult<EntityId> {
self.slots
.req_ref(trimmed_slot::BASIS_SURFACE, "BasisSurface")
}
pub fn rectangle(&self) -> GeometryResult<TrimRectangle> {
let u1 = self.slots.req_f64(trimmed_slot::U1, "U1")?;
let v1 = self.slots.req_f64(trimmed_slot::V1, "V1")?;
let u2 = self.slots.req_f64(trimmed_slot::U2, "U2")?;
let v2 = self.slots.req_f64(trimmed_slot::V2, "V2")?;
if u1 == u2 {
return Err(self
.slots
.degenerate(format!("U1 and U2 are both {u1}; the patch has no extent")));
}
if v1 == v2 {
return Err(self
.slots
.degenerate(format!("V1 and V2 are both {v1}; the patch has no extent")));
}
Ok(TrimRectangle {
u1,
v1,
u2,
v2,
usense: self.slots.req_bool(trimmed_slot::USENSE, "Usense")?,
vsense: self.slots.req_bool(trimmed_slot::VSENSE, "Vsense")?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use ifc_model::Value;
fn refs(ids: &[u64]) -> Value {
Value::List(ids.iter().map(|i| Value::Ref(EntityId(*i))).collect())
}
fn trimmed(u1: f64, v1: f64, u2: f64, v2: f64, usense: bool, vsense: bool) -> Entity {
Entity::new(
"IFCRECTANGULARTRIMMEDSURFACE",
vec![
Value::Ref(EntityId(100)),
Value::Real(u1),
Value::Real(v1),
Value::Real(u2),
Value::Real(v2),
Value::Bool(usense),
Value::Bool(vsense),
],
)
}
#[test]
fn a_curve_bounded_plane_separates_its_outline_from_its_holes() {
let e = Entity::new(
"IFCCURVEBOUNDEDPLANE",
vec![
Value::Ref(EntityId(100)),
Value::Ref(EntityId(101)),
refs(&[102, 103]),
],
);
let view = CurveBoundedPlane::new(EntityId(1), &e);
assert_eq!(view.basis_surface_ref().unwrap(), EntityId(100));
assert_eq!(view.outer_boundary_ref().unwrap(), EntityId(101));
assert_eq!(
view.inner_boundary_refs(),
vec![EntityId(102), EntityId(103)]
);
}
#[test]
fn a_plane_with_no_holes_reports_an_empty_inner_boundary_list() {
let e = Entity::new(
"IFCCURVEBOUNDEDPLANE",
vec![
Value::Ref(EntityId(100)),
Value::Ref(EntityId(101)),
Value::List(vec![]),
],
);
assert!(CurveBoundedPlane::new(EntityId(1), &e)
.inner_boundary_refs()
.is_empty());
let absent = Entity::new(
"IFCCURVEBOUNDEDPLANE",
vec![Value::Ref(EntityId(100)), Value::Ref(EntityId(101))],
);
assert!(CurveBoundedPlane::new(EntityId(1), &absent)
.inner_boundary_refs()
.is_empty());
}
#[test]
fn implicit_outer_makes_every_listed_boundary_a_hole() {
let implicit = Entity::new(
"IFCCURVEBOUNDEDSURFACE",
vec![
Value::Ref(EntityId(100)),
refs(&[101, 102]),
Value::Bool(true),
],
);
let view = CurveBoundedSurface::new(EntityId(1), &implicit);
assert!(view.implicit_outer());
assert_eq!(view.boundary_refs().unwrap().len(), 2);
let explicit = Entity::new(
"IFCCURVEBOUNDEDSURFACE",
vec![
Value::Ref(EntityId(100)),
refs(&[101, 102]),
Value::Bool(false),
],
);
assert!(!CurveBoundedSurface::new(EntityId(1), &explicit).implicit_outer());
}
#[test]
fn an_absent_implicit_outer_defaults_to_false() {
let e = Entity::new(
"IFCCURVEBOUNDEDSURFACE",
vec![Value::Ref(EntityId(100)), refs(&[101])],
);
assert!(!CurveBoundedSurface::new(EntityId(1), &e).implicit_outer());
}
#[test]
fn a_curve_bounded_surface_with_no_boundaries_is_degenerate() {
let e = Entity::new(
"IFCCURVEBOUNDEDSURFACE",
vec![Value::Ref(EntityId(100)), Value::List(vec![])],
);
assert!(CurveBoundedSurface::new(EntityId(1), &e)
.boundary_refs()
.is_err());
}
#[test]
fn trim_parameters_are_read_in_u1_v1_u2_v2_declaration_order() {
let e = trimmed(0.0, 1.0, 2.0, 3.0, true, true);
let rect = RectangularTrimmedSurface::new(EntityId(1), &e)
.rectangle()
.unwrap();
assert_eq!(rect.u1, 0.0);
assert_eq!(rect.v1, 1.0);
assert_eq!(rect.u2, 2.0);
assert_eq!(rect.v2, 3.0);
}
#[test]
fn descending_trim_bounds_are_preserved_not_normalised() {
let e = trimmed(350.0, 0.0, 10.0, 1.0, true, true);
let rect = RectangularTrimmedSurface::new(EntityId(1), &e)
.rectangle()
.unwrap();
assert_eq!(rect.u1, 350.0);
assert_eq!(rect.u2, 10.0);
assert!(rect.u_wraps());
assert!(!rect.v_wraps());
}
#[test]
fn the_sense_flags_distinguish_otherwise_identical_rectangles() {
let a = trimmed(0.0, 0.0, 90.0, 1.0, true, true);
let b = trimmed(0.0, 0.0, 90.0, 1.0, false, true);
let rect_a = RectangularTrimmedSurface::new(EntityId(1), &a)
.rectangle()
.unwrap();
let rect_b = RectangularTrimmedSurface::new(EntityId(1), &b)
.rectangle()
.unwrap();
assert_ne!(rect_a, rect_b);
assert!(!rect_a.u_wraps());
assert!(rect_b.u_wraps());
}
#[test]
fn a_zero_extent_trim_rectangle_is_degenerate_and_names_the_direction() {
let flat_u = trimmed(1.0, 0.0, 1.0, 2.0, true, true);
let err = RectangularTrimmedSurface::new(EntityId(4), &flat_u)
.rectangle()
.unwrap_err();
assert!(err.to_string().contains("U1 and U2"), "got: {err}");
let flat_v = trimmed(0.0, 5.0, 1.0, 5.0, true, true);
let err = RectangularTrimmedSurface::new(EntityId(4), &flat_v)
.rectangle()
.unwrap_err();
assert!(err.to_string().contains("V1 and V2"), "got: {err}");
}
#[test]
fn trim_parameters_read_through_parameter_value_wrappers() {
let e = Entity::new(
"IFCRECTANGULARTRIMMEDSURFACE",
vec![
Value::Ref(EntityId(100)),
Value::Typed {
type_name: "IFCPARAMETERVALUE".into(),
value: Box::new(Value::Real(0.0)),
},
Value::Real(0.0),
Value::Typed {
type_name: "IFCPARAMETERVALUE".into(),
value: Box::new(Value::Real(1.0)),
},
Value::Real(1.0),
Value::Bool(true),
Value::Bool(true),
],
);
let rect = RectangularTrimmedSurface::new(EntityId(1), &e)
.rectangle()
.unwrap();
assert_eq!(rect.u2, 1.0);
}
}