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//!
11//! # Coverage is per release
12//!
13//! [`IMPLEMENTED`], [`PLANNED`] and [`PARTIAL`] together classify every
14//! concrete representation item of IFC4 ADD2 TC1 and IFC4X3 ADD2 that is a
15//! root item rather than nested input; `tests/schema_coverage.rs` and
16//! `tests/lower_dispatch_corpus.rs` derive both inventories from the schemas
17//! and fail on an unclassified family.
18//!
19//! # Subtypes
20//!
21//! Dispatch matches exact type names, because most subtypes change meaning
22//! (`IfcGradientCurve` is an `IfcCompositeCurve` whose segments are heights
23//! over a horizontal base curve). A subtype the supertype's lowering handles
24//! exactly is routed through [`SPECIALISATIONS`], which names the attributes
25//! it adds and why ignoring or checking them is exact.
26
27use axiolid_model::NodeId;
28use ifc_model::EntityId;
29
30use crate::error::GeometryResult;
31use crate::lower::bbox::lower_bounding_box_node;
32use crate::lower::boolean::lower_boolean_result_node;
33use crate::lower::brep::{lower_face_surface_node, lower_faceted_brep_node};
34use crate::lower::collection::lower_collection_node;
35use crate::lower::csg::{
36    lower_csg_primitive_node, lower_csg_solid_node, lower_surface_curve_swept_area_solid_node,
37    lower_swept_disk_node,
38};
39use crate::lower::curve::lower_curve_node;
40use crate::lower::halfspace::lower_half_space_node;
41use crate::lower::mapped::lower_mapped_item_node;
42use crate::lower::point::{lower_point_on_curve_node, lower_point_on_surface_node};
43use crate::lower::sectioned::{
44    lower_sectioned_solid_horizontal_node, lower_sectioned_surface_node,
45};
46use crate::lower::session::LoweringSession;
47use crate::lower::surface::lower_surface_node;
48use crate::lower::swept::{
49    lower_directrix_derived_reference_sweep_node, lower_extruded_area_solid_node,
50    lower_fixed_reference_sweep_node, lower_revolved_area_solid_node, lower_sectioned_spine_node,
51    lower_tapered_extrusion_node, lower_tapered_revolution_node,
52};
53use crate::lower::tessellated::{lower_polygonal_face_set_node, lower_triangulated_face_set_node};
54use crate::select::is_a;
55use crate::transform::Transform;
56
57/// Families this crate lowers today, paired with what is still missing.
58///
59/// Kept as data so the census test can assert on it rather than re-deriving
60/// the list by scraping source text.
61pub const IMPLEMENTED: &[&str] = &[
62    "IFCEXTRUDEDAREASOLID",
63    "IFCREVOLVEDAREASOLID",
64    "IFCBOOLEANRESULT",
65    "IFCBOOLEANCLIPPINGRESULT",
66    "IFCMAPPEDITEM",
67    "IFCFACETEDBREP",
68    "IFCFACETEDBREPWITHVOIDS",
69    "IFCADVANCEDBREP",
70    "IFCADVANCEDBREPWITHVOIDS",
71    "IFCHALFSPACESOLID",
72    "IFCBOXEDHALFSPACE",
73    "IFCPOLYGONALBOUNDEDHALFSPACE",
74    "IFCTRIANGULATEDFACESET",
75    // IFC4X3: routed through SPECIALISATIONS; voids and holes are refused.
76    "IFCTRIANGULATEDIRREGULARNETWORK",
77    "IFCPOLYGONALFACESET",
78    "IFCCSGSOLID",
79    "IFCSWEPTDISKSOLID",
80    "IFCSWEPTDISKSOLIDPOLYGONAL",
81    "IFCSURFACECURVESWEPTAREASOLID",
82    "IFCBLOCK",
83    "IFCSPHERE",
84    "IFCRIGHTCIRCULARCYLINDER",
85    "IFCRIGHTCIRCULARCONE",
86    "IFCRECTANGULARPYRAMID",
87    "IFCBOUNDINGBOX",
88    "IFCEXTRUDEDAREASOLIDTAPERED",
89    "IFCREVOLVEDAREASOLIDTAPERED",
90    "IFCFIXEDREFERENCESWEPTAREASOLID",
91    // IFC4X3: tangent-only directrix; a tangent-plane directrix is refused.
92    "IFCDIRECTRIXDERIVEDREFERENCESWEPTAREASOLID",
93    "IFCSECTIONEDSPINE",
94    "IFCSHELLBASEDSURFACEMODEL",
95    "IFCFACEBASEDSURFACEMODEL",
96    // A face surface is a legal item and a member of IfcSurfaceOrFaceSurface
97    // (connection surfaces); it lowers as a single-face open BRep.
98    "IFCFACESURFACE",
99    "IFCADVANCEDFACE",
100    "IFCGEOMETRICSET",
101    "IFCGEOMETRICCURVESET",
102    // Bare curves/surfaces are valid representation items in Curve2D,
103    // Curve3D, SurfaceModel, and plan representations.
104    "IFCLINE",
105    "IFCCIRCLE",
106    "IFCELLIPSE",
107    "IFCPOLYLINE",
108    "IFCINDEXEDPOLYCURVE",
109    "IFCCOMPOSITECURVE",
110    "IFCCOMPOSITECURVEONSURFACE",
111    "IFCBOUNDARYCURVE",
112    "IFCOUTERBOUNDARYCURVE",
113    "IFCTRIMMEDCURVE",
114    "IFCOFFSETCURVE2D",
115    "IFCOFFSETCURVE3D",
116    "IFCPCURVE",
117    "IFCSURFACECURVE",
118    "IFCINTERSECTIONCURVE",
119    "IFCSEAMCURVE",
120    "IFCBSPLINECURVEWITHKNOTS",
121    "IFCRATIONALBSPLINECURVEWITHKNOTS",
122    "IFCPLANE",
123    "IFCCYLINDRICALSURFACE",
124    "IFCSPHERICALSURFACE",
125    "IFCTOROIDALSURFACE",
126    "IFCSURFACEOFLINEAREXTRUSION",
127    "IFCSURFACEOFREVOLUTION",
128    "IFCRECTANGULARTRIMMEDSURFACE",
129    "IFCCURVEBOUNDEDPLANE",
130    "IFCCURVEBOUNDEDSURFACE",
131    "IFCBSPLINESURFACEWITHKNOTS",
132    "IFCRATIONALBSPLINESURFACEWITHKNOTS",
133    "IFCPOINTONCURVE",
134    "IFCPOINTONSURFACE",
135    // IFC4X3 alignment geometry (#243): placed segments and centrelines.
136    "IFCCURVESEGMENT",
137    "IFCGRADIENTCURVE",
138];
139
140/// Recognized representation items that are not lowered yet.
141///
142/// Each entry names the concrete reason so a caller building a viewer can
143/// report progress instead of a bare failure. Adding a family here is how a
144/// stub is declared; implementing it means moving the name to [`IMPLEMENTED`].
145/// The dispatcher reports the reason in its typed `Unsupported` refusal.
146///
147/// Every entry is an IFC4X3 ADD2 family: every IFC4 ADD2 TC1 root item is
148/// lowered. Each reason is the runtime refusal text, which
149/// `tests/lower_dispatch_corpus.rs` keeps equal.
150///
151/// The IFC4X3 spirals and `IfcPolynomialCurve` are unbounded on their own;
152/// they lower exactly as the `ParentCurve` of an `IfcCurveSegment`, which is
153/// where IFC4.3 uses them.
154pub const PLANNED: &[(&str, &str)] = &[
155    // Alignment curves (IfcGeometryResource, IFC4X3); `lower::curve`.
156    ("IFCCLOTHOID", "an IfcSpiral is unbounded (-inf < u < inf) and the neutral intrinsic curve needs a \
157         finite arc length; it lowers exactly as the ParentCurve of an IfcCurveSegment"),
158    ("IFCSECONDORDERPOLYNOMIALSPIRAL", "an IfcSpiral is unbounded (-inf < u < inf) and the neutral intrinsic curve needs a \
159         finite arc length; it lowers exactly as the ParentCurve of an IfcCurveSegment"),
160    ("IFCTHIRDORDERPOLYNOMIALSPIRAL", "an IfcSpiral is unbounded (-inf < u < inf) and the neutral intrinsic curve needs a \
161         finite arc length; it lowers exactly as the ParentCurve of an IfcCurveSegment"),
162    ("IFCSEVENTHORDERPOLYNOMIALSPIRAL", "an IfcSpiral is unbounded (-inf < u < inf) and the neutral intrinsic curve needs a \
163         finite arc length; it lowers exactly as the ParentCurve of an IfcCurveSegment"),
164    ("IFCCOSINESPIRAL", "an IfcCosineSpiral or IfcSineSpiral law depends on the length L of the IfcCurveSegment \
165         using it; it lowers exactly only as the ParentCurve of an IfcCurveSegment"),
166    ("IFCSINESPIRAL", "an IfcCosineSpiral or IfcSineSpiral law depends on the length L of the IfcCurveSegment \
167         using it; it lowers exactly only as the ParentCurve of an IfcCurveSegment"),
168    ("IFCPOLYNOMIALCURVE", "an IfcPolynomialCurve is unbounded (-inf < u < inf) and the neutral vocabulary has no \
169         unbounded polynomial curve; bounded by an IfcCurveSegment it needs an arc-length trim (#90)"),
170    ("IFCSEGMENTEDREFERENCECURVE", "IfcSegmentedReferenceCurve adds cant (a roll of the section about the centreline); the \
171         pinned neutral curve vocabulary has no roll law to carry it exactly (#93)"),
172    // Sections along an alignment (IFC4X3); `lower::sectioned` raises these.
173    (
174        "IFCSECTIONEDSOLIDHORIZONTAL",
175        "kernel: sections stand at IfcAxis2PlacementLinear stations (a measure \
176         along the directrix plus offsets) and are swept horizontally with \
177         tag-matched linear interpolation; the neutral SectionedSpine takes only \
178         resolved section frames, and resolving a station is curve evaluation",
179    ),
180    (
181        "IFCSECTIONEDSURFACE",
182        "kernel: no neutral sectioned-surface relation exists; its open sections \
183         stand at IfcAxis2PlacementLinear stations along the directrix and are \
184         joined by tag, and the neutral SectionedSpine is a solid over area \
185         profiles",
186    ),
187    // Distance-along-curve geometry (IFC4X3).
188    (
189        "IFCOFFSETCURVEBYDISTANCES",
190        "offsets are stated at stations along the basis curve as \
191         IfcPointByDistanceExpression values; the neutral model has no \
192         distance-along-curve point or station-offset curve to hold them",
193    ),
194    (
195        "IFCPOINTBYDISTANCEEXPRESSION",
196        "a point at a distance along a basis curve, offset in that curve's \
197         frame; the neutral model has no distance-along-curve point relation",
198    ),
199    (
200        "IFCAXIS2PLACEMENTLINEAR",
201        "a frame located by an IfcPointByDistanceExpression; the neutral model \
202         has no distance-along-curve point relation to anchor it",
203    ),
204];
205
206/// A subtype the supertype's lowering handles exactly.
207///
208/// Routing a subtype to its supertype's lowerer is exact only when every
209/// attribute and rule the subtype adds either leaves the shape unchanged or
210/// is checked by that lowerer. `tests/schema_coverage.rs` asserts each row
211/// against the IFC4X3 schema: the subtype relation, the added attributes,
212/// and that both names are in [`IMPLEMENTED`].
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub struct Specialisation {
215    /// The subtype as a STEP type name.
216    pub subtype: &'static str,
217    /// The supertype whose lowering it is routed to.
218    pub supertype: &'static str,
219    /// The explicit attributes the subtype declares, in schema order.
220    pub added_attributes: &'static [&'static str],
221    /// Why the supertype's lowering is exact for it.
222    pub rationale: &'static str,
223}
224
225/// Subtypes routed to their supertype's lowering.
226pub const SPECIALISATIONS: &[Specialisation] = &[Specialisation {
227    subtype: "IFCTRIANGULATEDIRREGULARNETWORK",
228    supertype: "IFCTRIANGULATEDFACESET",
229    added_attributes: &["Flags"],
230    rationale: "the face-set slots are unchanged; the lowerer checks Flags \
231                and refuses voids, holes and undocumented codes, so only \
232                breakline codes, which leave the triangles unchanged, lower",
233}];
234
235/// A variant within a family that is admitted or refused independently.
236///
237/// [`IMPLEMENTED`] and [`PLANNED`] classify at *family* granularity, which is
238/// too coarse for families whose support depends on how the instance is
239/// authored. `IFCPCURVE` is implemented, but only for some reference-curve
240/// forms; a flat "implemented" claim hides the refusals inside it.
241#[derive(Debug, Clone, Copy, PartialEq, Eq)]
242pub struct Variant {
243    /// The concrete family this variant belongs to; always in [`IMPLEMENTED`].
244    pub family: &'static str,
245    /// The distinguishing condition, as a caller would recognize it.
246    pub variant: &'static str,
247    /// Whether this specific variant lowers or is a typed refusal.
248    pub support: Support,
249    /// Why it is admitted or refused. Refusals name the missing contract.
250    pub rationale: &'static str,
251}
252
253/// Whether a [`Variant`] lowers exactly or reports a typed refusal.
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255#[non_exhaustive]
256pub enum Support {
257    /// Lowers exactly, with no approximation.
258    Admitted,
259    /// Reports a typed [`crate::GeometryError::Unsupported`] naming the entity.
260    Refused,
261}
262
263/// Variant-level dispositions for partially supported families.
264///
265/// Every family named here must appear in [`IMPLEMENTED`] and must declare at
266/// least one `Admitted` and one `Refused` variant -- a family with no refusals
267/// is not partial and belongs in `IMPLEMENTED` alone. Enforced by
268/// `tests/lower_dispatch_corpus.rs`.
269pub const PARTIAL: &[Variant] = &[
270    Variant {
271        family: "IFCPCURVE",
272        variant: "reference curve is an IfcPolyline",
273        support: Support::Admitted,
274        rationale: "an ordered 2D point sequence needs no evaluation",
275    },
276    Variant {
277        family: "IFCPCURVE",
278        variant: "reference curve is an IfcIndexedPolyCurve with no explicit \
279                  Segments, or only IfcLineIndex segments",
280        support: Support::Admitted,
281        rationale: "reads identically to a plain ordered point sequence",
282    },
283    Variant {
284        family: "IFCPCURVE",
285        variant: "reference curve is an IfcLine, IfcCircle or IfcEllipse \
286                  positioned by an IfcAxis2Placement2D",
287        support: Support::Admitted,
288        rationale: "defining values are read verbatim in the surface's own \
289                    (u, v) domain with no unit conversion",
290    },
291    Variant {
292        family: "IFCPCURVE",
293        variant: "reference conic positioned by an IfcAxis2Placement3D",
294        support: Support::Refused,
295        rationale: "a 3D placement's axis has no meaning in a 2D parameter \
296                    domain; admitting it would require inventing a projection",
297    },
298    Variant {
299        family: "IFCPCURVE",
300        variant: "reference curve is an IfcIndexedPolyCurve with an explicit \
301                  IfcArcIndex segment",
302        support: Support::Admitted,
303        rationale: "a three-point arc composes exactly from a parameter-space \
304                    circumcentre into Circle2 plus a Cartesian trim, mirroring \
305                    the 3D path with no approximation",
306    },
307    Variant {
308        family: "IFCPCURVE",
309        variant: "reference curve is an explicit-knot IfcBSplineCurveWithKnots \
310                  or IfcRationalBSplineCurveWithKnots",
311        support: Support::Admitted,
312        rationale: "every field is dimensionless or a curve parameter; knots \
313                    already pass through the 3D path unscaled, and control \
314                    points are read as raw (u, v) pairs",
315    },
316    Variant {
317        family: "IFCPCURVE",
318        variant: "reference curve is a trimmed or composite curve",
319        support: Support::Admitted,
320        rationale: "trim parameters and segments stay in the surface (u, v) \
321                    domain, unscaled, so no dimensional contract is needed",
322    },
323    Variant {
324        family: "IFCPCURVE",
325        variant: "reference curve is a convention-only IfcBSplineCurve",
326        support: Support::Refused,
327        rationale: "a base spline carries no authored knot vector to preserve",
328    },
329    Variant {
330        family: "IFCDIRECTRIXDERIVEDREFERENCESWEPTAREASOLID",
331        variant: "directrix defines only a tangent (no IfcCurveSegment, \
332                  segment-built or surface curve reachable)",
333        support: Support::Admitted,
334        rationale: "IFC4.3 gives it exactly the behaviour of \
335                    IfcFixedReferenceSweptAreaSolid in this case, so it lowers \
336                    to the same FixedReferenceSweep",
337    },
338    Variant {
339        family: "IFCDIRECTRIXDERIVEDREFERENCESWEPTAREASOLID",
340        variant: "directrix defines a tangent plane",
341        support: Support::Refused,
342        rationale: "kernel: the directrix defines a tangent plane (it is built from \
343                    IfcCurveSegment placements or lies on a surface), so the derived \
344                    reference adds that plane's rotation to FixedReference; the neutral \
345                    FixedReferenceSweep carries only a constant reference direction",
346    },
347    Variant {
348        family: "IFCTRIANGULATEDIRREGULARNETWORK",
349        variant: "every Flags value is a breakline code, 0 to 7",
350        support: Support::Admitted,
351        rationale: "a breakline marks an edge the triangulation already has, \
352                    so the triangle surface is the supertype's; the flags \
353                    are not carried into the mesh",
354    },
355    Variant {
356        family: "IFCTRIANGULATEDIRREGULARNETWORK",
357        variant: "a Flags value is -1 (hole) or -2 (void)",
358        support: Support::Refused,
359        rationale: "the triangle is excluded from the surface, and a hole may \
360                    fall back on another surface; the neutral mesh has no \
361                    face-exclusion or fall-back channel",
362    },
363    Variant {
364        family: "IFCTRIANGULATEDIRREGULARNETWORK",
365        variant: "a Flags value is outside -2 to 7",
366        support: Support::Refused,
367        rationale: "the documentation defines no meaning for it",
368    },
369    Variant {
370        family: "IFCSURFACECURVE",
371        variant: "MasterRepresentation is Curve3D, PCurveS1, or PCurveS2 with \
372                  the named side present",
373        support: Support::Admitted,
374        rationale: "each side pairs a surface with its own p-curve, so the \
375                    neutral MasterRepresentation names S1 and S2 exactly",
376    },
377    Variant {
378        family: "IFCCURVESEGMENT",
379        variant: "ParentCurve is an IfcLine, IfcCircle or 2D IfcPolyline, \
380                  measured by IfcLengthMeasure",
381        support: Support::Admitted,
382        rationale: "a line, arc or polyline cut by arc length and placed rigidly \
383                    is elementary: a polyline or an angle-trimmed circle",
384    },
385    Variant {
386        family: "IFCCURVESEGMENT",
387        variant: "ParentCurve is an IfcSpiral subtype, measured by \
388                  IfcLengthMeasure",
389        support: Support::Admitted,
390        rationale: "the spiral's curvature law, rebased to the segment in closed \
391                    form, on a planar intrinsic curve; nothing is integrated",
392    },
393    Variant {
394        family: "IFCCURVESEGMENT",
395        variant: "SegmentLength is zero (the closing segment of a layout)",
396        support: Support::Admitted,
397        rationale: "its placement exactly: a planar intrinsic curve of length zero",
398    },
399    Variant {
400        family: "IFCCURVESEGMENT",
401        variant: "ParentCurve is an IfcPolynomialCurve",
402        support: Support::Refused,
403        rationale: "an IfcPolynomialCurve trimmed by arc length: the end parameter inverts a non-elementary \
404         arc-length integral and the neutral vocabulary has no arc-length trim (#90)",
405    },
406    Variant {
407        family: "IFCCURVESEGMENT",
408        variant: "SegmentStart or SegmentLength is an IfcParameterValue",
409        support: Support::Refused,
410        rationale: "SegmentStart/SegmentLength given as IfcParameterValue: IFC4.3 ADD2 defines no parametric \
411         space for IfcCurveSegment parents yet (informal proposition 1 requires IfcLengthMeasure)",
412    },
413    Variant {
414        family: "IFCCURVESEGMENT",
415        variant: "Placement is an IfcAxis2PlacementLinear",
416        support: Support::Refused,
417        rationale: "an IfcAxis2PlacementLinear placement belongs to an IfcSegmentedReferenceCurve (cant); \
418         the neutral vocabulary has no roll law to carry it (#93)",
419    },
420    Variant {
421        family: "IFCGRADIENTCURVE",
422        variant: "horizontal IfcCurveSegments over lines, arcs and spirals; \
423                  vertical IfcCurveSegments over IfcLine or a degree-2 \
424                  IfcPolynomialCurve that keeps its start tangent",
425        support: Support::Admitted,
426        rationale: "one intrinsic plan with a piecewise curvature law plus a \
427                    piecewise polynomial elevation law: Curve3::Elevated, exact",
428    },
429    Variant {
430        family: "IFCGRADIENTCURVE",
431        variant: "a vertical IfcCircle or IfcClothoid segment",
432        support: Support::Refused,
433        rationale: "neither is polynomial in plan distance and the pinned \
434                    ElevationLaw has only polynomial pieces (#258)",
435    },
436    Variant {
437        family: "IFCGRADIENTCURVE",
438        variant: "a vertical parabola with no following segment, closing \
439                  segment or EndPoint",
440        support: Support::Refused,
441        rationale: "its end abscissa inverts a non-elementary arc-length \
442                    integral (#90)",
443    },
444    Variant {
445        family: "IFCGRADIENTCURVE",
446        variant: "a heading kink, a closed-form position gap, or a profile \
447                  that does not span the base curve",
448        support: Support::Refused,
449        rationale: "one plan curve and one elevation law cannot carry a kink or \
450                    a gap, and an elevation law must cover the whole plan",
451    },
452    Variant {
453        family: "IFCGRADIENTCURVE",
454        variant: "placed by a frame that tilts, scales or mirrors the vertical",
455        support: Support::Refused,
456        rationale: "a plan plus a height is carried only by frames that keep \
457                    the vertical axis",
458    },
459    Variant {
460        family: "IFCSURFACECURVE",
461        variant: "MasterRepresentation is PCurveS2 with only one associated \
462                  p-curve",
463        support: Support::Refused,
464        rationale: "the master names a parametric side the curve does not \
465                    have; the schema calls this inconsistent, so it is \
466                    refused rather than resolved to the remaining p-curve",
467    },
468];
469
470/// Lower any representation item into the caller's session.
471///
472/// Returns the node for implemented families and a typed
473/// [`crate::GeometryError::Unsupported`] naming the source entity otherwise.
474pub fn lower_representation_item(
475    session: &mut LoweringSession<'_>,
476    id: EntityId,
477    frame: Transform,
478) -> GeometryResult<NodeId> {
479    let type_name = session.type_name(id)?;
480    // IFC4X3 sectioned surface, routed before the inheritance test so the
481    // named refusal holds whether or not the subtype table knows the type.
482    if type_name == "IFCSECTIONEDSURFACE" {
483        return lower_sectioned_surface_node(session, id);
484    }
485    // An exact specialisation lowers through its supertype's arm. The
486    // refusal below still names the entity's own type.
487    let routed = SPECIALISATIONS
488        .iter()
489        .find(|row| row.subtype == type_name)
490        .map_or(type_name.as_str(), |row| row.supertype);
491    // Shape representations may legitimately contain bare curve and surface
492    // items (Curve2D/Curve3D/SurfaceModel). Route by generated IFC inheritance
493    // before the concrete solid table so plan and surface selections lower
494    // through the same total entry point as body geometry.
495    if is_a(&type_name, "IFCCURVE") {
496        return lower_curve_node(session, id, frame);
497    }
498    if is_a(&type_name, "IFCSURFACE") {
499        return lower_surface_node(session, id, frame);
500    }
501    match routed {
502        "IFCEXTRUDEDAREASOLID" => lower_extruded_area_solid_node(session, id, frame),
503        "IFCREVOLVEDAREASOLID" => lower_revolved_area_solid_node(session, id, frame),
504        "IFCBOOLEANRESULT" | "IFCBOOLEANCLIPPINGRESULT" => {
505            lower_boolean_result_node(session, id, frame)
506        }
507        "IFCHALFSPACESOLID" | "IFCBOXEDHALFSPACE" | "IFCPOLYGONALBOUNDEDHALFSPACE" => {
508            lower_half_space_node(session, id, frame)
509        }
510        "IFCMAPPEDITEM" => lower_mapped_item_node(session, id, frame),
511        // IFC4X3 curve families the IFC4 subtype table does not know, and
512        // `IfcCurveSegment`, an IfcSegment the curve module lowers.
513        "IFCCURVESEGMENT"
514        | "IFCGRADIENTCURVE"
515        | "IFCSEGMENTEDREFERENCECURVE"
516        | "IFCPOLYNOMIALCURVE"
517        | "IFCCLOTHOID"
518        | "IFCCOSINESPIRAL"
519        | "IFCSINESPIRAL"
520        | "IFCSECONDORDERPOLYNOMIALSPIRAL"
521        | "IFCTHIRDORDERPOLYNOMIALSPIRAL"
522        | "IFCSEVENTHORDERPOLYNOMIALSPIRAL" => lower_curve_node(session, id, frame),
523        "IFCPOINTONCURVE" => lower_point_on_curve_node(session, id, frame),
524        "IFCPOINTONSURFACE" => lower_point_on_surface_node(session, id, frame),
525        "IFCFACETEDBREP"
526        | "IFCFACETEDBREPWITHVOIDS"
527        | "IFCADVANCEDBREP"
528        | "IFCADVANCEDBREPWITHVOIDS" => lower_faceted_brep_node(session, id, frame),
529        "IFCFACESURFACE" | "IFCADVANCEDFACE" => lower_face_surface_node(session, id, frame),
530        "IFCTRIANGULATEDFACESET" => lower_triangulated_face_set_node(session, id, frame),
531        "IFCPOLYGONALFACESET" => lower_polygonal_face_set_node(session, id, frame),
532        "IFCCSGSOLID" => lower_csg_solid_node(session, id, frame),
533        "IFCSWEPTDISKSOLID" | "IFCSWEPTDISKSOLIDPOLYGONAL" => {
534            lower_swept_disk_node(session, id, frame)
535        }
536        "IFCSURFACECURVESWEPTAREASOLID" => {
537            lower_surface_curve_swept_area_solid_node(session, id, frame)
538        }
539        "IFCBOUNDINGBOX" => lower_bounding_box_node(session, id, frame),
540        "IFCEXTRUDEDAREASOLIDTAPERED" => lower_tapered_extrusion_node(session, id, frame),
541        "IFCREVOLVEDAREASOLIDTAPERED" => lower_tapered_revolution_node(session, id, frame),
542        "IFCFIXEDREFERENCESWEPTAREASOLID" => lower_fixed_reference_sweep_node(session, id, frame),
543        "IFCDIRECTRIXDERIVEDREFERENCESWEPTAREASOLID" => {
544            lower_directrix_derived_reference_sweep_node(session, id, frame)
545        }
546        "IFCSECTIONEDSOLIDHORIZONTAL" => lower_sectioned_solid_horizontal_node(session, id),
547        "IFCSECTIONEDSPINE" => lower_sectioned_spine_node(session, id, frame),
548        "IFCSHELLBASEDSURFACEMODEL"
549        | "IFCFACEBASEDSURFACEMODEL"
550        | "IFCGEOMETRICSET"
551        | "IFCGEOMETRICCURVESET" => lower_collection_node(session, id, frame),
552        "IFCBLOCK"
553        | "IFCSPHERE"
554        | "IFCRIGHTCIRCULARCYLINDER"
555        | "IFCRIGHTCIRCULARCONE"
556        | "IFCRECTANGULARPYRAMID" => lower_csg_primitive_node(session, id, frame),
557        _ => Err(session.unsupported(id, &type_name, detail_for(&type_name))),
558    }
559}
560
561/// The documented reason a recognized family is not lowered yet.
562fn detail_for(type_name: &str) -> &'static str {
563    PLANNED
564        .iter()
565        .find(|(name, _)| *name == type_name)
566        .map(|(_, detail)| *detail)
567        .unwrap_or("representation item family is not lowered yet")
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573
574    #[test]
575    fn implemented_and_planned_families_do_not_overlap() {
576        for name in IMPLEMENTED {
577            assert!(
578                !PLANNED.iter().any(|(planned, _)| planned == name),
579                "{name} is listed as both implemented and planned"
580            );
581        }
582    }
583
584    #[test]
585    fn every_planned_family_states_a_concrete_reason() {
586        for (name, detail) in PLANNED {
587            assert!(!detail.is_empty(), "{name} has no stated reason");
588            assert_ne!(
589                *detail, "unsupported",
590                "{name} must say what specifically is missing"
591            );
592        }
593    }
594}