ifc-geometry 0.5.0

IFC semantic views lowered into the format-neutral geometry DAG.
Documentation
//! `IfcLine`: a point and a vector, unbounded in both directions.
//!
//! # The trap: `Dir` is an `IfcVector`, not an `IfcDirection`
//!
//! The parameterisation is `P(u) = Pnt + u * Dir`, where `Dir` carries a
//! *magnitude* as well as an orientation. A caller that normalises `Dir` to a
//! unit vector silently rescales the parameter space by that magnitude, which
//! matters the moment the line is used as an `IfcTrimmedCurve` basis curve
//! trimmed by parameter: the trim values are in units of `Dir`, not in length
//! units. This view therefore hands back the `IfcVector` reference untouched.

use crate::error::GeometryResult;
use crate::resource::point::CartesianPoint;
use crate::resource::resolve;
use crate::slots::Slots;
use ifc_model::{Entity, EntityId, Model};

/// `IfcLine` attribute slots.
///
/// From IFC4 ADD2 TC1: `IfcLine` inherits nothing explicit from
/// `IfcCurve` / `IfcGeometricRepresentationItem` / `IfcRepresentationItem`
/// (those declare only inverse and derived attributes), so its own two
/// attributes occupy slots 0 and 1.
pub(crate) mod slot {
    /// `Pnt`: `IfcCartesianPoint`, the point at parameter zero.
    pub const PNT: usize = 0;
    /// `Dir`: `IfcVector`, the direction *and* the parameter scale.
    pub const DIR: usize = 1;
}

/// A borrowed view of an `IfcLine`.
#[derive(Debug, Clone, Copy)]
pub struct Line<'m> {
    slots: Slots<'m>,
}

impl<'m> Line<'m> {
    /// Wrap an entity known to be an `IfcLine`.
    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
        Self {
            slots: Slots::new(id, entity),
        }
    }

    /// The entity id.
    pub fn id(&self) -> EntityId {
        self.slots.id()
    }

    /// The `IfcCartesianPoint` at parameter zero.
    ///
    /// Returned as a raw reference; [`Self::point`] resolves it.
    pub fn point_ref(&self) -> GeometryResult<EntityId> {
        self.slots.req_ref(slot::PNT, "Pnt")
    }

    /// The point at parameter zero as a typed view, resolved from the model.
    ///
    /// Its dimension is the file's own: a 2D line stays 2D until the caller
    /// asks for [`CartesianPoint::coordinates_3d`].
    pub fn point<'v>(&self, model: &'v Model) -> GeometryResult<CartesianPoint<'v>> {
        resolve::cartesian_point(model, self.id(), self.point_ref()?)
    }

    /// The `IfcVector` giving direction and parameter scale.
    ///
    /// Deliberately *not* an `IfcDirection`: see the module docs on why
    /// normalising this reference changes the meaning of trim parameters.
    pub fn direction_vector_ref(&self) -> GeometryResult<EntityId> {
        self.slots.req_ref(slot::DIR, "Dir")
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use ifc_model::Value;

    fn line() -> Entity {
        Entity::new(
            "IFCLINE",
            vec![Value::Ref(EntityId(10)), Value::Ref(EntityId(11))],
        )
    }

    #[test]
    fn reads_origin_point_and_direction_vector_from_their_own_slots() {
        let e = line();
        let view = Line::new(EntityId(1), &e);
        assert_eq!(view.point_ref().unwrap(), EntityId(10));
        assert_eq!(view.direction_vector_ref().unwrap(), EntityId(11));
    }

    #[test]
    fn a_line_missing_its_direction_reports_the_attribute_by_name() {
        let e = Entity::new("IFCLINE", vec![Value::Ref(EntityId(10))]);
        let view = Line::new(EntityId(7), &e);
        let err = view.direction_vector_ref().unwrap_err();
        assert!(err.to_string().contains("Dir"), "got: {err}");
        assert!(err.to_string().contains("#7"), "got: {err}");
    }

    #[test]
    fn the_origin_point_resolves_to_its_coordinates() {
        let mut model = Model::new();
        model.insert(
            EntityId(10),
            Entity::new(
                "IFCCARTESIANPOINT",
                vec![Value::List(vec![Value::Real(3.0), Value::Real(4.0)])],
            ),
        );
        let e = line();
        let point = Line::new(EntityId(1), &e).point(&model).unwrap();
        assert_eq!(point.id(), EntityId(10));
        assert_eq!(point.coordinates().unwrap(), vec![3.0, 4.0]);
    }

    #[test]
    fn an_origin_that_is_not_a_point_is_a_typed_error() {
        let mut model = Model::new();
        model.insert(EntityId(10), Entity::new("IFCDIRECTION", vec![]));
        let e = line();
        let err = Line::new(EntityId(1), &e).point(&model).unwrap_err();
        assert!(matches!(
            err,
            crate::GeometryError::WrongEntityType {
                entity: EntityId(10),
                ..
            }
        ));
        let err = Line::new(EntityId(1), &e).point(&Model::new()).unwrap_err();
        assert_eq!(err.entity(), Some(EntityId(1)), "dangling names the line");
    }

    #[test]
    fn id_is_carried_through_for_error_attribution() {
        let e = line();
        assert_eq!(Line::new(EntityId(42), &e).id(), EntityId(42));
    }
}