Skip to main content

ifc_geometry/surface/
bspline.rs

1//! `IfcBSplineSurface` and its knotted / rational refinements.
2//!
3//! # The same job as [`crate::curve::bspline`], one dimension up
4//!
5//! Read the parameters, check the invariants, evaluate nothing. What changes
6//! in 2D is that every invariant now has a u form and a v form, and the
7//! control points are a *grid* rather than a list. The grid is where files go
8//! wrong: `ControlPointsList` is `LIST OF LIST OF IfcCartesianPoint`, and a
9//! ragged inner list means the surface is not a tensor product at all.
10//!
11//! # Which index is which
12//!
13//! `ControlPointsList[i][j]` has `i` running along **u** and `j` along **v**.
14//! So the outer list length is the u control point count and the inner length
15//! the v count. Transposing them yields a surface that is plausible, is not
16//! the one in the file, and passes every count check because the two knot
17//! vectors are usually the same length in test data. This module's accessors
18//! are named `u_*` and `v_*` for exactly that reason, and the row/column
19//! convention is asserted in the tests.
20//!
21//! # Invariants checked here
22//!
23//! - Control point grid is rectangular (no ragged rows).
24//! - `sum(UMultiplicities) = u control points + UDegree + 1`, and the v form.
25//! - `UKnots` / `UMultiplicities` are parallel lists, and the v form.
26//! - Weights form a grid of the same shape as the control points, all positive.
27
28use crate::curve::bspline::{KnotType, KnotVector};
29use crate::error::GeometryResult;
30use crate::resource::point::CartesianPoint;
31use crate::resource::resolve;
32use crate::slots::Slots;
33use ifc_model::{Entity, EntityId, Model, Value};
34
35/// `IfcBSplineSurface` family attribute slots.
36///
37/// From IFC4 ADD2 TC1. Slots 0-6 come from `IfcBSplineSurface`, 7-11 from
38/// `IfcBSplineSurfaceWithKnots`, and 12 from
39/// `IfcRationalBSplineSurfaceWithKnots`.
40pub(crate) mod slot {
41    /// `UDegree`: `IfcInteger`.
42    pub const U_DEGREE: usize = 0;
43    /// `VDegree`: `IfcInteger`.
44    pub const V_DEGREE: usize = 1;
45    /// `ControlPointsList`: `LIST OF LIST OF IfcCartesianPoint`, u outer.
46    pub const CONTROL_POINTS: usize = 2;
47    /// `SurfaceForm`: `IfcBSplineSurfaceForm`.
48    pub const SURFACE_FORM: usize = 3;
49    /// `UClosed`: `IfcLogical`.
50    pub const U_CLOSED: usize = 4;
51    /// `VClosed`: `IfcLogical`.
52    pub const V_CLOSED: usize = 5;
53    /// `SelfIntersect`: `IfcLogical`.
54    pub const SELF_INTERSECT: usize = 6;
55    /// `UMultiplicities`, from `IfcBSplineSurfaceWithKnots`.
56    pub const U_MULTIPLICITIES: usize = 7;
57    /// `VMultiplicities`, from `IfcBSplineSurfaceWithKnots`.
58    pub const V_MULTIPLICITIES: usize = 8;
59    /// `UKnots`, from `IfcBSplineSurfaceWithKnots`.
60    pub const U_KNOTS: usize = 9;
61    /// `VKnots`, from `IfcBSplineSurfaceWithKnots`.
62    pub const V_KNOTS: usize = 10;
63    /// `KnotSpec`: `IfcKnotType`, from `IfcBSplineSurfaceWithKnots`.
64    pub const KNOT_SPEC: usize = 11;
65    /// `WeightsData`, from `IfcRationalBSplineSurfaceWithKnots`.
66    pub const WEIGHTS_DATA: usize = 12;
67}
68
69/// `IfcBSplineSurfaceForm`: what shape the surface originally was.
70///
71/// Informational only, exactly like [`crate::curve::BSplineCurveForm`]. A
72/// `CYLINDRICAL_SURF` form is not a licence to substitute an
73/// `IfcCylindricalSurface`: the control points are what the file actually
74/// means.
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum BSplineSurfaceForm {
77    /// Originally a plane.
78    PlaneSurf,
79    /// Originally a cylinder.
80    CylindricalSurf,
81    /// Originally a cone.
82    ConicalSurf,
83    /// Originally a sphere.
84    SphericalSurf,
85    /// Originally a torus.
86    ToroidalSurf,
87    /// Originally a surface of revolution.
88    SurfOfRevolution,
89    /// Originally a ruled surface.
90    RuledSurf,
91    /// Originally a generalised cone.
92    GeneralisedCone,
93    /// Originally a quadric.
94    QuadricSurf,
95    /// Originally a linear extrusion.
96    SurfOfLinearExtrusion,
97    /// No original form is claimed.
98    Unspecified,
99}
100
101impl BSplineSurfaceForm {
102    /// Parse the enumeration token, `None` if unrecognised.
103    pub fn from_token(token: &str) -> Option<Self> {
104        match token.to_ascii_uppercase().as_str() {
105            "PLANE_SURF" => Some(Self::PlaneSurf),
106            "CYLINDRICAL_SURF" => Some(Self::CylindricalSurf),
107            "CONICAL_SURF" => Some(Self::ConicalSurf),
108            "SPHERICAL_SURF" => Some(Self::SphericalSurf),
109            "TOROIDAL_SURF" => Some(Self::ToroidalSurf),
110            "SURF_OF_REVOLUTION" => Some(Self::SurfOfRevolution),
111            "RULED_SURF" => Some(Self::RuledSurf),
112            "GENERALISED_CONE" => Some(Self::GeneralisedCone),
113            "QUADRIC_SURF" => Some(Self::QuadricSurf),
114            "SURF_OF_LINEAR_EXTRUSION" => Some(Self::SurfOfLinearExtrusion),
115            "UNSPECIFIED" => Some(Self::Unspecified),
116            _ => None,
117        }
118    }
119}
120
121/// The control point grid, `[u][v]`.
122///
123/// A distinct type rather than a bare `Vec<Vec<EntityId>>` so the u/v
124/// convention is stated once and the rectangularity check cannot be skipped.
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct ControlPointGrid {
127    rows: Vec<Vec<EntityId>>,
128}
129
130impl ControlPointGrid {
131    /// Number of control points along u; the outer list length.
132    pub fn u_count(&self) -> usize {
133        self.rows.len()
134    }
135
136    /// Number of control points along v; the inner list length.
137    pub fn v_count(&self) -> usize {
138        self.rows.first().map_or(0, Vec::len)
139    }
140
141    /// The control point at `(u_index, v_index)`.
142    pub fn get(&self, u_index: usize, v_index: usize) -> Option<EntityId> {
143        self.rows.get(u_index)?.get(v_index).copied()
144    }
145
146    /// The rows, each a constant-u run of control points.
147    pub fn rows(&self) -> &[Vec<EntityId>] {
148        &self.rows
149    }
150}
151
152/// A borrowed view of any `IfcBSplineSurface` subtype.
153#[derive(Debug, Clone, Copy)]
154pub struct BSplineSurface<'m> {
155    slots: Slots<'m>,
156}
157
158impl<'m> BSplineSurface<'m> {
159    /// Wrap an entity known to be an `IfcBSplineSurface` subtype.
160    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
161        Self {
162            slots: Slots::new(id, entity),
163        }
164    }
165
166    /// The entity id.
167    pub fn id(&self) -> EntityId {
168        self.slots.id()
169    }
170
171    /// The u polynomial degree, guaranteed to fit the u control count.
172    pub fn u_degree(&self) -> GeometryResult<usize> {
173        self.degree(slot::U_DEGREE, "UDegree", self.control_points()?.u_count())
174    }
175
176    /// The v polynomial degree, guaranteed to fit the v control count.
177    pub fn v_degree(&self) -> GeometryResult<usize> {
178        self.degree(slot::V_DEGREE, "VDegree", self.control_points()?.v_count())
179    }
180
181    /// The control point grid, checked to be rectangular.
182    ///
183    /// A ragged grid is not a tensor-product surface, so it cannot be
184    /// evaluated at all; catching it here beats an out-of-bounds read in a
185    /// kernel's inner loop. [`Self::control_point_views`] resolves the ids.
186    pub fn control_points(&self) -> GeometryResult<ControlPointGrid> {
187        let value = self.slots.req(slot::CONTROL_POINTS, "ControlPointsList")?;
188        let outer = value.as_list().ok_or_else(|| {
189            self.slots
190                .degenerate("ControlPointsList must be a list of lists")
191        })?;
192        if outer.len() < 2 {
193            return Err(self.slots.degenerate(format!(
194                "ControlPointsList needs at least 2 rows along u, found {}",
195                outer.len()
196            )));
197        }
198
199        let mut rows: Vec<Vec<EntityId>> = Vec::with_capacity(outer.len());
200        for (u_index, row_value) in outer.iter().enumerate() {
201            let inner = row_value.as_list().ok_or_else(|| {
202                self.slots
203                    .degenerate(format!("ControlPointsList row {u_index} is not a list"))
204            })?;
205            let mut row = Vec::with_capacity(inner.len());
206            for (v_index, point) in inner.iter().enumerate() {
207                let id = point.as_ref_id().ok_or_else(|| {
208                    self.slots.degenerate(format!(
209                        "ControlPointsList[{u_index}][{v_index}] is not an entity reference"
210                    ))
211                })?;
212                row.push(id);
213            }
214            rows.push(row);
215        }
216
217        let v_count = rows[0].len();
218        if v_count < 2 {
219            return Err(self.slots.degenerate(format!(
220                "ControlPointsList needs at least 2 columns along v, found {v_count}"
221            )));
222        }
223        for (u_index, row) in rows.iter().enumerate() {
224            if row.len() != v_count {
225                return Err(self.slots.degenerate(format!(
226                    "ControlPointsList row {u_index} has {} points but row 0 has {v_count}; \
227                     the grid must be rectangular",
228                    row.len()
229                )));
230            }
231        }
232        Ok(ControlPointGrid { rows })
233    }
234
235    /// The rectangular grid resolved to typed point views, `[u][v]`.
236    ///
237    /// Same shape and u/v convention as [`Self::control_points`]. One bad
238    /// reference fails the whole grid: a hole in a tensor-product net has
239    /// no meaning, so there is no partial result.
240    pub fn control_point_views<'v>(
241        &self,
242        model: &'v Model,
243    ) -> GeometryResult<Vec<Vec<CartesianPoint<'v>>>> {
244        self.control_points()?
245            .rows()
246            .iter()
247            .map(|row| resolve::cartesian_points(model, self.id(), row))
248            .collect()
249    }
250
251    /// The declared original form, defaulting to `Unspecified`.
252    pub fn surface_form(&self) -> BSplineSurfaceForm {
253        self.slots
254            .opt_enum(slot::SURFACE_FORM)
255            .and_then(BSplineSurfaceForm::from_token)
256            .unwrap_or(BSplineSurfaceForm::Unspecified)
257    }
258
259    /// The asserted `UClosed` flag; `None` for `.U.` or absent.
260    pub fn u_closed(&self) -> Option<bool> {
261        self.slots.opt_bool(slot::U_CLOSED)
262    }
263
264    /// The asserted `VClosed` flag; `None` for `.U.` or absent.
265    pub fn v_closed(&self) -> Option<bool> {
266        self.slots.opt_bool(slot::V_CLOSED)
267    }
268
269    /// The asserted `SelfIntersect` flag; `None` for `.U.` or absent.
270    pub fn self_intersect(&self) -> Option<bool> {
271        self.slots.opt_bool(slot::SELF_INTERSECT)
272    }
273
274    /// The declared knot type, defaulting to `Unspecified`.
275    pub fn knot_spec(&self) -> KnotType {
276        self.slots
277            .opt_enum(slot::KNOT_SPEC)
278            .and_then(KnotType::from_token)
279            .unwrap_or(KnotType::Unspecified)
280    }
281
282    /// Is this the knotted subtype?
283    pub fn has_knots(&self) -> bool {
284        self.slots.opt(slot::U_KNOTS).is_some()
285    }
286
287    /// Whether the entity declares the rational subtype.
288    pub fn is_rational(&self) -> bool {
289        self.slots
290            .type_name()
291            .eq_ignore_ascii_case("IFCRATIONALBSPLINESURFACEWITHKNOTS")
292    }
293
294    /// The u knot vector, checked against the u control point count.
295    ///
296    /// `Ok(None)` for a surface without knots.
297    pub fn u_knots(&self) -> GeometryResult<Option<KnotVector>> {
298        if !self.has_knots() {
299            return Ok(None);
300        }
301        let expected = self
302            .control_points()?
303            .u_count()
304            .checked_add(self.u_degree()?)
305            .and_then(|value| value.checked_add(1))
306            .ok_or_else(|| self.slots.degenerate("u knot count overflows usize"))?;
307        self.knot_vector(
308            slot::U_KNOTS,
309            "UKnots",
310            slot::U_MULTIPLICITIES,
311            "UMultiplicities",
312            expected,
313        )
314        .map(Some)
315    }
316
317    /// The v knot vector, checked against the v control point count.
318    ///
319    /// `Ok(None)` for a surface without knots.
320    pub fn v_knots(&self) -> GeometryResult<Option<KnotVector>> {
321        if !self.has_knots() {
322            return Ok(None);
323        }
324        let expected = self
325            .control_points()?
326            .v_count()
327            .checked_add(self.v_degree()?)
328            .and_then(|value| value.checked_add(1))
329            .ok_or_else(|| self.slots.degenerate("v knot count overflows usize"))?;
330        self.knot_vector(
331            slot::V_KNOTS,
332            "VKnots",
333            slot::V_MULTIPLICITIES,
334            "VMultiplicities",
335            expected,
336        )
337        .map(Some)
338    }
339
340    /// The weight grid, matching the control point grid exactly.
341    ///
342    /// `Ok(None)` for a non-rational surface. Every weight must be finite and positive:
343    /// a zero divides by zero at that control point and a negative one flips
344    /// the patch through infinity.
345    pub fn weights(&self) -> GeometryResult<Option<Vec<Vec<f64>>>> {
346        let supplied = self.slots.opt(slot::WEIGHTS_DATA).is_some();
347        match (self.is_rational(), supplied) {
348            (false, false) => return Ok(None),
349            (true, false) => {
350                return Err(self
351                    .slots
352                    .degenerate("rational B-spline surface is missing WeightsData"));
353            }
354            (false, true) => {
355                return Err(self
356                    .slots
357                    .degenerate("polynomial B-spline surface must not carry WeightsData"));
358            }
359            (true, true) => {}
360        }
361        let grid = self.control_points()?;
362        let value = self.slots.req(slot::WEIGHTS_DATA, "WeightsData")?;
363        let outer = value
364            .as_list()
365            .ok_or_else(|| self.slots.degenerate("WeightsData must be a list of lists"))?;
366
367        if outer.len() != grid.u_count() {
368            return Err(self.slots.degenerate(format!(
369                "WeightsData has {} rows but the control point grid has {}",
370                outer.len(),
371                grid.u_count()
372            )));
373        }
374
375        let mut weights = Vec::with_capacity(outer.len());
376        for (u_index, row_value) in outer.iter().enumerate() {
377            let inner = row_value.as_list().ok_or_else(|| {
378                self.slots
379                    .degenerate(format!("WeightsData row {u_index} is not a list"))
380            })?;
381            if inner.len() != grid.v_count() {
382                return Err(self.slots.degenerate(format!(
383                    "WeightsData row {u_index} has {} entries but the grid has {}",
384                    inner.len(),
385                    grid.v_count()
386                )));
387            }
388            let mut row = Vec::with_capacity(inner.len());
389            for (v_index, w) in inner.iter().enumerate() {
390                let weight = w.unwrap_typed().as_f64().ok_or_else(|| {
391                    self.slots
392                        .degenerate(format!("WeightsData[{u_index}][{v_index}] is not a number"))
393                })?;
394                if !weight.is_finite() {
395                    return Err(self.slots.degenerate(format!(
396                        "weight {weight} at control point [{u_index}][{v_index}] must be finite"
397                    )));
398                }
399                if weight <= 0.0 {
400                    return Err(self.slots.degenerate(format!(
401                        "weight {weight} at control point [{u_index}][{v_index}] must be positive"
402                    )));
403                }
404                row.push(weight);
405            }
406            weights.push(row);
407        }
408        Ok(Some(weights))
409    }
410
411    fn degree(
412        &self,
413        index: usize,
414        name: &'static str,
415        control_count: usize,
416    ) -> GeometryResult<usize> {
417        let raw = self.slots.req_i64(index, name)?;
418        if raw < 1 {
419            return Err(self
420                .slots
421                .degenerate(format!("{name} must be at least 1, found {raw}")));
422        }
423        let degree = usize::try_from(raw).map_err(|_| {
424            self.slots
425                .degenerate(format!("{name} exceeds platform limits"))
426        })?;
427        if degree >= control_count {
428            return Err(self.slots.degenerate(format!(
429                "{name} {degree} must be smaller than the {control_count} control points"
430            )));
431        }
432        Ok(degree)
433    }
434
435    /// Read one knot vector and check it against `expected` total multiplicity.
436    fn knot_vector(
437        &self,
438        knots_index: usize,
439        knots_name: &'static str,
440        mult_index: usize,
441        mult_name: &'static str,
442        expected: usize,
443    ) -> GeometryResult<KnotVector> {
444        let values = self.slots.req_f64_list(knots_index, knots_name)?;
445        let raw = self.slots.req(mult_index, mult_name)?;
446        let items = raw
447            .as_list()
448            .ok_or_else(|| self.slots.degenerate(format!("{mult_name} must be a list")))?;
449
450        let mut multiplicities = Vec::with_capacity(items.len());
451        for item in items {
452            match item.unwrap_typed() {
453                Value::Integer(i) if *i >= 1 => {
454                    multiplicities.push(usize::try_from(*i).map_err(|_| {
455                        self.slots
456                            .degenerate(format!("{mult_name} exceeds platform limits"))
457                    })?);
458                }
459                other => {
460                    return Err(self.slots.degenerate(format!(
461                        "{mult_name} entry must be a positive integer, found {other:?}"
462                    )));
463                }
464            }
465        }
466
467        if values.len() != multiplicities.len() {
468            return Err(self.slots.degenerate(format!(
469                "{knots_name} has {} entries but {mult_name} has {}; they are parallel lists",
470                values.len(),
471                multiplicities.len()
472            )));
473        }
474        for (index, value) in values.iter().enumerate() {
475            if !value.is_finite() {
476                return Err(self.slots.degenerate(format!(
477                    "{knots_name}[{index}] must be finite, found {value}"
478                )));
479            }
480        }
481        for pair in values.windows(2) {
482            if pair[1] <= pair[0] {
483                return Err(self.slots.degenerate(format!(
484                    "{knots_name} must be strictly increasing; found {} after {}",
485                    pair[1], pair[0]
486                )));
487            }
488        }
489        let total = multiplicities.iter().try_fold(0usize, |total, &value| {
490            total.checked_add(value).ok_or_else(|| {
491                self.slots
492                    .degenerate(format!("{mult_name} total overflows usize"))
493            })
494        })?;
495        if total != expected {
496            return Err(self.slots.degenerate(format!(
497                "{mult_name} sums to {total} but must equal control points + degree + 1 = {expected}"
498            )));
499        }
500        Ok(KnotVector {
501            values,
502            multiplicities,
503        })
504    }
505}
506
507/// Marker kept so the curve module's knot types are visibly reused.
508///
509/// The knot representation is identical in one and two dimensions, so
510/// duplicating [`KnotVector`] here would create two types that must be kept in
511/// sync for no benefit.
512pub type SurfaceKnotVector = KnotVector;
513
514#[cfg(test)]
515mod tests;