From 048242783eafe097eb9733eee9c99a4ded235682 Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Wed, 5 Aug 2026 07:26:27 -0700 Subject: [PATCH] Updates update_key_point_referents to confirm to CT 4.1.4.4.3 --- .../alignment/update_key_point_referents.py | 42 ++++++++----------- .../test_update_key_point_referents.py | 38 ++++++++++++++--- 2 files changed, 50 insertions(+), 30 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 b339942bda..6d5bd37ac0 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 @@ -32,21 +32,6 @@ from ifcopenshell.api.alignment._sort_nest import _sort_nest from ifcopenshell.api.alignment.update_fallback_position import update_fallback_position -def _get_key_point_referent_nest(layout: entity_instance) -> Optional[entity_instance]: - """ - Searches layout.IsNestedBy for the IfcRelNests whose RelatedObjects are IfcReferent. - - This is distinct from both get_stationing_nest (scoped to the parent IfcAlignment, and - specifically the STATION/station-equation nest) and get_alignment_segment_nest (the *segment* - nest that also lives on layout.IsNestedBy, holding IfcAlignmentSegment, never IfcReferent). - """ - for nest in layout.IsNestedBy: - for related_object in nest.RelatedObjects: - if related_object.is_a("IfcReferent"): - return nest - return None - - def _remove_referent(file: ifcopenshell.file, referent: entity_instance) -> None: """Cleanly deletes a key-point IfcReferent: its Pset_Stationing, its ObjectPlacement (if exclusively owned by it), and finally the referent itself.""" @@ -129,9 +114,11 @@ 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. May live anywhere (e.g. the parent - IfcAlignment, the layout, or elsewhere) -- the caller decides. If omitted, an existing - referent-nest already on `layout` is reused, or a new one is created and related to `layout`. + :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 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. @@ -167,12 +154,20 @@ def update_key_point_referents( f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received {layout.is_a()}" ) - if rel_nests is None: - rel_nests = _get_key_point_referent_nest(layout) - if rel_nests is None: - rel_nests = file.createIfcRelNests( - GlobalId=ifcopenshell.guid.new(), RelatingObject=layout, RelatedObjects=() + alignment = ifcopenshell.api.alignment.get_alignment(layout) + if alignment is None: + raise ValueError(f"{layout.is_a()} #{layout.id()} is not nested under an IfcAlignment.") + + 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()}" ) + else: + rel_nests = file.createIfcRelNests( + GlobalId=ifcopenshell.guid.new(), RelatingObject=alignment, RelatedObjects=() + ) if clear: for referent in list(rel_nests.RelatedObjects): @@ -189,7 +184,6 @@ def update_key_point_referents( ) return rel_nests - alignment = ifcopenshell.api.alignment.get_alignment(layout) start_station = ifcopenshell.api.alignment.get_alignment_start_station(file, 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 f58dc67443..d914763ddd 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 @@ -82,13 +82,13 @@ def test_default_rel_nests_created_when_none_provided(): nest = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal) assert nest.is_a("IfcRelNests") - assert nest.RelatingObject == horizontal + assert nest.RelatingObject == alignment assert nest.id() != segment_nest.id() assert len(nest.RelatedObjects) == 8 assert all(r.is_a("IfcReferent") for r in nest.RelatedObjects) -def test_second_call_without_rel_nests_reuses_existing_nest(): +def test_second_call_without_rel_nests_creates_separate_nest(): file = _new_file() alignment = _build_alignment(file) horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment) @@ -97,18 +97,31 @@ def test_second_call_without_rel_nests_reuses_existing_nest(): nest1 = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal) nest2 = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal) - assert nest1.id() == nest2.id() - assert len(nest2.RelatedObjects) == 16 + assert nest1.id() != nest2.id() + assert len(nest1.RelatedObjects) == 8 + assert len(nest2.RelatedObjects) == 8 segment_count_after = len(ifcopenshell.api.alignment.get_alignment_segment_nest(horizontal).RelatedObjects) assert segment_count_after == segment_count_before +def test_passing_previous_nest_back_in_accumulates(): + file = _new_file() + alignment = _build_alignment(file) + horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment) + + nest1 = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal) + nest2 = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal, rel_nests=nest1) + + assert nest1.id() == nest2.id() + assert len(nest2.RelatedObjects) == 16 + + def test_provided_rel_nests_is_used_as_is(): file = _new_file() alignment = _build_alignment(file) horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment) - # the nest may live anywhere the caller chooses, e.g. hung off the parent IfcAlignment + # rel_nests.RelatingObject must be the IfcAlignment that nests `layout` rel_nests = file.createIfcRelNests(GlobalId=ifcopenshell.guid.new(), RelatingObject=alignment, RelatedObjects=()) result = ifcopenshell.api.alignment.update_key_point_referents(file, horizontal, rel_nests=rel_nests) @@ -118,6 +131,17 @@ def test_provided_rel_nests_is_used_as_is(): assert len(result.RelatedObjects) == 8 +def test_provided_rel_nests_with_wrong_relating_object_raises_type_error(): + file = _new_file() + alignment = _build_alignment(file) + horizontal = ifcopenshell.api.alignment.get_horizontal_layout(alignment) + + rel_nests = file.createIfcRelNests(GlobalId=ifcopenshell.guid.new(), RelatingObject=horizontal, RelatedObjects=()) + + with pytest.raises(TypeError): + ifcopenshell.api.alignment.update_key_point_referents(file, horizontal, rel_nests=rel_nests) + + def test_clear_true_removes_old_referents_and_psets(): file = _new_file() alignment = _build_alignment(file) @@ -356,8 +380,10 @@ def test_returns_ifc_rel_nests(): test_wrong_layout_type_raises_type_error() test_default_rel_nests_created_when_none_provided() -test_second_call_without_rel_nests_reuses_existing_nest() +test_second_call_without_rel_nests_creates_separate_nest() +test_passing_previous_nest_back_in_accumulates() test_provided_rel_nests_is_used_as_is() +test_provided_rel_nests_with_wrong_relating_object_raises_type_error() test_clear_true_removes_old_referents_and_psets() test_clear_false_appends_without_dedup() test_default_horizontal_labels_and_order()