From a2a5780d59df97bfe31fdd41947b66253b65dcc3 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Sat, 13 Dec 2025 09:37:56 -0600 Subject: [PATCH 01/14] Fix #6392: when duplicating a window/door/etc, the associated IfcOpenElement duplicates as well. Also when `bim.append_library_element` it copies the IfcOpenElement, as well. --- src/bonsai/bonsai/bim/module/model/opening.py | 137 +++++++++++++++--- .../bonsai/bim/module/project/operator.py | 52 +++++++ src/bonsai/bonsai/tool/root.py | 41 ++++-- 3 files changed, 199 insertions(+), 31 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/opening.py b/src/bonsai/bonsai/bim/module/model/opening.py index 958716954b..d028425fb2 100644 --- a/src/bonsai/bonsai/bim/module/model/opening.py +++ b/src/bonsai/bonsai/bim/module/model/opening.py @@ -151,11 +151,29 @@ class FilledOpeningGenerator: existing_opening_occurrence, "Model", "Body", "MODEL_VIEW" ) assert representation - representation = ifcopenshell.util.representation.resolve_representation(representation) + + # Check if mapped representation - preserve it + if (representation.RepresentationType == 'MappedRepresentation' and + len(representation.Items) == 1 and + representation.Items[0].is_a("IfcMappedItem")): + source_rep = representation.Items[0].MappingSource.MappedRepresentation + representation = ifcopenshell.util.element.copy_deep( + tool.Ifc.get(), + source_rep, + exclude=["IfcGeometricRepresentationContext"] + ) + else: + representation = ifcopenshell.util.representation.resolve_representation(representation) else: - representation = self.generate_opening_from_filling( - filling, filling_obj, opening_thickness_si=opening_thickness_si - ) + # Check for library template before generating from filling + template_rep = self.get_opening_template_from_type(filling) + + if template_rep: + representation = template_rep + else: + representation = self.generate_opening_from_filling( + filling, filling_obj, opening_thickness_si=opening_thickness_si + ) mapped_representation = ifcopenshell.api.geometry.map_representation( tool.Ifc.get(), representation=representation @@ -204,38 +222,85 @@ class FilledOpeningGenerator: voided_element = opening.VoidsElements[0].RelatingBuildingElement opening_rep = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW") + + # ALWAYS preserve the existing opening representation (Tessellation, SweptSolid, etc.) + preserved_representation = None + if opening_rep: + if (opening_rep.RepresentationType == 'MappedRepresentation' and + len(opening_rep.Items) == 1 and + opening_rep.Items[0].is_a("IfcMappedItem")): + # For mapped representations, copy the underlying representation + preserved_representation = ifcopenshell.util.element.copy_deep( + tool.Ifc.get(), + opening_rep.Items[0].MappingSource.MappedRepresentation, + exclude=["IfcGeometricRepresentationContext"] + ) + else: + # For direct representations (non-mapped), copy them too + preserved_representation = ifcopenshell.util.element.copy_deep( + tool.Ifc.get(), + opening_rep, + exclude=["IfcGeometricRepresentationContext"] + ) + ifcopenshell.api.geometry.unassign_representation(tool.Ifc.get(), product=opening, representation=opening_rep) ifcopenshell.api.geometry.remove_representation(tool.Ifc.get(), representation=opening_rep) existing_opening_occurrence = self.get_existing_opening_occurrence_if_any(filling) - + + # Priority order for choosing representation: + # 1. Existing occurrence with Tessellation (best quality) + # 2. Library template with Tessellation + # 3. Preserved representation from old opening (maintain user's work) + # 4. Generate from filling (last resort) + + representation_to_use = None + if existing_opening_occurrence: representation = ifcopenshell.util.representation.get_representation( existing_opening_occurrence, "Model", "Body", "MODEL_VIEW" ) - representation = ifcopenshell.util.representation.resolve_representation(representation) - mapped_representation = ifcopenshell.api.geometry.map_representation( - tool.Ifc.get(), representation=representation - ) - ifcopenshell.api.geometry.assign_representation( - tool.Ifc.get(), product=opening, representation=mapped_representation - ) - else: + + if (representation and + representation.RepresentationType == 'MappedRepresentation' and + len(representation.Items) == 1 and + representation.Items[0].is_a("IfcMappedItem")): + source_rep = representation.Items[0].MappingSource.MappedRepresentation + # Prefer Tessellation from existing occurrence over preserved representation + if source_rep.RepresentationType == 'Tessellation': + representation_to_use = ifcopenshell.util.element.copy_deep( + tool.Ifc.get(), + source_rep, + exclude=["IfcGeometricRepresentationContext"] + ) + else: + representation_to_use = ifcopenshell.util.representation.resolve_representation(representation) + + if not representation_to_use: + template_rep = self.get_opening_template_from_type(filling) + if template_rep and template_rep.RepresentationType == 'Tessellation': + representation_to_use = template_rep + + if not representation_to_use and preserved_representation: + representation_to_use = preserved_representation + + if not representation_to_use: opening_obj = tool.Ifc.get_object(opening) if opening_obj: tool.Ifc.unlink(element=opening) tool.Blender.remove_data_blocks([opening_obj], remove_unused_data=True) filling_obj = tool.Ifc.get_object(filling) - representation = self.generate_opening_from_filling(filling, filling_obj) - mapped_representation = ifcopenshell.api.geometry.map_representation( - tool.Ifc.get(), representation=representation - ) - ifcopenshell.api.geometry.assign_representation( - tool.Ifc.get(), product=opening, representation=mapped_representation - ) + representation_to_use = self.generate_opening_from_filling(filling, filling_obj) - # update voided object representation or all it's parts if it's an aggregate + mapped_representation = ifcopenshell.api.geometry.map_representation( + tool.Ifc.get(), representation=representation_to_use + ) + ifcopenshell.api.geometry.assign_representation( + tool.Ifc.get(), product=opening, representation=mapped_representation + ) + + # update voided object representation... voided_elements = ifcopenshell.util.element.get_parts(voided_element) or [voided_element] for voided_element in voided_elements: voided_obj = tool.Ifc.get_object(voided_element) @@ -249,6 +314,36 @@ class FilledOpeningGenerator: representation=representation, ) + def get_opening_template_from_type(self, filling: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: + """ + Check if the filling's type has a stored opening template from library import. + """ + element_type = ifcopenshell.util.element.get_type(filling) + + if not element_type: + return None + + desc = element_type.Description + + if not desc or "||BonsaiOpeningTemplate:" not in desc: + return None + + # Extract template ID + marker = desc.split("||BonsaiOpeningTemplate:")[-1] + template_id = int(marker.split("||")[0]) + + try: + template_rep = tool.Ifc.get().by_id(template_id) + # Make a copy so we don't reuse the same representation instance + copied = ifcopenshell.util.element.copy_deep( + tool.Ifc.get(), + template_rep, + exclude=["IfcGeometricRepresentationContext"] + ) + return copied + except: + return None + def generate_opening_from_filling( self, filling: ifcopenshell.entity_instance, diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 2f5b9c906e..0650cda238 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -611,6 +611,8 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): if not element: return {"FINISHED"} if element.is_a("IfcTypeProduct"): + # Store opening template from library if it exists + self.store_opening_template_from_library(element, library_file) self.import_type_from_ifc(element, context) elif element.is_a("IfcProduct"): # NOTE: Non-types are not exposed in UI directly @@ -712,6 +714,56 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): ifc_importer.create_style(element) + def store_opening_template_from_library( + self, + element: ifcopenshell.entity_instance, + library_file: ifcopenshell.file + ) -> None: + """ + Find an opening representation in the library and copy it to the current file + as a template. Store the template ID on the type for later retrieval. + """ + try: + library_element = library_file.by_guid(element.GlobalId) + except: + return + + # Find occurrences with openings in the library + library_occurrences = ifcopenshell.util.element.get_types(library_element) + + for occurrence in library_occurrences: + if not getattr(occurrence, "FillsVoids", None): + continue + + library_opening = occurrence.FillsVoids[0].RelatingOpeningElement + library_opening_rep = ifcopenshell.util.representation.get_representation( + library_opening, "Model", "Body", "MODEL_VIEW" + ) + + if not library_opening_rep: + continue + + # Check if mapped representation + if (library_opening_rep.RepresentationType == 'MappedRepresentation' and + len(library_opening_rep.Items) == 1 and + library_opening_rep.Items[0].is_a("IfcMappedItem")): + + mapped_rep = library_opening_rep.Items[0].MappingSource.MappedRepresentation + + # Store ALL representation types (Tessellation, SweptSolid, etc.) + template_rep = ifcopenshell.util.element.copy_deep( + self.file, + mapped_rep, + exclude=["IfcGeometricRepresentationContext"] + ) + + # Store reference in type's Description + current_desc = element.Description or "" + element.Description = f"{current_desc}||BonsaiOpeningTemplate:{template_rep.id()}" + return + break + + class EditProjectLibrary(bpy.types.Operator): bl_idname = "bim.edit_project_library" bl_label = "Edit Project Library" diff --git a/src/bonsai/bonsai/tool/root.py b/src/bonsai/bonsai/tool/root.py index 22e8b42d81..109af267d6 100644 --- a/src/bonsai/bonsai/tool/root.py +++ b/src/bonsai/bonsai/tool/root.py @@ -91,16 +91,37 @@ class Root(bonsai.core.tool.Root): elif dest.is_a("IfcTypeProduct"): if not source.RepresentationMaps: return copied_entities - dest.RepresentationMaps = [ - ifcopenshell.util.element.copy_deep( - tool.Ifc.get(), - m, - exclude=["IfcGeometricRepresentationContext"], - exclude_callback=exclude_callback, - copied_entities=copied_entities, - ) - for m in source.RepresentationMaps - ] + + # Copy representation maps while preserving mapped representation structures + new_maps = [] + for i, rep_map in enumerate(source.RepresentationMaps): + source_rep = rep_map.MappedRepresentation + + + # Copy the map itself + new_map = ifcopenshell.util.element.copy(tool.Ifc.get(), rep_map) + + # Handle the mapped representation - preserve mapping structure if present + if (source_rep.RepresentationType == 'MappedRepresentation' and + len(source_rep.Items) == 1 and + source_rep.Items[0].is_a("IfcMappedItem")): + # This is a mapped representation - preserve the structure + new_rep = ifcopenshell.util.element.copy(tool.Ifc.get(), source_rep) + new_rep.Items = [ifcopenshell.util.element.copy(tool.Ifc.get(), item) for item in source_rep.Items] + new_map.MappedRepresentation = new_rep + else: + # Not a mapped representation - use copy_deep as before + new_map.MappedRepresentation = ifcopenshell.util.element.copy_deep( + tool.Ifc.get(), + source_rep, + exclude=["IfcGeometricRepresentationContext"], + exclude_callback=exclude_callback, + copied_entities=copied_entities, + ) + + new_maps.append(new_map) + + dest.RepresentationMaps = new_maps return copied_entities @classmethod From 6ab7acfa57532383cae78962ac86b608da808f8b Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Sat, 13 Dec 2025 20:45:48 -0600 Subject: [PATCH 02/14] closes #3340: right click on section/elevation tag to open corresponding drawing. video: https://github.com/IfcOpenShell/IfcOpenShell/issues/3340#issuecomment-3650097896 --- .../bonsai/bim/module/drawing/__init__.py | 11 ++++ .../bonsai/bim/module/drawing/operator.py | 64 +++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/drawing/__init__.py b/src/bonsai/bonsai/bim/module/drawing/__init__.py index 8153671f09..8b5a787ebe 100644 --- a/src/bonsai/bonsai/bim/module/drawing/__init__.py +++ b/src/bonsai/bonsai/bim/module/drawing/__init__.py @@ -18,10 +18,12 @@ import bpy from . import ui, prop, operator, handler, gizmos, workspace +import bonsai.tool as tool classes = ( operator.ActivateDrawing, operator.ActivateDrawingFromSheet, + operator.ActivateDrawingByAnnotation, operator.ActivateDrawingStyle, operator.ActivateModel, operator.AddAnnotation, @@ -134,6 +136,13 @@ classes = ( ) +def menu_func(self, context): + active_obj = context.active_object + if active_obj: + element = tool.Ifc.get_entity(active_obj) + if element and element.is_a("IfcAnnotation") and element.ObjectType in ["SECTION", "ELEVATION"]: + self.layout.operator("bim.activate_drawing_by_annotation", text="Go to Drawing") + def register(): if not bpy.app.background: bpy.utils.register_tool(workspace.AnnotationTool, after={"bim.bim_tool"}, separator=True, group=False) @@ -146,6 +155,7 @@ def register(): bpy.app.handlers.load_post.append(handler.load_post) bpy.app.handlers.depsgraph_update_pre.append(handler.depsgraph_update_pre_handler) bpy.types.VIEW3D_MT_image_add.append(ui.add_object_button) + bpy.types.VIEW3D_MT_object_context_menu.append(menu_func) def unregister(): @@ -160,3 +170,4 @@ def unregister(): bpy.app.handlers.load_post.remove(handler.load_post) bpy.app.handlers.depsgraph_update_pre.remove(handler.depsgraph_update_pre_handler) bpy.types.VIEW3D_MT_image_add.remove(ui.add_object_button) + bpy.types.VIEW3D_MT_object_context_menu.remove(menu_func) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 0648e58a1f..9c687641bd 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -3876,3 +3876,67 @@ class ExcludeAnnotation(bpy.types.Operator, tool.Ifc.Operator): if referenced_element := tool.Drawing.get_annotation_element(element): tool.Drawing.exclude_annotation_from_drawing(referenced_element, drawing) core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing) + +class ActivateDrawingByAnnotation(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.activate_drawing_by_annotation" + bl_label = "Activate Drawing" + bl_description = "Activate the drawing corresponding to the selected annotation" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + # Check if an annotation object is selected + if not context.selected_objects: + cls.poll_message_set("No object selected") + return False + + active_obj = context.active_object + if not active_obj: + cls.poll_message_set("No active object") + return False + + element = tool.Ifc.get_entity(active_obj) + if not element: + cls.poll_message_set("Selected object is not an IFC element") + return False + + # Check if it's an IfcAnnotation with ObjectType = "SECTION" or "ELEVATION" + if not element.is_a("IfcAnnotation") or element.ObjectType not in ["SECTION", "ELEVATION"]: + cls.poll_message_set("Selected object is not a drawing annotation") + return False + + return True + + def _execute(self, context): + active_obj = context.active_object + element = tool.Ifc.get_entity(active_obj) + + if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in ["SECTION", "ELEVATION"]: + self.report({"ERROR"}, "Selected object is not a drawing annotation") + return {"CANCELLED"} + + # Find the drawing/camera element that this annotation references + drawing_element = self.find_drawing_from_annotation(element) + + if not drawing_element: + self.report({"ERROR"}, "Could not find drawing element for this annotation") + return {"CANCELLED"} + + # Use the existing ActivateDrawing operator with the drawing element's ID + bpy.ops.bim.activate_drawing(drawing=drawing_element.id()) + + return {"FINISHED"} + + def find_drawing_from_annotation(self, annotation_element): + """Find the drawing/camera element that this annotation references.""" + ifc = tool.Ifc.get() + + # Check IfcRelAssignsToProduct relationships + for rel in ifc.get_inverse(annotation_element): + if rel.is_a("IfcRelAssignsToProduct") and rel.RelatingProduct: + if rel.RelatingProduct.is_a("IfcAnnotation"): + # Found the drawing element! + return rel.RelatingProduct + + + return None \ No newline at end of file From 4a2058ebc6cf7586a2f100b3add0a96e57fe13cc Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Dec 2025 12:46:15 +0500 Subject: [PATCH 03/14] build-all - remove debug print --- nix/build-all.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nix/build-all.py b/nix/build-all.py index ff53ce6d4b..b548b874c8 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -405,8 +405,6 @@ if any(f.startswith("py-") for f in flags): if any(f.startswith("occt-") for f in flags): OCCT_VERSION = next(f.split("-", 1)[1] for f in flags if f.startswith("occt-")) -print(OCCT_VERSION) - if explicit_targets: targets = {dep for target in explicit_targets for dep in gather_dependencies(target)} else: From 46dc6cf08bd124a25b1086c59b49672b9dbc82a2 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Dec 2025 12:52:10 +0500 Subject: [PATCH 04/14] ci.yml - use `depth 1` for swig --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d488c6764b..001417cd7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -150,9 +150,8 @@ jobs: # Remove default swig to avoid conflicts. sudo apt remove --purge swig swig4.0 sudo apt-get install -y libpcre2-dev bison - git clone https://github.com/swig/swig + git clone https://github.com/swig/swig --branch v4.1.0 --depth 1 cd swig - git checkout v4.1.0 mkdir build && cd build cmake .. \ -DCMAKE_BUILD_TYPE=Release \ From 57197dd439197cda90ee5e0e4a0c156605e08036 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Dec 2025 16:32:47 +0500 Subject: [PATCH 05/14] cmake export - export `OpenCASCADE_INTERFACE` target To avoid issues if user provided OCC_INCLUDE_DIR and OCC_LIBRARY_DIR. --- cmake/CMakeLists.txt | 2 -- cmake/FindOpenCASCADE.cmake | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index f1ae9f66bf..e95d40574d 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -208,8 +208,6 @@ endforeach() if(BUILD_IFCGEOM AND WITH_OPENCASCADE) find_package(OpenCASCADE REQUIRED) - # Map OpenCASCADE_LIBRARIES variable from OpenCASCADEConfig.cmake to OpenCASCADE_LIBRARIES used by kernel generic cmake file - set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES}) add_definitions(-DIFOPSH_WITH_OPENCASCADE) set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_OPENCASCADE) list(APPEND GEOMETRY_KERNELS opencascade) diff --git a/cmake/FindOpenCASCADE.cmake b/cmake/FindOpenCASCADE.cmake index f02b2559e3..9a52524d1e 100644 --- a/cmake/FindOpenCASCADE.cmake +++ b/cmake/FindOpenCASCADE.cmake @@ -145,8 +145,10 @@ if(OCCT_STATIC) endif() endif() -add_library(OpenCASCADE_INTERFACE INTERFACE IMPORTED) +add_library(OpenCASCADE_INTERFACE INTERFACE) target_include_directories(OpenCASCADE_INTERFACE INTERFACE "${OCC_INCLUDE_DIR}") target_link_libraries(OpenCASCADE_INTERFACE INTERFACE ${OpenCASCADE_LIBRARIES}) target_link_directories(OpenCASCADE_INTERFACE INTERFACE "${OCC_LIBRARY_DIR}") set(OpenCASCADE_LIBRARIES OpenCASCADE_INTERFACE) +install(TARGETS OpenCASCADE_INTERFACE + EXPORT ${IFCOPENSHELL_EXPORT_TARGETS}) From c7b75d28631e39f0a0a666c4d3d5448b3b523f4e Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Dec 2025 15:25:22 +0500 Subject: [PATCH 06/14] ci.yml - add packages to use occt cmake config --- .github/workflows/ci.yml | 10 ++++++++-- src/examples/CMakeLists.txt | 3 +-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 001417cd7a..7bc12b9f3d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,6 +55,11 @@ jobs: - name: Install C++ dependencies run: | sudo apt update + # `occt-misc` is only needed for 22.04, since it has cmake configs. + # In 24.04+, the needed files were moved `libocct-foundation-dev` and `occt-misc` can be removed. + # Other libs in `OCCT_CMAKE_DEPS` are needed only for cmake config to work properly, they're not used directly. + OCCT_CMAKE_DEPS="occt-misc libocct-draw-dev tcl-dev tk-dev libxi-dev" + sudo apt-get install --no-install-recommends \ git cmake gcc g++ \ libboost-date-time-dev \ @@ -67,6 +72,7 @@ jobs: libpcre3-dev libxml2-dev \ libtbb-dev nlohmann-json3-dev \ libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \ + ${OCCT_CMAKE_DEPS} \ libhdf5-dev libcgal-dev libeigen3-dev - name: ccache @@ -209,8 +215,8 @@ jobs: cmake .. -DCMAKE_BUILD_TYPE=Release cmake --build . ./IfcParseExamples "../IfcParseExamples_test.ifc" - # ./IfcOpenHouse - # ./IfcAdvancedHouse + ./IfcOpenHouse + ./IfcAdvancedHouse ./IfcAlignment ./IfcSimplifiedAlignment diff --git a/src/examples/CMakeLists.txt b/src/examples/CMakeLists.txt index fdf5608328..9133b77b7c 100644 --- a/src/examples/CMakeLists.txt +++ b/src/examples/CMakeLists.txt @@ -55,8 +55,7 @@ if(SCHEMA_VERSIONS MATCHES "2x3") endif() install(TARGETS IfcParseExamples) - # TODO: enable after figuring out where to get get OCCT config for CI. - if(WITH_OPENCASCADE AND NOT STANDALONE_PROJECT) + if(WITH_OPENCASCADE) add_executable(IfcOpenHouse IfcOpenHouse.cpp) add_executable(IfcAdvancedHouse IfcAdvancedHouse.cpp) add_library(IfcHouseInterface INTERFACE) From 4138d9f4642b578f167fa0076cfe12d84024d039 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Dec 2025 17:03:27 +0500 Subject: [PATCH 07/14] ci.yml - drop OCCT cmake variables preferring cmake config Just found that it's available from `occt-misc` package. --- .github/workflows/ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7bc12b9f3d..2a8a4d962f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -171,15 +171,11 @@ jobs: echo ${{ env.pythonLocation }} mkdir build && cd build - # Ubuntu 22.04's libocct-foundation-dev package doesn't have Config.cmake, so we provide OCC paths directly. - # In later versions of Ubuntu, this can be simplified and the OCC paths can be removed. cmake \ -DCMAKE_CXX_STANDARD=17 \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH=/usr \ -DCMAKE_SYSTEM_PREFIX_PATH=/usr \ - -DOCC_INCLUDE_DIR=/usr/include/opencascade \ - -DOCC_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \ -DPYTHON_EXECUTABLE:FILEPATH=${{ env.pythonLocation }}/bin/python \ -DPYTHON_INCLUDE_DIR:PATH=${{ env.pythonLocation }}/include/python3.11 \ -DUSE_MMAP=On \ From 87e4846f71293d68a3f67e6a83fa4d0f2186360f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Dec 2025 17:05:58 +0500 Subject: [PATCH 08/14] ci.yml - use ccache for examples too --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a8a4d962f..6fcaf11951 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -208,7 +208,9 @@ jobs: run: | cd src/examples mkdir build && cd build - cmake .. -DCMAKE_BUILD_TYPE=Release + cmake .. -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache cmake --build . ./IfcParseExamples "../IfcParseExamples_test.ifc" ./IfcOpenHouse From cd87217f4e4cc13a5613769f458aa7186b1fa0a5 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Dec 2025 17:33:15 +0500 Subject: [PATCH 09/14] build scripts - don't provide cxx-std-17 As it's automatically set by the cmake script. --- .github/workflows/ci-ifcopenshell-docker.yml | 1 - .github/workflows/ci.yml | 1 - .github/workflows/release.yml | 1 - conda/build.bat | 5 ++--- conda/build.sh | 1 - nix/build-all.py | 1 - 6 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-ifcopenshell-docker.yml b/.github/workflows/ci-ifcopenshell-docker.yml index 5d79e5fbb1..52764cd4b6 100644 --- a/.github/workflows/ci-ifcopenshell-docker.yml +++ b/.github/workflows/ci-ifcopenshell-docker.yml @@ -44,7 +44,6 @@ jobs: cmake \ -DCMAKE_C_COMPILER_LAUNCHER=ccache \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ - -DCMAKE_CXX_STANDARD=17 \ -DCMAKE_INSTALL_PREFIX=$PWD/install/ \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH=/usr \ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6fcaf11951..ad00406ed8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -172,7 +172,6 @@ jobs: mkdir build && cd build cmake \ - -DCMAKE_CXX_STANDARD=17 \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH=/usr \ -DCMAKE_SYSTEM_PREFIX_PATH=/usr \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8050c10e87..b5cbb4f28c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,7 +46,6 @@ jobs: mkdir build && cd build cmake \ -DCMAKE_INSTALL_PREFIX=$PWD/install/ \ - -DCMAKE_CXX_STANDARD=17 \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH=/usr \ -DCMAKE_SYSTEM_PREFIX_PATH=/usr \ diff --git a/conda/build.bat b/conda/build.bat index fe8cef01c1..242bc7d683 100644 --- a/conda/build.bat +++ b/conda/build.bat @@ -9,7 +9,6 @@ set LIBXML2="%LIBRARY_PREFIX%/lib/libxml2.lib" cmake -G "Ninja" ^ -D SCHEMA_VERSIONS="2x3;4;4x1;4x3_add2" ^ -D CMAKE_BUILD_TYPE:STRING=Release ^ - -D CMAKE_CXX_STANDARD=17 ^ -D CMAKE_INSTALL_PREFIX:FILEPATH="%LIBRARY_PREFIX%" ^ -D CMAKE_PREFIX_PATH:FILEPATH="%LIBRARY_PREFIX%" ^ -D CMAKE_SYSTEM_PREFIX_PATH:FILEPATH="%LIBRARY_PREFIX%" ^ @@ -43,7 +42,7 @@ cmake -G "Ninja" ^ -D Boost_INCLUDE_DIR:FILEPATH="%LIBRARY_PREFIX%\include" ^ -D Boost_USE_STATIC_LIBS:BOOL=OFF ^ ../cmake - + if errorlevel 1 exit 1 ninja install -j 1 @@ -52,4 +51,4 @@ if errorlevel 1 exit 1 python %RECIPE_DIR%/update_version_init.py %PKG_VERSION% %SP_DIR%/ifcopenshell/__init__.py -if errorlevel 1 exit 1 \ No newline at end of file +if errorlevel 1 exit 1 diff --git a/conda/build.sh b/conda/build.sh index 571b6959f8..132bcf2be9 100644 --- a/conda/build.sh +++ b/conda/build.sh @@ -16,7 +16,6 @@ cmake ${CMAKE_ARGS} -G Ninja \ -DSCHEMA_VERSIONS="2x3;4;4x1;4x3_add2" \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=$PREFIX \ - -DCMAKE_CXX_STANDARD=17 \ ${CMAKE_PLATFORM_FLAGS[@]} \ -DCMAKE_PREFIX_PATH=$PREFIX \ -DCMAKE_SYSTEM_PREFIX_PATH=$PREFIX \ diff --git a/nix/build-all.py b/nix/build-all.py index b548b874c8..3bd12885c3 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -1330,7 +1330,6 @@ os.makedirs(executables_dir, exist_ok=True) cmake_args = [ - "-DCMAKE_CXX_STANDARD=17", "-DUSE_MMAP=OFF", "-DBUILD_EXAMPLES=OFF", "-DBUILD_SHARED_LIBS=" + OFF_ON[not BUILD_STATIC], From 621cc4f039df390503b3bf41553ab56027ff2b65 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Dec 2025 17:34:24 +0500 Subject: [PATCH 10/14] build scripts - don't provide ccache explicitly as it's autodetected --- .github/workflows/ci-ifcopenshell-docker.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci-ifcopenshell-docker.yml b/.github/workflows/ci-ifcopenshell-docker.yml index 52764cd4b6..d6663ff198 100644 --- a/.github/workflows/ci-ifcopenshell-docker.yml +++ b/.github/workflows/ci-ifcopenshell-docker.yml @@ -42,8 +42,6 @@ jobs: run: | mkdir build && cd build cmake \ - -DCMAKE_C_COMPILER_LAUNCHER=ccache \ - -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ -DCMAKE_INSTALL_PREFIX=$PWD/install/ \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH=/usr \ From deb6243ce546d9ef098bef26133a5ff3563f11b7 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Dec 2025 18:21:59 +0500 Subject: [PATCH 11/14] cmake - use `INCLUDES DESTINATION` for includes paths export As it's more idiomatic. --- src/ifcparse/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcparse/CMakeLists.txt b/src/ifcparse/CMakeLists.txt index 5da3182406..e3bd50fb5b 100644 --- a/src/ifcparse/CMakeLists.txt +++ b/src/ifcparse/CMakeLists.txt @@ -32,7 +32,6 @@ set(IFCPARSE_FILES ${IFCPARSE_CPP_FILES} ${IFCPARSE_H_FILES}) add_library(IfcParse ${IFCPARSE_FILES}) target_compile_definitions(IfcParse PUBLIC ${SCHEMA_DEFINITIONS}) -target_include_directories(IfcParse PUBLIC $) set_target_properties(IfcParse PROPERTIES COMPILE_FLAGS -DIFC_PARSE_EXPORTS @@ -60,4 +59,5 @@ target_link_libraries(IfcParse ${ROCKSDB_LIBRARIES}) install(TARGETS IfcParse EXPORT ${IFCOPENSHELL_EXPORT_TARGETS} PUBLIC_HEADER DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/ifcparse" + INCLUDES DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" ) From bf2c27528c641a6fcfe9ff420adf3763e8cbdeff Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Dec 2025 18:27:17 +0500 Subject: [PATCH 12/14] cmake - fix issue with using cmake occt packages <7.7.0 --- cmake/FindOpenCASCADE.cmake | 7 +++++++ src/examples/CMakeLists.txt | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/cmake/FindOpenCASCADE.cmake b/cmake/FindOpenCASCADE.cmake index 9a52524d1e..d035c868ed 100644 --- a/cmake/FindOpenCASCADE.cmake +++ b/cmake/FindOpenCASCADE.cmake @@ -39,6 +39,13 @@ if(NOT OCC_INCLUDE_DIR AND NOT OCC_LIBRARY_DIR) mark_as_advanced(OpenCASCADE_DIR) message(STATUS "Found OpenCASCADE config: ${OpenCASCADE_DIR}") + if(OpenCASCADE_VERSION VERSION_LESS "7.7.0") + # cmake configs < 7.7.0 were not adding include directories to targets automatically. + set_target_properties(TKernel PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${OpenCASCADE_INCLUDE_DIR}" + ) + endif() + if(OpenCASCADE_VERSION VERSION_LESS "7.9.0") # Bug in OCCT cmake configs < 7.9.0 - missing linked library. list(APPEND OpenCASCADE_LIBRARIES WSOCK32.lib) diff --git a/src/examples/CMakeLists.txt b/src/examples/CMakeLists.txt index 9133b77b7c..be3a3e42d9 100644 --- a/src/examples/CMakeLists.txt +++ b/src/examples/CMakeLists.txt @@ -65,6 +65,13 @@ if(SCHEMA_VERSIONS MATCHES "2x3") find_package(CGAL CONFIG REQUIRED) find_package(OpenCASCADE CONFIG REQUIRED) + if(OpenCASCADE_VERSION VERSION_LESS "7.7.0") + # cmake configs < 7.7.0 were not adding include directories to targets automatically. + set_target_properties(TKernel PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${OpenCASCADE_INCLUDE_DIR}" + ) + endif() + target_link_libraries(IfcHouseInterface INTERFACE IfcOpenShell::IfcParse IfcOpenShell::geometry_serializer) else() target_link_libraries(IfcHouseInterface INTERFACE IfcParse geometry_serializer) From b42581b2230284ff3aaec14049073b012f9c92b8 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Dec 2025 18:43:48 +0500 Subject: [PATCH 13/14] cmake - fix WSOCK32 lib required for OCCT on Unix (ce7fdcc) --- cmake/FindOpenCASCADE.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/FindOpenCASCADE.cmake b/cmake/FindOpenCASCADE.cmake index d035c868ed..64ce0427a7 100644 --- a/cmake/FindOpenCASCADE.cmake +++ b/cmake/FindOpenCASCADE.cmake @@ -46,7 +46,7 @@ if(NOT OCC_INCLUDE_DIR AND NOT OCC_LIBRARY_DIR) ) endif() - if(OpenCASCADE_VERSION VERSION_LESS "7.9.0") + if(OpenCASCADE_VERSION VERSION_LESS "7.9.0" AND WIN32) # Bug in OCCT cmake configs < 7.9.0 - missing linked library. list(APPEND OpenCASCADE_LIBRARIES WSOCK32.lib) endif() From cf5ffad9af7dc911d1a3c9a90767fb8c950f7dbc Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Mon, 15 Dec 2025 13:18:29 -0600 Subject: [PATCH 14/14] feat(drawing): support multiple file selection in Add Reference Enable importing multiple .svg reference files at once using standard Blender multi-select (Shift/Ctrl+click). Includes backward compatibility and test coverage. --- .../bonsai/bim/module/drawing/operator.py | 15 ++++- src/bonsai/test/tool/test_drawing.py | 66 +++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 9c687641bd..e3726d5073 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -3050,10 +3050,21 @@ class AddReference(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): filter_glob: bpy.props.StringProperty(default="*.svg", options={"HIDDEN"}) use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True) filename_ext = ".svg" + + files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement) + directory: bpy.props.StringProperty(subtype='DIR_PATH') def _execute(self, context): - filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path) - core.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=filepath) + # Handle both single and multiple file selection + if self.files: + for file_elem in self.files: + filepath = os.path.join(self.directory, file_elem.name) + uri = tool.Ifc.get_uri(filepath, use_relative_path=self.use_relative_path) + core.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=uri) + else: + # Fallback for single file (backward compatibility) + filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path) + core.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=filepath) class RemoveReference(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py index 040dfb8dbb..28f95614f8 100644 --- a/src/bonsai/test/tool/test_drawing.py +++ b/src/bonsai/test/tool/test_drawing.py @@ -927,3 +927,69 @@ class TestAddReferenceImage(NewFile): uv_node = material_nodes["Texture Coordinate"] assert len(uv_node.outputs["Generated"].links[:]) == 1 + +class TestAddReference(NewFile): + def test_add_single_reference(self): + """Test adding a single reference file (backward compatibility)""" + bpy.ops.bim.create_project() + ifc_path = Path("test/files/temp/test.ifc").absolute() + bpy.ops.bim.save_project(filepath=str(ifc_path), should_save_as=True) + + # Create a temporary SVG file + svg_path = Path("test/files/temp/reference.svg").absolute() + svg_path.parent.mkdir(parents=True, exist_ok=True) + with open(svg_path, "w") as f: + f.write('') + + try: + # Add single reference + bpy.ops.bim.add_reference(filepath=str(svg_path)) + + # Verify reference was added + ifc = tool.Ifc.get() + references = [doc for doc in ifc.by_type("IfcDocumentInformation") if doc.Scope == "REFERENCE"] + assert len(references) == 1 + assert references[0].Name == "reference" + finally: + # Cleanup + if svg_path.exists(): + svg_path.unlink() + + def test_add_multiple_references(self): + """Test adding multiple reference files at once""" + bpy.ops.bim.create_project() + ifc_path = Path("test/files/temp/test.ifc").absolute() + bpy.ops.bim.save_project(filepath=str(ifc_path), should_save_as=True) + + # Create temporary SVG files + temp_dir = Path("test/files/temp").absolute() + temp_dir.mkdir(parents=True, exist_ok=True) + + svg_files = [] + for i in range(3): + svg_path = temp_dir / f"reference_{i}.svg" + with open(svg_path, "w") as f: + f.write('') + svg_files.append(svg_path) + + try: + # Test by directly calling core.add_document multiple times + # (simulating what the operator does with multiple files) + ifc = tool.Ifc.get() + for svg_file in svg_files: + uri = tool.Ifc.get_uri(str(svg_file), use_relative_path=True) + from bonsai.bim import core + core.drawing.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=uri) + + # Verify all references were added + references = [doc for doc in ifc.by_type("IfcDocumentInformation") if doc.Scope == "REFERENCE"] + assert len(references) == 3 + + reference_names = {ref.Name for ref in references} + expected_names = {f"reference_{i}" for i in range(3)} + assert reference_names == expected_names + finally: + # Cleanup + for svg_file in svg_files: + if svg_file.exists(): + svg_file.unlink()