mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-18 14:31:39 +00:00
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:
@@ -353,11 +353,29 @@ 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 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:
|
else:
|
||||||
representation = self.generate_opening_from_filling(
|
# Check for library template before generating from filling
|
||||||
filling, filling_obj, opening_thickness_si=opening_thickness_si
|
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:
|
||||||
@@ -431,38 +449,85 @@ 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 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:
|
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)
|
|
||||||
mapped_representation = ifcopenshell.api.geometry.map_representation(
|
if (representation and
|
||||||
tool.Ifc.get(), representation=representation
|
representation.RepresentationType == 'MappedRepresentation' and
|
||||||
)
|
len(representation.Items) == 1 and
|
||||||
ifcopenshell.api.geometry.assign_representation(
|
representation.Items[0].is_a("IfcMappedItem")):
|
||||||
tool.Ifc.get(), product=opening, representation=mapped_representation
|
source_rep = representation.Items[0].MappingSource.MappedRepresentation
|
||||||
)
|
# Prefer Tessellation from existing occurrence over preserved representation
|
||||||
else:
|
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)
|
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 = self.generate_opening_from_filling(filling, filling_obj)
|
representation_to_use = 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
|
|
||||||
)
|
|
||||||
|
|
||||||
# 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]
|
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)
|
||||||
@@ -476,6 +541,36 @@ 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,
|
||||||
|
|||||||
@@ -633,6 +633,8 @@ 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
|
||||||
@@ -734,6 +736,56 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
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"
|
||||||
bl_label = "Edit Project Library"
|
bl_label = "Edit Project Library"
|
||||||
|
|||||||
Reference in New Issue
Block a user