use crate::error::GeometryResult;
use crate::resource::placement::Axis2Placement3D;
use crate::resource::resolve;
use crate::slots::Slots;
use ifc_model::{Entity, EntityId, Model};
pub(crate) mod slot {
pub const POSITION: usize = 0;
pub const RADIUS: usize = 1;
pub const MAJOR_RADIUS: usize = 1;
pub const MINOR_RADIUS: usize = 2;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParameterKind {
Length,
Angle,
}
#[derive(Debug, Clone, Copy)]
pub struct Plane<'m> {
slots: Slots<'m>,
}
impl<'m> Plane<'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(slot::POSITION, "Position")
}
pub fn position<'v>(&self, model: &'v Model) -> GeometryResult<Axis2Placement3D<'v>> {
resolve::axis2_placement_3d(model, self.id(), self.position_ref()?)
}
pub fn parameter_kinds(&self) -> (ParameterKind, ParameterKind) {
(ParameterKind::Length, ParameterKind::Length)
}
}
#[derive(Debug, Clone, Copy)]
pub struct CylindricalSurface<'m> {
slots: Slots<'m>,
}
impl<'m> CylindricalSurface<'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(slot::POSITION, "Position")
}
pub fn position<'v>(&self, model: &'v Model) -> GeometryResult<Axis2Placement3D<'v>> {
resolve::axis2_placement_3d(model, self.id(), self.position_ref()?)
}
pub fn radius(&self) -> GeometryResult<f64> {
positive(&self.slots, slot::RADIUS, "Radius")
}
pub fn parameter_kinds(&self) -> (ParameterKind, ParameterKind) {
(ParameterKind::Angle, ParameterKind::Length)
}
}
#[derive(Debug, Clone, Copy)]
pub struct SphericalSurface<'m> {
slots: Slots<'m>,
}
impl<'m> SphericalSurface<'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(slot::POSITION, "Position")
}
pub fn position<'v>(&self, model: &'v Model) -> GeometryResult<Axis2Placement3D<'v>> {
resolve::axis2_placement_3d(model, self.id(), self.position_ref()?)
}
pub fn radius(&self) -> GeometryResult<f64> {
positive(&self.slots, slot::RADIUS, "Radius")
}
pub fn parameter_kinds(&self) -> (ParameterKind, ParameterKind) {
(ParameterKind::Angle, ParameterKind::Angle)
}
}
#[derive(Debug, Clone, Copy)]
pub struct ToroidalSurface<'m> {
slots: Slots<'m>,
}
impl<'m> ToroidalSurface<'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(slot::POSITION, "Position")
}
pub fn position<'v>(&self, model: &'v Model) -> GeometryResult<Axis2Placement3D<'v>> {
resolve::axis2_placement_3d(model, self.id(), self.position_ref()?)
}
pub fn major_radius(&self) -> GeometryResult<f64> {
positive(&self.slots, slot::MAJOR_RADIUS, "MajorRadius")
}
pub fn minor_radius(&self) -> GeometryResult<f64> {
positive(&self.slots, slot::MINOR_RADIUS, "MinorRadius")
}
pub fn is_self_intersecting(&self) -> GeometryResult<bool> {
Ok(self.minor_radius()? >= self.major_radius()?)
}
pub fn parameter_kinds(&self) -> (ParameterKind, ParameterKind) {
(ParameterKind::Angle, ParameterKind::Angle)
}
}
fn positive(slots: &Slots<'_>, index: usize, name: &'static str) -> GeometryResult<f64> {
let value = slots.req_f64(index, name)?;
if value > 0.0 {
Ok(value)
} else {
Err(slots.degenerate(format!("{name} must be positive, found {value}")))
}
}
#[cfg(test)]
mod tests {
use super::*;
use ifc_model::Value;
fn surface(type_name: &str, radii: &[f64]) -> Entity {
let mut attributes = vec![Value::Ref(EntityId(70))];
attributes.extend(radii.iter().map(|r| Value::Real(*r)));
Entity::new(type_name, attributes)
}
#[test]
fn every_elementary_surface_reads_position_from_the_inherited_slot_zero() {
let plane = surface("IFCPLANE", &[]);
assert_eq!(
Plane::new(EntityId(1), &plane).position_ref().unwrap(),
EntityId(70)
);
let cylinder = surface("IFCCYLINDRICALSURFACE", &[2.0]);
assert_eq!(
CylindricalSurface::new(EntityId(1), &cylinder)
.position_ref()
.unwrap(),
EntityId(70)
);
let sphere = surface("IFCSPHERICALSURFACE", &[3.0]);
assert_eq!(
SphericalSurface::new(EntityId(1), &sphere)
.position_ref()
.unwrap(),
EntityId(70)
);
let torus = surface("IFCTOROIDALSURFACE", &[5.0, 1.0]);
assert_eq!(
ToroidalSurface::new(EntityId(1), &torus)
.position_ref()
.unwrap(),
EntityId(70)
);
}
#[test]
fn cylinder_and_sphere_radii_are_read_from_the_slot_after_position() {
let cylinder = surface("IFCCYLINDRICALSURFACE", &[2.5]);
assert_eq!(
CylindricalSurface::new(EntityId(1), &cylinder)
.radius()
.unwrap(),
2.5
);
let sphere = surface("IFCSPHERICALSURFACE", &[4.0]);
assert_eq!(
SphericalSurface::new(EntityId(1), &sphere)
.radius()
.unwrap(),
4.0
);
}
#[test]
fn torus_radii_are_read_in_major_then_minor_order() {
let e = surface("IFCTOROIDALSURFACE", &[5.0, 1.0]);
let view = ToroidalSurface::new(EntityId(1), &e);
assert_eq!(view.major_radius().unwrap(), 5.0);
assert_eq!(view.minor_radius().unwrap(), 1.0);
assert!(!view.is_self_intersecting().unwrap());
}
#[test]
fn a_minor_radius_at_least_the_major_is_reported_as_self_intersecting() {
let e = surface("IFCTOROIDALSURFACE", &[1.0, 2.0]);
assert!(ToroidalSurface::new(EntityId(1), &e)
.is_self_intersecting()
.unwrap());
}
#[test]
fn a_zero_radius_surface_is_degenerate_and_names_the_attribute() {
let cylinder = surface("IFCCYLINDRICALSURFACE", &[0.0]);
let err = CylindricalSurface::new(EntityId(8), &cylinder)
.radius()
.unwrap_err();
assert!(err.to_string().contains("#8"), "got: {err}");
assert!(err.to_string().contains("Radius"), "got: {err}");
let torus = surface("IFCTOROIDALSURFACE", &[5.0, -1.0]);
let err = ToroidalSurface::new(EntityId(1), &torus)
.minor_radius()
.unwrap_err();
assert!(err.to_string().contains("MinorRadius"), "got: {err}");
}
#[test]
fn only_the_plane_has_two_length_parameters() {
let plane = surface("IFCPLANE", &[]);
assert_eq!(
Plane::new(EntityId(1), &plane).parameter_kinds(),
(ParameterKind::Length, ParameterKind::Length)
);
let cylinder = surface("IFCCYLINDRICALSURFACE", &[1.0]);
assert_eq!(
CylindricalSurface::new(EntityId(1), &cylinder).parameter_kinds(),
(ParameterKind::Angle, ParameterKind::Length)
);
let sphere = surface("IFCSPHERICALSURFACE", &[1.0]);
assert_eq!(
SphericalSurface::new(EntityId(1), &sphere).parameter_kinds(),
(ParameterKind::Angle, ParameterKind::Angle)
);
let torus = surface("IFCTOROIDALSURFACE", &[5.0, 1.0]);
assert_eq!(
ToroidalSurface::new(EntityId(1), &torus).parameter_kinds(),
(ParameterKind::Angle, ParameterKind::Angle)
);
}
#[test]
fn a_surface_missing_its_position_reports_the_attribute_by_name() {
let e = Entity::new("IFCPLANE", vec![]);
let err = Plane::new(EntityId(1), &e).position_ref().unwrap_err();
assert!(err.to_string().contains("Position"), "got: {err}");
}
#[test]
fn every_elementary_surface_resolves_a_typed_placement_view() {
let mut model = Model::new();
model.insert(
EntityId(60),
Entity::new(
"IFCCARTESIANPOINT",
vec![Value::List(vec![
Value::Real(1.0),
Value::Real(2.0),
Value::Real(3.0),
])],
),
);
model.insert(
EntityId(70),
Entity::new("IFCAXIS2PLACEMENT3D", vec![Value::Ref(EntityId(60))]),
);
let plane = surface("IFCPLANE", &[]);
let view = Plane::new(EntityId(1), &plane).position(&model).unwrap();
assert_eq!(view.id(), EntityId(70));
assert_eq!(view.location(&model).unwrap(), [1.0, 2.0, 3.0]);
let cylinder = surface("IFCCYLINDRICALSURFACE", &[2.0]);
let view = CylindricalSurface::new(EntityId(2), &cylinder)
.position(&model)
.unwrap();
assert_eq!(view.location(&model).unwrap(), [1.0, 2.0, 3.0]);
let sphere = surface("IFCSPHERICALSURFACE", &[3.0]);
let view = SphericalSurface::new(EntityId(3), &sphere)
.position(&model)
.unwrap();
assert_eq!(view.location(&model).unwrap(), [1.0, 2.0, 3.0]);
let torus = surface("IFCTOROIDALSURFACE", &[5.0, 1.0]);
let view = ToroidalSurface::new(EntityId(4), &torus)
.position(&model)
.unwrap();
assert_eq!(view.location(&model).unwrap(), [1.0, 2.0, 3.0]);
}
#[test]
fn a_dangling_placement_reference_is_reported() {
let model = Model::new();
let plane = surface("IFCPLANE", &[]);
let error = Plane::new(EntityId(1), &plane)
.position(&model)
.expect_err("placement 70 is not in the model");
assert_eq!(error.entity(), Some(EntityId(1)));
}
fn model_with_2d_position() -> Model {
let mut model = Model::new();
model.insert(
EntityId(60),
Entity::new(
"IFCCARTESIANPOINT",
vec![Value::List(vec![Value::Real(1.0), Value::Real(2.0)])],
),
);
model.insert(
EntityId(70),
Entity::new("IFCAXIS2PLACEMENT2D", vec![Value::Ref(EntityId(60))]),
);
model
}
fn assert_wrong_placement(result: GeometryResult<Axis2Placement3D<'_>>) {
let error = result.expect_err("#70 is not an IfcAxis2Placement3D");
assert!(
matches!(
error,
crate::GeometryError::WrongEntityType {
entity: EntityId(70),
expected: "IfcAxis2Placement3D",
..
}
),
"got: {error:?}"
);
}
#[test]
fn a_plane_rejects_a_position_that_is_not_an_axis2_placement_3d() {
let model = model_with_2d_position();
let plane = surface("IFCPLANE", &[]);
assert_wrong_placement(Plane::new(EntityId(1), &plane).position(&model));
}
#[test]
fn a_cylinder_rejects_a_position_that_is_not_an_axis2_placement_3d() {
let model = model_with_2d_position();
let cylinder = surface("IFCCYLINDRICALSURFACE", &[2.0]);
assert_wrong_placement(CylindricalSurface::new(EntityId(2), &cylinder).position(&model));
}
#[test]
fn a_sphere_rejects_a_position_that_is_not_an_axis2_placement_3d() {
let model = model_with_2d_position();
let sphere = surface("IFCSPHERICALSURFACE", &[3.0]);
assert_wrong_placement(SphericalSurface::new(EntityId(3), &sphere).position(&model));
}
#[test]
fn a_torus_rejects_a_position_that_is_not_an_axis2_placement_3d() {
let model = model_with_2d_position();
let torus = surface("IFCTOROIDALSURFACE", &[5.0, 1.0]);
assert_wrong_placement(ToroidalSurface::new(EntityId(4), &torus).position(&model));
}
}