From 7d27a2bf7f8f1d23ee1fa7c535db3e750da817e1 Mon Sep 17 00:00:00 2001 From: Dipayan Sardar Date: Sun, 29 Mar 2026 14:41:31 +0530 Subject: [PATCH 1/2] Refactor: Use ifcopenshell.util.profiler.Profiler in drawing operator This PR replaces the local 'profile' context manager in 'src/bonsai/bonsai/bim/module/drawing/operator.py' with the shared 'ifcopenshell.util.profiler.Profiler' utility, as requested in #7328. --- .../bonsai/bim/module/drawing/operator.py | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 45f67b0769f..dfe35549d75 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -26,7 +26,6 @@ import time from math import radians from pathlib import Path -from timeit import default_timer as timer from typing import ( TYPE_CHECKING, Any, @@ -52,6 +51,7 @@ import ifcopenshell.util.selector import ifcopenshell.util.shape_builder import ifcopenshell.util.unit +from ifcopenshell.util.profiler import Profiler import numpy as np import shapely from bpy_extras.image_utils import load_image @@ -86,20 +86,6 @@ cwd = os.path.dirname(os.path.realpath(__file__)) -class profile: - """ - A python context manager timing utility - """ - - def __init__(self, task): - self.task = task - - def __enter__(self): - self.start = timer() - - def __exit__(self, *args): - print(self.task, timer() - self.start) - class LineworkContexts(NamedTuple): body: list[list[int]] @@ -328,8 +314,8 @@ def execute(self, context): self.camera_document = tool.Drawing.get_drawing_document(self.camera_element) self.file = tool.Ifc.get() - with profile("Drawing generation process"): - with profile("Initialize drawing generation process"): + with Profiler("Drawing generation process"): + with Profiler("Initialize drawing generation process"): self.cprops = tool.Drawing.get_camera_props(self.camera) self.drawing = self.file.by_id(drawing_id) self.drawing_name = self.drawing.Name @@ -355,7 +341,7 @@ def execute(self, context): linework_svg = None annotation_svg = None - with profile("Generate underlay"): + with Profiler("Generate underlay"): if ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasUnderlay"): drawing_style = self.cprops.get_active_drawing_style() if not drawing_style: @@ -383,7 +369,7 @@ def execute(self, context): underlay_svg = self.generate_underlay(context) - with profile("Generate linework"): + with Profiler("Generate linework"): if tool.Drawing.is_camera_orthographic(): if self.cprops.linework_mode == "OPENCASCADE": linework_svg = self.generate_linework(context) @@ -392,11 +378,11 @@ def execute(self, context): elif self.cprops.linework_mode == "FREESTYLE": linework_svg = self.generate_freestyle_linework(context) - with profile("Generate annotation"): + with Profiler("Generate annotation"): if tool.Drawing.is_camera_orthographic(): annotation_svg = self.generate_annotation(context) - with profile("Combine SVG layers"): + with Profiler("Combine SVG layers"): svg_path = self.combine_svgs(context, underlay_svg, linework_svg, annotation_svg) if self.open_viewer: @@ -606,7 +592,7 @@ def serialize_contexts_elements( drawing_elements = drawing_elements.copy() contexts_: list[list[int]] = getattr(contexts, context_type) for context in contexts_: - with profile(f"Processing {context_type} context"): + with Profiler(f"Processing {context_type} context"): if not context or not drawing_elements: continue geom_settings = ifcopenshell.geom.settings() @@ -897,7 +883,7 @@ def generate_linework(self, context: bpy.types.Context) -> Union[str, None]: # in case of printing multiple drawings we need to sync just once if self.sync and self.drawing_index == 0: - with profile("sync"): + with Profiler("sync"): # All very hackish whilst prototyping exporter = bonsai.bim.export_ifc.IfcExporter(None) exporter.file = tool.Ifc.get() @@ -953,7 +939,7 @@ def generate_linework(self, context: bpy.types.Context) -> Union[str, None]: self.serialize_contexts_elements(ifc, tree, contexts, "annotation", drawing_elements, target_view) if tool.Ifc.get() == ifc and self.camera_element not in drawing_elements: - with profile("Camera element"): + with Profiler("Camera element"): # The camera must always be included, regardless of any include/exclude filters. geom_settings = ifcopenshell.geom.settings() geom_settings.set("iterator-output", ifcopenshell.ifcopenshell_wrapper.NATIVE) @@ -961,7 +947,7 @@ def generate_linework(self, context: bpy.types.Context) -> Union[str, None]: for elem in it: self.serialiser.write(elem) - with profile("Finalizing"): + with Profiler("Finalizing"): self.serialiser.finalize() results = self.svg_buffer.get_value() From 712da05fa1654ef8b3759cdec5135b943a71caf3 Mon Sep 17 00:00:00 2001 From: Dipayan Sardar Date: Mon, 30 Mar 2026 00:47:08 +0530 Subject: [PATCH 2/2] Fix Bonsai collector NoneType crash (#7861) and add spatial_reference selector key (#7806) --- src/bonsai/bonsai/tool/collector.py | 2 ++ src/ifcopenshell-python/ifcopenshell/util/selector.py | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/src/bonsai/bonsai/tool/collector.py b/src/bonsai/bonsai/tool/collector.py index 1e6653acd1c..7f05bc2665e 100644 --- a/src/bonsai/bonsai/tool/collector.py +++ b/src/bonsai/bonsai/tool/collector.py @@ -28,6 +28,8 @@ class Collector(bonsai.core.tool.Collector): @classmethod def assign(cls, obj: bpy.types.Object, should_clean_users_collection=True) -> None: """Links an object to an appropriate Blender collection.""" + if obj is None: + return if should_clean_users_collection: for users_collection in obj.users_collection: if tool.Blender.get_object_bim_props(obj).collection == users_collection: diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index bbe8125927a..58d9a8ffb73 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -449,6 +449,13 @@ def _get_element_value(element: ifcopenshell.entity_instance, keys: list[str]) - value = ifcopenshell.util.element.get_parent(value, ifc_class="IfcSite") elif key == "parent": value = ifcopenshell.util.element.get_parent(value) + elif key == "spatial_reference": + # Support for IfcRelReferencedInSpatialStructure (Issue #7806) + for rel in getattr(value, "ReferencedInStructures", []): + value = rel.RelatingStructure + break + else: + value = None elif key in ("types", "occurrences"): value = ifcopenshell.util.element.get_types(value) elif key == "count":