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}