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
This commit is contained in:
Bruno Perdigão
2025-07-21 10:57:19 -03:00
parent 4a2411c05d
commit 17d6b8af61
@@ -1192,16 +1192,20 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
obj = tool.Ifc.get_object(part) obj = tool.Ifc.get_object(part)
obj.select_set(True) obj.select_set(True)
def add_linked_aggregate_pset(part, index): def add_linked_aggregate_pset(element, index):
pset = ifcopenshell.util.element.get_pset(part, self.pset_name) 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: 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( ifcopenshell.api.pset.edit_pset(
ifc_file, ifc_file,
pset=pset, pset=pset,
properties={"Index": index}, properties=properties,
) )
index += 1 index += 1
@@ -1233,18 +1237,9 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
) )
number = len(group_elements) - 1 pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Linked_Aggregate")
number = f"{number:02d}"
new_obj = tool.Ifc.get_object(new[0]) new_obj = tool.Ifc.get_object(new[0])
pattern1 = r"_\d" new_obj.name = pset["Name"] + "_" + str(pset["Aggregate_Index"])
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
def get_max_index(parts): def get_max_index(parts):
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in 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): def copy_linked_aggregate_data(old_to_new):
for old, new in old_to_new.items(): 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"): if new[0].is_a("IfcElementAssembly"):
linked_aggregate_group = [ linked_aggregate_group = [
r.RelatingGroup 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) 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): def get_location_from_3d_cursor(old_to_new, aggregate):
base_obj = tool.Ifc.get_object(aggregate) base_obj = tool.Ifc.get_object(aggregate)
base_obj_location = base_obj.location.copy() 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" self.pset_name = "BBIM_Linked_Aggregate"
refresh_start_time = time() refresh_start_time = time()
old_to_new = {} 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: def delete_objects(element: ifcopenshell.entity_instance) -> None:
"""Remove IfcElementAssembly and it's parts.""" """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)) 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( group = next(
r.RelatingGroup r.RelatingGroup
for r in getattr(element, "HasAssignments", []) or [] for r in getattr(element, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup") if r.is_a("IfcRelAssignsToGroup")
if self.group_name in r.RelatingGroup.Name if self.group_name in r.RelatingGroup.Name
).id() ).id()
original_names[group] = {} original_data[group] = {}
pset = ifcopenshell.util.element.get_pset(element, self.pset_name) pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
index = pset["Index"] 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) parts = ifcopenshell.util.element.get_parts(element)
if parts: if parts:
for part in parts: for part in parts:
if part.is_a("IfcElementAssembly"): if part.is_a("IfcElementAssembly"):
# TODO: missing assignment. original_data | get_original_data(part)
original_names | get_original_names(part)
else: else:
try: try:
pset = ifcopenshell.util.element.get_pset(part, self.pset_name) pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
@@ -1398,11 +1431,12 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
continue continue
if pset: if pset:
index = pset["Index"] 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) element = tool.Ifc.get_entity(obj)
aggregate = ifcopenshell.util.element.get_aggregate(element) aggregate = ifcopenshell.util.element.get_aggregate(element)
if ifcopenshell.util.element.get_parts( 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 ): # if element has parts it means it is the base of and aggregate or sub-aggregate
aggregate = element aggregate = element
group = [ group = next(
r.RelatingGroup r.RelatingGroup
for r in getattr(aggregate, "HasAssignments", []) or [] for r in getattr(aggregate, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup") if r.is_a("IfcRelAssignsToGroup")
if self.group_name in r.RelatingGroup.Name if self.group_name in r.RelatingGroup.Name
] ).id()
if not group: if not group:
return return
group = group[0].id()
pset = ifcopenshell.util.element.get_pset(element, self.pset_name) pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
index = pset["Index"] index = pset["Index"]
try: if index == 0:
obj.name = original_names[group][index] obj.name = pset["Name"]
except: ifc_file = tool.Ifc.get()
return 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]: def get_element_assembly(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
if element.is_a("IfcElementAssembly"): if element.is_a("IfcElementAssembly"):
@@ -1525,6 +1575,9 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
base_instance = active_element base_instance = active_element
instances_to_refresh = elements 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: for element in instances_to_refresh:
if element.GlobalId == base_instance.GlobalId: if element.GlobalId == base_instance.GlobalId:
continue continue
@@ -1533,7 +1586,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
selected_matrix, duplicate_matrix = get_original_matrix(element, base_instance) 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) delete_objects(element)
@@ -1561,7 +1614,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
for old, new in old_to_new.items(): for old, new in old_to_new.items():
new_obj = tool.Ifc.get_object(new[0]) 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() bonsai.bim.handler.refresh_ui_data()