Skip to main content

ifc_geometry/authoring/
surface.rs

1//! Surfaces: the analytic and bounded forms.
2//!
3//! Two things here are worth more than slot filling.
4//!
5//! # A B-spline surface has two knot vectors
6//!
7//! The identity from the curve writers applies once per direction:
8//!
9//! ```text
10//! sum(UMultiplicities) = UDegree + rows + 1
11//! sum(VMultiplicities) = VDegree + columns + 1
12//! ```
13//!
14//! and the control grid must be rectangular, because
15//! `LIST OF LIST OF IfcCartesianPoint` does not itself say the inner
16//! lists agree in length. A ragged grid parses and denotes nothing.
17//!
18//! # A rectangular trim states its own direction twice
19//!
20//! `IfcRectangularTrimmedSurface` carries `Usense`/`Vsense` *and* the
21//! parameters they describe, and the schema requires they agree:
22//! `Vsense = (V2 > V1)`. Rather than take a sense from the caller and
23//! hope, this writer derives both from the parameters -- the one value
24//! that cannot then contradict the other.
25
26use ifc_model::{Entity, EntityId, Transaction, Value};
27
28use crate::error::GeometryError;
29use crate::surface::bounded::{plane_slot, surface_slot, trimmed_slot};
30use crate::surface::bspline::slot as bspline_slot;
31use crate::surface::elementary::slot as elementary_slot;
32
33use super::std_profile::positive;
34use super::{invalid, refs, require_finite};
35
36/// Stage an `IfcSphericalSurface`.
37///
38/// # Errors
39///
40/// Refuses a non-positive or non-finite radius.
41pub fn spherical_surface(
42    tx: &mut Transaction,
43    position: EntityId,
44    radius: f64,
45) -> Result<EntityId, GeometryError> {
46    const T: &str = "IFCSPHERICALSURFACE";
47    positive(T, "Radius", radius)?;
48    let mut attrs = vec![Value::Null; 2];
49    attrs[elementary_slot::POSITION] = Value::Ref(position);
50    attrs[elementary_slot::RADIUS] = Value::Real(radius);
51    Ok(tx.create(Entity::new(T, attrs)))
52}
53
54/// Stage an `IfcToroidalSurface`.
55///
56/// `major_radius` runs from the axis to the centre of the tube;
57/// `minor_radius` is the tube itself. The schema types both as positive
58/// lengths and does not relate them: a minor radius exceeding the major
59/// one gives a self-intersecting spindle torus, which is unusual but
60/// legal, so it is not refused here.
61///
62/// # Errors
63///
64/// Refuses a non-positive or non-finite radius.
65pub fn toroidal_surface(
66    tx: &mut Transaction,
67    position: EntityId,
68    major_radius: f64,
69    minor_radius: f64,
70) -> Result<EntityId, GeometryError> {
71    const T: &str = "IFCTOROIDALSURFACE";
72    positive(T, "MajorRadius", major_radius)?;
73    positive(T, "MinorRadius", minor_radius)?;
74    let mut attrs = vec![Value::Null; 3];
75    attrs[elementary_slot::POSITION] = Value::Ref(position);
76    attrs[elementary_slot::MAJOR_RADIUS] = Value::Real(major_radius);
77    attrs[elementary_slot::MINOR_RADIUS] = Value::Real(minor_radius);
78    Ok(tx.create(Entity::new(T, attrs)))
79}
80
81/// Stage an `IfcCurveBoundedPlane`: a plane cut to an outline.
82///
83/// `inner_boundaries` is `SET [0:?]`, so a plane with no holes is
84/// written with an empty set rather than `$`.
85pub fn curve_bounded_plane(
86    tx: &mut Transaction,
87    basis: EntityId,
88    outer_boundary: EntityId,
89    inner_boundaries: &[EntityId],
90) -> EntityId {
91    let mut attrs = vec![Value::Null; 3];
92    attrs[plane_slot::BASIS_SURFACE] = Value::Ref(basis);
93    attrs[plane_slot::OUTER_BOUNDARY] = Value::Ref(outer_boundary);
94    // SET [0:?]: empty is a legal value and means "no holes", which is
95    // not the same claim as `$` ("not stated").
96    attrs[plane_slot::INNER_BOUNDARIES] = refs(inner_boundaries);
97    tx.create(Entity::new("IFCCURVEBOUNDEDPLANE", attrs))
98}
99
100/// Stage an `IfcCurveBoundedSurface`.
101///
102/// `implicit_outer` says the surface's own extent bounds it, in which
103/// case `boundaries` carries only the holes.
104///
105/// # Errors
106///
107/// Refuses an empty boundary set: `SET [1:?]`.
108pub fn curve_bounded_surface(
109    tx: &mut Transaction,
110    basis: EntityId,
111    boundaries: &[EntityId],
112    implicit_outer: bool,
113) -> Result<EntityId, GeometryError> {
114    const T: &str = "IFCCURVEBOUNDEDSURFACE";
115    if boundaries.is_empty() {
116        return Err(invalid(T, "Boundaries", "expected at least one boundary"));
117    }
118    let mut attrs = vec![Value::Null; 3];
119    attrs[surface_slot::BASIS_SURFACE] = Value::Ref(basis);
120    attrs[surface_slot::BOUNDARIES] = refs(boundaries);
121    attrs[surface_slot::IMPLICIT_OUTER] = Value::Bool(implicit_outer);
122    Ok(tx.create(Entity::new(T, attrs)))
123}
124
125/// Stage an `IfcRectangularTrimmedSurface`.
126///
127/// The senses are **derived from the parameters**, not taken from the
128/// caller: the schema requires `Vsense = (V2 > V1)` (and the same for u
129/// on most basis surfaces), so accepting a sense that could disagree
130/// would only create a way to write an invalid file.
131///
132/// # Errors
133///
134/// Refuses `u1 == u2` or `v1 == v2` -- a trim of zero extent in either
135/// direction -- or a non-finite parameter.
136pub fn rectangular_trimmed_surface(
137    tx: &mut Transaction,
138    basis: EntityId,
139    u: (f64, f64),
140    v: (f64, f64),
141) -> Result<EntityId, GeometryError> {
142    const T: &str = "IFCRECTANGULARTRIMMEDSURFACE";
143    let (u1, u2) = u;
144    let (v1, v2) = v;
145    require_finite(T, "U1", &[u1, u2])?;
146    require_finite(T, "V1", &[v1, v2])?;
147    if u1 == u2 {
148        return Err(invalid(T, "U1", "U1 and U2 must differ"));
149    }
150    if v1 == v2 {
151        return Err(invalid(T, "V1", "V1 and V2 must differ"));
152    }
153
154    let mut attrs = vec![Value::Null; 7];
155    attrs[trimmed_slot::BASIS_SURFACE] = Value::Ref(basis);
156    attrs[trimmed_slot::U1] = parameter(u1);
157    attrs[trimmed_slot::V1] = parameter(v1);
158    attrs[trimmed_slot::U2] = parameter(u2);
159    attrs[trimmed_slot::V2] = parameter(v2);
160    // UsenseCompatible / VsenseCompatible: the sense is a restatement of
161    // the parameter order, so derive it and it cannot contradict.
162    attrs[trimmed_slot::USENSE] = Value::Bool(u2 > u1);
163    attrs[trimmed_slot::VSENSE] = Value::Bool(v2 > v1);
164    Ok(tx.create(Entity::new(T, attrs)))
165}
166
167/// An `IfcParameterValue` in `U1`/`V1`/`U2`/`V2`.
168///
169/// Those are declared with the defined type `IfcParameterValue`, not a
170/// SELECT, in IFC2X3, IFC4 and IFC4X3, so the value is written bare (#200).
171fn parameter(value: f64) -> Value {
172    Value::Real(value)
173}
174
175/// A knot vector along one surface direction.
176#[derive(Debug, Clone, Copy)]
177pub struct SurfaceKnots<'a> {
178    /// How many times each distinct knot repeats.
179    pub multiplicities: &'a [i64],
180    /// The distinct knot values, strictly increasing.
181    pub knots: &'a [f64],
182}
183
184/// Everything a B-spline surface states apart from its control grid.
185///
186/// Grouped because the u and v data must be kept straight: four bare
187/// arguments of two types invite a transposition that still compiles.
188#[derive(Debug, Clone, Copy)]
189pub struct SurfaceBasis<'a> {
190    /// `UDegree` and `VDegree`, in that order.
191    pub degree: (i64, i64),
192    /// The u knot vector, checked against the grid's row count.
193    pub u: SurfaceKnots<'a>,
194    /// The v knot vector, checked against the grid's column count.
195    pub v: SurfaceKnots<'a>,
196    /// `SurfaceForm`, informational.
197    pub form: &'a str,
198    /// `KnotSpec`, e.g. `UNSPECIFIED`.
199    pub knot_spec: &'a str,
200}
201
202/// Stage an `IfcBSplineSurfaceWithKnots`.
203///
204/// `control_points` is indexed `[u][v]`: the outer list runs along u,
205/// matching `ControlPointsList` in the schema.
206///
207/// # Errors
208///
209/// Refuses a degree below one in either direction, a control grid with
210/// fewer than two rows or columns, a **ragged** grid, non-increasing or
211/// non-positive knot data, or either knot identity broken:
212/// `sum(UMultiplicities) = UDegree + rows + 1`, and likewise for v.
213pub fn bspline_surface_with_knots(
214    tx: &mut Transaction,
215    control_points: &[&[EntityId]],
216    basis: SurfaceBasis<'_>,
217) -> Result<EntityId, GeometryError> {
218    const T: &str = "IFCBSPLINESURFACEWITHKNOTS";
219    let attrs = surface_attrs(T, control_points, basis)?;
220    Ok(tx.create(Entity::new(T, attrs)))
221}
222
223/// Stage an `IfcRationalBSplineSurfaceWithKnots`.
224///
225/// # Errors
226///
227/// Everything [`bspline_surface_with_knots`] refuses, plus a weight
228/// grid whose shape does not match the control grid exactly.
229pub fn rational_bspline_surface_with_knots(
230    tx: &mut Transaction,
231    control_points: &[&[EntityId]],
232    basis: SurfaceBasis<'_>,
233    weights: &[&[f64]],
234) -> Result<EntityId, GeometryError> {
235    const T: &str = "IFCRATIONALBSPLINESURFACEWITHKNOTS";
236    if weights.len() != control_points.len() {
237        return Err(invalid(
238            T,
239            "WeightsData",
240            format!(
241                "{} weight rows for {} control point rows",
242                weights.len(),
243                control_points.len()
244            ),
245        ));
246    }
247    for (index, (row, points)) in weights.iter().zip(control_points).enumerate() {
248        if row.len() != points.len() {
249            return Err(invalid(
250                T,
251                "WeightsData",
252                format!(
253                    "weight row {index} has {} entries, its control row has {}",
254                    row.len(),
255                    points.len()
256                ),
257            ));
258        }
259        require_finite(T, "WeightsData", row)?;
260    }
261    let mut attrs = surface_attrs(T, control_points, basis)?;
262    attrs.push(Value::List(
263        weights
264            .iter()
265            .map(|row| Value::List(row.iter().copied().map(Value::Real).collect()))
266            .collect(),
267    ));
268    Ok(tx.create(Entity::new(T, attrs)))
269}
270
271/// Build the twelve shared slots, enforcing both knot identities.
272fn surface_attrs(
273    type_name: &'static str,
274    control_points: &[&[EntityId]],
275    basis: SurfaceBasis<'_>,
276) -> Result<Vec<Value>, GeometryError> {
277    let SurfaceBasis {
278        degree: (u_degree, v_degree),
279        u,
280        v,
281        form,
282        knot_spec,
283    } = basis;
284    let rows = control_points.len();
285    if rows < 2 {
286        return Err(invalid(
287            type_name,
288            "ControlPointsList",
289            format!("expected at least 2 rows, got {rows}"),
290        ));
291    }
292    let columns = control_points[0].len();
293    if columns < 2 {
294        return Err(invalid(
295            type_name,
296            "ControlPointsList",
297            format!("expected at least 2 columns, got {columns}"),
298        ));
299    }
300    // LIST OF LIST does not constrain the inner lengths, so a ragged
301    // grid is expressible and meaningless. Catch it here.
302    if let Some(bad) = control_points.iter().position(|row| row.len() != columns) {
303        return Err(invalid(
304            type_name,
305            "ControlPointsList",
306            format!(
307                "row {bad} has {} control points, row 0 has {columns}; the grid must be rectangular",
308                control_points[bad].len()
309            ),
310        ));
311    }
312
313    check_knots(type_name, "U", u_degree, rows, u)?;
314    check_knots(type_name, "V", v_degree, columns, v)?;
315
316    let mut attrs = vec![Value::Null; 12];
317    attrs[bspline_slot::U_DEGREE] = Value::Integer(u_degree);
318    attrs[bspline_slot::V_DEGREE] = Value::Integer(v_degree);
319    attrs[bspline_slot::CONTROL_POINTS] =
320        Value::List(control_points.iter().map(|row| refs(row)).collect());
321    attrs[bspline_slot::SURFACE_FORM] = Value::Enum(form.into());
322    // Closure and self-intersection need an evaluator to decide.
323    attrs[bspline_slot::U_CLOSED] = Value::LogicalUnknown;
324    attrs[bspline_slot::V_CLOSED] = Value::LogicalUnknown;
325    attrs[bspline_slot::SELF_INTERSECT] = Value::LogicalUnknown;
326    attrs[bspline_slot::U_MULTIPLICITIES] = integers(u.multiplicities);
327    attrs[bspline_slot::V_MULTIPLICITIES] = integers(v.multiplicities);
328    attrs[bspline_slot::U_KNOTS] = reals(u.knots);
329    attrs[bspline_slot::V_KNOTS] = reals(v.knots);
330    attrs[bspline_slot::KNOT_SPEC] = Value::Enum(knot_spec.into());
331    Ok(attrs)
332}
333
334/// One direction's knot vector, against its own degree and extent.
335fn check_knots(
336    type_name: &'static str,
337    axis: &'static str,
338    degree: i64,
339    extent: usize,
340    knots: SurfaceKnots<'_>,
341) -> Result<(), GeometryError> {
342    if degree < 1 {
343        return Err(invalid(
344            type_name,
345            "Degree",
346            format!("{axis}Degree must be at least 1, got {degree}"),
347        ));
348    }
349    if knots.multiplicities.len() != knots.knots.len() {
350        return Err(invalid(
351            type_name,
352            "Multiplicities",
353            format!(
354                "{axis}: {} multiplicities for {} knots",
355                knots.multiplicities.len(),
356                knots.knots.len()
357            ),
358        ));
359    }
360    if knots.knots.len() < 2 {
361        return Err(invalid(
362            type_name,
363            "Knots",
364            format!("{axis}: expected at least 2 distinct knots"),
365        ));
366    }
367    require_finite(type_name, "Knots", knots.knots)?;
368    if knots.multiplicities.iter().any(|m| *m < 1) {
369        return Err(invalid(
370            type_name,
371            "Multiplicities",
372            format!("{axis}: a multiplicity is not positive"),
373        ));
374    }
375    if knots.knots.windows(2).any(|w| w[1] <= w[0]) {
376        return Err(invalid(
377            type_name,
378            "Knots",
379            format!("{axis}: knots must strictly increase"),
380        ));
381    }
382    let total: i64 = knots.multiplicities.iter().sum();
383    let expected = degree + extent as i64 + 1;
384    if total != expected {
385        return Err(invalid(
386            type_name,
387            "Multiplicities",
388            format!(
389                "{axis}: multiplicities sum to {total}, but degree {degree} over {extent} control points requires {expected}"
390            ),
391        ));
392    }
393    Ok(())
394}
395
396/// A list of integers.
397fn integers(values: &[i64]) -> Value {
398    Value::List(values.iter().copied().map(Value::Integer).collect())
399}
400
401/// A list of reals.
402fn reals(values: &[f64]) -> Value {
403    Value::List(values.iter().copied().map(Value::Real).collect())
404}