From 9400eab0ff4f90bec177d215e4bc6c76b7492550 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 12 Jan 2021 15:12:53 +1100 Subject: [PATCH] WIP implement void module with add / remove openings. See #1222. --- .../blenderbim/bim/__init__.py | 105 ++++++------------ .../bim/module/geometry/operator.py | 7 +- .../blenderbim/bim/module/root/operator.py | 9 +- .../blenderbim/bim/module/root/ui.py | 4 +- .../blenderbim/bim/module/void/__init__.py | 16 +++ .../blenderbim/bim/module/void/add_opening.py | 16 +++ .../blenderbim/bim/module/void/data.py | 53 +++++++++ .../blenderbim/bim/module/void/operator.py | 74 ++++++++++++ .../bim/module/void/remove_opening.py | 20 ++++ .../blenderbim/bim/module/void/ui.py | 56 ++++++++++ 10 files changed, 282 insertions(+), 78 deletions(-) create mode 100644 src/ifcblenderexport/blenderbim/bim/module/void/__init__.py create mode 100644 src/ifcblenderexport/blenderbim/bim/module/void/add_opening.py create mode 100644 src/ifcblenderexport/blenderbim/bim/module/void/data.py create mode 100644 src/ifcblenderexport/blenderbim/bim/module/void/operator.py create mode 100644 src/ifcblenderexport/blenderbim/bim/module/void/remove_opening.py create mode 100644 src/ifcblenderexport/blenderbim/bim/module/void/ui.py diff --git a/src/ifcblenderexport/blenderbim/bim/__init__.py b/src/ifcblenderexport/blenderbim/bim/__init__.py index 4d0efb5ab2..511fa5d248 100644 --- a/src/ifcblenderexport/blenderbim/bim/__init__.py +++ b/src/ifcblenderexport/blenderbim/bim/__init__.py @@ -5,25 +5,33 @@ bpy = sys.modules.get("bpy") if bpy is not None: import bpy - import blenderbim.bim.module.root as module_root - import blenderbim.bim.module.aggregate as module_aggregate - import blenderbim.bim.module.attribute as module_attribute - import blenderbim.bim.module.bcf as module_bcf - import blenderbim.bim.module.context as module_context - import blenderbim.bim.module.covetool as module_covetool - import blenderbim.bim.module.debug as module_debug - import blenderbim.bim.module.geometry as module_geometry - import blenderbim.bim.module.model as module_model - import blenderbim.bim.module.owner as module_owner - import blenderbim.bim.module.project as module_project - import blenderbim.bim.module.pset as module_pset - import blenderbim.bim.module.spatial as module_spatial - import blenderbim.bim.module.style as module_style - import blenderbim.bim.module.type as module_type - import blenderbim.bim.module.unit as module_unit - import blenderbim.bim.module.cobie as module_cobie + import importlib from . import ui, prop, operator + modules = { + "root": None, + "aggregate": None, + "attribute": None, + "bcf": None, + "cobie": None, + "context": None, + "covetool": None, + "debug": None, + "geometry": None, + "model": None, + "owner": None, + "project": None, + "pset": None, + "spatial": None, + "style": None, + "type": None, + "unit": None, + "void": None, + } + + for name in modules.keys(): + modules[name] = importlib.import_module(f"blenderbim.bim.module.{name}") + classes = [ operator.SelectClass, operator.SelectType, @@ -286,24 +294,9 @@ if bpy is not None: ui.BIM_ADDON_preferences, ] - classes.extend(module_root.classes) - classes.extend(module_aggregate.classes) - classes.extend(module_attribute.classes) - classes.extend(module_bcf.classes) - classes.extend(module_context.classes) - classes.extend(module_covetool.classes) - classes.extend(module_debug.classes) - classes.extend(module_geometry.classes) - classes.extend(module_model.classes) - classes.extend(module_owner.classes) - classes.extend(module_project.classes) - classes.extend(module_pset.classes) - classes.extend(module_spatial.classes) - classes.extend(module_style.classes) - classes.extend(module_type.classes) - classes.extend(module_unit.classes) - classes.extend(module_cobie.classes) - + for module in modules.values(): + classes.extend(module.classes) + def menu_func_export(self, context): self.layout.operator(operator.ExportIFC.bl_idname, text="Industry Foundation Classes (.ifc/.ifczip/.ifcjson)") @@ -333,23 +326,10 @@ if bpy is not None: bpy.types.Camera.BIMCameraProperties = bpy.props.PointerProperty(type=prop.BIMCameraProperties) bpy.types.TextCurve.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties) bpy.types.SCENE_PT_unit.append(ui.ifc_units) - module_root.register() - module_aggregate.register() - module_attribute.register() - module_bcf.register() - module_context.register() - module_covetool.register() - module_debug.register() - module_geometry.register() - module_model.register() - module_owner.register() - module_project.register() - module_pset.register() - module_spatial.register() - module_style.register() - module_type.register() - module_unit.register() - module_cobie.register() + + for module in modules.values(): + module.register() + bpy.app.handlers.depsgraph_update_pre.append(operator.depsgraph_update_pre_handler) bpy.app.handlers.load_post.append(prop.toggleDecorationsOnLoad) @@ -370,21 +350,8 @@ if bpy is not None: del bpy.types.Camera.BIMCameraProperties del bpy.types.TextCurve.BIMTextProperties bpy.types.SCENE_PT_unit.remove(ui.ifc_units) - module_cobie.unregister() - module_unit.unregister() - module_type.unregister() - module_style.unregister() - module_spatial.unregister() - module_pset.unregister() - module_project.unregister() - module_owner.unregister() - module_model.unregister() - module_geometry.unregister() - module_debug.unregister() - module_covetool.unregister() - module_context.unregister() - module_bcf.unregister() - module_attribute.register() - module_aggregate.register() - module_root.unregister() + + for module in reversed(list(modules.values())): + module.unregister() + bpy.app.handlers.depsgraph_update_pre.remove(operator.depsgraph_update_pre_handler) diff --git a/src/ifcblenderexport/blenderbim/bim/module/geometry/operator.py b/src/ifcblenderexport/blenderbim/bim/module/geometry/operator.py index 552d502530..10e1855669 100644 --- a/src/ifcblenderexport/blenderbim/bim/module/geometry/operator.py +++ b/src/ifcblenderexport/blenderbim/bim/module/geometry/operator.py @@ -34,11 +34,12 @@ class AddRepresentation(bpy.types.Operator): bl_idname = "bim.add_representation" bl_label = "Add Representation" obj: bpy.props.StringProperty() + context_id: bpy.props.IntProperty() def execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object self.file = IfcStore.get_file() - self.context_id = bpy.context.scene.BIMProperties.contexts + context_id = self.context_id or int(bpy.context.scene.BIMProperties.contexts) bpy.ops.bim.edit_object_placement(obj=obj.name) @@ -46,7 +47,7 @@ class AddRepresentation(bpy.types.Operator): result = add_representation.Usecase( self.file, { - "context": self.file.by_id(int(self.context_id)), + "context": self.file.by_id(context_id), "geometry": obj.data, "total_items": max(1, len(obj.material_slots)), }, @@ -71,7 +72,7 @@ class AddRepresentation(bpy.types.Operator): existing_mesh = obj.data mesh = obj.data.copy() - mesh.name = "{}/{}".format(self.context_id, result.id()) + mesh.name = "{}/{}".format(context_id, result.id()) mesh.BIMMeshProperties.ifc_definition_id = int(result.id()) obj.data = mesh Data.load(obj.BIMObjectProperties.ifc_definition_id) diff --git a/src/ifcblenderexport/blenderbim/bim/module/root/operator.py b/src/ifcblenderexport/blenderbim/bim/module/root/operator.py index 657425cc00..aacf814278 100644 --- a/src/ifcblenderexport/blenderbim/bim/module/root/operator.py +++ b/src/ifcblenderexport/blenderbim/bim/module/root/operator.py @@ -76,6 +76,7 @@ class AssignClass(bpy.types.Operator): ifc_class: bpy.props.StringProperty() predefined_type: bpy.props.StringProperty() userdefined_type: bpy.props.StringProperty() + context_id: bpy.props.IntProperty() def execute(self, context): objects = [bpy.data.objects.get(self.obj)] if self.obj else bpy.context.selected_objects @@ -107,7 +108,7 @@ class AssignClass(bpy.types.Operator): obj.name = "{}/{}".format(product.is_a(), obj.name) obj.BIMObjectProperties.ifc_definition_id = int(product.id()) - bpy.ops.bim.add_representation(obj=obj.name) + bpy.ops.bim.add_representation(obj=obj.name, context_id=self.context_id) if product.is_a("IfcElementType"): self.place_in_types_collection(obj) @@ -160,12 +161,12 @@ class AssignClass(bpy.types.Operator): class UnassignClass(bpy.types.Operator): bl_idname = "bim.unassign_class" bl_label = "Unassign IFC Class" - object_name: bpy.props.StringProperty() + obj: bpy.props.StringProperty() def execute(self, context): self.file = IfcStore.get_file() - if self.object_name: - objects = [bpy.data.objects.get(self.object_name)] + if self.obj: + objects = [bpy.data.objects.get(self.obj)] else: objects = bpy.context.selected_objects for obj in objects: diff --git a/src/ifcblenderexport/blenderbim/bim/module/root/ui.py b/src/ifcblenderexport/blenderbim/bim/module/root/ui.py index 7ae4819f67..1d3e6180bd 100644 --- a/src/ifcblenderexport/blenderbim/bim/module/root/ui.py +++ b/src/ifcblenderexport/blenderbim/bim/module/root/ui.py @@ -3,6 +3,7 @@ from bpy.types import Panel from blenderbim.bim.module.root.data import Data from blenderbim.bim.ifc import IfcStore + class BIM_PT_class(Panel): bl_label = "IFC Class" bl_idname = "BIM_PT_class" @@ -34,8 +35,7 @@ class BIM_PT_class(Panel): row = self.layout.row(align=True) row.label(text=name) row.operator("bim.enable_reassign_class", icon="GREASEPENCIL", text="") - op = row.operator("bim.unassign_class", icon="X", text="") - op.object_name = context.active_object.name + row.operator("bim.unassign_class", icon="X", text="").obj = context.active_object.name else: self.draw_class_dropdowns() row = self.layout.row(align=True) diff --git a/src/ifcblenderexport/blenderbim/bim/module/void/__init__.py b/src/ifcblenderexport/blenderbim/bim/module/void/__init__.py new file mode 100644 index 0000000000..a5f2fe7f70 --- /dev/null +++ b/src/ifcblenderexport/blenderbim/bim/module/void/__init__.py @@ -0,0 +1,16 @@ +import bpy +from . import ui, operator + +classes = ( + operator.AddOpening, + operator.RemoveOpening, + ui.BIM_PT_voids, +) + + +def register(): + pass + + +def unregister(): + pass diff --git a/src/ifcblenderexport/blenderbim/bim/module/void/add_opening.py b/src/ifcblenderexport/blenderbim/bim/module/void/add_opening.py new file mode 100644 index 0000000000..5e813b405c --- /dev/null +++ b/src/ifcblenderexport/blenderbim/bim/module/void/add_opening.py @@ -0,0 +1,16 @@ +import ifcopenshell + + +class Usecase: + def __init__(self, file, settings=None): + self.file = file + self.settings = {"opening": None, "element": None} + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + self.file.create_entity("IfcRelVoidsElement", **{ + "GlobalId": ifcopenshell.guid.new(), + "RelatingBuildingElement": self.settings["element"], + "RelatedOpeningElement": self.settings["opening"] + }) diff --git a/src/ifcblenderexport/blenderbim/bim/module/void/data.py b/src/ifcblenderexport/blenderbim/bim/module/void/data.py new file mode 100644 index 0000000000..13d018f72c --- /dev/null +++ b/src/ifcblenderexport/blenderbim/bim/module/void/data.py @@ -0,0 +1,53 @@ +from blenderbim.bim.ifc import IfcStore + + +class Data: + products = {} + openings = {} + fillings = {} + + @classmethod + def load(cls, product_id): + print('loading', product_id) + file = IfcStore.get_file() + if not file: + return + cls.products[product_id] = set() + product = file.by_id(product_id) + for rel in file.by_type("IfcRelVoidsElement"): + building_element = rel.RelatingBuildingElement + if building_element != product: + continue + opening = rel.RelatedOpeningElement + opening_id = int(opening.id()) + cls.products[product_id].add(opening_id) + cls.openings[opening_id] = {"Name": opening.Name, "HasFillings": set()} + for rel in file.by_type("IfcRelFillsElement"): + opening = rel.RelatingOpeningElement + opening_id = int(opening.id()) + if opening_id not in cls.openings: + continue + filling = rel.RelatedBuildingElement + filling_id = int(filling.id()) + cls.fillings[filling_id] = {"Name": filling.Name, "FillsVoid": opening_id} + cls.openings[opening_id]["HasFillings"].add(filling_id) + return # See bug #1224 + #if not hasattr(product, "HasOpenings") or not product.HasOpenings: + # return + #for rel_voids_element in product.HasOpenings: + # opening = rel_voids_element.RelatedOpeningElement + # opening_id = int(opening.id()) + # fillings = [] + # if opening.HasFillings: + # for rel_fills_element in opening.HasFillings: + # filling = rel_fills_element.RelatedBuildingElement + # filling_id = int(filling.id()) + # fillings.append(filling_id) + # cls.fillings[filling_id] = { + # "Name": filling.Name, + # } + # cls.openings[opening_id] = { + # "Name": opening.Name, + # "HasFillings": fillings, + # } + # cls.products[product_id].append(opening_id) diff --git a/src/ifcblenderexport/blenderbim/bim/module/void/operator.py b/src/ifcblenderexport/blenderbim/bim/module/void/operator.py new file mode 100644 index 0000000000..970bfdc8ef --- /dev/null +++ b/src/ifcblenderexport/blenderbim/bim/module/void/operator.py @@ -0,0 +1,74 @@ +import bpy +import blenderbim.bim.module.void.add_opening as add_opening +import blenderbim.bim.module.void.remove_opening as remove_opening +from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.module.void.data import Data +from blenderbim.bim.module.context.data import Data as ContextData + + +class AddOpening(bpy.types.Operator): + bl_idname = "bim.add_opening" + bl_label = "Add Opening" + opening: bpy.props.StringProperty() + obj: bpy.props.StringProperty() + + def execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + opening = bpy.data.objects.get(self.opening) + if not opening.BIMObjectProperties.ifc_definition_id: + body_context_id = None + if not ContextData.is_loaded: + ContextData.load() + for context in ContextData.contexts.values(): + for subcontext_id, subcontext in context["HasSubContexts"].items(): + if subcontext["ContextType"] == "Model" and subcontext["ContextIdentifier"] == "Body": + body_context_id = subcontext_id + break + if not body_context_id: + return {"FINISHED"} + bpy.ops.bim.assign_class(obj=opening.name, ifc_class="IfcOpeningElement", context_id=body_context_id) + + self.file = IfcStore.get_file() + element_id = obj.BIMObjectProperties.ifc_definition_id + add_opening.Usecase(self.file, { + "opening": self.file.by_id(opening.BIMObjectProperties.ifc_definition_id), + "element": self.file.by_id(element_id) + }).execute() + Data.load(element_id) + + has_modifier = False + + for modifier in obj.modifiers: + if modifier.type == "BOOLEAN" and modifier.object and modifier.object == opening: + has_modifier = True + break + + if not has_modifier: + modifier = obj.modifiers.new("IfcOpeningElement", "BOOLEAN") + modifier.operation = "DIFFERENCE" + modifier.object = opening + return {"FINISHED"} + + +class RemoveOpening(bpy.types.Operator): + bl_idname = "bim.remove_opening" + bl_label = "Remove Opening" + opening_id: bpy.props.IntProperty() + obj: bpy.props.StringProperty() + + def execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + self.file = IfcStore.get_file() + for modifier in obj.modifiers: + if modifier.type != "BOOLEAN": + continue + if modifier.object and modifier.object.BIMObjectProperties.ifc_definition_id == self.opening_id: + modifier.object.BIMObjectProperties.ifc_definition_id = 0 + if "/" in modifier.object.name and modifier.object.name[0:3] == "Ifc": + modifier.object.name = "/".join(modifier.object.name.split("/")[1:]) + obj.modifiers.remove(modifier) + break + + remove_opening.Usecase(self.file, {"opening": self.file.by_id(self.opening_id)}).execute() + Data.load(obj.BIMObjectProperties.ifc_definition_id) + return {"FINISHED"} diff --git a/src/ifcblenderexport/blenderbim/bim/module/void/remove_opening.py b/src/ifcblenderexport/blenderbim/bim/module/void/remove_opening.py new file mode 100644 index 0000000000..cc84466790 --- /dev/null +++ b/src/ifcblenderexport/blenderbim/bim/module/void/remove_opening.py @@ -0,0 +1,20 @@ +class Usecase: + def __init__(self, file, settings=None): + self.file = file + self.settings = {"opening": None} + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + to_remove = [] # See bug #1224 + for rel in self.file.by_type("IfcRelVoidsElement"): + if rel.RelatedOpeningElement == self.settings["opening"]: + to_remove.append(rel) + break + for rel in self.file.by_type("IfcRelFillsElement"): + if rel.RelatingOpeningElement == self.settings["opening"]: + to_remove.append(rel) + break + self.file.remove(self.settings["opening"]) + for element in to_remove: + self.file.remove(element) diff --git a/src/ifcblenderexport/blenderbim/bim/module/void/ui.py b/src/ifcblenderexport/blenderbim/bim/module/void/ui.py new file mode 100644 index 0000000000..cc5144b7ee --- /dev/null +++ b/src/ifcblenderexport/blenderbim/bim/module/void/ui.py @@ -0,0 +1,56 @@ +import bpy +from bpy.types import Panel +from blenderbim.bim.module.void.data import Data + + +class BIM_PT_voids(Panel): + bl_label = "IFC Voids" + bl_idname = "BIM_PT_voids" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + + def draw(self, context): + props = context.active_object.BIMObjectProperties + if props.ifc_definition_id not in Data.products: + Data.load(props.ifc_definition_id) + + row = self.layout.row(align=True) + if len(context.selected_objects) == 2: + op = row.operator("bim.add_opening", icon="ADD", text="Add Selected") + for obj in context.selected_objects: + if "IfcOpeningElement" in obj.name or not obj.BIMObjectProperties.ifc_definition_id: + op.opening = obj.name + else: + op.obj = obj.name + + opening_id = None + obj_name = None + for obj in context.selected_objects: + if obj.BIMObjectProperties.ifc_definition_id in Data.openings: + opening_id = obj.BIMObjectProperties.ifc_definition_id + elif obj.BIMObjectProperties.ifc_definition_id in Data.fillings: + opening_id = Data.fillings[obj.BIMObjectProperties.ifc_definition_id]["FillsVoid"] + else: + obj_name = obj.name + if opening_id and obj_name: + op = row.operator("bim.remove_opening", icon="X", text="Remove Selected") + op.opening_id = opening_id + op.obj = obj_name + else: + row.label(text="Select an opening and an element to modify", icon="HELP") + + for opening_id in Data.products[props.ifc_definition_id]: + opening = Data.openings[opening_id] + if opening["HasFillings"]: + for filling_id in opening["HasFillings"]: + filling = Data.fillings[filling_id] + row = self.layout.row(align=True) + row.label(text=opening["Name"]) + row.label(text=filling["Name"]) + else: + row = self.layout.row(align=True) + row.label(text=opening["Name"]) + op = row.operator("bim.remove_opening", icon="X", text="") + op.opening_id = opening_id