diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index 2ff9d34678..723cd2f4b7 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -1201,20 +1201,20 @@ class RefreshAggregate(bpy.types.Operator): "instance_of": [part.GlobalId], } data = [data_children] - + pset = ifcopenshell.api.run( - "pset.add_pset", tool.Ifc.get(), product=part, name="BBIM_Aggregate_Data" - ) + "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": 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, @@ -1223,34 +1223,34 @@ class RefreshAggregate(bpy.types.Operator): 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" - ) + "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": json.dumps(data)}, ) - + part_obj = tool.Ifc.get_object(part) new_part = duplicate_objects(part_obj) blenderbim.core.aggregate.assign_object( @@ -1271,7 +1271,7 @@ class RefreshAggregate(bpy.types.Operator): 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 @@ -1283,15 +1283,15 @@ class RefreshAggregate(bpy.types.Operator): 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), - ) + 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] @@ -1314,7 +1314,6 @@ class RefreshAggregate(bpy.types.Operator): 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.") @@ -1337,7 +1336,6 @@ class RefreshAggregate(bpy.types.Operator): 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: @@ -1355,7 +1353,7 @@ class RefreshAggregate(bpy.types.Operator): new_obj = tool.Ifc.get_object(new[0]) - matrix_diff = Matrix.inverted(original_matrix) @ new_obj.matrix_world + matrix_diff = Matrix.inverted(original_matrix) @ new_obj.matrix_world new_matrix = selected_matrix @ matrix_diff new_obj.matrix_world = new_matrix diff --git a/src/blenderbim/blenderbim/bim/module/model/data.py b/src/blenderbim/blenderbim/bim/module/model/data.py index c9fde070e8..b97b16c614 100644 --- a/src/blenderbim/blenderbim/bim/module/model/data.py +++ b/src/blenderbim/blenderbim/bim/module/model/data.py @@ -251,6 +251,8 @@ class AuthoringData: @classmethod def predefined_type(cls): relating_type_id = cls.props.relating_type_id + if not relating_type_id: + return relating_type = tool.Ifc.get().by_id(int(relating_type_id)) predefined_type = relating_type.PredefinedType return predefined_type diff --git a/src/blenderbim/blenderbim/bim/module/model/opening.py b/src/blenderbim/blenderbim/bim/module/model/opening.py index ce57371569..9cb00d0169 100644 --- a/src/blenderbim/blenderbim/bim/module/model/opening.py +++ b/src/blenderbim/blenderbim/bim/module/model/opening.py @@ -18,6 +18,7 @@ import bpy import gpu +import json import bmesh import shapely import logging @@ -523,10 +524,21 @@ class AddBoolean(Operator, tool.Ifc.Operator): elif obj2.data: mesh_data = {"type": "Mesh", "blender_obj": obj1, "blender_void": obj2} - ifcopenshell.api.run( + booleans = ifcopenshell.api.run( "geometry.add_boolean", tool.Ifc.get(), representation=representation, operator="DIFFERENCE", **mesh_data ) + pset = ifcopenshell.util.element.get_pset(element1, "BBIM_Boolean") + if pset: + pset = tool.Ifc.get().by_id(pset["id"]) + data = json.loads(pset["Data"]) + data.extend([b.id() for b in booleans]) + data = list(set(data)) + else: + pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element1, name="BBIM_Boolean") + data = [b.id() for b in booleans] + ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": json.dumps(data)}) + tool.Model.clear_scene_openings() blenderbim.core.geometry.switch_representation( @@ -647,6 +659,7 @@ class RemoveBooleans(Operator, tool.Ifc.Operator, AddObjectHelper): def _execute(self, context): upstream_obj = None + bbim_boolean_updates = {} for obj in context.selected_objects: if ( not obj.data @@ -655,13 +668,21 @@ class RemoveBooleans(Operator, tool.Ifc.Operator, AddObjectHelper): ): continue try: - boolean = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_boolean_id) + item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_boolean_id) except: continue - ifcopenshell.api.run("geometry.remove_boolean", tool.Ifc.get(), item=boolean) + + boolean_id = None + for inverse in tool.Ifc.get().get_inverse(item): + if inverse.is_a("IfcBooleanResult"): + boolean_id = inverse.id() + break + ifcopenshell.api.run("geometry.remove_boolean", tool.Ifc.get(), item=item) + if obj.data.BIMMeshProperties.obj: upstream_obj = obj.data.BIMMeshProperties.obj element = tool.Ifc.get_entity(upstream_obj) + bbim_boolean_updates.setdefault(element, []).append(boolean_id) body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") if body: blenderbim.core.geometry.switch_representation( @@ -674,7 +695,20 @@ class RemoveBooleans(Operator, tool.Ifc.Operator, AddObjectHelper): should_sync_changes_first=False, ) bpy.data.objects.remove(obj) - + + for element, boolean_ids in bbim_boolean_updates.items(): + pset = ifcopenshell.util.element.get_pset(element, "BBIM_Boolean") + if not pset: + continue + data = set(json.loads(pset["Data"])) + data -= set(boolean_ids) + data = list(data) + pset = tool.Ifc.get().by_id(pset["id"]) + if data: + ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": json.dumps(data)}) + else: + ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset) + tool.Blender.set_active_object(upstream_obj) return {"FINISHED"} @@ -782,6 +816,7 @@ class EditOpenings(Operator, tool.Ifc.Operator): results.add(obj) return results + class CloneOpening(Operator, tool.Ifc.Operator): bl_idname = "bim.clone_opening" bl_label = "Clone Opening" @@ -805,12 +840,15 @@ class CloneOpening(Operator, tool.Ifc.Operator): new_opening = ifcopenshell.api.run("root.create_entity", tool.Ifc.get(), ifc_class="IfcOpeningElement") for representation in opening_representations: - ifcopenshell.api.run("geometry.assign_representation", tool.Ifc.get(), product = new_opening, representation = representation) + ifcopenshell.api.run( + "geometry.assign_representation", tool.Ifc.get(), product=new_opening, representation=representation + ) - ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening = new_opening, element = wall) + ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening=new_opening, element=wall) new_opening.ObjectPlacement = opening_placement return {"FINISHED"} + # TODO: merge with ProfileDecorator? class DecorationsHandler: installed = None diff --git a/src/blenderbim/blenderbim/bim/module/void/ui.py b/src/blenderbim/blenderbim/bim/module/void/ui.py index de0b978650..dd199ef5d0 100644 --- a/src/blenderbim/blenderbim/bim/module/void/ui.py +++ b/src/blenderbim/blenderbim/bim/module/void/ui.py @@ -123,9 +123,8 @@ class BIM_PT_booleans(Panel): upstream_obj_ifc_id = upsteam_obj.BIMObjectProperties.ifc_definition_id row = layout.row(align=True) - row.label(text="Used as a boolean operand with:") - row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = upstream_obj_ifc_id row.label(text=upsteam_obj.name) + row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = upstream_obj_ifc_id row = layout.row() row.operator("bim.remove_booleans", text="Remove Boolean", icon="X") diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index 47ae276c3a..b296bb5501 100644 --- a/src/blenderbim/blenderbim/tool/model.py +++ b/src/blenderbim/blenderbim/tool/model.py @@ -17,7 +17,10 @@ # along with BlenderBIM Add-on. If not, see . import bpy +import json import bmesh +import collections +import numpy as np import ifcopenshell import ifcopenshell.util.unit import ifcopenshell.util.placement @@ -29,9 +32,6 @@ from mathutils import Matrix, Vector from blenderbim.bim import import_ifc from blenderbim.bim.module.geometry.helper import Helper from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData, WindowData, DoorData -import collections -import json -import numpy as np class Model(blenderbim.core.tool.Model): @@ -483,6 +483,10 @@ class Model(blenderbim.core.tool.Model): @classmethod def get_manual_booleans(cls, element): + pset = ifcopenshell.util.element.get_pset(element, "BBIM_Boolean") + if not pset: + return [] + boolean_ids = json.loads(pset["Data"]) body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") if not body: return [] @@ -490,10 +494,9 @@ class Model(blenderbim.core.tool.Model): items = list(body.Items) while items: item = items.pop() - if item.is_a() == "IfcBooleanResult": - booleans.append(item) - items.append(item.FirstOperand) - elif item.is_a("IfcBooleanClippingResult"): + if item.is_a("IfcBooleanResult"): + if item.id() in boolean_ids: + booleans.append(item) items.append(item.FirstOperand) return booleans diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_boolean.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_boolean.py index c477d0cfe6..3973b0a27f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_boolean.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_boolean.py @@ -47,13 +47,21 @@ class Usecase: result = self.create_blender_mesh() items = [] for item in self.settings["representation"].Items: - # For now, we don't use IfcBooleanClippingResult. - # This is unofficial but we assume that clipping results are part - # of automated clips, whereas IfcBooleanResults are manual bools. - # This is really terrible, but until we find a better solution... - items.append(self.file.createIfcBooleanResult(self.settings["operator"], item, result)) + if ( + self.settings["operator"] == "DIFFERENCE" + and result.is_a("IfcHalfSpaceSolid") + and ( + item.is_a("IfcSweptAreaSolid") + or item.is_a("IfcSweptDiskSolid") + or item.is_a("IfcBooleanClippingResult") + ) + ): + items.append(self.file.createIfcBooleanClippingResult(self.settings["operator"], item, result)) + else: + items.append(self.file.createIfcBooleanResult(self.settings["operator"], item, result)) self.settings["representation"].RepresentationType = "CSG" self.settings["representation"].Items = items + return items def create_half_space_solid(self): clipping = np.array(self.settings["matrix"])[:3] diff --git a/src/ifcopenshell-python/test/util/test_selector.py b/src/ifcopenshell-python/test/util/test_selector.py index c5b69a8724..982f3a5095 100644 --- a/src/ifcopenshell-python/test/util/test_selector.py +++ b/src/ifcopenshell-python/test/util/test_selector.py @@ -351,8 +351,8 @@ class TestSelector(test.bootstrap.IFC4): pset_2 = ifcopenshell.api.run("pset.add_pset", self.file, product=element_2, name="Foo_Bar") ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset_1, properties={"Foo": "Bar"}) ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset_2, properties={"Foo": "BOO"}) - assert subject.Selector.parse(self.file, '.IfcElement[Foo_Bar.Foo != "Bar"]') == [element_2] - assert subject.Selector.parse(self.file, '.IfcElement[Foo_Bar.Foo != "BOO"]') == [element_1] + assert subject.Selector.parse(self.file, '.IfcElement["Foo_Bar"."Foo" != "Bar"]') == [element_2] + assert subject.Selector.parse(self.file, '.IfcElement["Foo_Bar"."Foo" != "BOO"]') == [element_1] def test_selecting_when_attribute_is_none(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")