Skip to main content

ifc_geometry/authoring/
surface_curve.rs

1//! Curves that live on a surface, and the segment forms.
2//!
3//! # A boundary curve must be closed
4//!
5//! `IfcBoundaryCurve` carries `IsClosed : SELF\IfcCompositeCurve.ClosedCurve`.
6//! Everywhere else in this crate `ClosedCurve` is written UNKNOWN,
7//! because deciding it needs an evaluator -- but here the schema
8//! *requires* it true, so a boundary curve that admits UNKNOWN is
9//! non-conforming. The writer states TRUE and the caller is telling
10//! it the curve closes; that is a claim the file makes either way.
11//!
12//! # Two pcurves, and whether their surfaces match
13//!
14//! `IfcIntersectionCurve` and `IfcSeamCurve` both require exactly two
15//! associated pcurves. They differ in what the surfaces must be: an
16//! intersection needs two *distinct* surfaces, a seam needs the *same*
17//! surface twice -- it is the join where a closed surface meets
18//! itself. The count is checkable here; which surface each pcurve
19//! sits on requires resolving the reference, so that is left to the
20//! validator.
21
22use ifc_model::{Entity, EntityId, Transaction, Value};
23
24use crate::curve::composite::{curve_slot, segment_slot};
25use crate::curve::TransitionCode;
26use crate::error::GeometryError;
27
28use super::{invalid, refs, require_finite};
29
30/// Which representation of a surface curve is authoritative.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum SurfaceCurveRepresentation {
33    /// The 3D curve is the master.
34    Curve3D,
35    /// The first parameter-space curve is the master.
36    PCurveS1,
37    /// The second parameter-space curve is the master.
38    PCurveS2,
39}
40
41impl SurfaceCurveRepresentation {
42    /// The EXPRESS token.
43    fn token(self) -> &'static str {
44        match self {
45            Self::Curve3D => "CURVE3D",
46            Self::PCurveS1 => "PCURVE_S1",
47            Self::PCurveS2 => "PCURVE_S2",
48        }
49    }
50}
51
52/// Stage an `IfcPcurve`: a curve in a surface's parameter space.
53pub fn pcurve(
54    tx: &mut Transaction,
55    basis_surface: EntityId,
56    reference_curve: EntityId,
57) -> EntityId {
58    let attrs = vec![Value::Ref(basis_surface), Value::Ref(reference_curve)];
59    tx.create(Entity::new("IFCPCURVE", attrs))
60}
61
62/// Which surface curve flavour to author.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum SurfaceCurveKind {
65    /// `IfcSurfaceCurve`: a curve lying on one or two surfaces.
66    Plain,
67    /// `IfcIntersectionCurve`: where two *distinct* surfaces meet.
68    Intersection,
69    /// `IfcSeamCurve`: where one closed surface meets itself.
70    Seam,
71}
72
73impl SurfaceCurveKind {
74    /// The entity type name.
75    fn type_name(self) -> &'static str {
76        match self {
77            Self::Plain => "IFCSURFACECURVE",
78            Self::Intersection => "IFCINTERSECTIONCURVE",
79            Self::Seam => "IFCSEAMCURVE",
80        }
81    }
82
83    /// Does this flavour require exactly two pcurves?
84    fn needs_two(self) -> bool {
85        matches!(self, Self::Intersection | Self::Seam)
86    }
87}
88
89/// Stage a surface curve.
90///
91/// # Errors
92///
93/// Refuses an empty or over-long pcurve list -- `LIST [1:2]` -- and
94/// refuses anything but exactly two for the intersection and seam
95/// forms, whose `TwoPCurves` rule demands both.
96pub fn surface_curve(
97    tx: &mut Transaction,
98    kind: SurfaceCurveKind,
99    curve_3d: EntityId,
100    associated_geometry: &[EntityId],
101    master: SurfaceCurveRepresentation,
102) -> Result<EntityId, GeometryError> {
103    let type_name = kind.type_name();
104    if associated_geometry.is_empty() || associated_geometry.len() > 2 {
105        return Err(invalid(
106            type_name,
107            "AssociatedGeometry",
108            format!("expected 1 or 2 pcurves, got {}", associated_geometry.len()),
109        ));
110    }
111    if kind.needs_two() && associated_geometry.len() != 2 {
112        return Err(invalid(
113            type_name,
114            "AssociatedGeometry",
115            "TwoPCurves: this form needs a pcurve on each surface",
116        ));
117    }
118    let attrs = vec![
119        Value::Ref(curve_3d),
120        refs(associated_geometry),
121        Value::Enum(master.token().into()),
122    ];
123    Ok(tx.create(Entity::new(type_name, attrs)))
124}
125
126/// Which composite-curve-on-surface flavour to author.
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub enum OnSurfaceKind {
129    /// `IfcCompositeCurveOnSurface`: no closure requirement.
130    Composite,
131    /// `IfcBoundaryCurve`: must be closed.
132    Boundary,
133    /// `IfcOuterBoundaryCurve`: the outermost boundary, also closed.
134    OuterBoundary,
135}
136
137impl OnSurfaceKind {
138    /// The entity type name.
139    fn type_name(self) -> &'static str {
140        match self {
141            Self::Composite => "IFCCOMPOSITECURVEONSURFACE",
142            Self::Boundary => "IFCBOUNDARYCURVE",
143            Self::OuterBoundary => "IFCOUTERBOUNDARYCURVE",
144        }
145    }
146
147    /// Does the schema require `ClosedCurve` to be true?
148    fn must_close(self) -> bool {
149        matches!(self, Self::Boundary | Self::OuterBoundary)
150    }
151}
152
153/// Stage a composite curve lying on a surface.
154///
155/// For the two boundary forms `ClosedCurve` is written TRUE, because
156/// `IsClosed` requires it: a boundary that is not closed bounds
157/// nothing. Elsewhere this crate writes UNKNOWN, since closure needs
158/// an evaluator -- here the schema has already decided.
159///
160/// # Errors
161///
162/// Refuses an empty segment list: `LIST [1:?]`.
163pub fn composite_curve_on_surface(
164    tx: &mut Transaction,
165    kind: OnSurfaceKind,
166    segments: &[EntityId],
167) -> Result<EntityId, GeometryError> {
168    let type_name = kind.type_name();
169    if segments.is_empty() {
170        return Err(invalid(
171            type_name,
172            "Segments",
173            "expected at least one segment",
174        ));
175    }
176    let mut attrs = vec![Value::Null; 2];
177    attrs[curve_slot::SEGMENTS] = refs(segments);
178    attrs[curve_slot::SELF_INTERSECT] = if kind.must_close() {
179        // IsClosed: the schema requires this, so UNKNOWN is not an option.
180        Value::Bool(true)
181    } else {
182        Value::LogicalUnknown
183    };
184    Ok(tx.create(Entity::new(type_name, attrs)))
185}
186
187/// Stage an `IfcReparametrisedCompositeCurveSegment`.
188///
189/// The plain segment's three slots plus `ParamLength`, which restates
190/// the segment's parameter range so a composite can be walked without
191/// evaluating each piece.
192///
193/// # Errors
194///
195/// Refuses a non-positive or non-finite parameter length.
196pub fn reparametrised_composite_curve_segment(
197    tx: &mut Transaction,
198    transition: TransitionCode,
199    same_sense: bool,
200    parent_curve: EntityId,
201    param_length: f64,
202) -> Result<EntityId, GeometryError> {
203    const T: &str = "IFCREPARAMETRISEDCOMPOSITECURVESEGMENT";
204    require_finite(T, "ParamLength", &[param_length])?;
205    if param_length <= 0.0 {
206        return Err(invalid(
207            T,
208            "ParamLength",
209            format!("expected a positive parameter length, got {param_length}"),
210        ));
211    }
212    let mut attrs = vec![Value::Null; 4];
213    attrs[segment_slot::TRANSITION] = Value::Enum(transition.token().into());
214    attrs[segment_slot::SAME_SENSE] = Value::Bool(same_sense);
215    attrs[segment_slot::PARENT_CURVE] = Value::Ref(parent_curve);
216    attrs[segment_slot::PARAM_LENGTH] = Value::Typed {
217        type_name: "IFCPARAMETERVALUE".into(),
218        value: Box::new(Value::Real(param_length)),
219    };
220    Ok(tx.create(Entity::new(T, attrs)))
221}
222
223/// A length along a curve, or a parameter in its own space.
224///
225/// `IfcCurveMeasureSelect` admits either, and the two are not
226/// interchangeable: a length is in the model's length unit, a
227/// parameter is in whatever the curve's own parameterisation uses.
228/// Writing one where the other is meant rescales the segment.
229#[derive(Debug, Clone, Copy)]
230pub enum CurveMeasure {
231    /// `IfcNonNegativeLengthMeasure`: a distance along the curve.
232    Length(f64),
233    /// `IfcParameterValue`: a value in the curve's parameter space.
234    Parameter(f64),
235}
236
237impl CurveMeasure {
238    /// Encode with the measure type that says which kind this is.
239    fn to_value(
240        self,
241        type_name: &'static str,
242        attribute: &'static str,
243    ) -> Result<Value, GeometryError> {
244        let (measure, value) = match self {
245            Self::Length(v) => ("IFCNONNEGATIVELENGTHMEASURE", v),
246            Self::Parameter(v) => ("IFCPARAMETERVALUE", v),
247        };
248        require_finite(type_name, attribute, &[value])?;
249        if matches!(self, Self::Length(_)) && value < 0.0 {
250            return Err(invalid(
251                type_name,
252                attribute,
253                format!("expected a non-negative length, got {value}"),
254            ));
255        }
256        Ok(Value::Typed {
257            type_name: measure.into(),
258            value: Box::new(Value::Real(value)),
259        })
260    }
261}
262
263/// Stage an `IfcCurveSegment`.
264///
265/// The alignment-era segment: it carries its own placement and states
266/// where along the parent curve it starts and how far it runs, rather
267/// than relying on the parent's parameterisation alone.
268///
269/// # Errors
270///
271/// Refuses a non-finite measure, or a negative length measure.
272pub fn curve_segment(
273    tx: &mut Transaction,
274    transition: TransitionCode,
275    placement: EntityId,
276    segment_start: CurveMeasure,
277    segment_length: CurveMeasure,
278    parent_curve: EntityId,
279) -> Result<EntityId, GeometryError> {
280    const T: &str = "IFCCURVESEGMENT";
281    let attrs = vec![
282        Value::Enum(transition.token().into()),
283        Value::Ref(placement),
284        segment_start.to_value(T, "SegmentStart")?,
285        segment_length.to_value(T, "SegmentLength")?,
286        Value::Ref(parent_curve),
287    ];
288    Ok(tx.create(Entity::new(T, attrs)))
289}
290
291/// Stage an `IfcGradientCurve`.
292///
293/// A composite curve carrying a vertical profile over a horizontal
294/// `base_curve` -- the alignment case where gradient is layered on
295/// plan geometry.
296///
297/// # Errors
298///
299/// Refuses an empty segment list: `LIST [1:?]`.
300pub fn gradient_curve(
301    tx: &mut Transaction,
302    segments: &[EntityId],
303    base_curve: EntityId,
304    end_point: Option<EntityId>,
305) -> Result<EntityId, GeometryError> {
306    const T: &str = "IFCGRADIENTCURVE";
307    if segments.is_empty() {
308        return Err(invalid(T, "Segments", "expected at least one segment"));
309    }
310    let attrs = vec![
311        refs(segments),
312        Value::LogicalUnknown,
313        Value::Ref(base_curve),
314        end_point.map_or(Value::Null, Value::Ref),
315    ];
316    Ok(tx.create(Entity::new(T, attrs)))
317}