Skip to main content

ifc_geometry/lower/
dispatch.rs

1//! Total representation-item dispatch.
2//!
3//! # Why totality matters here
4//!
5//! `GeometryNode` is `#[non_exhaustive]` and the crate contract says an
6//! unknown family must become a typed `Unsupported` result, never a panic and
7//! never a silently substituted shape. This dispatcher is the single place
8//! that decides which IFC representation items are implemented, so coverage is
9//! auditable from one table instead of scattered across families.
10
11use axiolid_model::NodeId;
12use ifc_model::EntityId;
13
14use crate::error::GeometryResult;
15use crate::lower::bbox::lower_bounding_box_node;
16use crate::lower::boolean::lower_boolean_result_node;
17use crate::lower::brep::lower_faceted_brep_node;
18use crate::lower::collection::lower_collection_node;
19use crate::lower::csg::{
20    lower_csg_primitive_node, lower_csg_solid_node, lower_surface_curve_swept_area_solid_node,
21    lower_swept_disk_node,
22};
23use crate::lower::curve::lower_curve_node;
24use crate::lower::halfspace::lower_half_space_node;
25use crate::lower::mapped::lower_mapped_item_node;
26use crate::lower::point::{lower_point_on_curve_node, lower_point_on_surface_node};
27use crate::lower::session::LoweringSession;
28use crate::lower::surface::lower_surface_node;
29use crate::lower::swept::{
30    lower_extruded_area_solid_node, lower_fixed_reference_sweep_node,
31    lower_revolved_area_solid_node, lower_sectioned_spine_node, lower_tapered_extrusion_node,
32    lower_tapered_revolution_node,
33};
34use crate::lower::tessellated::{lower_polygonal_face_set_node, lower_triangulated_face_set_node};
35use crate::select::is_a;
36use crate::transform::Transform;
37
38/// Families this crate lowers today, paired with what is still missing.
39///
40/// Kept as data so the census test can assert on it rather than re-deriving
41/// the list by scraping source text.
42pub const IMPLEMENTED: &[&str] = &[
43    "IFCEXTRUDEDAREASOLID",
44    "IFCREVOLVEDAREASOLID",
45    "IFCBOOLEANRESULT",
46    "IFCBOOLEANCLIPPINGRESULT",
47    "IFCMAPPEDITEM",
48    "IFCFACETEDBREP",
49    "IFCFACETEDBREPWITHVOIDS",
50    "IFCADVANCEDBREP",
51    "IFCADVANCEDBREPWITHVOIDS",
52    "IFCHALFSPACESOLID",
53    "IFCBOXEDHALFSPACE",
54    "IFCPOLYGONALBOUNDEDHALFSPACE",
55    "IFCTRIANGULATEDFACESET",
56    "IFCPOLYGONALFACESET",
57    "IFCCSGSOLID",
58    "IFCSWEPTDISKSOLID",
59    "IFCSWEPTDISKSOLIDPOLYGONAL",
60    "IFCSURFACECURVESWEPTAREASOLID",
61    "IFCBLOCK",
62    "IFCSPHERE",
63    "IFCRIGHTCIRCULARCYLINDER",
64    "IFCRIGHTCIRCULARCONE",
65    "IFCRECTANGULARPYRAMID",
66    "IFCBOUNDINGBOX",
67    "IFCEXTRUDEDAREASOLIDTAPERED",
68    "IFCREVOLVEDAREASOLIDTAPERED",
69    "IFCFIXEDREFERENCESWEPTAREASOLID",
70    "IFCSECTIONEDSPINE",
71    "IFCSHELLBASEDSURFACEMODEL",
72    "IFCFACEBASEDSURFACEMODEL",
73    "IFCGEOMETRICSET",
74    "IFCGEOMETRICCURVESET",
75    // Bare curves/surfaces are valid representation items in Curve2D,
76    // Curve3D, SurfaceModel, and plan representations.
77    "IFCLINE",
78    "IFCCIRCLE",
79    "IFCELLIPSE",
80    "IFCPOLYLINE",
81    "IFCINDEXEDPOLYCURVE",
82    "IFCCOMPOSITECURVE",
83    "IFCCOMPOSITECURVEONSURFACE",
84    "IFCBOUNDARYCURVE",
85    "IFCOUTERBOUNDARYCURVE",
86    "IFCTRIMMEDCURVE",
87    "IFCOFFSETCURVE2D",
88    "IFCOFFSETCURVE3D",
89    "IFCPCURVE",
90    "IFCSURFACECURVE",
91    "IFCINTERSECTIONCURVE",
92    "IFCSEAMCURVE",
93    "IFCBSPLINECURVEWITHKNOTS",
94    "IFCRATIONALBSPLINECURVEWITHKNOTS",
95    "IFCPLANE",
96    "IFCCYLINDRICALSURFACE",
97    "IFCSPHERICALSURFACE",
98    "IFCTOROIDALSURFACE",
99    "IFCSURFACEOFLINEAREXTRUSION",
100    "IFCSURFACEOFREVOLUTION",
101    "IFCRECTANGULARTRIMMEDSURFACE",
102    "IFCCURVEBOUNDEDPLANE",
103    "IFCCURVEBOUNDEDSURFACE",
104    "IFCBSPLINESURFACEWITHKNOTS",
105    "IFCRATIONALBSPLINESURFACEWITHKNOTS",
106    "IFCPOINTONCURVE",
107    "IFCPOINTONSURFACE",
108];
109
110/// Recognized representation items that are not lowered yet.
111///
112/// Each entry names the concrete reason so a caller building a viewer can
113/// report progress instead of a bare failure. Adding a family here is how a
114/// stub is declared; implementing it means moving the name to [`IMPLEMENTED`].
115///
116/// Currently empty: every recognized representation item is lowered. A new
117/// unimplemented family is declared by adding it here.
118pub const PLANNED: &[(&str, &str)] = &[];
119
120/// A variant within a family that is admitted or refused independently.
121///
122/// [`IMPLEMENTED`] and [`PLANNED`] classify at *family* granularity, which is
123/// too coarse for families whose support depends on how the instance is
124/// authored. `IFCPCURVE` is implemented, but only for some reference-curve
125/// forms; a flat "implemented" claim hides the refusals inside it.
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub struct Variant {
128    /// The concrete family this variant belongs to; always in [`IMPLEMENTED`].
129    pub family: &'static str,
130    /// The distinguishing condition, as a caller would recognize it.
131    pub variant: &'static str,
132    /// Whether this specific variant lowers or is a typed refusal.
133    pub support: Support,
134    /// Why it is admitted or refused. Refusals name the missing contract.
135    pub rationale: &'static str,
136}
137
138/// Whether a [`Variant`] lowers exactly or reports a typed refusal.
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum Support {
141    /// Lowers exactly, with no approximation.
142    Admitted,
143    /// Reports a typed [`crate::GeometryError::Unsupported`] naming the entity.
144    Refused,
145}
146
147/// Variant-level dispositions for partially supported families.
148///
149/// Every family named here must appear in [`IMPLEMENTED`] and must declare at
150/// least one `Admitted` and one `Refused` variant -- a family with no refusals
151/// is not partial and belongs in `IMPLEMENTED` alone. Enforced by
152/// `tests/lower_dispatch_corpus.rs`.
153pub const PARTIAL: &[Variant] = &[
154    Variant {
155        family: "IFCPCURVE",
156        variant: "reference curve is an IfcPolyline",
157        support: Support::Admitted,
158        rationale: "an ordered 2D point sequence needs no evaluation",
159    },
160    Variant {
161        family: "IFCPCURVE",
162        variant: "reference curve is an IfcIndexedPolyCurve with no explicit \
163                  Segments, or only IfcLineIndex segments",
164        support: Support::Admitted,
165        rationale: "reads identically to a plain ordered point sequence",
166    },
167    Variant {
168        family: "IFCPCURVE",
169        variant: "reference curve is an IfcLine, IfcCircle or IfcEllipse \
170                  positioned by an IfcAxis2Placement2D",
171        support: Support::Admitted,
172        rationale: "defining values are read verbatim in the surface's own \
173                    (u, v) domain with no unit conversion",
174    },
175    Variant {
176        family: "IFCPCURVE",
177        variant: "reference conic positioned by an IfcAxis2Placement3D",
178        support: Support::Refused,
179        rationale: "a 3D placement's axis has no meaning in a 2D parameter \
180                    domain; admitting it would require inventing a projection",
181    },
182    Variant {
183        family: "IFCPCURVE",
184        variant: "reference curve is an IfcIndexedPolyCurve with an explicit \
185                  IfcArcIndex segment",
186        support: Support::Admitted,
187        rationale: "a three-point arc composes exactly from a parameter-space \
188                    circumcentre into Circle2 plus a Cartesian trim, mirroring \
189                    the 3D path with no approximation",
190    },
191    Variant {
192        family: "IFCPCURVE",
193        variant: "reference curve is an explicit-knot IfcBSplineCurveWithKnots \
194                  or IfcRationalBSplineCurveWithKnots",
195        support: Support::Admitted,
196        rationale: "every field is dimensionless or a curve parameter; knots \
197                    already pass through the 3D path unscaled, and control \
198                    points are read as raw (u, v) pairs",
199    },
200    Variant {
201        family: "IFCPCURVE",
202        variant: "reference curve is a trimmed or composite curve",
203        support: Support::Admitted,
204        rationale: "trim parameters and segments stay in the surface (u, v) \
205                    domain, unscaled, so no dimensional contract is needed",
206    },
207    Variant {
208        family: "IFCPCURVE",
209        variant: "reference curve is a convention-only IfcBSplineCurve",
210        support: Support::Refused,
211        rationale: "a base spline carries no authored knot vector to preserve",
212    },
213    Variant {
214        family: "IFCSURFACECURVE",
215        variant: "MasterRepresentation is Curve3D, PCurveS1, or PCurveS2 with \
216                  the named side present",
217        support: Support::Admitted,
218        rationale: "each side pairs a surface with its own p-curve, so the \
219                    neutral MasterRepresentation names S1 and S2 exactly",
220    },
221    Variant {
222        family: "IFCSURFACECURVE",
223        variant: "MasterRepresentation is PCurveS2 with only one associated \
224                  p-curve",
225        support: Support::Refused,
226        rationale: "the master names a parametric side the curve does not \
227                    have; the schema calls this inconsistent, so it is \
228                    refused rather than resolved to the remaining p-curve",
229    },
230];
231
232/// Lower any representation item into the caller's session.
233///
234/// Returns the node for implemented families and a typed
235/// [`crate::GeometryError::Unsupported`] naming the source entity otherwise.
236pub fn lower_representation_item(
237    session: &mut LoweringSession<'_>,
238    id: EntityId,
239    frame: Transform,
240) -> GeometryResult<NodeId> {
241    let type_name = session.type_name(id)?;
242    // Shape representations may legitimately contain bare curve and surface
243    // items (Curve2D/Curve3D/SurfaceModel). Route by generated IFC inheritance
244    // before the concrete solid table so plan and surface selections lower
245    // through the same total entry point as body geometry.
246    if is_a(&type_name, "IFCCURVE") {
247        return lower_curve_node(session, id, frame);
248    }
249    if is_a(&type_name, "IFCSURFACE") {
250        return lower_surface_node(session, id, frame);
251    }
252    match type_name.as_str() {
253        "IFCEXTRUDEDAREASOLID" => lower_extruded_area_solid_node(session, id, frame),
254        "IFCREVOLVEDAREASOLID" => lower_revolved_area_solid_node(session, id, frame),
255        "IFCBOOLEANRESULT" | "IFCBOOLEANCLIPPINGRESULT" => {
256            lower_boolean_result_node(session, id, frame)
257        }
258        "IFCHALFSPACESOLID" | "IFCBOXEDHALFSPACE" | "IFCPOLYGONALBOUNDEDHALFSPACE" => {
259            lower_half_space_node(session, id, frame)
260        }
261        "IFCMAPPEDITEM" => lower_mapped_item_node(session, id, frame),
262        "IFCPOINTONCURVE" => lower_point_on_curve_node(session, id, frame),
263        "IFCPOINTONSURFACE" => lower_point_on_surface_node(session, id, frame),
264        "IFCFACETEDBREP"
265        | "IFCFACETEDBREPWITHVOIDS"
266        | "IFCADVANCEDBREP"
267        | "IFCADVANCEDBREPWITHVOIDS" => lower_faceted_brep_node(session, id, frame),
268        "IFCTRIANGULATEDFACESET" => lower_triangulated_face_set_node(session, id, frame),
269        "IFCPOLYGONALFACESET" => lower_polygonal_face_set_node(session, id, frame),
270        "IFCCSGSOLID" => lower_csg_solid_node(session, id, frame),
271        "IFCSWEPTDISKSOLID" | "IFCSWEPTDISKSOLIDPOLYGONAL" => {
272            lower_swept_disk_node(session, id, frame)
273        }
274        "IFCSURFACECURVESWEPTAREASOLID" => {
275            lower_surface_curve_swept_area_solid_node(session, id, frame)
276        }
277        "IFCBOUNDINGBOX" => lower_bounding_box_node(session, id, frame),
278        "IFCEXTRUDEDAREASOLIDTAPERED" => lower_tapered_extrusion_node(session, id, frame),
279        "IFCREVOLVEDAREASOLIDTAPERED" => lower_tapered_revolution_node(session, id, frame),
280        "IFCFIXEDREFERENCESWEPTAREASOLID" => lower_fixed_reference_sweep_node(session, id, frame),
281        "IFCSECTIONEDSPINE" => lower_sectioned_spine_node(session, id, frame),
282        "IFCSHELLBASEDSURFACEMODEL"
283        | "IFCFACEBASEDSURFACEMODEL"
284        | "IFCGEOMETRICSET"
285        | "IFCGEOMETRICCURVESET" => lower_collection_node(session, id, frame),
286        "IFCBLOCK"
287        | "IFCSPHERE"
288        | "IFCRIGHTCIRCULARCYLINDER"
289        | "IFCRIGHTCIRCULARCONE"
290        | "IFCRECTANGULARPYRAMID" => lower_csg_primitive_node(session, id, frame),
291        other => Err(session.unsupported(id, other, detail_for(other))),
292    }
293}
294
295/// The documented reason a recognized family is not lowered yet.
296fn detail_for(type_name: &str) -> &'static str {
297    PLANNED
298        .iter()
299        .find(|(name, _)| *name == type_name)
300        .map(|(_, detail)| *detail)
301        .unwrap_or("representation item family is not lowered yet")
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn implemented_and_planned_families_do_not_overlap() {
310        for name in IMPLEMENTED {
311            assert!(
312                !PLANNED.iter().any(|(planned, _)| planned == name),
313                "{name} is listed as both implemented and planned"
314            );
315        }
316    }
317
318    #[test]
319    fn every_planned_family_states_a_concrete_reason() {
320        for (name, detail) in PLANNED {
321            assert!(!detail.is_empty(), "{name} has no stated reason");
322            assert_ne!(
323                *detail, "unsupported",
324                "{name} must say what specifically is missing"
325            );
326        }
327    }
328}