Revert "Fix #6392: when duplicating a window/door/etc, the associated IfcOpenElement duplicates as well."

This reverts commit a2a5780d59.
This commit is contained in:
Dion Moult
2026-03-16 17:52:02 +11:00
parent 021e6b9ef5
commit b38316336c
3 changed files with 29 additions and 219 deletions
+19 -138
View File
@@ -151,29 +151,11 @@ class FilledOpeningGenerator:
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW" existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
) )
assert representation assert representation
representation = ifcopenshell.util.representation.resolve_representation(representation)
# Check if mapped representation - PRESERVE the mapping structure else:
if ( representation = self.generate_opening_from_filling(
representation.RepresentationType == "MappedRepresentation" filling, filling_obj, opening_thickness_si=opening_thickness_si
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
)
# Create mapped representation # Create mapped representation
if reuse_mapped_representation: if reuse_mapped_representation:
@@ -247,109 +229,38 @@ class FilledOpeningGenerator:
voided_element = opening.VoidsElements[0].RelatingBuildingElement voided_element = opening.VoidsElements[0].RelatingBuildingElement
opening_rep = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW") 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.unassign_representation(tool.Ifc.get(), product=opening, representation=opening_rep)
ifcopenshell.api.geometry.remove_representation(tool.Ifc.get(), 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) 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: if existing_opening_occurrence:
representation = ifcopenshell.util.representation.get_representation( representation = ifcopenshell.util.representation.get_representation(
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW" existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
) )
representation = ifcopenshell.util.representation.resolve_representation(representation)
if ( mapped_representation = ifcopenshell.api.geometry.map_representation(
representation tool.Ifc.get(), representation=representation
and representation.RepresentationType == "MappedRepresentation" )
and len(representation.Items) == 1 ifcopenshell.api.geometry.assign_representation(
and representation.Items[0].is_a("IfcMappedItem") tool.Ifc.get(), product=opening, representation=mapped_representation
): )
# PRESERVE the mapped structure - reuse the same RepresentationMap else:
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:
opening_obj = tool.Ifc.get_object(opening) opening_obj = tool.Ifc.get_object(opening)
if opening_obj: if opening_obj:
tool.Ifc.unlink(element=opening) tool.Ifc.unlink(element=opening)
tool.Blender.remove_data_blocks([opening_obj], remove_unused_data=True) tool.Blender.remove_data_blocks([opening_obj], remove_unused_data=True)
filling_obj = tool.Ifc.get_object(filling) filling_obj = tool.Ifc.get_object(filling)
representation_to_use = self.generate_opening_from_filling(filling, filling_obj) representation = 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:
mapped_representation = ifcopenshell.api.geometry.map_representation( 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( # update voided object representation or all it's parts if it's an aggregate
tool.Ifc.get(), product=opening, representation=mapped_representation
)
# update voided object representation...
voided_elements = ifcopenshell.util.element.get_parts(voided_element) or [voided_element] voided_elements = ifcopenshell.util.element.get_parts(voided_element) or [voided_element]
for voided_element in voided_elements: for voided_element in voided_elements:
voided_obj = tool.Ifc.get_object(voided_element) voided_obj = tool.Ifc.get_object(voided_element)
@@ -363,36 +274,6 @@ class FilledOpeningGenerator:
representation=representation, 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( def generate_opening_from_filling(
self, self,
filling: ifcopenshell.entity_instance, filling: ifcopenshell.entity_instance,
@@ -618,8 +618,6 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
if not element: if not element:
return {"FINISHED"} return {"FINISHED"}
if element.is_a("IfcTypeProduct"): 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) self.import_type_from_ifc(element, context)
elif element.is_a("IfcProduct"): elif element.is_a("IfcProduct"):
# NOTE: Non-types are not exposed in UI directly # 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()): if element.is_a("IfcSurfaceStyle") and not tool.Ifc.get_object_by_identifier(element.id()):
ifc_importer.create_style(element) 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): class EditProjectLibrary(bpy.types.Operator):
bl_idname = "bim.edit_project_library" bl_idname = "bim.edit_project_library"
+10 -32
View File
@@ -93,38 +93,16 @@ class Root(bonsai.core.tool.Root):
elif dest.is_a("IfcTypeProduct"): elif dest.is_a("IfcTypeProduct"):
if not source.RepresentationMaps: if not source.RepresentationMaps:
return copied_entities return copied_entities
dest.RepresentationMaps = [
# Copy representation maps while preserving mapped representation structures ifcopenshell.util.element.copy_deep(
new_maps = [] tool.Ifc.get(),
for i, rep_map in enumerate(source.RepresentationMaps): m,
source_rep = rep_map.MappedRepresentation exclude=["IfcGeometricRepresentationContext"],
exclude_callback=exclude_callback,
# Copy the map itself copied_entities=copied_entities,
new_map = ifcopenshell.util.element.copy(tool.Ifc.get(), rep_map) )
for m in source.RepresentationMaps
# 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 return copied_entities
@classmethod @classmethod