Warn on shared-rep parametric edits

A user clicking the pen icon on a typed-product occurrence whose body
representation is mapped from its type would silently mutate every
sibling occurrence's geometry. Add a confirmation dialog at the pen-icon
dispatcher (the single chokepoint every feature routes through) showing
the sibling count, with a session-scoped suppress checkbox.

The check is read-only: tool.Model.get_sibling_occurrence_count wraps
tool.Geometry.get_elements_by_representation against the resolved body
rep and subtracts self + type. A forward-compat AST guard pins the
dispatcher monopoly so any future feature that binds pen_gizmo directly
to a feature-specific enable op fails the test before merge.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Gorgious56
2026-06-09 17:32:44 +02:00
parent 784f0b1fe2
commit b22687891b
7 changed files with 339 additions and 0 deletions
+85
View File
@@ -934,3 +934,88 @@ class TestOffsetWall(NewFile):
usage.DirectionSense = "NEGATIVE"
subject.offset_wall(obj, "EXTERIOR")
assert usage.OffsetFromReferenceLine == 100
class TestGetSiblingOccurrenceCount(NewFile):
"""The pen-icon dispatcher's pre-edit warning depends on this count: zero
means the edit is safe (unique geometry), non-zero means the edit will
silently mutate other instances sharing the same resolved body rep."""
def _make_body_subcontext(self, ifc: ifcopenshell.file) -> ifcopenshell.entity_instance:
import ifcopenshell.api.context
ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcProject", name="Project")
parent = ifcopenshell.api.context.add_context(ifc, context_type="Model")
return ifcopenshell.api.context.add_context(
ifc,
context_type="Model",
context_identifier="Body",
target_view="MODEL_VIEW",
parent=parent,
)
def _create_wall_with_body_rep(
self,
ifc: ifcopenshell.file,
body_subcontext: ifcopenshell.entity_instance,
name: str = "Wall",
) -> ifcopenshell.entity_instance:
wall = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcWall", name=name)
rep = ifc.createIfcShapeRepresentation(
ContextOfItems=body_subcontext,
RepresentationIdentifier="Body",
RepresentationType="SweptSolid",
Items=[ifc.createIfcExtrudedAreaSolid()],
)
ifcopenshell.api.geometry.assign_representation(ifc, product=wall, representation=rep)
return wall
def test_returns_zero_when_element_has_no_body_representation(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
wall = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcWall")
assert subject.get_sibling_occurrence_count(wall) == 0
def test_returns_zero_when_element_has_unique_body_representation(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
body = self._make_body_subcontext(ifc)
wall = self._create_wall_with_body_rep(ifc, body)
assert subject.get_sibling_occurrence_count(wall) == 0
def test_returns_sibling_count_excluding_self_and_type(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
body = self._make_body_subcontext(ifc)
wall_type = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcWallType", name="WAL01")
type_rep = ifc.createIfcShapeRepresentation(
ContextOfItems=body,
RepresentationIdentifier="Body",
RepresentationType="SweptSolid",
Items=[ifc.createIfcExtrudedAreaSolid()],
)
ifcopenshell.api.geometry.assign_representation(ifc, product=wall_type, representation=type_rep)
occurrences = [ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcWall", name=f"Wall{i}") for i in range(3)]
ifcopenshell.api.type.assign_type(ifc, related_objects=occurrences, relating_type=wall_type)
assert subject.get_sibling_occurrence_count(occurrences[0]) == 2
assert subject.get_sibling_occurrence_count(occurrences[1]) == 2
assert subject.get_sibling_occurrence_count(occurrences[2]) == 2
def test_type_with_occurrences_reports_its_occurrence_count(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
body = self._make_body_subcontext(ifc)
wall_type = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcWallType", name="WAL01")
type_rep = ifc.createIfcShapeRepresentation(
ContextOfItems=body,
RepresentationIdentifier="Body",
RepresentationType="SweptSolid",
Items=[ifc.createIfcExtrudedAreaSolid()],
)
ifcopenshell.api.geometry.assign_representation(ifc, product=wall_type, representation=type_rep)
occurrences = [ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcWall", name=f"Wall{i}") for i in range(2)]
ifcopenshell.api.type.assign_type(ifc, related_objects=occurrences, relating_type=wall_type)
assert subject.get_sibling_occurrence_count(wall_type) == 2