diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index a1a377d2ee..ae1061903a 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -181,6 +181,7 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator): core.duplicate_drawing( tool.Ifc, tool.Drawing, + tool.Geometry, drawing=tool.Ifc.get().by_id(self.drawing), should_duplicate_annotations=self.should_duplicate_annotations, ) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 70121aa88e..8487df2365 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -965,8 +965,6 @@ class OverrideDuplicateMove(bpy.types.Operator): # Deep magick from the dawn of time if tool.Ifc.get(): IfcStore.execute_ifc_operator(self, context) - if self.new_active_obj: - context.view_layer.objects.active = self.new_active_obj return {"FINISHED"} if linked: @@ -977,227 +975,13 @@ class OverrideDuplicateMove(bpy.types.Operator): @staticmethod def execute_ifc_duplicate_operator(self, context, linked=False): - objects_to_duplicate = set(context.selected_objects) - - # handle arrays - arrays_to_duplicate, array_children = OverrideDuplicateMove.process_arrays(self, context) - objects_to_duplicate -= array_children - for child in array_children: - child.select_set(False) - - self.new_active_obj = None - # Track decompositions so they can be recreated after the operation - decomposition_relationships = tool.Root.get_decomposition_relationships(objects_to_duplicate) - connection_relationships = tool.Root.get_connection_relationships(objects_to_duplicate) - old_to_new = {} - - for obj in objects_to_duplicate: - element = tool.Ifc.get_entity(obj) - if element: - if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING": - self.report( - {"INFO"}, "Did not duplicate. Duplicate drawings through the Drawings and Documents UI." - ) - obj.select_set(False) - continue # For now, don't copy drawings until we stabilise a bit more. It's tricky. - elif tool.Geometry.is_locked(element): - tool.Blender.deselect_object(obj, ensure_active_object=True) - self.report({"ERROR"}, lock_error_message(obj.name)) - continue - elif tool.Geometry.is_representation_item(obj): - OverrideDuplicateMove.duplicate_item(obj) - continue - - tracked_opening_type = tool.Model.get_tracked_opening_type(obj) - is_tracked_opening = bool(tracked_opening_type) - keep_data_linked = linked and not element and not is_tracked_opening - - # Prior to duplicating, sync the object placement to make decomposition recreation more stable. - if tool.Ifc.is_moved(obj): - bonsai.core.geometry.edit_object_placement( - tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj, apply_scale=False - ) - - new_obj = obj.copy() - temp_data = None - - # Currently for optimization we do not apply pending changes (scale or changed .data) - # to the original and duplicated objects. - # Keep new object edited if original is. - if tool.Ifc.is_edited(obj, ignore_scale=True): - tool.Ifc.edit(new_obj) - - if obj.data and not keep_data_linked: - # assure root.copy_class won't replace the previous mesh globally - temp_data = obj.data.copy() - new_obj.data = temp_data - - # Unlink from previous boolean element - # and keep object tracked for decorations. - if is_tracked_opening: - mprops = tool.Geometry.get_mesh_props(new_obj.data) - mprops.ifc_boolean_id = 0 - tool.Root.add_tracked_opening(new_obj, tracked_opening_type) - - if obj == context.active_object: - self.new_active_obj = new_obj - for collection in obj.users_collection: - collection.objects.link(new_obj) - obj.select_set(False) - new_obj.select_set(True) - - if not element: - continue - - # clear object's collection so it will be able to have it's own - tool.Blender.get_object_bim_props(new_obj).collection = None - # copy the actual class - new = bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj) - - # clean up the orphaned mesh with ifc id of the original object to avoid confusion - # IfcGridAxis keeps the same mesh data (it's pointing to ifc id 0, so it's not a problem) - if new and temp_data and not new.is_a("IfcGridAxis"): - if new.is_a("IfcRelSpaceBoundary"): - surface = new.ConnectionGeometry.SurfaceOnRelatingElement - temp_data.name = f"0/{surface.id()}" - tool.Ifc.link(surface, temp_data) - else: - tool.Blender.remove_data_block(temp_data) - - if new: - # TODO: handle array data for other cases of duplication - array_data = arrays_to_duplicate.get(obj, None) - tool.Model.handle_array_on_copied_element(new, array_data) - if array_data: - for child in tool.Blender.Modifier.Array.get_all_children_objects(new): - child.select_set(True) - - # TODO: add new array children to recreate their decomposition too - old_to_new[element] = [new] - if new.is_a("IfcRelSpaceBoundary"): - tool.Boundary.decorate_boundary(new_obj) - - # Recreate aggregate relationship - for old in old_to_new.keys(): - if old.is_a("IfcElementAssembly"): - tool.Root.recreate_aggregate(old_to_new) - - # Remove connections with old objects and recreates paths - OverrideDuplicateMove.remove_old_connections(old_to_new) - tool.Root.recreate_connections(connection_relationships, old_to_new) - - # Recreate decompositions - tool.Root.recreate_decompositions(decomposition_relationships, old_to_new) - OverrideDuplicateMove.remove_linked_aggregate_data(old_to_new) - bonsai.bim.handler.refresh_ui_data() - tool.Root.reload_grid_decorator() + old_to_new, new_active_object = tool.Geometry.duplicate_ifc_objects( + set(context.selected_objects), linked=linked, active_object=context.active_object + ) + if new_active_obj: + context.view_layer.objects.active = new_active_obj return old_to_new - @staticmethod - def duplicate_item(obj: bpy.types.Object) -> None: - props = tool.Geometry.get_geometry_props() - item = tool.Geometry.get_active_representation(obj) - assert item - new_item = ifcopenshell.util.element.copy_deep(tool.Ifc.get(), item) - new_obj = obj.copy() - assert tool.Geometry.has_mesh_properties(obj.data) - temp_data = obj.data.copy() - new_obj.data = temp_data - tool.Ifc.link(new_item, temp_data) - new_obj.name = obj.data.name = f"Item/{new_item.is_a()}/{new_item.id()}" - props.add_item_object(new_obj, new_item) - - for collection in obj.users_collection: - collection.objects.link(new_obj) - - representation = tool.Geometry.get_active_representation(props.representation_obj) - representation = ifcopenshell.util.representation.resolve_representation(representation) - representation.Items = list(representation.Items) + [new_item] - - tool.Geometry.reload_representation(props.representation_obj) - - obj.select_set(False) - tool.Root.reload_item_decorator() - - @staticmethod - def process_arrays( - self, context: bpy.types.Context - ) -> tuple[dict[bpy.types.Object, Any], set[ifcopenshell.entity_instance]]: - """ "Process arrays for currently selected objects. - - :return: A tuple of two elements:\n - - dictionary of objects and their array data. Those objects are safe to duplicate and regenerate arrays using the data.\n - - set of array children objects. Those objects can be ignored during duplication, they will be recreated automatically - when arrays are regenerated for objects from the dictionary. - """ - selected_objects = set(context.selected_objects) - array_parents = set() - arrays_to_create: dict[bpy.types.Object, Any] = dict() - array_children: set[ifcopenshell.entity_instance] = set() # will be ignored during the duplication - - for obj in context.selected_objects: - element = tool.Ifc.get_entity(obj) - if not element: - continue - pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array") - if not pset: - continue - array_parents.add(tool.Ifc.get().by_guid(pset["Parent"])) - - for array_parent in array_parents: - array_parent_obj = tool.Ifc.get_object(array_parent) - if array_parent_obj not in selected_objects: - continue - - array_data = [] - for modifier_data in tool.Blender.Modifier.Array.get_modifiers_data(array_parent): - children = set(tool.Blender.Modifier.Array.get_children_objects(modifier_data)) - if children.issubset(selected_objects): - modifier_data["children"] = [] - array_data.append(modifier_data) - array_children.update(children) - else: - break # allows to duplicate only n first layers of an array - - if array_data: - arrays_to_create[array_parent_obj] = array_data - - return arrays_to_create, array_children - - def remove_old_connections(old_to_new): - single_obj = False - if len(old_to_new) == 1: - single_obj = True - - for new in old_to_new.values(): - if not hasattr(new[0], "ConnectedTo"): - continue - for connection in new[0].ConnectedTo: - entity = connection.RelatedElement - if entity in old_to_new.keys() or single_obj: - core.remove_connection(tool.Geometry, connection=connection) - for connection in new[0].ConnectedFrom: - entity = connection.RelatingElement - if entity in old_to_new.keys() or single_obj: - core.remove_connection(tool.Geometry, connection=connection) - - def remove_linked_aggregate_data(old_to_new): - for old, new in old_to_new.items(): - pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Linked_Aggregate") - if pset: - pset = tool.Ifc.get().by_id(pset["id"]) - ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new[0], pset=pset) - - if new[0].is_a("IfcElementAssembly"): - linked_aggregate_group = [ - r.RelatingGroup - for r in getattr(new[0], "HasAssignments", []) or [] - if r.is_a("IfcRelAssignsToGroup") - if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name - ] - if linked_aggregate_group: - tool.Ifc.run("group.unassign_group", group=linked_aggregate_group[0], products=[new[0]]) - class OverrideDuplicateMoveLinkedMacro(bpy.types.Macro): bl_idname = "bim.override_object_duplicate_move_linked_macro" diff --git a/src/bonsai/bonsai/core/drawing.py b/src/bonsai/bonsai/core/drawing.py index c01b70bd55..a87a5a8eee 100644 --- a/src/bonsai/bonsai/core/drawing.py +++ b/src/bonsai/bonsai/core/drawing.py @@ -297,6 +297,7 @@ def add_drawing( def duplicate_drawing( ifc: tool.Ifc, drawing_tool: tool.Drawing, + geometry: tool.Geometry, drawing: ifcopenshell.entity_instance, should_duplicate_annotations: bool = False, ) -> ifcopenshell.entity_instance: @@ -310,13 +311,14 @@ def duplicate_drawing( ifc.run("group.edit_group", group=new_group, attributes={"Name": drawing_name, "ObjectType": "DRAWING"}) ifc.run("group.assign_group", group=new_group, products=[new_drawing]) if should_duplicate_annotations: - for annotation in drawing_tool.get_group_elements(group): - if annotation == drawing: - continue - new_annotation = ifc.run("root.copy_class", product=annotation) - drawing_tool.copy_representation(annotation, new_annotation) - ifc.run("group.unassign_group", group=group, products=[new_annotation]) - ifc.run("group.assign_group", group=new_group, products=[new_annotation]) + new_annotations = [] + annotation_objs = [ifc.get_object(a) for a in drawing_tool.get_group_elements(group) if a != drawing] + old_to_new, _ = geometry.duplicate_ifc_objects(annotation_objs) + for new_elements in old_to_new.values(): + new_annotations.extend(new_elements) + new_annotation = set(new_annotations) + ifc.run("group.unassign_group", group=group, products=new_annotations) + ifc.run("group.assign_group", group=new_group, products=new_annotations) old_reference = drawing_tool.get_drawing_document(new_drawing) ifc.run("document.unassign_document", products=[new_drawing], document=old_reference) diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index e97fd5c869..70d5101a75 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -2015,3 +2015,229 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def run_edit_object_placement(cls, obj: bpy.types.Object) -> None: return bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + + @classmethod + def duplicate_ifc_objects( + cls, objects_to_duplicate: Iterable[bpy.types.Object], active_object=None, linked=False + ) -> dict: + # Handle arrays + objects_to_duplicate = set(objects_to_duplicate) + arrays_to_duplicate, array_children = cls.process_arrays_for_duplication(objects_to_duplicate) + objects_to_duplicate -= array_children + for child in array_children: + child.select_set(False) + + new_active_obj = None + # Track decompositions so they can be recreated after the operation + decomposition_relationships = tool.Root.get_decomposition_relationships(objects_to_duplicate) + connection_relationships = tool.Root.get_connection_relationships(objects_to_duplicate) + old_to_new = {} + + for obj in objects_to_duplicate: + element = tool.Ifc.get_entity(obj) + if element: + if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING": + self.report( + {"INFO"}, "Did not duplicate. Duplicate drawings through the Drawings and Documents UI." + ) + obj.select_set(False) + continue # For now, don't copy drawings until we stabilise a bit more. It's tricky. + elif tool.Geometry.is_locked(element): + tool.Blender.deselect_object(obj, ensure_active_object=True) + self.report({"ERROR"}, lock_error_message(obj.name)) + continue + elif tool.Geometry.is_representation_item(obj): + cls.duplicate_ifc_item(obj) + continue + + tracked_opening_type = tool.Model.get_tracked_opening_type(obj) + is_tracked_opening = bool(tracked_opening_type) + keep_data_linked = linked and not element and not is_tracked_opening + + # Prior to duplicating, sync the object placement to make decomposition recreation more stable. + if tool.Ifc.is_moved(obj): + bonsai.core.geometry.edit_object_placement( + tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj, apply_scale=False + ) + + new_obj = obj.copy() + temp_data = None + + # Currently for optimization we do not apply pending changes (scale or changed .data) + # to the original and duplicated objects. + # Keep new object edited if original is. + if tool.Ifc.is_edited(obj, ignore_scale=True): + tool.Ifc.edit(new_obj) + + if obj.data and not keep_data_linked: + # assure root.copy_class won't replace the previous mesh globally + temp_data = obj.data.copy() + new_obj.data = temp_data + + # Unlink from previous boolean element + # and keep object tracked for decorations. + if is_tracked_opening: + mprops = tool.Geometry.get_mesh_props(new_obj.data) + mprops.ifc_boolean_id = 0 + tool.Root.add_tracked_opening(new_obj, tracked_opening_type) + + if obj == active_object: + new_active_obj = new_obj + for collection in obj.users_collection: + collection.objects.link(new_obj) + obj.select_set(False) + new_obj.select_set(True) + + if not element: + continue + + # clear object's collection so it will be able to have it's own + tool.Blender.get_object_bim_props(new_obj).collection = None + # copy the actual class + new = bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj) + + # clean up the orphaned mesh with ifc id of the original object to avoid confusion + # IfcGridAxis keeps the same mesh data (it's pointing to ifc id 0, so it's not a problem) + if new and temp_data and not new.is_a("IfcGridAxis"): + if new.is_a("IfcRelSpaceBoundary"): + surface = new.ConnectionGeometry.SurfaceOnRelatingElement + temp_data.name = f"0/{surface.id()}" + tool.Ifc.link(surface, temp_data) + else: + tool.Blender.remove_data_block(temp_data) + + if new: + # TODO: handle array data for other cases of duplication + array_data = arrays_to_duplicate.get(obj, None) + tool.Model.handle_array_on_copied_element(new, array_data) + if array_data: + for child in tool.Blender.Modifier.Array.get_all_children_objects(new): + child.select_set(True) + + # TODO: add new array children to recreate their decomposition too + old_to_new[element] = [new] + if new.is_a("IfcRelSpaceBoundary"): + tool.Boundary.decorate_boundary(new_obj) + + # Recreate aggregate relationship + for old in old_to_new.keys(): + if old.is_a("IfcElementAssembly"): + tool.Root.recreate_aggregate(old_to_new) + + # Remove connections with old objects and recreates paths + cls.remove_old_connections(old_to_new) + tool.Root.recreate_connections(connection_relationships, old_to_new) + + # Recreate decompositions + tool.Root.recreate_decompositions(decomposition_relationships, old_to_new) + cls.remove_linked_aggregate_data(old_to_new) + bonsai.bim.handler.refresh_ui_data() + tool.Root.reload_grid_decorator() + return old_to_new, active_object + + @classmethod + def duplicate_ifc_item(cls, obj: bpy.types.Object) -> None: + props = tool.Geometry.get_geometry_props() + item = tool.Geometry.get_active_representation(obj) + assert item + new_item = ifcopenshell.util.element.copy_deep(tool.Ifc.get(), item) + new_obj = obj.copy() + assert tool.Geometry.has_mesh_properties(obj.data) + temp_data = obj.data.copy() + new_obj.data = temp_data + tool.Ifc.link(new_item, temp_data) + new_obj.name = obj.data.name = f"Item/{new_item.is_a()}/{new_item.id()}" + props.add_item_object(new_obj, new_item) + + for collection in obj.users_collection: + collection.objects.link(new_obj) + + representation = tool.Geometry.get_active_representation(props.representation_obj) + representation = ifcopenshell.util.representation.resolve_representation(representation) + representation.Items = list(representation.Items) + [new_item] + + tool.Geometry.reload_representation(props.representation_obj) + + obj.select_set(False) + tool.Root.reload_item_decorator() + + @classmethod + def process_arrays_for_duplication( + cls, objects_to_duplicate: Iterable[bpy.types.Object] + ) -> tuple[dict[bpy.types.Object, Any], set[ifcopenshell.entity_instance]]: + """ "Process arrays for currently selected objects. + + :return: A tuple of two elements:\n + - dictionary of objects and their array data. Those objects are safe to duplicate and regenerate arrays using the data.\n + - set of array children objects. Those objects can be ignored during duplication, they will be recreated automatically + when arrays are regenerated for objects from the dictionary. + """ + selected_objects = set(objects_to_duplicate) + array_parents = set() + arrays_to_create: dict[bpy.types.Object, Any] = dict() + array_children: set[ifcopenshell.entity_instance] = set() # will be ignored during the duplication + + for obj in objects_to_duplicate: + element = tool.Ifc.get_entity(obj) + if not element: + continue + pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array") + if not pset: + continue + array_parents.add(tool.Ifc.get().by_guid(pset["Parent"])) + + for array_parent in array_parents: + array_parent_obj = tool.Ifc.get_object(array_parent) + if array_parent_obj not in selected_objects: + continue + + array_data = [] + for modifier_data in tool.Blender.Modifier.Array.get_modifiers_data(array_parent): + children = set(tool.Blender.Modifier.Array.get_children_objects(modifier_data)) + if children.issubset(selected_objects): + modifier_data["children"] = [] + array_data.append(modifier_data) + array_children.update(children) + else: + break # allows to duplicate only n first layers of an array + + if array_data: + arrays_to_create[array_parent_obj] = array_data + + return arrays_to_create, array_children + + @classmethod + def remove_old_connections(cls, old_to_new): + single_obj = False + if len(old_to_new) == 1: + single_obj = True + + for new in old_to_new.values(): + if not hasattr(new[0], "ConnectedTo"): + continue + for connection in new[0].ConnectedTo: + entity = connection.RelatedElement + if entity in old_to_new.keys() or single_obj: + core.remove_connection(tool.Geometry, connection=connection) + for connection in new[0].ConnectedFrom: + entity = connection.RelatingElement + if entity in old_to_new.keys() or single_obj: + core.remove_connection(tool.Geometry, connection=connection) + + @classmethod + def remove_linked_aggregate_data(cls, old_to_new): + for old, new in old_to_new.items(): + pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Linked_Aggregate") + if pset: + pset = tool.Ifc.get().by_id(pset["id"]) + ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new[0], pset=pset) + + if new[0].is_a("IfcElementAssembly"): + linked_aggregate_group = [ + r.RelatingGroup + for r in getattr(new[0], "HasAssignments", []) or [] + if r.is_a("IfcRelAssignsToGroup") + if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name + ] + if linked_aggregate_group: + tool.Ifc.run("group.unassign_group", group=linked_aggregate_group[0], products=[new[0]])