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; it lowers exactly as the ParentCurve of an IfcCurveSegment"),
170    ("IFCSEGMENTEDREFERENCECURVE", "IfcSegmentedReferenceCurve states cant through segments placed at stations along its base \
171         curve and parent curves with no normative mapping to a cant law; the business cant layout \
172         lowers to a banked curve through ifc-alignment (#311)"),
173    // Sections along an alignment (IFC4X3); `lower::sectioned` raises these.
174    (
175        "IFCSECTIONEDSOLIDHORIZONTAL",
176        "kernel: sections stand at IfcAxis2PlacementLinear stations (a measure \
177         along the directrix plus offsets) and are swept horizontally with \
178         tag-matched linear interpolation; the neutral SectionedSpine takes only \
179         resolved section frames, and resolving a station is curve evaluation",
180    ),
181    (
182        "IFCSECTIONEDSURFACE",
183        "kernel: no neutral sectioned-surface relation exists; its open sections \
184         stand at IfcAxis2PlacementLinear stations along the directrix and are \
185         joined by tag, and the neutral SectionedSpine is a solid over area \
186         profiles",
187    ),
188    // Distance-along-curve geometry (IFC4X3).
189    (
190        "IFCOFFSETCURVEBYDISTANCES",
191        "offsets are stated at stations along the basis curve as \
192         IfcPointByDistanceExpression values; the neutral model has no \
193         distance-along-curve point or station-offset curve to hold them",
194    ),
195    (
196        "IFCPOINTBYDISTANCEEXPRESSION",
197        "a point at a distance along a basis curve, offset in that curve's \
198         frame; the neutral model has no distance-along-curve point relation",
199    ),
200    (
201        "IFCAXIS2PLACEMENTLINEAR",
202        "a frame located by an IfcPointByDistanceExpression; the neutral model \
203         has no distance-along-curve point relation to anchor it",
204    ),
205];
206
207/// A subtype the supertype's lowering handles exactly.
208///
209/// Routing a subtype to its supertype's lowerer is exact only when every
210/// attribute and rule the subtype adds either leaves the shape unchanged or
211/// is checked by that lowerer. `tests/schema_coverage.rs` asserts each row
212/// against the IFC4X3 schema: the subtype relation, the added attributes,
213/// and that both names are in [`IMPLEMENTED`].
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub struct Specialisation {
216    /// The subtype as a STEP type name.
217    pub subtype: &'static str,
218    /// The supertype whose lowering it is routed to.
219    pub supertype: &'static str,
220    /// The explicit attributes the subtype declares, in schema order.
221    pub added_attributes: &'static [&'static str],
222    /// Why the supertype's lowering is exact for it.
223    pub rationale: &'static str,
224}
225
226/// Subtypes routed to their supertype's lowering.
227pub const SPECIALISATIONS: &[Specialisation] = &[Specialisation {
228    subtype: "IFCTRIANGULATEDIRREGULARNETWORK",
229    supertype: "IFCTRIANGULATEDFACESET",
230    added_attributes: &["Flags"],
231    rationale: "the face-set slots are unchanged; the lowerer checks Flags \
232                and refuses voids, holes and undocumented codes, so only \
233                breakline codes, which leave the triangles unchanged, lower",
234}];
235
236/// A variant within a family that is admitted or refused independently.
237///
238/// [`IMPLEMENTED`] and [`PLANNED`] classify at *family* granularity, which is
239/// too coarse for families whose support depends on how the instance is
240/// authored. `IFCPCURVE` is implemented, but only for some reference-curve
241/// forms; a flat "implemented" claim hides the refusals inside it.
242#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243pub struct Variant {
244    /// The concrete family this variant belongs to; always in [`IMPLEMENTED`].
245    pub family: &'static str,
246    /// The distinguishing condition, as a caller would recognize it.
247    pub variant: &'static str,
248    /// Whether this specific variant lowers or is a typed refusal.
249    pub support: Support,
250    /// Why it is admitted or refused. Refusals name the missing contract.
251    pub rationale: &'static str,
252}
253
254/// Whether a [`Variant`] lowers exactly or reports a typed refusal.
255#[derive(Debug, Clone, Copy, PartialEq, Eq)]
256#[non_exhaustive]
257pub enum Support {
258    /// Lowers exactly, with no approximation.
259    Admitted,
260    /// Reports a typed [`crate::GeometryError::Unsupported`] naming the entity.
261    Refused,
262}
263
264/// Variant-level dispositions for partially supported families.
265///
266/// Every family named here must appear in [`IMPLEMENTED`] and must declare at
267/// least one `Admitted` and one `Refused` variant -- a family with no refusals
268/// is not partial and belongs in `IMPLEMENTED` alone. Enforced by
269/// `tests/lower_dispatch_corpus.rs`.
270pub const PARTIAL: &[Variant] = &[
271    Variant {
272        family: "IFCPCURVE",
273        variant: "reference curve is an IfcPolyline",
274        support: Support::Admitted,
275        rationale: "an ordered 2D point sequence needs no evaluation",
276    },
277    Variant {
278        family: "IFCPCURVE",
279        variant: "reference curve is an IfcIndexedPolyCurve with no explicit \
280                  Segments, or only IfcLineIndex segments",
281        support: Support::Admitted,
282        rationale: "reads identically to a plain ordered point sequence",
283    },
284    Variant {
285        family: "IFCPCURVE",
286        variant: "reference curve is an IfcLine, IfcCircle or IfcEllipse \
287                  positioned by an IfcAxis2Placement2D",
288        support: Support::Admitted,
289        rationale: "defining values are read verbatim in the surface's own \
290                    (u, v) domain with no unit conversion",
291    },
292    Variant {
293        family: "IFCPCURVE",
294        variant: "reference conic positioned by an IfcAxis2Placement3D",
295        support: Support::Refused,
296        rationale: "a 3D placement's axis has no meaning in a 2D parameter \
297                    domain; admitting it would require inventing a projection",
298    },
299    Variant {
300        family: "IFCPCURVE",
301        variant: "reference curve is an IfcIndexedPolyCurve with an explicit \
302                  IfcArcIndex segment",
303        support: Support::Admitted,
304        rationale: "a three-point arc composes exactly from a parameter-space \
305                    circumcentre into Circle2 plus a Cartesian trim, mirroring \
306                    the 3D path with no approximation",
307    },
308    Variant {
309        family: "IFCPCURVE",
310        variant: "reference curve is an explicit-knot IfcBSplineCurveWithKnots \
311                  or IfcRationalBSplineCurveWithKnots",
312        support: Support::Admitted,
313        rationale: "every field is dimensionless or a curve parameter; knots \
314                    already pass through the 3D path unscaled, and control \
315                    points are read as raw (u, v) pairs",
316    },
317    Variant {
318        family: "IFCPCURVE",
319        variant: "reference curve is a trimmed or composite curve",
320        support: Support::Admitted,
321        rationale: "trim parameters and segments stay in the surface (u, v) \
322                    domain, unscaled, so no dimensional contract is needed",
323    },
324    Variant {
325        family: "IFCPCURVE",
326        variant: "reference curve is a convention-only IfcBSplineCurve",
327        support: Support::Refused,
328        rationale: "a base spline carries no authored knot vector to preserve",
329    },
330    Variant {
331        family: "IFCDIRECTRIXDERIVEDREFERENCESWEPTAREASOLID",
332        variant: "directrix defines only a tangent (no IfcCurveSegment, \
333                  segment-built or surface curve reachable)",
334        support: Support::Admitted,
335        rationale: "IFC4.3 gives it exactly the behaviour of \
336                    IfcFixedReferenceSweptAreaSolid in this case, so it lowers \
337                    to the same FixedReferenceSweep",
338    },
339    Variant {
340        family: "IFCDIRECTRIXDERIVEDREFERENCESWEPTAREASOLID",
341        variant: "directrix defines a tangent plane",
342        support: Support::Refused,
343        rationale: "kernel: the directrix defines a tangent plane (it is built from \
344                    IfcCurveSegment placements or lies on a surface), so the derived \
345                    reference adds that plane's rotation to FixedReference; the neutral \
346                    FixedReferenceSweep carries only a constant reference direction",
347    },
348    Variant {
349        family: "IFCTRIANGULATEDIRREGULARNETWORK",
350        variant: "every Flags value is a breakline code, 0 to 7",
351        support: Support::Admitted,
352        rationale: "a breakline marks an edge the triangulation already has, \
353                    so the triangle surface is the supertype's; the flags \
354                    are not carried into the mesh",
355    },
356    Variant {
357        family: "IFCTRIANGULATEDIRREGULARNETWORK",
358        variant: "a Flags value is -1 (hole) or -2 (void)",
359        support: Support::Refused,
360        rationale: "the triangle is excluded from the surface, and a hole may \
361                    fall back on another surface; the neutral mesh has no \
362                    face-exclusion or fall-back channel",
363    },
364    Variant {
365        family: "IFCTRIANGULATEDIRREGULARNETWORK",
366        variant: "a Flags value is outside -2 to 7",
367        support: Support::Refused,
368        rationale: "the documentation defines no meaning for it",
369    },
370    Variant {
371        family: "IFCSURFACECURVE",
372        variant: "MasterRepresentation is Curve3D, PCurveS1, or PCurveS2 with \
373                  the named side present",
374        support: Support::Admitted,
375        rationale: "each side pairs a surface with its own p-curve, so the \
376                    neutral MasterRepresentation names S1 and S2 exactly",
377    },
378    Variant {
379        family: "IFCCURVESEGMENT",
380        variant: "ParentCurve is an IfcLine, IfcCircle or 2D IfcPolyline, \
381                  measured by IfcLengthMeasure",
382        support: Support::Admitted,
383        rationale: "a line, arc or polyline cut by arc length and placed rigidly \
384                    is elementary: a polyline or an angle-trimmed circle",
385    },
386    Variant {
387        family: "IFCCURVESEGMENT",
388        variant: "ParentCurve is an IfcSpiral subtype, measured by \
389                  IfcLengthMeasure",
390        support: Support::Admitted,
391        rationale: "the spiral's curvature law, rebased to the segment in closed \
392                    form, on a planar intrinsic curve; nothing is integrated",
393    },
394    Variant {
395        family: "IFCCURVESEGMENT",
396        variant: "SegmentLength is zero (the closing segment of a layout)",
397        support: Support::Admitted,
398        rationale: "its placement exactly: a planar intrinsic curve of length zero",
399    },
400    Variant {
401        family: "IFCCURVESEGMENT",
402        variant: "ParentCurve is a 2D IfcPolynomialCurve with a degree-one \
403                  coordinate, cut forwards from SegmentStart 0",
404        support: Support::Admitted,
405        rationale: "its Bezier over a closed-form parameter bound, placed \
406                    rigidly and trimmed at TrimSelector::ArcLength: the kernel \
407                    inverts the arc length, nothing is integrated here",
408    },
409    Variant {
410        family: "IFCCURVESEGMENT",
411        variant: "ParentCurve is an IfcPolynomialCurve cut from a non-zero \
412                  SegmentStart, walked backwards, 3D, or with no degree-one \
413                  coordinate",
414        support: Support::Refused,
415        rationale: "the placed start point inverts a non-elementary arc-length \
416                    integral, a 3D polynomial has no plane for the placement, \
417                    and without a degree-one coordinate the trim has no \
418                    closed-form parameter bound",
419    },
420    Variant {
421        family: "IFCCURVESEGMENT",
422        variant: "SegmentStart or SegmentLength is an IfcParameterValue",
423        support: Support::Refused,
424        rationale: "SegmentStart/SegmentLength given as IfcParameterValue: IFC4.3 ADD2 defines no parametric \
425         space for IfcCurveSegment parents yet (informal proposition 1 requires IfcLengthMeasure)",
426    },
427    Variant {
428        family: "IFCCURVESEGMENT",
429        variant: "Placement is an IfcAxis2PlacementLinear",
430        support: Support::Refused,
431        rationale: "an IfcAxis2PlacementLinear placement stands at a distance along a basis curve; the neutral \
432         model has no distance-along-curve point relation to anchor it (#307)",
433    },
434    Variant {
435        family: "IFCGRADIENTCURVE",
436        variant: "horizontal IfcCurveSegments over lines, arcs, spirals and \
437                  2D IfcPolynomialCurves; vertical IfcCurveSegments over \
438                  IfcLine, IfcCircle, an IfcSpiral subtype, or a degree-2 \
439                  IfcPolynomialCurve that keeps its start tangent",
440        support: Support::Admitted,
441        rationale: "one plan parameterised by arc length (an intrinsic curve, \
442                    or an arc-length chain with a polynomial piece) plus a \
443                    piecewise elevation law of polynomial, circular and \
444                    intrinsic pieces: Curve3::Elevated, exact",
445    },
446    Variant {
447        family: "IFCGRADIENTCURVE",
448        variant: "a vertical parabola or spiral with no following segment, \
449                  closing segment or EndPoint",
450        support: Support::Refused,
451        rationale: "its plan extent inverts a non-elementary arc-length \
452                    integral, and nothing states it",
453    },
454    Variant {
455        family: "IFCGRADIENTCURVE",
456        variant: "a heading kink, a closed-form position gap, or a profile \
457                  that does not span the base curve",
458        support: Support::Refused,
459        rationale: "one plan curve and one elevation law cannot carry a kink or \
460                    a gap, and an elevation law must cover the whole plan",
461    },
462    Variant {
463        family: "IFCGRADIENTCURVE",
464        variant: "placed by a frame that tilts, scales or mirrors the vertical",
465        support: Support::Refused,
466        rationale: "a plan plus a height is carried only by frames that keep \
467                    the vertical axis",
468    },
469    Variant {
470        family: "IFCSURFACECURVE",
471        variant: "MasterRepresentation is PCurveS2 with only one associated \
472                  p-curve",
473        support: Support::Refused,
474        rationale: "the master names a parametric side the curve does not \
475                    have; the schema calls this inconsistent, so it is \
476                    refused rather than resolved to the remaining p-curve",
477    },
478];
479
480/// Lower any representation item into the caller's session.
481///
482/// Returns the node for implemented families and a typed
483/// [`crate::GeometryError::Unsupported`] naming the source entity otherwise.
484pub fn lower_representation_item(
485    session: &mut LoweringSession<'_>,
486    id: EntityId,
487    frame: Transform,
488) -> GeometryResult<NodeId> {
489    let type_name = session.type_name(id)?;
490    // IFC4X3 sectioned surface, routed before the inheritance test so the
491    // named refusal holds whether or not the subtype table knows the type.
492    if type_name == "IFCSECTIONEDSURFACE" {
493        return lower_sectioned_surface_node(session, id);
494    }
495    // An exact specialisation lowers through its supertype's arm. The
496    // refusal below still names the entity's own type.
497    let routed = SPECIALISATIONS
498        .iter()
499        .find(|row| row.subtype == type_name)
500        .map_or(type_name.as_str(), |row| row.supertype);
501    // Shape representations may legitimately contain bare curve and surface
502    // items (Curve2D/Curve3D/SurfaceModel). Route by generated IFC inheritance
503    // before the concrete solid table so plan and surface selections lower
504    // through the same total entry point as body geometry.
505    if is_a(&type_name, "IFCCURVE") {
506        return lower_curve_node(session, id, frame);
507    }
508    if is_a(&type_name, "IFCSURFACE") {
509        return lower_surface_node(session, id, frame);
510    }
511    match routed {
512        "IFCEXTRUDEDAREASOLID" => lower_extruded_area_solid_node(session, id, frame),
513        "IFCREVOLVEDAREASOLID" => lower_revolved_area_solid_node(session, id, frame),
514        "IFCBOOLEANRESULT" | "IFCBOOLEANCLIPPINGRESULT" => {
515            lower_boolean_result_node(session, id, frame)
516        }
517        "IFCHALFSPACESOLID" | "IFCBOXEDHALFSPACE" | "IFCPOLYGONALBOUNDEDHALFSPACE" => {
518            lower_half_space_node(session, id, frame)
519        }
520        "IFCMAPPEDITEM" => lower_mapped_item_node(session, id, frame),
521        // `IfcCurveSegment` is an IfcSegment, not an IfcCurve, so the
522        // inheritance test above does not route it; the curve module lowers
523        // it. The IFC4X3 curves themselves route by inheritance (#293).
524        "IFCCURVESEGMENT" => lower_curve_node(session, id, frame),
525        "IFCPOINTONCURVE" => lower_point_on_curve_node(session, id, frame),
526        "IFCPOINTONSURFACE" => lower_point_on_surface_node(session, id, frame),
527        "IFCFACETEDBREP"
528        | "IFCFACETEDBREPWITHVOIDS"
529        | "IFCADVANCEDBREP"
530        | "IFCADVANCEDBREPWITHVOIDS" => lower_faceted_brep_node(session, id, frame),
531        "IFCFACESURFACE" | "IFCADVANCEDFACE" => lower_face_surface_node(session, id, frame),
532        "IFCTRIANGULATEDFACESET" => lower_triangulated_face_set_node(session, id, frame),
533        "IFCPOLYGONALFACESET" => lower_polygonal_face_set_node(session, id, frame),
534        "IFCCSGSOLID" => lower_csg_solid_node(session, id, frame),
535        "IFCSWEPTDISKSOLID" | "IFCSWEPTDISKSOLIDPOLYGONAL" => {
536            lower_swept_disk_node(session, id, frame)
537        }
538        "IFCSURFACECURVESWEPTAREASOLID" => {
539            lower_surface_curve_swept_area_solid_node(session, id, frame)
540        }
541        "IFCBOUNDINGBOX" => lower_bounding_box_node(session, id, frame),
542        "IFCEXTRUDEDAREASOLIDTAPERED" => lower_tapered_extrusion_node(session, id, frame),
543        "IFCREVOLVEDAREASOLIDTAPERED" => lower_tapered_revolution_node(session, id, frame),
544        "IFCFIXEDREFERENCESWEPTAREASOLID" => lower_fixed_reference_sweep_node(session, id, frame),
545        "IFCDIRECTRIXDERIVEDREFERENCESWEPTAREASOLID" => {
546            lower_directrix_derived_reference_sweep_node(session, id, frame)
547        }
548        "IFCSECTIONEDSOLIDHORIZONTAL" => lower_sectioned_solid_horizontal_node(session, id),
549        "IFCSECTIONEDSPINE" => lower_sectioned_spine_node(session, id, frame),
550        "IFCSHELLBASEDSURFACEMODEL"
551        | "IFCFACEBASEDSURFACEMODEL"
552        | "IFCGEOMETRICSET"
553        | "IFCGEOMETRICCURVESET" => lower_collection_node(session, id, frame),
554        "IFCBLOCK"
555        | "IFCSPHERE"
556        | "IFCRIGHTCIRCULARCYLINDER"
557        | "IFCRIGHTCIRCULARCONE"
558        | "IFCRECTANGULARPYRAMID" => lower_csg_primitive_node(session, id, frame),
559        _ => Err(session.unsupported(id, &type_name, detail_for(&type_name))),
560    }
561}
562
563/// The documented reason a recognized family is not lowered yet.
564fn detail_for(type_name: &str) -> &'static str {
565    planned_detail(type_name).unwrap_or("representation item family is not lowered yet")
566}
567
568/// The documented [`PLANNED`] reason for a family, if it has one.
569///
570/// Shared with the curve lowerer: once the subtype table routes
571/// an IFC4X3 curve there by inheritance (#293), the ledger's reason must
572/// still be the one reported.
573pub(crate) fn planned_detail(type_name: &str) -> Option<&'static str> {
574    PLANNED
575        .iter()
576        .find(|(name, _)| *name == type_name)
577        .map(|(_, detail)| *detail)
578}
579
580#[cfg(test)]
581mod tests {
582    use super::*;
583
584    #[test]
585    fn implemented_and_planned_families_do_not_overlap() {
586        for name in IMPLEMENTED {
587            assert!(
588                !PLANNED.iter().any(|(planned, _)| planned == name),
589                "{name} is listed as both implemented and planned"
590            );
591        }
592    }
593
594    #[test]
595    fn every_planned_family_states_a_concrete_reason() {
596        for (name, detail) in PLANNED {
597            assert!(!detail.is_empty(), "{name} has no stated reason");
598            assert_ne!(
599                *detail, "unsupported",
600                "{name} must say what specifically is missing"
601            );
602        }
603    }
604}