From 30c850e37da59c65171f9d57b06a8bda34c5d53d Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 12 Mar 2023 12:52:18 +1100 Subject: [PATCH] Fix #2140. Voids and fills can now be added, removed, and edited for non-geometric aggregates. --- .../blenderbim/bim/module/model/opening.py | 128 +++++++++++------- .../blenderbim/bim/module/void/operator.py | 58 +++++--- .../test/bim/feature/aggregate.feature | 37 +++++ 3 files changed, 151 insertions(+), 72 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/opening.py b/src/blenderbim/blenderbim/bim/module/model/opening.py index 134dc75720..a2c42b2da8 100644 --- a/src/blenderbim/blenderbim/bim/module/model/opening.py +++ b/src/blenderbim/blenderbim/bim/module/model/opening.py @@ -58,6 +58,7 @@ class FilledOpeningGenerator: unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) filling = tool.Ifc.get_entity(filling_obj) + element = tool.Ifc.get_entity(voided_obj) if not voided_obj or not filling_obj: return @@ -69,28 +70,30 @@ class FilledOpeningGenerator: if target is None: target = bpy.context.scene.cursor.location - element = tool.Ifc.get_entity(voided_obj) - raycast = voided_obj.closest_point_on_mesh(voided_obj.matrix_world.inverted() @ target, distance=0.01) - if not raycast[0]: - target = filling_obj.matrix_world.col[3].to_3d().copy() - raycast = voided_obj.closest_point_on_mesh(voided_obj.matrix_world.inverted() @ target, distance=0.5) + + # Sometimes, the voided_obj may be an aggregate, which won't have any representation. + if voided_obj.data: + raycast = voided_obj.closest_point_on_mesh(voided_obj.matrix_world.inverted() @ target, distance=0.01) if not raycast[0]: - return + target = filling_obj.matrix_world.col[3].to_3d().copy() + raycast = voided_obj.closest_point_on_mesh(voided_obj.matrix_world.inverted() @ target, distance=0.5) + if not raycast[0]: + return - # In this prototype, we assume openings are only added to axis-based elements - layers = tool.Model.get_material_layer_parameters(element) - axis = tool.Model.get_wall_axis(voided_obj, layers=layers)["base"] + # In this prototype, we assume openings are only added to axis-based elements + layers = tool.Model.get_material_layer_parameters(element) + axis = tool.Model.get_wall_axis(voided_obj, layers=layers)["base"] - new_matrix = voided_obj.matrix_world.copy() - new_matrix.col[3] = tool.Cad.point_on_edge(target, axis).to_4d() + new_matrix = voided_obj.matrix_world.copy() + new_matrix.col[3] = tool.Cad.point_on_edge(target, axis).to_4d() - if filling.is_a("IfcDoor"): - new_matrix[2][3] = voided_obj.matrix_world[2][3] - else: - new_matrix[2][3] = voided_obj.matrix_world[2][3] + (props.rl2 * unit_scale) + if filling.is_a("IfcDoor"): + new_matrix[2][3] = voided_obj.matrix_world[2][3] + else: + new_matrix[2][3] = voided_obj.matrix_world[2][3] + (props.rl2 * unit_scale) - filling_obj.matrix_world = new_matrix - bpy.context.view_layer.update() + filling_obj.matrix_world = new_matrix + bpy.context.view_layer.update() if tool.Ifc.is_moved(voided_obj): blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=voided_obj) @@ -139,18 +142,25 @@ class FilledOpeningGenerator: ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening=opening, element=element) ifcopenshell.api.run("void.add_filling", tool.Ifc.get(), opening=opening, element=filling) - representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id) - blenderbim.core.geometry.switch_representation( - tool.Ifc, - tool.Geometry, - obj=voided_obj, - representation=representation, - should_reload=True, - is_global=True, - should_sync_changes_first=False, - ) + voided_objs = [voided_obj] + # Openings affect all subelements of an aggregate + for subelement in ifcopenshell.util.element.get_decomposition(element): + subobj = tool.Ifc.get_object(subelement) + if subobj: + voided_objs.append(subobj) - return {"FINISHED"} + for voided_obj in voided_objs: + if voided_obj.data: + representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id) + blenderbim.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=voided_obj, + representation=representation, + should_reload=True, + is_global=True, + should_sync_changes_first=False, + ) def regenerate_from_type(self, usecase_path, ifc_file, settings): filling = settings["related_object"] @@ -306,21 +316,28 @@ class RecalculateFill(bpy.types.Operator, tool.Ifc.Operator): ifcopenshell.api.run( "geometry.edit_object_placement", tool.Ifc.get(), product=opening, matrix=obj.matrix_world ) + + decomposed_building_elements = set() for building_element in building_elements: + decomposed_building_elements.add(building_element) + decomposed_building_elements.update(ifcopenshell.util.element.get_decomposition(building_element)) + + for building_element in decomposed_building_elements: building_obj = tool.Ifc.get_object(building_element) - body = ifcopenshell.util.representation.get_representation( - building_element, "Model", "Body", "MODEL_VIEW" - ) - if body: - blenderbim.core.geometry.switch_representation( - tool.Ifc, - tool.Geometry, - obj=building_obj, - representation=body, - should_reload=True, - is_global=True, - should_sync_changes_first=False, + if building_obj and building_obj.data: + body = ifcopenshell.util.representation.get_representation( + building_element, "Model", "Body", "MODEL_VIEW" ) + if body: + blenderbim.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=building_obj, + representation=body, + should_reload=True, + is_global=True, + should_sync_changes_first=False, + ) return {"FINISHED"} @@ -695,18 +712,27 @@ class EditOpenings(Operator, tool.Ifc.Operator): tool.Ifc.unlink(element=opening, obj=opening_obj) bpy.data.objects.remove(opening_obj) + decomposed_building_objs = set() for obj in building_objs: - element = tool.Ifc.get_entity(obj) - body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") - blenderbim.core.geometry.switch_representation( - tool.Ifc, - tool.Geometry, - obj=obj, - representation=body, - should_reload=True, - is_global=True, - should_sync_changes_first=False, - ) + decomposed_building_objs.add(obj) + for subelement in ifcopenshell.util.element.get_decomposition(tool.Ifc.get_entity(obj)): + subobj = tool.Ifc.get_object(subelement) + if subobj: + decomposed_building_objs.add(subobj) + + for obj in decomposed_building_objs: + if obj.data: + element = tool.Ifc.get_entity(obj) + body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") + blenderbim.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=obj, + representation=body, + should_reload=True, + is_global=True, + should_sync_changes_first=False, + ) return {"FINISHED"} def get_all_building_objects_of_similar_openings(self, opening): diff --git a/src/blenderbim/blenderbim/bim/module/void/operator.py b/src/blenderbim/blenderbim/bim/module/void/operator.py index ca9cdccb8c..3559dea9c9 100644 --- a/src/blenderbim/blenderbim/bim/module/void/operator.py +++ b/src/blenderbim/blenderbim/bim/module/void/operator.py @@ -22,6 +22,7 @@ import ifcopenshell.util.representation import blenderbim.tool as tool import blenderbim.core.geometry from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.module.model.opening import FilledOpeningGenerator class AddOpening(bpy.types.Operator, tool.Ifc.Operator): @@ -52,8 +53,6 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator): element1, element2 = element2, element1 if element1.is_a("IfcOpeningElement"): return {"FINISHED"} - if not obj1.data or not hasattr(obj1.data, "BIMMeshProperties"): - return {"FINISHED"} if tool.Ifc.is_moved(obj1): blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj1) @@ -76,16 +75,24 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator): ) ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening=element2, element=element1) - representation = tool.Ifc.get().by_id(obj1.data.BIMMeshProperties.ifc_definition_id) - blenderbim.core.geometry.switch_representation( - tool.Ifc, - tool.Geometry, - obj=obj1, - representation=representation, - should_reload=True, - is_global=True, - should_sync_changes_first=False, - ) + voided_objs = [obj1] + for subelement in ifcopenshell.util.element.get_decomposition(element1): + subobj = tool.Ifc.get_object(subelement) + if subobj: + voided_objs.append(subobj) + + for voided_obj in voided_objs: + if voided_obj.data: + representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id) + blenderbim.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=voided_obj, + representation=representation, + should_reload=True, + is_global=True, + should_sync_changes_first=False, + ) if not has_visible_openings: tool.Ifc.unlink(obj=obj2) @@ -113,15 +120,24 @@ class RemoveOpening(bpy.types.Operator, tool.Ifc.Operator): ifcopenshell.api.run("void.remove_opening", tool.Ifc.get(), opening=opening) - blenderbim.core.geometry.switch_representation( - tool.Ifc, - tool.Geometry, - obj=obj, - representation=tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id), - should_reload=True, - is_global=True, - should_sync_changes_first=False, - ) + decomposed_building_elements = set([element]) + decomposed_building_elements.update(ifcopenshell.util.element.get_decomposition(element)) + + for building_element in decomposed_building_elements: + building_obj = tool.Ifc.get_object(building_element) + if building_obj and building_obj.data: + body = ifcopenshell.util.representation.get_representation( + building_element, "Model", "Body", "MODEL_VIEW" + ) + blenderbim.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=building_obj, + representation=body, + should_reload=True, + is_global=True, + should_sync_changes_first=False, + ) tool.Geometry.clear_cache(element) return {"FINISHED"} diff --git a/src/blenderbim/test/bim/feature/aggregate.feature b/src/blenderbim/test/bim/feature/aggregate.feature index 0403f91384..0a2cca652f 100644 --- a/src/blenderbim/test/bim/feature/aggregate.feature +++ b/src/blenderbim/test/bim/feature/aggregate.feature @@ -39,6 +39,25 @@ Scenario: Unassign object And the object "IfcBuildingStorey/My Storey" is in the collection "IfcBuildingStorey/My Storey" And the collection "IfcBuildingStorey/My Storey" is in the collection "IfcProject/My Project" +Scenario: Unassign object - multiple objects are contained again to their indirect container + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcMember" + And I press "bim.assign_class" + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcCovering" + And I press "bim.assign_class" + And the object "IfcMember/Cube" is selected + And additionally the object "IfcCovering/Cube" is selected + And I press "bim.add_aggregate(ifc_class='IfcWall')" + And the object "IfcMember/Cube" is selected + And additionally the object "IfcCovering/Cube" is selected + When I press "bim.unassign_object" + Then the object "IfcMember/Cube" is in the collection "IfcBuildingStorey/My Storey" + And the object "IfcCovering/Cube" is in the collection "IfcBuildingStorey/My Storey" + Scenario: Add aggregate Given an empty IFC project And I add a cube @@ -79,3 +98,21 @@ Scenario: Add aggregate - add a nested aggregate And the object "IfcElementAssembly/Assembly.001" is in the collection "IfcElementAssembly/Assembly.001" And the collection "IfcElementAssembly/Assembly.001" is in the collection "IfcElementAssembly/Assembly" And the collection "IfcElementAssembly/Assembly" is in the collection "IfcBuildingStorey/My Storey" + +Scenario: Add aggregate - add multiple elements to a custom aggregate class + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcMember" + And I press "bim.assign_class" + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcCovering" + And I press "bim.assign_class" + And the object "IfcMember/Cube" is selected + And additionally the object "IfcCovering/Cube" is selected + When I press "bim.add_aggregate(ifc_class='IfcWall')" + Then the object "IfcMember/Cube" is in the collection "IfcWall/Assembly" + And the object "IfcCovering/Cube" is in the collection "IfcWall/Assembly" + And the object "IfcWall/Assembly" is in the collection "IfcWall/Assembly" + And the collection "IfcWall/Assembly" is in the collection "IfcBuildingStorey/My Storey"