From 9d2e25aa4e0b222aea7ddc972057d402d561f11c Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Tue, 17 Mar 2026 08:46:54 -0500 Subject: [PATCH] Fix #7799: Fix elements hidden when activating plan drawings Elements with multiple IFC representations (plan, RCP, body) were incorrectly excluded from drawings because the camera frustum test used obj.bound_box, which reflects only the currently active Blender mesh. After switching from an RCP drawing, the plan-view mesh was no longer active, placing the element's bbox outside the plan camera's frustum. Fix by unioning three bbox sources in is_in_camera_view: the IfcBoundingBox (representation-independent footprint), accumulated custom properties built up across representation switches, and the current obj.bound_box. This ensures the spatial test covers the element's full extent regardless of which representation is active. Generated with the assistance of an AI coding tool. --- src/bonsai/bonsai/core/geometry.py | 1 + src/bonsai/bonsai/tool/drawing.py | 86 ++++++++++++++++++++++++++---- src/bonsai/bonsai/tool/geometry.py | 24 +++++++++ 3 files changed, 101 insertions(+), 10 deletions(-) diff --git a/src/bonsai/bonsai/core/geometry.py b/src/bonsai/bonsai/core/geometry.py index 83ba921d30..8ac208c3f9 100644 --- a/src/bonsai/bonsai/core/geometry.py +++ b/src/bonsai/bonsai/core/geometry.py @@ -127,6 +127,7 @@ def switch_representation( assert element geometry.clear_cache(element) geometry.reimport_element_representations(obj, representation, apply_openings=apply_openings) + geometry.update_bbox_accumulation(obj) def get_representation_ifc_parameters(geometry: type[tool.Geometry], obj: bpy.types.Object) -> None: diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index a78fd67ed2..21848c522c 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -2530,15 +2530,80 @@ class Drawing(bonsai.core.tool.Drawing): y = props.height camera_inverse_matrix = camera.matrix_world.inverted() - return set( - [ - tool.Ifc.get_entity(o) - for o in objs - if o - and cls.is_in_camera_view(o, camera_inverse_matrix, x, y, camera.data.clip_start, camera.data.clip_end) - and tool.Ifc.get_entity(o) - ] - ) + result = set() + for o in objs: + if not o: + continue + element = tool.Ifc.get_entity(o) + if not element: + continue + if cls.is_in_camera_view(o, camera_inverse_matrix, x, y, camera.data.clip_start, camera.data.clip_end, element): + result.add(element) + return result + + @classmethod + def _get_bbox_corners( + cls, obj: bpy.types.Object, element: Optional[ifcopenshell.entity_instance] = None + ) -> list[Vector]: + """Return 8 bbox corners in object-local space for camera frustum testing. + + Unions all available bbox sources so that the result covers the full geometric + extent of the element regardless of which representation is currently active: + 1. IfcBoundingBox from a ``Box`` representation context. + 2. Accumulated union bbox stored in ``obj["ifc_bbox_min/max"]`` custom properties. + 3. Current ``obj.bound_box``. + """ + all_corners: list[Vector] = [] + + # 1. IfcBoundingBox (typically the floor-plan XY footprint) + if element is not None and getattr(element, "Representation", None): + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + for rep in element.Representation.Representations: + try: + resolved = tool.Geometry.resolve_mapped_representation(rep) + except Exception: + continue + ctx = resolved.ContextOfItems + if getattr(ctx, "ContextIdentifier", None) == "Box": + for item in resolved.Items: + if item.is_a("IfcBoundingBox"): + c = item.Corner.Coordinates + cx = c[0] * unit_scale + cy = c[1] * unit_scale + cz = c[2] * unit_scale + dx = item.XDim * unit_scale + dy = item.YDim * unit_scale + dz = item.ZDim * unit_scale + all_corners.extend([ + Vector((cx, cy, cz)), + Vector((cx + dx, cy + dy, cz + dz)), + ]) + + # 2. Accumulated bbox custom properties (union of all representations ever loaded) + if "ifc_bbox_min" in obj and "ifc_bbox_max" in obj: + mn = obj["ifc_bbox_min"] + mx = obj["ifc_bbox_max"] + all_corners.extend([Vector((mn[0], mn[1], mn[2])), Vector((mx[0], mx[1], mx[2]))]) + + # 3. Current Blender bound_box + all_corners.extend(Vector(v) for v in obj.bound_box) + + # Return 8 corners of the union bounding box + xs = [v.x for v in all_corners] + ys = [v.y for v in all_corners] + zs = [v.z for v in all_corners] + mn_x, mn_y, mn_z = min(xs), min(ys), min(zs) + mx_x, mx_y, mx_z = max(xs), max(ys), max(zs) + return [ + Vector((mn_x, mn_y, mn_z)), + Vector((mx_x, mn_y, mn_z)), + Vector((mn_x, mx_y, mn_z)), + Vector((mx_x, mx_y, mn_z)), + Vector((mn_x, mn_y, mx_z)), + Vector((mx_x, mn_y, mx_z)), + Vector((mn_x, mx_y, mx_z)), + Vector((mx_x, mx_y, mx_z)), + ] @classmethod def is_in_camera_view( @@ -2549,8 +2614,9 @@ class Drawing(bonsai.core.tool.Drawing): y: float, clip_start: float, clip_end: float, + element: Optional[ifcopenshell.entity_instance] = None, ) -> bool: - local_bbox = [camera_inverse_matrix @ obj.matrix_world @ Vector(v) for v in obj.bound_box] + local_bbox = [camera_inverse_matrix @ obj.matrix_world @ v for v in cls._get_bbox_corners(obj, element)] local_x = [v.x for v in local_bbox] local_y = [v.y for v in local_bbox] local_z = [v.z for v in local_bbox] diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index e24152d642..ef47efd155 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -2469,3 +2469,27 @@ class Geometry(bonsai.core.tool.Geometry): bm.to_mesh(mesh) bm.free() mesh.update() + + @classmethod + def update_bbox_accumulation(cls, obj: bpy.types.Object) -> None: + """Expand the accumulated IFC bounding box custom properties to include the current mesh bbox. + + Stores the union of all representation bboxes ever loaded for this object as + ``obj["ifc_bbox_min"]`` and ``obj["ifc_bbox_max"]`` in object-local space. + """ + if not obj.bound_box: + return + corners = obj.bound_box + xs = [v[0] for v in corners] + ys = [v[1] for v in corners] + zs = [v[2] for v in corners] + new_min = [min(xs), min(ys), min(zs)] + new_max = [max(xs), max(ys), max(zs)] + if "ifc_bbox_min" in obj: + prev_min = list(obj["ifc_bbox_min"]) + prev_max = list(obj["ifc_bbox_max"]) + obj["ifc_bbox_min"] = [min(new_min[i], prev_min[i]) for i in range(3)] + obj["ifc_bbox_max"] = [max(new_max[i], prev_max[i]) for i in range(3)] + else: + obj["ifc_bbox_min"] = new_min + obj["ifc_bbox_max"] = new_max