use crate::error::GeometryResult;
use crate::slots::Slots;
use ifc_model::{Entity, EntityId};
pub(crate) mod slot {
pub const BASE_SURFACE: usize = 0;
pub const AGREEMENT_FLAG: usize = 1;
pub const ENCLOSURE: usize = 2;
pub const POSITION: usize = 2;
pub const POLYGONAL_BOUNDARY: usize = 3;
}
#[derive(Debug, Clone, Copy)]
pub struct HalfSpaceSolid<'m> {
slots: Slots<'m>,
}
impl<'m> HalfSpaceSolid<'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 base_surface(&self) -> GeometryResult<EntityId> {
self.slots.req_ref(slot::BASE_SURFACE, "BaseSurface")
}
pub fn agreement_flag(&self) -> GeometryResult<bool> {
self.slots.req_bool(slot::AGREEMENT_FLAG, "AgreementFlag")
}
pub fn is_infinite(&self) -> bool {
!self.is_bounded()
}
pub fn is_bounded(&self) -> bool {
let name = self.type_name();
name.eq_ignore_ascii_case("IFCBOXEDHALFSPACE")
|| name.eq_ignore_ascii_case("IFCPOLYGONALBOUNDEDHALFSPACE")
}
pub fn reject_standalone_use(&self) -> crate::GeometryError {
self.slots
.unsupported("IfcHalfSpaceSolid is infinite and can only be used as a boolean operand")
}
}
#[derive(Debug, Clone, Copy)]
pub struct BoxedHalfSpace<'m> {
slots: Slots<'m>,
}
impl<'m> BoxedHalfSpace<'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) -> HalfSpaceSolid<'m> {
HalfSpaceSolid { slots: self.slots }
}
pub fn enclosure(&self) -> GeometryResult<EntityId> {
self.slots.req_ref(slot::ENCLOSURE, "Enclosure")
}
}
#[derive(Debug, Clone, Copy)]
pub struct PolygonalBoundedHalfSpace<'m> {
slots: Slots<'m>,
}
impl<'m> PolygonalBoundedHalfSpace<'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) -> HalfSpaceSolid<'m> {
HalfSpaceSolid { slots: self.slots }
}
pub fn position(&self) -> GeometryResult<EntityId> {
self.slots.req_ref(slot::POSITION, "Position")
}
pub fn polygonal_boundary(&self) -> GeometryResult<EntityId> {
self.slots
.req_ref(slot::POLYGONAL_BOUNDARY, "PolygonalBoundary")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::solid::testkit::{entity, r};
use ifc_model::Value;
#[test]
fn base_surface_and_agreement_flag_are_slots_zero_and_one() {
let e = entity("IFCHALFSPACESOLID", vec![r(10), Value::Bool(true)]);
let view = HalfSpaceSolid::new(EntityId(1), &e);
assert_eq!(view.base_surface().unwrap(), EntityId(10));
assert!(view.agreement_flag().unwrap());
}
#[test]
fn agreement_flag_preserves_both_states_without_defaulting() {
for expected in [true, false] {
let e = entity("IFCHALFSPACESOLID", vec![r(10), Value::Bool(expected)]);
assert_eq!(
HalfSpaceSolid::new(EntityId(1), &e)
.agreement_flag()
.unwrap(),
expected
);
}
}
#[test]
fn a_logical_unknown_agreement_flag_is_an_error_not_a_false() {
let e = entity("IFCHALFSPACESOLID", vec![r(10), Value::LogicalUnknown]);
assert!(HalfSpaceSolid::new(EntityId(1), &e)
.agreement_flag()
.is_err());
}
#[test]
fn a_plain_half_space_is_infinite_and_refuses_standalone_use() {
let e = entity("IFCHALFSPACESOLID", vec![r(10), Value::Bool(true)]);
let view = HalfSpaceSolid::new(EntityId(77), &e);
assert!(view.is_infinite());
assert!(!view.is_bounded());
let err = view.reject_standalone_use();
assert!(err.is_unsupported());
assert_eq!(err.entity(), Some(EntityId(77)));
assert!(err.to_string().contains("boolean operand"));
}
#[test]
fn the_bounded_subtypes_are_not_reported_as_infinite() {
for name in ["IFCBOXEDHALFSPACE", "IFCPOLYGONALBOUNDEDHALFSPACE"] {
let e = entity(name, vec![r(10), Value::Bool(true), r(20), r(30)]);
let view = HalfSpaceSolid::new(EntityId(1), &e);
assert!(view.is_bounded(), "{name}");
assert!(!view.is_infinite(), "{name}");
}
}
#[test]
fn boxed_half_space_enclosure_follows_the_inherited_pair() {
let e = entity("IFCBOXEDHALFSPACE", vec![r(10), Value::Bool(false), r(20)]);
let view = BoxedHalfSpace::new(EntityId(1), &e);
assert_eq!(view.base().base_surface().unwrap(), EntityId(10));
assert!(!view.base().agreement_flag().unwrap());
assert_eq!(view.enclosure().unwrap(), EntityId(20));
}
#[test]
fn polygonal_bounded_half_space_reads_position_at_two_and_boundary_at_three() {
let e = entity(
"IFCPOLYGONALBOUNDEDHALFSPACE",
vec![r(10), Value::Bool(true), r(20), r(30)],
);
let view = PolygonalBoundedHalfSpace::new(EntityId(1), &e);
assert_eq!(view.base().base_surface().unwrap(), EntityId(10));
assert!(view.base().agreement_flag().unwrap());
assert_eq!(view.position().unwrap(), EntityId(20));
assert_eq!(view.polygonal_boundary().unwrap(), EntityId(30));
}
#[test]
fn boundary_placement_is_independent_of_the_base_surface() {
let e = entity(
"IFCPOLYGONALBOUNDEDHALFSPACE",
vec![r(10), Value::Bool(true), r(20), r(30)],
);
let view = PolygonalBoundedHalfSpace::new(EntityId(1), &e);
assert_ne!(
view.position().unwrap(),
view.base().base_surface().unwrap(),
"Position places the 2D boundary; BaseSurface divides space"
);
}
#[test]
fn a_polygonal_bounded_half_space_without_a_position_is_an_error() {
let e = entity(
"IFCPOLYGONALBOUNDEDHALFSPACE",
vec![r(10), Value::Bool(true)],
);
let view = PolygonalBoundedHalfSpace::new(EntityId(6), &e);
assert!(view.position().is_err());
assert!(view.polygonal_boundary().is_err());
}
}