From b38316336cc2baed93dc8b8c1fe8e4352bf5765a Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 16 Mar 2026 17:52:02 +1100 Subject: [PATCH] Revert "Fix #6392: when duplicating a window/door/etc, the associated IfcOpenElement duplicates as well." This reverts commit a2a5780d59df97bfe31fdd41947b66253b65dcc3. --- src/bonsai/bonsai/bim/module/model/opening.py | 157 +++--------------- .../bonsai/bim/module/project/operator.py | 49 ------ src/bonsai/bonsai/tool/root.py | 42 ++--- 3 files changed, 29 insertions(+), 219 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/opening.py b/src/bonsai/bonsai/bim/module/model/opening.py index c20325c2e7..e680ceeb54 100644 --- a/src/bonsai/bonsai/bim/module/model/opening.py +++ b/src/bonsai/bonsai/bim/module/model/opening.py @@ -151,29 +151,11 @@ class FilledOpeningGenerator: existing_opening_occurrence, "Model", "Body", "MODEL_VIEW" ) assert representation - - # Check if mapped representation - PRESERVE the mapping structure - if ( - representation.RepresentationType == "MappedRepresentation" - and len(representation.Items) == 1 - and representation.Items[0].is_a("IfcMappedItem") - ): - # Store the existing RepresentationMap to reuse it - existing_mapping_source = representation.Items[0].MappingSource - reuse_mapped_representation = True - else: - representation = ifcopenshell.util.representation.resolve_representation(representation) - - if not reuse_mapped_representation: - # 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 - ) + representation = ifcopenshell.util.representation.resolve_representation(representation) + else: + representation = self.generate_opening_from_filling( + filling, filling_obj, opening_thickness_si=opening_thickness_si + ) # Create mapped representation if reuse_mapped_representation: @@ -247,109 +229,38 @@ 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 MappedRepresentation (preserve mapping!) - # 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 - reuse_mapped_representation = False - existing_mapping_source = None - if existing_opening_occurrence: representation = ifcopenshell.util.representation.get_representation( existing_opening_occurrence, "Model", "Body", "MODEL_VIEW" ) - - if ( - representation - and representation.RepresentationType == "MappedRepresentation" - and len(representation.Items) == 1 - and representation.Items[0].is_a("IfcMappedItem") - ): - # PRESERVE the mapped structure - reuse the same RepresentationMap - existing_mapping_source = representation.Items[0].MappingSource - reuse_mapped_representation = True - else: - representation_to_use = ifcopenshell.util.representation.resolve_representation(representation) - - if not representation_to_use and not reuse_mapped_representation: - 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 not reuse_mapped_representation and preserved_representation: - representation_to_use = preserved_representation - - if not representation_to_use and not reuse_mapped_representation: + 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: 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_to_use = self.generate_opening_from_filling(filling, filling_obj) - - # Create the mapped representation - if reuse_mapped_representation: - # Reuse existing RepresentationMap - don't create a new one! - context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") - new_mapped_item = tool.Ifc.get().create_entity( - "IfcMappedItem", - MappingSource=existing_mapping_source, - MappingTarget=tool.Ifc.get().create_entity( - "IfcCartesianTransformationOperator3D", - Axis1=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(1.0, 0.0, 0.0)), - Axis2=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0.0, 1.0, 0.0)), - LocalOrigin=tool.Ifc.get().create_entity("IfcCartesianPoint", Coordinates=(0.0, 0.0, 0.0)), - Scale=1.0, - Axis3=tool.Ifc.get().create_entity("IfcDirection", DirectionRatios=(0.0, 0.0, 1.0)), - ), - ) - mapped_representation = tool.Ifc.get().create_entity( - "IfcShapeRepresentation", - ContextOfItems=context, - RepresentationIdentifier="Body", - RepresentationType="MappedRepresentation", - Items=[new_mapped_item], - ) - else: + representation = self.generate_opening_from_filling(filling, filling_obj) mapped_representation = ifcopenshell.api.geometry.map_representation( - tool.Ifc.get(), representation=representation_to_use + tool.Ifc.get(), representation=representation + ) + ifcopenshell.api.geometry.assign_representation( + tool.Ifc.get(), product=opening, representation=mapped_representation ) - ifcopenshell.api.geometry.assign_representation( - tool.Ifc.get(), product=opening, representation=mapped_representation - ) - - # update voided object representation... + # update voided object representation or all it's parts if it's an aggregate 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) @@ -363,36 +274,6 @@ 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 1cc5518eb4..9d0e2ff8d4 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -618,8 +618,6 @@ 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 @@ -720,53 +718,6 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): if element.is_a("IfcSurfaceStyle") and not tool.Ifc.get_object_by_identifier(element.id()): 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" diff --git a/src/bonsai/bonsai/tool/root.py b/src/bonsai/bonsai/tool/root.py index 02ddbe9745..8880a168fe 100644 --- a/src/bonsai/bonsai/tool/root.py +++ b/src/bonsai/bonsai/tool/root.py @@ -93,38 +93,16 @@ class Root(bonsai.core.tool.Root): elif dest.is_a("IfcTypeProduct"): if not source.RepresentationMaps: return copied_entities - - # 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 + 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 + ] return copied_entities @classmethod