Skip to main content

ifc_geometry/curve/
line.rs

1//! `IfcLine`: a point and a vector, unbounded in both directions.
2//!
3//! # The trap: `Dir` is an `IfcVector`, not an `IfcDirection`
4//!
5//! The parameterisation is `P(u) = Pnt + u * Dir`, where `Dir` carries a
6//! *magnitude* as well as an orientation. A caller that normalises `Dir` to a
7//! unit vector silently rescales the parameter space by that magnitude, which
8//! matters the moment the line is used as an `IfcTrimmedCurve` basis curve
9//! trimmed by parameter: the trim values are in units of `Dir`, not in length
10//! units. This view therefore hands back the `IfcVector` reference untouched.
11
12use crate::error::GeometryResult;
13use crate::resource::point::CartesianPoint;
14use crate::resource::resolve;
15use crate::slots::Slots;
16use ifc_model::{Entity, EntityId, Model};
17
18/// `IfcLine` attribute slots.
19///
20/// From IFC4 ADD2 TC1: `IfcLine` inherits nothing explicit from
21/// `IfcCurve` / `IfcGeometricRepresentationItem` / `IfcRepresentationItem`
22/// (those declare only inverse and derived attributes), so its own two
23/// attributes occupy slots 0 and 1.
24pub(crate) mod slot {
25    /// `Pnt`: `IfcCartesianPoint`, the point at parameter zero.
26    pub const PNT: usize = 0;
27    /// `Dir`: `IfcVector`, the direction *and* the parameter scale.
28    pub const DIR: usize = 1;
29}
30
31/// A borrowed view of an `IfcLine`.
32#[derive(Debug, Clone, Copy)]
33pub struct Line<'m> {
34    slots: Slots<'m>,
35}
36
37impl<'m> Line<'m> {
38    /// Wrap an entity known to be an `IfcLine`.
39    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
40        Self {
41            slots: Slots::new(id, entity),
42        }
43    }
44
45    /// The entity id.
46    pub fn id(&self) -> EntityId {
47        self.slots.id()
48    }
49
50    /// The `IfcCartesianPoint` at parameter zero.
51    ///
52    /// Returned as a raw reference; [`Self::point`] resolves it.
53    pub fn point_ref(&self) -> GeometryResult<EntityId> {
54        self.slots.req_ref(slot::PNT, "Pnt")
55    }
56
57    /// The point at parameter zero as a typed view, resolved from the model.
58    ///
59    /// Its dimension is the file's own: a 2D line stays 2D until the caller
60    /// asks for [`CartesianPoint::coordinates_3d`].
61    pub fn point<'v>(&self, model: &'v Model) -> GeometryResult<CartesianPoint<'v>> {
62        resolve::cartesian_point(model, self.id(), self.point_ref()?)
63    }
64
65    /// The `IfcVector` giving direction and parameter scale.
66    ///
67    /// Deliberately *not* an `IfcDirection`: see the module docs on why
68    /// normalising this reference changes the meaning of trim parameters.
69    pub fn direction_vector_ref(&self) -> GeometryResult<EntityId> {
70        self.slots.req_ref(slot::DIR, "Dir")
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use ifc_model::Value;
78
79    fn line() -> Entity {
80        Entity::new(
81            "IFCLINE",
82            vec![Value::Ref(EntityId(10)), Value::Ref(EntityId(11))],
83        )
84    }
85
86    #[test]
87    fn reads_origin_point_and_direction_vector_from_their_own_slots() {
88        let e = line();
89        let view = Line::new(EntityId(1), &e);
90        assert_eq!(view.point_ref().unwrap(), EntityId(10));
91        assert_eq!(view.direction_vector_ref().unwrap(), EntityId(11));
92    }
93
94    #[test]
95    fn a_line_missing_its_direction_reports_the_attribute_by_name() {
96        let e = Entity::new("IFCLINE", vec![Value::Ref(EntityId(10))]);
97        let view = Line::new(EntityId(7), &e);
98        let err = view.direction_vector_ref().unwrap_err();
99        assert!(err.to_string().contains("Dir"), "got: {err}");
100        assert!(err.to_string().contains("#7"), "got: {err}");
101    }
102
103    #[test]
104    fn the_origin_point_resolves_to_its_coordinates() {
105        let mut model = Model::new();
106        model.insert(
107            EntityId(10),
108            Entity::new(
109                "IFCCARTESIANPOINT",
110                vec![Value::List(vec![Value::Real(3.0), Value::Real(4.0)])],
111            ),
112        );
113        let e = line();
114        let point = Line::new(EntityId(1), &e).point(&model).unwrap();
115        assert_eq!(point.id(), EntityId(10));
116        assert_eq!(point.coordinates().unwrap(), vec![3.0, 4.0]);
117    }
118
119    #[test]
120    fn an_origin_that_is_not_a_point_is_a_typed_error() {
121        let mut model = Model::new();
122        model.insert(EntityId(10), Entity::new("IFCDIRECTION", vec![]));
123        let e = line();
124        let err = Line::new(EntityId(1), &e).point(&model).unwrap_err();
125        assert!(matches!(
126            err,
127            crate::GeometryError::WrongEntityType {
128                entity: EntityId(10),
129                ..
130            }
131        ));
132        let err = Line::new(EntityId(1), &e).point(&Model::new()).unwrap_err();
133        assert_eq!(err.entity(), Some(EntityId(1)), "dangling names the line");
134    }
135
136    #[test]
137    fn id_is_carried_through_for_error_attribution() {
138        let e = line();
139        assert_eq!(Line::new(EntityId(42), &e).id(), EntityId(42));
140    }
141}