1use 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
57pub 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 "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 "IFCDIRECTRIXDERIVEDREFERENCESWEPTAREASOLID",
93 "IFCSECTIONEDSPINE",
94 "IFCSHELLBASEDSURFACEMODEL",
95 "IFCFACEBASEDSURFACEMODEL",
96 "IFCFACESURFACE",
99 "IFCADVANCEDFACE",
100 "IFCGEOMETRICSET",
101 "IFCGEOMETRICCURVESET",
102 "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 "IFCCURVESEGMENT",
137 "IFCGRADIENTCURVE",
138];
139
140pub const PLANNED: &[(&str, &str)] = &[
155 ("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 (
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 (
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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub struct Specialisation {
216 pub subtype: &'static str,
218 pub supertype: &'static str,
220 pub added_attributes: &'static [&'static str],
222 pub rationale: &'static str,
224}
225
226pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243pub struct Variant {
244 pub family: &'static str,
246 pub variant: &'static str,
248 pub support: Support,
250 pub rationale: &'static str,
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
256#[non_exhaustive]
257pub enum Support {
258 Admitted,
260 Refused,
262}
263
264pub 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
480pub 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 if type_name == "IFCSECTIONEDSURFACE" {
493 return lower_sectioned_surface_node(session, id);
494 }
495 let routed = SPECIALISATIONS
498 .iter()
499 .find(|row| row.subtype == type_name)
500 .map_or(type_name.as_str(), |row| row.supertype);
501 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" => 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
563fn detail_for(type_name: &str) -> &'static str {
565 planned_detail(type_name).unwrap_or("representation item family is not lowered yet")
566}
567
568pub(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}