Skip to content

Instantly share code, notes, and snippets.

@strike-digital
Created December 13, 2022 00:34
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save strike-digital/42b04de40b3eff0da6b3788550132be9 to your computer and use it in GitHub Desktop.
Save strike-digital/42b04de40b3eff0da6b3788550132be9 to your computer and use it in GitHub Desktop.
# Copyright (C) 2021 'BD3D DIGITAL DESIGN, SLU'
#
# This program 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.
#
# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
bl_info = {
"name": "'Group Wrapper' for Geometry-Node",
"author": "Andrew Stevenson, BD3D",
"description": "wrap a node group",
"blender": (3, 3, 0),
"version": (1, 1, 0),
"location": "Node Editor > Geometry Node > Add Menu > Extra",
"warning": "",
"tracker_url": "https://devtalk.blender.org/t/extra-nodes-for-geometrynodes/20942",
"category": "Node",
}
"""
# About creating GeometryNodeCustomGroup:
>Here are the possibilities:
- you can either create a custom interface that interact with a nodegroup
- or create simple input node, this plugin is only creating input values. all boiler plate below is dedicated to output sockets.
> if you want to process data, forget about it:
- currently there's no way to get the value out of a socket, not sure how they could translate field to python.
- writing simple output value is possible, forget about fields tho.
> update management is not ideal
- socket_value_update() function should send us signal when a socket value is being updated, the api is dead for now
- to update, rely on handlers or msgbus ( https://docs.blender.org/api/blender2.8/bpy.app.handlers.html?highlight=handler#module-bpy.app.handlers )
> socket.type is read only, everything is hardcoded in operators
- to change socket type, we forced to use operator `bpy.ops.node.tree_socket_change_type(in_out='IN', socket_type='')` + context 'override'. this is far from ideal.
this means that changing socket type outside the node editor context is not possible.
> in order to change the default value of an output, nodegroup.outputs[n].default value won't work use, api is confusing, it is done via the nodegroup.nodes instead:
- nodegroup.nodes["Group Output"].inputs[n].default_value ->see boiler plate functions i wrote below
> Warning `node_groups[x].outputs.new("NodeSocketBool","test")` is tricky, type need to be exact, no warning upon error, will just return none
About this script:
>You will note that there is an extra attention to detail in order to not register handlers twice
>You will note that there is an extra attention in the extension of the Add menu with this new 'Extra' category.
In, my opinion all plugin nodes should be in this "Extra" menu.
Feel free to reuse the menu registration snippets so all custom node makers can share the 'Extra' menu without confilcts.
"""
import bpy
##################################################
# BOILER PLATE
##################################################
def get_socket_value(ng, idx):
return ng.nodes["Group Output"].inputs[idx].default_value
def set_socket_value(ng, idx, value=None):
"""Useful if you just want to output a single value from an output socket"""
ng.nodes["Group Output"].inputs[idx].default_value = value
return ng.nodes["Group Output"].inputs[idx].default_value
def set_socket_label(ng, idx, label=None):
ng.outputs[idx].name = str(label)
return None
def get_socket_type(ng, idx):
return ng.outputs[idx].type
def create_socket(ng, socket_type="NodeSocketFloat", socket_name="Value"):
socket = ng.outputs.new(socket_type, socket_name)
return socket
def remove_socket(ng, idx):
todel = ng.outputs[idx]
ng.outputs.remove(todel)
return None
##################################################
# CUSTOM NODE
##################################################
"""
This is the main custom node group class. If you want it to wrap one of your own node groups from another file,
I suggest having a look a appending (there is a very good scripting for artists video on it) with python, and then
you can just set the node tree in the init function.
"""
class EXTRANODEGROUPWRAPPER_NG_group_wrapper(bpy.types.GeometryNodeCustomGroup):
bl_idname = "GeometryNodeGroupWrapper"
bl_label = "Group Wrapper"
@classmethod
def poll(cls, context): #mandatory with geonode
"""If this returns False, when trying to add the node from an operator you will get an error."""
return True
def init(self, context):
"""this is run when appending the node for the first time"""
self.width = 250
# here you could set the node group you want to wrap on startup
# self.node_tree = ...
def copy(self, node):
"""Run when dupplicating the node"""
self.node_tree = node.node_tree.copy()
def update(self):
"""generic update function. Called when the node tree is updated"""
pass
def draw_label(self,):
"""The return result of this is used shown as the label of the node"""
if not self.node_tree:
return "Pick a group!"
return self.node_tree.name if self.node_tree.name else ""
def draw_buttons(self, context, layout):
"""This is where you can draw custom properties for your node"""
# remove this if you don't want people to be able to change which node is selected
layout.template_ID(self, "node_tree")
##################################################
# EXTEND MENU
##################################################
"""
Everything in this section is to do with adding a custom menu to the add menu,
But it's a lot more complicated than you would probably need, since it has to take into account
Multiple scripts trying to add the same menu. You could just remove this and add each node to your own
custom menu, which would be much easier.
"""
def extra_geonode_menu(self, context):
"""extend NODE_MT_add with new extra menu"""
self.layout.menu("NODE_MT_category_GEO_EXTRA", text="Extra")
return None
class NODE_MT_category_GEO_EXTRA(bpy.types.Menu):
bl_idname = "NODE_MT_category_GEO_EXTRA"
bl_label = ""
@classmethod
def poll(cls, context):
return (bpy.context.space_data.tree_type == "GeometryNodeTree")
def draw(self, context):
return None
#extra menu extension
def extra_node_group_wrapper(self, context):
"""extend extra menu with new node"""
op = self.layout.operator("node.add_node", text="Group Wrapper")
op.type = "GeometryNodeGroupWrapper"
op.use_transform = True
#register
def register_menus(status):
"""register extra menu, if not already, append item, if not already"""
if (status == "register"):
#register new extra menu class if not exists already, perhaps another plugin already implemented it
if "NODE_MT_category_GEO_EXTRA" not in bpy.types.__dir__():
bpy.utils.register_class(NODE_MT_category_GEO_EXTRA)
#extend add menu with extra menu if not already, _dyn_ui_initialize() will get appended drawing functions of a menu
add_menu = bpy.types.NODE_MT_add
if "extra_geonode_menu" not in [f.__name__ for f in add_menu._dyn_ui_initialize()]:
add_menu.append(extra_geonode_menu)
#extend extra menu with our custom nodes if not already
extra_menu = bpy.types.NODE_MT_category_GEO_EXTRA
if "extra_node_group_wrapper" not in [f.__name__ for f in extra_menu._dyn_ui_initialize()]:
extra_menu.append(extra_node_group_wrapper)
return None
elif (status == "unregister"):
add_menu = bpy.types.NODE_MT_add
extra_menu = bpy.types.NODE_MT_category_GEO_EXTRA
#remove our custom function to extra menu
for f in extra_menu._dyn_ui_initialize().copy():
if (f.__name__ == "extra_node_group_wrapper"):
extra_menu.remove(f)
#if extra menu is empty
if len(extra_menu._dyn_ui_initialize()) == 1:
#remove our extra menu item draw fct add menu
for f in add_menu._dyn_ui_initialize().copy():
if (f.__name__ == "extra_geonode_menu"):
add_menu.remove(f)
#unregister extra menu
bpy.utils.unregister_class(extra_menu)
return None
##################################################
# PROPERTIES & PREFS
##################################################
# The preferences of this addon
class EXTRANODEGROUPWRAPPER_AddonPref(bpy.types.AddonPreferences):
"""addon_prefs = bpy.context.preferences.addons["extra_node_group_wrapper"].preferences"""
bl_idname = "extra_node_group_wrapper"
#drawing part in ui module
# def draw(self, context):
# layout = self.layout
# box = layout.box()
# return None
##################################################
# INIT REGISTRATION
##################################################
classes = [
EXTRANODEGROUPWRAPPER_AddonPref,
EXTRANODEGROUPWRAPPER_NG_group_wrapper,
]
def register():
#classes
for cls in classes:
bpy.utils.register_class(cls)
#extend add menu
register_menus("register")
return None
def unregister():
#extend add menu
register_menus("unregister")
#classes
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
return None
if __name__ == "__main__":
try:
unregister()
except:
pass
register()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment