Fix MEP pair-disconnect crash and bend re-edit pen icon

Three user-facing fixes for the MEP-system gizmo surface:

1. MEP pair-disconnect no longer crashes Blender. The
   MEPSystemPathDecorator cached entity_instance references in
   _cached_walk; deleting a bridging fitting via the gizmo left a freed
   SWIG handle in the list, and the next _build_geometry pass segfaulted
   on .is_a. The cache now stores STEP integer ids and re-resolves via
   ifc_file.by_id on each draw, plus folds tool.Parametric.get_geom_generation
   into the cache key — ifcopenshell.api mutations invalidate before the
   next frame regardless of how the deletion was routed.

2. Bend re-edit pen icon stays reachable. The bend creation path
   tessellates the swept-disk body (upstream geometry-kernel workaround),
   so tool.System.has_parametric_body correctly returns False for a
   freshly-committed bend. _active_is_bend_fitting and
   GizmoMEPActions.is_eligible_object now fall back to the type's
   BBIM_Fitting pset — the same source bim.enable_bend_preview_from_bend
   reads parameters from — keeping the pen icon eligible.

3. MEP pair / per-port unjoin icons unified through bim.disconnect_elements.
   The MEP gizmo group's three unjoin icons (pair, start, end) now share
   the wall-disconnect surface: same VIEW3D_GT_wall_link_toggle icon, same
   bim.disconnect_elements operator. tool.Connection.find_rels learned a
   new "mep-pair-fitting" kind that returns the bridging fitting as the
   disconnect target; core.connection.disconnect_rel grew the matching
   dispatch arm. The old MEPUnjoinAtPort and MEPUnjoinPair operators are
   removed.

Also registered wall.GizmoPairDisconnect (previously declared but never
in the classes tuple, so dead code) for the wall+slab pair-disconnect
surface, and extracted MEP port-topology helpers (find_bridging_fitting,
is_disconnectable_fitting, neighbours_at_ports) onto tool.System so the
canonical walk has a single home.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Gorgious56
2026-06-23 14:16:33 +02:00
parent 262f5f9a85
commit 6fa984ce2b
14 changed files with 1102 additions and 398 deletions
+127
View File
@@ -23,6 +23,7 @@ import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.root
import ifcopenshell.api.system
import ifcopenshell.util.representation
import ifcopenshell.util.system
import ifcopenshell.util.unit
import numpy as np
@@ -39,6 +40,132 @@ class TestImplementsTool(NewFile):
assert isinstance(subject(), bonsai.core.tool.System)
class TestHasParametricBody(NewFile):
"""The MEP-action gizmo predicates gate on ``has_parametric_body``;
fittings whose swept body lives on the type via ``IfcMappedItem`` must
return True so the pen-icon and lock-icon rows show on the occurrence."""
def _build_bend_occurrence_with_mapped_body(self):
bpy.ops.bim.create_project()
ifc_file = tool.Ifc.get()
body_ctx = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
placement = ifc_file.create_entity(
"IfcAxis2Placement3D",
Location=ifc_file.create_entity("IfcCartesianPoint", Coordinates=(0.0, 0.0, 0.0)),
)
line = ifc_file.create_entity(
"IfcLine",
Pnt=ifc_file.create_entity("IfcCartesianPoint", Coordinates=(0.0, 0.0, 0.0)),
Dir=ifc_file.create_entity(
"IfcVector",
Orientation=ifc_file.create_entity("IfcDirection", DirectionRatios=(1.0, 0.0, 0.0)),
Magnitude=1.0,
),
)
trimmed = ifc_file.create_entity(
"IfcTrimmedCurve",
BasisCurve=line,
Trim1=(ifc_file.create_entity("IfcParameterValue", wrappedValue=0.0),),
Trim2=(ifc_file.create_entity("IfcParameterValue", wrappedValue=1.0),),
SenseAgreement=True,
MasterRepresentation="PARAMETER",
)
swept = ifc_file.create_entity("IfcSweptDiskSolid", Directrix=trimmed, Radius=0.05)
type_body = ifc_file.create_entity(
"IfcShapeRepresentation",
ContextOfItems=body_ctx,
RepresentationIdentifier="Body",
RepresentationType="AdvancedSweptSolid",
Items=(swept,),
)
rep_map = ifc_file.create_entity(
"IfcRepresentationMap", MappingOrigin=placement, MappedRepresentation=type_body
)
fitting_type = ifc_file.create_entity(
"IfcPipeFittingType",
GlobalId=ifcopenshell.guid.new(),
Name="BendType",
PredefinedType="BEND",
RepresentationMaps=(rep_map,),
)
mapped_item = ifc_file.create_entity(
"IfcMappedItem",
MappingSource=rep_map,
MappingTarget=ifc_file.create_entity(
"IfcCartesianTransformationOperator3D",
LocalOrigin=ifc_file.create_entity("IfcCartesianPoint", Coordinates=(0.0, 0.0, 0.0)),
),
)
occurrence_body = ifc_file.create_entity(
"IfcShapeRepresentation",
ContextOfItems=body_ctx,
RepresentationIdentifier="Body",
RepresentationType="MappedRepresentation",
Items=(mapped_item,),
)
fitting = ifc_file.create_entity(
"IfcPipeFitting",
GlobalId=ifcopenshell.guid.new(),
Name="Bend",
PredefinedType="BEND",
Representation=ifc_file.create_entity("IfcProductDefinitionShape", Representations=(occurrence_body,)),
)
ifc_file.create_entity(
"IfcRelDefinesByType",
GlobalId=ifcopenshell.guid.new(),
RelatedObjects=(fitting,),
RelatingType=fitting_type,
)
return fitting
def test_returns_true_for_swept_disk_via_mapped_item(self):
"""``traverse()`` follows the
``IfcMappedItem.MappingSource.MappedRepresentation`` chain so the
``IfcSweptDiskSolid`` on the type's body is reachable from the
occurrence's body representation. Bend fittings produced by the
bend-preview commit path use this exact representation shape."""
fitting = self._build_bend_occurrence_with_mapped_body()
assert subject.has_parametric_body(fitting) is True
def test_returns_false_for_tessellated_body(self):
"""The bend creation path replaces the swept-disk body with an
``IfcTriangulatedFaceSet`` as an upstream geometry-kernel
workaround. The traverse finds no extruded / swept solid, so the
predicate returns False — pinning the constraint that drives the
``BBIM_Fitting`` pset fallback in the bend-icon visibility
predicate."""
bpy.ops.bim.create_project()
ifc_file = tool.Ifc.get()
body_ctx = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
coords = ifc_file.create_entity(
"IfcCartesianPointList3D",
CoordList=((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)),
)
tessellation = ifc_file.create_entity(
"IfcTriangulatedFaceSet",
Coordinates=coords,
CoordIndex=((1, 2, 3),),
)
body = ifc_file.create_entity(
"IfcShapeRepresentation",
ContextOfItems=body_ctx,
RepresentationIdentifier="Body",
RepresentationType="Tessellation",
Items=(tessellation,),
)
fitting = ifc_file.create_entity(
"IfcPipeFitting",
GlobalId=ifcopenshell.guid.new(),
Name="TessellatedBend",
PredefinedType="BEND",
Representation=ifc_file.create_entity("IfcProductDefinitionShape", Representations=(body,)),
)
assert subject.has_parametric_body(fitting) is False
class TestAddPorts(NewFile):
def setup_mep_segment(self):
bpy.ops.bim.create_project()