diff --git a/src/blenderbim/blenderbim/bim/module/geometry/__init__.py b/src/blenderbim/blenderbim/bim/module/geometry/__init__.py index 90642c3375..db955f013f 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/__init__.py @@ -27,10 +27,10 @@ classes = ( operator.EnableEditingRepresentationItems, operator.FlipObject, operator.GetRepresentationIfcParameters, + operator.DuplicateMoveLinkedAggregate, + operator.DuplicateMoveLinkedAggregateMacro, operator.OverrideDelete, operator.OverrideDuplicateMove, - operator.OverrideDuplicateMoveAggregate, - operator.OverrideDuplicateMoveAggregateMacro, operator.OverrideDuplicateMoveLinked, operator.OverrideDuplicateMoveLinkedMacro, operator.OverrideDuplicateMoveMacro, @@ -42,7 +42,7 @@ classes = ( operator.OverrideOutlinerDelete, operator.OverridePasteBuffer, operator.PurgeUnusedRepresentations, - operator.RefreshAggregate, + operator.RefreshLinkedAggregate, operator.RemoveConnection, operator.RemoveRepresentation, operator.SelectConnection, @@ -74,8 +74,8 @@ def register(): operator.OverrideDuplicateMoveMacro.define("TRANSFORM_OT_translate") operator.OverrideDuplicateMoveLinkedMacro.define("BIM_OT_override_object_duplicate_move_linked") operator.OverrideDuplicateMoveLinkedMacro.define("TRANSFORM_OT_translate") - operator.OverrideDuplicateMoveAggregateMacro.define("BIM_OT_override_object_duplicate_move_aggregate") - operator.OverrideDuplicateMoveAggregateMacro.define("TRANSFORM_OT_translate") + operator.DuplicateMoveLinkedAggregateMacro.define("BIM_OT_object_duplicate_move_linked_aggregate") + operator.DuplicateMoveLinkedAggregateMacro.define("TRANSFORM_OT_translate") bpy.types.Object.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMObjectGeometryProperties) bpy.types.Scene.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMGeometryProperties) @@ -92,9 +92,7 @@ def register(): addon_keymaps.append((km, kmi)) kmi = km.keymap_items.new("bim.override_object_duplicate_move_linked_macro", "D", "PRESS", alt=True) addon_keymaps.append((km, kmi)) - kmi = km.keymap_items.new( - "bim.override_object_duplicate_move_aggregate_macro", "D", "PRESS", ctrl=True, shift=True - ) + kmi = km.keymap_items.new("bim.object_duplicate_move_linked_aggregate_macro", "D", "PRESS", ctrl=True, shift=True) addon_keymaps.append((km, kmi)) kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True) addon_keymaps.append((km, kmi)) diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index 3ca6b673f4..617fc34cd2 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -759,7 +759,8 @@ class OverrideDuplicateMove(bpy.types.Operator): self.new_active_obj = None # Track decompositions so they can be recreated after the operation - relationships = tool.Root.get_decomposition_relationships(objects_to_duplicate) + 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: @@ -819,9 +820,19 @@ class OverrideDuplicateMove(bpy.types.Operator): 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(relationships, old_to_new) + tool.Root.recreate_decompositions(decomposition_relationships, old_to_new) blenderbim.bim.handler.refresh_ui_data() + return old_to_new @staticmethod def process_arrays(self, context): @@ -860,6 +871,18 @@ class OverrideDuplicateMove(bpy.types.Operator): return arrays_to_create, array_children + def remove_old_connections(old_to_new): + for new in old_to_new.values(): + for connection in new[0].ConnectedTo: + entity = connection.RelatedElement + if entity in old_to_new.keys(): + core.remove_connection(tool.Geometry, connection=connection) + for connection in new[0].ConnectedFrom: + entity = connection.RelatingElement + if entity in old_to_new.keys(): + core.remove_connection(tool.Geometry, connection=connection) + + class OverrideDuplicateMoveLinkedMacro(bpy.types.Macro): bl_idname = "bim.override_object_duplicate_move_linked_macro" bl_label = "IFC Duplicate Linked" @@ -882,15 +905,15 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator): return OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True) -class OverrideDuplicateMoveAggregateMacro(bpy.types.Macro): - bl_idname = "bim.override_object_duplicate_move_aggregate_macro" - bl_label = "IFC Duplicate Objects Aggregate" +class DuplicateMoveLinkedAggregateMacro(bpy.types.Macro): + bl_idname = "bim.object_duplicate_move_linked_aggregate_macro" + bl_label = "IFC Duplicate Linked Aggregate" bl_options = {"REGISTER", "UNDO"} -class OverrideDuplicateMoveAggregate(bpy.types.Operator): - bl_idname = "bim.override_object_duplicate_move_aggregate" - bl_label = "IFC Duplicate Objects Aggregate" +class DuplicateMoveLinkedAggregate(bpy.types.Operator): + bl_idname = "bim.object_duplicate_move_linked_aggregate" + bl_label = "IFC Duplicate Linked Aggregate" bl_options = {"REGISTER", "UNDO"} is_interactive: bpy.props.BoolProperty(name="Is Interactive", default=True) @@ -902,263 +925,103 @@ class OverrideDuplicateMoveAggregate(bpy.types.Operator): return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=False) def _execute(self, context): + return DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(self, context) + + @staticmethod + def execute_ifc_duplicate_linked_aggregate_operator(self, context): self.new_active_obj = None + self.group_name = "BBIM_Linked_Aggregate" + self.pset_name = "BBIM_Linked_Aggregate" old_to_new = {} - ### Adding the assembly data - def add_assembly_data(element, parent, data_to_add): - pset = ifcopenshell.util.element.get_pset(element, "BBIM_Aggregate_Data") - data = [data_to_add] + def select_objects_and_add_data(element): + add_linked_aggregate_group(element) + obj = tool.Ifc.get_object(element) + obj.select_set(True) + parts = ifcopenshell.util.element.get_parts(element) + if parts: + index = 0 + for part in parts: + if part.is_a("IfcElementAssembly"): + select_objects_and_add_data(part) - if pset: - if parent == None: - ifcopenshell.api.run( - "pset.edit_pset", - tool.Ifc.get(), - pset=tool.Ifc.get().by_id(pset["id"]), - properties={"Data": tool.Ifc.get().createIfcText(json.dumps(data))}, - ) - else: - ifcopenshell.api.run( - "pset.edit_pset", - tool.Ifc.get(), - pset=tool.Ifc.get().by_id(pset["id"]), - properties={"Parent": parent.GlobalId, "Data": tool.Ifc.get().createIfcText(json.dumps(data))}, - ) + add_linked_aggregate_pset(part, index) + index += 1 + + obj = tool.Ifc.get_object(part) + obj.select_set(True) - else: + def add_linked_aggregate_group(element): + linked_aggregate_group = None + product_groups_name = [ + r.RelatingGroup.Name + for r in getattr(element, "HasAssignments", []) or [] + if r.is_a("IfcRelAssignsToGroup") + ] + if self.group_name in product_groups_name: + return + + linked_aggregate_group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.group_name) + ifcopenshell.api.run( + "group.assign_group", tool.Ifc.get(), products=[element], group=linked_aggregate_group + ) + + def add_linked_aggregate_pset(part, index): + pset = ifcopenshell.util.element.get_pset(part, self.pset_name) + + if not pset: pset = ifcopenshell.api.run( - "pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Aggregate_Data" + "pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name ) - - ifcopenshell.api.run( - "pset.edit_pset", - tool.Ifc.get(), - pset=pset, - properties={"Parent": parent.GlobalId, "Data": tool.Ifc.get().createIfcText(json.dumps(data))}, - ) - - def add_child_to_assembly_data(new_entity): - pset = ifcopenshell.util.element.get_pset(new_entity, "BBIM_Aggregate_Data") - parent_element = tool.Ifc.get().by_guid(pset["Parent"]) - parent_pset = ifcopenshell.util.element.get_pset(parent_element, "BBIM_Aggregate_Data") - data = json.loads(parent_pset["Data"]) - data[0]["children"].append(new_entity.GlobalId) + else: + pset = tool.Ifc.get().by_id(pset["id"]) ifcopenshell.api.run( "pset.edit_pset", tool.Ifc.get(), - pset=tool.Ifc.get().by_id(parent_pset["id"]), - properties={"Data": tool.Ifc.get().createIfcText(json.dumps(data))}, + pset=pset, + properties={"Index": index}, ) - def create_data_structure(entity, level=-1): - level += 1 - - data_children = { - "children": [], - "instance_of": [], - } - - data_parent = { - "children": [], - "instance_of": [entity.GlobalId], - } - - if not entity.is_a("IfcElementAssembly"): - return - else: - if level == 0: - add_assembly_data(entity, entity, data_parent) - else: - add_assembly_data(entity, None, data_parent) - parts = ifcopenshell.util.element.get_parts(entity) - for part in parts: - if part.is_a("IfcElementAssembly"): - add_assembly_data(part, entity, data_children) - add_child_to_assembly_data(part) - create_data_structure(part, level) - continue - add_assembly_data(part, entity, data_children) - add_child_to_assembly_data(part) - - return - - def recreate_data_structure(entity, level=-1): - level += 1 - - pset = ifcopenshell.util.element.get_pset(entity, "BBIM_Aggregate_Data") - pset_data = json.loads(pset["Data"])[0] - instance_of = pset_data["instance_of"] - - data_children = { - "children": [], - "instance_of": [], - } - - # Keeps instance ID through all copies - data_parent = { - "children": [], - "instance_of": instance_of, - } - - if not entity.is_a("IfcElementAssembly"): - return - else: - if level == 0: - add_assembly_data(entity, entity, data_parent) - else: - add_assembly_data(entity, None, data_parent) - parts = ifcopenshell.util.element.get_parts(entity) - for part in parts: - if part.is_a("IfcElementAssembly"): - add_child_to_assembly_data(part) - recreate_data_structure(part, level) - continue - add_assembly_data(part, entity, data_children) - add_child_to_assembly_data(part) - - return - - def duplicate_all(obj, level=-1, new_parent=None, parents=[]): - level += 1 - - entity = tool.Ifc.get_entity(obj) - - if level == 0: - new_parent = duplicate_objects(obj) - parents.append(new_parent) - else: - pair = [] - pair.append(new_parent) - new_parent = duplicate_objects(obj) - pair.append(new_parent) - parents.append(pair) - - if not entity.is_a("IfcElementAssembly"): - return - else: - parts = ifcopenshell.util.element.get_parts(entity) - - # Ensures that we are duplication all the IfcElementAssembly - # before duplicating the nested objects - for part in parts: - if part.is_a("IfcElementAssembly"): - pass - else: - part_obj = tool.Ifc.get_object(part) - new_part = duplicate_objects(part_obj) - blenderbim.core.aggregate.assign_object( - tool.Ifc, - tool.Aggregate, - tool.Collector, - relating_obj=tool.Ifc.get_object(new_parent), - related_obj=tool.Ifc.get_object(new_part), - ) - - for part in parts: - if part.is_a("IfcElementAssembly"): - part_obj = tool.Ifc.get_object(part) - - # Recursion Call - duplicate_all(part_obj, level, new_parent, parents) - - if level == 0: - for p in parents[1:]: - blenderbim.core.aggregate.assign_object( - tool.Ifc, - tool.Aggregate, - tool.Collector, - relating_obj=tool.Ifc.get_object(p[0]), - related_obj=tool.Ifc.get_object(p[1]), - ) - - return new_parent - return - - def duplicate_objects(obj, is_root=False): - new_obj = obj.copy() - if obj.data: - new_obj.data = obj.data.copy() - 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) - - # This is needed to make sure the new object gets unlink from - # the old object assembly collection - new_obj.BIMObjectProperties.collection = None - - # Copy the actual class - new_entity = blenderbim.core.root.copy_class( - tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj - ) - - if new_entity: - tool.Model.handle_array_on_copied_element(new_entity) - blenderbim.core.aggregate.unassign_object( - tool.Ifc, - tool.Aggregate, - tool.Collector, - relating_obj=tool.Ifc.get_object(selected_root_entity), - related_obj=tool.Ifc.get_object(new_entity), - ) - - old_to_new[tool.Ifc.get_entity(obj)] = [new_entity] - - return new_entity - - ### Check if only one element and it's assembly + return index + + if len(context.selected_objects) != 1: return {"FINISHED"} selected_obj = context.selected_objects[0] - selected_root_entity = tool.Ifc.get_entity(selected_obj) + selected_element = tool.Ifc.get_entity(selected_obj) - if not selected_root_entity.is_a("IfcElementAssembly"): + if selected_element.is_a("IfcElementAssembly"): + pass + elif selected_element.Decomposes: + if selected_element.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"): + selected_element = selected_element.Decomposes[0].RelatingObject + selected_obj = tool.Ifc.get_object(selected_element) + else: + self.report({"INFO"}, "Object is not part of a IfcElementAssembly.") return {"FINISHED"} - pset = ifcopenshell.util.element.get_pset(selected_root_entity, "BBIM_Aggregate_Data") - if not pset: - create_data_structure(selected_root_entity) - pset = ifcopenshell.util.element.get_pset(selected_root_entity, "BBIM_Aggregate_Data") + select_objects_and_add_data(selected_element) - selected_root_parent = selected_root_entity + old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True) - new_root_entity = duplicate_all(selected_obj) + + # Recreate aggregate relationship + for old in old_to_new.keys(): + if old.is_a("IfcElementAssembly"): + tool.Root.recreate_aggregate(old_to_new) - recreate_data_structure(new_root_entity) - - # Remove connections with old objects - for new in old_to_new.values(): - for connection in new[0].ConnectedTo: - entity = connection.RelatedElement - if entity in old_to_new.keys(): - core.remove_connection(tool.Geometry, connection=connection) - for connection in new[0].ConnectedFrom: - entity = connection.RelatingElement - if entity in old_to_new.keys(): - core.remove_connection(tool.Geometry, connection=connection) - - old_objs = [] - for old, new in old_to_new.items(): - old_objs.append(tool.Ifc.get_object(old)) - - relationships = tool.Root.get_decomposition_relationships(old_objs) - - tool.Root.recreate_decompositions(relationships, old_to_new) blenderbim.bim.handler.refresh_ui_data() - return {"FINISHED"} + return old_to_new -class RefreshAggregate(bpy.types.Operator): - bl_idname = "bim.refresh_aggregate" - bl_label = "IFC Refresh Aggregate" +class RefreshLinkedAggregate(bpy.types.Operator): + bl_idname = "bim.refresh_linked_aggregate" + bl_label = "IFC Refresh Linked Aggregate" bl_options = {"REGISTER", "UNDO"} @classmethod @@ -1174,214 +1037,132 @@ class RefreshAggregate(bpy.types.Operator): return {"FINISHED"} def _execute(self, context): - refresh_start_time = time() self.new_active_obj = None + self.group_name = 'BBIM_Linked_Aggregate' + self.pset_name = "BBIM_Linked_Aggregate" + refresh_start_time = time() old_to_new = {} - def remove_objects(entity, level=-1, parents=[]): - level += 1 - if level == 0: - parents = [entity] - parts = ifcopenshell.util.element.get_parts(entity) - for part in parts: - if part.is_a("IfcElementAssembly"): - parents.append(part) - remove_objects(part, level, parents) - continue - else: - part_obj = tool.Ifc.get_object(part) - if part_obj: - tool.Geometry.delete_ifc_object(part_obj) + def delete_objects(element): + parts = ifcopenshell.util.element.get_parts(element) + if parts: + for part in parts: + if part.is_a("IfcElementAssembly"): + delete_objects(part) + + tool.Geometry.delete_ifc_object(tool.Ifc.get_object(part)) + + tool.Geometry.delete_ifc_object(tool.Ifc.get_object(element)) - return parents - - def duplicate_children(entity): - pset = ifcopenshell.util.element.get_pset(entity, "BBIM_Aggregate_Data") - pset_data = json.loads(pset["Data"])[0] - instance_of = pset_data["instance_of"][0] - - instance_entity = tool.Ifc.get().by_guid(instance_of) - - if not instance_entity.is_a("IfcElementAssembly"): - return + def get_element_assembly(element): + if element.is_a("IfcElementAssembly"): + return element + elif element.Decomposes: + if element.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"): + element = element.Decomposes[0].RelatingObject + return element else: - parts = ifcopenshell.util.element.get_parts(instance_entity) - for part in parts: - pset = ifcopenshell.util.element.get_pset(part, "BBIM_Aggregate_Data") - if part.is_a("IfcElementAssembly"): - if not pset: - data_children = { - "children": [], - "instance_of": [part.GlobalId], - } - data = [data_children] + return None - pset = ifcopenshell.api.run( - "pset.add_pset", tool.Ifc.get(), product=part, name="BBIM_Aggregate_Data" - ) - ifcopenshell.api.run( - "pset.edit_pset", - tool.Ifc.get(), - pset=pset, - properties={ - "Parent": instance_entity.GlobalId, - "Data": tool.Ifc.get().createIfcText(json.dumps(data)), - }, - ) + def handle_selection(selected_objs): + selected_elements = [tool.Ifc.get_entity(selected_obj) for selected_obj in selected_objs] + if None in selected_elements: + self.report({"INFO"}, "Object has no Ifc Metadata.") + return None, None - part_obj = tool.Ifc.get_object(part) - new_part = duplicate_objects(part_obj) + selected_parents = [] + for selected_element in selected_elements: + selected_element = get_element_assembly(selected_element) + if not selected_element: + self.report({"INFO"}, "Object is not part of a IfcElementAssembly.") + return None, None + selected_parents.append(selected_element) + selected_parents = list(set(selected_parents)) + linked_aggregate_groups = [] + for selected_element in selected_parents: + product_linked_agg_group = [ + r.RelatingGroup + for r in getattr(selected_element, "HasAssignments", []) or [] + if r.is_a("IfcRelAssignsToGroup") + if self.group_name in r.RelatingGroup.Name + ] + try: + linked_aggregate_groups.append(product_linked_agg_group[0].id()) + except: + self.report({"INFO"}, "Object is not part of a Linked Aggregate.") + return None, None + + return list(set(linked_aggregate_groups)), selected_parents + + active_element = tool.Ifc.get_entity(context.active_object) + if not active_element: + self.report({"INFO"}, "Object has no Ifc metadata.") + return {"FINISHED"} + + active_element = get_element_assembly(active_element) + selected_objs = context.selected_objects + linked_aggregate_groups, selected_parents = handle_selection(selected_objs) + if not linked_aggregate_groups or not selected_parents: + return {"FINISHED"} + + if len(linked_aggregate_groups) > 1: + if len(selected_parents) != len(linked_aggregate_groups): + self.report({"INFO"}, "Select only one object from each Linked Aggregate or multiple objects from the same Linked Aggregate.") + return {"FINISHED"} + + for group in linked_aggregate_groups: + elements = tool.Drawing.get_group_elements(tool.Ifc.get().by_id(group)) + if len(linked_aggregate_groups) > 1: + base_instance = [e for e in elements if e in selected_parents][0] + instances_to_refresh = elements + + elif (len(linked_aggregate_groups) == 1) and (len(selected_parents) > 1): + base_instance = active_element + instances_to_refresh = [element for element in elements if element in selected_parents] + + else: + base_instance = active_element + instances_to_refresh = elements + + for element in instances_to_refresh: + if element.GlobalId == base_instance.GlobalId: + continue + + element_aggregate = ifcopenshell.util.element.get_aggregate(element) + + selected_obj = tool.Ifc.get_object(base_instance) + selected_matrix = selected_obj.matrix_world + object_duplicate = tool.Ifc.get_object(element) + duplicate_matrix = object_duplicate.matrix_world.decompose() + + delete_objects(element) + + for obj in context.selected_objects: + obj.select_set(False) + + tool.Ifc.get_object(base_instance).select_set(True) + + old_to_new = DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(self, context) + for old, new in old_to_new.items(): + new_obj = tool.Ifc.get_object(new[0]) + new_base_matrix = Matrix.LocRotScale(*duplicate_matrix) + matrix_diff = Matrix.inverted(selected_matrix) @ new_obj.matrix_world + new_obj_matrix = new_base_matrix @ matrix_diff + new_obj.matrix_world = new_obj_matrix + + if element_aggregate and new[0].is_a("IfcElementAssembly"): + new_aggregate = ifcopenshell.util.element.get_aggregate(new[0]) + + if not new_aggregate: blenderbim.core.aggregate.assign_object( - tool.Ifc, - tool.Aggregate, - tool.Collector, - relating_obj=tool.Ifc.get_object(entity), - related_obj=tool.Ifc.get_object(new_part), - ) - duplicate_children(new_part) - - for part in parts: - pset = ifcopenshell.util.element.get_pset(part, "BBIM_Aggregate_Data") - if part.is_a("IfcElementAssembly"): - pass - - else: - if not pset: - pset = ifcopenshell.api.run( - "pset.add_pset", tool.Ifc.get(), product=part, name="BBIM_Aggregate_Data" - ) - else: - pset = ifcopenshell.util.element.get_pset(part, "BBIM_Aggregate_Data") - pset = tool.Ifc.get().by_id(pset["id"]) - - data_children = { - "children": [], - "instance_of": [], - } - data = [data_children] - - ifcopenshell.api.run( - "pset.edit_pset", - tool.Ifc.get(), - pset=pset, - properties={ - "Parent": instance_entity.GlobalId, - "Data": tool.Ifc.get().createIfcText(json.dumps(data)), - }, - ) - - part_obj = tool.Ifc.get_object(part) - new_part = duplicate_objects(part_obj) - blenderbim.core.aggregate.assign_object( - tool.Ifc, - tool.Aggregate, - tool.Collector, - relating_obj=tool.Ifc.get_object(entity), - related_obj=tool.Ifc.get_object(new_part), - ) - - def duplicate_objects(obj, is_root=False): - new_obj = obj.copy() - if obj.data: - new_obj.data = obj.data.copy() - 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) - - # This is needed to make sure the new object gets unlink from - # the old object assembly collection - new_obj.BIMObjectProperties.collection = None - - # Copy the actual class - new_entity = blenderbim.core.root.copy_class( - tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj - ) - - if new_entity: - tool.Model.handle_array_on_copied_element(new_entity) - - if not new_entity.is_a("IfcElementAssembly"): - blenderbim.core.aggregate.unassign_object( - tool.Ifc, - tool.Aggregate, - tool.Collector, - relating_obj=obj, - related_obj=tool.Ifc.get_object(new_entity), - ) - - old_to_new[tool.Ifc.get_entity(obj)] = [new_entity] - - return new_entity - - if len(context.selected_objects) != 1: - self.report({"INFO"}, "Only 1 object need to be selected.") - return {"FINISHED"} - - selected_root_obj = context.selected_objects[0] - selected_root_entity = tool.Ifc.get_entity(selected_root_obj) - - if selected_root_entity.is_a("IfcElementAssembly"): - pass - elif selected_root_entity.Decomposes: - if selected_root_entity.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"): - selected_root_entity = selected_root_entity.Decomposes[0].RelatingObject - selected_root_obj = tool.Ifc.get_object(selected_root_entity) - else: - self.report({"INFO"}, "Object is not part of a IfcElementAssembly.") - return {"FINISHED"} - - pset = ifcopenshell.util.element.get_pset(selected_root_entity, "BBIM_Aggregate_Data") - if not pset: - self.report({"INFO"}, "Object is not part of an assembly aggregate.") - return {"FINISHED"} - - pset_data = json.loads(pset["Data"])[0] - instance_of = pset_data["instance_of"][0] - original_root_entity = tool.Ifc.get().by_guid(instance_of) - if original_root_entity == selected_root_entity: - self.report({"INFO"}, "Cannot refresh original assembly. Select an assembly instance.") - return {"FINISHED"} - - parents = remove_objects(selected_root_entity) - - original_root_object = tool.Ifc.get_object(original_root_entity) - - selected_matrix = selected_root_obj.matrix_world - original_matrix = original_root_object.matrix_world - - for parent in parents: - duplicate_children(parent) - - # Remove connections with old objects - for new in old_to_new.values(): - for connection in new[0].ConnectedTo: - entity = connection.RelatedElement - if entity in old_to_new.keys(): - core.remove_connection(tool.Geometry, connection=connection) - for connection in new[0].ConnectedFrom: - entity = connection.RelatingElement - if entity in old_to_new.keys(): - core.remove_connection(tool.Geometry, connection=connection) - - old_objs = [] - for old, new in old_to_new.items(): - old_objs.append(tool.Ifc.get_object(old)) - - new_obj = tool.Ifc.get_object(new[0]) - - matrix_diff = Matrix.inverted(original_matrix) @ new_obj.matrix_world - new_matrix = selected_matrix @ matrix_diff - - new_obj.matrix_world = new_matrix - - relationships = tool.Root.get_decomposition_relationships(old_objs) - - tool.Root.recreate_decompositions(relationships, old_to_new) - + tool.Ifc, + tool.Aggregate, + tool.Collector, + relating_obj=tool.Ifc.get_object(element_aggregate), + related_obj=tool.Ifc.get_object(new[0]), + ) + blenderbim.bim.handler.refresh_ui_data() operator_time = time() - refresh_start_time diff --git a/src/blenderbim/blenderbim/tool/root.py b/src/blenderbim/blenderbim/tool/root.py index 7cd623330e..fe5623b043 100644 --- a/src/blenderbim/blenderbim/tool/root.py +++ b/src/blenderbim/blenderbim/tool/root.py @@ -89,6 +89,26 @@ class Root(blenderbim.core.tool.Root): relationships[element] = {"type": "fill", "element": building} return relationships + @classmethod + def get_connection_relationships(cls, objs): + relationships = {} + for obj in objs: + element = tool.Ifc.get_entity(obj) + if not element: + continue + if hasattr(element, "ConnectedTo") and element.ConnectedTo: + paths = [connection for connection in element.ConnectedTo if connection.is_a("IfcRelConnectsPathElements")] + for path in paths: + relationships[element] = {"type": "path", + "related_connection_type": path.RelatedConnectionType, + "related_element": path.RelatedElement, + "related_priorities": path.RelatedPriorities, + "relating_connection_type": path.RelatingConnectionType, + "relating_element": path.RelatingElement, + "relating_priorities": path.RelatingPriorities, + } + return relationships + @classmethod def get_element_representation(cls, element, context): if context.is_a("IfcGeometricRepresentationSubContext"): @@ -197,6 +217,60 @@ class Root(blenderbim.core.tool.Root): should_sync_changes_first=False, ) + @classmethod + def recreate_connections(cls, relationship, old_to_new): + for element, data in relationship.items(): + new_relating_element = old_to_new.get(data["relating_element"])[0] + new_related_element = old_to_new.get(data["related_element"])[0] + ifcopenshell.api.run( + "geometry.connect_path", + tool.Ifc.get(), + relating_element=new_relating_element, + related_element=new_related_element, + relating_connection=data["relating_connection_type"], + related_connection=data["related_connection_type"], + ) + + + @classmethod + def recreate_aggregate(cls, old_to_new): + for old, new in old_to_new.items(): + old_aggregate = ifcopenshell.util.element.get_aggregate(old) + if old_aggregate: + try: + new_aggregate = old_to_new[old_aggregate] + except: + blenderbim.core.aggregate.unassign_object( + tool.Ifc, + tool.Aggregate, + tool.Collector, + relating_obj=tool.Ifc.get_object(old_aggregate), + related_obj=tool.Ifc.get_object(new[0]), + ) + continue + + blenderbim.core.aggregate.assign_object( + tool.Ifc, + tool.Aggregate, + tool.Collector, + relating_obj=tool.Ifc.get_object(new_aggregate[0]), + related_obj=tool.Ifc.get_object(new[0]), + ) + + # Make sure that the array children also get reassigned to the correct aggregate + pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Array") + if pset: + array_children = tool.Blender.Modifier.Array.get_all_children_objects(new[0]) + for obj in array_children: + blenderbim.core.aggregate.assign_object( + tool.Ifc, + tool.Aggregate, + tool.Collector, + relating_obj=tool.Ifc.get_object(new_aggregate[0]), + related_obj=tool.Ifc.get_object(tool.Ifc.get_entity(obj)), + ) + + @classmethod def run_geometry_add_representation( cls, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None diff --git a/src/blenderbim/test/bim/feature/geometry.feature b/src/blenderbim/test/bim/feature/geometry.feature index 206769fc64..163f0e5c7d 100644 --- a/src/blenderbim/test/bim/feature/geometry.feature +++ b/src/blenderbim/test/bim/feature/geometry.feature @@ -418,6 +418,83 @@ Scenario: Override duplicate move - copying a profiled extrusion Then the object "IfcWall/Cube.001" exists Then the object "IfcWall/Cube.001" has a "SweptSolid" representation of "Model/Body/MODEL_VIEW" +Scenario: Override duplicate move - copying an aggregate + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" + And the object "IfcWall/Cube" is selected + When I press "bim.add_aggregate" + Then the object "IfcWall/Cube" is in the collection "IfcElementAssembly/Assembly" + And the object "IfcElementAssembly/Assembly" is in the collection "IfcElementAssembly/Assembly" + And the collection "IfcElementAssembly/Assembly" is in the collection "IfcBuildingStorey/My Storey" + When the object "IfcWall/Cube" is selected + And additionally the object "IfcElementAssembly/Assembly" is selected + When I duplicate the selected objects + Then the object "IfcWall/Cube.001" exists + And the object "IfcWall/Cube.001" is in the collection "IfcElementAssembly/Assembly.001" + And the object "IfcElementAssembly/Assembly.001" exists + And the object "IfcElementAssembly/Assembly.001" is in the collection "IfcElementAssembly/Assembly.001" + And the collection "IfcElementAssembly/Assembly.001" is in the collection "IfcBuildingStorey/My Storey" + +Scenario: Override duplicate move - copying objects with connection + Given an empty IFC project + And I load the demo construction library + And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" + And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" + And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" + When I press "bim.add_constr_type_instance" + Then the object "IfcWall/Wall" is an "IfcWall" + And the object "IfcWall/Wall" dimensions are "1,0.1,3" + And the object "IfcWall/Wall" bottom left corner is at "0,0,0" + When I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType" + And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR150'][0].id()" + And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" + When I press "bim.add_constr_type_instance" + Then the object "IfcSlab/Slab" is an "IfcSlab" + When the object "IfcSlab/Slab" is selected + And the object "IfcSlab/Slab" is moved to "0,0,4" + When I deselect all objects + And the object "IfcWall/Wall" is selected + And additionally the object "IfcSlab/Slab" is selected + When I press "bim.hotkey(hotkey='S_E')" + Then the object "IfcWall/Wall" dimensions are "1,0.1,4" + When I duplicate the selected objects + Then the object "IfcWall/Wall.001" exists + And the variable "wall_name" is "[o.name for o in bpy.context.selected_objects if o.name == 'IfcWall/Wall.001'][0]" + Then the object "IfcSlab/Slab.001" exists + And the variable "slab_name" is "[o.name for o in bpy.context.selected_objects if o.name == 'IfcSlab/Slab.001'][0]" + Then the object "{wall_name}" has a connection with "{slab_name}" + +Scenario: Override duplicate move - copying walls with mitre joint + Given an empty IFC project + And I load the demo construction library + And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" + And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" + And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" + And I press "bim.hotkey(hotkey='S_A')" + And the cursor is at "0.5,0,0" + And I press "bim.hotkey(hotkey='S_A')" + And the object "IfcWall/Wall" is selected + And additionally the object "IfcWall/Wall.001" is selected + When I press "bim.hotkey(hotkey='S_Y')" + Then the object "IfcWall/Wall.001" dimensions are "0.5,0.1,3" + And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0" + And the object "IfcWall/Wall" dimensions are "1.1,0.1,3" + And the object "IfcWall/Wall" bottom left corner is at "0.5,0.1,0" + And the object "IfcWall/Wall" top right corner is at "0.6,-1,3" + When I deselect all objects + And the object "IfcWall/Wall" is selected + And additionally the object "IfcWall/Wall.001" is selected + When I duplicate the selected objects + Then the object "IfcWall/Wall.002" exists + And the variable "wall_name1" is "[o.name for o in bpy.context.selected_objects if o.name == 'IfcWall/Wall.002'][0]" + Then the object "IfcWall/Wall.003" exists + And the variable "wall_name2" is "[o.name for o in bpy.context.selected_objects if o.name == 'IfcWall/Wall.003'][0]" + Then the object "{wall_name1}" has a connection with "{wall_name2}" + Scenario: Override duplicate move linked - without active IFC data Given an empty Blender session And I add a cube diff --git a/src/blenderbim/test/bim/test_feature.py b/src/blenderbim/test/bim/test_feature.py index b906c9bc68..77dbd74347 100644 --- a/src/blenderbim/test/bim/test_feature.py +++ b/src/blenderbim/test/bim/test_feature.py @@ -874,3 +874,28 @@ def run_pdb(): import pdb pdb.set_trace() + +@then(parsers.parse('the object "{obj_name1}" has a connection with "{obj_name2}"')) +def the_obj1_has_a_connection_with_obj2(obj_name1, obj_name2): + element1 = replace_variables(obj_name1) + element1 = tool.Ifc.get_entity(the_object_name_exists(element1)) + element2 = replace_variables(obj_name2) + element2 = tool.Ifc.get_entity(the_object_name_exists(element2)) + connections = [] + if hasattr(element1, "ConnectedTo") and element1.ConnectedTo: + connections = [connection for connection in element1.ConnectedTo] + elif hasattr(element1, "ConnectedFrom") and element1.ConnectedFrom: + connections = [connection for connection in element1.ConnectedFrom] + else: + assert False, f'Object "{obj_name1}" has no connections' + + + relationships = {} + for conn in connections: + relationships[conn.RelatedElement] = conn.RelatingElement + + for key, value in relationships.items(): + print(key, value) + # assert False, f"1-{key} and {element1.id()} and {key.id()}" + assert (key.id() == element1.id() and value.id() == element2.id()) or (key.id() == element2.id() and value.id() == element1.id()), f"The object {obj_name1} is connected to {obj_name2}" +