mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 17:58:20 +00:00
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:
committed by
Thomas Krijnen
parent
f5cdf5777e
commit
6391dbebb5
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user