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. `ParamLength` is written bare: it is declared
192/// `IfcParameterValue`, a defined type.
193///
194/// # Errors
195///
196/// Refuses a non-positive or non-finite parameter length.
197pub fn reparametrised_composite_curve_segment(
198    tx: &mut Transaction,
199    transition: TransitionCode,
200    same_sense: bool,
201    parent_curve: EntityId,
202    param_length: f64,
203) -> Result<EntityId, GeometryError> {
204    const T: &str = "IFCREPARAMETRISEDCOMPOSITECURVESEGMENT";
205    require_finite(T, "ParamLength", &[param_length])?;
206    if param_length <= 0.0 {
207        return Err(invalid(
208            T,
209            "ParamLength",
210            format!("expected a positive parameter length, got {param_length}"),
211        ));
212    }
213    let mut attrs = vec![Value::Null; 4];
214    attrs[segment_slot::TRANSITION] = Value::Enum(transition.token().into());
215    attrs[segment_slot::SAME_SENSE] = Value::Bool(same_sense);
216    attrs[segment_slot::PARENT_CURVE] = Value::Ref(parent_curve);
217    // `ParamLength : IfcParameterValue`, a defined type in IFC4 and IFC4X3
218    // (IFC2X3 has no such entity): bare, not a typed parameter (#200).
219    attrs[segment_slot::PARAM_LENGTH] = Value::Real(param_length);
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///
230/// IFC4X3 ADD2 declares the SELECT's members exactly:
231///
232/// ```text
233/// TYPE IfcCurveMeasureSelect = SELECT (IfcLengthMeasure, IfcParameterValue);
234/// ```
235///
236/// so a length is written `IFCLENGTHMEASURE(..)` and a parameter
237/// `IFCPARAMETERVALUE(..)`. A specialisation of a member, such as
238/// `IfcNonNegativeLengthMeasure`, is not itself a member and is never
239/// written (#210).
240#[derive(Debug, Clone, Copy)]
241pub enum CurveMeasure {
242    /// `IfcLengthMeasure`: a signed distance along the curve.
243    Length(f64),
244    /// `IfcParameterValue`: a value in the curve's parameter space.
245    Parameter(f64),
246}
247
248impl CurveMeasure {
249    /// Encode with the `IfcCurveMeasureSelect` member that says which kind
250    /// this is.
251    fn to_value(
252        self,
253        type_name: &'static str,
254        attribute: &'static str,
255    ) -> Result<Value, GeometryError> {
256        let (measure, value) = match self {
257            Self::Length(v) => ("IFCLENGTHMEASURE", v),
258            Self::Parameter(v) => ("IFCPARAMETERVALUE", v),
259        };
260        require_finite(type_name, attribute, &[value])?;
261        Ok(Value::Typed {
262            type_name: measure.into(),
263            value: Box::new(Value::Real(value)),
264        })
265    }
266}
267
268/// Stage an `IfcCurveSegment`.
269///
270/// The alignment-era segment: it carries its own placement and states
271/// where along the parent curve it starts and how far it runs, rather
272/// than relying on the parent's parameterisation alone.
273///
274/// Each measure is written with the `IfcCurveMeasureSelect` member that
275/// names its kind (see [`CurveMeasure`]). `IfcLengthMeasure` is a signed
276/// `REAL` and the schema bounds neither slot, so a negative length is
277/// written as given rather than refused.
278///
279/// # Errors
280///
281/// Refuses a non-finite measure.
282pub fn curve_segment(
283    tx: &mut Transaction,
284    transition: TransitionCode,
285    placement: EntityId,
286    segment_start: CurveMeasure,
287    segment_length: CurveMeasure,
288    parent_curve: EntityId,
289) -> Result<EntityId, GeometryError> {
290    const T: &str = "IFCCURVESEGMENT";
291    let attrs = vec![
292        Value::Enum(transition.token().into()),
293        Value::Ref(placement),
294        segment_start.to_value(T, "SegmentStart")?,
295        segment_length.to_value(T, "SegmentLength")?,
296        Value::Ref(parent_curve),
297    ];
298    Ok(tx.create(Entity::new(T, attrs)))
299}
300
301/// Stage an `IfcGradientCurve`.
302///
303/// A composite curve carrying a vertical profile over a horizontal
304/// `base_curve` -- the alignment case where gradient is layered on
305/// plan geometry.
306///
307/// # Errors
308///
309/// Refuses an empty segment list: `LIST [1:?]`.
310pub fn gradient_curve(
311    tx: &mut Transaction,
312    segments: &[EntityId],
313    base_curve: EntityId,
314    end_point: Option<EntityId>,
315) -> Result<EntityId, GeometryError> {
316    const T: &str = "IFCGRADIENTCURVE";
317    if segments.is_empty() {
318        return Err(invalid(T, "Segments", "expected at least one segment"));
319    }
320    let attrs = vec![
321        refs(segments),
322        Value::LogicalUnknown,
323        Value::Ref(base_curve),
324        end_point.map_or(Value::Null, Value::Ref),
325    ];
326    Ok(tx.create(Entity::new(T, attrs)))
327}