mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-09 21:53:40 +00:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user