mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 19:07:57 +00:00
Experimental geo nodes integration
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user