diff --git a/src/bonsai/bonsai/bim/module/model/__init__.py b/src/bonsai/bonsai/bim/module/model/__init__.py index e3299578e2..9dbb766399 100644 --- a/src/bonsai/bonsai/bim/module/model/__init__.py +++ b/src/bonsai/bonsai/bim/module/model/__init__.py @@ -40,6 +40,7 @@ from . import ( railing, roof, mep, + external, ) from typing import NamedTuple @@ -138,6 +139,7 @@ classes = ( prop.BIMRoofProperties, prop.BIMPolylineProperties, prop.BIMProductPreviewProperties, + prop.BIMExternalParametricGeometryProperties, ui.BIM_PT_array, ui.BIM_PT_stair, ui.BIM_PT_sverchok, @@ -147,6 +149,7 @@ classes = ( ui.BIM_PT_roof, ui.BIM_MT_type_manager_menu, ui.BIM_MT_type_menu, + ui.BIM_PT_external_parametric_geometry, ui.LaunchTypeMenu, ui.LaunchTypeManager, grid.BIM_OT_add_object, @@ -198,6 +201,7 @@ classes = ( mep.MEPAddObstruction, mep.MEPAddTransition, mep.MEPAddBend, + external.ApplyExternalParametricGeometry, ) addon_keymaps = [] @@ -255,6 +259,9 @@ def register(): bpy.types.Object.BIMDoorProperties = bpy.props.PointerProperty(type=prop.BIMDoorProperties) bpy.types.Object.BIMRailingProperties = bpy.props.PointerProperty(type=prop.BIMRailingProperties) bpy.types.Object.BIMRoofProperties = bpy.props.PointerProperty(type=prop.BIMRoofProperties) + bpy.types.Object.BIMExternalParametricGeometryProperties = bpy.props.PointerProperty( + type=prop.BIMExternalParametricGeometryProperties + ) bpy.types.VIEW3D_MT_add.prepend(ui.add_menu) bpy.app.handlers.load_post.append(handler.load_post) @@ -277,6 +284,7 @@ def unregister(): del bpy.types.Object.BIMDoorProperties del bpy.types.Object.BIMRailingProperties del bpy.types.Object.BIMRoofProperties + del bpy.types.Object.BIMExternalParametricGeometryProperties bpy.app.handlers.load_post.remove(handler.load_post) bpy.types.VIEW3D_MT_add.remove(ui.add_menu) diff --git a/src/bonsai/bonsai/bim/module/model/external.py b/src/bonsai/bonsai/bim/module/model/external.py new file mode 100644 index 0000000000..cde84cd536 --- /dev/null +++ b/src/bonsai/bonsai/bim/module/model/external.py @@ -0,0 +1,40 @@ +# Bonsai - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021, 2022 Dion Moult , @Andrej730 +# +# This file is part of Bonsai. +# +# Bonsai 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. +# +# Bonsai 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 Bonsai. If not, see . + +import bpy +import bmesh + +import ifcopenshell +import bonsai.tool as tool + + +class ApplyExternalParametricGeometry(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.apply_external_parametric_geometry" + bl_label = "Apply External Parametric Geometry" + bl_description = "Apply external parametric geometry to the active object." + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + obj = context.active_object + assert obj + assert (active_representation := tool.Geometry.get_active_representation(obj)) + ifc_context = active_representation.ContextOfItems + tool.Model.add_representation(obj, ifc_context) + props = tool.Model.get_epg_props(obj) + props.is_editing = False + return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index e5e56115fd..66c962b2ae 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -1142,3 +1142,43 @@ class BIMProductPreviewProperties(PropertyGroup): verts: bpy.props.CollectionProperty(type=ProductPreviewItem) edges: bpy.props.CollectionProperty(type=ProductPreviewItem) tris: bpy.props.CollectionProperty(type=ProductPreviewItem) + + +def update_is_editing(self: "BIMExternalParametricGeometryProperties", context: bpy.types.Context) -> None: + if self.is_editing: + return + + tool.Model.clean_up_parametric_geometry(self.id_data) + del self["is_editing"] + del self["geo_nodes"] + + +def update_geo_nodes(self: "BIMExternalParametricGeometryProperties", context: bpy.types.Context) -> None: + if self.geo_nodes: + tool.Model.setup_parametric_geometry(self.id_data) + return + + modifier = tool.Model.get_epg_modifier(self.id_data) + assert modifier + modifier.show_viewport = False + del self["geo_nodes"] + + +class BIMExternalParametricGeometryProperties(bpy.types.PropertyGroup): + is_editing: bpy.props.BoolProperty( + name="Is Editing Paramteric Geometry", + description="Toggle editing parametric geometry.", + default=False, + update=update_is_editing, + ) + geo_nodes: bpy.props.PointerProperty( + name="Geometry Nodes", + description="Geometry nodes tree to use as a source for representation.", + type=bpy.types.GeometryNodeTree, + update=update_geo_nodes, + poll=lambda self, node_tree: not node_tree.name.startswith("BBIM_EPG"), + ) + + if TYPE_CHECKING: + is_editing: bool + geo_nodes: Union[bpy.types.GeometryNodeTree, None] diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index dec2f1f91a..f8eb50a38e 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -701,6 +701,49 @@ class BIM_PT_roof(bpy.types.Panel): row.operator("bim.add_roof", icon="ADD", text="") +class BIM_PT_external_parametric_geometry(bpy.types.Panel): + bl_label = "External Parametric Geometry" + bl_idname = "BIM_PT_external_parametric_geometry" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_options = {"DEFAULT_CLOSED"} + bl_parent_id = "BIM_PT_tab_parametric_geometry" + + @classmethod + def poll(cls, context): + return (obj := context.active_object) and obj.type == "MESH" + + def draw(self, context): + obj = context.active_object + assert obj + layout = self.layout + assert layout + props = tool.Model.get_epg_props(obj) + + row = layout.row(align=True) + row.alignment = "RIGHT" + + if not props.is_editing: + row.prop(props, "is_editing", icon="GREASEPENCIL", text="") + return + + row.operator("bim.apply_external_parametric_geometry", icon="CHECKMARK", text="") + row.prop(props, "is_editing", icon="CANCEL", text="") + row = layout.row(align=True) + row.prop_search(props, "geo_nodes", bpy.data, "node_groups") + if props.geo_nodes: + assert (modifier := tool.Model.get_epg_modifier(obj)) + inputs = tool.Model.get_parametric_geometry_inputs(modifier) + # NOTE: users won't be able to see inputs descriptions. + # If we add group node inputs as modifiers inputs, descriptions will be visible. + # But then we need to ensure inputs are up to date + # (e.g. probably just by adding a refresh button). + for input in inputs: + row = layout.row(align=True) + row.prop(input, "default_value", text=input.name) + + def add_menu(self: bpy.types.Menu, context: bpy.types.Context) -> None: self.layout.operator_context = "INVOKE_REGION_WIN" self.layout.operator("bim.add_element", icon_value=bonsai.bim.icons["IFC"].icon_id, text="IFC Element") diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 22a6487842..d5a8ec75f9 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -62,6 +62,7 @@ if TYPE_CHECKING: BIMWindowProperties, BIMStairProperties, BIMRailingProperties, + BIMExternalParametricGeometryProperties, ) @@ -94,6 +95,10 @@ class Model(bonsai.core.tool.Model): def get_array_props(cls, obj: bpy.types.Object) -> BIMArrayProperties: return obj.BIMArrayProperties + @classmethod + def get_epg_props(cls, obj: bpy.types.Object) -> BIMExternalParametricGeometryProperties: + return obj.BIMExternalParametricGeometryProperties + @classmethod def convert_si_to_unit(cls, value: T) -> T: if isinstance(value, (tuple, list)): @@ -1596,15 +1601,13 @@ class Model(bonsai.core.tool.Model): return occurrences @classmethod - def add_body_representation(cls, obj: bpy.types.Object) -> None: + def add_representation(cls, obj: bpy.types.Object, context: ifcopenshell.entity_instance) -> None: ifc_file = tool.Ifc.get() - body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") - assert body mesh = obj.data assert isinstance(mesh, bpy.types.Mesh) representation = ifcopenshell.api.geometry.add_representation( ifc_file, - context=body, + context=context, blender_object=obj, geometry=mesh, coordinate_offset=tool.Geometry.get_cartesian_point_offset(obj), @@ -1616,7 +1619,14 @@ class Model(bonsai.core.tool.Model): profile_set_usage=None, ) assert representation - tool.Model.replace_object_ifc_representation(body, obj, representation) + tool.Model.replace_object_ifc_representation(context, obj, representation) + + @classmethod + def add_body_representation(cls, obj: bpy.types.Object) -> None: + ifc_file = tool.Ifc.get() + body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") + assert body + cls.add_representation(obj, body) @classmethod def auto_detect_annotation_fill_area(cls, obj: bpy.types.Object, mesh: bpy.types.Mesh) -> dict | None: @@ -2206,3 +2216,112 @@ class Model(bonsai.core.tool.Model): ifcopenshell.api.geometry.connect_element( tool.Ifc.get(), relating_element=slab, related_element=wall, description="TOP" ) + + @classmethod + def get_epg_modifier(cls, obj: bpy.types.Object) -> Union[bpy.types.NodesModifier, None]: + for m in obj.modifiers: + if m.type == "NODES" and m.name.startswith("BBIM_EPG"): + assert isinstance(m, bpy.types.NodesModifier) + return m + return None + + @classmethod + def setup_external_nodes( + cls, modifier: bpy.types.NodesModifier, external_nodes: bpy.types.GeometryNodeTree + ) -> None: + bbim_nodes = modifier.node_group + + if bbim_nodes is not None: + # Just assign modifier to existing node group. + assert isinstance(bbim_nodes, bpy.types.GeometryNodeTree) + group_node = next(n for n in bbim_nodes.nodes if n.type == "GROUP") + assert isinstance(group_node, bpy.types.GeometryNodeGroup) + group_node.node_tree = external_nodes + return + + # Create a new node group. + bbim_nodes = bpy.data.node_groups.new(type="GeometryNodeTree", name="BBIM_EPG") + modifier.node_group = bbim_nodes + + assert isinstance(bbim_nodes, bpy.types.GeometryNodeTree) + bbim_nodes_interface = bbim_nodes.interface + assert bbim_nodes_interface + + geometry_socket_2 = bbim_nodes_interface.new_socket( + name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry" + ) + geometry_socket_2.attribute_domain = "POINT" + + # Socket Geometry + geometry_socket_3 = bbim_nodes_interface.new_socket( + name="Geometry", in_out="INPUT", socket_type="NodeSocketGeometry" + ) + geometry_socket_3.attribute_domain = "POINT" + + # Socket Socket + socket_socket = bbim_nodes_interface.new_socket(name="Socket", in_out="INPUT", socket_type="NodeSocketGeometry") + socket_socket.attribute_domain = "POINT" + + # Initialize bbim_epg nodes. + # Node Group Input. + group_input_1 = bbim_nodes.nodes.new("NodeGroupInput") + assert isinstance(group_input_1, bpy.types.NodeGroupInput) + group_input_1.name = "Group Input" + # Node Group Output. + group_output_1 = bbim_nodes.nodes.new("NodeGroupOutput") + assert isinstance(group_output_1, bpy.types.NodeGroupOutput) + group_output_1.name = "Group Output" + group_output_1.is_active_output = True + + # Node Group. + group = bbim_nodes.nodes.new("GeometryNodeGroup") + assert isinstance(group, bpy.types.GeometryNodeGroup) + group.name = "Group" + group.node_tree = external_nodes + + # Set locations + group_input_1.location = (-345.0525817871094, 65.80108642578125) + group_output_1.location = (200.0, 0.0) + group.location = (-83.36784362792969, 80.47976684570312) + + # Set dimensions + group_input_1.width, group_input_1.height = 140.0, 100.0 + group_output_1.width, group_output_1.height = 140.0, 100.0 + group.width, group.height = 179.41021728515625, 100.0 + + # Initialize bbim_epg links. + # group.Geometry -> group_output_1.Geometry + bbim_nodes.links.new(group.outputs[0], group_output_1.inputs[0]) + # group_input_1.Geometry -> group.Geometry + bbim_nodes.links.new(group_input_1.outputs[0], group.inputs[0]) + + @classmethod + def setup_parametric_geometry(cls, obj: bpy.types.Object) -> None: + props = cls.get_epg_props(obj) + external_nodes = props.geo_nodes + assert external_nodes + + if not (modifier := cls.get_epg_modifier(obj)): + modifier = obj.modifiers.new(type="NODES", name="BBIM_EPG") + assert isinstance(modifier, bpy.types.NodesModifier) + modifier.show_viewport = True + cls.setup_external_nodes(modifier, external_nodes) + + @classmethod + def clean_up_parametric_geometry(cls, obj: bpy.types.Object) -> None: + modifier = tool.Model.get_epg_modifier(obj) + assert modifier + node_tree = modifier.node_group + assert node_tree + bpy.data.node_groups.remove(node_tree) + obj.modifiers.clear() + + @classmethod + def get_parametric_geometry_inputs(cls, modifier: bpy.types.NodesModifier) -> list[bpy.types.NodeSocket]: + node_group = modifier.node_group + assert isinstance(node_group, bpy.types.GeometryNodeTree) + + group_node = next(n for n in node_group.nodes if n.type == "GROUP") + assert isinstance(group_node, bpy.types.GeometryNodeGroup) + + return [s for s in group_node.inputs if s.type != "GEOMETRY"]