diff --git a/src/bonsai/bonsai/bim/module/model/mep.py b/src/bonsai/bonsai/bim/module/model/mep.py index 2a906d4ec4..c4b1231179 100644 --- a/src/bonsai/bonsai/bim/module/model/mep.py +++ b/src/bonsai/bonsai/bim/module/model/mep.py @@ -159,6 +159,29 @@ class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator): bl_label = "Fit Flow Segments" bl_options = {"REGISTER", "UNDO"} + def _call_fitting_op(self, op) -> bool: + """Invoke a nested ``bim.mep_add_*`` operator, converting its reported + error into a clean ``self.report`` instead of letting it surface as a + raw developer traceback. + + Blender raises a ``RuntimeError`` back to the Python caller whenever an + operator invoked via ``bpy.ops.X()`` reports an ``ERROR`` (e.g. an + unsupported profile class for a bend), regardless of the nested + operator's own clean ``self.report`` + ``CANCELLED``. Left uncaught, + that exception bubbles all the way up to whatever ultimately called + ``bim.fit_flow_segments`` (e.g. a hotkey), producing Bonsai's generic + "experienced an error" popup with a full traceback (see #5450) instead + of the single-line message the nested operator already produced. + + Returns True on success, False if the nested operator reported an + error (already surfaced via ``self.report``).""" + try: + op() + except RuntimeError as e: + self.report({"ERROR"}, str(e).removeprefix("Error: ").strip()) + return False + return True + def _execute(self, context): # TODO: need to add ui for parameters: # - obstruction cap thickness @@ -184,7 +207,8 @@ class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator): if total_selected_objs == 1: fitting_type = "OBSTRUCTION" - bpy.ops.bim.mep_add_obstruction() + if not self._call_fitting_op(bpy.ops.bim.mep_add_obstruction): + return elif total_selected_objs == 2: # Shorten the axis by the profile size to allow for fuzzy intersections @@ -211,7 +235,8 @@ class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator): is_on_axis2 = tool.Cad.is_point_on_edge(intersect2, axis2) if not is_on_axis1 and not is_on_axis2: fitting_type = "BEND" - bpy.ops.bim.mep_add_bend() + if not self._call_fitting_op(bpy.ops.bim.mep_add_bend): + return elif is_on_axis1 and is_on_axis2: fitting_type = "CROSS" else: @@ -219,7 +244,8 @@ class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator): elif total_profiles == 2: if is_parallel: fitting_type = "TRANSITION" - bpy.ops.bim.mep_add_transition() + if not self._call_fitting_op(bpy.ops.bim.mep_add_transition): + return elif total_selected_objs == 3: if total_profiles > 1: diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index abe4f45113..2593688796 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -1411,7 +1411,18 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): if not bpy.context.selected_objects: return if self.active_class in ("IfcDuctSegment", "IfcPipeSegment", "IfcCableCarrierSegment", "IfcCableSegment"): - bpy.ops.bim.fit_flow_segments() + # bim.fit_flow_segments() reports a clean single-line ERROR (e.g. + # an unsupported profile class for a bend, see #5450) when it + # can't build a fitting. Blender re-raises any ERROR-level report + # from a nested bpy.ops.X() call as a RuntimeError to the Python + # caller, so without this guard that clean message would surface + # as an uncaught exception here, producing Bonsai's generic + # "experienced an error" popup with a full traceback instead of + # the intended one-line message. + try: + bpy.ops.bim.fit_flow_segments() + except RuntimeError as e: + self.report({"ERROR"}, str(e).removeprefix("Error: ").strip()) elif self.active_material_usage == "PROFILE": bpy.ops.bim.extend_profile(join_type="V")