Bbox dimensions key, DRY array operators, drop dead code

Three concerns sharing the same architectural theme (collapse inline
bbox / edit-state lookups, drop overrides that re-do base-class work):

== Bbox helpers and array operator DRY ==

* tool/blender.py: add a "dimensions" tuple key to both
  get_object_bounding_box and get_object_world_bounding_box return
  dicts. The (max - min) per-axis extent — which callers previously
  computed via local helpers — is now a key alongside min_x / max_x
  / min_point / max_point / center. Distinct from Blender's built-in
  obj.dimensions (which folds object-level scale): the local variant
  is the intrinsic mesh bbox extent; the world variant is the
  matrix_world-applied AABB.

* bim/module/model/array.py: drop the local _bbox_dims helper; the
  two callers now read tool.Blender.get_object_bounding_box["dimensions"]
  directly.

* Rename _parent_geometry_changed -> _array_children_need_rebuild.
  The old name suggested "did the parent change just now", implying
  the function was a parent-edit-finish trigger. It actually runs
  only inside the array-edit-finish path as a drift safety net (the
  upstream-deliberate design — see commit 83d97d7e9 "Fix #7616. Make
  regenerate array an operator instead of an array preference" —
  means the array doesn't auto-regen when its parent geometry edits
  finish). New name matches the call-site phrasing
  ``if X: _wipe_array_children(layers)`` and clarifies that this is
  a children-state check, not a parent-edit trigger.

* Extract _resolve_array_edit_props(context) — returns the active
  object's array props during an active edit lifecycle, or None.
  Collapses the obj-active-then-is-editing prologue (3 lines + return)
  to one resolver call across 4 sites: ToggleArrayMethod.execute,
  AdjustArrayCount.execute, RemoveArrayLayerFromEdit._execute and
  .poll. Each call site shrinks from 7 lines to 3.

* Migrate two inline bbox reads inside GizmoArrayEdition to the new
  dict keys: get_axis_world_face_center collapses the manual
  xs/ys/zs min/max + center math to bbox["center"] + bbox["max_x"] /
  ["max_y"] / ["max_z"]; get_element_height collapses
  ``max(corner[2] for corner in obj.bound_box)`` to
  tool.Blender.get_object_bounding_box(obj)["max_z"].

The _BBOX_EQUALITY_EPS = 1e-5 tolerance stays inline as a single-
consumer constant — no other call site needs tolerance-equality on
dimension tuples, so extracting it to a shared util would be
speculative abstraction.

== Drop dead code ==

* GizmoArrayEdition.update_editing_gizmos override + its
  _has_other_parametric_type helper: redundant with
  hide_pen_button = True at line 1024. The base class already hides
  the pen in every idle case (when hide_pen_button is truthy) AND in
  every editing case (unconditionally). The override's conditional
  hide-when-parametric only re-hid a pen that was already hidden in
  both branches. Removes the only remaining path that could re-show
  the array's pen icon; array-edit entry is now uniformly via the
  per-layer ARRAY icons (which is the documented preferred
  affordance, see the hide_pen_button comment).

* _wall_fillet_preview_active in wall.py: defined but never called.
  _wall_fillet_props (the sibling thin-wrapper around
  preview_base.get_preview_props) is heavily used; the
  is_preview_active wrapper was added speculatively and never picked
  up a consumer.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Gorgious56
