Linked aggregate preserve names (#4135)

* added simples linked aggregate test

* small fix in remove_old_connections function

* added test for refresh linked aggregate

* refresh linked aggregates now preserves the objects original name

* small fix in recreate_connections function

* tests for Refresh Linked Aggregate

* test for Refresh Linked Aggregate after copying an object

* small deletion

* added custom incremental naming

* small fix in tests

* small fix on the number for renaming
This commit is contained in:
Bruno Perdigão
2024-02-23 17:55:18 -03:00
committed by GitHub
parent d83924b73e
commit 1c1035e251
5 changed files with 1518 additions and 55 deletions
@@ -21,6 +21,7 @@ import bmesh
import logging import logging
import numpy as np import numpy as np
import json import json
import re
import ifcopenshell import ifcopenshell
import ifcopenshell.util.unit import ifcopenshell.util.unit
import ifcopenshell.util.element import ifcopenshell.util.element
@@ -840,6 +841,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
# Recreate decompositions # Recreate decompositions
tool.Root.recreate_decompositions(decomposition_relationships, old_to_new) tool.Root.recreate_decompositions(decomposition_relationships, old_to_new)
OverrideDuplicateMove.handle_linked_aggregates(old_to_new)
blenderbim.bim.handler.refresh_ui_data() blenderbim.bim.handler.refresh_ui_data()
return old_to_new return old_to_new
@@ -892,6 +894,26 @@ class OverrideDuplicateMove(bpy.types.Operator):
if entity in old_to_new.keys(): if entity in old_to_new.keys():
core.remove_connection(tool.Geometry, connection=connection) core.remove_connection(tool.Geometry, connection=connection)
@staticmethod
def handle_linked_aggregates(old_to_new):
for old, new in old_to_new.items():
pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Linked_Aggregate")
if pset:
old_aggregate = ifcopenshell.util.element.get_aggregate(old)
new_aggregate = ifcopenshell.util.element.get_aggregate(new[0])
if old_aggregate == new_aggregate:
parts = ifcopenshell.util.element.get_parts(new_aggregate)
if parts:
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
index += 1
pset = tool.Ifc.get().by_id(pset['id'])
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Index": index},
)
class OverrideDuplicateMoveLinkedMacro(bpy.types.Macro): class OverrideDuplicateMoveLinkedMacro(bpy.types.Macro):
bl_idname = "bim.override_object_duplicate_move_linked_macro" bl_idname = "bim.override_object_duplicate_move_linked_macro"
@@ -950,16 +972,38 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
obj.select_set(True) obj.select_set(True)
parts = ifcopenshell.util.element.get_parts(element) parts = ifcopenshell.util.element.get_parts(element)
if parts: if parts:
index = 0 index = DuplicateMoveLinkedAggregate.get_max_index(parts)
add_linked_aggregate_pset(element, index)
index +=1
for part in parts: for part in parts:
if part.is_a("IfcElementAssembly"): if part.is_a("IfcElementAssembly"):
select_objects_and_add_data(part) select_objects_and_add_data(part)
else:
add_linked_aggregate_pset(part, index) add_linked_aggregate_pset(part, index)
index += 1 index += 1
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):
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
if not pset:
pset = ifcopenshell.api.run(
"pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name
)
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Index": index},
)
else:
pass
return index
def add_linked_aggregate_group(element): def add_linked_aggregate_group(element):
linked_aggregate_group = None linked_aggregate_group = None
@@ -974,23 +1018,30 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
linked_aggregate_group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.group_name) 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) ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), products=[element], group=linked_aggregate_group)
def add_linked_aggregate_pset(part, index): def custom_incremental_naming_for_element_assembly(old_to_new):
pset = ifcopenshell.util.element.get_pset(part, self.pset_name) for new in old_to_new.values():
if new[0].is_a("IfcElementAssembly"):
if not pset: group_elements = [
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name) r.RelatedObjects
else: for r in getattr(new[0], "HasAssignments", []) or []
pset = tool.Ifc.get().by_id(pset["id"]) if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
ifcopenshell.api.run( ][0]
"pset.edit_pset",
tool.Ifc.get(), number = len(group_elements) - 1
pset=pset, number = f"{number:02d}"
properties={"Index": index}, new_obj = tool.Ifc.get_object(new[0])
) pattern1 = r'_\d'
if re.findall(pattern1, new_obj.name):
return index 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
if len(context.selected_objects) != 1: if len(context.selected_objects) != 1:
return {"FINISHED"} return {"FINISHED"}
@@ -1011,6 +1062,8 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True) old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
custom_incremental_naming_for_element_assembly(old_to_new)
# Recreate aggregate relationship # Recreate aggregate relationship
for old in old_to_new.keys(): for old in old_to_new.keys():
if old.is_a("IfcElementAssembly"): if old.is_a("IfcElementAssembly"):
@@ -1020,6 +1073,18 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
return old_to_new return old_to_new
@staticmethod
def get_max_index(parts):
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
index = [i['Index'] for i in psets if i]
if len(index) > 0:
index = max(index)
return index
else:
return 0
class RefreshLinkedAggregate(bpy.types.Operator): class RefreshLinkedAggregate(bpy.types.Operator):
bl_idname = "bim.refresh_linked_aggregate" bl_idname = "bim.refresh_linked_aggregate"
@@ -1044,6 +1109,7 @@ class RefreshLinkedAggregate(bpy.types.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 = {}
def delete_objects(element): def delete_objects(element):
parts = ifcopenshell.util.element.get_parts(element) parts = ifcopenshell.util.element.get_parts(element)
@@ -1056,6 +1122,61 @@ class RefreshLinkedAggregate(bpy.types.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):
group = [
r.RelatingGroup
for r in getattr(element, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if self.group_name in r.RelatingGroup.Name
][0].id()
original_names[group] = {}
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
index = pset['Index']
original_names[group][index] = tool.Ifc.get_object(element).name
parts = ifcopenshell.util.element.get_parts(element)
if parts:
for part in parts:
if part.is_a("IfcElementAssembly"):
original_names | get_original_names(part)
else:
try:
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
except:
continue
index = pset['Index']
original_names[group][index] = tool.Ifc.get_object(part).name
return original_names
def set_original_name(obj, original_names):
element = tool.Ifc.get_entity(obj)
aggregate = ifcopenshell.util.element.get_aggregate(element)
if ifcopenshell.util.element.get_parts(element): # if element has parts it means it is the base of and aggregate or sub-aggregate
aggregate = element
group = [
r.RelatingGroup
for r in getattr(aggregate, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if self.group_name in r.RelatingGroup.Name
]
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
def get_element_assembly(element): def get_element_assembly(element):
if element.is_a("IfcElementAssembly"): if element.is_a("IfcElementAssembly"):
return element return element
@@ -1140,7 +1261,9 @@ class RefreshLinkedAggregate(bpy.types.Operator):
selected_matrix = selected_obj.matrix_world selected_matrix = selected_obj.matrix_world
object_duplicate = tool.Ifc.get_object(element) object_duplicate = tool.Ifc.get_object(element)
duplicate_matrix = object_duplicate.matrix_world.decompose() duplicate_matrix = object_duplicate.matrix_world.decompose()
original_names = get_original_names(element)
delete_objects(element) delete_objects(element)
for obj in context.selected_objects: for obj in context.selected_objects:
@@ -1155,19 +1278,24 @@ class RefreshLinkedAggregate(bpy.types.Operator):
matrix_diff = Matrix.inverted(selected_matrix) @ new_obj.matrix_world matrix_diff = Matrix.inverted(selected_matrix) @ new_obj.matrix_world
new_obj_matrix = new_base_matrix @ matrix_diff new_obj_matrix = new_base_matrix @ matrix_diff
new_obj.matrix_world = new_obj_matrix new_obj.matrix_world = new_obj_matrix
for old, new in old_to_new.items():
if element_aggregate and new[0].is_a("IfcElementAssembly"): if element_aggregate and new[0].is_a("IfcElementAssembly"):
new_aggregate = ifcopenshell.util.element.get_aggregate(new[0]) new_aggregate = ifcopenshell.util.element.get_aggregate(new[0])
if not new_aggregate: if not new_aggregate:
blenderbim.core.aggregate.assign_object( blenderbim.core.aggregate.assign_object(
tool.Ifc, tool.Ifc,
tool.Aggregate, tool.Aggregate,
tool.Collector, tool.Collector,
relating_obj=tool.Ifc.get_object(element_aggregate), relating_obj=tool.Ifc.get_object(element_aggregate),
related_obj=tool.Ifc.get_object(new[0]), related_obj=tool.Ifc.get_object(new[0]),
) )
for old, new in old_to_new.items():
new_obj = tool.Ifc.get_object(new[0])
set_original_name(new_obj, original_names)
blenderbim.bim.handler.refresh_ui_data() blenderbim.bim.handler.refresh_ui_data()
operator_time = time() - refresh_start_time operator_time = time() - refresh_start_time
+5 -2
View File
@@ -220,8 +220,11 @@ class Root(blenderbim.core.tool.Root):
@classmethod @classmethod
def recreate_connections(cls, relationship, old_to_new): def recreate_connections(cls, relationship, old_to_new):
for element, data in relationship.items(): for element, data in relationship.items():
new_relating_element = old_to_new.get(data["relating_element"])[0] try:
new_related_element = old_to_new.get(data["related_element"])[0] new_relating_element = old_to_new.get(data["relating_element"])[0]
new_related_element = old_to_new.get(data["related_element"])[0]
except:
continue
ifcopenshell.api.run( ifcopenshell.api.run(
"geometry.connect_path", "geometry.connect_path",
tool.Ifc.get(), tool.Ifc.get(),
@@ -552,3 +552,58 @@ Scenario: Override paste buffer - with active IFC data
And the object "IfcWall/Cube.001" has a "Tessellation" representation of "Model/Body/MODEL_VIEW" And the object "IfcWall/Cube.001" has a "Tessellation" representation of "Model/Body/MODEL_VIEW"
And the object "IfcBuildingStorey/My Storey.001" exists And the object "IfcBuildingStorey/My Storey.001" exists
And the object "IfcBuildingStorey/My Storey.001" is an "IfcBuildingStorey" And the object "IfcBuildingStorey/My Storey.001" is an "IfcBuildingStorey"
Scenario: Duplicate linked aggregate
Given I load the IFC test file "/test/files/linked-aggregates.ifc"
And the object "IfcWall/Wall_01" is selected
When I duplicate linked aggregate the selected objects
Then the object "IfcWall/Wall_01.001" exists
And the object "IfcWall/Wall_02.001" exists
And the object "IfcElementAssembly/Assembly_02" exists
Then the object "IfcElementAssembly/Assembly" and "IfcElementAssembly/Assembly_02" belong to the same Linked Aggregate Group
Scenario: Refresh linked aggregate
Given I load the IFC test file "/test/files/linked-aggregates.ifc"
And the object "IfcWall/Wall_01" is selected
When I duplicate linked aggregate the selected objects
Then the object "IfcWall/Wall_01.001" exists
When I deselect all objects
And the object "IfcWall/Wall_01.001" is selected
When the object layer length is set to "3"
Then the object "IfcWall/Wall_01.001" dimensions are "3,0.1,3"
When I refresh linked aggregate the selected object
Then the object "IfcWall/Wall_01" exists
And the object "IfcWall/Wall_01" dimensions are "3,0.1,3"
Scenario: Refresh linked aggregate - after deleting an object
Given I load the IFC test file "/test/files/linked-aggregates.ifc"
And the object "IfcWall/Wall_01" is selected
When I duplicate linked aggregate the selected objects
Then the object "IfcWall/Wall_01.001" exists
When I deselect all objects
And the object "IfcWall/Wall_01.001" is selected
And I delete the selected objects
When I deselect all objects
And the object "IfcWall/Wall_02.001" is selected
When I refresh linked aggregate the selected object
Then the object "IfcWall/Wall_01" does not exist
And the object "IfcWall/Wall_02" exists
Scenario: Refresh linked aggregate - after duplicating an object
Given I load the IFC test file "/test/files/linked-aggregates.ifc"
And the object "IfcWall/Wall_01" is selected
When I duplicate linked aggregate the selected objects
Then the object "IfcWall/Wall_01.001" exists
And the object "IfcWall/Wall_02.001" exists
When I deselect all objects
And the object "IfcWall/Wall_01" is selected
When I duplicate the selected objects
Then the object "IfcWall/Wall_01.002" exists
When I rename the object "IfcWall/Wall_01.002" to "IfcWall/Wall_03"
And I deselect all objects
And the object "IfcWall/Wall_03" is selected
When I refresh linked aggregate the selected object
Then the object "IfcWall/Wall_01.001" exists
And the object "IfcWall/Wall_02.001" exists
And the object "IfcWall/Wall_03.001" exists
+79 -23
View File
@@ -233,6 +233,16 @@ def i_evaluate_expression(expression):
def i_duplicate_the_selected_objects(): def i_duplicate_the_selected_objects():
bpy.ops.bim.override_object_duplicate_move() bpy.ops.bim.override_object_duplicate_move()
blenderbim.bim.handler.active_object_callback() blenderbim.bim.handler.active_object_callback()
@when("I duplicate linked aggregate the selected objects")
def i_duplicate_the_selected_objects():
bpy.ops.bim.object_duplicate_move_linked_aggregate()
blenderbim.bim.handler.active_object_callback()
@when("I refresh linked aggregate the selected object")
def i_refresh_the_selected_objects():
bpy.ops.bim.refresh_linked_aggregate()
blenderbim.bim.handler.active_object_callback()
@when("I deselect all objects") @when("I deselect all objects")
@@ -360,6 +370,13 @@ def the_object_name_exists(name) -> bpy.types.Object:
return obj return obj
@then(parsers.parse('the object "{name}" does not exist'))
def the_object_name_does_not_exist(name) -> bpy.types.Object:
obj = bpy.data.objects.get(name)
if not obj:
assert True, f'The object "{name}" exists'
return obj
@given(parsers.parse('the collection "{name}" exists')) @given(parsers.parse('the collection "{name}" exists'))
@when(parsers.parse('the collection "{name}" exists')) @when(parsers.parse('the collection "{name}" exists'))
@then(parsers.parse('the collection "{name}" exists')) @then(parsers.parse('the collection "{name}" exists'))
@@ -774,6 +791,10 @@ def the_file_name_should_contain_value(name, value):
def the_object_name_has_no_modifiers(name): def the_object_name_has_no_modifiers(name):
assert len(the_object_name_exists(name).modifiers) == 0 assert len(the_object_name_exists(name).modifiers) == 0
@given(parsers.parse('I load the IFC test file "{filepath}"'))
def i_load_the_ifc_test_file(filepath):
filepath = f"{variables['cwd']}{filepath}"
bpy.ops.bim.load_project(filepath=filepath, use_relative_path=True)
@given("I load the demo construction library") @given("I load the demo construction library")
@when("I load the demo construction library") @when("I load the demo construction library")
@@ -826,6 +847,64 @@ def hit_undo():
bpy.ops.ed.undo_push(message="UNDO STEP") bpy.ops.ed.undo_push(message="UNDO STEP")
bpy.ops.ed.undo() bpy.ops.ed.undo()
@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():
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}"
@then(parsers.parse('the object "{obj_name1}" and "{obj_name2}" belong to the same Linked Aggregate Group'))
def the_obj1_and_obj2_belong_the_same_linked_aggregate_group(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))
elements = [element1, element2]
groups = []
for element in elements:
product_linked_agg_group = [
r.RelatingGroup
for r in getattr(element, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
try:
groups.append(product_linked_agg_group[0].id())
except:
assert False, "Object is not part of a Linked Aggregate."
assert groups[0] == groups[1], "Objects do not belong to the same Linked Aggregate group"
@when(parsers.parse('the object layer length is set to "{value}"'))
def the_obj_layer_lenght_is_set_to(value):
value = float(value)
try:
eval(f"bpy.context.scene.BIMModelProperties.length")
except:
assert False, f"Property BIMModelProperties.length does not exist when trying to set to value {value}"
print(50*"@", bpy.context.selected_objects)
bpy.context.scene.BIMModelProperties.length = value
bpy.ops.bim.change_layer_length(length=value)
# These definitions are not to be used in tests but simply in debugging failing tests # These definitions are not to be used in tests but simply in debugging failing tests
@@ -875,27 +954,4 @@ def run_pdb():
pdb.set_trace() 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}"
File diff suppressed because one or more lines are too long