From 17d6b8af61c8dd7cd82f6e72b2fb3831d851a33d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Mon, 21 Jul 2025 10:57:19 -0300 Subject: [PATCH] Add Name to Linked Aggregates and keep product assignment after refreshing. See #6831. Warning: this is a breaking change. Linked Aggregate names are numbered to keep them consistent when reopening the file instead of relying on Blender auto numbering objects with the same name. However, for annotation purposes, the numbering is irrelevant, and the user might want to use just the aggregate name. So it was added a "Name" to the "BBIM_Linked_Aggregate" Pset that can be used in these cases. When refreshing the linked aggregates, their names will be updated to reflect the "Name" value and the product assignment will be preserved to keep annotations in sync. In order to adapt existing files to this new Pset structure see this gist: https://gist.github.com/brunoperdigao/37a5b838935b15a91aa70b2bc4de325f --- .../bonsai/bim/module/geometry/operator.py | 141 ++++++++++++------ 1 file changed, 97 insertions(+), 44 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 933a885456..7fa76ea0ff 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -1192,16 +1192,20 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator): obj = tool.Ifc.get_object(part) obj.select_set(True) - def add_linked_aggregate_pset(part, index): - pset = ifcopenshell.util.element.get_pset(part, self.pset_name) + def add_linked_aggregate_pset(element, index): + pset = ifcopenshell.util.element.get_pset(element, self.pset_name) + if index == 0: + properties = {"Index": index, "Name": element.Name, "Aggregate_Index": 0} + else: + properties = {"Index": index} if not pset: - pset = ifcopenshell.api.pset.add_pset(ifc_file, product=part, name=self.pset_name) + pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name=self.pset_name) ifcopenshell.api.pset.edit_pset( ifc_file, pset=pset, - properties={"Index": index}, + properties=properties, ) index += 1 @@ -1233,18 +1237,9 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator): if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name ) - number = len(group_elements) - 1 - number = f"{number:02d}" + pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Linked_Aggregate") new_obj = tool.Ifc.get_object(new[0]) - pattern1 = r"_\d" - if re.findall(pattern1, new_obj.name): - split_name = new_obj.name.split("_") - new_obj.name = split_name[0] + "_" + number - continue - pattern2 = r"\.\d{3}" - if re.findall(pattern2, new_obj.name): - split_name = new_obj.name.split(".") - new_obj.name = split_name[0] + "_" + number + new_obj.name = pset["Name"] + "_" + str(pset["Aggregate_Index"]) def get_max_index(parts): psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts] @@ -1257,16 +1252,6 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator): def copy_linked_aggregate_data(old_to_new): for old, new in old_to_new.items(): - pset = ifcopenshell.util.element.get_pset(old, "BBIM_Linked_Aggregate") - if pset: - new_pset = ifcopenshell.api.pset.add_pset(ifc_file, product=new[0], name=self.pset_name) - - ifcopenshell.api.pset.edit_pset( - ifc_file, - pset=new_pset, - properties={"Index": pset["Index"]}, - ) - if new[0].is_a("IfcElementAssembly"): linked_aggregate_group = [ r.RelatingGroup @@ -1276,6 +1261,28 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator): ] ifcopenshell.api.group.assign_group(ifc_file, group=linked_aggregate_group[0], products=new) + group_elements: list[ifcopenshell.entity_instance] = next( + r.RelatedObjects + for r in getattr(new[0], "HasAssignments", []) or [] + if r.is_a("IfcRelAssignsToGroup") + if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name + ) + + pset = ifcopenshell.util.element.get_pset(old, "BBIM_Linked_Aggregate") + if pset: + new_pset = ifcopenshell.api.pset.add_pset(ifc_file, product=new[0], name=self.pset_name) + if pset["Index"] == 0: + properties = {"Index": pset["Index"], "Name": pset["Name"], "Aggregate_Index": len(group_elements) - 1} + else: + properties = {"Index": pset["Index"]} + + ifcopenshell.api.pset.edit_pset( + ifc_file, + pset=new_pset, + properties=properties, + ) + + def get_location_from_3d_cursor(old_to_new, aggregate): base_obj = tool.Ifc.get_object(aggregate) base_obj_location = base_obj.location.copy() @@ -1357,7 +1364,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator): self.pset_name = "BBIM_Linked_Aggregate" refresh_start_time = time() old_to_new = {} - original_names: dict[int, dict[int, str]] = {} + original_data: dict[int, dict[int, str]] = {} def delete_objects(element: ifcopenshell.entity_instance) -> None: """Remove IfcElementAssembly and it's parts.""" @@ -1372,25 +1379,51 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator): tool.Geometry.delete_ifc_object(tool.Ifc.get_object(element)) - def get_original_names(element: ifcopenshell.entity_instance) -> dict[int, dict[int, str]]: + def get_assignments(element: ifcopenshell.entity_instance) -> list: + annotations = [] + inverse = list(tool.Ifc.get().get_inverse(element)) + assignments = [a for a in inverse if a.is_a("IfcRelAssignsToProduct")] + if not assignments: + pass + else: + for assignment in assignments: + annotations = assignment.RelatedObjects + return annotations + + def assign_to_annotations(obj, assignments): + for assignment in assignments: + product = tool.Ifc.get_entity(obj) + annotation = tool.Ifc.get_object(assignment) + if annotation: + bonsai.core.drawing.edit_assigned_product(tool.Ifc, tool.Drawing, obj=annotation, product=product) + else: + existing_product = tool.Drawing.get_assigned_product(assignment) + if existing_product != product: + if existing_product: + tool.Ifc.run("drawing.unassign_product", relating_product=existing_product, related_object=assignment) + if product: + tool.Ifc.run("drawing.assign_product", relating_product=product, related_object=assignment) + tool.Blender.update_viewport() + + def get_original_data(element: ifcopenshell.entity_instance) -> dict[int, dict[int, str]]: group = next( r.RelatingGroup for r in getattr(element, "HasAssignments", []) or [] if r.is_a("IfcRelAssignsToGroup") if self.group_name in r.RelatingGroup.Name ).id() - original_names[group] = {} + original_data[group] = {} pset = ifcopenshell.util.element.get_pset(element, self.pset_name) index = pset["Index"] - original_names[group][index] = tool.Ifc.get_object(element).name + annotations = get_assignments(element) + original_data[group][index] = {"Name": str(pset["Aggregate_Index"]), "Assignment": annotations} # Get the link index number. The name will come from annotation tag parts = ifcopenshell.util.element.get_parts(element) if parts: for part in parts: if part.is_a("IfcElementAssembly"): - # TODO: missing assignment. - original_names | get_original_names(part) + original_data | get_original_data(part) else: try: pset = ifcopenshell.util.element.get_pset(part, self.pset_name) @@ -1398,11 +1431,12 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator): continue if pset: index = pset["Index"] - original_names[group][index] = tool.Ifc.get_object(part).name + annotations = get_assignments(part) + original_data[group][index] = {"Name": tool.Ifc.get_object(part).name, "Assignment": annotations} - return original_names + return original_data - def set_original_name(obj: bpy.types.Object, original_names: dict[int, dict[int, str]]) -> None: + def set_original_data(obj: bpy.types.Object, original_data: dict[int, dict[int, str]]) -> None: element = tool.Ifc.get_entity(obj) aggregate = ifcopenshell.util.element.get_aggregate(element) if ifcopenshell.util.element.get_parts( @@ -1410,24 +1444,40 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator): ): # if element has parts it means it is the base of and aggregate or sub-aggregate aggregate = element - group = [ + group = next( r.RelatingGroup for r in getattr(aggregate, "HasAssignments", []) or [] if r.is_a("IfcRelAssignsToGroup") if self.group_name in r.RelatingGroup.Name - ] + ).id() if not group: return - group = group[0].id() - pset = ifcopenshell.util.element.get_pset(element, self.pset_name) index = pset["Index"] - try: - obj.name = original_names[group][index] - except: - return + if index == 0: + obj.name = pset["Name"] + ifc_file = tool.Ifc.get() + ifcopenshell.api.pset.edit_pset( + ifc_file, + ifc_file.by_id(pset["id"]), + properties={"Aggregate_Index": int(original_data[group][index]["Name"])}, + ) + assignments = original_data[group][index]["Assignment"] + if assignments: + assign_to_annotations(obj, assignments) + else: + try: + obj.name = original_data[group][index]["Name"] + except: + pass + try: + assignments = original_data[group][index]["Assignment"] + except: + assignments = [] + if assignments: + assign_to_annotations(obj, assignments) def get_element_assembly(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: if element.is_a("IfcElementAssembly"): @@ -1525,6 +1575,9 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator): base_instance = active_element instances_to_refresh = elements + base_pset = ifcopenshell.util.element.get_pset(base_instance, self.pset_name) + base_obj = tool.Ifc.get_object(base_instance) + base_obj.name = base_pset["Name"] + "_" + str(base_pset["Aggregate_Index"]) for element in instances_to_refresh: if element.GlobalId == base_instance.GlobalId: continue @@ -1533,7 +1586,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator): selected_matrix, duplicate_matrix = get_original_matrix(element, base_instance) - original_names = get_original_names(element) + original_data = get_original_data(element) delete_objects(element) @@ -1561,7 +1614,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator): for old, new in old_to_new.items(): new_obj = tool.Ifc.get_object(new[0]) - set_original_name(new_obj, original_names) + set_original_data(new_obj, original_data) bonsai.bim.handler.refresh_ui_data()