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)]
144#[non_exhaustive]
145pub enum Support {
146    /// Lowers exactly, with no approximation.
147    Admitted,
148    /// Reports a typed [`crate::GeometryError::Unsupported`] naming the entity.
149    Refused,
150}
151
152/// Variant-level dispositions for partially supported families.
153///
154/// Every family named here must appear in [`IMPLEMENTED`] and must declare at
155/// least one `Admitted` and one `Refused` variant -- a family with no refusals
156/// is not partial and belongs in `IMPLEMENTED` alone. Enforced by
157/// `tests/lower_dispatch_corpus.rs`.
158pub const PARTIAL: &[Variant] = &[
159    Variant {
160        family: "IFCPCURVE",
161        variant: "reference curve is an IfcPolyline",
162        support: Support::Admitted,
163        rationale: "an ordered 2D point sequence needs no evaluation",
164    },
165    Variant {
166        family: "IFCPCURVE",
167        variant: "reference curve is an IfcIndexedPolyCurve with no explicit \
168                  Segments, or only IfcLineIndex segments",
169        support: Support::Admitted,
170        rationale: "reads identically to a plain ordered point sequence",
171    },
172    Variant {
173        family: "IFCPCURVE",
174        variant: "reference curve is an IfcLine, IfcCircle or IfcEllipse \
175                  positioned by an IfcAxis2Placement2D",
176        support: Support::Admitted,
177        rationale: "defining values are read verbatim in the surface's own \
178                    (u, v) domain with no unit conversion",
179    },
180    Variant {
181        family: "IFCPCURVE",
182        variant: "reference conic positioned by an IfcAxis2Placement3D",
183        support: Support::Refused,
184        rationale: "a 3D placement's axis has no meaning in a 2D parameter \
185                    domain; admitting it would require inventing a projection",
186    },
187    Variant {
188        family: "IFCPCURVE",
189        variant: "reference curve is an IfcIndexedPolyCurve with an explicit \
190                  IfcArcIndex segment",
191        support: Support::Admitted,
192        rationale: "a three-point arc composes exactly from a parameter-space \
193                    circumcentre into Circle2 plus a Cartesian trim, mirroring \
194                    the 3D path with no approximation",
195    },
196    Variant {
197        family: "IFCPCURVE",
198        variant: "reference curve is an explicit-knot IfcBSplineCurveWithKnots \
199                  or IfcRationalBSplineCurveWithKnots",
200        support: Support::Admitted,
201        rationale: "every field is dimensionless or a curve parameter; knots \
202                    already pass through the 3D path unscaled, and control \
203                    points are read as raw (u, v) pairs",
204    },
205    Variant {
206        family: "IFCPCURVE",
207        variant: "reference curve is a trimmed or composite curve",
208        support: Support::Admitted,
209        rationale: "trim parameters and segments stay in the surface (u, v) \
210                    domain, unscaled, so no dimensional contract is needed",
211    },
212    Variant {
213        family: "IFCPCURVE",
214        variant: "reference curve is a convention-only IfcBSplineCurve",
215        support: Support::Refused,
216        rationale: "a base spline carries no authored knot vector to preserve",
217    },
218    Variant {
219        family: "IFCSURFACECURVE",
220        variant: "MasterRepresentation is Curve3D, PCurveS1, or PCurveS2 with \
221                  the named side present",
222        support: Support::Admitted,
223        rationale: "each side pairs a surface with its own p-curve, so the \
224                    neutral MasterRepresentation names S1 and S2 exactly",
225    },
226    Variant {
227        family: "IFCSURFACECURVE",
228        variant: "MasterRepresentation is PCurveS2 with only one associated \
229                  p-curve",
230        support: Support::Refused,
231        rationale: "the master names a parametric side the curve does not \
232                    have; the schema calls this inconsistent, so it is \
233                    refused rather than resolved to the remaining p-curve",
234    },
235];
236
237/// Lower any representation item into the caller's session.
238///
239/// Returns the node for implemented families and a typed
240/// [`crate::GeometryError::Unsupported`] naming the source entity otherwise.
241pub fn lower_representation_item(
242    session: &mut LoweringSession<'_>,
243    id: EntityId,
244    frame: Transform,
245) -> GeometryResult<NodeId> {
246    let type_name = session.type_name(id)?;
247    // Shape representations may legitimately contain bare curve and surface
248    // items (Curve2D/Curve3D/SurfaceModel). Route by generated IFC inheritance
249    // before the concrete solid table so plan and surface selections lower
250    // through the same total entry point as body geometry.
251    if is_a(&type_name, "IFCCURVE") {
252        return lower_curve_node(session, id, frame);
253    }
254    if is_a(&type_name, "IFCSURFACE") {
255        return lower_surface_node(session, id, frame);
256    }
257    match type_name.as_str() {
258        "IFCEXTRUDEDAREASOLID" => lower_extruded_area_solid_node(session, id, frame),
259        "IFCREVOLVEDAREASOLID" => lower_revolved_area_solid_node(session, id, frame),
260        "IFCBOOLEANRESULT" | "IFCBOOLEANCLIPPINGRESULT" => {
261            lower_boolean_result_node(session, id, frame)
262        }
263        "IFCHALFSPACESOLID" | "IFCBOXEDHALFSPACE" | "IFCPOLYGONALBOUNDEDHALFSPACE" => {
264            lower_half_space_node(session, id, frame)
265        }
266        "IFCMAPPEDITEM" => lower_mapped_item_node(session, id, frame),
267        "IFCPOINTONCURVE" => lower_point_on_curve_node(session, id, frame),
268        "IFCPOINTONSURFACE" => lower_point_on_surface_node(session, id, frame),
269        "IFCFACETEDBREP"
270        | "IFCFACETEDBREPWITHVOIDS"
271        | "IFCADVANCEDBREP"
272        | "IFCADVANCEDBREPWITHVOIDS" => lower_faceted_brep_node(session, id, frame),
273        "IFCFACESURFACE" | "IFCADVANCEDFACE" => lower_face_surface_node(session, id, frame),
274        "IFCTRIANGULATEDFACESET" => lower_triangulated_face_set_node(session, id, frame),
275        "IFCPOLYGONALFACESET" => lower_polygonal_face_set_node(session, id, frame),
276        "IFCCSGSOLID" => lower_csg_solid_node(session, id, frame),
277        "IFCSWEPTDISKSOLID" | "IFCSWEPTDISKSOLIDPOLYGONAL" => {
278            lower_swept_disk_node(session, id, frame)
279        }
280        "IFCSURFACECURVESWEPTAREASOLID" => {
281            lower_surface_curve_swept_area_solid_node(session, id, frame)
282        }
283        "IFCBOUNDINGBOX" => lower_bounding_box_node(session, id, frame),
284        "IFCEXTRUDEDAREASOLIDTAPERED" => lower_tapered_extrusion_node(session, id, frame),
285        "IFCREVOLVEDAREASOLIDTAPERED" => lower_tapered_revolution_node(session, id, frame),
286        "IFCFIXEDREFERENCESWEPTAREASOLID" => lower_fixed_reference_sweep_node(session, id, frame),
287        "IFCSECTIONEDSPINE" => lower_sectioned_spine_node(session, id, frame),
288        "IFCSHELLBASEDSURFACEMODEL"
289        | "IFCFACEBASEDSURFACEMODEL"
290        | "IFCGEOMETRICSET"
291        | "IFCGEOMETRICCURVESET" => lower_collection_node(session, id, frame),
292        "IFCBLOCK"
293        | "IFCSPHERE"
294        | "IFCRIGHTCIRCULARCYLINDER"
295        | "IFCRIGHTCIRCULARCONE"
296        | "IFCRECTANGULARPYRAMID" => lower_csg_primitive_node(session, id, frame),
297        other => Err(session.unsupported(id, other, detail_for(other))),
298    }
299}
300
301/// The documented reason a recognized family is not lowered yet.
302fn detail_for(type_name: &str) -> &'static str {
303    PLANNED
304        .iter()
305        .find(|(name, _)| *name == type_name)
306        .map(|(_, detail)| *detail)
307        .unwrap_or("representation item family is not lowered yet")
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    #[test]
315    fn implemented_and_planned_families_do_not_overlap() {
316        for name in IMPLEMENTED {
317            assert!(
318                !PLANNED.iter().any(|(planned, _)| planned == name),
319                "{name} is listed as both implemented and planned"
320            );
321        }
322    }
323
324    #[test]
325    fn every_planned_family_states_a_concrete_reason() {
326        for (name, detail) in PLANNED {
327            assert!(!detail.is_empty(), "{name} has no stated reason");
328            assert_ne!(
329                *detail, "unsupported",
330                "{name} must say what specifically is missing"
331            );
332        }
333    }
334}