diff --git a/src/bonsai/bonsai/bim/module/model/mep.py b/src/bonsai/bonsai/bim/module/model/mep.py index 39f5699be7..dea4129e7d 100644 --- a/src/bonsai/bonsai/bim/module/model/mep.py +++ b/src/bonsai/bonsai/bim/module/model/mep.py @@ -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) diff --git a/src/bonsai/bonsai/tool/system.py b/src/bonsai/bonsai/tool/system.py index 8d2b421370..29b5223d9b 100644 --- a/src/bonsai/bonsai/tool/system.py +++ b/src/bonsai/bonsai/tool/system.py @@ -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 diff --git a/src/bonsai/test/bim/module/model/test_mep_actions_visibility.py b/src/bonsai/test/bim/module/model/test_mep_actions_visibility.py index 107d47dde9..858009cba1 100644 --- a/src/bonsai/test/bim/module/model/test_mep_actions_visibility.py +++ b/src/bonsai/test/bim/module/model/test_mep_actions_visibility.py @@ -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(): diff --git a/src/bonsai/test/bim/module/model/test_mep_segment_edition.py b/src/bonsai/test/bim/module/model/test_mep_segment_edition.py index 33997028ad..2451ceb7e2 100644 --- a/src/bonsai/test/bim/module/model/test_mep_segment_edition.py +++ b/src/bonsai/test/bim/module/model/test_mep_segment_edition.py @@ -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}"