From 1af5cc9bbe5e81814e1fb744f6fa77b41ba869b2 Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Thu, 13 Aug 2026 12:55:24 -0700 Subject: [PATCH] Allows key point referents to be nested to the parent alignment in the reusing horizontal scenario --- .../alignment/update_key_point_referents.py | 32 ++++++++++++----- .../test_update_key_point_referents.py | 36 +++++++++++++++++++ 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/update_key_point_referents.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/update_key_point_referents.py index 9ad933ef26..53fbe33a6c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/update_key_point_referents.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/update_key_point_referents.py @@ -115,11 +115,19 @@ def update_key_point_referents( get_stationing_nest) -- key-point referents never belong in either of those. :param layout: IfcAlignmentHorizontal, IfcAlignmentVertical, or IfcAlignmentCant - :param rel_nests: an existing IfcRelNests to (re)populate; its RelatingObject must be the - IfcAlignment that nests `layout` (TypeError is raised otherwise). If omitted, a new - IfcRelNests is always created and related to that IfcAlignment -- there is no implicit - search for or reuse of a previously created nest. Callers who want to regenerate into an - existing nest must pass it back in explicitly via `rel_nests`. + :param rel_nests: an existing IfcRelNests to (re)populate; its RelatingObject must be an + IfcAlignment (TypeError is raised otherwise), but need not be the IfcAlignment that + directly nests `layout` -- passing an ancestor's own IfcRelNests is supported + specifically so that a vertical/cant layout living under a child IfcAlignment (per CT + 4.1.4.4.1.2, once a second vertical layout is added) can still have its key-point + referents named after and nested to the top-level parent alignment, matching how the + alignment's horizontal key points are named, rather than a generic "Child of X" name. + When `rel_nests` is given, `rel_nests.RelatingObject` -- not `layout`'s own direct + parent -- is used for both the created referents' Name and the returned IfcRelNests. If + omitted, a new IfcRelNests is always created and related to `layout`'s own direct + parent alignment -- there is no implicit search for or reuse of a previously created + nest. Callers who want to regenerate into an existing nest must pass it back in + explicitly via `rel_nests`. :param clear: if True, deletes all IfcReferent currently in rel_nests.RelatedObjects (and their Pset_Stationing) before regenerating. If False (default), new referents are appended to whatever already exists -- no deduplication. @@ -155,17 +163,25 @@ def update_key_point_referents( f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received {layout.is_a()}" ) - alignment = ifcopenshell.api.alignment.get_alignment(layout) - if alignment is None: + layout_alignment = ifcopenshell.api.alignment.get_alignment(layout) + if layout_alignment is None: raise ValueError(f"{layout.is_a()} #{layout.id()} is not nested under an IfcAlignment.") + # `alignment` is used below for referent naming (and as the fallback-placement basis) -- + # it defaults to layout's own direct parent, but an explicitly passed rel_nests overrides + # it with rel_nests.RelatingObject instead (see the rel_nests docstring above). Station + # computation always uses layout_alignment, unaffected by this -- get_alignment_start_station + # already walks up to the true top-level alignment's own stationing referent regardless of + # which (possibly child) alignment it's given. if rel_nests is not None: if not rel_nests.RelatingObject.is_a("IfcAlignment"): raise TypeError( f"Expected rel_nests.RelatingObject to be IfcAlignment, instead received " f"{rel_nests.RelatingObject.is_a()}" ) + alignment = rel_nests.RelatingObject else: + alignment = layout_alignment rel_nests = file.createIfcRelNests( GlobalId=ifcopenshell.guid.new(), RelatingObject=alignment, RelatedObjects=() ) @@ -185,7 +201,7 @@ def update_key_point_referents( ) return rel_nests - start_station = ifcopenshell.api.alignment.get_alignment_start_station(file, alignment) + start_station = ifcopenshell.api.alignment.get_alignment_start_station(file, layout_alignment) curve = ifcopenshell.api.alignment.get_layout_curve(layout) is_horizontal = layout.is_a("IfcAlignmentHorizontal") diff --git a/src/ifcopenshell-python/test/api/alignment/test_update_key_point_referents.py b/src/ifcopenshell-python/test/api/alignment/test_update_key_point_referents.py index 5e9984fb2e..9a6bf436a3 100644 --- a/src/ifcopenshell-python/test/api/alignment/test_update_key_point_referents.py +++ b/src/ifcopenshell-python/test/api/alignment/test_update_key_point_referents.py @@ -373,6 +373,41 @@ def test_start_station_composes_for_child_alignment(): assert stations == pytest.approx([100.0, 600.0, 900.0]) +def test_rel_nests_from_ancestor_used_for_naming_and_nesting(): + """A vertical layout living under a child alignment (once a second vertical layout is + added, per CT 4.1.4.4.1.2) can still have its key-point referents named after and nested + to an ancestor alignment's own rel_nests -- e.g. the same one already holding that + ancestor's horizontal key points -- rather than the child's generic "Child of X" name.""" + file = _new_file() + alignment = ifcopenshell.api.alignment.create(file, "A1", include_vertical=False, start_station=100.0) + horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment) + horizontal_nest = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal) + horizontal_count = len(horizontal_nest.RelatedObjects) + + ifcopenshell.api.alignment.add_vertical_layout(file, alignment) + ifcopenshell.api.alignment.add_vertical_layout(file, alignment) # forces the child-alignment split + child_alignment = alignment.IsDecomposedBy[0].RelatedObjects[-1] + child_vertical = ifcopenshell.api.alignment.get_vertical_layout(child_alignment) + + dp = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=500.0, + StartHeight=10.0, + StartGradient=0.01, + EndGradient=0.01, + PredefinedType="CONSTANTGRADIENT", + ) + ifcopenshell.api.alignment.create_layout_segment(file, child_vertical, dp) + + result = ifcopenshell.api.alignment.update_key_point_referents(file, child_vertical, rel_nests=horizontal_nest) + + assert result == horizontal_nest + assert result.RelatingObject == alignment + assert len(result.RelatedObjects) == horizontal_count + 2 + assert all(r.Name.startswith("A1 ") for r in result.RelatedObjects) + assert not any("Child of" in r.Name for r in result.RelatedObjects) + + def test_returns_ifc_rel_nests(): file = _new_file() alignment = _build_alignment(file) @@ -399,4 +434,5 @@ test_cant_layout_boundary_labels() test_no_real_segments_produces_no_referents() test_single_real_segment_produces_only_boundary_labels() test_start_station_composes_for_child_alignment() +test_rel_nests_from_ancestor_used_for_naming_and_nesting() test_returns_ifc_rel_nests()