2026-06-05 12:03:26 +02:00
committed by Thomas Krijnen
parent f5cdf5777e
commit 6391dbebb5
3 changed files with 67 additions and 85 deletions
+48 -71
View File
@@ -58,24 +58,37 @@ def _wipe_array_children(layers: list) -> None:
layer["children"] = []
def _bbox_dims(bound_box) -> tuple[float, float, float]:
"""Return ``(width, depth, height)`` of an ``obj.bound_box`` 8-corner tuple."""
xs = [c[0] for c in bound_box]
ys = [c[1] for c in bound_box]
zs = [c[2] for c in bound_box]
return (max(xs) - min(xs), max(ys) - min(ys), max(zs) - min(zs))
_BBOX_EQUALITY_EPS = 1e-5
def _parent_geometry_changed(parent_obj, layers: list) -> bool:
"""Cheap heuristic: True when the parent's bbox differs from the first resolvable child's,
indicating a parametric edit since the last regen. Misses edits that preserve bbox dimensions
(e.g. shape changes within the same envelope); those need a manual "Regenerate Array"."""
def _resolve_array_edit_props(context: bpy.types.Context):
"""Resolve the active object's array props during an active edit
lifecycle. Returns ``None`` when there's no active object or the user
isn't mid-edit. Used as the execute / poll prologue for operators
bound to the array edit gizmos so they no-op cleanly outside the
edit lifecycle without bypassing the commit lifecycle."""
obj = context.active_object
if obj is None:
return None
props = tool.Model.get_array_props(obj)
if not props.is_editing:
return None
return props
def _array_children_need_rebuild(parent_obj, layers: list) -> bool:
"""Cheap drift detector: True when the parent's local bbox dimensions
differ from the first resolvable child's, indicating a parent geometry
edit since the last array regen. Misses edits that preserve bbox
dimensions (e.g. shape changes within the same envelope); those need
a manual "Regenerate Array" via the UI button.
Runs at array-edit finish as a safety net: when True, callers wipe and
rebuild children so the array picks up the drift; when False, in-place
transform updates suffice."""
if not parent_obj.bound_box:
return True
parent_dims = _bbox_dims(parent_obj.bound_box)
parent_dims = tool.Blender.get_object_bounding_box(parent_obj)["dimensions"]
for layer in layers:
for child_guid in layer.get("children", []):
try:
@@ -85,7 +98,7 @@ def _parent_geometry_changed(parent_obj, layers: list) -> bool:
child_obj = tool.Ifc.get_object(child_element)
if child_obj is None or not child_obj.bound_box:
continue
child_dims = _bbox_dims(child_obj.bound_box)
child_dims = tool.Blender.get_object_bounding_box(child_obj)["dimensions"]
return any(abs(a - b) > _BBOX_EQUALITY_EPS for a, b in zip(parent_dims, child_dims))
return False
@@ -289,12 +302,12 @@ class _ArrayEditMixin(ParametricEditMixinBase):
# each redundant call adds an entry to the IFC owner-history audit
# trail. Rely on the regenerator's pset write instead.
tool.Array.remove_constraints(element)
# Wipe-and-rebuild only when the parent's geometry differs from the
# children's (cheap bbox-dim compare). For pure count / offset edits
# the children are already valid and ``regenerate_array``'s in-place
# transform updates are enough — saves the delete + re-duplicate cost
# per instance on large arrays.
if _parent_geometry_changed(obj, layers):
# Wipe-and-rebuild only when the parent's bbox dims differ from the
# children's. For pure count / offset edits the children are already
# valid and ``regenerate_array``'s in-place transform updates are
# enough — saves the delete + re-duplicate cost per instance on
# large arrays.
if _array_children_need_rebuild(obj, layers):
_wipe_array_children(layers)
tool.Model.regenerate_array(obj, layers)
tool.Array.set_children_lock_state(element, item, True)
@@ -889,11 +902,8 @@ class ToggleArrayMethod(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
if not obj:
return {"CANCELLED"}
props = tool.Model.get_array_props(obj)
if not props.is_editing:
props = _resolve_array_edit_props(context)
if props is None:
return {"CANCELLED"}
props.method = "DISTRIBUTE" if props.method == "OFFSET" else "OFFSET"
return {"FINISHED"}
@@ -924,12 +934,9 @@ class RemoveArrayLayerFromEdit(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
obj = context.active_object
if not obj:
cls.poll_message_set("No active object selected")
return False
props = tool.Model.get_array_props(obj)
if not props.is_editing:
props = _resolve_array_edit_props(context)
if props is None:
cls.poll_message_set("No active object or not editing an array")
return False
# Mirror ``_execute``'s precondition so the gizmo correctly
# disables on stale states (is_editing flag set but the index
@@ -937,10 +944,9 @@ class RemoveArrayLayerFromEdit(bpy.types.Operator, tool.Ifc.Operator):
return props.editing_item_index >= 0
def _execute(self, context):
obj = context.active_object
if not obj:
props = _resolve_array_edit_props(context)
if props is None:
return {"CANCELLED"}
props = tool.Model.get_array_props(obj)
item = props.editing_item_index
if item < 0:
return {"CANCELLED"}
@@ -983,11 +989,8 @@ class AdjustArrayCount(bpy.types.Operator):
increment: bpy.props.IntProperty()
def execute(self, context):
obj = context.active_object
if not obj:
return {"CANCELLED"}
props = tool.Model.get_array_props(obj)
if not props.is_editing:
props = _resolve_array_edit_props(context)
if props is None:
return {"CANCELLED"}
props.count = max(1, props.count + self.increment)
return {"FINISHED"}
@@ -1148,17 +1151,13 @@ class GizmoArrayEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
obj = bpy.context.active_object
if obj is None or not obj.bound_box:
return Vector((0.0, 0.0, 0.0))
xs = [c[0] for c in obj.bound_box]
ys = [c[1] for c in obj.bound_box]
zs = [c[2] for c in obj.bound_box]
center_x = (min(xs) + max(xs)) / 2
center_y = (min(ys) + max(ys)) / 2
center_z = (min(zs) + max(zs)) / 2
bbox = tool.Blender.get_object_bounding_box(obj)
center = bbox["center"]
if axis_index == 0:
return Vector((max(xs), center_y, center_z))
return Vector((bbox["max_x"], center.y, center.z))
if axis_index == 1:
return Vector((center_x, max(ys), center_z))
return Vector((center_x, center_y, max(zs)))
return Vector((center.x, bbox["max_y"], center.z))
return Vector((center.x, center.y, bbox["max_z"]))
@classmethod
def is_element_type(cls, element: "ifcopenshell.entity_instance") -> bool:
@@ -1233,31 +1232,9 @@ class GizmoArrayEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
arrayable IFC type, parametric or otherwise."""
obj = bpy.context.active_object
if obj and obj.bound_box:
return max(corner[2] for corner in obj.bound_box)
return tool.Blender.get_object_bounding_box(obj)["max_z"]
return 1.0
def update_editing_gizmos(self, context: bpy.types.Context, mw: Matrix, props) -> None:
"""Suppress the array gizmo group's pen when a per-feature pen is already showing.
Parametric arrayed elements (a door array, a wall array, …) get TWO pen icons
without this — the per-feature one and the array one. The per-feature pen
is the entry point for that feature's parametric edit; the per-layer ARRAY
icons (drawn alongside it) are the entry point for array edit. The array
group's own pen is redundant here and gets hidden. Non-parametric arrays
(IfcAnnotation / Opening / SpatialElement) have no per-feature group, so
the array pen stays visible there as the only entry point."""
gizmo.BaseParametricGizmoGroup.update_editing_gizmos(self, context, mw, props)
if not props.is_editing:
obj = context.active_object
element = tool.Ifc.get_entity(obj) if obj else None
if element and self._has_other_parametric_type(element):
self.pen_gizmo.hide = True
@staticmethod
def _has_other_parametric_type(element) -> bool:
match = tool.Parametric.find_for_element(element)
return match is not None and match.name != "array"
def _refresh_element_specific(self, context: bpy.types.Context, mw: Matrix, props) -> None:
"""Position the count label and per-layer ARRAY icons.
@@ -2532,11 +2532,6 @@ def _wall_fillet_props(context: bpy.types.Context):
return preview_base.get_preview_props(context, "wall_fillet")
def _wall_fillet_preview_active(context: bpy.types.Context) -> bool:
"""``True`` while a wall-fillet preview is open."""
return preview_base.is_preview_active(context, "wall_fillet")
_FILLET_SLOPE_TOLERANCE_RAD = 1e-4
+19 -9
View File
@@ -887,16 +887,22 @@ class Blender(bonsai.core.tool.Blender):
# ( 1.0, 1.0, -1.0), # 7
# ]
bound_box = obj.bound_box
min_pt = Vector(bound_box[0])
max_pt = Vector(bound_box[6])
bbox_dict = {
"min_x": bound_box[0][0],
"max_x": bound_box[6][0],
"min_y": bound_box[0][1],
"max_y": bound_box[6][1],
"min_z": bound_box[0][2],
"max_z": bound_box[6][2],
"min_point": Vector(bound_box[0]),
"max_point": Vector(bound_box[6]),
"center": (Vector(bound_box[6]) + Vector(bound_box[0])) / 2,
"min_x": min_pt.x,
"max_x": max_pt.x,
"min_y": min_pt.y,
"max_y": max_pt.y,
"min_z": min_pt.z,
"max_z": max_pt.z,
"min_point": min_pt,
"max_point": max_pt,
"center": (max_pt + min_pt) / 2,
# Intrinsic per-axis size in object-local space. Distinct from
# ``obj.dimensions``, which folds object-level scale into its
# output; this is the raw mesh bbox extent.
"dimensions": (max_pt.x - min_pt.x, max_pt.y - min_pt.y, max_pt.z - min_pt.z),
}
return bbox_dict
@@ -926,6 +932,10 @@ class Blender(bonsai.core.tool.Blender):
"min_point": min_point,
"max_point": max_point,
"center": (min_point + max_point) / 2,
# World-axis-aligned per-axis size. For rotated objects this is
# the AABB extent, not the intrinsic mesh size (use the local
# variant for that).
"dimensions": (max_point.x - min_point.x, max_point.y - min_point.y, max_point.z - min_point.z),
}
@classmethod