use super::{extruded_slot, revolved_slot, swept_area_slot};
use crate::error::GeometryResult;
use crate::slots::Slots;
use ifc_model::{Entity, EntityId};
#[derive(Debug, Clone, Copy)]
pub struct SweptAreaSolid<'m> {
slots: Slots<'m>,
}
impl<'m> SweptAreaSolid<'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 swept_area(&self) -> GeometryResult<EntityId> {
self.slots.req_ref(swept_area_slot::SWEPT_AREA, "SweptArea")
}
pub fn position(&self) -> Option<EntityId> {
self.slots.opt_ref(swept_area_slot::POSITION)
}
}
#[derive(Debug, Clone, Copy)]
pub struct ExtrudedAreaSolid<'m> {
slots: Slots<'m>,
}
impl<'m> ExtrudedAreaSolid<'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) -> SweptAreaSolid<'m> {
SweptAreaSolid::from_slots(self.slots)
}
pub fn extruded_direction(&self) -> GeometryResult<EntityId> {
self.slots
.req_ref(extruded_slot::EXTRUDED_DIRECTION, "ExtrudedDirection")
}
pub fn depth(&self) -> GeometryResult<f64> {
self.slots.req_f64(extruded_slot::DEPTH, "Depth")
}
pub fn checked_depth(&self) -> GeometryResult<f64> {
let depth = self.depth()?;
if depth > 0.0 {
Ok(depth)
} else {
Err(self
.slots
.degenerate(format!("Depth must be positive, found {depth}")))
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ExtrudedAreaSolidTapered<'m> {
slots: Slots<'m>,
}
impl<'m> ExtrudedAreaSolidTapered<'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) -> ExtrudedAreaSolid<'m> {
ExtrudedAreaSolid { slots: self.slots }
}
pub fn end_swept_area(&self) -> GeometryResult<EntityId> {
self.slots
.req_ref(extruded_slot::END_SWEPT_AREA, "EndSweptArea")
}
}
#[derive(Debug, Clone, Copy)]
pub struct RevolvedAreaSolid<'m> {
slots: Slots<'m>,
}
impl<'m> RevolvedAreaSolid<'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) -> SweptAreaSolid<'m> {
SweptAreaSolid::from_slots(self.slots)
}
pub fn axis(&self) -> GeometryResult<EntityId> {
self.slots.req_ref(revolved_slot::AXIS, "Axis")
}
pub fn angle_raw(&self) -> GeometryResult<f64> {
self.slots.req_f64(revolved_slot::ANGLE, "Angle")
}
}
#[derive(Debug, Clone, Copy)]
pub struct RevolvedAreaSolidTapered<'m> {
slots: Slots<'m>,
}
impl<'m> RevolvedAreaSolidTapered<'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) -> RevolvedAreaSolid<'m> {
RevolvedAreaSolid { slots: self.slots }
}
pub fn end_swept_area(&self) -> GeometryResult<EntityId> {
self.slots
.req_ref(revolved_slot::END_SWEPT_AREA, "EndSweptArea")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::solid::testkit::{entity, n, r};
use ifc_model::Value;
fn extrusion(attrs: Vec<Value>) -> Entity {
entity("IFCEXTRUDEDAREASOLID", attrs)
}
#[test]
fn inherited_swept_area_slots_precede_the_subtype_own_slots() {
let e = extrusion(vec![r(10), r(20), r(30), n(3.0)]);
let view = ExtrudedAreaSolid::new(EntityId(1), &e);
assert_eq!(view.base().swept_area().unwrap(), EntityId(10));
assert_eq!(view.base().position(), Some(EntityId(20)));
assert_eq!(view.extruded_direction().unwrap(), EntityId(30));
assert_eq!(view.depth().unwrap(), 3.0);
}
#[test]
fn absent_position_is_reported_as_none_not_as_a_failure() {
let e = extrusion(vec![r(10), Value::Null, r(30), n(3.0)]);
let view = ExtrudedAreaSolid::new(EntityId(1), &e);
assert_eq!(view.base().position(), None);
assert!(view.base().swept_area().is_ok());
}
#[test]
fn extruded_direction_is_a_reference_and_never_defaulted_to_z() {
let e = extrusion(vec![r(10), r(20), r(99), n(1.0)]);
assert_eq!(
ExtrudedAreaSolid::new(EntityId(1), &e)
.extruded_direction()
.unwrap(),
EntityId(99)
);
let missing = extrusion(vec![r(10), r(20), Value::Null, n(1.0)]);
assert!(ExtrudedAreaSolid::new(EntityId(1), &missing)
.extruded_direction()
.is_err());
}
#[test]
fn non_positive_depth_is_rejected_as_degenerate() {
for bad in [0.0, -2.5] {
let e = extrusion(vec![r(10), r(20), r(30), n(bad)]);
let view = ExtrudedAreaSolid::new(EntityId(7), &e);
let err = view.checked_depth().unwrap_err();
assert_eq!(err.entity(), Some(EntityId(7)));
assert!(view.depth().is_ok(), "raw depth stays readable");
}
let good = extrusion(vec![r(10), r(20), r(30), n(2.5)]);
assert_eq!(
ExtrudedAreaSolid::new(EntityId(7), &good)
.checked_depth()
.unwrap(),
2.5
);
}
#[test]
fn tapered_extrusion_keeps_both_profiles_addressable() {
let e = entity(
"IFCEXTRUDEDAREASOLIDTAPERED",
vec![r(10), r(20), r(30), n(3.0), r(40)],
);
let view = ExtrudedAreaSolidTapered::new(EntityId(1), &e);
assert_eq!(view.base().base().swept_area().unwrap(), EntityId(10));
assert_eq!(view.base().depth().unwrap(), 3.0);
assert_eq!(view.end_swept_area().unwrap(), EntityId(40));
}
#[test]
fn revolution_angle_is_returned_raw_without_unit_conversion() {
let e = entity("IFCREVOLVEDAREASOLID", vec![r(10), r(20), r(30), n(90.0)]);
let view = RevolvedAreaSolid::new(EntityId(1), &e);
assert_eq!(view.angle_raw().unwrap(), 90.0);
assert_eq!(view.axis().unwrap(), EntityId(30));
assert_eq!(view.base().swept_area().unwrap(), EntityId(10));
}
#[test]
fn tapered_revolution_exposes_its_end_profile() {
let e = entity(
"IFCREVOLVEDAREASOLIDTAPERED",
vec![r(10), r(20), r(30), n(45.0), r(50)],
);
let view = RevolvedAreaSolidTapered::new(EntityId(1), &e);
assert_eq!(view.base().angle_raw().unwrap(), 45.0);
assert_eq!(view.end_swept_area().unwrap(), EntityId(50));
}
#[test]
fn abstract_view_reports_the_concrete_subtype_name() {
let e = extrusion(vec![r(10), r(20), r(30), n(1.0)]);
let view = SweptAreaSolid::new(EntityId(1), &e);
assert_eq!(view.type_name(), "IFCEXTRUDEDAREASOLID");
assert_eq!(view.id(), EntityId(1));
}
}