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.
This commit is contained in:
Ryan Schultz
2026-03-17 08:46:54 -05:00
parent ba36dc82ff
commit 9d2e25aa4e
3 changed files with 101 additions and 10 deletions
+1
View File
@@ -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:
+76 -10
View File
@@ -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]
+24
View File
@@ -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