Bonsai: report a clean error instead of a traceback for unsupported bend profiles

FitFlowSegments calls bpy.ops.bim.mep_add_bend/transition/obstruction as
nested operator invocations. Blender re-raises any ERROR-level report from
a nested bpy.ops.X() call as a RuntimeError to the Python caller, so a
clean self.report + CANCELLED from those nested operators (e.g. "profile
class not supported for a bend") was surfacing as an uncaught exception
instead. That exception then propagated further through the S,Y hotkey,
triggering Bonsai's generic "experienced an error" popup with a full
traceback rather than the intended single-line message.

Catch the RuntimeError at both nesting points (FitFlowSegments's calls to
the mep_add_* operators, and the S,Y hotkey's call to fit_flow_segments)
and re-surface it as a single clean self.report instead of letting it
propagate uncaught.

Fixes #5450.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Petru Conduraru
2026-07-16 13:11:35 +03:00
parent 821cf7b671
commit 08b1cb3004
2 changed files with 41 additions and 4 deletions
+29 -3
View File
@@ -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:
@@ -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")