Fix #5949. When duplicating objects, copy over IFC links (shape representation ID, item_ids, edge_item_ids) in a more reliable way

This commit is contained in:
Dion Moult
2025-01-10 15:22:53 +11:00
parent e4412e2f51
commit 83add56e6c
6 changed files with 44 additions and 25 deletions
+4 -6
View File
@@ -27,7 +27,7 @@ if TYPE_CHECKING:
def copy_class(
ifc: tool.Ifc, collector: tool.Collector, geometry: tool.Geometry, root: tool.Root, obj: bpy.types.Object
) -> ifcopenshell.entity_instance:
) -> ifcopenshell.entity_instance | None:
element = ifc.get_entity(obj)
if not element:
return
@@ -43,14 +43,12 @@ def copy_class(
ifc.run("type.map_type_representations", related_object=new, relating_type=relating_type)
root.link_object_data(ifc.get_object(relating_type), obj)
elif representation:
root.copy_representation(element, new)
new_representation = root.get_element_representation(new, root.get_representation_context(representation))
copied_entities = root.copy_representation(element, new)
data = geometry.duplicate_object_data(obj)
if data:
geometry.copy_data_links(data, copied_entities)
geometry.change_object_data(obj, data, is_global=True)
geometry.rename_object(data, geometry.get_representation_name(new_representation))
geometry.link(new_representation, data)
geometry.reload_representation_item_ids(new_representation, data)
geometry.rename_object(data, geometry.get_representation_name(ifc.get_entity(data)))
root.assign_body_styles(new, obj)
collector.assign(obj)
if root.is_element_a(new, "IfcOpeningElement"):
+1 -1
View File
@@ -387,6 +387,7 @@ class Geometry:
def clear_cache(cls, element): pass
def clear_modifiers(cls, obj): pass
def clear_scale(cls, obj): pass
def copy_data_links(cls, data, copied_entities) -> None: pass
def delete_data(cls, data): pass
def delete_ifc_object(cls, obj): pass
def delete_opening_object_placement(cls, opening): pass
@@ -420,7 +421,6 @@ class Geometry:
def record_object_position(cls, obj): pass
def recreate_object_with_data(cls, obj, data): pass
def reimport_element_representations(cls, obj, representation, apply_openings=True): pass
def reload_representation_item_ids(cls, representation, data) -> None: pass
def remove_connection(cls, connection): pass
def rename_object(cls, obj, name): pass
def replace_object_data_globally(cls, old_data, new_data): pass
+18 -8
View File
@@ -259,10 +259,14 @@ class Geometry(bonsai.core.tool.Geometry):
# a cylinder) so dissolving edges should not be allowed.
mesh_element = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
if (
mesh_element.is_a("IfcShapeRepresentation")
and ifcopenshell.util.representation.resolve_representation(mesh_element).RepresentationType
== "AdvancedBrep"
) or mesh_element.is_a("IfcAdvancedBrep") or not obj.data:
(
mesh_element.is_a("IfcShapeRepresentation")
and ifcopenshell.util.representation.resolve_representation(mesh_element).RepresentationType
== "AdvancedBrep"
)
or mesh_element.is_a("IfcAdvancedBrep")
or not obj.data
):
return
if hasattr(obj.data, "attributes") and (ios_edges_attribute := obj.data.attributes.get("ios_edges")):
# Edges from a forced triangulation are stored as True in a boolean attribute on the mesh
@@ -612,7 +616,7 @@ class Geometry(bonsai.core.tool.Geometry):
@classmethod
def get_representation_name(cls, representation: ifcopenshell.entity_instance) -> str:
return tool.Loader.get_mesh_name(representation.ContextOfItems.id(), representation.id())
return tool.Loader.get_mesh_name(representation)
@classmethod
def get_styles(
@@ -719,7 +723,7 @@ class Geometry(bonsai.core.tool.Geometry):
if not tool.Loader.is_native_swept_disk_solid(element, representation):
continue
if curve is None:
mesh_name = tool.Loader.get_mesh_name(context.id(), representation.id())
mesh_name = tool.Loader.get_mesh_name(representation)
native_data = {
"representation": representation,
# TODO: calculate mapped item matrix.
@@ -1729,8 +1733,14 @@ class Geometry(bonsai.core.tool.Geometry):
return results
@classmethod
def reload_representation_item_ids(cls, representation: ifcopenshell.entity_instance, data: bpy.types.Mesh) -> None:
data["ios_item_ids"] = [i["item"].id() for i in ifcopenshell.util.representation.resolve_items(representation)]
def copy_data_links(cls, data: bpy.types.Mesh, copied_entities: dict[int, ifcopenshell.entity_instance]) -> None:
representation = tool.Ifc.get_entity(data)
representation = copied_entities.get(representation.id(), representation)
tool.Ifc.link(representation, data)
if item_ids := data.get("ios_item_ids"):
data["ios_item_ids"] = [copied_entities.get(i, tool.Ifc.get().by_id(i)).id() for i in item_ids]
if item_ids := data.get("ios_edges_item_ids"):
data["ios_edges_item_ids"] = [copied_entities.get(i, tool.Ifc.get().by_id(i)).id() for i in item_ids]
@classmethod
def export_mesh_to_tessellation(
+3 -1
View File
@@ -102,7 +102,7 @@ class Ifc(bonsai.core.tool.Ifc):
Return None if object is not linked to IFC or it's linked to non-existent element.
"""
ifc = IfcStore.get_file()
if not ifc:
if not ifc or not obj:
return
props = None
@@ -110,6 +110,8 @@ class Ifc(bonsai.core.tool.Ifc):
props = obj.BIMObjectProperties
elif isinstance(obj, bpy.types.Material):
props = obj.BIMStyleProperties
else:
props = obj.BIMMeshProperties
if props and (ifc_definition_id := props.ifc_definition_id):
try:
+4 -5
View File
@@ -84,13 +84,12 @@ class Loader(bonsai.core.tool.Loader):
@classmethod
def get_mesh_name_from_shape(cls, geometry: ifcopenshell.geom.ShapeType) -> str:
representation_id = cls.get_representation_id_from_shape(geometry)
representation = tool.Ifc.get().by_id(representation_id)
context_id = representation.ContextOfItems.id() if hasattr(representation, "ContextOfItems") else 0
return cls.get_mesh_name(context_id, representation_id)
return cls.get_mesh_name(tool.Ifc.get().by_id(representation_id))
@classmethod
def get_mesh_name(cls, context_id: int, representation_id: int) -> str:
return "{}/{}".format(context_id, representation_id)
def get_mesh_name(cls, representation: ifcopenshell.entity_instance) -> str:
context_id = representation.ContextOfItems.id() if hasattr(representation, "ContextOfItems") else 0
return "{}/{}".format(context_id, representation.id())
@classmethod
def get_name(cls, element: ifcopenshell.entity_instance) -> str:
+14 -4
View File
@@ -55,28 +55,38 @@ class Root(bonsai.core.tool.Root):
)
@classmethod
def copy_representation(cls, source: ifcopenshell.entity_instance, dest: ifcopenshell.entity_instance) -> None:
def copy_representation(
cls, source: ifcopenshell.entity_instance, dest: ifcopenshell.entity_instance
) -> dict[int, ifcopenshell.entity_instance]:
def exclude_callback(attribute):
return attribute.is_a("IfcProfileDef") and attribute.ProfileName
copied_entities: dict[int, ifcopenshell.entity_instance] = {}
if dest.is_a("IfcProduct"):
if not source.Representation:
return
return copied_entities
dest.Representation = ifcopenshell.util.element.copy_deep(
tool.Ifc.get(),
source.Representation,
exclude=["IfcGeometricRepresentationContext"],
exclude_callback=exclude_callback,
copied_entities=copied_entities,
)
elif dest.is_a("IfcTypeProduct"):
if not source.RepresentationMaps:
return
return copied_entities
dest.RepresentationMaps = [
ifcopenshell.util.element.copy_deep(
tool.Ifc.get(), m, exclude=["IfcGeometricRepresentationContext"], exclude_callback=exclude_callback
tool.Ifc.get(),
m,
exclude=["IfcGeometricRepresentationContext"],
exclude_callback=exclude_callback,
copied_entities=copied_entities,
)
for m in source.RepresentationMaps
]
return copied_entities
@classmethod
def does_type_have_representations(cls, element: ifcopenshell.entity_instance) -> bool: