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.
This commit is contained in:
Ryan Schultz
2025-12-13 09:37:56 -06:00
parent 276c9c9833
commit a2a5780d59
3 changed files with 199 additions and 31 deletions
+116 -21
View File
@@ -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,
@@ -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"
+31 -10
View File
@@ -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