use crate::error::GeometryResult;
use crate::resource::placement::Axis2Placement;
use crate::resource::resolve;
use crate::slots::Slots;
use ifc_model::{Entity, EntityId, Model};
pub(crate) mod circle_slot {
pub const POSITION: usize = 0;
pub const RADIUS: usize = 1;
}
pub(crate) mod ellipse_slot {
pub const POSITION: usize = 0;
pub const SEMI_AXIS_1: usize = 1;
pub const SEMI_AXIS_2: usize = 2;
}
#[derive(Debug, Clone, Copy)]
pub struct Circle<'m> {
slots: Slots<'m>,
}
impl<'m> Circle<'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 position_ref(&self) -> GeometryResult<EntityId> {
self.slots.req_ref(circle_slot::POSITION, "Position")
}
pub fn position<'v>(&self, model: &'v Model) -> GeometryResult<Axis2Placement<'v>> {
resolve::axis2_placement(model, self.id(), self.position_ref()?)
}
pub fn radius(&self) -> GeometryResult<f64> {
let r = self.slots.req_f64(circle_slot::RADIUS, "Radius")?;
if r > 0.0 {
Ok(r)
} else {
Err(self
.slots
.degenerate(format!("Radius must be positive, found {r}")))
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Ellipse<'m> {
slots: Slots<'m>,
}
impl<'m> Ellipse<'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 position_ref(&self) -> GeometryResult<EntityId> {
self.slots.req_ref(ellipse_slot::POSITION, "Position")
}
pub fn position<'v>(&self, model: &'v Model) -> GeometryResult<Axis2Placement<'v>> {
resolve::axis2_placement(model, self.id(), self.position_ref()?)
}
pub fn semi_axis_1(&self) -> GeometryResult<f64> {
self.positive(ellipse_slot::SEMI_AXIS_1, "SemiAxis1")
}
pub fn semi_axis_2(&self) -> GeometryResult<f64> {
self.positive(ellipse_slot::SEMI_AXIS_2, "SemiAxis2")
}
pub fn semi_axes(&self) -> GeometryResult<(f64, f64)> {
Ok((self.semi_axis_1()?, self.semi_axis_2()?))
}
fn positive(&self, index: usize, name: &'static str) -> GeometryResult<f64> {
let v = self.slots.req_f64(index, name)?;
if v > 0.0 {
Ok(v)
} else {
Err(self
.slots
.degenerate(format!("{name} must be positive, found {v}")))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ifc_model::Value;
fn circle(radius: Value) -> Entity {
Entity::new("IFCCIRCLE", vec![Value::Ref(EntityId(5)), radius])
}
fn ellipse(a: f64, b: f64) -> Entity {
Entity::new(
"IFCELLIPSE",
vec![Value::Ref(EntityId(5)), Value::Real(a), Value::Real(b)],
)
}
fn placement_model(type_name: &str) -> Model {
let mut model = Model::new();
let coords = Value::List(vec![Value::Real(1.0), Value::Real(2.0)]);
model.insert(EntityId(4), Entity::new("IFCCARTESIANPOINT", vec![coords]));
let placement = Entity::new(type_name, vec![Value::Ref(EntityId(4))]);
model.insert(EntityId(5), placement);
model
}
#[test]
fn conic_positions_resolve_to_the_select_member_the_file_wrote() {
let e = circle(Value::Real(1.0));
let model = placement_model("IFCAXIS2PLACEMENT2D");
let position = Circle::new(EntityId(1), &e).position(&model).unwrap();
assert!(matches!(position, Axis2Placement::TwoD(_)));
assert_eq!(position.location(&model).unwrap(), [1.0, 2.0, 0.0]);
let e = ellipse(2.0, 1.0);
let model = placement_model("IFCAXIS2PLACEMENT3D");
let position = Ellipse::new(EntityId(1), &e).position(&model).unwrap();
assert!(matches!(position, Axis2Placement::ThreeD(_)));
let transform = position.transform(&model).unwrap();
assert_eq!(transform.apply([0.0; 3]), [1.0, 2.0, 0.0]);
}
#[test]
fn a_conic_placed_by_an_axis1_placement_is_a_typed_error() {
let model = placement_model("IFCAXIS1PLACEMENT");
let e = circle(Value::Real(1.0));
let err = Circle::new(EntityId(1), &e).position(&model).unwrap_err();
assert!(matches!(
err,
crate::GeometryError::WrongEntityType {
entity: EntityId(5),
..
}
));
let e = ellipse(2.0, 1.0);
let err = Ellipse::new(EntityId(1), &e)
.position(&Model::new())
.unwrap_err();
assert_eq!(err.entity(), Some(EntityId(1)));
}
#[test]
fn circle_reads_position_and_radius_from_conic_inherited_slots() {
let e = circle(Value::Real(2.5));
let view = Circle::new(EntityId(1), &e);
assert_eq!(view.position_ref().unwrap(), EntityId(5));
assert_eq!(view.radius().unwrap(), 2.5);
}
#[test]
fn circle_radius_reads_through_a_positive_length_measure_wrapper() {
let e = circle(Value::Typed {
type_name: "IFCPOSITIVELENGTHMEASURE".into(),
value: Box::new(Value::Real(3.0)),
});
assert_eq!(Circle::new(EntityId(1), &e).radius().unwrap(), 3.0);
}
#[test]
fn zero_radius_circle_is_degenerate_not_a_zero_length_curve() {
let e = circle(Value::Real(0.0));
let err = Circle::new(EntityId(9), &e).radius().unwrap_err();
assert!(err.to_string().contains("#9"), "got: {err}");
assert!(err.to_string().contains("positive"), "got: {err}");
assert!(
!err.is_unsupported(),
"a bad radius is corruption, not a gap"
);
}
#[test]
fn negative_radius_circle_is_degenerate() {
let e = circle(Value::Real(-1.0));
assert!(Circle::new(EntityId(1), &e).radius().is_err());
}
#[test]
fn ellipse_returns_both_semi_axes_in_declaration_order() {
let e = ellipse(4.0, 2.0);
assert_eq!(
Ellipse::new(EntityId(1), &e).semi_axes().unwrap(),
(4.0, 2.0)
);
}
#[test]
fn ellipse_accepts_a_second_semi_axis_larger_than_the_first() {
let e = ellipse(1.0, 7.0);
assert_eq!(
Ellipse::new(EntityId(1), &e).semi_axes().unwrap(),
(1.0, 7.0)
);
}
#[test]
fn zero_semi_axis_is_degenerate_and_names_which_one() {
let e = ellipse(4.0, 0.0);
let err = Ellipse::new(EntityId(1), &e).semi_axis_2().unwrap_err();
assert!(err.to_string().contains("SemiAxis2"), "got: {err}");
}
}