diff --git a/src/bonsai/bonsai/bim/module/model/external.py b/src/bonsai/bonsai/bim/module/model/external.py index cde84cd536..5ed99f29e8 100644 --- a/src/bonsai/bonsai/bim/module/model/external.py +++ b/src/bonsai/bonsai/bim/module/model/external.py @@ -17,9 +17,7 @@ # along with Bonsai. If not, see . import bpy -import bmesh -import ifcopenshell import bonsai.tool as tool @@ -32,9 +30,23 @@ class ApplyExternalParametricGeometry(bpy.types.Operator, tool.Ifc.Operator): 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) + + # TODO: consider nodes being empty. + if props.geometry_source == "GEONODES": + assert (active_representation := tool.Geometry.get_active_representation(obj)) + ifc_context = active_representation.ContextOfItems + + tool.Model.add_representation(obj, ifc_context) + elif props.geometry_source == "IFCSVERCHOK": + import ifcsverchok.helper as helper + + nodes = props.sverchok_nodes + assert nodes is not None + tool.Model.run_ifcsverchok_graph_on_bonsai_file(nodes) + output_node = tool.Model.get_ifcsverchok_shape_output(nodes) + representation = helper.get_socket_value(output_node.inputs, "Representation") + tool.Model.replace_object_ifc_representation(representation.ContextOfItems, obj, representation) + 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 5066ceb9b2..b26466aa15 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -33,6 +33,9 @@ from typing import TYPE_CHECKING, Literal, get_args, Union, Any, Optional from collections.abc import Callable from mathutils import Vector +if TYPE_CHECKING: + import sverchok.node_tree + def get_ifc_class(self: "BIMModelProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not AuthoringData.is_loaded: @@ -1709,12 +1712,16 @@ def update_is_editing(self: "BIMExternalParametricGeometryProperties", context: if self.is_editing: return + assert isinstance(self.id_data, bpy.types.Object) tool.Model.clean_up_parametric_geometry(self.id_data) - del self["is_editing"] - del self["geo_nodes"] + self.property_unset("is_editing") + self.property_unset("geo_nodes") + self.property_unset("sverchok_nodes") def update_geo_nodes(self: "BIMExternalParametricGeometryProperties", context: bpy.types.Context) -> None: + assert isinstance(self.id_data, bpy.types.Object) + if self.geo_nodes: tool.Model.setup_parametric_geometry(self.id_data) return @@ -1725,14 +1732,25 @@ def update_geo_nodes(self: "BIMExternalParametricGeometryProperties", context: b del self["geo_nodes"] +def poll_sverchok_nodes(self: "BIMExternalParametricGeometryProperties", node_tree: bpy.types.NodeTree) -> bool: + return node_tree.bl_idname == "SverchCustomTreeType" + + class BIMExternalParametricGeometryProperties(bpy.types.PropertyGroup): - is_editing: bpy.props.BoolProperty( + is_editing: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration] name="Is Editing Paramteric Geometry", description="Toggle editing parametric geometry.", default=False, update=update_is_editing, ) - geo_nodes: bpy.props.PointerProperty( + geometry_source: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + name="Geometry Source", + items=[ + ("GEONODES", "Geometry Nodes", ""), + ("IFCSVERCHOK", "IFC Sverchok", ""), + ], + ) + geo_nodes: bpy.props.PointerProperty( # pyright: ignore[reportRedeclaration] name="Geometry Nodes", description="Geometry nodes tree to use as a source for representation.", type=bpy.types.GeometryNodeTree, @@ -1740,6 +1758,15 @@ class BIMExternalParametricGeometryProperties(bpy.types.PropertyGroup): poll=lambda self, node_tree: not node_tree.name.startswith("BBIM_EPG"), ) + sverchok_nodes: bpy.props.PointerProperty( # pyright: ignore[reportRedeclaration] + name="Sverchok Nodes", + description="Sverchok node tree to use as a source for representation.", + type=bpy.types.NodeTree, + poll=poll_sverchok_nodes, + ) + if TYPE_CHECKING: is_editing: bool + geometry_source: Literal["GEONODES", "IFCSVERCHOK"] geo_nodes: Union[bpy.types.GeometryNodeTree, None] + sverchok_nodes: Union[sverchok.node_tree.SverchCustomTree, None] diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index 8ff8ae3b34..86beea6079 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -19,6 +19,7 @@ from __future__ import annotations import bpy import bonsai.bim +from bonsai.bim import module import bonsai.tool as tool from bpy.types import Panel, Menu from bonsai.bim.helper import prop_with_search @@ -668,18 +669,30 @@ class BIM_PT_external_parametric_geometry(bpy.types.Panel): 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) + layout.prop(props, "geometry_source") + if props.geometry_source == "GEONODES": + 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) + elif props.geometry_source == "IFCSVERCHOK": + row = layout.row(align=True) + row.prop(props, "sverchok_nodes") + if props.sverchok_nodes: + # TODO: Updating mesh in `draw` is not ideal, + # should find a way to update only on graph changes. + res = tool.Model.update_mesh_from_sverchok(obj, props.sverchok_nodes) + if res is not None: + print(res) + layout.label(text=f"Error Updating from Graph, See System Console", icon="ERROR") def draw_door_properties(layout: bpy.types.UILayout, props: module_prop.BIMDoorProperties) -> None: diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index d4c85e8d87..2f4c4b77b7 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -54,6 +54,8 @@ T = TypeVar("T") V_ = tool.Blender.V_ if TYPE_CHECKING: + import sverchok.node_tree + import ifcsverchok.nodes.ifc.shape_builder.shape_output from bonsai.bim.module.model.prop import ( BIMModelProperties, BIMDoorProperties, @@ -2540,12 +2542,24 @@ class Model(bonsai.core.tool.Model): @classmethod def clean_up_parametric_geometry(cls, obj: bpy.types.Object) -> None: + # Geo nodes are using modifier. 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() + if modifier is not None: + node_tree = modifier.node_group + assert node_tree + bpy.data.node_groups.remove(node_tree) + obj.modifiers.clear() + + # Sverchok are changing the mesh data to preview changes. + # TODO: probably should use modifiers with nodes and temp mesh instead. + active_representation = tool.Geometry.get_active_representation(obj) + assert active_representation is not None + bonsai.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=obj, + representation=active_representation, + ) @classmethod def get_parametric_geometry_inputs(cls, modifier: bpy.types.NodesModifier) -> list[bpy.types.NodeSocket]: @@ -2557,6 +2571,51 @@ class Model(bonsai.core.tool.Model): return [s for s in group_node.inputs if s.type != "GEOMETRY"] + @classmethod + def get_ifcsverchok_shape_output( + cls, node_tree: sverchok.node_tree.SverchCustomTree + ) -> ifcsverchok.nodes.ifc.shape_builder.shape_output.SvSbShapeOutput: + from ifcsverchok.nodes.ifc.shape_builder.shape_output import SvSbShapeOutput + + return next(n for n in node_tree.nodes if isinstance(n, SvSbShapeOutput)) + + @classmethod + def update_mesh_from_sverchok( + cls, obj: bpy.types.Object, node_tree: sverchok.node_tree.SverchCustomTree + ) -> str | None: + """ + :return: ``None`` if successful, otherwise error message. + """ + import ifcsverchok.helper as helper + + output_node = cls.get_ifcsverchok_shape_output(node_tree) + verts = helper.get_socket_value(output_node.outputs, "Vers", value_type="CONTAINER") + edges = helper.get_socket_value(output_node.outputs, "Edgs", value_type="CONTAINER") + faces = helper.get_socket_value(output_node.outputs, "Pols", value_type="CONTAINER") + mesh = obj.data + assert isinstance(mesh, bpy.types.Mesh) + mesh.clear_geometry() + + # `shade_flat=False`, because `from_pydata` is using method that doesn't support changing shading + # during `Panel.draw` execution. We shade flat later ourselves. + mesh.from_pydata(verts, edges, faces, shade_flat=False) + + def _name_convention_attribute_ensure(attributes, name, domain, data_type): + try: + attribute = attributes[name] + except KeyError: + return attributes.new(name, data_type, domain) + if attribute.domain == domain and attribute.data_type == data_type: + return attribute + attributes.remove(attribute) + return attributes.new(name, data_type, domain) + + sharp_faces = _name_convention_attribute_ensure(mesh.attributes, "sharp_face", "FACE", "BOOLEAN") + assert isinstance(sharp_faces, bpy.types.BoolAttribute) + data = sharp_faces.data + ones = np.ones(len(data), dtype=bool) + data.foreach_set("value", ones) + @classmethod def align_objects( cls,