Skip to main content

ifc_style/
annotation.rs

1//! Borrowed annotation and presentable-text projections.
2
3use ifc_model::EntityId;
4
5use crate::error::{StyleError, StyleResult};
6use crate::view::Record;
7
8/// Direction in which a text literal is laid out.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum TextPath {
11    /// `IfcTextPathEnum.LEFT`: text runs right-to-left.
12    Left,
13    /// `IfcTextPathEnum.RIGHT`: text runs left-to-right.
14    Right,
15    /// `IfcTextPathEnum.UP`: text runs bottom-to-top.
16    Up,
17    /// `IfcTextPathEnum.DOWN`: text runs top-to-bottom.
18    Down,
19}
20
21impl TextPath {
22    pub(crate) const fn as_ifc(self) -> &'static str {
23        match self {
24            Self::Left => "LEFT",
25            Self::Right => "RIGHT",
26            Self::Up => "UP",
27            Self::Down => "DOWN",
28        }
29    }
30
31    fn parse(value: &str, id: EntityId) -> StyleResult<Self> {
32        match value.to_ascii_uppercase().as_str() {
33            "LEFT" => Ok(Self::Left),
34            "RIGHT" => Ok(Self::Right),
35            "UP" => Ok(Self::Up),
36            "DOWN" => Ok(Self::Down),
37            _ => Err(StyleError::InvalidValue {
38                entity: "IFCTEXTLITERAL".to_owned(),
39                id,
40                attribute: "Path",
41                value: value.to_owned(),
42            }),
43        }
44    }
45}
46
47/// Alignment of an `IfcTextLiteralWithExtent` inside its planar extent.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum BoxAlignment {
50    /// `top-left`: anchored to the extent's top-left corner.
51    TopLeft,
52    /// `top-middle`: centred along the extent's top edge.
53    TopMiddle,
54    /// `top-right`: anchored to the extent's top-right corner.
55    TopRight,
56    /// `middle-left`: centred along the extent's left edge.
57    MiddleLeft,
58    /// `center`: centred in both axes within the extent.
59    Center,
60    /// `middle-right`: centred along the extent's right edge.
61    MiddleRight,
62    /// `bottom-left`: anchored to the extent's bottom-left corner.
63    BottomLeft,
64    /// `bottom-middle`: centred along the extent's bottom edge.
65    BottomMiddle,
66    /// `bottom-right`: anchored to the extent's bottom-right corner.
67    BottomRight,
68}
69
70impl BoxAlignment {
71    pub(crate) const fn as_ifc(self) -> &'static str {
72        match self {
73            Self::TopLeft => "top-left",
74            Self::TopMiddle => "top-middle",
75            Self::TopRight => "top-right",
76            Self::MiddleLeft => "middle-left",
77            Self::Center => "center",
78            Self::MiddleRight => "middle-right",
79            Self::BottomLeft => "bottom-left",
80            Self::BottomMiddle => "bottom-middle",
81            Self::BottomRight => "bottom-right",
82        }
83    }
84
85    fn parse(value: &str, record: Record<'_, '_>) -> StyleResult<Self> {
86        match value.to_ascii_lowercase().as_str() {
87            "top-left" => Ok(Self::TopLeft),
88            "top-middle" => Ok(Self::TopMiddle),
89            "top-right" => Ok(Self::TopRight),
90            "middle-left" => Ok(Self::MiddleLeft),
91            "center" => Ok(Self::Center),
92            "middle-right" => Ok(Self::MiddleRight),
93            "bottom-left" => Ok(Self::BottomLeft),
94            "bottom-middle" => Ok(Self::BottomMiddle),
95            "bottom-right" => Ok(Self::BottomRight),
96            _ => Err(StyleError::InvalidValue {
97                entity: record.entity.type_name.to_string(),
98                id: record.id,
99                attribute: "BoxAlignment",
100                value: value.to_owned(),
101            }),
102        }
103    }
104}
105
106/// The `IfcAnnotationTypeEnum` classifying an `IfcAnnotation`'s purpose.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum AnnotationType {
109    /// `CONTOURLINE`: a line of constant elevation or value.
110    ContourLine,
111    /// `DIMENSION`: a dimension annotation.
112    Dimension,
113    /// `ISOBAR`: a line of constant pressure.
114    Isobar,
115    /// `ISOLUX`: a line of constant illuminance.
116    Isolux,
117    /// `ISOTHERM`: a line of constant temperature.
118    Isotherm,
119    /// `LEADER`: a leader line pointing from text to a feature.
120    Leader,
121    /// `SURVEY`: a survey annotation.
122    Survey,
123    /// `SYMBOL`: a symbolic annotation.
124    Symbol,
125    /// `TEXT`: a text annotation.
126    Text,
127    /// `USERDEFINED`: a case named by `ObjectType`, outside this enumeration.
128    UserDefined,
129    /// `NOTDEFINED`: the annotation kind is intentionally unspecified.
130    NotDefined,
131}
132
133impl AnnotationType {
134    pub(crate) const fn as_ifc(self) -> &'static str {
135        match self {
136            Self::ContourLine => "CONTOURLINE",
137            Self::Dimension => "DIMENSION",
138            Self::Isobar => "ISOBAR",
139            Self::Isolux => "ISOLUX",
140            Self::Isotherm => "ISOTHERM",
141            Self::Leader => "LEADER",
142            Self::Survey => "SURVEY",
143            Self::Symbol => "SYMBOL",
144            Self::Text => "TEXT",
145            Self::UserDefined => "USERDEFINED",
146            Self::NotDefined => "NOTDEFINED",
147        }
148    }
149
150    fn parse(value: &str, record: Record<'_, '_>) -> StyleResult<Self> {
151        match value.to_ascii_uppercase().as_str() {
152            "CONTOURLINE" => Ok(Self::ContourLine),
153            "DIMENSION" => Ok(Self::Dimension),
154            "ISOBAR" => Ok(Self::Isobar),
155            "ISOLUX" => Ok(Self::Isolux),
156            "ISOTHERM" => Ok(Self::Isotherm),
157            "LEADER" => Ok(Self::Leader),
158            "SURVEY" => Ok(Self::Survey),
159            "SYMBOL" => Ok(Self::Symbol),
160            "TEXT" => Ok(Self::Text),
161            "USERDEFINED" => Ok(Self::UserDefined),
162            "NOTDEFINED" => Ok(Self::NotDefined),
163            _ => Err(StyleError::InvalidValue {
164                entity: record.entity.type_name.to_string(),
165                id: record.id,
166                attribute: "PredefinedType",
167                value: value.to_owned(),
168            }),
169        }
170    }
171}
172
173/// A schema-aware view of `IfcAnnotation`.
174#[derive(Debug, Clone, Copy)]
175pub struct Annotation<'m, 's> {
176    record: Record<'m, 's>,
177}
178
179impl<'m, 's> Annotation<'m, 's> {
180    pub(crate) fn from_record(record: Record<'m, 's>) -> StyleResult<Self> {
181        Ok(Self { record })
182    }
183
184    /// The entity id of this `IfcAnnotation`.
185    #[must_use]
186    pub fn id(&self) -> EntityId {
187        self.record.id
188    }
189
190    /// The `GlobalId` (IFC GUID) attribute. Mandatory.
191    pub fn global_id(&self) -> StyleResult<&'m str> {
192        self.record.required_text("GlobalId")
193    }
194
195    /// The `Name` attribute, when authored.
196    pub fn name(&self) -> StyleResult<Option<&'m str>> {
197        self.record.optional_text("Name")
198    }
199
200    /// The `Description` attribute, when authored.
201    pub fn description(&self) -> StyleResult<Option<&'m str>> {
202        self.record.optional_text("Description")
203    }
204
205    /// The `ObjectType` attribute, when authored.
206    pub fn object_type(&self) -> StyleResult<Option<&'m str>> {
207        self.record.optional_text("ObjectType")
208    }
209
210    /// The `OwnerHistory` reference to an `IfcOwnerHistory`, when authored.
211    pub fn owner_history(&self) -> StyleResult<Option<EntityId>> {
212        self.record.optional_ref("OwnerHistory", "IfcOwnerHistory")
213    }
214
215    /// The `ObjectPlacement` reference to an `IfcObjectPlacement`, when authored.
216    pub fn object_placement(&self) -> StyleResult<Option<EntityId>> {
217        self.record
218            .optional_ref("ObjectPlacement", "IfcObjectPlacement")
219    }
220
221    /// The `Representation` reference to an `IfcProductRepresentation`, when authored.
222    pub fn representation(&self) -> StyleResult<Option<EntityId>> {
223        self.record
224            .optional_ref("Representation", "IfcProductRepresentation")
225    }
226
227    /// IFC4X3's annotation predefined type; older schemas return `None`.
228    pub fn predefined_type(&self) -> StyleResult<Option<AnnotationType>> {
229        self.record
230            .optional_enum("PredefinedType")?
231            .map(|value| AnnotationType::parse(value, self.record))
232            .transpose()
233    }
234}
235
236/// A schema-aware view of `IfcTextLiteral` (including its subtype).
237#[derive(Debug, Clone, Copy)]
238pub struct TextLiteral<'m, 's> {
239    record: Record<'m, 's>,
240}
241
242impl<'m, 's> TextLiteral<'m, 's> {
243    pub(crate) fn from_record(record: Record<'m, 's>) -> StyleResult<Self> {
244        Ok(Self { record })
245    }
246
247    /// The entity id of this `IfcTextLiteral`.
248    #[must_use]
249    pub fn id(&self) -> EntityId {
250        self.record.id
251    }
252
253    /// The `Literal` attribute: the text content itself.
254    pub fn literal(&self) -> StyleResult<&'m str> {
255        self.record.required_text("Literal")
256    }
257
258    /// The `Placement` reference to the `IfcPlacement` positioning the text.
259    pub fn placement(&self) -> StyleResult<EntityId> {
260        self.record.required_ref("Placement", "IfcPlacement")
261    }
262
263    /// The `Path` attribute: reading direction of the text.
264    pub fn path(&self) -> StyleResult<TextPath> {
265        TextPath::parse(self.record.required_enum("Path")?, self.record.id)
266    }
267}
268
269/// A schema-aware view of `IfcTextLiteralWithExtent`.
270#[derive(Debug, Clone, Copy)]
271pub struct TextLiteralWithExtent<'m, 's> {
272    record: Record<'m, 's>,
273}
274
275impl<'m, 's> TextLiteralWithExtent<'m, 's> {
276    pub(crate) fn from_record(record: Record<'m, 's>) -> StyleResult<Self> {
277        Ok(Self { record })
278    }
279
280    /// The entity id of this `IfcTextLiteralWithExtent`.
281    #[must_use]
282    pub fn id(&self) -> EntityId {
283        self.record.id
284    }
285
286    /// The `Literal` attribute: the text content itself.
287    pub fn literal(&self) -> StyleResult<&'m str> {
288        self.record.required_text("Literal")
289    }
290
291    /// The `Placement` reference to the `IfcPlacement` positioning the text.
292    pub fn placement(&self) -> StyleResult<EntityId> {
293        self.record.required_ref("Placement", "IfcPlacement")
294    }
295
296    /// The `Path` attribute: reading direction of the text.
297    pub fn path(&self) -> StyleResult<TextPath> {
298        TextPath::parse(self.record.required_enum("Path")?, self.record.id)
299    }
300
301    /// The `Extent` reference to the `IfcPlanarExtent` bounding the text.
302    pub fn extent(&self) -> StyleResult<EntityId> {
303        self.record.required_ref("Extent", "IfcPlanarExtent")
304    }
305
306    /// The `BoxAlignment` attribute: how the text is anchored within its extent.
307    pub fn box_alignment(&self) -> StyleResult<BoxAlignment> {
308        BoxAlignment::parse(self.record.required_text("BoxAlignment")?, self.record)
309    }
310}
311
312/// A bounded fill region made from one outer and optional inner curves.
313#[derive(Debug, Clone, Copy)]
314pub struct AnnotationFillArea<'m, 's> {
315    record: Record<'m, 's>,
316}
317
318impl<'m, 's> AnnotationFillArea<'m, 's> {
319    pub(crate) fn from_record(record: Record<'m, 's>) -> StyleResult<Self> {
320        Ok(Self { record })
321    }
322
323    /// The entity id of this `IfcAnnotationFillArea`.
324    #[must_use]
325    pub fn id(&self) -> EntityId {
326        self.record.id
327    }
328
329    /// The `OuterBoundary` reference to the enclosing `IfcCurve`. Mandatory.
330    pub fn outer_boundary(&self) -> StyleResult<EntityId> {
331        self.record.required_ref("OuterBoundary", "IfcCurve")
332    }
333
334    /// The `InnerBoundaries` references to `IfcCurve`s cut out of the fill area, if any.
335    pub fn inner_boundaries(&self) -> StyleResult<Vec<EntityId>> {
336        self.record.optional_refs("InnerBoundaries", "IfcCurve")
337    }
338}