Skip to main content

ifc_alignment/authoring/
segment.rs

1//! Alignment parameter segments: horizontal, vertical and cant.
2//!
3//! Each writes one `IfcAlignment*Segment`. `StartTag`/`EndTag` are the
4//! inherited optional labels at slots 0..1 and are left null unless named.
5
6use ifc_model::{Entity, EntityId, Transaction, Value};
7
8use super::{finite, invalid};
9use crate::error::AlignmentError;
10use crate::slot;
11
12/// Authored fields for `IfcAlignmentHorizontalSegment`.
13///
14/// Radii follow the IFC convention the reader already applies: zero means
15/// straight, and a signed value carries the turn direction.
16#[derive(Debug, Clone, Copy)]
17#[non_exhaustive]
18pub struct HorizontalSegmentDraft {
19    /// `StartPoint`, an existing `IfcCartesianPoint`.
20    pub start_point: EntityId,
21    /// `StartDirection`, the bearing in the file's angle unit.
22    pub start_direction: f64,
23    /// `StartRadiusOfCurvature`; zero for a straight.
24    pub start_radius: f64,
25    /// `EndRadiusOfCurvature`; zero for a straight.
26    pub end_radius: f64,
27    /// `SegmentLength`; must be non-negative.
28    pub segment_length: f64,
29    /// `GravityCenterLineHeight`, optional.
30    pub gravity_center_line_height: Option<f64>,
31    /// `PredefinedType`, e.g. `LINE` or `CIRCULARARC`.
32    pub predefined_type: &'static str,
33}
34
35impl HorizontalSegmentDraft {
36    /// Starts a draft with the segment's required attributes.
37    #[must_use]
38    pub const fn new(
39        start_point: EntityId,
40        start_direction: f64,
41        start_radius: f64,
42        end_radius: f64,
43        segment_length: f64,
44        predefined_type: &'static str,
45    ) -> Self {
46        Self {
47            start_point,
48            start_direction,
49            start_radius,
50            end_radius,
51            segment_length,
52            gravity_center_line_height: None,
53            predefined_type,
54        }
55    }
56
57    /// Sets `GravityCenterLineHeight`.
58    #[must_use]
59    pub const fn gravity_center_line_height(mut self, value: f64) -> Self {
60        self.gravity_center_line_height = Some(value);
61        self
62    }
63}
64
65const HSEG: &str = "IFCALIGNMENTHORIZONTALSEGMENT";
66
67/// Stage an `IfcAlignmentHorizontalSegment`.
68///
69/// # Errors
70///
71/// Refuses a non-finite measure, a negative `SegmentLength`, and a `LINE`
72/// carrying a non-zero radius -- the reader rejects that combination when
73/// lowering, so writing it would produce a file this crate cannot read.
74pub fn horizontal_segment(
75    tx: &mut Transaction,
76    draft: &HorizontalSegmentDraft,
77) -> Result<EntityId, AlignmentError> {
78    finite(HSEG, "StartDirection", draft.start_direction)?;
79    finite(HSEG, "StartRadiusOfCurvature", draft.start_radius)?;
80    finite(HSEG, "EndRadiusOfCurvature", draft.end_radius)?;
81    finite(HSEG, "SegmentLength", draft.segment_length)?;
82    if let Some(height) = draft.gravity_center_line_height {
83        finite(HSEG, "GravityCenterLineHeight", height)?;
84    }
85    if draft.segment_length < 0.0 {
86        return Err(invalid(
87            HSEG,
88            "SegmentLength",
89            "a segment cannot be shorter than nothing",
90        ));
91    }
92    if draft.predefined_type == "LINE" && (draft.start_radius != 0.0 || draft.end_radius != 0.0) {
93        return Err(invalid(
94            HSEG,
95            "PredefinedType",
96            "LINE requires zero start and end radii",
97        ));
98    }
99    let mut attrs = vec![Value::Null; slot::horizontal::PREDEFINED_TYPE + 1];
100    attrs[slot::horizontal::START_POINT] = Value::Ref(draft.start_point);
101    attrs[slot::horizontal::START_DIRECTION] = Value::Real(draft.start_direction);
102    attrs[slot::horizontal::START_RADIUS] = Value::Real(draft.start_radius);
103    attrs[slot::horizontal::END_RADIUS] = Value::Real(draft.end_radius);
104    attrs[slot::horizontal::SEGMENT_LENGTH] = Value::Real(draft.segment_length);
105    if let Some(height) = draft.gravity_center_line_height {
106        attrs[slot::horizontal::GRAVITY_CENTER_LINE_HEIGHT] = Value::Real(height);
107    }
108    attrs[slot::horizontal::PREDEFINED_TYPE] = Value::Enum(draft.predefined_type.into());
109    Ok(tx.create(Entity::new(HSEG, attrs)))
110}
111
112/// Authored fields for `IfcAlignmentVerticalSegment`.
113#[derive(Debug, Clone, Copy)]
114#[non_exhaustive]
115pub struct VerticalSegmentDraft {
116    /// `StartDistAlong`, measured along the horizontal layout.
117    pub start_dist_along: f64,
118    /// `HorizontalLength`; must be non-negative.
119    pub horizontal_length: f64,
120    /// `StartHeight`.
121    pub start_height: f64,
122    /// `StartGradient`, a ratio, not an angle.
123    pub start_gradient: f64,
124    /// `EndGradient`, a ratio.
125    pub end_gradient: f64,
126    /// `RadiusOfCurvature`; required by arc families.
127    pub radius_of_curvature: Option<f64>,
128    /// `PredefinedType`, e.g. `CONSTANTGRADIENT` or `CIRCULARARC`.
129    pub predefined_type: &'static str,
130}
131
132impl VerticalSegmentDraft {
133    /// Starts a draft with the segment's required attributes.
134    #[must_use]
135    pub const fn new(
136        start_dist_along: f64,
137        horizontal_length: f64,
138        start_height: f64,
139        start_gradient: f64,
140        end_gradient: f64,
141        predefined_type: &'static str,
142    ) -> Self {
143        Self {
144            start_dist_along,
145            horizontal_length,
146            start_height,
147            start_gradient,
148            end_gradient,
149            radius_of_curvature: None,
150            predefined_type,
151        }
152    }
153
154    /// Sets `RadiusOfCurvature`, required by the arc families.
155    #[must_use]
156    pub const fn radius_of_curvature(mut self, value: f64) -> Self {
157        self.radius_of_curvature = Some(value);
158        self
159    }
160}
161
162const VSEG: &str = "IFCALIGNMENTVERTICALSEGMENT";
163
164/// Stage an `IfcAlignmentVerticalSegment`.
165///
166/// # Errors
167///
168/// Refuses a non-finite measure, a negative `HorizontalLength`, and a
169/// radius that disagrees with the family: the reader requires one exactly
170/// for `CIRCULARARC` and `PARABOLICARC`, so both a missing radius there and
171/// a stray radius elsewhere are refused here.
172pub fn vertical_segment(
173    tx: &mut Transaction,
174    draft: &VerticalSegmentDraft,
175) -> Result<EntityId, AlignmentError> {
176    finite(VSEG, "StartDistAlong", draft.start_dist_along)?;
177    finite(VSEG, "HorizontalLength", draft.horizontal_length)?;
178    finite(VSEG, "StartHeight", draft.start_height)?;
179    finite(VSEG, "StartGradient", draft.start_gradient)?;
180    finite(VSEG, "EndGradient", draft.end_gradient)?;
181    if let Some(radius) = draft.radius_of_curvature {
182        finite(VSEG, "RadiusOfCurvature", radius)?;
183    }
184    if draft.horizontal_length < 0.0 {
185        return Err(invalid(
186            VSEG,
187            "HorizontalLength",
188            "a segment cannot be shorter than nothing",
189        ));
190    }
191    let needs_radius = matches!(draft.predefined_type, "CIRCULARARC" | "PARABOLICARC");
192    if needs_radius != draft.radius_of_curvature.is_some() {
193        return Err(invalid(
194            VSEG,
195            "RadiusOfCurvature",
196            "radius is required exactly for circular and parabolic vertical segments",
197        ));
198    }
199    let mut attrs = vec![Value::Null; slot::vertical::PREDEFINED_TYPE + 1];
200    attrs[slot::vertical::START_DIST_ALONG] = Value::Real(draft.start_dist_along);
201    attrs[slot::vertical::HORIZONTAL_LENGTH] = Value::Real(draft.horizontal_length);
202    attrs[slot::vertical::START_HEIGHT] = Value::Real(draft.start_height);
203    attrs[slot::vertical::START_GRADIENT] = Value::Real(draft.start_gradient);
204    attrs[slot::vertical::END_GRADIENT] = Value::Real(draft.end_gradient);
205    if let Some(radius) = draft.radius_of_curvature {
206        attrs[slot::vertical::RADIUS_OF_CURVATURE] = Value::Real(radius);
207    }
208    attrs[slot::vertical::PREDEFINED_TYPE] = Value::Enum(draft.predefined_type.into());
209    Ok(tx.create(Entity::new(VSEG, attrs)))
210}
211
212/// Authored fields for `IfcAlignmentCantSegment`.
213#[derive(Debug, Clone, Copy)]
214#[non_exhaustive]
215pub struct CantSegmentDraft {
216    /// `StartDistAlong`.
217    pub start_dist_along: f64,
218    /// `HorizontalLength`; must be non-negative.
219    pub horizontal_length: f64,
220    /// `StartCantLeft`.
221    pub start_cant_left: f64,
222    /// `EndCantLeft`; paired with `end_cant_right`.
223    pub end_cant_left: Option<f64>,
224    /// `StartCantRight`.
225    pub start_cant_right: f64,
226    /// `EndCantRight`; paired with `end_cant_left`.
227    pub end_cant_right: Option<f64>,
228    /// `PredefinedType`, e.g. `CONSTANTCANT` or `LINEARTRANSITION`.
229    pub predefined_type: &'static str,
230}
231
232impl CantSegmentDraft {
233    /// Starts a draft with the segment's required attributes and no end
234    /// cant.
235    #[must_use]
236    pub const fn new(
237        start_dist_along: f64,
238        horizontal_length: f64,
239        start_cant_left: f64,
240        start_cant_right: f64,
241        predefined_type: &'static str,
242    ) -> Self {
243        Self {
244            start_dist_along,
245            horizontal_length,
246            start_cant_left,
247            end_cant_left: None,
248            start_cant_right,
249            end_cant_right: None,
250            predefined_type,
251        }
252    }
253
254    /// Sets `EndCantLeft`; pair it with [`Self::end_cant_right`].
255    #[must_use]
256    pub const fn end_cant_left(mut self, value: f64) -> Self {
257        self.end_cant_left = Some(value);
258        self
259    }
260
261    /// Sets `EndCantRight`; pair it with [`Self::end_cant_left`].
262    #[must_use]
263    pub const fn end_cant_right(mut self, value: f64) -> Self {
264        self.end_cant_right = Some(value);
265        self
266    }
267}
268
269const CSEG: &str = "IFCALIGNMENTCANTSEGMENT";
270
271/// Stage an `IfcAlignmentCantSegment`.
272///
273/// # Errors
274///
275/// Refuses a non-finite measure, a negative `HorizontalLength`, and an
276/// unpaired end cant: the reader requires left and right to be both present
277/// or both absent, because one alone does not describe a rail pair.
278pub fn cant_segment(
279    tx: &mut Transaction,
280    draft: &CantSegmentDraft,
281) -> Result<EntityId, AlignmentError> {
282    finite(CSEG, "StartDistAlong", draft.start_dist_along)?;
283    finite(CSEG, "HorizontalLength", draft.horizontal_length)?;
284    finite(CSEG, "StartCantLeft", draft.start_cant_left)?;
285    finite(CSEG, "StartCantRight", draft.start_cant_right)?;
286    if let Some(value) = draft.end_cant_left {
287        finite(CSEG, "EndCantLeft", value)?;
288    }
289    if let Some(value) = draft.end_cant_right {
290        finite(CSEG, "EndCantRight", value)?;
291    }
292    if draft.horizontal_length < 0.0 {
293        return Err(invalid(
294            CSEG,
295            "HorizontalLength",
296            "a segment cannot be shorter than nothing",
297        ));
298    }
299    if draft.end_cant_left.is_some() != draft.end_cant_right.is_some() {
300        return Err(invalid(
301            CSEG,
302            "EndCantLeft",
303            "left and right end cant must both be supplied or both omitted",
304        ));
305    }
306    let mut attrs = vec![Value::Null; slot::cant::PREDEFINED_TYPE + 1];
307    attrs[slot::cant::START_DIST_ALONG] = Value::Real(draft.start_dist_along);
308    attrs[slot::cant::HORIZONTAL_LENGTH] = Value::Real(draft.horizontal_length);
309    attrs[slot::cant::START_CANT_LEFT] = Value::Real(draft.start_cant_left);
310    if let Some(value) = draft.end_cant_left {
311        attrs[slot::cant::END_CANT_LEFT] = Value::Real(value);
312    }
313    attrs[slot::cant::START_CANT_RIGHT] = Value::Real(draft.start_cant_right);
314    if let Some(value) = draft.end_cant_right {
315        attrs[slot::cant::END_CANT_RIGHT] = Value::Real(value);
316    }
317    attrs[slot::cant::PREDEFINED_TYPE] = Value::Enum(draft.predefined_type.into());
318    Ok(tx.create(Entity::new(CSEG, attrs)))
319}