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()