Hide MEP gizmos on non-parametric elements

MEP elements imported as tessellation / brep (no IfcExtrudedAreaSolid
or IfcSweptDiskSolid in their body representation) can't be
parametrically edited — the gizmos offer affordances the geometry
kernel has no path to honour. tool.System.has_parametric_body
inspects the Model/Body/MODEL_VIEW representation and returns True
only when at least one item resolves to one of the two
profile-sweep primitives.

The gate is wired into:
- GizmoMEPActions.is_eligible_object (the action icon group)
- _active_is_flow_segment / _active_is_bend_fitting visibility
  predicates the icon row consults per-icon
- GizmoPipeSegmentEdition / GizmoDuctSegmentEdition is_element_type

tool.Parametric.is_pipe_segment / is_duct_segment stay IFC-class-only
so their truth-table contract test keeps reading a single concern.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Gorgious56
2026-06-10 13:00:01 +02:00
parent 6c9cfccc43
commit 4d24cef0c9
4 changed files with 39 additions and 14 deletions
+10 -7
View File
@@ -2619,7 +2619,7 @@ class GizmoPipeSegmentEdition(bpy.types.GizmoGroup, _MEPSegmentEditionMixin, giz
@classmethod
def is_element_type(cls, element):
return tool.Parametric.is_pipe_segment(element)
return tool.Parametric.is_pipe_segment(element) and tool.System.has_parametric_body(element)
class GizmoDuctSegmentEdition(bpy.types.GizmoGroup, _MEPSegmentEditionMixin, gizmo.BaseParametricGizmoGroup):
@@ -2646,7 +2646,7 @@ class GizmoDuctSegmentEdition(bpy.types.GizmoGroup, _MEPSegmentEditionMixin, giz
@classmethod
def is_element_type(cls, element):
return tool.Parametric.is_duct_segment(element)
return tool.Parametric.is_duct_segment(element) and tool.System.has_parametric_body(element)
# --- GizmoMEPActions group + visibility helpers ----------------------------
@@ -2658,9 +2658,9 @@ def _selection_size() -> int:
def _active_is_flow_segment(obj: bpy.types.Object) -> bool:
element = tool.Ifc.get_entity(obj)
if element is None:
if element is None or not element.is_a("IfcFlowSegment"):
return False
return element.is_a("IfcFlowSegment")
return tool.System.has_parametric_body(element)
def _active_mep_has_connected_neighbor(obj: bpy.types.Object) -> bool:
@@ -2677,7 +2677,10 @@ def _active_mep_has_connected_neighbor(obj: bpy.types.Object) -> bool:
def _active_is_bend_fitting(obj: bpy.types.Object) -> bool:
return _is_bend_fitting(tool.Ifc.get_entity(obj))
element = tool.Ifc.get_entity(obj)
if not _is_bend_fitting(element):
return False
return tool.System.has_parametric_body(element)
class GizmoMEPActions(bpy.types.GizmoGroup, gizmo.BaseIconActionGroup):
@@ -2789,9 +2792,9 @@ class GizmoMEPActions(bpy.types.GizmoGroup, gizmo.BaseIconActionGroup):
if bend_props is not None and bend_props.is_active:
return False
element = tool.Ifc.get_entity(obj)
if element is None:
if element is None or not tool.System.is_mep_element(element):
return False
return tool.System.is_mep_element(element)
return tool.System.has_parametric_body(element)
def setup(self, context: bpy.types.Context) -> None:
super().setup(context)
+18
View File
@@ -488,6 +488,24 @@ class System(bonsai.core.tool.System):
def is_mep_element(cls, element: ifcopenshell.entity_instance) -> bool:
return element.is_a("IfcFlowSegment") or element.is_a("IfcFlowFitting")
@classmethod
def has_parametric_body(cls, element: ifcopenshell.entity_instance) -> bool:
"""True when the MEP element's body representation is a profile sweep
(``IfcExtrudedAreaSolid`` for segments, ``IfcSweptDiskSolid`` for
fittings) — the shape the parametric edit + MEP action gizmos can
actually mutate. Tessellation- or brep-imported MEP elements return
False so their gizmos hide rather than offer edits the geometry
kernel can't honour."""
import bonsai.tool as tool
body = tool.Geometry.get_body_representation(element)
if body is None:
return False
for item in tool.Ifc.get().traverse(body):
if item.is_a("IfcExtrudedAreaSolid") or item.is_a("IfcSweptDiskSolid"):
return True
return False
@classmethod
def walk_connected_mep_elements(
cls, start_element: ifcopenshell.entity_instance
@@ -214,7 +214,9 @@ def test_active_is_flow_segment_handles_unbound_object():
def test_active_is_flow_segment_classifies_segment_vs_fitting():
"""Only IfcFlowSegment lights the lock-icon row; IfcFlowFitting (the
bend's own class) does not."""
bend's own class) does not. The parametric-body gate is mocked True
here — its dedicated truth-table is in test_mep_actions_visibility
sibling tests."""
from bonsai.bim.module.model.mep import _active_is_flow_segment
segment_elem = Mock()
@@ -223,10 +225,11 @@ def test_active_is_flow_segment_classifies_segment_vs_fitting():
fitting_elem.is_a = lambda c: c == "IfcFlowFitting"
plain = Mock()
with patch("bonsai.bim.module.model.mep.tool.Ifc.get_entity", return_value=segment_elem):
assert _active_is_flow_segment(plain) is True
with patch("bonsai.bim.module.model.mep.tool.Ifc.get_entity", return_value=fitting_elem):
assert _active_is_flow_segment(plain) is False
with patch("bonsai.bim.module.model.mep.tool.System.has_parametric_body", return_value=True):
with patch("bonsai.bim.module.model.mep.tool.Ifc.get_entity", return_value=segment_elem):
assert _active_is_flow_segment(plain) is True
with patch("bonsai.bim.module.model.mep.tool.Ifc.get_entity", return_value=fitting_elem):
assert _active_is_flow_segment(plain) is False
def test_active_mep_has_connected_neighbor_returns_false_on_no_entity():
@@ -170,11 +170,12 @@ def test_gizmo_group_class_wiring(gizmo_cls_name, bl_idname, is_element_predicat
cls = getattr(mep, gizmo_cls_name)
assert cls.bl_idname == bl_idname
# The element_type predicate must delegate to the matching tool.Parametric.is_*.
predicate = getattr(tool.Parametric, is_element_predicate)
fake_element = Mock()
fake_element.is_a.return_value = True
with patch.object(tool.Parametric, is_element_predicate, side_effect=predicate) as p:
with patch.object(tool.Parametric, is_element_predicate, side_effect=predicate) as p, patch.object(
tool.System, "has_parametric_body", return_value=True
):
cls.is_element_type(fake_element)
assert p.called, f"{gizmo_cls_name}.is_element_type did not delegate to Parametric.{is_element_predicate}"