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