use crate::error::GeometryResult;
use crate::resource::direction::Direction;
use crate::resource::placement::{Axis1Placement, Axis2Placement3D};
use crate::resource::resolve;
use crate::slots::Slots;
use ifc_model::{Entity, EntityId, Model};
pub(crate) mod slot {
pub const SWEPT_CURVE: usize = 0;
pub const POSITION: usize = 1;
pub const EXTRUDED_DIRECTION: usize = 2;
pub const DEPTH: usize = 3;
pub const AXIS_POSITION: usize = 2;
}
#[derive(Debug, Clone, Copy)]
struct SweptBase<'m> {
slots: Slots<'m>,
}
impl<'m> SweptBase<'m> {
fn swept_curve_ref(&self) -> GeometryResult<EntityId> {
self.slots.req_ref(slot::SWEPT_CURVE, "SweptCurve")
}
fn position_ref(&self) -> Option<EntityId> {
self.slots.opt_ref(slot::POSITION)
}
fn position<'v>(&self, model: &'v Model) -> GeometryResult<Option<Axis2Placement3D<'v>>> {
self.position_ref()
.map(|id| resolve::axis2_placement_3d(model, self.slots.id(), id))
.transpose()
}
}
#[derive(Debug, Clone, Copy)]
pub struct SurfaceOfLinearExtrusion<'m> {
base: SweptBase<'m>,
}
impl<'m> SurfaceOfLinearExtrusion<'m> {
pub fn new(id: EntityId, entity: &'m Entity) -> Self {
Self {
base: SweptBase {
slots: Slots::new(id, entity),
},
}
}
pub fn id(&self) -> EntityId {
self.base.slots.id()
}
pub fn swept_curve_ref(&self) -> GeometryResult<EntityId> {
self.base.swept_curve_ref()
}
pub fn position_ref(&self) -> Option<EntityId> {
self.base.position_ref()
}
pub fn position<'v>(&self, model: &'v Model) -> GeometryResult<Option<Axis2Placement3D<'v>>> {
self.base.position(model)
}
pub fn extruded_direction_ref(&self) -> GeometryResult<EntityId> {
self.base
.slots
.req_ref(slot::EXTRUDED_DIRECTION, "ExtrudedDirection")
}
pub fn extruded_direction<'v>(&self, model: &'v Model) -> GeometryResult<Direction<'v>> {
resolve::direction(model, self.id(), self.extruded_direction_ref()?)
}
pub fn depth(&self) -> GeometryResult<f64> {
let depth = self.base.slots.req_f64(slot::DEPTH, "Depth")?;
if depth == 0.0 {
return Err(self
.base
.slots
.degenerate("Depth is zero; the surface collapses to its generating curve"));
}
Ok(depth)
}
}
#[derive(Debug, Clone, Copy)]
pub struct SurfaceOfRevolution<'m> {
base: SweptBase<'m>,
}
impl<'m> SurfaceOfRevolution<'m> {
pub fn new(id: EntityId, entity: &'m Entity) -> Self {
Self {
base: SweptBase {
slots: Slots::new(id, entity),
},
}
}
pub fn id(&self) -> EntityId {
self.base.slots.id()
}
pub fn swept_curve_ref(&self) -> GeometryResult<EntityId> {
self.base.swept_curve_ref()
}
pub fn position_ref(&self) -> Option<EntityId> {
self.base.position_ref()
}
pub fn position<'v>(&self, model: &'v Model) -> GeometryResult<Option<Axis2Placement3D<'v>>> {
self.base.position(model)
}
pub fn axis_position_ref(&self) -> GeometryResult<EntityId> {
self.base.slots.req_ref(slot::AXIS_POSITION, "AxisPosition")
}
pub fn axis_position<'v>(&self, model: &'v Model) -> GeometryResult<Axis1Placement<'v>> {
resolve::axis1_placement(model, self.id(), self.axis_position_ref()?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use ifc_model::Value;
fn extrusion(position: Value, depth: Value) -> Entity {
Entity::new(
"IFCSURFACEOFLINEAREXTRUSION",
vec![
Value::Ref(EntityId(80)),
position,
Value::Ref(EntityId(82)),
depth,
],
)
}
#[test]
fn extrusion_reads_inherited_swept_curve_before_its_own_direction_and_depth() {
let e = extrusion(Value::Ref(EntityId(81)), Value::Real(3.0));
let view = SurfaceOfLinearExtrusion::new(EntityId(1), &e);
assert_eq!(view.swept_curve_ref().unwrap(), EntityId(80));
assert_eq!(view.position_ref(), Some(EntityId(81)));
assert_eq!(view.extruded_direction_ref().unwrap(), EntityId(82));
assert_eq!(view.depth().unwrap(), 3.0);
}
#[test]
fn an_absent_position_is_reported_as_none_rather_than_as_an_error() {
let e = extrusion(Value::Null, Value::Real(1.0));
let view = SurfaceOfLinearExtrusion::new(EntityId(1), &e);
assert_eq!(view.position_ref(), None);
assert_eq!(view.extruded_direction_ref().unwrap(), EntityId(82));
assert_eq!(view.depth().unwrap(), 1.0);
}
#[test]
fn a_negative_depth_is_accepted_because_it_only_reverses_the_sweep() {
let e = extrusion(Value::Ref(EntityId(81)), Value::Real(-2.0));
assert_eq!(
SurfaceOfLinearExtrusion::new(EntityId(1), &e)
.depth()
.unwrap(),
-2.0
);
}
#[test]
fn a_zero_depth_is_degenerate() {
let e = extrusion(Value::Ref(EntityId(81)), Value::Real(0.0));
let err = SurfaceOfLinearExtrusion::new(EntityId(5), &e)
.depth()
.unwrap_err();
assert!(err.to_string().contains("#5"), "got: {err}");
assert!(err.to_string().contains("zero"), "got: {err}");
}
#[test]
fn depth_reads_through_a_length_measure_wrapper() {
let e = extrusion(
Value::Ref(EntityId(81)),
Value::Typed {
type_name: "IFCLENGTHMEASURE".into(),
value: Box::new(Value::Real(1.5)),
},
);
assert_eq!(
SurfaceOfLinearExtrusion::new(EntityId(1), &e)
.depth()
.unwrap(),
1.5
);
}
#[test]
fn revolution_reads_its_axis_from_the_slot_after_the_inherited_two() {
let e = Entity::new(
"IFCSURFACEOFREVOLUTION",
vec![
Value::Ref(EntityId(90)),
Value::Ref(EntityId(91)),
Value::Ref(EntityId(92)),
],
);
let view = SurfaceOfRevolution::new(EntityId(1), &e);
assert_eq!(view.swept_curve_ref().unwrap(), EntityId(90));
assert_eq!(view.position_ref(), Some(EntityId(91)));
assert_eq!(view.axis_position_ref().unwrap(), EntityId(92));
}
#[test]
fn a_revolution_without_an_axis_reports_the_attribute_by_name() {
let e = Entity::new(
"IFCSURFACEOFREVOLUTION",
vec![Value::Ref(EntityId(90)), Value::Null],
);
let err = SurfaceOfRevolution::new(EntityId(1), &e)
.axis_position_ref()
.unwrap_err();
assert!(err.to_string().contains("AxisPosition"), "got: {err}");
}
fn frame_model() -> Model {
let reals = |v: &[f64]| Value::List(v.iter().copied().map(Value::Real).collect());
let mut m = Model::new();
m.insert(
EntityId(70),
Entity::new("IFCCARTESIANPOINT", vec![reals(&[1.0, 0.0, 0.0])]),
);
m.insert(
EntityId(71),
Entity::new("IFCDIRECTION", vec![reals(&[0.0, 0.0, 2.0])]),
);
let at = vec![Value::Ref(EntityId(70)), Value::Ref(EntityId(71))];
m.insert(EntityId(81), Entity::new("IFCAXIS2PLACEMENT3D", at.clone()));
m.insert(
EntityId(82),
Entity::new("IFCDIRECTION", vec![reals(&[0.0, 3.0, 0.0])]),
);
m.insert(EntityId(92), Entity::new("IFCAXIS1PLACEMENT", at));
m
}
#[test]
fn extrusion_position_and_direction_resolve_to_typed_views() {
let m = frame_model();
let e = extrusion(Value::Ref(EntityId(81)), Value::Real(1.0));
let view = SurfaceOfLinearExtrusion::new(EntityId(1), &e);
let position = view.position(&m).unwrap().expect("Position is present");
assert_eq!(position.location(&m).unwrap(), [1.0, 0.0, 0.0]);
assert_eq!(
view.extruded_direction(&m).unwrap().unit().unwrap(),
[0.0, 1.0, 0.0]
);
let e = extrusion(Value::Null, Value::Real(1.0));
let view = SurfaceOfLinearExtrusion::new(EntityId(1), &e);
assert!(
view.position(&m).unwrap().is_none(),
"absent is the identity"
);
}
#[test]
fn a_mistyped_extrusion_position_or_direction_is_an_error_not_none() {
let m = frame_model();
let e = extrusion(Value::Ref(EntityId(92)), Value::Real(1.0));
let view = SurfaceOfLinearExtrusion::new(EntityId(1), &e);
assert!(matches!(
view.position(&m).unwrap_err(),
crate::GeometryError::WrongEntityType {
entity: EntityId(92),
..
}
));
let err = view.extruded_direction(&Model::new()).unwrap_err();
assert_eq!(err.entity(), Some(EntityId(1)));
}
#[test]
fn revolution_axis_and_position_resolve_to_typed_views() {
let m = frame_model();
let refs = [90, 81, 92].map(|id| Value::Ref(EntityId(id))).to_vec();
let e = Entity::new("IFCSURFACEOFREVOLUTION", refs);
let view = SurfaceOfRevolution::new(EntityId(1), &e);
let axis = view.axis_position(&m).unwrap();
assert_eq!(axis.location(&m).unwrap(), [1.0, 0.0, 0.0]);
assert_eq!(axis.axis(&m).unwrap(), [0.0, 0.0, 1.0]);
assert_eq!(view.position(&m).unwrap().unwrap().id(), EntityId(81));
}
#[test]
fn a_revolution_axis_that_is_an_axis2_placement_is_a_typed_error() {
let m = frame_model();
let refs = [90, 81, 81].map(|id| Value::Ref(EntityId(id))).to_vec();
let e = Entity::new("IFCSURFACEOFREVOLUTION", refs);
let err = SurfaceOfRevolution::new(EntityId(1), &e)
.axis_position(&m)
.unwrap_err();
assert!(matches!(
err,
crate::GeometryError::WrongEntityType {
entity: EntityId(81),
expected: "IfcAxis1Placement",
..
}
));
}
#[test]
fn a_swept_surface_without_its_profile_reports_the_attribute_by_name() {
let e = Entity::new("IFCSURFACEOFLINEAREXTRUSION", vec![]);
let err = SurfaceOfLinearExtrusion::new(EntityId(1), &e)
.swept_curve_ref()
.unwrap_err();
assert!(err.to_string().contains("SweptCurve"), "got: {err}");
}
}