From 62229b032ec708d2753c33fda787d13a653b490c Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 27 Oct 2022 23:35:51 +1100 Subject: [PATCH] You can now array IFC elements parametrically --- .../blenderbim/bim/module/model/__init__.py | 12 +- .../blenderbim/bim/module/model/array.py | 176 ++++++++++++++++++ .../blenderbim/bim/module/model/data.py | 29 +++ .../blenderbim/bim/module/model/product.py | 2 +- .../blenderbim/bim/module/model/prop.py | 8 + .../blenderbim/bim/module/model/ui.py | 56 +++++- src/blenderbim/blenderbim/tool/model.py | 58 ++++++ 7 files changed, 337 insertions(+), 4 deletions(-) create mode 100644 src/blenderbim/blenderbim/bim/module/model/array.py diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py index 3ac8f1317b..eb8b8f7bec 100644 --- a/src/blenderbim/blenderbim/bim/module/model/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py @@ -17,9 +17,15 @@ # along with BlenderBIM Add-on. If not, see . import bpy -from . import handler, prop, ui, grid, product, wall, slab, stair, opening, pie, workspace, profile +from . import handler, prop, ui, grid, array, product, wall, slab, stair, opening, pie, workspace, profile classes = ( + array.AddArray, + array.DisableEditingArray, + array.EditArray, + array.EnableEditingArray, + array.RemoveArray, + array.SelectArrayParent, product.AddConstrTypeInstance, product.AddEmptyType, product.AlignProduct, @@ -66,7 +72,9 @@ classes = ( prop.ConstrClassInfo, prop.ConstrBrowserState, prop.BIMModelProperties, + prop.BIMArrayProperties, ui.BIM_PT_authoring, + ui.BIM_PT_array, ui.DisplayConstrTypesUI, ui.LaunchTypeManager, ui.HelpConstrTypes, @@ -87,6 +95,7 @@ def register(): if not bpy.app.background: bpy.utils.register_tool(workspace.BimTool, after={"builtin.scale_cage"}, separator=True, group=True) bpy.types.Scene.BIMModelProperties = bpy.props.PointerProperty(type=prop.BIMModelProperties) + bpy.types.Object.BIMArrayProperties = bpy.props.PointerProperty(type=prop.BIMArrayProperties) bpy.types.VIEW3D_MT_mesh_add.append(grid.add_object_button) bpy.types.VIEW3D_MT_mesh_add.append(stair.add_object_button) bpy.types.VIEW3D_MT_add.append(ui.add_menu) @@ -103,6 +112,7 @@ def unregister(): if not bpy.app.background: bpy.utils.unregister_tool(workspace.BimTool) del bpy.types.Scene.BIMModelProperties + del bpy.types.Object.BIMArrayProperties bpy.app.handlers.load_post.remove(handler.load_post) bpy.types.VIEW3D_MT_mesh_add.remove(grid.add_object_button) bpy.types.VIEW3D_MT_mesh_add.remove(stair.add_object_button) diff --git a/src/blenderbim/blenderbim/bim/module/model/array.py b/src/blenderbim/blenderbim/bim/module/model/array.py new file mode 100644 index 0000000000..924b6c3f02 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/model/array.py @@ -0,0 +1,176 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2022 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +import json +import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.element +import blenderbim.tool as tool +from mathutils import Vector + + +class AddArray(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.add_array" + bl_label = "Add Array" + bl_options = {"REGISTER"} + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + + array = {"children": [], "count": 1, "x": 0.0, "y": 0.0, "z": 0.0} + + psets = ifcopenshell.util.element.get_psets(element) + pset = psets.get("BBIM_Array", None) + + if pset: + data = json.loads(pset["Data"]) + data.append(array) + pset = tool.Ifc.get().by_id(pset["id"]) + else: + pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Array") + data = [array] + + ifcopenshell.api.run( + "pset.edit_pset", + tool.Ifc.get(), + pset=pset, + properties={"Parent": element.GlobalId, "Data": json.dumps(data)}, + ) + return {"FINISHED"} + + +class DisableEditingArray(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.disable_editing_array" + bl_label = "Disable Editing Array" + bl_options = {"REGISTER"} + + def _execute(self, context): + props = context.active_object.BIMArrayProperties.is_editing = -1 + return {"FINISHED"} + + +class EditArray(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.edit_array" + bl_label = "Edit Array" + bl_options = {"REGISTER"} + item: bpy.props.IntProperty() + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + props = obj.BIMArrayProperties + + psets = ifcopenshell.util.element.get_psets(element) + pset = psets["BBIM_Array"] + data = json.loads(pset["Data"]) + data[self.item] = { + "children": data[self.item]["children"], + "count": props.count, + "x": props.x, + "y": props.y, + "z": props.z, + } + + props.is_editing = -1 + + try: + parent = tool.Ifc.get_object(tool.Ifc.get().by_guid(pset["Parent"])) + except: + return {"FINISHED"} + + tool.Model.regenerate_array(parent, data) + + pset = tool.Ifc.get().by_id(pset["id"]) + data = json.dumps(data) + ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": data}) + return {"FINISHED"} + + +class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.enable_editing_array" + bl_label = "Enable Editing Array" + bl_options = {"REGISTER"} + item: bpy.props.IntProperty() + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + psets = ifcopenshell.util.element.get_psets(element) + data = json.loads(psets["BBIM_Array"]["Data"])[self.item] + props = obj.BIMArrayProperties + props.count = data["count"] + props.x = data["x"] + props.y = data["y"] + props.z = data["z"] + props.is_editing = self.item + + +class RemoveArray(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.remove_array" + bl_label = "Remove Array" + bl_options = {"REGISTER"} + item: bpy.props.IntProperty() + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + props = obj.BIMArrayProperties + + psets = ifcopenshell.util.element.get_psets(element) + pset = psets["BBIM_Array"] + data = json.loads(pset["Data"]) + data[self.item]["count"] = 1 + + props.is_editing = -1 + + try: + parent = tool.Ifc.get_object(tool.Ifc.get().by_guid(pset["Parent"])) + except: + return {"FINISHED"} + + tool.Model.regenerate_array(parent, data) + + pset = tool.Ifc.get().by_id(pset["id"]) + if len(data) == 1: + ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset) + else: + del data[self.item] + data = json.dumps(data) + ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": data}) + + return {"FINISHED"} + + +class SelectArrayParent(bpy.types.Operator): + bl_idname = "bim.select_array_parent" + bl_label = "Select Array Parent" + bl_options = {"REGISTER", "UNDO"} + parent: bpy.props.StringProperty() + + def execute(self, context): + try: + element = tool.Ifc.get().by_guid(self.parent) + except: + return {"FINISHED"} + obj = tool.Ifc.get_object(element) + if obj: + context.view_layer.objects.active = obj + obj.select_set(True) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/model/data.py b/src/blenderbim/blenderbim/bim/module/model/data.py index b900c3af59..65d82b464e 100644 --- a/src/blenderbim/blenderbim/bim/module/model/data.py +++ b/src/blenderbim/blenderbim/bim/module/model/data.py @@ -18,8 +18,10 @@ import bpy import math +import json import functools import ifcopenshell +import ifcopenshell.util.element import blenderbim.tool as tool from blenderbim.bim.ifc import IfcStore from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound @@ -28,6 +30,7 @@ from blenderbim.bim.prop import get_ifc_entity_description, get_predefined_type_ def refresh(): AuthoringData.is_loaded = False + ArrayData.is_loaded = False class AuthoringData: @@ -316,3 +319,29 @@ class AuthoringData: def relating_type_id_by_name(cls, ifc_class, relating_type): relating_types = [ct[0] for ct in cls.relating_types(ifc_class=ifc_class) if ct[1] == relating_type] return None if len(relating_types) == 0 else relating_types[0] + + +class ArrayData: + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.is_loaded = True + cls.data = { "parameters": cls.parameters() } + + @classmethod + def parameters(cls): + element = tool.Ifc.get_entity(bpy.context.active_object) + if element: + psets = ifcopenshell.util.element.get_psets(element) + parameters = psets.get("BBIM_Array", None) + if parameters: + try: + parent = tool.Ifc.get().by_guid(parameters["Parent"]) + parameters["has_parent"] = True + parameters["parent_name"] = parent.Name or "Unnamed" + parameters["data"] = json.loads(parameters.get("Data", "[]") or "[]") + except: + parameters["has_parent"] = False + return parameters diff --git a/src/blenderbim/blenderbim/bim/module/model/product.py b/src/blenderbim/blenderbim/bim/module/model/product.py index 56976f9b7e..adec92d59a 100644 --- a/src/blenderbim/blenderbim/bim/module/model/product.py +++ b/src/blenderbim/blenderbim/bim/module/model/product.py @@ -1,5 +1,5 @@ # BlenderBIM Add-on - OpenBIM Blender Add-on -# Copyright (C) 2020, 2021 Dion Moult +# Copyright (C) 2020, 2021, 2022 Dion Moult # # This file is part of BlenderBIM Add-on. # diff --git a/src/blenderbim/blenderbim/bim/module/model/prop.py b/src/blenderbim/blenderbim/bim/module/model/prop.py index c85c708fb1..7cb5340221 100644 --- a/src/blenderbim/blenderbim/bim/module/model/prop.py +++ b/src/blenderbim/blenderbim/bim/module/model/prop.py @@ -245,3 +245,11 @@ class BIMModelProperties(PropertyGroup): ) type_class: bpy.props.EnumProperty(items=get_type_class, name="IFC Class", update=update_type_class) type_predefined_type: bpy.props.EnumProperty(items=get_type_predefined_type, name="Predefined Type", default=None) + + +class BIMArrayProperties(PropertyGroup): + is_editing: bpy.props.IntProperty(default=-1) + count: bpy.props.IntProperty(name="Count", default=0) + x: bpy.props.FloatProperty(name="X", default=0) + y: bpy.props.FloatProperty(name="Y", default=0) + z: bpy.props.FloatProperty(name="Z", default=0) diff --git a/src/blenderbim/blenderbim/bim/module/model/ui.py b/src/blenderbim/blenderbim/bim/module/model/ui.py index 855fb6c30b..b172d667f7 100644 --- a/src/blenderbim/blenderbim/bim/module/model/ui.py +++ b/src/blenderbim/blenderbim/bim/module/model/ui.py @@ -1,5 +1,5 @@ # BlenderBIM Add-on - OpenBIM Blender Add-on -# Copyright (C) 2020, 2021 Dion Moult +# Copyright (C) 2020, 2021, 2022 Dion Moult # # This file is part of BlenderBIM Add-on. # @@ -17,8 +17,9 @@ # along with BlenderBIM Add-on. If not, see . import bpy +import blenderbim.tool as tool from bpy.types import Panel, Operator, Menu -from blenderbim.bim.module.model.data import AuthoringData +from blenderbim.bim.module.model.data import AuthoringData, ArrayData from blenderbim.bim.module.model.prop import store_cursor_position from blenderbim.bim.helper import prop_with_search @@ -254,6 +255,57 @@ class HelpConstrTypes(Operator): ] +class BIM_PT_array(bpy.types.Panel): + bl_label = "IFC Array" + bl_idname = "BIM_PT_array" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "modifier" + + @classmethod + def poll(cls, context): + return tool.Ifc.get() and tool.Ifc.get_entity(context.active_object) + + def draw(self, context): + if not ArrayData.is_loaded: + ArrayData.load() + + props = context.active_object.BIMArrayProperties + + if ArrayData.data["parameters"]: + row = self.layout.row(align=True) + row.label(text=ArrayData.data["parameters"]["parent_name"], icon="CON_CHILDOF") + op = row.operator("bim.select_array_parent", icon="OBJECT_DATA", text="") + op.parent = ArrayData.data["parameters"]["Parent"] + if ArrayData.data["parameters"]["data"]: + row.operator("bim.add_array", icon="ADD", text="") + + for i, array in enumerate(ArrayData.data["parameters"]["data"]): + box = self.layout.box() + if props.is_editing == i: + row = box.row(align=True) + row.prop(props, "count", icon="MOD_ARRAY") + row.operator("bim.edit_array", icon="CHECKMARK", text="").item = i + row.operator("bim.disable_editing_array", icon="CANCEL", text="") + row = box.row(align=True) + row.prop(props, "x") + row.prop(props, "y") + row.prop(props, "z") + else: + row = box.row(align=True) + row.label(text=f"{array['count']} Items", icon="MOD_ARRAY") + row.operator("bim.enable_editing_array", icon="GREASEPENCIL", text="").item = i + row.operator("bim.remove_array", icon="X", text="").item = i + row = box.row(align=True) + row.label(text=f"X: {array['x']}") + row.label(text=f"Y: {array['y']}") + row.label(text=f"Z: {array['z']}") + else: + row = self.layout.row() + row.label(text="No Array Found") + row.operator("bim.add_array", icon="ADD", text="") + + class BIM_MT_model(Menu): bl_idname = "BIM_MT_model" bl_label = "IFC Objects" diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index ba41199edd..cd2d5acd0d 100644 --- a/src/blenderbim/blenderbim/tool/model.py +++ b/src/blenderbim/blenderbim/tool/model.py @@ -139,3 +139,61 @@ class Model(blenderbim.core.tool.Model): (obj.matrix_world @ Vector((max_x, 0.0, 0.0))).to_2d(), ] return axes + + @classmethod + def regenerate_array(cls, parent, data): + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + obj_stack = [parent] + for array in data: + child_i = 0 + existing_children = set(array["children"]) + total_existing_children = len(array["children"]) + children_elements = [] + children_objs = [] + for i in range(0, array["count"]): + if i == 0: + continue + offset = Vector((i * array["x"] * unit_scale, i * array["y"] * unit_scale, i * array["z"] * unit_scale)) + for obj in obj_stack: + if child_i >= total_existing_children: + child_obj = tool.Spatial.duplicate_object_and_data(obj) + child_element = tool.Spatial.run_root_copy_class(obj=child_obj) + else: + global_id = array["children"][child_i] + try: + child_element = tool.Ifc.get().by_guid(global_id) + child_obj = tool.Ifc.get_object(child_element) + assert child_obj + except: + child_obj = tool.Spatial.duplicate_object_and_data(obj) + child_element = tool.Spatial.run_root_copy_class(obj=child_obj) + + child_psets = ifcopenshell.util.element.get_psets(child_element) + child_pset = child_psets.get("BBIM_Array") + if child_pset: + ifcopenshell.api.run( + "pset.edit_pset", + tool.Ifc.get(), + product=child_element, + pset=tool.Ifc.get().by_id(child_pset["id"]), + properties={"Data": None}, + ) + + new_matrix = obj.matrix_world.copy() + new_matrix.col[3] = (obj.matrix_world.col[3].to_3d() + offset).to_4d() + child_obj.matrix_world = new_matrix + children_objs.append(child_obj) + children_elements.append(child_element) + child_i += 1 + obj_stack.extend(children_objs) + array["children"] = [e.GlobalId for e in children_elements] + + removed_children = set(existing_children) - set(array["children"]) + for removed_child in removed_children: + element = tool.Ifc.get().by_guid(removed_child) + obj = tool.Ifc.get_object(element) + if obj: + bpy.data.objects.remove(obj) + # TODO: Not sufficient, refactor OverrideDeleteTrait + + bpy.context.view_layer.update()