Manual booleans are now tracked via a Pset instead of arbitrary class conventions which should fix a bunch of issues.

This commit is contained in:
Dion Moult
2023-10-10 17:33:00 +11:00
parent e4f5b0e3d8
commit 05f6f4efab
7 changed files with 93 additions and 45 deletions
@@ -1203,8 +1203,8 @@ class RefreshAggregate(bpy.types.Operator):
data = [data_children] data = [data_children]
pset = ifcopenshell.api.run( 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( ifcopenshell.api.run(
"pset.edit_pset", "pset.edit_pset",
tool.Ifc.get(), tool.Ifc.get(),
@@ -1232,8 +1232,8 @@ class RefreshAggregate(bpy.types.Operator):
else: else:
if not pset: if not pset:
pset = ifcopenshell.api.run( 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: else:
pset = ifcopenshell.util.element.get_pset(part, "BBIM_Aggregate_Data") pset = ifcopenshell.util.element.get_pset(part, "BBIM_Aggregate_Data")
pset = tool.Ifc.get().by_id(pset["id"]) pset = tool.Ifc.get().by_id(pset["id"])
@@ -1286,12 +1286,12 @@ class RefreshAggregate(bpy.types.Operator):
if not new_entity.is_a("IfcElementAssembly"): if not new_entity.is_a("IfcElementAssembly"):
blenderbim.core.aggregate.unassign_object( blenderbim.core.aggregate.unassign_object(
tool.Ifc, tool.Ifc,
tool.Aggregate, tool.Aggregate,
tool.Collector, tool.Collector,
relating_obj=obj, relating_obj=obj,
related_obj=tool.Ifc.get_object(new_entity), related_obj=tool.Ifc.get_object(new_entity),
) )
old_to_new[tool.Ifc.get_entity(obj)] = [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.") self.report({"INFO"}, "Object is not part of a IfcElementAssembly.")
return {"FINISHED"} return {"FINISHED"}
pset = ifcopenshell.util.element.get_pset(selected_root_entity, "BBIM_Aggregate_Data") pset = ifcopenshell.util.element.get_pset(selected_root_entity, "BBIM_Aggregate_Data")
if not pset: if not pset:
self.report({"INFO"}, "Object is not part of an assembly aggregate.") self.report({"INFO"}, "Object is not part of an assembly aggregate.")
@@ -1337,7 +1336,6 @@ class RefreshAggregate(bpy.types.Operator):
for parent in parents: for parent in parents:
duplicate_children(parent) duplicate_children(parent)
# Remove connections with old objects # Remove connections with old objects
for new in old_to_new.values(): for new in old_to_new.values():
for connection in new[0].ConnectedTo: for connection in new[0].ConnectedTo:
@@ -1355,7 +1353,7 @@ class RefreshAggregate(bpy.types.Operator):
new_obj = tool.Ifc.get_object(new[0]) 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_matrix = selected_matrix @ matrix_diff
new_obj.matrix_world = new_matrix new_obj.matrix_world = new_matrix
@@ -251,6 +251,8 @@ class AuthoringData:
@classmethod @classmethod
def predefined_type(cls): def predefined_type(cls):
relating_type_id = cls.props.relating_type_id 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)) relating_type = tool.Ifc.get().by_id(int(relating_type_id))
predefined_type = relating_type.PredefinedType predefined_type = relating_type.PredefinedType
return predefined_type return predefined_type
@@ -18,6 +18,7 @@
import bpy import bpy
import gpu import gpu
import json
import bmesh import bmesh
import shapely import shapely
import logging import logging
@@ -523,10 +524,21 @@ class AddBoolean(Operator, tool.Ifc.Operator):
elif obj2.data: elif obj2.data:
mesh_data = {"type": "Mesh", "blender_obj": obj1, "blender_void": obj2} 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 "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() tool.Model.clear_scene_openings()
blenderbim.core.geometry.switch_representation( blenderbim.core.geometry.switch_representation(
@@ -647,6 +659,7 @@ class RemoveBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
def _execute(self, context): def _execute(self, context):
upstream_obj = None upstream_obj = None
bbim_boolean_updates = {}
for obj in context.selected_objects: for obj in context.selected_objects:
if ( if (
not obj.data not obj.data
@@ -655,13 +668,21 @@ class RemoveBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
): ):
continue continue
try: 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: except:
continue 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: if obj.data.BIMMeshProperties.obj:
upstream_obj = obj.data.BIMMeshProperties.obj upstream_obj = obj.data.BIMMeshProperties.obj
element = tool.Ifc.get_entity(upstream_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") body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if body: if body:
blenderbim.core.geometry.switch_representation( blenderbim.core.geometry.switch_representation(
@@ -675,6 +696,19 @@ class RemoveBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
) )
bpy.data.objects.remove(obj) 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) tool.Blender.set_active_object(upstream_obj)
return {"FINISHED"} return {"FINISHED"}
@@ -782,6 +816,7 @@ class EditOpenings(Operator, tool.Ifc.Operator):
results.add(obj) results.add(obj)
return results return results
class CloneOpening(Operator, tool.Ifc.Operator): class CloneOpening(Operator, tool.Ifc.Operator):
bl_idname = "bim.clone_opening" bl_idname = "bim.clone_opening"
bl_label = "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") new_opening = ifcopenshell.api.run("root.create_entity", tool.Ifc.get(), ifc_class="IfcOpeningElement")
for representation in opening_representations: 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 new_opening.ObjectPlacement = opening_placement
return {"FINISHED"} return {"FINISHED"}
# TODO: merge with ProfileDecorator? # TODO: merge with ProfileDecorator?
class DecorationsHandler: class DecorationsHandler:
installed = None installed = None
@@ -123,9 +123,8 @@ class BIM_PT_booleans(Panel):
upstream_obj_ifc_id = upsteam_obj.BIMObjectProperties.ifc_definition_id upstream_obj_ifc_id = upsteam_obj.BIMObjectProperties.ifc_definition_id
row = layout.row(align=True) 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.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 = layout.row()
row.operator("bim.remove_booleans", text="Remove Boolean", icon="X") row.operator("bim.remove_booleans", text="Remove Boolean", icon="X")
+10 -7
View File
@@ -17,7 +17,10 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>. # along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy import bpy
import json
import bmesh import bmesh
import collections
import numpy as np
import ifcopenshell import ifcopenshell
import ifcopenshell.util.unit import ifcopenshell.util.unit
import ifcopenshell.util.placement import ifcopenshell.util.placement
@@ -29,9 +32,6 @@ from mathutils import Matrix, Vector
from blenderbim.bim import import_ifc from blenderbim.bim import import_ifc
from blenderbim.bim.module.geometry.helper import Helper from blenderbim.bim.module.geometry.helper import Helper
from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData, WindowData, DoorData 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): class Model(blenderbim.core.tool.Model):
@@ -483,6 +483,10 @@ class Model(blenderbim.core.tool.Model):
@classmethod @classmethod
def get_manual_booleans(cls, element): 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") body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not body: if not body:
return [] return []
@@ -490,10 +494,9 @@ class Model(blenderbim.core.tool.Model):
items = list(body.Items) items = list(body.Items)
while items: while items:
item = items.pop() item = items.pop()
if item.is_a() == "IfcBooleanResult": if item.is_a("IfcBooleanResult"):
booleans.append(item) if item.id() in boolean_ids:
items.append(item.FirstOperand) booleans.append(item)
elif item.is_a("IfcBooleanClippingResult"):
items.append(item.FirstOperand) items.append(item.FirstOperand)
return booleans return booleans
@@ -47,13 +47,21 @@ class Usecase:
result = self.create_blender_mesh() result = self.create_blender_mesh()
items = [] items = []
for item in self.settings["representation"].Items: for item in self.settings["representation"].Items:
# For now, we don't use IfcBooleanClippingResult. if (
# This is unofficial but we assume that clipping results are part self.settings["operator"] == "DIFFERENCE"
# of automated clips, whereas IfcBooleanResults are manual bools. and result.is_a("IfcHalfSpaceSolid")
# This is really terrible, but until we find a better solution... and (
items.append(self.file.createIfcBooleanResult(self.settings["operator"], item, result)) 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"].RepresentationType = "CSG"
self.settings["representation"].Items = items self.settings["representation"].Items = items
return items
def create_half_space_solid(self): def create_half_space_solid(self):
clipping = np.array(self.settings["matrix"])[:3] clipping = np.array(self.settings["matrix"])[:3]
@@ -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") 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_1, properties={"Foo": "Bar"})
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset_2, properties={"Foo": "BOO"}) 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" != "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" != "BOO"]') == [element_1]
def test_selecting_when_attribute_is_none(self): def test_selecting_when_attribute_is_none(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")