mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
Bonsai: preserve occurrence geometry/material/styles when deleting a type
Deleting a type used to strip its occurrences: any that displayed the
type's mapped representation lost their geometry, and inherited material
and presentation styles were dropped too.
The no-SHIFT "Delete Type" path now bakes each occurrence's geometry,
styles, and inherited material onto the occurrence before the type is
removed:
- Refactor UnassignType's unmap logic into a reusable
UnassignType.unassign_and_unmap(), and extend it to re-attach styled
items (copy_deep only follows forward refs, so IfcStyledItem is lost)
and bake down any inherited (non-owned) material.
- Add RemoveType._detach_type_material_set(): unhook the type's
IfcMaterialLayerSet/ProfileSet association cascade-free before deletion,
so remove_product's aggressive unassign_material never fires and the
occurrences' layer/profile-set usages survive intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit a90064929b)
This commit is contained in:
@@ -20,6 +20,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.api.attribute
|
||||
import ifcopenshell.api.material
|
||||
import ifcopenshell.api.type
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.representation
|
||||
@@ -115,51 +116,96 @@ class UnassignType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if TYPE_CHECKING:
|
||||
related_object: str
|
||||
|
||||
def _execute(self, context):
|
||||
@staticmethod
|
||||
def _reattach_styles(
|
||||
file: ifcopenshell.file, copied_entities: dict[int, ifcopenshell.entity_instance]
|
||||
) -> None:
|
||||
"""copy_deep only follows forward references, so IfcStyledItem (an inverse,
|
||||
``StyledByItem``) is not carried onto the copied geometry. Re-create a
|
||||
styled item on each copy that points at the same presentation styles as
|
||||
the original, so the unmapped occurrence keeps its appearance."""
|
||||
for original_id, copied in copied_entities.items():
|
||||
original = file.by_id(original_id)
|
||||
for styled_item in getattr(original, "StyledByItem", None) or []:
|
||||
file.create_entity(
|
||||
"IfcStyledItem",
|
||||
Item=copied,
|
||||
Styles=styled_item.Styles,
|
||||
Name=styled_item.Name,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def unassign_and_unmap(obj: bpy.types.Object) -> None:
|
||||
"""Unassign the type from ``obj`` and bake a private copy of any mapped
|
||||
representation onto it, so the occurrence keeps its geometry, styles, and
|
||||
material once the type (the source of all three) is gone."""
|
||||
|
||||
def exclude_callback(attribute):
|
||||
return attribute.is_a("IfcProfileDef") and attribute.ProfileName
|
||||
|
||||
self.file = tool.Ifc.get()
|
||||
file = tool.Ifc.get()
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcObject"):
|
||||
return
|
||||
|
||||
# Capture the material inherited from the type before we sever the link,
|
||||
# but only if the occurrence has no material of its own to override it.
|
||||
own_material = ifcopenshell.util.element.get_material(element, should_inherit=False)
|
||||
inherited_material = ifcopenshell.util.element.get_material(element, should_inherit=True)
|
||||
|
||||
ifcopenshell.api.type.unassign_type(file, related_objects=[element])
|
||||
|
||||
if element.Representation:
|
||||
new_active_representation = None
|
||||
active_representation = tool.Geometry.get_active_representation(obj)
|
||||
active_context = active_representation.ContextOfItems
|
||||
representations = []
|
||||
for representation in element.Representation.Representations:
|
||||
resolved_representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
if representation == resolved_representation:
|
||||
representations.append(representation)
|
||||
else:
|
||||
# We must unmap representations, carrying over their styles.
|
||||
copied_entities: dict[int, ifcopenshell.entity_instance] = {}
|
||||
copied_representation = ifcopenshell.util.element.copy_deep(
|
||||
file,
|
||||
resolved_representation,
|
||||
exclude=["IfcGeometricRepresentationContext"],
|
||||
exclude_callback=exclude_callback,
|
||||
copied_entities=copied_entities,
|
||||
)
|
||||
UnassignType._reattach_styles(file, copied_entities)
|
||||
representations.append(copied_representation)
|
||||
if representation.ContextOfItems == active_context:
|
||||
new_active_representation = copied_representation
|
||||
element.Representation.Representations = representations
|
||||
|
||||
if new_active_representation:
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=new_active_representation,
|
||||
)
|
||||
|
||||
# Bake the inherited material down onto the occurrence now that its type
|
||||
# link (and, in the delete-type case, the type itself) is gone. Usages are
|
||||
# occurrence-specific and never inherited, so they need no handling here.
|
||||
if inherited_material is not None and own_material is None:
|
||||
material_type = inherited_material.is_a()
|
||||
if material_type not in ("IfcMaterialLayerSetUsage", "IfcMaterialProfileSetUsage"):
|
||||
ifcopenshell.api.material.assign_material(
|
||||
file, products=[element], type=material_type, material=inherited_material
|
||||
)
|
||||
|
||||
def _execute(self, context):
|
||||
if self.related_object:
|
||||
related_objects = [bpy.data.objects[self.related_object]]
|
||||
else:
|
||||
related_objects = tool.Blender.get_selected_objects()
|
||||
|
||||
for obj in related_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcObject"):
|
||||
continue
|
||||
ifcopenshell.api.type.unassign_type(self.file, related_objects=[element])
|
||||
|
||||
if element.Representation:
|
||||
new_active_representation = None
|
||||
active_representation = tool.Geometry.get_active_representation(obj)
|
||||
active_context = active_representation.ContextOfItems
|
||||
representations = []
|
||||
for representation in element.Representation.Representations:
|
||||
resolved_representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
if representation == resolved_representation:
|
||||
representations.append(representation)
|
||||
else:
|
||||
# We must unmap representations.
|
||||
copied_representation = ifcopenshell.util.element.copy_deep(
|
||||
tool.Ifc.get(),
|
||||
resolved_representation,
|
||||
exclude=["IfcGeometricRepresentationContext"],
|
||||
exclude_callback=exclude_callback,
|
||||
)
|
||||
representations.append(copied_representation)
|
||||
if representation.ContextOfItems == active_context:
|
||||
new_active_representation = copied_representation
|
||||
element.Representation.Representations = representations
|
||||
|
||||
if new_active_representation:
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=new_active_representation,
|
||||
)
|
||||
self.unassign_and_unmap(obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -318,6 +364,34 @@ class RemoveType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
element: int
|
||||
also_delete_instances: bool
|
||||
|
||||
@staticmethod
|
||||
def _detach_type_material_set(element: ifcopenshell.entity_instance) -> None:
|
||||
"""Cascade-free removal of the type's IfcMaterialLayerSet / IfcMaterialProfileSet
|
||||
association, called just before the type is deleted.
|
||||
|
||||
``remove_product`` would otherwise route the type's material association
|
||||
through ``unassign_material``, which deletes *every* usage of that set
|
||||
across the model (documented behaviour, with an upstream TODO calling it
|
||||
too aggressive) — stripping the material off the very occurrences we are
|
||||
trying to keep. By unhooking the type<->set link by hand here, the type
|
||||
has no material at delete time, so that cascade never fires and the set
|
||||
plus the occurrences' usages survive intact."""
|
||||
file = tool.Ifc.get()
|
||||
material = ifcopenshell.util.element.get_material(element, should_inherit=False)
|
||||
if not material or material.is_a() not in ("IfcMaterialLayerSet", "IfcMaterialProfileSet"):
|
||||
return
|
||||
for rel in list(getattr(element, "HasAssociations", None) or []):
|
||||
if not (rel.is_a("IfcRelAssociatesMaterial") and rel.RelatingMaterial == material):
|
||||
continue
|
||||
remaining = [o for o in rel.RelatedObjects if o != element]
|
||||
if remaining:
|
||||
rel.RelatedObjects = remaining
|
||||
else:
|
||||
history = rel.OwnerHistory
|
||||
file.remove(rel)
|
||||
if history:
|
||||
ifcopenshell.util.element.remove_deep2(file, history)
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.also_delete_instances = event.shift
|
||||
if self.also_delete_instances:
|
||||
@@ -334,11 +408,22 @@ class RemoveType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
element = tool.Ifc.get().by_id(self.element)
|
||||
occurrences = ifcopenshell.util.element.get_types(element)
|
||||
if self.also_delete_instances:
|
||||
for occurrence in ifcopenshell.util.element.get_types(element):
|
||||
for occurrence in occurrences:
|
||||
occurrence_obj = tool.Ifc.get_object(occurrence)
|
||||
if occurrence_obj:
|
||||
tool.Geometry.delete_ifc_object(occurrence_obj)
|
||||
else:
|
||||
# Keep the occurrences: bake their (previously type-mapped) geometry,
|
||||
# styles, and inherited material onto each one so nothing is lost when
|
||||
# the type is deleted...
|
||||
for occurrence in occurrences:
|
||||
occ_obj = tool.Ifc.get_object(occurrence)
|
||||
if occ_obj:
|
||||
UnassignType.unassign_and_unmap(occ_obj)
|
||||
# ...and keep any layer/profile-set material usages alive across the deletion.
|
||||
self._detach_type_material_set(element)
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj:
|
||||
tool.Geometry.delete_ifc_object(obj)
|
||||
|
||||
Reference in New Issue
Block a user