Experimental geo nodes integration

This commit is contained in:
Andrej730
2025-03-28 17:59:00 +05:00
parent 24ef03c96e
commit 273d6c1039
5 changed files with 255 additions and 5 deletions
@@ -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)
@@ -0,0 +1,40 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021, 2022 Dion Moult <dion@thinkmoult.com>, @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 <http://www.gnu.org/licenses/>.
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"}
@@ -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]
+43
View File
@@ -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")
+124 -5
View File
@@ -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"]