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