2025-07-01
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# blender_visual_scripting_addon
|
||||
Visual Scripting addon for blender with nodes
|
||||
@@ -0,0 +1,155 @@
|
||||
# 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
|
||||
# MERCHANTIBILITY 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": "Serpens",
|
||||
"author": "Joshua Knauber, Finn Knauber",
|
||||
"description": "Adds a node editor for building addons with nodes",
|
||||
"blender": (4, 2, 0),
|
||||
"version": (3, 4, 0),
|
||||
"location": "Editors -> Visual Scripting Editor",
|
||||
"doc_url": "https://joshuaknauber.notion.site/Serpens-Documentation-d44c98df6af64d7c9a7925020af11233",
|
||||
"tracker_url": "https://discord.com/invite/NK6kyae",
|
||||
"category": "Node",
|
||||
}
|
||||
|
||||
import bpy
|
||||
from bpy.utils import previews
|
||||
import atexit
|
||||
|
||||
import os
|
||||
|
||||
from .keymaps.keymap import register_keymaps, unregister_keymaps
|
||||
from .node_tree.node_categories import (
|
||||
draw_node_menu,
|
||||
register_node_menus,
|
||||
unregister_node_menus,
|
||||
)
|
||||
from .interface.header.header import (
|
||||
footer_status,
|
||||
header_prepend,
|
||||
header_append,
|
||||
node_info_append,
|
||||
)
|
||||
from .interface.panels.warnings import append_warning
|
||||
from .interface.menus.rightclick import serpens_right_click
|
||||
from .msgbus import subscribe_to_name_change, unsubscribe_from_name_change
|
||||
|
||||
from .settings.addon_properties import SN_AddonProperties
|
||||
|
||||
from . import handlers
|
||||
|
||||
from . import auto_load
|
||||
|
||||
auto_load.init()
|
||||
|
||||
|
||||
def register_icons():
|
||||
bpy.types.Scene.sn_icons = bpy.utils.previews.new()
|
||||
icons_dir = os.path.join(os.path.dirname(__file__), "assets", "icons")
|
||||
|
||||
icons = ["discord", "serpens"]
|
||||
|
||||
for icon in icons:
|
||||
bpy.types.Scene.sn_icons.load(
|
||||
icon, os.path.join(icons_dir, icon + ".png"), "IMAGE"
|
||||
)
|
||||
|
||||
|
||||
def unregister_icons():
|
||||
bpy.utils.previews.remove(bpy.types.Scene.sn_icons)
|
||||
|
||||
|
||||
def register():
|
||||
# register the classes of the addon
|
||||
auto_load.register()
|
||||
|
||||
# addon properties
|
||||
bpy.types.Scene.sn = bpy.props.PointerProperty(
|
||||
type=SN_AddonProperties, name="Serpens Properties"
|
||||
)
|
||||
|
||||
# register the keymaps
|
||||
register_keymaps()
|
||||
|
||||
# register the icons
|
||||
register_icons()
|
||||
|
||||
# register node categories
|
||||
register_node_menus()
|
||||
bpy.types.NODE_MT_add.append(draw_node_menu)
|
||||
|
||||
# add the node tree header
|
||||
bpy.types.NODE_HT_header.append(header_append)
|
||||
bpy.types.NODE_MT_editor_menus.append(header_prepend)
|
||||
bpy.types.NODE_PT_active_node_generic.append(node_info_append)
|
||||
bpy.types.STATUSBAR_HT_header.append(footer_status)
|
||||
|
||||
# add no edit warnings
|
||||
bpy.types.NODE_PT_node_tree_interface.append(append_warning)
|
||||
|
||||
# add name change update
|
||||
subscribe_to_name_change()
|
||||
|
||||
# app handlers
|
||||
bpy.app.handlers.depsgraph_update_post.append(handlers.depsgraph_handler)
|
||||
bpy.app.handlers.load_post.append(handlers.load_handler)
|
||||
bpy.app.handlers.load_pre.append(handlers.unload_handler)
|
||||
bpy.app.handlers.undo_post.append(handlers.undo_post)
|
||||
bpy.app.handlers.save_pre.append(handlers.save_pre)
|
||||
atexit.register(handlers.unload_handler)
|
||||
|
||||
# add right click menu
|
||||
bpy.types.WM_MT_button_context.append(serpens_right_click)
|
||||
|
||||
|
||||
def unregister():
|
||||
# remove the node tree header
|
||||
bpy.types.NODE_MT_editor_menus.remove(header_prepend)
|
||||
bpy.types.NODE_HT_header.remove(header_append)
|
||||
bpy.types.NODE_PT_active_node_generic.remove(node_info_append)
|
||||
bpy.types.STATUSBAR_HT_header.remove(footer_status)
|
||||
|
||||
# remove no edit warnings
|
||||
bpy.types.NODE_PT_node_tree_interface.remove(append_warning)
|
||||
|
||||
# addon properties
|
||||
del bpy.types.Scene.sn
|
||||
|
||||
# unregister the keymaps
|
||||
unregister_keymaps()
|
||||
|
||||
# unregister the icons
|
||||
unregister_icons()
|
||||
|
||||
# unregister node categories
|
||||
bpy.types.NODE_MT_add.remove(draw_node_menu)
|
||||
unregister_node_menus()
|
||||
|
||||
# remove handlers
|
||||
bpy.app.handlers.depsgraph_update_post.remove(handlers.depsgraph_handler)
|
||||
bpy.app.handlers.load_post.remove(handlers.load_handler)
|
||||
bpy.app.handlers.load_pre.remove(handlers.unload_handler)
|
||||
bpy.app.handlers.undo_post.remove(handlers.undo_post)
|
||||
bpy.app.handlers.save_pre.remove(handlers.save_pre)
|
||||
atexit.unregister(handlers.unload_handler)
|
||||
|
||||
# remove name change msgbus
|
||||
unsubscribe_from_name_change()
|
||||
|
||||
# remove right click menu
|
||||
if hasattr(bpy.types, "WM_MT_button_context"):
|
||||
bpy.types.WM_MT_button_context.remove(serpens_right_click)
|
||||
|
||||
# unregister the addon classes
|
||||
auto_load.unregister()
|
||||
@@ -0,0 +1,186 @@
|
||||
import bpy
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
import os
|
||||
|
||||
|
||||
|
||||
class SN_OT_AddAsset(bpy.types.Operator):
|
||||
bl_idname = "sn.add_asset"
|
||||
bl_label = "Add Asset"
|
||||
bl_description = "Adds a asset to the addon"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
add_type: bpy.props.EnumProperty(default="FILE",
|
||||
items=[("FILE", "File", "Import a single file"),
|
||||
("DIRECTORY", "Directory", "Import a full directory")],
|
||||
name="Type",
|
||||
description="Add this directory or this file as an asset",
|
||||
options={"SKIP_SAVE"})
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.sn.load_asset("INVOKE_DEFAULT", add_type=self.add_type)
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(self, "add_type", expand=True)
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self, width=200)
|
||||
|
||||
|
||||
|
||||
class SN_OT_LoadAsset(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "sn.load_asset"
|
||||
bl_label = "Add Asset"
|
||||
bl_description = "Adds a asset to the addon"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
add_type: bpy.props.EnumProperty(default="FILE",
|
||||
items=[("FILE", "File", "Import a single file"),
|
||||
("DIRECTORY", "Directory", "Import a full directory")],
|
||||
name="Type",
|
||||
description="Add this directory or this file as an asset",
|
||||
options={"SKIP_SAVE"})
|
||||
|
||||
def execute(self, context):
|
||||
sn = context.scene.sn
|
||||
new_asset = sn.assets.add()
|
||||
if self.add_type == "DIRECTORY":
|
||||
new_asset.path = os.path.dirname(self.filepath)
|
||||
else:
|
||||
new_asset.path = self.filepath
|
||||
for index, asset in enumerate(sn.assets):
|
||||
if asset == new_asset:
|
||||
sn.asset_index = index
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
|
||||
class SN_OT_RemoveAsset(bpy.types.Operator):
|
||||
bl_idname = "sn.remove_asset"
|
||||
bl_label = "Remove Asset"
|
||||
bl_description = "Removes this asset from the addon"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.scene.sn.asset_index < len(context.scene.sn.assets)
|
||||
|
||||
def execute(self, context):
|
||||
sn = context.scene.sn
|
||||
asset = sn.assets[sn.asset_index]
|
||||
# remove removed from asset nodes
|
||||
for ntree in bpy.data.node_groups:
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
for node in ntree.nodes:
|
||||
if node.bl_idname == "SN_AssetNode":
|
||||
if node.asset == asset.name:
|
||||
node.asset = ""
|
||||
# remove asset
|
||||
sn.assets.remove(sn.asset_index)
|
||||
sn.asset_index -= 1
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_FindNode(bpy.types.Operator):
|
||||
bl_idname = "sn.find_node"
|
||||
bl_label = "Find Node"
|
||||
bl_description = "Find Node"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
node_tree: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
node: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
|
||||
def execute(self, context):
|
||||
ntree = bpy.data.node_groups[self.node_tree]
|
||||
# set active graph and select
|
||||
context.space_data.node_tree = ntree
|
||||
for index, group in enumerate(bpy.data.node_groups):
|
||||
if group == ntree:
|
||||
context.scene.sn.node_tree_index = index
|
||||
# select node and frame
|
||||
for node in ntree.nodes:
|
||||
node.select = node.name == self.node
|
||||
bpy.ops.node.view_selected("INVOKE_DEFAULT")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_FindAsset(bpy.types.Operator):
|
||||
bl_idname = "sn.find_asset"
|
||||
bl_label = "Find Asset"
|
||||
bl_description = "Finds this asset in the addon"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
# init asset nodes
|
||||
empty_nodes = []
|
||||
asset_nodes = []
|
||||
asset = None
|
||||
if context.scene.sn.asset_index < len(context.scene.sn.assets):
|
||||
asset = context.scene.sn.assets[context.scene.sn.asset_index]
|
||||
|
||||
# find assets nodes
|
||||
for ntree in bpy.data.node_groups:
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
for node in ntree.nodes:
|
||||
if node.bl_idname == "SN_AssetNode":
|
||||
if asset and node.asset == asset.name:
|
||||
asset_nodes.append(node)
|
||||
elif not node.asset:
|
||||
empty_nodes.append(node)
|
||||
|
||||
# draw nodes for selected asset
|
||||
if context.scene.sn.asset_index < len(context.scene.sn.assets):
|
||||
col = layout.column()
|
||||
row = col.row()
|
||||
row.enabled = False
|
||||
row.label(text=f"Asset: {asset.name}")
|
||||
|
||||
for node in asset_nodes:
|
||||
op = col.operator("sn.find_node", text=node.name, icon="RESTRICT_SELECT_OFF")
|
||||
op.node_tree = node.node_tree.name
|
||||
op.node = node.name
|
||||
|
||||
if not asset_nodes:
|
||||
col.label(text="No nodes found for this asset", icon="INFO")
|
||||
|
||||
# draw nodes with empty asset
|
||||
col = layout.column()
|
||||
row = col.row()
|
||||
row.label(text="Empty Asset Nodes")
|
||||
row.enabled = False
|
||||
|
||||
for node in empty_nodes:
|
||||
op = col.operator("sn.find_node", text=node.name, icon="RESTRICT_SELECT_OFF")
|
||||
op.node_tree = node.node_tree.name
|
||||
op.node = node.name
|
||||
|
||||
if not empty_nodes:
|
||||
col.label(text="No empty asset nodes found", icon="INFO")
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_popup(self, width=250)
|
||||
|
||||
|
||||
|
||||
class SN_OT_AddAssetNode(bpy.types.Operator):
|
||||
bl_idname = "sn.add_asset_node"
|
||||
bl_label = "Add Asset Node"
|
||||
bl_description = "Adds an asset node"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.node.add_node("INVOKE_DEFAULT", type="SN_AssetNode", use_transform=True)
|
||||
node = context.space_data.node_tree.nodes.active
|
||||
if context.scene.sn.asset_index < len(context.scene.sn.assets):
|
||||
node.asset = context.scene.sn.assets[context.scene.sn.asset_index].name
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,47 @@
|
||||
import bpy
|
||||
import os
|
||||
|
||||
|
||||
|
||||
class SN_AssetProperties(bpy.types.PropertyGroup):
|
||||
|
||||
def get_to_update(self):
|
||||
to_update = []
|
||||
for ntree in bpy.data.node_groups:
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
for node in ntree.nodes:
|
||||
if node.bl_idname == "SN_AssetNode":
|
||||
if node.asset == self.name:
|
||||
to_update.append(node)
|
||||
return to_update
|
||||
|
||||
def update_file_path(self, context):
|
||||
if not self.path == bpy.path.abspath(self.path):
|
||||
self["path"] = bpy.path.abspath(self.path)
|
||||
else:
|
||||
if self.name == "Asset" and self.path:
|
||||
self.name = os.path.basename(self.path)
|
||||
for node in self.get_to_update():
|
||||
node._evaluate(context)
|
||||
|
||||
def get_asset_name(self):
|
||||
return self.get("name", "Asset")
|
||||
|
||||
def set_asset_name(self, new_name):
|
||||
# update asset nodes that had this asset
|
||||
to_update = self.get_to_update()
|
||||
self["name"] = new_name
|
||||
for node in to_update:
|
||||
node.asset = new_name
|
||||
|
||||
|
||||
name: bpy.props.StringProperty(name="Name",
|
||||
description="Name of the asset",
|
||||
default="Asset",
|
||||
get=get_asset_name,
|
||||
set=set_asset_name)
|
||||
|
||||
path: bpy.props.StringProperty(name="Path",
|
||||
description="Path to the asset file",
|
||||
subtype="FILE_PATH",
|
||||
update=update_file_path)
|
||||
@@ -0,0 +1,31 @@
|
||||
import bpy
|
||||
from .property_utils import get_sorted_props
|
||||
|
||||
|
||||
|
||||
def property_imperative_code():
|
||||
""" Returns the imperative code for all properties """
|
||||
props = get_sorted_props(bpy.context.scene.sn.properties.values())
|
||||
imperative = ""
|
||||
for prop in props:
|
||||
imperative += prop.imperative_code + "\n"
|
||||
return imperative
|
||||
|
||||
|
||||
def property_register_code():
|
||||
""" Returns the register code for all properties """
|
||||
props = get_sorted_props(bpy.context.scene.sn.properties.values())
|
||||
register = ""
|
||||
for prop in props:
|
||||
register += prop.register_code + "\n"
|
||||
return register
|
||||
|
||||
|
||||
def property_unregister_code():
|
||||
""" Returns the unregister code for all properties """
|
||||
props = get_sorted_props(bpy.context.scene.sn.properties.values())
|
||||
props.reverse()
|
||||
unregister = ""
|
||||
for prop in props:
|
||||
unregister += prop.unregister_code + "\n"
|
||||
return unregister
|
||||
@@ -0,0 +1,115 @@
|
||||
import bpy
|
||||
from ...nodes.compiler import compile_addon
|
||||
from .property_basic import BasicProperty
|
||||
from .settings.settings import id_items, id_data, property_icons
|
||||
from .settings.group import SN_PT_GroupProperty
|
||||
|
||||
|
||||
|
||||
class FullBasicProperty(BasicProperty):
|
||||
|
||||
property_type: bpy.props.EnumProperty(name="Type",
|
||||
description="The type of data this property can store",
|
||||
update=BasicProperty.trigger_reference_update,
|
||||
items=[("String", "String", "Stores text, can display a text input or a filepath field", property_icons["String"], 0),
|
||||
("Boolean", "Boolean", "Stores True or False, can be used for a checkbox", property_icons["Boolean"], 1),
|
||||
("Float", "Float", "Stores a decimal number or a vector", property_icons["Float"], 2),
|
||||
("Integer", "Integer", "Stores an integer number or a vector", property_icons["Integer"], 3),
|
||||
("Enum", "Enum", "Stores multiple entries to be used as dropdowns", property_icons["Enum"], 4),
|
||||
("Pointer", "Pointer", "Stores a reference to certain types of blend data, collection or group properties", property_icons["Pointer"], 5),
|
||||
("Collection", "Collection", "Stores a list of certain blend data or property groups to be displayed in lists", property_icons["Collection"], 6),
|
||||
("Group", "Group", "Stores multiple properties to be used in a collection or pointer property", property_icons["Group"], 7)])
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
return {
|
||||
"String": self.stngs_string,
|
||||
"Boolean": self.stngs_boolean,
|
||||
"Float": self.stngs_float,
|
||||
"Integer": self.stngs_integer,
|
||||
"Enum": self.stngs_enum,
|
||||
"Pointer": self.stngs_pointer,
|
||||
"Collection": self.stngs_collection,
|
||||
"Group": self.stngs_group,
|
||||
}[self.property_type]
|
||||
|
||||
|
||||
stngs_group: bpy.props.PointerProperty(type=SN_PT_GroupProperty)
|
||||
|
||||
|
||||
|
||||
class SN_GeneralProperties(FullBasicProperty, bpy.types.PropertyGroup):
|
||||
|
||||
is_scene_prop = True
|
||||
|
||||
def draw(self, context, layout):
|
||||
""" Draws the general property settings """
|
||||
row = layout.row()
|
||||
row.prop(self, "property_type")
|
||||
row.operator("sn.tooltip", text="", emboss=False, icon="QUESTION").text = self.settings.type_description
|
||||
if not self.property_type == "Group":
|
||||
layout.prop(self, "attach_to")
|
||||
layout.prop(self, "description")
|
||||
if self.property_type in {"Float", "Integer", "Boolean"}:
|
||||
layout.prop_enum(self, "prop_options", "ANIMATABLE", text="Animatable")
|
||||
elif self.property_type == "String":
|
||||
layout.prop_enum(self, "prop_options", "TEXTEDIT_UPDATE", text="Textedit Update")
|
||||
|
||||
|
||||
@property
|
||||
def data_path(self):
|
||||
return f"{self.attach_to.upper()}_PLACEHOLDER.{self.python_name}"
|
||||
|
||||
|
||||
@property
|
||||
def register_code(self):
|
||||
self.python_name
|
||||
# register non group properties
|
||||
if not self.property_type == "Group":
|
||||
code = f"bpy.types.{self.attach_to}.{self.python_name} = bpy.props.{self.settings.prop_type_name}(name='{self.name}', description='{self.description}',{self.get_prop_options} {self.settings.register_options})"
|
||||
# register group properties
|
||||
else:
|
||||
code = f"bpy.utils.register_class(SNA_GROUP_{self.python_name})"
|
||||
return code
|
||||
|
||||
@property
|
||||
def unregister_code(self):
|
||||
# unregister non group properties
|
||||
if not self.property_type == "Group":
|
||||
return f"del bpy.types.{self.attach_to}.{self.python_name}"
|
||||
# unregister group properties
|
||||
else:
|
||||
return f"bpy.utils.unregister_class(SNA_GROUP_{self.python_name})"
|
||||
|
||||
@property
|
||||
def imperative_code(self):
|
||||
if hasattr(self.settings, "imperative_code"):
|
||||
return self.settings.imperative_code()
|
||||
return ""
|
||||
|
||||
|
||||
def compile(self, context=None):
|
||||
""" Registers the property and unregisters previous version """
|
||||
# print(f"Serpens Log: Property {self.name} received an update")
|
||||
compile_addon()
|
||||
|
||||
|
||||
def get_attach_to_items(self, context):
|
||||
items = []
|
||||
for item in id_items:
|
||||
items.append((item, item, item))
|
||||
return items
|
||||
|
||||
|
||||
def get_attach_data(self):
|
||||
return id_data[self.attach_to]
|
||||
|
||||
attach_to: bpy.props.EnumProperty(name="Attach To",
|
||||
description="The type of blend data to attach this property to",
|
||||
items=get_attach_to_items,
|
||||
update=FullBasicProperty.trigger_reference_update)
|
||||
|
||||
def copy(self):
|
||||
new_prop = super().copy()
|
||||
new_prop.attach_to = self.attach_to
|
||||
return new_prop
|
||||
@@ -0,0 +1,292 @@
|
||||
import bpy
|
||||
from ...utils import get_python_name, unique_collection_name
|
||||
from .settings.settings import property_icons
|
||||
from .settings.string import SN_PT_StringProperty
|
||||
from .settings.boolean import SN_PT_BooleanProperty
|
||||
from .settings.float import SN_PT_FloatProperty
|
||||
from .settings.integer import SN_PT_IntegerProperty
|
||||
from .settings.enum import SN_PT_EnumProperty
|
||||
from .settings.pointer import SN_PT_PointerProperty
|
||||
from .settings.collection import SN_PT_CollectionProperty
|
||||
|
||||
|
||||
_prop_collection_cache = {} # stores key, value of prop.as_pointer, prop collection
|
||||
_prop_origin_cache = {} # stores key, value of prop.as_pointer, prop collection origin
|
||||
|
||||
class BasicProperty():
|
||||
|
||||
def draw(self, context, layout):
|
||||
""" Draws the general property settings """
|
||||
row = layout.row()
|
||||
row.prop(self, "property_type")
|
||||
row.operator("sn.tooltip", text="", emboss=False, icon="QUESTION").text = self.settings.type_description
|
||||
layout.prop(self, "description")
|
||||
|
||||
col = layout.column(align=True)
|
||||
col.prop_enum(self, "prop_options", "HIDDEN", text="Hidden")
|
||||
col.prop_enum(self, "prop_options", "SKIP_SAVE", text="Skip Save")
|
||||
if self.property_type in {"Float", "Integer", "Boolean"}:
|
||||
col.prop_enum(self, "prop_options", "ANIMATABLE", text="Animatable")
|
||||
if self.property_type == "String":
|
||||
col.prop_enum(self, "prop_options", "TEXTEDIT_UPDATE", text="Textedit Update")
|
||||
|
||||
|
||||
# cache python names so they only have to be generated once
|
||||
cached_python_names = {}
|
||||
cached_python_name: bpy.props.StringProperty()
|
||||
cached_human_name: bpy.props.StringProperty()
|
||||
|
||||
@property
|
||||
def python_name(self):
|
||||
if self.name == self.cached_human_name and self.cached_python_name: return self.cached_python_name
|
||||
if self.name in self.cached_python_names: return self.cached_python_names[self.name]
|
||||
|
||||
names = []
|
||||
for prop in self.prop_collection:
|
||||
if prop == self:
|
||||
break
|
||||
names.append(prop.python_name)
|
||||
|
||||
name = unique_collection_name(f"sna_{get_python_name(self.name, 'new_property')}", "new_property", names, "_")
|
||||
try:
|
||||
self.cached_python_name = name
|
||||
self.cached_human_name = self.name
|
||||
except AttributeError: pass
|
||||
self.cached_python_names[self.name] = name
|
||||
return name
|
||||
|
||||
|
||||
@property
|
||||
def get_prop_options(self):
|
||||
options = ""
|
||||
if self.prop_options:
|
||||
options = " options={" + ", ".join(map(lambda option: f"'{option}'", list(self.prop_options))) + "},"
|
||||
return options
|
||||
|
||||
|
||||
@property
|
||||
def register_code(self):
|
||||
code = f"{self.python_name}: bpy.props.{self.settings.prop_type_name}(name='{self.name}', description='{self.description}',{self.get_prop_options} {self.settings.register_options})"
|
||||
if hasattr(self.settings, "register_code"):
|
||||
return self.settings.register_code(code)
|
||||
return code
|
||||
|
||||
|
||||
@property
|
||||
def prop_collection(self):
|
||||
""" Returns the collection this property lives in """
|
||||
if self.id_data.bl_rna.identifier == "ScriptingNodesTree":
|
||||
# find property in nodes to return
|
||||
if not str(self.as_pointer()) in _prop_collection_cache:
|
||||
for node in self.id_data.nodes:
|
||||
if hasattr(node, "properties"):
|
||||
for prop in node.properties:
|
||||
if prop == self:
|
||||
_prop_collection_cache[str(self.as_pointer())] = node.properties
|
||||
break
|
||||
elif prop.property_type == "Group":
|
||||
for subprop in prop.settings.properties:
|
||||
if subprop == self:
|
||||
_prop_collection_cache[str(self.as_pointer())] = prop.settings.properties
|
||||
break
|
||||
return _prop_collection_cache[str(self.as_pointer())]
|
||||
|
||||
else:
|
||||
path = "[".join(repr(self.path_resolve("name", False)).split("[")[:-1])
|
||||
if path.endswith("scenes"):
|
||||
path = "bpy.context.scene.sn.properties"
|
||||
coll = eval(path)
|
||||
return coll
|
||||
|
||||
|
||||
@property
|
||||
def prop_collection_origin(self):
|
||||
""" Returns the source where the main property collection lives """
|
||||
if self.id_data.bl_rna.identifier == "ScriptingNodesTree":
|
||||
# find property in nodes to return
|
||||
if not str(self.as_pointer()) in _prop_origin_cache:
|
||||
for node in self.id_data.nodes:
|
||||
if hasattr(node, "properties"):
|
||||
for prop in node.properties:
|
||||
if prop == self:
|
||||
_prop_origin_cache[str(self.as_pointer())] = node
|
||||
break
|
||||
elif prop.property_type == "Group":
|
||||
for subprop in prop.settings.properties:
|
||||
if subprop == self:
|
||||
_prop_origin_cache[str(self.as_pointer())] = node
|
||||
break
|
||||
return _prop_origin_cache[str(self.as_pointer())]
|
||||
|
||||
else:
|
||||
parent_path = repr(self.path_resolve("name", False)).split("properties")[0][:-1]
|
||||
parent = eval(parent_path)
|
||||
return parent
|
||||
|
||||
|
||||
@property
|
||||
def full_prop_path(self):
|
||||
""" Returns the full data path for this property """
|
||||
main_prop_path = f"{repr(self.prop_collection_origin)}.properties"
|
||||
if hasattr(self, "group_prop_parent"):
|
||||
main_prop_path += f"['{self.group_prop_parent.name}'].settings.properties"
|
||||
main_prop_path += f"['{self.name}']"
|
||||
return main_prop_path
|
||||
|
||||
|
||||
def _compile(self, context=None):
|
||||
""" Update the property with the parent classes compile function """
|
||||
if hasattr(self, "compile"):
|
||||
self.compile(context)
|
||||
|
||||
|
||||
def get_name(self):
|
||||
return self.get("name", "Prop Default")
|
||||
|
||||
def get_unique_name(self, value):
|
||||
names = list(map(lambda item: item.name, list(filter(lambda item: item!=self, self.prop_collection))))
|
||||
return unique_collection_name(value, "New Property", names, " ")
|
||||
|
||||
def set_name(self, value):
|
||||
value = self.get_unique_name(value)
|
||||
|
||||
# get nodes to update references
|
||||
to_update_nodes = []
|
||||
for ntree in bpy.data.node_groups:
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
for node in ntree.nodes:
|
||||
if getattr(node, "prop_name", None) == self.name or getattr(node, "prop_group", None) == self.name:
|
||||
if self.property_type == "Group":
|
||||
if hasattr(node, "get_prop_source") and node.get_prop_source() == self.settings:
|
||||
to_update_nodes.append((node, "prop_group"))
|
||||
else:
|
||||
if hasattr(node, "get_prop_source") and node.get_prop_source() and node.get_prop_source().properties == self.prop_collection and self.name in node.get_prop_source().properties:
|
||||
to_update_nodes.append((node, "prop_name"))
|
||||
|
||||
# get properties to update references
|
||||
to_update_props = []
|
||||
if self.property_type == "Group":
|
||||
for prop in self.prop_collection:
|
||||
if prop.property_type in ["Pointer", "Collection"] and prop.settings.prop_group == self.name:
|
||||
to_update_props.append(prop)
|
||||
elif prop.property_type == "Group" and prop != self:
|
||||
for subprop in prop.settings.properties:
|
||||
if subprop.property_type in ["Pointer", "Collection"] and subprop.settings.prop_group == self.name:
|
||||
to_update_props.append(subprop)
|
||||
|
||||
# set value
|
||||
self["name"] = value
|
||||
|
||||
# update property references
|
||||
for prop in to_update_props:
|
||||
prop.settings.prop_group = value
|
||||
for node, key in to_update_nodes:
|
||||
setattr(node, key, value)
|
||||
|
||||
name: bpy.props.StringProperty(name="Property Name",
|
||||
description="Name of this property",
|
||||
default="Prop Default",
|
||||
get=get_name,
|
||||
set=set_name,
|
||||
update=_compile)
|
||||
|
||||
|
||||
description: bpy.props.StringProperty(name="Description",
|
||||
description="The description of this property, shown in tooltips",
|
||||
update=_compile)
|
||||
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
return property_icons[self.property_type]
|
||||
|
||||
|
||||
def trigger_reference_update(self, context):
|
||||
# get nodes to update references
|
||||
to_update_nodes = []
|
||||
key = "prop_group" if self.property_type == "Group" else "prop_name"
|
||||
for ntree in bpy.data.node_groups:
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
for node in ntree.nodes:
|
||||
if hasattr(node, key) and getattr(node, key) == self.name:
|
||||
to_update_nodes.append((node, key))
|
||||
|
||||
for node, key in to_update_nodes:
|
||||
# trigger an update on the affected nodes
|
||||
setattr(node, key, self.name)
|
||||
self._compile()
|
||||
|
||||
|
||||
def get_types(self, context):
|
||||
items = [("String", "String", "Stores text, can display a text input or a filepath field", property_icons["String"], 0),
|
||||
("Boolean", "Boolean", "Stores True or False, can be used for a checkbox", property_icons["Boolean"], 1),
|
||||
("Float", "Float", "Stores a decimal number or a vector", property_icons["Float"], 2),
|
||||
("Integer", "Integer", "Stores an integer number or a vector", property_icons["Integer"], 3),
|
||||
("Enum", "Enum", "Stores multiple entries to be used as dropdowns", property_icons["Enum"], 4),
|
||||
("Pointer", "Pointer", "Stores a reference to certain types of blend data, collection or group properties", property_icons["Pointer"], 5),
|
||||
("Collection", "Collection", "Stores a list of certain blend data or property groups to be displayed in lists", property_icons["Collection"], 6)]
|
||||
if not self.allow_pointers:
|
||||
items.pop(5)
|
||||
return items
|
||||
|
||||
property_type: bpy.props.EnumProperty(name="Type",
|
||||
description="The type of data this property can store",
|
||||
update=trigger_reference_update,
|
||||
items=get_types)
|
||||
|
||||
allow_pointers: bpy.props.BoolProperty(default=True)
|
||||
|
||||
|
||||
def get_prop_option_items(self, context):
|
||||
items = [("HIDDEN", "Hidden", "Hide property from operator popups"),
|
||||
("SKIP_SAVE", "Skip Save", "Don't save this property between calls"),
|
||||
("ANIMATABLE", "Animatable", "Enable if this property should be animatable"),
|
||||
("TEXTEDIT_UPDATE", "Textedit Update", "Calls the update function every time the property is edited (Only string properties; not operator popups)")]
|
||||
return items
|
||||
|
||||
prop_options: bpy.props.EnumProperty(name="Options",
|
||||
description="Options for this property",
|
||||
options={"ENUM_FLAG"},
|
||||
items=get_prop_option_items,
|
||||
update=_compile)
|
||||
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
return {
|
||||
"String": self.stngs_string,
|
||||
"Boolean": self.stngs_boolean,
|
||||
"Float": self.stngs_float,
|
||||
"Integer": self.stngs_integer,
|
||||
"Enum": self.stngs_enum,
|
||||
"Pointer": self.stngs_pointer,
|
||||
"Collection": self.stngs_collection,
|
||||
}[self.property_type]
|
||||
|
||||
stngs_string: bpy.props.PointerProperty(type=SN_PT_StringProperty)
|
||||
stngs_boolean: bpy.props.PointerProperty(type=SN_PT_BooleanProperty)
|
||||
stngs_float: bpy.props.PointerProperty(type=SN_PT_FloatProperty)
|
||||
stngs_integer: bpy.props.PointerProperty(type=SN_PT_IntegerProperty)
|
||||
stngs_enum: bpy.props.PointerProperty(type=SN_PT_EnumProperty)
|
||||
stngs_pointer: bpy.props.PointerProperty(type=SN_PT_PointerProperty)
|
||||
stngs_collection: bpy.props.PointerProperty(type=SN_PT_CollectionProperty)
|
||||
|
||||
|
||||
category: bpy.props.StringProperty(name="Category", default="OTHER",
|
||||
description="The category this property is displayed in")
|
||||
|
||||
def match_settings(self, new_prop):
|
||||
new_prop["name"] = self.get_unique_name(self.get("name"))
|
||||
new_prop["property_type"] = self.get("property_type")
|
||||
new_prop["description"] = self.get("description")
|
||||
new_prop["allow_pointers"] = self.get("allow_pointers")
|
||||
new_prop["prop_options"] = self.get("prop_options")
|
||||
new_prop["category"] = self.get("category")
|
||||
for attr in self.settings.copy_attributes:
|
||||
new_prop.settings[attr] = self.settings.get(attr)
|
||||
self.settings.copy(new_prop.settings)
|
||||
|
||||
def copy(self):
|
||||
new_prop = self.prop_collection.add()
|
||||
self.match_settings(new_prop)
|
||||
return new_prop
|
||||
@@ -0,0 +1,135 @@
|
||||
import bpy
|
||||
from ...interface.panels.property_ui_list import get_selected_property
|
||||
|
||||
|
||||
class SN_PropertyCategory(bpy.types.PropertyGroup):
|
||||
|
||||
def set_name(self, value):
|
||||
for prop in bpy.context.scene.sn.properties:
|
||||
if prop.category and prop.category == self.name:
|
||||
prop.category = value
|
||||
self["name"] = value
|
||||
|
||||
def get_name(self):
|
||||
return self.get("name", "New Category")
|
||||
|
||||
name: bpy.props.StringProperty(name="Name", default="New Category",
|
||||
description="The name of this property category",
|
||||
set=set_name, get=get_name)
|
||||
|
||||
|
||||
|
||||
class SN_OT_AddPropertyCategory(bpy.types.Operator):
|
||||
bl_idname = "sn.add_property_category"
|
||||
bl_label = "Add Property Category"
|
||||
bl_description = "Adds a property category"
|
||||
bl_options = {"REGISTER", "INTERNAL", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.sn.property_categories.add()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_RemovePropertyCategory(bpy.types.Operator):
|
||||
bl_idname = "sn.remove_property_category"
|
||||
bl_label = "Remove Property Category"
|
||||
bl_description = "Removes a property category"
|
||||
bl_options = {"REGISTER", "INTERNAL", "UNDO"}
|
||||
|
||||
index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.sn.property_categories.remove(self.index)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_EditPropertyCategories(bpy.types.Operator):
|
||||
bl_idname = "sn.edit_property_categories"
|
||||
bl_label = "Edit Property Categories"
|
||||
bl_description = "Edit the addon property categories"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="Categories")
|
||||
for i, cat in enumerate(context.scene.sn.property_categories):
|
||||
row = layout.row()
|
||||
row.scale_y = 1.2
|
||||
row.prop(cat, "name", text="")
|
||||
row.operator("sn.remove_property_category", text="", icon="REMOVE", emboss=False).index = i
|
||||
|
||||
if not context.scene.sn.property_categories:
|
||||
row = layout.row()
|
||||
row.enabled = False
|
||||
row.label(text="No categories added", icon="ERROR")
|
||||
|
||||
row = layout.row()
|
||||
row.scale_y = 1.2
|
||||
row.operator("sn.add_property_category", text="Add Category", icon="ADD")
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_popup(self, width=250)
|
||||
|
||||
|
||||
|
||||
class SN_OT_MovePropertyToCategory(bpy.types.Operator):
|
||||
bl_idname = "sn.move_property_to_category"
|
||||
bl_label = "Move Property Category"
|
||||
bl_description = "Move the selected property to a different category"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
category: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
prop = get_selected_property()
|
||||
if prop:
|
||||
if self.category == -1:
|
||||
prop.category = "OTHER"
|
||||
else:
|
||||
prop.category = context.scene.sn.property_categories[self.category].name
|
||||
context.area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_MovePropertyCategory(bpy.types.Operator):
|
||||
bl_idname = "sn.move_property_category"
|
||||
bl_label = "Move Property Category"
|
||||
bl_description = "Move the selected property to a different category"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
prop = get_selected_property()
|
||||
|
||||
layout.label(text="Categories")
|
||||
for i, cat in enumerate(context.scene.sn.property_categories):
|
||||
row = layout.row()
|
||||
row.enabled = prop != None and prop.category != cat.name
|
||||
row.scale_y = 1.2
|
||||
row.operator("sn.move_property_to_category", text=f"Move to '{cat.name}'", icon="FORWARD").category = i
|
||||
|
||||
row = layout.row()
|
||||
row.enabled = prop != None and prop.category and prop.category != "OTHER"
|
||||
row.scale_y = 1.2
|
||||
row.operator("sn.move_property_to_category", text=f"Remove Category", icon="REMOVE").category = -1
|
||||
|
||||
if not len(context.scene.sn.property_categories):
|
||||
row = layout.row()
|
||||
row.enabled = False
|
||||
row.label(text="No categories added", icon="ERROR")
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.scene.sn.property_index = self.index
|
||||
return context.window_manager.invoke_popup(self, width=250)
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
import bpy
|
||||
from ...nodes.compiler import compile_addon
|
||||
from ...interface.panels.property_ui_list import get_selected_property, get_selected_property_offset
|
||||
|
||||
|
||||
|
||||
class SN_OT_AddProperty(bpy.types.Operator):
|
||||
bl_idname = "sn.add_property"
|
||||
bl_label = "Add Property"
|
||||
bl_description = "Adds a property to the addon"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
sn = context.scene.sn
|
||||
new_prop = sn.properties.add()
|
||||
new_prop.name = "New Property"
|
||||
if sn.active_prop_category: new_prop.category = sn.active_prop_category
|
||||
for index, property in enumerate(sn.properties):
|
||||
if property == new_prop:
|
||||
sn.property_index = index
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_RemoveProperty(bpy.types.Operator):
|
||||
bl_idname = "sn.remove_property"
|
||||
bl_label = "Remove Property"
|
||||
bl_description = "Removes this property from the addon"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.scene.sn.property_index < len(context.scene.sn.properties)
|
||||
|
||||
def execute(self, context):
|
||||
sn = context.scene.sn
|
||||
sn.properties.remove(sn.property_index)
|
||||
sn.property_index -= 1
|
||||
compile_addon()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_RemoveGroupProperty(bpy.types.Operator):
|
||||
bl_idname = "sn.remove_group_property"
|
||||
bl_label = "Remove Property"
|
||||
bl_description = "Removes this property from the addon"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
group_items_path: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
items = eval(self.group_items_path)
|
||||
items.remove(self.index)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_MoveProperty(bpy.types.Operator):
|
||||
bl_idname = "sn.move_property"
|
||||
bl_label = "Move Property"
|
||||
bl_description = "Moves this property"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
move_up: bpy.props.BoolProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
sn = context.scene.sn
|
||||
if self.move_up:
|
||||
before = get_selected_property_offset(-1)
|
||||
new_index = list(sn.properties).index(before)
|
||||
sn.properties.move(sn.property_index, new_index)
|
||||
sn.property_index = new_index
|
||||
else:
|
||||
after = get_selected_property_offset(1)
|
||||
new_index = list(sn.properties).index(after)
|
||||
sn.properties.move(sn.property_index, new_index)
|
||||
sn.property_index = new_index
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_DuplicateProperty(bpy.types.Operator):
|
||||
bl_idname = "sn.duplicate_property"
|
||||
bl_label = "Duplicate Property"
|
||||
bl_description = "Duplicates the selected property"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
sn = context.scene.sn
|
||||
prop = get_selected_property()
|
||||
if prop:
|
||||
prop.copy()
|
||||
sn.properties.move(len(sn.properties)-1, sn.property_index+1)
|
||||
sn.property_index += 1
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_MoveGroupProperty(bpy.types.Operator):
|
||||
bl_idname = "sn.move_group_property"
|
||||
bl_label = "Move Property"
|
||||
bl_description = "Moves this property"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
group_items_path: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
move_up: bpy.props.BoolProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
items = eval(self.group_items_path)
|
||||
if self.move_up:
|
||||
items.move(self.index, self.index - 1)
|
||||
else:
|
||||
items.move(self.index, self.index + 1)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_CopyPythonName(bpy.types.Operator):
|
||||
bl_idname = "sn.copy_python_name"
|
||||
bl_label = "Copy Python Name"
|
||||
bl_description = "Copies the python name of this item to use in scripts"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
name: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
context.window_manager.clipboard = self.name
|
||||
self.report({"INFO"}, message="Copied!")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_AddEnumItem(bpy.types.Operator):
|
||||
bl_idname = "sn.add_enum_item"
|
||||
bl_label = "Add Enum Item"
|
||||
bl_description = "Adds an enum item to this property"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
item_data_path: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
items = eval(self.item_data_path)
|
||||
item = items.add()
|
||||
item.update(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_RemoveEnumItem(bpy.types.Operator):
|
||||
bl_idname = "sn.remove_enum_item"
|
||||
bl_label = "Remove Enum Item"
|
||||
bl_description = "Removes an enum item from this property"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
settings_data_path: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
item_index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
settings = eval(self.settings_data_path)
|
||||
settings.items.remove(self.item_index)
|
||||
settings.compile(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_MoveEnumItem(bpy.types.Operator):
|
||||
bl_idname = "sn.move_enum_item"
|
||||
bl_label = "Move Enum Item"
|
||||
bl_description = "Moves this enum item"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
settings_data_path: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
item_index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
move_up: bpy.props.BoolProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
settings = eval(self.settings_data_path)
|
||||
if self.move_up:
|
||||
settings.items.move(self.item_index, self.item_index-1)
|
||||
else:
|
||||
settings.items.move(self.item_index, self.item_index+1)
|
||||
settings.compile(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_AddPropertyItem(bpy.types.Operator):
|
||||
bl_idname = "sn.add_property_item"
|
||||
bl_label = "Add Property"
|
||||
bl_description = "Adds a property to this group"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
group_data_path: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
prop = eval(self.group_data_path)
|
||||
new_prop = prop.settings.properties.add()
|
||||
new_prop.name = "New Property"
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_AddPropertyNodePopup(bpy.types.Operator):
|
||||
bl_idname = "sn.add_property_node_popup"
|
||||
bl_label = "Add Property Node Popup"
|
||||
bl_description = "Opens a popup to let you choose a property node"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
node: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column(align=True)
|
||||
col.scale_y = 1.5
|
||||
op = col.operator("sn.add_property_node", text="Property", icon="ADD")
|
||||
op.type = "SN_SerpensPropertyNode"
|
||||
op.node = self.node
|
||||
op = col.operator("sn.add_property_node", text="Display Property", icon="ADD")
|
||||
op.type = "SN_DisplayPropertyNodeNew"
|
||||
op.node = self.node
|
||||
op = col.operator("sn.add_property_node", text="Set Property", icon="ADD")
|
||||
op.type = "SN_SetPropertyNode"
|
||||
op.node = self.node
|
||||
op = col.operator("sn.add_property_node", text="On Property Update", icon="ADD")
|
||||
op.type = "SN_OnPropertyUpdateNode"
|
||||
op.node = self.node
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_popup(self)
|
||||
|
||||
|
||||
|
||||
class SN_OT_AddPropertyNode(bpy.types.Operator):
|
||||
bl_idname = "sn.add_property_node"
|
||||
bl_label = "Add Property Node"
|
||||
bl_description = "Adds this node to the editor"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
type: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
node: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.node.add_node("INVOKE_DEFAULT", type=self.type, use_transform=True)
|
||||
node = context.space_data.node_tree.nodes.active
|
||||
|
||||
prop = None
|
||||
if self.node:
|
||||
prop_node = context.space_data.node_tree.nodes[self.node]
|
||||
if prop_node.property_index < len(prop_node.properties):
|
||||
prop = prop_node.properties[prop_node.property_index]
|
||||
elif context.scene.sn.property_index < len(context.scene.sn.properties):
|
||||
prop = context.scene.sn.properties[context.scene.sn.property_index]
|
||||
|
||||
if prop:
|
||||
if self.type in ["SN_SerpensPropertyNode", "SN_OnPropertyUpdateNode"]:
|
||||
if self.node:
|
||||
node.prop_source = "NODE"
|
||||
node.from_node = self.node
|
||||
node.prop_name = prop.name
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_FindProperty(bpy.types.Operator):
|
||||
bl_idname = "sn.find_property"
|
||||
bl_label = "Find Property"
|
||||
bl_description = "Finds this property in the addon"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
# init property nodes
|
||||
empty_nodes = []
|
||||
property_nodes = []
|
||||
property = None
|
||||
if context.scene.sn.property_index < len(context.scene.sn.properties):
|
||||
property = context.scene.sn.properties[context.scene.sn.property_index]
|
||||
|
||||
# find property nodes
|
||||
for ngroup in bpy.data.node_groups:
|
||||
if ngroup.bl_idname == "ScriptingNodesTree":
|
||||
for node in ngroup.nodes:
|
||||
if node.bl_idname == "SN_SerpensPropertyNode":
|
||||
prop_src = node.get_prop_source()
|
||||
if prop_src and node.prop_name in prop_src.properties:
|
||||
prop = prop_src.properties[node.prop_name]
|
||||
if prop == property:
|
||||
property_nodes.append(node)
|
||||
elif not prop_src or not node.prop_name:
|
||||
empty_nodes.append(node)
|
||||
|
||||
# draw nodes for selected property
|
||||
if context.scene.sn.property_index < len(context.scene.sn.properties):
|
||||
col = layout.column()
|
||||
row = col.row()
|
||||
row.enabled = False
|
||||
row.label(text=f"Property: {property.name}")
|
||||
|
||||
for node in property_nodes:
|
||||
op = col.operator("sn.find_node", text=node.name, icon="RESTRICT_SELECT_OFF")
|
||||
op.node_tree = node.node_tree.name
|
||||
op.node = node.name
|
||||
|
||||
if not property_nodes:
|
||||
col.label(text="No nodes found for this property", icon="INFO")
|
||||
|
||||
# draw nodes with empty property
|
||||
col = layout.column()
|
||||
row = col.row()
|
||||
row.label(text="Empty Propert Nodes")
|
||||
row.enabled = False
|
||||
|
||||
for node in empty_nodes:
|
||||
op = col.operator("sn.find_node", text=node.name, icon="RESTRICT_SELECT_OFF")
|
||||
op.node_tree = node.node_tree.name
|
||||
op.node = node.name
|
||||
|
||||
if not empty_nodes:
|
||||
col.label(text="No empty property nodes found", icon="INFO")
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_popup(self, width=250)
|
||||
@@ -0,0 +1,21 @@
|
||||
def get_sorted_props(prop_list):
|
||||
""" Returns a list of the properties, sorted for registering """
|
||||
# sort groups to the top of the list
|
||||
prop_list.sort(key=lambda prop: prop.property_type == "Group", reverse=True)
|
||||
# split property groups with collections or pointers with use prop group enabled
|
||||
prop_groups = list(filter(lambda prop: prop.property_type == "Group", prop_list))
|
||||
other_props = list(filter(lambda prop: not prop in prop_groups, prop_list))
|
||||
ref_prop_groups = list(filter(_is_propgroup_with_references, prop_groups))
|
||||
prop_groups = list(filter(lambda prop: not prop in ref_prop_groups, prop_groups))
|
||||
# TODO sort ref_prop_groups -> may fail if a prop group has a prop with a collection or pointer to another prop group with the same
|
||||
return prop_groups + ref_prop_groups + other_props
|
||||
|
||||
|
||||
def _is_propgroup_with_references(prop):
|
||||
""" Returns if the given property group has pointer or collection props with references to other prop groups """
|
||||
for subprop in prop.settings.properties:
|
||||
if subprop.property_type == "Collection" and subprop.settings.prop_group in prop.prop_collection_origin.properties:
|
||||
return True
|
||||
elif subprop.property_type == "Pointer" and subprop.settings.use_prop_group and subprop.settings.prop_group in prop.prop_collection_origin.properties:
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,64 @@
|
||||
import bpy
|
||||
from .settings import PropertySettings
|
||||
|
||||
|
||||
|
||||
class SN_PT_BooleanProperty(PropertySettings, bpy.types.PropertyGroup):
|
||||
|
||||
type_description = "Boolean properties can hold a value of True or False.\n" \
|
||||
+ "They can also be turned into a vector which holds multiple of these.\n" \
|
||||
+ "\n" \
|
||||
+ "Booleans are displayed as checkboxes or toggles in the UI."
|
||||
|
||||
copy_attributes = ["default", "is_vector", "size", "vector_default"]
|
||||
|
||||
def draw(self, context, layout):
|
||||
""" Draws the settings for this property type """
|
||||
row = layout.row(heading="Default")
|
||||
row.enabled = not self.is_vector
|
||||
row.prop(self, "default", toggle=True, text=str(self.default))
|
||||
layout.separator()
|
||||
layout.prop(self, "is_vector")
|
||||
col = layout.column()
|
||||
col.enabled = self.is_vector
|
||||
col.prop(self, "size")
|
||||
sub_col = col.column(align=True, heading="Default")
|
||||
for i in range(self.size):
|
||||
sub_col.prop(self, "vector_default", index=i, text=str(self.vector_default[i]), toggle=True)
|
||||
|
||||
|
||||
@property
|
||||
def prop_type_name(self):
|
||||
if self.is_vector:
|
||||
return "BoolVectorProperty"
|
||||
return "BoolProperty"
|
||||
|
||||
|
||||
@property
|
||||
def register_options(self):
|
||||
if self.is_vector:
|
||||
options = f"size={self.size}, default={tuple(list(self.vector_default)[:self.size])}"
|
||||
else:
|
||||
options = f"default={self.default}"
|
||||
return options + self.update_option
|
||||
|
||||
|
||||
default: bpy.props.BoolProperty(name="Default",
|
||||
description="Default value of this property (This may not reset automatically for existing attached items)",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
def update_vector(self, context):
|
||||
self.prop.trigger_reference_update(context)
|
||||
|
||||
is_vector: bpy.props.BoolProperty(name="Is Vector",
|
||||
description="If this property is a vector",
|
||||
update=update_vector)
|
||||
|
||||
size: bpy.props.IntProperty(name="Vector Size", min=2, max=32, default=3,
|
||||
description="Length of the vector property",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
vector_default: bpy.props.BoolVectorProperty(name="Default",
|
||||
description="Default value of this property (This may not reset automatically for existing attached items)",
|
||||
size=32,
|
||||
update=PropertySettings.compile)
|
||||
@@ -0,0 +1,47 @@
|
||||
import bpy
|
||||
from .settings import PropertySettings
|
||||
|
||||
|
||||
|
||||
class SN_PT_CollectionProperty(PropertySettings, bpy.types.PropertyGroup):
|
||||
|
||||
type_description = "Integer properties can hold decimal number.\n" \
|
||||
+ "They can also be turned into a vector which holds multiple of these.\n" \
|
||||
+ "\n" \
|
||||
+ "Integers are displayed as number inputs."
|
||||
|
||||
copy_attributes = ["prop_group"]
|
||||
|
||||
|
||||
def draw(self, context, layout):
|
||||
""" Draws the settings for this property type """
|
||||
src = context.scene.sn
|
||||
layout.prop_search(self, "prop_group", src, "properties")
|
||||
row = layout.row()
|
||||
row.alert = True
|
||||
if self.prop_group in src.properties:
|
||||
if not src.properties[self.prop_group].property_type == "Group":
|
||||
row.label(text="The selected property is not a group!", icon="ERROR")
|
||||
elif hasattr(self.prop, "group_prop_parent") and self.prop.group_prop_parent.name == self.prop_group:
|
||||
row.label(text="Can't use self reference for this collection!", icon="ERROR")
|
||||
else:
|
||||
row.label(text="There is no valid property group selected!", icon="ERROR")
|
||||
|
||||
|
||||
@property
|
||||
def prop_type_name(self):
|
||||
return "CollectionProperty"
|
||||
|
||||
|
||||
@property
|
||||
def register_options(self):
|
||||
src = self.prop.prop_collection_origin
|
||||
if self.prop_group in src.properties and src.properties[self.prop_group].property_type == "Group":
|
||||
if not hasattr(self.prop, "group_prop_parent") or (hasattr(self.prop, "group_prop_parent") and self.prop.group_prop_parent.name != self.prop_group):
|
||||
return f"type=SNA_GROUP_{src.properties[self.prop_group].python_name}"
|
||||
return "type=bpy.types.PropertyGroup.__subclasses__()[0]"
|
||||
|
||||
|
||||
prop_group: bpy.props.StringProperty(name="Property Group",
|
||||
description="The property group you want to point to",
|
||||
update=PropertySettings.compile)
|
||||
@@ -0,0 +1,180 @@
|
||||
import bpy
|
||||
from ....utils import normalize_code
|
||||
from .settings import PropertySettings
|
||||
|
||||
|
||||
|
||||
_enum_prop_cache = {} # stores key, value of enum.as_pointer, prop
|
||||
|
||||
class EnumItem(bpy.types.PropertyGroup):
|
||||
|
||||
@property
|
||||
def prop(self):
|
||||
if self.id_data.bl_rna.identifier == "ScriptingNodesTree":
|
||||
# find property in nodes to return
|
||||
if not str(self.as_pointer()) in _enum_prop_cache:
|
||||
for node in self.id_data.nodes:
|
||||
if hasattr(node, "properties"):
|
||||
for prop in node.properties:
|
||||
if prop.property_type == "Enum":
|
||||
for item in prop.settings.items:
|
||||
if item == self:
|
||||
_enum_prop_cache[str(self.as_pointer())] = prop
|
||||
break
|
||||
elif prop.property_type == "Group":
|
||||
for subprop in prop.settings.properties:
|
||||
if subprop.property_type == "Enum":
|
||||
for item in subprop.settings.items:
|
||||
if item == self:
|
||||
_enum_prop_cache[str(self.as_pointer())] = prop
|
||||
break
|
||||
return _enum_prop_cache[str(self.as_pointer())]
|
||||
|
||||
else:
|
||||
path = ".".join(repr(self.path_resolve("name", False)).split(".")[:-2])
|
||||
prop = eval(path)
|
||||
return prop
|
||||
|
||||
def update(self, context):
|
||||
self.prop.compile()
|
||||
|
||||
name: bpy.props.StringProperty(name="Name", default="New Item",
|
||||
description="Name of this enum item",
|
||||
update=update)
|
||||
|
||||
description: bpy.props.StringProperty(name="Description",
|
||||
description="Description of this enum item",
|
||||
update=update)
|
||||
|
||||
icon: bpy.props.IntProperty(name="Icon", default=0, min=0,
|
||||
description="Icon value of this enum item",
|
||||
update=update)
|
||||
|
||||
|
||||
|
||||
|
||||
class SN_PT_EnumProperty(PropertySettings, bpy.types.PropertyGroup):
|
||||
|
||||
type_description = "Enum properties can hold multiple items with a name and description.\n" \
|
||||
+ "\n" \
|
||||
+ "Enum properties are displayed as dropdowns or a list of toggles.\n" \
|
||||
+ "Dynamic enums can be used to display custom icons such as a list of asset images."
|
||||
|
||||
copy_attributes = ["enum_flag", "is_dynamic"]
|
||||
|
||||
def copy(self, new_settings):
|
||||
for item in self.items:
|
||||
new = new_settings.items.add()
|
||||
new.name = item.name
|
||||
new.description = item.description
|
||||
new.icon = item.icon
|
||||
|
||||
|
||||
def draw(self, context, layout):
|
||||
""" Draws the settings for this property type """
|
||||
layout.prop(self, "enum_flag")
|
||||
layout.prop(self, "is_dynamic")
|
||||
|
||||
layout.separator()
|
||||
row = layout.row()
|
||||
row.scale_y = 1.2
|
||||
if not self.is_dynamic:
|
||||
op = row.operator("sn.add_enum_item", text="Add Item", icon="ADD")
|
||||
op.item_data_path = f"{self.prop.full_prop_path}.settings.items"
|
||||
|
||||
for i, item in enumerate(self.items):
|
||||
box = layout.box()
|
||||
col = box.column()
|
||||
box.use_property_split = False
|
||||
row = col.row()
|
||||
subrow = row.row(align=True)
|
||||
subrow.prop(item, "name", text="")
|
||||
op = subrow.operator("sn.select_icon", icon_value=item.icon if item.icon != 0 else 101, text="", emboss=item.icon==0)
|
||||
op.icon_data_path = f"{self.prop.full_prop_path}.settings.items[{i}]"
|
||||
subrow = row.row(align=True)
|
||||
subcol = subrow.column(align=True)
|
||||
subcol.enabled = i > 0
|
||||
op = subcol.operator("sn.move_enum_item", text="", icon="TRIA_UP", emboss=False)
|
||||
op.settings_data_path = f"{self.prop.full_prop_path}.settings"
|
||||
op.item_index = i
|
||||
op.move_up = True
|
||||
subcol = subrow.column(align=True)
|
||||
subcol.enabled = i < len(self.items)-1
|
||||
op = subcol.operator("sn.move_enum_item", text="", icon="TRIA_DOWN", emboss=False)
|
||||
op.settings_data_path = f"{self.prop.full_prop_path}.settings"
|
||||
op.item_index = i
|
||||
op.move_up = False
|
||||
op = subrow.operator("sn.remove_enum_item", text="", icon="PANEL_CLOSE", emboss=False)
|
||||
op.settings_data_path = f"{self.prop.full_prop_path}.settings"
|
||||
op.item_index = i
|
||||
col.prop(item, "description")
|
||||
|
||||
else:
|
||||
op = row.operator("sn.add_generate_items_node", text="Generate Items", icon="ADD")
|
||||
|
||||
|
||||
@property
|
||||
def prop_type_name(self):
|
||||
return "EnumProperty"
|
||||
|
||||
|
||||
@property
|
||||
def item_func_name(self):
|
||||
name = f"{self.prop.python_name}_enum_items"
|
||||
if hasattr(self.prop, "group_prop_parent"):
|
||||
return f"{self.prop.group_prop_parent.python_name}_{name}"
|
||||
return name
|
||||
|
||||
|
||||
@property
|
||||
def register_options(self):
|
||||
options = ""
|
||||
if not self.is_dynamic:
|
||||
items = []
|
||||
for i, item in enumerate(self.items):
|
||||
if self.enum_flag:
|
||||
i = 2 ** i
|
||||
items.append(f"('{item.name}', '{item.name}', '{item.description}', {item.icon}, {i})")
|
||||
options = f"items=[{', '.join(items)}]"
|
||||
else:
|
||||
options = f"items={self.item_func_name}"
|
||||
|
||||
if self.enum_flag:
|
||||
options += ", options={'ENUM_FLAG'}"
|
||||
return options + self.update_option
|
||||
|
||||
|
||||
def imperative_code(self):
|
||||
# node exists for this property
|
||||
for ntree in bpy.data.node_groups:
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
for ref in ntree.node_collection("SN_GenerateEnumItemsNode").refs:
|
||||
node = ref.node
|
||||
enum_src = node.get_prop_source()
|
||||
if enum_src and node.prop_name in enum_src.properties and enum_src.properties[node.prop_name] == self.prop:
|
||||
return ""
|
||||
|
||||
code = f"""
|
||||
def {self.item_func_name}(self, context):
|
||||
return [("No Items", "No Items", "No generate enum items node found to create items!", "ERROR", 0)]
|
||||
"""
|
||||
return normalize_code(code) + "\n" + self.update_function
|
||||
|
||||
|
||||
def update_enum_flag(self, context):
|
||||
self.prop.trigger_reference_update(context)
|
||||
self.compile(context)
|
||||
|
||||
enum_flag: bpy.props.BoolProperty(name="Select Multiple (Enum Set)",
|
||||
description="Lets you select multiple options from this property",
|
||||
update=update_enum_flag)
|
||||
|
||||
|
||||
is_dynamic: bpy.props.BoolProperty(name="Dynamic Items",
|
||||
description="The items are generated with a function and aren't predefined",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
|
||||
items: bpy.props.CollectionProperty(type=EnumItem,
|
||||
name="Items",
|
||||
description="Enum Items")
|
||||
@@ -0,0 +1,206 @@
|
||||
import bpy
|
||||
from .settings import PropertySettings
|
||||
|
||||
|
||||
|
||||
class SN_PT_FloatProperty(PropertySettings, bpy.types.PropertyGroup):
|
||||
|
||||
type_description = "Float properties can hold decimal number.\n" \
|
||||
+ "They can also be turned into a vector which holds multiple of these.\n" \
|
||||
+ "\n" \
|
||||
+ "Floats are displayed as number inputs or sliders.\n" \
|
||||
+ "Float vectors can be used with subtypes, for example to make a color input."
|
||||
|
||||
copy_attributes = ["is_vector", "default", "subtype", "unit", "use_min", "min", "use_max", "use_soft_min",
|
||||
"soft_min", "max", "use_soft_max", "soft_max", "step", "precision", "size", "vector_default"]
|
||||
|
||||
def draw(self, context, layout):
|
||||
""" Draws the settings for this property type """
|
||||
layout.prop(self, "subtype")
|
||||
layout.prop(self, "unit")
|
||||
row = layout.row()
|
||||
row.enabled = not self.is_vector
|
||||
row.prop(self, "default")
|
||||
|
||||
layout.separator()
|
||||
layout.prop(self, "step")
|
||||
layout.prop(self, "precision")
|
||||
|
||||
layout.separator()
|
||||
row = layout.row(heading="Minimum")
|
||||
row.prop(self, "use_min", text="")
|
||||
sub_row = row.row()
|
||||
sub_row.enabled = self.use_min
|
||||
sub_row.prop(self, "min")
|
||||
|
||||
row = layout.row(heading="Maximum")
|
||||
row.prop(self, "use_max", text="")
|
||||
sub_row = row.row()
|
||||
sub_row.enabled = self.use_max
|
||||
sub_row.prop(self, "max")
|
||||
|
||||
row = layout.row(heading="Soft Minimum")
|
||||
row.prop(self, "use_soft_min", text="")
|
||||
sub_row = row.row()
|
||||
sub_row.enabled = self.use_soft_min
|
||||
sub_row.prop(self, "soft_min")
|
||||
|
||||
row = layout.row(heading="Soft Maximum")
|
||||
row.prop(self, "use_soft_max", text="")
|
||||
sub_row = row.row()
|
||||
sub_row.enabled = self.use_soft_max
|
||||
sub_row.prop(self, "soft_max")
|
||||
|
||||
layout.separator()
|
||||
layout.prop(self, "is_vector")
|
||||
col = layout.column()
|
||||
col.enabled = self.is_vector
|
||||
col.prop(self, "size")
|
||||
|
||||
row = col.row()
|
||||
split = row.split(factor=0.4)
|
||||
split.alignment = "RIGHT"
|
||||
split.label(text="Default")
|
||||
sub_col = split.column(align=True)
|
||||
for i in range(self.size):
|
||||
sub_col.prop(self, "vector_default", index=i, text="")
|
||||
|
||||
|
||||
@property
|
||||
def prop_type_name(self):
|
||||
if self.is_vector:
|
||||
return "FloatVectorProperty"
|
||||
return "FloatProperty"
|
||||
|
||||
|
||||
@property
|
||||
def register_options(self):
|
||||
if self.is_vector:
|
||||
options = f"size={self.size}, default={tuple(list(self.vector_default)[:self.size])}"
|
||||
else:
|
||||
options = f"default={self.default}"
|
||||
options += f", subtype='{self.subtype}'"
|
||||
options += f", unit='{self.unit}'"
|
||||
if self.use_min: options += f", min={self.min}"
|
||||
if self.use_soft_min: options += f", soft_min={self.soft_min}"
|
||||
if self.use_max: options += f", max={self.max}"
|
||||
if self.use_soft_max: options += f", soft_max={self.soft_max}"
|
||||
options += f", step={self.step}"
|
||||
options += f", precision={self.precision}"
|
||||
return options + self.update_option
|
||||
|
||||
|
||||
default: bpy.props.FloatProperty(name="Default",
|
||||
description="Default value of this property (This may not reset automatically for existing attached items)",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
|
||||
def get_subtype_items(self, context):
|
||||
items = [("NONE", "None", "No subtype, just a default float input"),
|
||||
("PIXEL", "Pixel", "Pixel"),
|
||||
("UNSIGNED", "Unsigned", "Unsigned"),
|
||||
("PERCENTAGE", "Percentage", "Percentage"),
|
||||
("FACTOR", "Factor", "Factor"),
|
||||
("ANGLE", "Angle", "Angle"),
|
||||
("TIME", "Time", "Time"),
|
||||
("DISTANCE", "Distance", "Distance"),
|
||||
("DISTANCE_CAMERA", "Distance Camera", "Distance Camera"),
|
||||
("POWER", "Power", "Power"),
|
||||
("TEMPERATURE", "Temperature", "Temperature")]
|
||||
if self.is_vector:
|
||||
items = [("NONE", "None", "No subtype, just a default float vector input"),
|
||||
("COLOR", "Color", "Color"),
|
||||
("TRANSLATION", "Translation", "Translation"),
|
||||
("DIRECTION", "Direction", "Direction"),
|
||||
("VELOCITY", "Velocity", "Velocity"),
|
||||
("ACCELERATION", "Acceleration", "Acceleration"),
|
||||
("MATRIX", "Matrix", "Matrix"),
|
||||
("EULER", "Euler", "Euler"),
|
||||
("QUATERNION", "Quaternion", "Quaternion"),
|
||||
("AXISANGLE", "Axisangle", "Axisangle"),
|
||||
("XYZ", "XYZ", "XYZ"),
|
||||
("XYZ_LENGTH", "XYZ Length", "XYZ Length"),
|
||||
("COLOR_GAMMA", "Color Gamma", "Color Gamma"),
|
||||
("COORDINATES", "Coordinates", "Coordinates"),
|
||||
("LAYER", "Layer", "Layer"),
|
||||
("LAYER_MEMBER", "Layer Member", "Layer Member"),]
|
||||
return items
|
||||
|
||||
subtype: bpy.props.EnumProperty(name="Subtype",
|
||||
description="The subtype of this property. This changes how the property is displayed",
|
||||
update=PropertySettings.compile,
|
||||
items=get_subtype_items)
|
||||
|
||||
|
||||
unit: bpy.props.EnumProperty(name="Unit",
|
||||
description="The unit of this property. This changes how the property is displayed",
|
||||
update=PropertySettings.compile,
|
||||
items=[("NONE", "None", "No unit, just a default float input"),
|
||||
("LENGTH", "Length", "Length"),
|
||||
("AREA", "Area", "Area"),
|
||||
("VOLUME", "Volume", "Volume"),
|
||||
("ROTATION", "Rotation", "Rotation"),
|
||||
("TIME", "Time", "Time"),
|
||||
("VELOCITY", "Velocity", "Velocity"),
|
||||
("ACCELERATION", "Acceleration", "Acceleration"),
|
||||
("MASS", "Mass", "Mass"),
|
||||
("CAMERA", "Camera", "Camera"),
|
||||
("POWER", "Power", "Power")])
|
||||
|
||||
|
||||
use_min: bpy.props.BoolProperty(name="Minimum",
|
||||
description="Use a minimum property value",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
min: bpy.props.FloatProperty(name="Minimum", default=-0,
|
||||
description="The minimum value of this property",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
use_max: bpy.props.BoolProperty(name="Maximum",
|
||||
description="Use a maximum property value",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
use_soft_min: bpy.props.BoolProperty(name="Soft Minimum",
|
||||
description="Use a soft minimum property value",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
soft_min: bpy.props.FloatProperty(name="Soft Minimum", default=-0,
|
||||
description="The soft minimum value of this property",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
max: bpy.props.FloatProperty(name="Maximum", default=1,
|
||||
description="The maximum value of this property",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
use_soft_max: bpy.props.BoolProperty(name="Soft Maximum",
|
||||
description="Use a soft maximum property value",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
soft_max: bpy.props.FloatProperty(name="Soft Maximum", default=1,
|
||||
description="The soft maximum value of this property",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
|
||||
step: bpy.props.IntProperty(name="Step", min=1, max=100, default=3,
|
||||
description="Step of increment/decrement in the UI",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
precision: bpy.props.IntProperty(name="Precision", min=0, max=6, default=6,
|
||||
description="Maximum number of decimal digits to display",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
def update_vector(self, context):
|
||||
self.prop.trigger_reference_update(context)
|
||||
|
||||
is_vector: bpy.props.BoolProperty(name="Is Vector",
|
||||
description="If this property is a vector",
|
||||
update=update_vector)
|
||||
|
||||
size: bpy.props.IntProperty(name="Vector Size", min=2, max=32, default=3,
|
||||
description="Length of the vector property",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
vector_default: bpy.props.FloatVectorProperty(name="Default",
|
||||
description="Default value of this property (This may not reset automatically for existing attached items)",
|
||||
size=32,
|
||||
update=PropertySettings.compile)
|
||||
@@ -0,0 +1,120 @@
|
||||
import bpy
|
||||
from .settings import PropertySettings
|
||||
from ..property_basic import BasicProperty
|
||||
|
||||
|
||||
|
||||
_group_prop_cache = {} # stores key, value of prop.as_pointer, prop
|
||||
|
||||
class SN_SimpleProperty(BasicProperty, bpy.types.PropertyGroup):
|
||||
|
||||
expand: bpy.props.BoolProperty(default=True, name="Expand", description="Expand this property")
|
||||
|
||||
@property
|
||||
def group_prop_parent(self):
|
||||
""" Returns the parent of the property collection this property lives in """
|
||||
if self.id_data.bl_rna.identifier == "ScriptingNodesTree":
|
||||
# find property in nodes to return
|
||||
if not str(self.as_pointer()) in _group_prop_cache:
|
||||
for node in self.id_data.nodes:
|
||||
if hasattr(node, "properties"):
|
||||
for prop in node.properties:
|
||||
if prop.property_type == "Group":
|
||||
for subprop in prop.settings.properties:
|
||||
if subprop == self:
|
||||
_group_prop_cache[str(self.as_pointer())] = prop
|
||||
break
|
||||
return _group_prop_cache[str(self.as_pointer())]
|
||||
|
||||
else:
|
||||
coll_path = "[".join(repr(self.path_resolve("name", False)).split("[")[:-1])
|
||||
parent_path = coll_path.split("stngs_group")[0][:-1]
|
||||
return eval(parent_path)
|
||||
|
||||
@property
|
||||
def python_name(self):
|
||||
return super().python_name[4:] # cut of sna_ for props in prop group (mainly for name prop)
|
||||
|
||||
def compile(self, context=None):
|
||||
self.group_prop_parent.compile()
|
||||
|
||||
|
||||
|
||||
class SN_PT_GroupProperty(PropertySettings, bpy.types.PropertyGroup):
|
||||
|
||||
type_description = "Group properties can hold multiple other properties.\n" \
|
||||
+ "They are used in combination with a pointer or collection property.\n" \
|
||||
+ "Use a property called 'Name' to find properties in a collection.\n" \
|
||||
+ "\n" \
|
||||
+ "A common use for group properties is to group your addons settings together."
|
||||
|
||||
copy_attributes = []
|
||||
|
||||
def copy(self, new_settings):
|
||||
for prop in self.properties:
|
||||
new_prop = new_settings.properties.add()
|
||||
prop.match_settings(new_prop)
|
||||
|
||||
|
||||
def draw(self, context, layout):
|
||||
""" Draws the settings for this property type """
|
||||
row = layout.row()
|
||||
row.scale_y = 1.2
|
||||
op = row.operator("sn.add_property_item", text="Add Property", icon="ADD")
|
||||
op.group_data_path = f"{self.prop.full_prop_path}"
|
||||
|
||||
for i, prop in enumerate(self.properties):
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
subrow = row.row()
|
||||
subrow.prop(prop, "expand", text="", icon="DISCLOSURE_TRI_DOWN" if prop.expand else "DISCLOSURE_TRI_RIGHT", emboss=False)
|
||||
row.prop(prop, "name", text="")
|
||||
|
||||
subrow = row.row(align=True)
|
||||
subcol = subrow.column(align=True)
|
||||
subcol.enabled = i > 0
|
||||
op = subcol.operator("sn.move_group_property", text="", icon="TRIA_UP")
|
||||
op.group_items_path = f"{self.prop.full_prop_path}.settings.properties"
|
||||
op.index = i
|
||||
op.move_up = True
|
||||
subcol = subrow.column(align=True)
|
||||
subcol.enabled = i < len(self.properties)-1
|
||||
op = subcol.operator("sn.move_group_property", text="", icon="TRIA_DOWN")
|
||||
op.group_items_path = f"{self.prop.full_prop_path}.settings.properties"
|
||||
op.index = i
|
||||
op.move_up = False
|
||||
|
||||
op = row.operator("sn.remove_group_property", text="", icon="TRASH", emboss=False)
|
||||
op.group_items_path = f"{self.prop.full_prop_path}.settings.properties"
|
||||
op.index = i
|
||||
|
||||
row.operator("sn.copy_python_name", text="", icon="COPYDOWN", emboss=False).name = "PROP_PATH_PLACEHOLDER."+prop.python_name
|
||||
|
||||
if prop.expand:
|
||||
prop.draw(context, box)
|
||||
box.separator()
|
||||
prop.settings.draw(context, box)
|
||||
|
||||
|
||||
@property
|
||||
def prop_type_name(self):
|
||||
return f"SNA_GROUP_{self.prop.python_name}"
|
||||
|
||||
|
||||
@property
|
||||
def register_options(self):
|
||||
return f""
|
||||
|
||||
|
||||
def imperative_code(self):
|
||||
code = f"class {self.prop_type_name}(bpy.types.PropertyGroup):\n\n"
|
||||
for prop in self.properties:
|
||||
for line in prop.register_code.split("\n"):
|
||||
code += " "*4 + line + "\n"
|
||||
code += "\n"
|
||||
if not len(self.properties):
|
||||
code += " "*4 + "pass\n\n"
|
||||
return code
|
||||
|
||||
|
||||
properties: bpy.props.CollectionProperty(type=SN_SimpleProperty)
|
||||
@@ -0,0 +1,172 @@
|
||||
import bpy
|
||||
from .settings import PropertySettings
|
||||
|
||||
|
||||
|
||||
class SN_PT_IntegerProperty(PropertySettings, bpy.types.PropertyGroup):
|
||||
|
||||
type_description = "Integer properties can hold decimal number.\n" \
|
||||
+ "They can also be turned into a vector which holds multiple of these.\n" \
|
||||
+ "\n" \
|
||||
+ "Integers are displayed as number inputs."
|
||||
|
||||
copy_attributes = ["is_vector", "default", "subtype", "use_min", "min", "use_max", "use_soft_min",
|
||||
"soft_min", "max", "use_soft_max", "soft_max", "step", "size", "vector_default"]
|
||||
|
||||
def draw(self, context, layout):
|
||||
""" Draws the settings for this property type """
|
||||
layout.prop(self, "subtype")
|
||||
row = layout.row()
|
||||
row.enabled = not self.is_vector
|
||||
row.prop(self, "default")
|
||||
|
||||
layout.separator()
|
||||
row = layout.row(heading="Minimum")
|
||||
row.prop(self, "use_min", text="")
|
||||
sub_row = row.row()
|
||||
sub_row.enabled = self.use_min
|
||||
sub_row.prop(self, "min")
|
||||
|
||||
row = layout.row(heading="Maximum")
|
||||
row.prop(self, "use_max", text="")
|
||||
sub_row = row.row()
|
||||
sub_row.enabled = self.use_max
|
||||
sub_row.prop(self, "max")
|
||||
|
||||
row = layout.row(heading="Soft Minimum")
|
||||
row.prop(self, "use_soft_min", text="")
|
||||
sub_row = row.row()
|
||||
sub_row.enabled = self.use_soft_min
|
||||
sub_row.prop(self, "soft_min")
|
||||
|
||||
row = layout.row(heading="Soft Maximum")
|
||||
row.prop(self, "use_soft_max", text="")
|
||||
sub_row = row.row()
|
||||
sub_row.enabled = self.use_soft_max
|
||||
sub_row.prop(self, "soft_max")
|
||||
|
||||
layout.separator()
|
||||
layout.prop(self, "is_vector")
|
||||
col = layout.column()
|
||||
col.enabled = self.is_vector
|
||||
col.prop(self, "size")
|
||||
|
||||
row = col.row()
|
||||
split = row.split(factor=0.4)
|
||||
split.alignment = "RIGHT"
|
||||
split.label(text="Default")
|
||||
sub_col = split.column(align=True)
|
||||
for i in range(self.size):
|
||||
sub_col.prop(self, "vector_default", index=i, text="")
|
||||
|
||||
|
||||
@property
|
||||
def prop_type_name(self):
|
||||
if self.is_vector:
|
||||
return "IntVectorProperty"
|
||||
return "IntProperty"
|
||||
|
||||
|
||||
@property
|
||||
def register_options(self):
|
||||
if self.is_vector:
|
||||
options = f"size={self.size}, default={tuple(list(self.vector_default)[:self.size])}"
|
||||
else:
|
||||
options = f"default={self.default}"
|
||||
options += f", subtype='{self.subtype}'"
|
||||
if self.use_min: options += f", min={self.min}"
|
||||
if self.use_soft_min: options += f", soft_min={self.soft_min}"
|
||||
if self.use_max: options += f", max={self.max}"
|
||||
if self.use_soft_max: options += f", soft_max={self.soft_max}"
|
||||
return options + self.update_option
|
||||
|
||||
|
||||
default: bpy.props.IntProperty(name="Default",
|
||||
description="Default value of this property (This may not reset automatically for existing attached items)",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
|
||||
def get_subtype_items(self, context):
|
||||
items = [("NONE", "None", "No subtype, just a default float input"),
|
||||
("PIXEL", "Pixel", "Pixel"),
|
||||
("UNSIGNED", "Unsigned", "Unsigned"),
|
||||
("PERCENTAGE", "Percentage", "Percentage"),
|
||||
("FACTOR", "Factor", "Factor"),
|
||||
("ANGLE", "Angle", "Angle"),
|
||||
("TIME", "Time", "Time"),
|
||||
("DISTANCE", "Distance", "Distance"),
|
||||
("DISTANCE_CAMERA", "Distance Camera", "Distance Camera"),
|
||||
("POWER", "Power", "Power"),
|
||||
("TEMPERATURE", "Temperature", "Temperature")]
|
||||
if self.is_vector:
|
||||
items = [("NONE", "None", "No subtype, just a default float vector input"),
|
||||
("COLOR", "Color", "Color"),
|
||||
("TRANSLATION", "Translation", "Translation"),
|
||||
("DIRECTION", "Direction", "Direction"),
|
||||
("VELOCITY", "Velocity", "Velocity"),
|
||||
("ACCELERATION", "Acceleration", "Acceleration"),
|
||||
("MATRIX", "Matrix", "Matrix"),
|
||||
("EULER", "Euler", "Euler"),
|
||||
("QUATERNION", "Quaternion", "Quaternion"),
|
||||
("AXISANGLE", "Axisangle", "Axisangle"),
|
||||
("XYZ", "XYZ", "XYZ"),
|
||||
("XYZ_LENGTH", "XYZ Length", "XYZ Length"),
|
||||
("COLOR_GAMMA", "Color Gamma", "Color Gamma"),
|
||||
("COORDINATES", "Coordinates", "Coordinates"),
|
||||
("LAYER", "Layer", "Layer"),
|
||||
("LAYER_MEMBER", "Layer Member", "Layer Member"),]
|
||||
return items
|
||||
|
||||
subtype: bpy.props.EnumProperty(name="Subtype",
|
||||
description="The subtype of this property. This changes how the property is displayed",
|
||||
update=PropertySettings.compile,
|
||||
items=get_subtype_items)
|
||||
|
||||
|
||||
use_min: bpy.props.BoolProperty(name="Minimum",
|
||||
description="Use a minimum property value",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
min: bpy.props.IntProperty(name="Minimum", default=-0,
|
||||
description="The minimum value of this property",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
use_max: bpy.props.BoolProperty(name="Maximum",
|
||||
description="Use a maximum property value",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
use_soft_min: bpy.props.BoolProperty(name="Soft Minimum",
|
||||
description="Use a soft minimum property value",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
soft_min: bpy.props.IntProperty(name="Soft Minimum", default=-0,
|
||||
description="The soft minimum value of this property",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
max: bpy.props.IntProperty(name="Maximum", default=1,
|
||||
description="The maximum value of this property",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
use_soft_max: bpy.props.BoolProperty(name="Soft Maximum",
|
||||
description="Use a soft maximum property value",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
soft_max: bpy.props.IntProperty(name="Soft Maximum", default=1,
|
||||
description="The soft maximum value of this property",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
def update_vector(self, context):
|
||||
self.prop.trigger_reference_update(context)
|
||||
|
||||
is_vector: bpy.props.BoolProperty(name="Is Vector",
|
||||
description="If this property is a vector",
|
||||
update=update_vector)
|
||||
|
||||
size: bpy.props.IntProperty(name="Vector Size", min=2, max=32, default=3,
|
||||
description="Length of the vector property",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
vector_default: bpy.props.IntVectorProperty(name="Default",
|
||||
description="Default value of this property (This may not reset automatically for existing attached items)",
|
||||
size=32,
|
||||
update=PropertySettings.compile)
|
||||
@@ -0,0 +1,75 @@
|
||||
import bpy
|
||||
from .settings import PropertySettings, id_items
|
||||
|
||||
|
||||
|
||||
class SN_PT_PointerProperty(PropertySettings, bpy.types.PropertyGroup):
|
||||
|
||||
type_description = "Pointer properties can point to specific types of blend data or property groups.\n" \
|
||||
+ "\n" \
|
||||
+ "They are often used to point to your addons settings, which could live grouped\n" \
|
||||
+ "in a property group and be attached to the scene.\n" \
|
||||
+ "\n" \
|
||||
+ "When used with blend data, you can use pointers to let the user select the data\n" \
|
||||
+ "from a dropdown and get the blend data from the property."
|
||||
|
||||
copy_attributes = ["data_type", "use_prop_group", "prop_group"]
|
||||
|
||||
|
||||
def draw(self, context, layout):
|
||||
""" Draws the settings for this property type """
|
||||
src = context.scene.sn
|
||||
layout.prop(self, "use_prop_group")
|
||||
if not self.use_prop_group:
|
||||
layout.prop(self, "data_type")
|
||||
else:
|
||||
layout.prop_search(self, "prop_group", src, "properties")
|
||||
row = layout.row()
|
||||
row.alert = True
|
||||
if self.prop_group and self.prop_group in src.properties:
|
||||
if not src.properties[self.prop_group].property_type == "Group":
|
||||
row.label(text="The selected property is not a group!", icon="ERROR")
|
||||
elif hasattr(self.prop, "group_prop_parent") and self.prop.group_prop_parent.name == self.prop_group:
|
||||
row.label(text="Can't use self reference for this collection!", icon="ERROR")
|
||||
else:
|
||||
row.label(text="There is no valid property group selected!", icon="ERROR")
|
||||
|
||||
|
||||
@property
|
||||
def prop_type_name(self):
|
||||
return "PointerProperty"
|
||||
|
||||
|
||||
@property
|
||||
def register_options(self):
|
||||
if not self.use_prop_group:
|
||||
data_type = "bpy.types."+self.data_type
|
||||
else:
|
||||
src = self.prop.prop_collection_origin
|
||||
data_type = "bpy.types.Scene"
|
||||
if self.prop_group in src.properties and src.properties[self.prop_group].property_type == "Group":
|
||||
if not hasattr(self.prop, "group_prop_parent") or (hasattr(self.prop, "group_prop_parent") and self.prop.group_prop_parent.name != self.prop_group):
|
||||
data_type = f"SNA_GROUP_{bpy.context.scene.sn.properties[self.prop_group].python_name}"
|
||||
return f"type={data_type}{self.update_option}"
|
||||
|
||||
|
||||
def get_data_items(self, context):
|
||||
items = []
|
||||
for item in id_items:
|
||||
items.append((item, item, item))
|
||||
return items
|
||||
|
||||
data_type: bpy.props.EnumProperty(name="Data Type",
|
||||
description="The type of blend data to have this property point to",
|
||||
items=get_data_items,
|
||||
update=PropertySettings.compile)
|
||||
|
||||
|
||||
use_prop_group: bpy.props.BoolProperty(name="Use Property Group",
|
||||
description="Point to a custom property group you created",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
|
||||
prop_group: bpy.props.StringProperty(name="Property Group",
|
||||
description="The property group you want to point to",
|
||||
update=PropertySettings.compile)
|
||||
@@ -0,0 +1,149 @@
|
||||
import bpy
|
||||
|
||||
|
||||
|
||||
id_items = ["Scene", "Action", "Armature", "Brush", "CacheFile", "Camera",
|
||||
"Collection", "Curve", "FreestyleLineStyle", "GreasePencil",
|
||||
"Image", "Key", "Lattice", "Library", "Light", "LightProbe",
|
||||
"Mask", "Material", "Mesh", "MetaBall", "MovieClip", "NodeTree",
|
||||
"Object", "PaintCurve", "Palette", "ParticleSettings",
|
||||
"Screen", "Sound", "Speaker", "Text", "Texture", "VectorFont",
|
||||
"Volume", "WindowManager", "WorkSpace", "World"]
|
||||
|
||||
id_data = {"Scene": "scenes", "Action":"actions", "Armature":"armatures",
|
||||
"Brush":"bruhes", "CacheFile":"cache_files", "Camera":"cameras",
|
||||
"Collection":"collections", "Curve":"curves", "FreestyleLineStyle":"linestyles",
|
||||
"GreasePencil":"grease_pencils", "Image": "images", "Key": "shape_keys",
|
||||
"Lattice": "lattices", "Library": "libraries", "Light": "lights",
|
||||
"LightProbe": "lightprobes", "Mask": "masks", "Material": "materials",
|
||||
"Mesh": "meshes", "MetaBall": "metaballs", "MovieClip": "movieclips",
|
||||
"NodeTree": "node_groups", "Object": "objects", "PaintCurve": "paint_curves",
|
||||
"Palette": "palettes", "ParticleSettings": "particles", "Screen": "screens",
|
||||
"Sound": "sounds", "Speaker": "speakers", "Text": "texts", "Texture": "textures",
|
||||
"VectorFont": "fonts", "Volume": "volumes", "WindowManager": "window_managers",
|
||||
"WorkSpace": "workspaces", "World": "worlds"}
|
||||
|
||||
|
||||
|
||||
property_icons = {
|
||||
"String": "SYNTAX_OFF",
|
||||
"Boolean": "FORCE_CHARGE",
|
||||
"Boolean Vector": "FORCE_CHARGE",
|
||||
"Float": "CON_TRANSLIKE",
|
||||
"Float Vector": "CON_TRANSLIKE",
|
||||
"Integer": "DRIVER_TRANSFORM",
|
||||
"Integer Vector": "DRIVER_TRANSFORM",
|
||||
"Enum": "PRESET",
|
||||
"Enum Set": "PRESET",
|
||||
"Pointer": "MONKEY",
|
||||
"Property": "MONKEY",
|
||||
"Collection": "ASSET_MANAGER",
|
||||
"Collection Property": "ASSET_MANAGER",
|
||||
"Group": "FILEBROWSER",
|
||||
"List": "LONGDISPLAY",
|
||||
"Data": "OBJECT_DATA",
|
||||
"Icon": "DRIVER_TRANSFORM",
|
||||
|
||||
"Function": "FILE_SCRIPT",
|
||||
"Built In Function": "SCRIPTPLUGINS",
|
||||
}
|
||||
|
||||
|
||||
|
||||
property_socket = {
|
||||
"String": "String",
|
||||
"Boolean": "Boolean",
|
||||
"Float": "Float",
|
||||
"Integer": "Integer",
|
||||
"Enum": "Enum",
|
||||
"Pointer": "Property",
|
||||
"Collection": "Collection Property",
|
||||
"Group": "Data",
|
||||
}
|
||||
|
||||
def prop_to_socket(prop):
|
||||
socket_name = property_socket[prop.property_type]
|
||||
if getattr(prop.settings, "enum_flag", False):
|
||||
socket_name = "Enum Set"
|
||||
if getattr(prop.settings, "is_vector", False):
|
||||
socket_name += " Vector"
|
||||
return socket_name
|
||||
|
||||
|
||||
_prop_cache = {} # stores key, value of settings.as_pointer with prop for settings
|
||||
|
||||
class PropertySettings:
|
||||
|
||||
dummy: bpy.props.StringProperty(name="DUMMY", description="Dummy prop for resolving path")
|
||||
|
||||
copy_attributes = []
|
||||
|
||||
def copy(self, new_settings): pass
|
||||
|
||||
@property
|
||||
def prop(self):
|
||||
""" Returns the property these settings belong to """
|
||||
if self.id_data.bl_rna.identifier == "ScriptingNodesTree":
|
||||
# find property in nodes to return
|
||||
if not str(self.as_pointer()) in _prop_cache:
|
||||
for node in self.id_data.nodes:
|
||||
if hasattr(node, "properties"):
|
||||
for prop in node.properties:
|
||||
if prop.settings == self:
|
||||
_prop_cache[str(self.as_pointer())] = prop
|
||||
break
|
||||
elif prop.property_type == "Group":
|
||||
for subprop in prop.settings.properties:
|
||||
if subprop.settings == self:
|
||||
_prop_cache[str(self.as_pointer())] = subprop
|
||||
break
|
||||
return _prop_cache[str(self.as_pointer())]
|
||||
else:
|
||||
path = ".".join(repr(self.path_resolve("dummy", False)).split(".")[:-2])
|
||||
prop = eval(path)
|
||||
return prop
|
||||
|
||||
def compile(self, context=None):
|
||||
""" Compile the property for these settings """
|
||||
self.prop.compile()
|
||||
|
||||
def imperative_code(self):
|
||||
return self.update_function
|
||||
|
||||
def _update_function_names(self):
|
||||
""" Returns the code for the on property update function """
|
||||
updates = []
|
||||
for ntree in bpy.data.node_groups:
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
for node in ntree.node_collection("SN_OnPropertyUpdateNode").nodes:
|
||||
prop_src = node.get_prop_source()
|
||||
if prop_src and node.prop_name in prop_src.properties:
|
||||
prop = prop_src.properties[node.prop_name]
|
||||
if prop.name == self.prop.name:
|
||||
updates.append((node.update_func_name(prop), node.order))
|
||||
return list(map(lambda item: item[0], sorted(updates, key=lambda i: i[1])))
|
||||
|
||||
|
||||
@property
|
||||
def update_function(self):
|
||||
""" Returns the code for the update function """
|
||||
update_names = self._update_function_names()
|
||||
if len(update_names) < 2:
|
||||
return ""
|
||||
else:
|
||||
code = f"def sna_update_{self.prop.python_name}(self, context):\n"
|
||||
for func in update_names:
|
||||
code += " "*4 + f"{func}(self, context)\n"
|
||||
return code
|
||||
|
||||
|
||||
@property
|
||||
def update_option(self):
|
||||
""" Returns the code for the update function option """
|
||||
update_names = self._update_function_names()
|
||||
if len(update_names) == 0 or self.prop.property_type in ["Group", "Collection"]:
|
||||
return ""
|
||||
elif len(update_names) == 1:
|
||||
return f", update={update_names[0]}"
|
||||
return f", update=sna_update_{self.prop.python_name}"
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import operator
|
||||
import bpy
|
||||
|
||||
|
||||
|
||||
class SN_OT_AddGenerateItemsNode(bpy.types.Operator):
|
||||
bl_idname = "sn.add_generate_items_node"
|
||||
bl_label = "Generate Items"
|
||||
bl_description = "Adds a node to generate dynamic enum items"
|
||||
bl_options = {"REGISTER", "INTERNAL", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.node.add_node("INVOKE_DEFAULT", type="SN_GenerateEnumItemsNode", use_transform=True)
|
||||
node = context.space_data.node_tree.nodes.active
|
||||
if context.scene.sn.property_index < len(context.scene.sn.properties):
|
||||
prop = context.scene.sn.properties[context.scene.sn.property_index]
|
||||
node.prop_name = prop.name
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,54 @@
|
||||
import bpy
|
||||
from .settings import PropertySettings
|
||||
|
||||
|
||||
|
||||
class SN_PT_StringProperty(PropertySettings, bpy.types.PropertyGroup):
|
||||
|
||||
type_description = "String properties can hold a line of text.\n" \
|
||||
+ "\n" \
|
||||
+ "String properties are displayed as text inputs in the UI. \n" \
|
||||
+ "There are subtypes to add a file selector to the string property."
|
||||
|
||||
copy_attributes = ["default", "subtype", "maxlen"]
|
||||
|
||||
|
||||
def draw(self, context, layout):
|
||||
""" Draws the settings for this property type """
|
||||
layout.prop(self, "subtype")
|
||||
layout.prop(self, "default")
|
||||
layout.separator()
|
||||
layout.prop(self, "maxlen")
|
||||
|
||||
|
||||
@property
|
||||
def prop_type_name(self):
|
||||
return "StringProperty"
|
||||
|
||||
|
||||
@property
|
||||
def register_options(self):
|
||||
return f"default='{self.default}', subtype='{self.subtype}', maxlen={self.maxlen}{self.update_option}"
|
||||
|
||||
|
||||
default: bpy.props.StringProperty(name="Default",
|
||||
description="Default value of this property (This may not reset automatically for existing attached items)",
|
||||
update=PropertySettings.compile)
|
||||
|
||||
|
||||
subtype: bpy.props.EnumProperty(name="Subtype",
|
||||
description="The subtype of this property. This changes how the property is displayed",
|
||||
update=PropertySettings.compile,
|
||||
items=[("NONE", "None", "No subtype, just a default string input"),
|
||||
("FILE_PATH", "File Path", "Display this property as a file path"),
|
||||
("DIR_PATH", "Directory Path", "Display this property as a directory path"),
|
||||
# ("FILE_NAME", "File Name", "Display that property as a file name"),
|
||||
("BYTE_STRING", "Byte String", "Stores the string as a UTF-8 encoded byte string"),
|
||||
("PASSWORD", "Password", "Displays asterisks in the UI to hide the typed string")])
|
||||
|
||||
|
||||
maxlen: bpy.props.IntProperty(name="Max Length",
|
||||
description="The maximum length of the string (0 is unlimited)",
|
||||
min=0,
|
||||
default=0,
|
||||
update=PropertySettings.compile)
|
||||
@@ -0,0 +1,18 @@
|
||||
import bpy
|
||||
|
||||
|
||||
def ntree_variable_register_code(ntree):
|
||||
if len(ntree.variables) == 0: return ""
|
||||
code = f"{ntree.python_name} = {{"
|
||||
for var in ntree.variables:
|
||||
code += f"'{var.python_name}': {var.var_default}, "
|
||||
code += "}\n"
|
||||
return code
|
||||
|
||||
|
||||
def variable_register_code():
|
||||
code = ""
|
||||
for ntree in bpy.data.node_groups:
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
code += ntree_variable_register_code(ntree)
|
||||
return code
|
||||
@@ -0,0 +1,177 @@
|
||||
import bpy
|
||||
from ...nodes.compiler import compile_addon
|
||||
|
||||
|
||||
|
||||
class SN_OT_AddVariable(bpy.types.Operator):
|
||||
bl_idname = "sn.add_variable"
|
||||
bl_label = "Add Variable"
|
||||
bl_description = "Adds a variable to the addon"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
ntree = bpy.data.node_groups[self.node_tree]
|
||||
new_var = ntree.variables.add()
|
||||
new_var.name = "New Variable"
|
||||
ntree.variables.move(len(ntree.variables)-1, ntree.variable_index+1)
|
||||
ntree.variable_index += 1
|
||||
ntree.variable_index = min(ntree.variable_index, len(ntree.variables)-1)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_RemoveVariable(bpy.types.Operator):
|
||||
bl_idname = "sn.remove_variable"
|
||||
bl_label = "Remove Variable"
|
||||
bl_description = "Removes this variable from the addon"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
ntree = bpy.data.node_groups[self.node_tree]
|
||||
ntree.variables.remove(ntree.variable_index)
|
||||
ntree.variable_index -= 1
|
||||
compile_addon()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_MoveVariable(bpy.types.Operator):
|
||||
bl_idname = "sn.move_variable"
|
||||
bl_label = "Move Variable"
|
||||
bl_description = "Moves this variable"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
move_up: bpy.props.BoolProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
ntree = bpy.data.node_groups[self.node_tree]
|
||||
if self.move_up:
|
||||
ntree.variables.move(ntree.variable_index, ntree.variable_index - 1)
|
||||
ntree.variable_index -= 1
|
||||
else:
|
||||
ntree.variables.move(ntree.variable_index, ntree.variable_index + 1)
|
||||
ntree.variable_index += 1
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_AddVariableNodePopup(bpy.types.Operator):
|
||||
bl_idname = "sn.add_variable_node_popup"
|
||||
bl_label = "Add Variable Node Popup"
|
||||
bl_description = "Opens a popup to let you choose a variable node"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column(align=True)
|
||||
col.scale_y = 1.5
|
||||
op = col.operator("sn.add_variable_node", text="Get Variable", icon="ADD")
|
||||
op.type = "SN_GetVariableNode"
|
||||
op.node_tree = self.node_tree
|
||||
op = col.operator("sn.add_variable_node", text="Set Variable", icon="ADD")
|
||||
op.type = "SN_SetVariableNode"
|
||||
op.node_tree = self.node_tree
|
||||
op = col.operator("sn.add_variable_node", text="Reset Variable", icon="ADD")
|
||||
op.type = "SN_ResetVariableNode"
|
||||
op.node_tree = self.node_tree
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_popup(self)
|
||||
|
||||
|
||||
|
||||
class SN_OT_AddVariableNode(bpy.types.Operator):
|
||||
bl_idname = "sn.add_variable_node"
|
||||
bl_label = "Add Variable Node"
|
||||
bl_description = "Adds this node to the editor"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
type: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.node.add_node("INVOKE_DEFAULT", type=self.type, use_transform=True)
|
||||
node = context.space_data.node_tree.nodes.active
|
||||
ntree = bpy.data.node_groups[self.node_tree]
|
||||
|
||||
if ntree.variable_index < len(ntree.variables):
|
||||
var = ntree.variables[ntree.variable_index]
|
||||
node.ref_ntree = ntree
|
||||
node.var_name = var.name
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_FindVariable(bpy.types.Operator):
|
||||
bl_idname = "sn.find_variable"
|
||||
bl_label = "Find Variable"
|
||||
bl_description = "Finds this variable in the addon"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
ntree = bpy.data.node_groups[self.node_tree]
|
||||
|
||||
# init variable nodes
|
||||
empty_nodes = []
|
||||
variable_nodes = []
|
||||
variable = None
|
||||
if ntree.variable_index < len(ntree.variables):
|
||||
variable = ntree.variables[ntree.variable_index]
|
||||
|
||||
# find variable nodes
|
||||
for ngroup in bpy.data.node_groups:
|
||||
if ngroup.bl_idname == "ScriptingNodesTree":
|
||||
for node in ngroup.nodes:
|
||||
if hasattr(node, "var_name") and hasattr(node, "ref_ntree"):
|
||||
if variable and node.var_name == variable.name and node.ref_ntree == ntree:
|
||||
variable_nodes.append(node)
|
||||
elif not node.var_name or not node.ref_ntree:
|
||||
empty_nodes.append(node)
|
||||
|
||||
# draw nodes for selected variable
|
||||
if ntree.variable_index < len(ntree.variables):
|
||||
col = layout.column()
|
||||
row = col.row()
|
||||
row.enabled = False
|
||||
row.label(text=f"Variable: {variable.name}")
|
||||
|
||||
for node in variable_nodes:
|
||||
op = col.operator("sn.find_node", text=node.name, icon="RESTRICT_SELECT_OFF")
|
||||
op.node_tree = node.node_tree.name
|
||||
op.node = node.name
|
||||
|
||||
if not variable_nodes:
|
||||
col.label(text="No nodes found for this variable", icon="INFO")
|
||||
|
||||
# draw nodes with empty variable
|
||||
col = layout.column()
|
||||
row = col.row()
|
||||
row.label(text="Empty Variable Nodes")
|
||||
row.enabled = False
|
||||
|
||||
for node in empty_nodes:
|
||||
op = col.operator("sn.find_node", text=node.name, icon="RESTRICT_SELECT_OFF")
|
||||
op.node_tree = node.node_tree.name
|
||||
op.node = node.name
|
||||
|
||||
if not empty_nodes:
|
||||
col.label(text="No empty variable nodes found", icon="INFO")
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_popup(self, width=250)
|
||||
@@ -0,0 +1,126 @@
|
||||
import bpy
|
||||
from ...utils import get_python_name, unique_collection_name
|
||||
from ..properties.settings.settings import property_icons
|
||||
from ...nodes.compiler import compile_addon
|
||||
|
||||
|
||||
|
||||
class SN_VariableProperties(bpy.types.PropertyGroup):
|
||||
|
||||
|
||||
@property
|
||||
def node_tree(self):
|
||||
return self.id_data
|
||||
|
||||
|
||||
# cache python names so they only have to be generated once
|
||||
cached_python_names = {}
|
||||
cached_python_name: bpy.props.StringProperty()
|
||||
cached_human_name: bpy.props.StringProperty()
|
||||
|
||||
@property
|
||||
def python_name(self):
|
||||
if self.name == self.cached_human_name and self.cached_python_name: return self.cached_python_name
|
||||
if self.name in self.cached_python_names: return self.cached_python_names[self.name]
|
||||
|
||||
names = []
|
||||
for var in self.node_tree.variables:
|
||||
if var == self:
|
||||
break
|
||||
names.append(var.python_name)
|
||||
|
||||
name = unique_collection_name(f"sna_{get_python_name(self.name, 'sna_new_variable')}", "sna_new_variable", names, "_")
|
||||
try:
|
||||
self.cached_python_name = name
|
||||
self.cached_human_name = self.name
|
||||
except AttributeError: pass
|
||||
self.cached_python_names[self.name] = name
|
||||
return name
|
||||
|
||||
|
||||
@property
|
||||
def data_path(self):
|
||||
return f"{self.node_tree.python_name}['{self.python_name}']"
|
||||
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
return property_icons[self.variable_type]
|
||||
|
||||
|
||||
def compile(self, context=None):
|
||||
""" Registers the variable and unregisters previous version """
|
||||
# print(f"Serpens Log: Variable {self.name} received an update")
|
||||
compile_addon()
|
||||
|
||||
|
||||
def get_name(self):
|
||||
return self.get("name", "Variable Default")
|
||||
|
||||
|
||||
def get_to_update_nodes(self):
|
||||
to_update_nodes = []
|
||||
for ntree in bpy.data.node_groups:
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
for node in ntree.nodes:
|
||||
if getattr(node, "var_name", None) == self.name:
|
||||
to_update_nodes.append(node)
|
||||
return to_update_nodes
|
||||
|
||||
def set_name(self, value):
|
||||
names = list(map(lambda item: item.name, list(filter(lambda item: item!=self, self.node_tree.variables))))
|
||||
value = unique_collection_name(value, "New Variable", names, " ")
|
||||
to_update = self.get_to_update_nodes()
|
||||
|
||||
# set value
|
||||
self["name"] = value
|
||||
self.compile()
|
||||
|
||||
# update node references
|
||||
for node in to_update:
|
||||
node.var_name = value
|
||||
|
||||
name: bpy.props.StringProperty(name="Variable Name",
|
||||
description="Name of this variable",
|
||||
default="Variable Default",
|
||||
get=get_name,
|
||||
set=set_name,
|
||||
update=compile)
|
||||
|
||||
|
||||
def update_variable_type(self, context):
|
||||
for node in self.get_to_update_nodes():
|
||||
if hasattr(node, "on_var_changed"):
|
||||
node.on_var_changed()
|
||||
self.compile()
|
||||
|
||||
variable_type: bpy.props.EnumProperty(name="Type",
|
||||
description="The type of data this variable stores",
|
||||
update=update_variable_type,
|
||||
items=[("Data", "Data", "Stores any type of data", property_icons["Data"], 0),
|
||||
("String", "String", "Stores a string of characters", property_icons["String"], 1),
|
||||
("Boolean", "Boolean", "Stores True or False", property_icons["Boolean"], 2),
|
||||
("Float", "Float", "Stores a decimal number", property_icons["Float"], 3),
|
||||
("Integer", "Integer", "Stores an integer number", property_icons["Integer"], 4),
|
||||
("List", "List", "Stores a list of data", property_icons["List"], 5),
|
||||
("Pointer", "Pointer", "Stores a reference to certain types of blend data, collection or group properties", property_icons["Pointer"], 6),
|
||||
("Collection", "Collection", "Stores a list of certain blend data or property groups to be displayed in lists", property_icons["Collection"], 7)])
|
||||
|
||||
string_default: bpy.props.StringProperty(name="Default", description="Default value for the variable", update=compile)
|
||||
boolean_default: bpy.props.BoolProperty(name="Default", description="Default value for the variable", update=compile)
|
||||
float_default: bpy.props.FloatProperty(name="Default", description="Default value for the variable", update=compile)
|
||||
integer_default: bpy.props.IntProperty(name="Default", description="Default value for the variable", update=compile)
|
||||
|
||||
|
||||
@property
|
||||
def var_default(self):
|
||||
return {
|
||||
"Data": None,
|
||||
"String": f"'{self.string_default}'",
|
||||
"Boolean": self.boolean_default,
|
||||
"Float": self.float_default,
|
||||
"Integer": self.integer_default,
|
||||
"List": [],
|
||||
"Pointer": None,
|
||||
"Collection": None,
|
||||
}[self.variable_type]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,163 @@
|
||||
import os
|
||||
import bpy
|
||||
import sys
|
||||
import typing
|
||||
import inspect
|
||||
import pkgutil
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
|
||||
__all__ = (
|
||||
"init",
|
||||
"register",
|
||||
"unregister",
|
||||
)
|
||||
|
||||
blender_version = bpy.app.version
|
||||
|
||||
modules = None
|
||||
ordered_classes = None
|
||||
|
||||
def init():
|
||||
global modules
|
||||
global ordered_classes
|
||||
|
||||
modules = get_all_submodules(Path(__file__).parent)
|
||||
ordered_classes = get_ordered_classes_to_register(modules)
|
||||
|
||||
def register():
|
||||
for cls in ordered_classes:
|
||||
isContextMenu = getattr(cls, "__name__", None) == "WM_MT_button_context"
|
||||
if not isContextMenu or (isContextMenu and getattr(bpy.types, "WM_MT_button_context", None) == None):
|
||||
try: bpy.utils.register_class(cls)
|
||||
except: pass
|
||||
|
||||
for module in modules:
|
||||
if module.__name__ == __name__:
|
||||
continue
|
||||
if hasattr(module, "register"):
|
||||
module.register()
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(ordered_classes):
|
||||
isContextMenu = getattr(cls, "__name__", None) == "WM_MT_button_context"
|
||||
if not isContextMenu:
|
||||
try: bpy.utils.unregister_class(cls)
|
||||
except: pass
|
||||
|
||||
for module in modules:
|
||||
if module.__name__ == __name__:
|
||||
continue
|
||||
if hasattr(module, "unregister"):
|
||||
module.unregister()
|
||||
|
||||
|
||||
# Import modules
|
||||
#################################################
|
||||
|
||||
def get_all_submodules(directory):
|
||||
return list(iter_submodules(directory, directory.name))
|
||||
|
||||
def iter_submodules(path, package_name):
|
||||
for name in sorted(iter_submodule_names(path)):
|
||||
yield importlib.import_module("." + name, package_name)
|
||||
|
||||
def iter_submodule_names(path, root=""):
|
||||
for _, module_name, is_package in pkgutil.iter_modules([str(path)]):
|
||||
if is_package:
|
||||
sub_path = path / module_name
|
||||
sub_root = root + module_name + "."
|
||||
yield from iter_submodule_names(sub_path, sub_root)
|
||||
else:
|
||||
yield root + module_name
|
||||
|
||||
|
||||
# Find classes to register
|
||||
#################################################
|
||||
|
||||
def get_ordered_classes_to_register(modules):
|
||||
return toposort(get_register_deps_dict(modules))
|
||||
|
||||
def get_register_deps_dict(modules):
|
||||
my_classes = set(iter_my_classes(modules))
|
||||
my_classes_by_idname = {cls.bl_idname : cls for cls in my_classes if hasattr(cls, "bl_idname")}
|
||||
|
||||
deps_dict = {}
|
||||
for cls in my_classes:
|
||||
deps_dict[cls] = set(iter_my_register_deps(cls, my_classes, my_classes_by_idname))
|
||||
return deps_dict
|
||||
|
||||
def iter_my_register_deps(cls, my_classes, my_classes_by_idname):
|
||||
yield from iter_my_deps_from_annotations(cls, my_classes)
|
||||
yield from iter_my_deps_from_parent_id(cls, my_classes_by_idname)
|
||||
|
||||
def iter_my_deps_from_annotations(cls, my_classes):
|
||||
for value in typing.get_type_hints(cls, {}, {}).values():
|
||||
dependency = get_dependency_from_annotation(value)
|
||||
if dependency is not None:
|
||||
if dependency in my_classes:
|
||||
yield dependency
|
||||
|
||||
def get_dependency_from_annotation(value):
|
||||
if blender_version >= (2, 93):
|
||||
if isinstance(value, bpy.props._PropertyDeferred):
|
||||
return value.keywords.get("type")
|
||||
else:
|
||||
if isinstance(value, tuple) and len(value) == 2:
|
||||
if value[0] in (bpy.props.PointerProperty, bpy.props.CollectionProperty):
|
||||
return value[1]["type"]
|
||||
return None
|
||||
|
||||
def iter_my_deps_from_parent_id(cls, my_classes_by_idname):
|
||||
if bpy.types.Panel in cls.__bases__:
|
||||
parent_idname = getattr(cls, "bl_parent_id", None)
|
||||
if parent_idname is not None:
|
||||
parent_cls = my_classes_by_idname.get(parent_idname)
|
||||
if parent_cls is not None:
|
||||
yield parent_cls
|
||||
|
||||
def iter_my_classes(modules):
|
||||
base_types = get_register_base_types()
|
||||
for cls in get_classes_in_modules(modules):
|
||||
if any(base in base_types for base in cls.__bases__):
|
||||
if not getattr(cls, "is_registered", False):
|
||||
yield cls
|
||||
|
||||
def get_classes_in_modules(modules):
|
||||
classes = set()
|
||||
for module in modules:
|
||||
for cls in iter_classes_in_module(module):
|
||||
classes.add(cls)
|
||||
return classes
|
||||
|
||||
def iter_classes_in_module(module):
|
||||
for value in module.__dict__.values():
|
||||
if inspect.isclass(value):
|
||||
yield value
|
||||
|
||||
def get_register_base_types():
|
||||
return set(getattr(bpy.types, name) for name in [
|
||||
"Panel", "Operator", "PropertyGroup",
|
||||
"AddonPreferences", "Header", "Menu",
|
||||
"Node", "NodeSocket", "NodeTree",
|
||||
"UIList", "RenderEngine",
|
||||
"Gizmo", "GizmoGroup",
|
||||
])
|
||||
|
||||
|
||||
# Find order to register to solve dependencies
|
||||
#################################################
|
||||
|
||||
def toposort(deps_dict):
|
||||
sorted_list = []
|
||||
sorted_values = set()
|
||||
while len(deps_dict) > 0:
|
||||
unsorted = []
|
||||
for value, deps in deps_dict.items():
|
||||
if len(deps) == 0:
|
||||
sorted_list.append(value)
|
||||
sorted_values.add(value)
|
||||
else:
|
||||
unsorted.append(value)
|
||||
deps_dict = {value : deps_dict[value] - sorted_values for value in unsorted}
|
||||
return sorted_list
|
||||
@@ -0,0 +1,140 @@
|
||||
import bpy
|
||||
import os
|
||||
import shutil
|
||||
import json
|
||||
import zipfile
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
|
||||
|
||||
|
||||
loaded_packages = [] # temp var for the loaded packages in a file
|
||||
require_reload = False # set to true after a package is installed
|
||||
|
||||
|
||||
|
||||
class SN_OT_InstallPackage(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "sn.install_package"
|
||||
bl_label = "Install Package"
|
||||
bl_description = "Let's you install a package from a zip file"
|
||||
bl_options = {"REGISTER","UNDO","INTERNAL"}
|
||||
|
||||
filter_glob: bpy.props.StringProperty( default='*.zip', options={'HIDDEN'} )
|
||||
|
||||
def extract_zip(self):
|
||||
node_directory = os.path.join(os.path.dirname(os.path.dirname(__file__)),"nodes")
|
||||
names = []
|
||||
with zipfile.ZipFile(self.filepath, 'r') as zip_ref:
|
||||
zip_ref.extractall(node_directory)
|
||||
names = zip_ref.namelist()
|
||||
return names
|
||||
|
||||
def get_package_info(self):
|
||||
info_path = os.path.join(os.path.dirname(os.path.dirname(__file__)),"nodes","package_info.json")
|
||||
package_info = None
|
||||
if os.path.exists(info_path):
|
||||
with open(info_path) as info:
|
||||
package_info = json.loads(info.read())
|
||||
os.remove(info_path)
|
||||
return package_info
|
||||
|
||||
def write_to_installed(self, new_package, extracted):
|
||||
installed_path = os.path.join(os.path.dirname(__file__),"installed.json")
|
||||
if not os.path.exists(installed_path):
|
||||
with open(installed_path, "w") as data_file: data_file.write(json.dumps({ "packages": [], "snippets": [] }))
|
||||
with open(installed_path, "r+") as installed:
|
||||
data = json.loads(installed.read())
|
||||
new_package["nodes"] = extracted
|
||||
data["packages"].append(new_package)
|
||||
installed.seek(0)
|
||||
installed.write(json.dumps(data,indent=4))
|
||||
installed.truncate()
|
||||
|
||||
def execute(self, context):
|
||||
filename, extension = os.path.splitext(self.filepath)
|
||||
if extension == ".zip":
|
||||
extracted_files = self.extract_zip()
|
||||
package_info = self.get_package_info()
|
||||
if not package_info:
|
||||
package_info = {
|
||||
"name": os.path.basename(filename),
|
||||
"author": "Unknown",
|
||||
"description": "",
|
||||
"version": "1.0.0",
|
||||
"wiki": ""}
|
||||
self.write_to_installed(package_info, extracted_files)
|
||||
|
||||
bpy.ops.sn.reload_packages()
|
||||
global require_reload
|
||||
require_reload = True
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_ReloadPackages(bpy.types.Operator):
|
||||
bl_idname = "sn.reload_packages"
|
||||
bl_label = "Reload Packages"
|
||||
bl_description = "Reloads the installed packages list"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
global loaded_packages
|
||||
installed_path = os.path.join(os.path.dirname(__file__),"installed.json")
|
||||
if os.path.exists(installed_path):
|
||||
with open(installed_path, "r") as installed:
|
||||
data = json.loads(installed.read())
|
||||
loaded_packages = data["packages"]
|
||||
else:
|
||||
loaded_packages = []
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_UninstallPackage(bpy.types.Operator):
|
||||
bl_idname = "sn.uninstall_package"
|
||||
bl_label = "Uninstall Package"
|
||||
bl_description = "Uninstalls this package"
|
||||
bl_options = {"REGISTER","UNDO","INTERNAL"}
|
||||
|
||||
index: bpy.props.IntProperty()
|
||||
|
||||
def get_package(self):
|
||||
installed_path = os.path.join(os.path.dirname(__file__),"installed.json")
|
||||
if not os.path.exists(installed_path):
|
||||
with open(installed_path, "w") as data_file: data_file.write(json.dumps({ "packages": [], "snippets": [] }))
|
||||
package = None
|
||||
with open(installed_path,"r+") as installed:
|
||||
data = json.loads(installed.read())
|
||||
package = data["packages"][self.index]
|
||||
data["packages"].pop(self.index)
|
||||
installed.seek(0)
|
||||
installed.write(json.dumps(data,indent=4))
|
||||
installed.truncate()
|
||||
return package
|
||||
|
||||
def remove_nodes(self,nodes):
|
||||
for name in nodes:
|
||||
if not "__init__" in name:
|
||||
path = os.path.join(os.path.dirname(os.path.dirname(__file__)),"nodes",name)
|
||||
if os.path.exists(path) and not os.path.isdir(path):
|
||||
os.remove(path)
|
||||
|
||||
def remove_empty_dirs(self):
|
||||
dir = os.path.join(os.path.dirname(os.path.dirname(__file__)),"nodes")
|
||||
for file in os.listdir(dir):
|
||||
path = os.path.join(dir, file)
|
||||
if os.path.isdir(path):
|
||||
files = os.listdir(path)
|
||||
if len(files) == 0 or (len(files) == 1 and "__init__.py" in files):
|
||||
shutil.rmtree(path)
|
||||
|
||||
def execute(self, context):
|
||||
package = self.get_package()
|
||||
if package:
|
||||
self.remove_nodes(package["nodes"])
|
||||
self.remove_empty_dirs()
|
||||
|
||||
bpy.ops.sn.reload_packages()
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_confirm(self, event)
|
||||
@@ -0,0 +1,480 @@
|
||||
import bpy
|
||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||
import os
|
||||
import shutil
|
||||
import json
|
||||
import zipfile
|
||||
|
||||
|
||||
class SN_SnippetCategory(bpy.types.PropertyGroup):
|
||||
name: bpy.props.StringProperty()
|
||||
path: bpy.props.StringProperty()
|
||||
|
||||
expand: bpy.props.BoolProperty(default=True)
|
||||
|
||||
|
||||
class SN_BoolCollection(bpy.types.PropertyGroup):
|
||||
name: bpy.props.StringProperty()
|
||||
enabled: bpy.props.BoolProperty(default=True)
|
||||
|
||||
|
||||
loaded_snippets = [] # temp var for the loaded snippets
|
||||
|
||||
|
||||
def load_snippets():
|
||||
global loaded_snippets
|
||||
sn = bpy.context.scene.sn
|
||||
installed_path = os.path.join(os.path.dirname(__file__), "installed.json")
|
||||
|
||||
if os.path.exists(installed_path):
|
||||
added = []
|
||||
with open(installed_path, "r") as installed:
|
||||
data = json.loads(installed.read())
|
||||
loaded_snippets = data["snippets"]
|
||||
|
||||
for snippet in data["snippets"]:
|
||||
if not type(snippet) == str:
|
||||
added.append(snippet["name"])
|
||||
if not snippet["name"] in sn.snippet_categories:
|
||||
item = sn.snippet_categories.add()
|
||||
else:
|
||||
item = sn.snippet_categories[snippet["name"]]
|
||||
item.name = snippet["name"]
|
||||
item.path = os.path.join(os.path.dirname(
|
||||
installed_path), "snippets", snippet["name"])
|
||||
all_items = sn.snippet_categories.keys()
|
||||
for name in all_items:
|
||||
if not name in added:
|
||||
sn.snippet_categories.remove(
|
||||
sn.snippet_categories.find(name))
|
||||
else:
|
||||
loaded_snippets = []
|
||||
|
||||
|
||||
class SN_OT_InstallSnippet(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "sn.install_snippet"
|
||||
bl_label = "Install Snippet"
|
||||
bl_description = "Install a single or a zip file of snippets"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
filter_glob: bpy.props.StringProperty(
|
||||
default='*.json;*.zip', options={'HIDDEN'})
|
||||
|
||||
def execute(self, context):
|
||||
_, extension = os.path.splitext(self.filepath)
|
||||
if extension in [".json", ".zip"]:
|
||||
path = os.path.join(os.path.dirname(__file__), "installed.json")
|
||||
if not os.path.exists(path):
|
||||
with open(path, "w") as data_file:
|
||||
data_file.write(json.dumps(
|
||||
{"packages": [], "snippets": []}))
|
||||
with open(path, "r+") as data_file:
|
||||
data = json.loads(data_file.read())
|
||||
name = os.path.basename(self.filepath)
|
||||
if not name in data["snippets"]:
|
||||
if extension == ".json":
|
||||
data["snippets"].append(name)
|
||||
shutil.copyfile(self.filepath, os.path.join(
|
||||
os.path.dirname(__file__), "snippets", name))
|
||||
if extension == ".zip":
|
||||
name = name.split(".")[0]
|
||||
path = os.path.join(os.path.dirname(
|
||||
__file__), "snippets", name)
|
||||
with zipfile.ZipFile(self.filepath, 'r') as zip_ref:
|
||||
zip_ref.extractall(path)
|
||||
data["snippets"].append({
|
||||
"name": name,
|
||||
"snippets": os.listdir(path)
|
||||
})
|
||||
data_file.seek(0)
|
||||
data_file.write(json.dumps(data, indent=4))
|
||||
data_file.truncate()
|
||||
load_snippets()
|
||||
self.report({"INFO"}, message="Snippet installed!")
|
||||
else:
|
||||
self.report({"ERROR"}, message="Please only install .json files!")
|
||||
return {"CANCELLED"}
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SN_OT_UninstallSnippet(bpy.types.Operator):
|
||||
bl_idname = "sn.uninstall_snippet"
|
||||
bl_label = "Uninstall Snippet"
|
||||
bl_description = "Uninstalls this snippet"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
path = os.path.join(os.path.dirname(__file__))
|
||||
if not os.path.exists(path):
|
||||
with open(path, "w") as data_file:
|
||||
data_file.write(json.dumps({"packages": [], "snippets": []}))
|
||||
with open(os.path.join(path, "installed.json"), "r+") as data_file:
|
||||
data = json.loads(data_file.read())
|
||||
snippet = data["snippets"].pop(self.index)
|
||||
data_file.seek(0)
|
||||
data_file.write(json.dumps(data, indent=4))
|
||||
data_file.truncate()
|
||||
if type(snippet) == str:
|
||||
os.remove(os.path.join(path, "snippets", snippet))
|
||||
else:
|
||||
shutil.rmtree(os.path.join(path, "snippets", snippet["name"]))
|
||||
load_snippets()
|
||||
self.report({"INFO"}, message="Snippet uninstalled!")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SN_OT_AddSnippet(bpy.types.Operator):
|
||||
bl_idname = "sn.add_snippet"
|
||||
bl_label = "Add Snippet"
|
||||
bl_description = "Adds this snippets node"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
path: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.node.add_node(
|
||||
"INVOKE_DEFAULT", type="SN_SnippetNode", use_transform=True)
|
||||
context.space_data.node_tree.nodes.active.path = self.path
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SN_OT_ExportSnippetDraw(bpy.types.Operator):
|
||||
bl_idname = "sn.draw_export_snippet"
|
||||
bl_label = "Draw Export Snippet Popup"
|
||||
bl_description = "Draw Export Snippet Popup"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
node: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def get_connected_functions(self, function_node):
|
||||
nodes = []
|
||||
for node in function_node._get_linked_nodes(started_at_trigger=True):
|
||||
if node.bl_idname == "SN_RunFunctionNode":
|
||||
parent_tree = node.ref_ntree if node.ref_ntree else node.node_tree
|
||||
if node.ref_SN_FunctionNode in parent_tree.nodes:
|
||||
new_node = parent_tree.nodes[node.ref_SN_FunctionNode]
|
||||
nodes.append(new_node)
|
||||
nodes += self.get_connected_functions(new_node)
|
||||
return nodes
|
||||
|
||||
def invoke(self, context, event):
|
||||
node = context.space_data.node_tree.nodes.active
|
||||
function_node = node.node_tree.nodes[node.ref_SN_FunctionNode]
|
||||
function_nodes = self.get_connected_functions(function_node)
|
||||
vars = []
|
||||
props = []
|
||||
context.scene.sn.snippet_vars_customizable.clear()
|
||||
context.scene.sn.snippet_props_customizable.clear()
|
||||
for func_node in function_nodes + [function_node]:
|
||||
for some_node in func_node._get_linked_nodes(started_at_trigger=True):
|
||||
if hasattr(some_node, "var_name") and hasattr(some_node, "ref_ntree"):
|
||||
var = some_node.get_var()
|
||||
if var:
|
||||
if not var.name in vars:
|
||||
vars.append(var.name)
|
||||
item = context.scene.sn.snippet_vars_customizable.add()
|
||||
item.name = var.name
|
||||
|
||||
if hasattr(some_node, "prop_name"):
|
||||
prop_src = some_node.get_prop_source()
|
||||
if prop_src and some_node.prop_name in prop_src.properties:
|
||||
prop = prop_src.properties[some_node.prop_name]
|
||||
if not prop.name in props:
|
||||
props.append(prop.name)
|
||||
item = context.scene.sn.snippet_props_customizable.add()
|
||||
item.name = prop.name
|
||||
|
||||
if len(context.scene.sn.snippet_props_customizable) or len(context.scene.sn.snippet_vars_customizable):
|
||||
wm = context.window_manager
|
||||
return wm.invoke_popup(self, width=200)
|
||||
|
||||
node = context.space_data.node_tree.nodes.active
|
||||
bpy.ops.sn.export_snippet(
|
||||
"INVOKE_DEFAULT", node=node.name, tree=node.node_tree.name)
|
||||
return self.execute(context)
|
||||
|
||||
def draw(self, context):
|
||||
node = context.space_data.node_tree.nodes.active
|
||||
layout = self.layout
|
||||
if len(context.scene.sn.snippet_vars_customizable):
|
||||
layout.label(text="Select editable variables:")
|
||||
for var in context.scene.sn.snippet_vars_customizable:
|
||||
layout.prop(var, "enabled", text=var.name, toggle=True)
|
||||
if len(context.scene.sn.snippet_props_customizable):
|
||||
layout.separator()
|
||||
layout.label(text="Select editable properties:")
|
||||
for prop in context.scene.sn.snippet_props_customizable:
|
||||
layout.prop(prop, "enabled", text=prop.name, toggle=True)
|
||||
|
||||
layout.separator()
|
||||
row = layout.row()
|
||||
row.scale_y = 1.5
|
||||
op = row.operator("sn.export_snippet",
|
||||
text="Export Snippet", icon="EXPORT")
|
||||
op.node = node.name
|
||||
op.tree = node.node_tree.name
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SN_OT_ExportSnippet(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "sn.export_snippet"
|
||||
bl_label = "Export Snippet"
|
||||
bl_description = "Export this node as a snippet"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
filename_ext = ".json"
|
||||
filter_glob: bpy.props.StringProperty(
|
||||
default="*.json", options={'HIDDEN'}, maxlen=255)
|
||||
node: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def get_connected_functions(self, function_node):
|
||||
nodes = []
|
||||
for node in function_node._get_linked_nodes(started_at_trigger=True):
|
||||
if node.bl_idname == "SN_RunFunctionNode":
|
||||
parent_tree = node.ref_ntree if node.ref_ntree else node.node_tree
|
||||
if node.ref_SN_FunctionNode in parent_tree.nodes:
|
||||
new_node = parent_tree.nodes[node.ref_SN_FunctionNode]
|
||||
nodes.append(new_node)
|
||||
nodes += self.get_connected_functions(new_node)
|
||||
return nodes
|
||||
|
||||
def get_connected_interface(self, function_node):
|
||||
nodes = []
|
||||
for node in function_node._get_linked_nodes(started_at_trigger=True):
|
||||
if node.bl_idname == "SN_RunInterfaceFunctionNode":
|
||||
parent_tree = node.ref_ntree if node.ref_ntree else node.node_tree
|
||||
if node.ref_SN_InterfaceFunctionNode in parent_tree.nodes:
|
||||
new_node = parent_tree.nodes[node.ref_SN_InterfaceFunctionNode]
|
||||
nodes.append(new_node)
|
||||
nodes += self.get_connected_functions(new_node)
|
||||
return nodes
|
||||
|
||||
def execute(self, context):
|
||||
data = {}
|
||||
node = bpy.data.node_groups[self.tree].nodes[self.node]
|
||||
parent_tree = node.ref_ntree if node.ref_ntree else node.node_tree
|
||||
function_node = None
|
||||
if node.bl_idname == "SN_RunFunctionNode" and node.ref_SN_FunctionNode in parent_tree.nodes:
|
||||
function_node = parent_tree.nodes[node.ref_SN_FunctionNode]
|
||||
if not function_node:
|
||||
self.report({"ERROR"}, message="No function selected!")
|
||||
return {"CANCELLED"}
|
||||
|
||||
data["version"] = 3
|
||||
data["name"] = function_node.name
|
||||
data["func_name"] = function_node.func_name
|
||||
data["inputs"] = []
|
||||
data["outputs"] = []
|
||||
for inp in node.inputs:
|
||||
if not inp.hide:
|
||||
if inp.bl_idname in ["SN_EnumSocket", "SN_EnumSetSocket"]:
|
||||
items = [item[0] for item in inp.get_items(None)]
|
||||
data["inputs"].append({"idname": inp.bl_idname,"name": inp.name,"subtype": inp.subtype, "enum_items": str(items)})
|
||||
elif "Vector" in inp.bl_idname:
|
||||
data["inputs"].append({"idname": inp.bl_idname,"name": inp.name,"subtype": inp.subtype, "size": inp.size})
|
||||
else:
|
||||
data["inputs"].append(
|
||||
{"idname": inp.bl_idname, "name": inp.name, "subtype": inp.subtype})
|
||||
|
||||
for out in node.outputs:
|
||||
if not out.hide:
|
||||
data["outputs"].append(
|
||||
{"idname": out.bl_idname, "name": out.name, "subtype": out.subtype})
|
||||
|
||||
data["function"] = function_node._get_code()
|
||||
data["import"] = function_node._get_code_import()
|
||||
data["imperative"] = function_node._get_code_imperative()
|
||||
data["register"] = function_node._get_code_register()
|
||||
data["unregister"] = function_node._get_code_unregister()
|
||||
function_nodes = self.get_connected_functions(function_node)
|
||||
for func_node in function_nodes:
|
||||
data["import"] += ("\n" + func_node._get_code_import()
|
||||
) if func_node._get_code_import() else ""
|
||||
data["imperative"] += "\n" + func_node._get_code() + "\n" + \
|
||||
func_node._get_code_imperative()
|
||||
data["register"] += ("\n" + func_node._get_code_register()
|
||||
) if func_node._get_code_register() else ""
|
||||
data["unregister"] += ("\n" + func_node._get_code_unregister()
|
||||
) if func_node._get_code_unregister() else ""
|
||||
|
||||
variables = {}
|
||||
used_vars = []
|
||||
properties = {}
|
||||
data["variables"] = []
|
||||
data["properties"] = []
|
||||
for func_node in function_nodes + [function_node]:
|
||||
for node in func_node._get_linked_nodes(started_at_trigger=True):
|
||||
if hasattr(node, "var_name") and hasattr(node, "ref_ntree"):
|
||||
var = node.get_var()
|
||||
if var:
|
||||
if not var.node_tree.python_name + "_SNIPPET_VARS" in variables:
|
||||
variables[var.node_tree.python_name +
|
||||
"_SNIPPET_VARS"] = {}
|
||||
|
||||
if not var.name in used_vars:
|
||||
used_vars.append(var.name)
|
||||
customizable = context.scene.sn.snippet_vars_customizable[
|
||||
var.name].enabled
|
||||
data["variables"].append({"name": var.name, "python_name": var.python_name,
|
||||
"tree": var.node_tree.python_name, "type": var.variable_type, "customizable": customizable})
|
||||
variables[var.node_tree.python_name +
|
||||
"_SNIPPET_VARS"][var.python_name] = str(var.var_default)
|
||||
data["function"] = data["function"].replace(
|
||||
var.node_tree.python_name + "[", var.node_tree.python_name + "_SNIPPET_VARS[")
|
||||
data["imperative"] = data["imperative"].replace(
|
||||
var.node_tree.python_name + "[", var.node_tree.python_name + "_SNIPPET_VARS[")
|
||||
data["register"] = data["register"].replace(
|
||||
var.node_tree.python_name + "[", var.node_tree.python_name + "_SNIPPET_VARS[")
|
||||
data["unregister"] = data["unregister"].replace(
|
||||
var.node_tree.python_name + "[", var.node_tree.python_name + "_SNIPPET_VARS[")
|
||||
|
||||
if hasattr(node, "prop_name"):
|
||||
prop_src = node.get_prop_source()
|
||||
if prop_src and node.prop_name in prop_src.properties:
|
||||
prop = prop_src.properties[node.prop_name]
|
||||
if not prop.name in properties:
|
||||
properties[prop.python_name] = [prop.register_code.replace(
|
||||
prop.python_name, prop.python_name+"_SNIPPET_VARS"), prop.unregister_code.replace(prop.python_name, prop.python_name+"_SNIPPET_VARS")]
|
||||
customizable = context.scene.sn.snippet_props_customizable[
|
||||
prop.name].enabled
|
||||
data["properties"].append({"name": prop.name, "python_name": prop.python_name,
|
||||
"type": prop.property_type, "attach_to": prop.attach_to, "customizable": customizable})
|
||||
data["function"] = data["function"].replace(
|
||||
prop.python_name, prop.python_name + "_SNIPPET_VARS")
|
||||
data["imperative"] = data["imperative"].replace(
|
||||
prop.python_name, prop.python_name + "_SNIPPET_VARS")
|
||||
data["register"] = data["register"].replace(
|
||||
prop.python_name, prop.python_name + "_SNIPPET_VARS")
|
||||
data["unregister"] = data["unregister"].replace(
|
||||
prop.python_name, prop.python_name + "_SNIPPET_VARS")
|
||||
|
||||
data["variable_defs"] = variables
|
||||
data["properties_defs"] = properties
|
||||
elif node.bl_idname == "SN_RunInterfaceFunctionNode" and node.ref_SN_InterfaceFunctionNode in parent_tree.nodes:
|
||||
function_node = parent_tree.nodes[node.ref_SN_InterfaceFunctionNode]
|
||||
if not function_node:
|
||||
self.report({"ERROR"}, message="No function selected!")
|
||||
return {"CANCELLED"}
|
||||
|
||||
data["version"] = 3
|
||||
data["name"] = function_node.name
|
||||
data["func_name"] = function_node.func_name
|
||||
data["inputs"] = []
|
||||
data["outputs"] = []
|
||||
for inp in node.inputs:
|
||||
if not inp.hide:
|
||||
if inp.bl_idname == "SN_EnumSocket":
|
||||
items = [item[0] for item in inp.get_items(None)]
|
||||
data["inputs"].append(
|
||||
{"idname": inp.bl_idname, "name": inp.name, "subtype": inp.subtype, "enum_items": str(items)})
|
||||
else:
|
||||
data["inputs"].append(
|
||||
{"idname": inp.bl_idname, "name": inp.name, "subtype": inp.subtype})
|
||||
|
||||
data["function"] = function_node._get_code()
|
||||
data["import"] = function_node._get_code_import()
|
||||
data["imperative"] = function_node._get_code_imperative()
|
||||
data["register"] = function_node._get_code_register()
|
||||
data["unregister"] = function_node._get_code_unregister()
|
||||
function_nodes = self.get_connected_functions(function_node)
|
||||
for func_node in function_nodes:
|
||||
data["import"] += ("\n" + func_node._get_code_import()
|
||||
) if func_node._get_code_import() else ""
|
||||
data["imperative"] += "\n" + func_node._get_code() + "\n" + \
|
||||
func_node._get_code_imperative()
|
||||
data["register"] += ("\n" + func_node._get_code_register()
|
||||
) if func_node._get_code_register() else ""
|
||||
data["unregister"] += ("\n" + func_node._get_code_unregister()
|
||||
) if func_node._get_code_unregister() else ""
|
||||
|
||||
variables = {}
|
||||
used_vars = []
|
||||
properties = {}
|
||||
data["variables"] = []
|
||||
data["properties"] = []
|
||||
for func_node in function_nodes + [function_node]:
|
||||
for node in func_node._get_linked_nodes(started_at_trigger=True):
|
||||
if hasattr(node, "var_name") and hasattr(node, "ref_ntree"):
|
||||
var = node.get_var()
|
||||
if var:
|
||||
if not var.node_tree.python_name + "_SNIPPET_VARS" in variables:
|
||||
variables[var.node_tree.python_name +
|
||||
"_SNIPPET_VARS"] = {}
|
||||
|
||||
if not var.name in used_vars:
|
||||
used_vars.append(var.name)
|
||||
customizable = context.scene.sn.snippet_vars_customizable[
|
||||
var.name].enabled
|
||||
data["variables"].append({"name": var.name, "python_name": var.python_name,
|
||||
"tree": var.node_tree.python_name, "type": var.variable_type, "customizable": customizable})
|
||||
variables[var.node_tree.python_name +
|
||||
"_SNIPPET_VARS"][var.python_name] = str(var.var_default)
|
||||
data["function"] = data["function"].replace(
|
||||
var.node_tree.python_name + "[", var.node_tree.python_name + "_SNIPPET_VARS[")
|
||||
data["imperative"] = data["imperative"].replace(
|
||||
var.node_tree.python_name + "[", var.node_tree.python_name + "_SNIPPET_VARS[")
|
||||
data["register"] = data["register"].replace(
|
||||
var.node_tree.python_name + "[", var.node_tree.python_name + "_SNIPPET_VARS[")
|
||||
data["unregister"] = data["unregister"].replace(
|
||||
var.node_tree.python_name + "[", var.node_tree.python_name + "_SNIPPET_VARS[")
|
||||
|
||||
if hasattr(node, "prop_name"):
|
||||
prop_src = node.get_prop_source()
|
||||
if prop_src and node.prop_name in prop_src.properties:
|
||||
prop = prop_src.properties[node.prop_name]
|
||||
if not prop.name in properties:
|
||||
properties[prop.python_name] = [prop.register_code.replace(
|
||||
prop.python_name, prop.python_name+"_SNIPPET_VARS"), prop.unregister_code.replace(prop.python_name, prop.python_name+"_SNIPPET_VARS")]
|
||||
customizable = context.scene.sn.snippet_props_customizable[
|
||||
prop.name].enabled
|
||||
data["properties"].append(
|
||||
{"name": prop.name, "python_name": prop.python_name, "type": prop.property_type, "customizable": customizable})
|
||||
data["function"] = data["function"].replace(
|
||||
prop.python_name, prop.python_name + "_SNIPPET_VARS")
|
||||
data["imperative"] = data["imperative"].replace(
|
||||
prop.python_name, prop.python_name + "_SNIPPET_VARS")
|
||||
data["register"] = data["register"].replace(
|
||||
prop.python_name, prop.python_name + "_SNIPPET_VARS")
|
||||
data["unregister"] = data["unregister"].replace(
|
||||
prop.python_name, prop.python_name + "_SNIPPET_VARS")
|
||||
|
||||
data["variable_defs"] = variables
|
||||
data["properties_defs"] = properties
|
||||
|
||||
with open(self.filepath, "w") as data_file:
|
||||
data_file.seek(0)
|
||||
data_file.write(json.dumps(data, indent=4))
|
||||
data_file.truncate()
|
||||
if data:
|
||||
self.report({"INFO"}, message="Snippet exported!")
|
||||
bpy.ops.sn.snippet_info("INVOKE_DEFAULT")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SN_OT_SnippetInfo(bpy.types.Operator):
|
||||
bl_idname = "sn.snippet_info"
|
||||
bl_label = "Snippet Info"
|
||||
bl_description = "Info"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="Awesome snippet!", icon="FUND")
|
||||
col = layout.column(align=True)
|
||||
col.label(text="Do you think others could use this too?")
|
||||
col.label(text="Why not share it with the Serpens community?")
|
||||
row = col.row()
|
||||
row.operator("wm.url_open", text="Upload it to the #marketplace channel!",
|
||||
icon_value=bpy.context.scene.sn_icons["discord"].icon_id).url = "https://discord.com/invite/NK6kyae"
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_popup(self, width=300)
|
||||
@@ -0,0 +1,64 @@
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
from .interface.menus.rightclick import serpens_right_click
|
||||
from . import bl_info
|
||||
from .nodes.compiler import compile_addon, unregister_addon
|
||||
from .settings.updates import check_serpens_updates
|
||||
from .settings.easybpy import check_easy_bpy_install
|
||||
from .settings.handle_script_changes import (
|
||||
unwatch_script_changes,
|
||||
watch_script_changes,
|
||||
update_script_nodes,
|
||||
)
|
||||
from .extensions.snippet_ops import load_snippets
|
||||
from .msgbus import subscribe_to_name_change
|
||||
|
||||
|
||||
@persistent
|
||||
def depsgraph_handler(dummy):
|
||||
for group in bpy.data.node_groups:
|
||||
if group.bl_idname == "ScriptingNodesTree":
|
||||
group.use_fake_user = True
|
||||
# add empty collection for node drawing
|
||||
if not "empty" in group.node_refs:
|
||||
group.node_refs.add().name = "empty"
|
||||
|
||||
|
||||
@persistent
|
||||
def load_handler(dummy):
|
||||
if hasattr(bpy.context.scene, "sn"):
|
||||
bpy.context.scene.sn.picker_active = False
|
||||
subscribe_to_name_change()
|
||||
check_easy_bpy_install()
|
||||
if bpy.context.scene.sn.compile_on_load:
|
||||
compile_addon()
|
||||
check_serpens_updates(bl_info["version"])
|
||||
bpy.ops.sn.reload_packages()
|
||||
load_snippets()
|
||||
bpy.context.scene.sn.hide_preferences = False
|
||||
unwatch_script_changes()
|
||||
if bpy.context.scene.sn.watch_script_changes:
|
||||
watch_script_changes()
|
||||
|
||||
|
||||
@persistent
|
||||
def unload_handler(dummy=None):
|
||||
if hasattr(bpy.context.scene, "sn"):
|
||||
unwatch_script_changes()
|
||||
unregister_addon()
|
||||
|
||||
|
||||
@persistent
|
||||
def undo_post(dummy=None):
|
||||
if hasattr(bpy.context, "space_data") and hasattr(
|
||||
bpy.context.space_data, "node_tree"
|
||||
):
|
||||
ntree = bpy.context.space_data.node_tree
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
compile_addon()
|
||||
|
||||
|
||||
@persistent
|
||||
def save_pre(dummy=None):
|
||||
if bpy.context.scene.sn.watch_script_changes:
|
||||
update_script_nodes(True)
|
||||
@@ -0,0 +1,16 @@
|
||||
import bpy
|
||||
import os
|
||||
|
||||
|
||||
class SN_OT_ClearConsole(bpy.types.Operator):
|
||||
bl_idname = "sn.clear_console"
|
||||
bl_label = "Clear System Console"
|
||||
bl_description = "This operator clears the system console."
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
if os.name == "nt":
|
||||
os.system("cls")
|
||||
else:
|
||||
os.system("clear")
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,92 @@
|
||||
import bpy
|
||||
import platform
|
||||
|
||||
|
||||
class SN_PT_HeaderSettings(bpy.types.Panel):
|
||||
bl_idname = "SN_PT_HeaderSettings"
|
||||
bl_label = "Settings"
|
||||
bl_space_type = "NODE_EDITOR"
|
||||
bl_region_type = "HEADER"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(context.scene.sn, "insert_sockets")
|
||||
layout.prop(
|
||||
context.preferences.view,
|
||||
"show_tooltips_python",
|
||||
text="Show Python Tooltips",
|
||||
)
|
||||
|
||||
|
||||
def header_prepend(self, context):
|
||||
if (
|
||||
context.space_data.node_tree
|
||||
and context.space_data.node_tree.bl_idname == "ScriptingNodesTree"
|
||||
):
|
||||
layout = self.layout
|
||||
row = layout.row()
|
||||
|
||||
if len(context.space_data.node_tree.nodes) == 0:
|
||||
row.operator(
|
||||
"node.add_node", text="Tutorial", icon="PLAY", depress=True
|
||||
).type = "SN_TutorialNode"
|
||||
|
||||
row.operator("sn.show_data_overview", text="Blend Data", icon="RNA")
|
||||
|
||||
subrow = row.row(align=True)
|
||||
if platform.system() == "Windows":
|
||||
subrow.operator("sn.clear_console", text="", icon="TRASH")
|
||||
subrow.operator("wm.console_toggle", text="Console", icon="CONSOLE")
|
||||
|
||||
row.operator("screen.userpref_show", text="", icon="PREFERENCES")
|
||||
row.popover("SN_PT_HeaderSettings", text="", icon="WINDOW")
|
||||
|
||||
if context.scene.sn.has_update:
|
||||
row.separator()
|
||||
row.operator("sn.update_message", text="Update!", icon="INFO", depress=True)
|
||||
|
||||
|
||||
def header_append(self, context):
|
||||
if (
|
||||
context.space_data.node_tree
|
||||
and context.space_data.node_tree.bl_idname == "ScriptingNodesTree"
|
||||
):
|
||||
layout = self.layout
|
||||
|
||||
row = layout.row()
|
||||
sub_row = row.row(align=True)
|
||||
col = sub_row.column(align=True)
|
||||
col.scale_x = 1.5
|
||||
col.operator("sn.force_compile", text="", icon="FILE_REFRESH")
|
||||
sub_row.operator("sn.force_unregister", text="", icon="UNLINKED")
|
||||
sub_row = row.row(align=True)
|
||||
sub_row.operator(
|
||||
"wm.url_open",
|
||||
text="",
|
||||
icon_value=bpy.context.scene.sn_icons["discord"].icon_id,
|
||||
).url = "https://discord.com/invite/NK6kyae"
|
||||
sub_row.operator(
|
||||
"wm.url_open", text="", icon="HELP"
|
||||
).url = "https://joshuaknauber.notion.site/Serpens-Documentation-d44c98df6af64d7c9a7925020af11233"
|
||||
ms = round(context.scene.sn.compile_time * 1000, 2)
|
||||
row.label(text=str(ms) + "ms")
|
||||
row.prop(
|
||||
context.scene.sn,
|
||||
"pause_reregister",
|
||||
text="",
|
||||
icon="PLAY" if context.scene.sn.pause_reregister else "PAUSE",
|
||||
)
|
||||
|
||||
|
||||
def node_info_append(self, context):
|
||||
layout = self.layout
|
||||
node = context.space_data.node_tree.nodes.active
|
||||
if getattr(node, "is_sn", False):
|
||||
layout.operator(
|
||||
"wm.url_open", text="Node Documentation", icon="QUESTION"
|
||||
).url = "https://joshuaknauber.notion.site/555efb921f50426ea4d5812f1aa3e462?v=d781b590cc8f47449cb20812deab0cc6"
|
||||
|
||||
|
||||
def footer_status(self, context):
|
||||
layout = self.layout
|
||||
sn = context.scene.sn
|
||||
@@ -0,0 +1,154 @@
|
||||
import json
|
||||
import bpy
|
||||
from ...nodes.base_node import SN_ScriptingBaseNode
|
||||
|
||||
|
||||
|
||||
class SN_OT_RemovePreset(bpy.types.Operator):
|
||||
bl_idname = "sn.remove_preset"
|
||||
bl_label = "Remove Preset"
|
||||
bl_description = "Removes this preset"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
prefs = bpy.context.preferences.addons[__name__.partition('.')[ 0]].preferences
|
||||
prefs.presets.remove(self.index)
|
||||
bpy.ops.wm.save_userpref()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_RemovePresets(bpy.types.Operator):
|
||||
bl_idname = "sn.remove_presets"
|
||||
bl_label = "Remove Presets"
|
||||
bl_description = "Remove presets"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
prefs = bpy.context.preferences.addons[__name__.partition('.')[ 0]].preferences
|
||||
layout.label(text="Presets")
|
||||
|
||||
for i, preset in enumerate(prefs.presets):
|
||||
layout.operator("sn.remove_preset", text=f"Remove '{preset.name}'", icon="REMOVE").index = i
|
||||
|
||||
if not len(prefs.presets):
|
||||
layout.label(text="No presets", icon="INFO")
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_popup(self, width=300)
|
||||
|
||||
|
||||
|
||||
class SN_OT_AddPreset(bpy.types.Operator):
|
||||
bl_idname = "sn.add_preset"
|
||||
bl_label = "Add Preset"
|
||||
bl_description = "Adds the active node as a preset"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.space_data.node_tree.nodes.active
|
||||
|
||||
def get_save_value(self, data, attr):
|
||||
value = getattr(data, attr)
|
||||
if "bpy_prop_array" in str(type(value)) or "Color" in str(type(value)):
|
||||
return tuple(value)
|
||||
return value
|
||||
|
||||
def execute(self, context):
|
||||
node = context.space_data.node_tree.nodes.active
|
||||
prefs = bpy.context.preferences.addons[__name__.partition('.')[ 0]].preferences
|
||||
|
||||
item = prefs.presets.add()
|
||||
item.name = node.label if node.label else node.name
|
||||
item.idname = node.bl_idname
|
||||
|
||||
data = { "node": {}, "inputs": [], "outputs": [] }
|
||||
|
||||
# get node attributes
|
||||
attributes = [a for a in dir(node) if not callable(getattr(node, a))]
|
||||
data_attributes = ["width", "color", "use_custom_color"]
|
||||
for attr in attributes:
|
||||
if not attr.startswith("__") and not attr.startswith("bl_")\
|
||||
and not attr == "code" and not attr.startswith("code_") and not attr.startswith("ref_")\
|
||||
and not hasattr(SN_ScriptingBaseNode, attr) and not attr in bpy.types.Node.bl_rna.properties.keys()\
|
||||
and not attr in ["active_layout", "disable_evaluation", "skip_export", "static_uid",]:
|
||||
data_attributes.append(attr)
|
||||
|
||||
# save node attributes
|
||||
for attr in data_attributes:
|
||||
data["node"][attr] = self.get_save_value(node, attr)
|
||||
|
||||
socket_save_attributes = ["name", "disabled", "index_type", "data_type", "default_value"]
|
||||
# get input attributes
|
||||
for inp in node.inputs:
|
||||
input_data = {}
|
||||
if not inp.is_program:
|
||||
for attr in socket_save_attributes:
|
||||
if hasattr(inp, attr):
|
||||
input_data[attr] = self.get_save_value(inp, attr)
|
||||
data["inputs"].append(input_data)
|
||||
|
||||
# get output attributes
|
||||
for out in node.outputs:
|
||||
output_data = {}
|
||||
if not out.is_program:
|
||||
for attr in socket_save_attributes:
|
||||
if hasattr(out, attr):
|
||||
output_data[attr] = self.get_save_value(out, attr)
|
||||
data["outputs"].append(output_data)
|
||||
|
||||
item.data = json.dumps(data)
|
||||
|
||||
bpy.ops.wm.save_userpref()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_LoadPreset(bpy.types.Operator):
|
||||
bl_idname = "sn.load_preset"
|
||||
bl_label = "Load Preset"
|
||||
bl_description = "Loads this preset node"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def get_write_value(self, value):
|
||||
return list(value) if type(value) == list else value
|
||||
|
||||
def execute(self, context):
|
||||
prefs = bpy.context.preferences.addons[__name__.partition('.')[ 0]].preferences
|
||||
preset = prefs.presets[self.index]
|
||||
|
||||
bpy.ops.node.add_node("INVOKE_DEFAULT", type=preset.idname, use_transform=True)
|
||||
node = context.space_data.node_tree.nodes.active
|
||||
|
||||
node.label = preset.name
|
||||
|
||||
data = json.loads(preset.data)
|
||||
# load node data
|
||||
node.disable_evaluation = True
|
||||
for attr in data["node"].keys():
|
||||
setattr(node, attr, self.get_write_value(data["node"][attr]))
|
||||
|
||||
# load input data
|
||||
for i, inp_data in enumerate(data["inputs"]):
|
||||
node.disable_evaluation = True
|
||||
for attr in inp_data.keys():
|
||||
setattr(node.inputs[i], attr, self.get_write_value(inp_data[attr]))
|
||||
|
||||
# load output data
|
||||
for i, out_data in enumerate(data["outputs"]):
|
||||
node.disable_evaluation = True
|
||||
for attr in out_data.keys():
|
||||
setattr(node.outputs[i], attr, self.get_write_value(out_data[attr]))
|
||||
|
||||
node.disable_evaluation = False
|
||||
node._evaluate(context)
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,28 @@
|
||||
import bpy
|
||||
|
||||
|
||||
class SN_MT_PresetMenu(bpy.types.Menu):
|
||||
bl_idname = "SN_MT_PresetMenu"
|
||||
bl_label = "Presets"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
prefs = bpy.context.preferences.addons[__name__.partition('.')[ 0]].preferences
|
||||
|
||||
for i, preset in enumerate(prefs.presets):
|
||||
layout.operator("sn.load_preset", text=preset.name).index = i
|
||||
|
||||
if not len(prefs.presets):
|
||||
layout.label(text="No presets", icon="INFO")
|
||||
|
||||
layout.separator()
|
||||
|
||||
node = context.space_data.node_tree.nodes.active
|
||||
if node:
|
||||
layout.operator("sn.add_preset", icon="ADD", text=f"Add '{node.label if node.label else node.name}'")
|
||||
else:
|
||||
layout.operator("sn.add_preset", icon="ADD")
|
||||
|
||||
row = layout.row()
|
||||
row.enabled = len(prefs.presets) > 0
|
||||
row.operator("sn.remove_presets", text="Remove Preset", icon="REMOVE")
|
||||
@@ -0,0 +1,30 @@
|
||||
import bpy
|
||||
|
||||
|
||||
|
||||
class WM_MT_button_context(bpy.types.Menu):
|
||||
bl_label = ""
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
def serpens_right_click(self, context):
|
||||
layout = self.layout
|
||||
|
||||
property_pointer = getattr(context, "button_pointer", None)
|
||||
property_value = getattr(context, "button_prop", None)
|
||||
button_value = getattr(context, "button_operator", None)
|
||||
|
||||
if property_value or button_value:
|
||||
layout.separator()
|
||||
|
||||
if property_value and property_pointer:
|
||||
layout.operator("sn.copy_property", text="Get Serpens Property", icon="FILE_SCRIPT")
|
||||
|
||||
if button_value:
|
||||
layout.operator("sn.copy_operator", text="Get Serpens Operator", icon="FILE_SCRIPT")
|
||||
|
||||
if context:
|
||||
layout.operator("sn.copy_context", text="Copy Context", icon="COPYDOWN")
|
||||
@@ -0,0 +1,147 @@
|
||||
import bpy
|
||||
|
||||
|
||||
REPLACE_NAMES = {
|
||||
"ObjectBase": "bpy.data.objects['Object']", # outliner object hide
|
||||
"LayerCollection": "bpy.context.view_layer.active_layer_collection", # outliner collection hide
|
||||
"SpaceView3D": "bpy.context.screen.areas[0].spaces[0]", # 3d space data
|
||||
"ToolSettings": "bpy.context.scene.tool_settings", # any space tool settings
|
||||
}
|
||||
|
||||
|
||||
class SN_OT_CopyProperty(bpy.types.Operator):
|
||||
bl_idname = "sn.copy_property"
|
||||
bl_label = "Copy Property"
|
||||
bl_description = "Copy the path of this property"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
# get property details
|
||||
property_pointer = getattr(context, "button_pointer", None)
|
||||
property_value = getattr(context, "button_prop", None)
|
||||
|
||||
# copy data path if available
|
||||
if bpy.ops.ui.copy_data_path_button.poll():
|
||||
bpy.ops.ui.copy_data_path_button("INVOKE_DEFAULT", full_path=True)
|
||||
|
||||
context.scene.sn.last_copied_datatype = property_value.type.title()
|
||||
path = context.window_manager.clipboard.replace('"', "'")
|
||||
|
||||
if path and path[-1] == "]" and path[:-1].split("[")[-1].isdigit():
|
||||
path = "[".join(path.split("[")[:-1])
|
||||
context.scene.sn.last_copied_datatype += " Vector"
|
||||
elif getattr(property_value, "subtype", None) == "COLOR":
|
||||
context.scene.sn.last_copied_datatype += " Vector"
|
||||
|
||||
context.window_manager.clipboard = path
|
||||
if property_value.type == "ENUM" and property_value.is_enum_flag:
|
||||
context.scene.sn.last_copied_datatype += " Set"
|
||||
|
||||
context.scene.sn.last_copied_datapath = context.window_manager.clipboard
|
||||
|
||||
self.report({"INFO"}, message="Copied!")
|
||||
return {"FINISHED"}
|
||||
|
||||
# check if replacement is available
|
||||
if property_pointer and property_value:
|
||||
if property_pointer.bl_rna.identifier in REPLACE_NAMES:
|
||||
context.window_manager.clipboard = f"{REPLACE_NAMES[property_pointer.bl_rna.identifier]}.{property_value.identifier}"
|
||||
context.window_manager.clipboard = (
|
||||
context.window_manager.clipboard.replace('"', "'")
|
||||
)
|
||||
context.scene.sn.last_copied_datatype = property_value.type.title()
|
||||
if property_value.type == "ENUM" and property_value.is_enum_flag:
|
||||
context.scene.sn.last_copied_datatype += " Set"
|
||||
context.scene.sn.last_copied_datapath = context.window_manager.clipboard
|
||||
self.report({"INFO"}, message="Copied!")
|
||||
return {"FINISHED"}
|
||||
|
||||
# error when property not available
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
message="We can't copy this property yet! Please use the Blend Data browser to find it!",
|
||||
)
|
||||
print("Serpens Log: ", property_pointer, property_value)
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
class SN_OT_CopyOperator(bpy.types.Operator):
|
||||
bl_idname = "sn.copy_operator"
|
||||
bl_label = "Copy Operator"
|
||||
bl_description = "Copy the path of this operator"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def find_ops_path_from_rna(self, rna_identifier):
|
||||
for cat_name in dir(bpy.ops):
|
||||
if cat_name[0].isalpha() and not cat_name == "class":
|
||||
try:
|
||||
cat = eval(f"bpy.ops.{cat_name}")
|
||||
except:
|
||||
cat = None
|
||||
if cat:
|
||||
for op_name in dir(cat):
|
||||
if op_name[0].isalpha():
|
||||
try:
|
||||
op = eval(f"bpy.ops.{cat_name}.{op_name}")
|
||||
except:
|
||||
op = None
|
||||
if op and op.get_rna_type().identifier == rna_identifier:
|
||||
return f"bpy.ops.{cat_name}.{op_name}()"
|
||||
return None
|
||||
|
||||
def execute(self, context):
|
||||
# copy operator if available
|
||||
if bpy.ops.ui.copy_python_command_button.poll():
|
||||
bpy.ops.ui.copy_python_command_button("INVOKE_DEFAULT")
|
||||
self.report({"INFO"}, message="Copied!")
|
||||
return {"FINISHED"}
|
||||
|
||||
# get button details
|
||||
button_value = getattr(context, "button_operator", None)
|
||||
|
||||
# check if value exists
|
||||
if button_value:
|
||||
op_path = self.find_ops_path_from_rna(button_value.bl_rna.identifier)
|
||||
if op_path:
|
||||
context.window_manager.clipboard = op_path
|
||||
self.report({"INFO"}, message="Copied!")
|
||||
return {"FINISHED"}
|
||||
|
||||
# error when button not available
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
message="We can't copy this operator yet! Please report this to the developers!",
|
||||
)
|
||||
print("Serpens Log: ", button_value)
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
def copy_context():
|
||||
context = {}
|
||||
for attribute in dir(bpy.context):
|
||||
if (
|
||||
attribute[0].isalpha()
|
||||
and not attribute in ["property"]
|
||||
and hasattr(bpy.context, attribute)
|
||||
and not callable(getattr(bpy.context, attribute))
|
||||
):
|
||||
context[attribute] = getattr(bpy.context, attribute)
|
||||
return context
|
||||
|
||||
|
||||
class SN_OT_CopyContext(bpy.types.Operator):
|
||||
bl_idname = "sn.copy_context"
|
||||
bl_label = "Copy Context"
|
||||
bl_description = "Copy the context from this area"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
copied = copy_context()
|
||||
context.scene.sn.copied_context.clear()
|
||||
context.scene.sn.copied_context.append(copied)
|
||||
context.scene.sn.hide_preferences = True
|
||||
for screen in bpy.data.screens:
|
||||
for area in screen.areas:
|
||||
area.tag_redraw()
|
||||
self.report({"INFO"}, message="Copied and reloaded!")
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,45 @@
|
||||
import bpy
|
||||
import os
|
||||
from ...extensions import snippet_ops
|
||||
|
||||
|
||||
class SN_MT_SnippetsMenu(bpy.types.Menu):
|
||||
bl_idname = "SN_MT_SnippetsMenu"
|
||||
bl_label = "Snippets"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
op = layout.operator("node.add_node", text="Snippet")
|
||||
op.type = "SN_SnippetNode"
|
||||
op.use_transform = True
|
||||
|
||||
no_cat_snippets = False
|
||||
for snippet in snippet_ops.loaded_snippets:
|
||||
if type(snippet) != str:
|
||||
row = layout.row()
|
||||
row.context_pointer_set("snippet", context.scene.sn.snippet_categories[snippet["name"]])
|
||||
row.menu("SN_MT_SnippetMenu", text=snippet["name"])
|
||||
else:
|
||||
no_cat_snippets = True
|
||||
if no_cat_snippets:
|
||||
layout.menu("SN_MT_SnippetMenu", text="Others")
|
||||
|
||||
|
||||
|
||||
class SN_MT_SnippetMenu(bpy.types.Menu):
|
||||
bl_idname = "SN_MT_SnippetMenu"
|
||||
bl_label = "Snippets"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
if hasattr(context, "snippet"):
|
||||
for data in snippet_ops.loaded_snippets:
|
||||
if not type(data) == str and data["name"] == context.snippet.name:
|
||||
for snippet in data["snippets"]:
|
||||
layout.operator("sn.add_snippet", text=snippet.split(".")[0]).path = os.path.join(context.snippet.path, snippet)
|
||||
else:
|
||||
path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "extensions", "snippets")
|
||||
for name in snippet_ops.loaded_snippets:
|
||||
if type(name) == str:
|
||||
layout.operator("sn.add_snippet", text=name.split(".")[0]).path = os.path.join(path, name)
|
||||
@@ -0,0 +1,50 @@
|
||||
import bpy
|
||||
|
||||
|
||||
|
||||
class SN_PT_AddonInfoPanel(bpy.types.Panel):
|
||||
bl_idname = "SN_PT_AddonInfoPanel"
|
||||
bl_label = ""
|
||||
bl_space_type = 'NODE_EDITOR'
|
||||
bl_region_type = 'UI'
|
||||
bl_category = "Serpens"
|
||||
bl_options = {"DEFAULT_CLOSED", "HEADER_LAYOUT_EXPAND"}
|
||||
bl_order = 4
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.space_data.tree_type == "ScriptingNodesTree" and context.space_data.node_tree
|
||||
|
||||
def draw_header(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="Addon")
|
||||
layout.operator("wm.url_open", text="", icon="QUESTION", emboss=False).url = "https://joshuaknauber.notion.site/Export-496335f1abe44262885bde330efe59c0"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
sn = context.scene.sn
|
||||
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
layout.prop(sn, "addon_name")
|
||||
layout.prop(sn, "description")
|
||||
layout.prop(sn, "author")
|
||||
layout.prop(sn, "location")
|
||||
layout.prop(sn, "warning")
|
||||
layout.prop(sn, "doc_url")
|
||||
layout.prop(sn, "tracker_url")
|
||||
col = layout.column(align=True)
|
||||
col.prop(sn, "category")
|
||||
if sn.category == "CUSTOM":
|
||||
col.prop(sn, "custom_category", text=" ")
|
||||
layout.prop(sn, "version")
|
||||
layout.prop(sn, "blender")
|
||||
# layout.prop(sn, "multifile")
|
||||
|
||||
row = layout.row()
|
||||
row.scale_y = 1.5
|
||||
col = row.column(align=True)
|
||||
col.operator("sn.export_addon", text="Save Addon", icon="EXPORT")
|
||||
row = col.row()
|
||||
row.scale_y = 0.7
|
||||
row.operator("sn.export_to_marketplace",text="Add to Marketplace",icon_value=bpy.context.scene.sn_icons[ "discord" ].icon_id)
|
||||
@@ -0,0 +1,88 @@
|
||||
import bpy
|
||||
|
||||
|
||||
|
||||
class SN_PT_AddonSettingsPanel(bpy.types.Panel):
|
||||
bl_idname = "SN_PT_AddonSettingsPanel"
|
||||
bl_label = ""
|
||||
bl_space_type = 'NODE_EDITOR'
|
||||
bl_region_type = 'UI'
|
||||
bl_category = "Serpens"
|
||||
bl_options = {"DEFAULT_CLOSED", "HEADER_LAYOUT_EXPAND"}
|
||||
bl_order = 8
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.space_data.tree_type == "ScriptingNodesTree" and context.space_data.node_tree
|
||||
|
||||
def draw_header(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="Settings")
|
||||
layout.operator("wm.url_open", text="", icon="QUESTION", emboss=False).url = "https://joshuaknauber.notion.site/Workflow-Introduction-d235d03178124dc9b752088d75a25192"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
sn = context.scene.sn
|
||||
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
col = layout.column(heading="General")
|
||||
col.prop(sn, "watch_script_changes")
|
||||
col.prop(sn, "show_graph_categories")
|
||||
col.prop(sn, "show_property_categories")
|
||||
col.prop(sn, "overwrite_variable_graph")
|
||||
col.prop(sn, "compile_on_load")
|
||||
|
||||
layout.separator()
|
||||
col = layout.column(heading="Generated Code")
|
||||
col.prop(sn, "debug_code", text="Keep Code File")
|
||||
subcol = col.column()
|
||||
subcol.enabled = sn.debug_code
|
||||
subcol.prop(sn, "remove_duplicate_code")
|
||||
subcol.prop(sn, "format_code")
|
||||
|
||||
layout.separator()
|
||||
col = layout.column(heading="Debug")
|
||||
col.prop(sn, "debug_compile_time", text="Log Compile Time")
|
||||
col.prop(sn, "debug_python_nodes")
|
||||
col.prop(sn, "debug_python_sockets")
|
||||
subrow = col.row()
|
||||
subrow.active = sn.debug_python_nodes or sn.debug_python_sockets
|
||||
subrow.prop(sn, "debug_selected_only")
|
||||
col.prop(sn, "debug_python_properties")
|
||||
|
||||
|
||||
|
||||
class SN_PT_EasyBpyPanel(bpy.types.Panel):
|
||||
bl_idname = "SN_PT_EasyBpyPanel"
|
||||
bl_parent_id = "SN_PT_AddonSettingsPanel"
|
||||
bl_label = ""
|
||||
bl_space_type = 'NODE_EDITOR'
|
||||
bl_region_type = 'UI'
|
||||
bl_category = "Serpens"
|
||||
bl_order = 0
|
||||
bl_options={"HEADER_LAYOUT_EXPAND"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.space_data.tree_type == "ScriptingNodesTree" and context.space_data.node_tree
|
||||
|
||||
def draw_header(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="Easy BPY")
|
||||
layout.operator("wm.url_open", text="", icon="QUESTION", emboss=False).url = "https://joshuaknauber.notion.site/Easy-BPY-e3a894c7bf4c469389e6caa7640c3219"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
sn = context.scene.sn
|
||||
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
if sn.easy_bpy_path:
|
||||
layout.label(text="Easy BPY installed", icon="CHECKMARK")
|
||||
layout.operator("sn.open_explorer", text="Open Install", icon="FILE_FOLDER").path = sn.easy_bpy_path
|
||||
else:
|
||||
layout.label(text="Easy BPY not installed", icon="CANCEL")
|
||||
layout.operator("wm.url_open", text="Documentation", icon="URL").url = "https://curtisholt.online/easybpy"
|
||||
@@ -0,0 +1,11 @@
|
||||
import bpy
|
||||
import os
|
||||
|
||||
|
||||
|
||||
class SN_UL_AssetList(bpy.types.UIList):
|
||||
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||
row = layout.row()
|
||||
row.label(text="", icon="ASSET_MANAGER" if "." in item.path else "FILEBROWSER")
|
||||
row.prop(item, "name", text="", emboss=False)
|
||||
@@ -0,0 +1,44 @@
|
||||
import bpy
|
||||
|
||||
|
||||
|
||||
class SN_PT_AssetsPanel(bpy.types.Panel):
|
||||
bl_idname = "SN_PT_AssetsPanel"
|
||||
bl_label = ""
|
||||
bl_space_type = 'NODE_EDITOR'
|
||||
bl_region_type = 'UI'
|
||||
bl_category = "Serpens"
|
||||
bl_order = 2
|
||||
bl_options = {"DEFAULT_CLOSED", "HEADER_LAYOUT_EXPAND"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.space_data.tree_type == "ScriptingNodesTree" and context.space_data.node_tree
|
||||
|
||||
def draw_header(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="Assets")
|
||||
layout.operator("wm.url_open", text="", icon="QUESTION", emboss=False).url = "https://joshuaknauber.notion.site/Assets-c013c317a1b840b8824a4161da296614"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
sn = context.scene.sn
|
||||
|
||||
# draw asset list
|
||||
row = layout.row(align=False)
|
||||
col = row.column(align=True)
|
||||
col.template_list("SN_UL_AssetList", "Assets", sn, "assets", sn, "asset_index", rows=3)
|
||||
col.operator("sn.add_asset_node", text="Add Node", icon="ADD")
|
||||
col = row.column(align=True)
|
||||
col.operator("sn.add_asset", text="", icon="ADD")
|
||||
col.operator("sn.find_asset", text="", icon="VIEWZOOM")
|
||||
col.operator("sn.remove_asset", text="", icon="REMOVE")
|
||||
|
||||
# draw asset settings
|
||||
if sn.asset_index < len(sn.assets):
|
||||
asset = sn.assets[sn.asset_index]
|
||||
col = layout.column()
|
||||
col.use_property_split = True
|
||||
col.use_property_decorate = False
|
||||
|
||||
col.prop(asset, "path", text="")
|
||||
@@ -0,0 +1,252 @@
|
||||
import bpy
|
||||
from ...settings.data_properties import get_data_items, item_from_path, filter_items, filter_defaults
|
||||
|
||||
|
||||
|
||||
class SN_OT_ShowDataOverview(bpy.types.Operator):
|
||||
bl_idname = "sn.show_data_overview"
|
||||
bl_label = "Show Data Overview"
|
||||
bl_description = "Opens a window that shows a data overview"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
for area in context.screen.areas:
|
||||
if area.type == "PREFERENCES":
|
||||
break
|
||||
else:
|
||||
bpy.ops.screen.userpref_show("INVOKE_DEFAULT")
|
||||
if not context.scene.sn.hide_preferences:
|
||||
context.scene.sn.hide_preferences = True
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
|
||||
class SN_OT_ExitDataSearch(bpy.types.Operator):
|
||||
bl_idname = "sn.exit_search"
|
||||
bl_label = "Exit Data Search"
|
||||
bl_description = "Exits the data search mode"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.sn.hide_preferences = False
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_ExpandData(bpy.types.Operator):
|
||||
bl_idname = "sn.expand_data"
|
||||
bl_label = "Expand Data"
|
||||
bl_description = "Loads the items for the given item"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
path: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
sn = context.scene.sn
|
||||
|
||||
if "bpy.ops" in self.path:
|
||||
item = sn.ops_items["operators"][self.path.split(".")[-1]]
|
||||
item["expanded"] = not item["expanded"]
|
||||
|
||||
else:
|
||||
item = item_from_path(sn.data_items, self.path)
|
||||
item["expanded"] = not item["expanded"]
|
||||
if not item["properties"]:
|
||||
try:
|
||||
item["data"]
|
||||
item["properties"] = get_data_items(self.path, item["data"])
|
||||
except:
|
||||
item["has_properties"] = False
|
||||
item["expanded"] = False
|
||||
self.report({"ERROR"}, message="This data doesn't exist anymore!")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_ExpandAllOperators(bpy.types.Operator):
|
||||
bl_idname = "sn.expand_operators"
|
||||
bl_label = "Expand Operators"
|
||||
bl_description = "Expands all operators"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
sn = context.scene.sn
|
||||
if sn.ops_items["operators"]:
|
||||
expand = not list(sn.ops_items["operators"].values())[0]["expanded"]
|
||||
|
||||
for item in sn.ops_items["operators"].values():
|
||||
item["expanded"] = expand
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_FilterData(bpy.types.Operator):
|
||||
bl_idname = "sn.filter_data"
|
||||
bl_label = "Filter Data"
|
||||
bl_description = "Filters this items data"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
path: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def update_filters(self, context):
|
||||
item = item_from_path(context.scene.sn.data_items, self.path)
|
||||
item["data_search"] = self.data_search
|
||||
item["data_filter"] = self.data_filter
|
||||
|
||||
data_search: bpy.props.StringProperty(default="",
|
||||
options={"SKIP_SAVE", "HIDDEN", "TEXTEDIT_UPDATE"},
|
||||
update=update_filters)
|
||||
|
||||
data_filter: bpy.props.EnumProperty(name="Type",
|
||||
options={"ENUM_FLAG"},
|
||||
description="Filter by data type",
|
||||
items=filter_items,
|
||||
default=filter_defaults,
|
||||
update=update_filters)
|
||||
|
||||
def update_reset(self, context):
|
||||
if not self.reset:
|
||||
self["reset"] = True
|
||||
self.data_search = ""
|
||||
self.data_filter = filter_defaults
|
||||
|
||||
reset: bpy.props.BoolProperty(name="Reset", default=True,
|
||||
description="Reset the filters",
|
||||
update=update_reset)
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="Search:")
|
||||
row = layout.row()
|
||||
row.prop(self, "data_search", text="")
|
||||
row.prop(self, "reset", text="", icon="LOOP_BACK", invert_checkbox=True)
|
||||
layout.separator()
|
||||
col = layout.column()
|
||||
col.prop(self, "data_filter", expand=True)
|
||||
|
||||
def invoke(self, context, event):
|
||||
item = item_from_path(context.scene.sn.data_items, self.path)
|
||||
last_filter = item["data_filter"]
|
||||
last_search = item["data_search"]
|
||||
self.data_filter = last_filter
|
||||
self.data_search = last_search
|
||||
return context.window_manager.invoke_popup(self, width=300)
|
||||
|
||||
|
||||
|
||||
class SN_OT_ResetFilters(bpy.types.Operator):
|
||||
bl_idname = "sn.reset_filters"
|
||||
bl_label = "Reset Filters"
|
||||
bl_description = "Resets these filters"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.sn.data_filter = filter_defaults
|
||||
context.scene.sn.data_search = ""
|
||||
for area in context.screen.areas:
|
||||
area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_ReloadData(bpy.types.Operator):
|
||||
bl_idname = "sn.reload_data"
|
||||
bl_label = "Reload Data"
|
||||
bl_description = "Reloads the listed scene data"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.sn.hide_preferences = True
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_ResetItemFilters(bpy.types.Operator):
|
||||
bl_idname = "sn.reset_item_filters"
|
||||
bl_label = "Reset Item Filters"
|
||||
bl_description = "Reset this items filters"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
path: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
item = item_from_path(context.scene.sn.data_items, self.path)
|
||||
try:
|
||||
item["data"]
|
||||
item["data_filter"] = filter_defaults
|
||||
item["data_search"] = ""
|
||||
except:
|
||||
item["has_properties"] = False
|
||||
item["expanded"] = False
|
||||
self.report({"ERROR"}, message="This data doesn't exist anymore!")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_ReloadItemData(bpy.types.Operator):
|
||||
bl_idname = "sn.reload_item_data"
|
||||
bl_label = "Reload Item Data"
|
||||
bl_description = "Reloads this items data"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
path: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
item = item_from_path(context.scene.sn.data_items, self.path)
|
||||
try:
|
||||
item["data"]
|
||||
item["properties"] = get_data_items(self.path, item["data"])
|
||||
except:
|
||||
item["has_properties"] = False
|
||||
item["expanded"] = False
|
||||
self.report({"ERROR"}, message="This data doesn't exist anymore!")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_CopyDataPath(bpy.types.Operator):
|
||||
bl_idname = "sn.copy_data_path"
|
||||
bl_label = "Copy Data Path"
|
||||
bl_description = "Copy data path to paste in a node"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
path: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
type: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
required: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
context.window_manager.clipboard = self.path
|
||||
context.scene.sn.last_copied_datapath = self.path
|
||||
context.scene.sn.last_copied_datatype = self.type
|
||||
context.scene.sn.last_copied_required = self.required
|
||||
self.report({"INFO"}, message="Copied!")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_AddToSearch(bpy.types.Operator):
|
||||
bl_idname = "sn.add_to_search"
|
||||
bl_label = "Add To Search"
|
||||
bl_description = "Adds this section to the search"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
section: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
sn = context.scene.sn
|
||||
if sn.discover_search.startswith(self.section):
|
||||
sn.discover_search = sn.discover_search[len(self.section)+1:]
|
||||
elif sn.discover_search.endswith(self.section):
|
||||
sn.discover_search = sn.discover_search[:-len(self.section)-1]
|
||||
elif f",{self.section}," in sn.discover_search:
|
||||
sn.discover_search = sn.discover_search.replace(f",{self.section},", ",")
|
||||
else:
|
||||
if sn.discover_search:
|
||||
sn.discover_search += f",{self.section}"
|
||||
else:
|
||||
sn.discover_search = self.section
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,284 @@
|
||||
import bpy
|
||||
from ...addon.properties.settings.settings import property_icons
|
||||
from ...settings.data_properties import filter_defaults
|
||||
from ...settings import global_search
|
||||
|
||||
|
||||
|
||||
class SN_PT_navigation_bar(bpy.types.Panel):
|
||||
bl_label = "Preferences Navigation"
|
||||
bl_space_type = 'PREFERENCES'
|
||||
bl_region_type = 'NAVIGATION_BAR'
|
||||
bl_options = {'HIDE_HEADER'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.scene.sn.hide_preferences
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
sn = context.scene.sn
|
||||
|
||||
row = layout.row()
|
||||
row.scale_y = 1.4
|
||||
row.alert = True
|
||||
row.operator("sn.exit_search", text="Exit", icon="PANEL_CLOSE")
|
||||
|
||||
layout.separator()
|
||||
layout.operator("wm.url_open", text="How to", icon="QUESTION").url = "https://joshuaknauber.notion.site/Blend-Data-33e9f2ea40f44c2498cb26838662b621"
|
||||
|
||||
layout.separator(factor=2)
|
||||
col = layout.column(align=True)
|
||||
col.scale_y = 1.4
|
||||
col.operator("sn.reload_data", text="Reload", icon="FILE_REFRESH")
|
||||
col.separator()
|
||||
col.prop_enum(sn, "data_category", value="discover", text="Discover (BETA)", icon="WORLD")
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="Source:")
|
||||
col = layout.column(align=True)
|
||||
col.scale_y = 1.4
|
||||
col.prop_enum(sn, "data_category", value="app")
|
||||
col.prop_enum(sn, "data_category", value="context")
|
||||
col.prop_enum(sn, "data_category", value="data")
|
||||
col.separator()
|
||||
col.prop_enum(sn, "data_category", value="ops")
|
||||
|
||||
layout.separator()
|
||||
col = layout.column()
|
||||
row = col.row()
|
||||
row.label(text="Filter Overview:")
|
||||
row.operator("sn.reset_filters", text="", icon="LOOP_BACK", emboss=False)
|
||||
|
||||
if sn.data_category == "discover":
|
||||
row = col.row()
|
||||
row.enabled = False
|
||||
row.label(text=f"Total: {len(global_search.data_flat)} items")
|
||||
row = col.row()
|
||||
row.enabled = False
|
||||
row.label(text=f"Full Matches: {sn.discover_data['full_matches']} items")
|
||||
|
||||
row = col.row()
|
||||
row.scale_y = 1.2
|
||||
if sn.data_category == "discover":
|
||||
row.prop(sn, "discover_search", text="", icon="VIEWZOOM")
|
||||
else:
|
||||
row.prop(sn, "data_search", text="", icon="VIEWZOOM")
|
||||
subcol = col.column()
|
||||
subcol.enabled = sn.data_category != "ops"
|
||||
subcol.prop(sn, "data_filter", expand=True)
|
||||
|
||||
layout.separator()
|
||||
layout.prop(sn, "show_path")
|
||||
if sn.data_category == "discover":
|
||||
layout.prop(sn, "discover_full_only")
|
||||
layout.prop(sn, "discover_show_amount", text="Max Amount")
|
||||
|
||||
|
||||
|
||||
class SN_PT_FilterDataSettings(bpy.types.Panel):
|
||||
bl_idname = "SN_PT_FilterDataSettings"
|
||||
bl_label = "Filter"
|
||||
bl_space_type = "PREFERENCES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_options = {"HIDE_HEADER"}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
if getattr(context, "sn_filter_path", None):
|
||||
row = layout.row()
|
||||
row.prop(context.sn_filter_path, "data_search", text="", icon="VIEWZOOM")
|
||||
col = layout.column()
|
||||
col.prop(context.sn_filter_path, "data_filter")
|
||||
|
||||
|
||||
|
||||
path_notes = {
|
||||
"bpy.context.preferences.keymap": "Copy shortcuts from Context -> Window Manager -> Keyconfigs -> Your Shortcut -> Type",
|
||||
"bpy.context.window_manager.keyconfigs": "To display a shortcut, find it in the User Key Config below, copy its Type property and check Full Shortcut on the node",
|
||||
"bpy.context.active_object": "To set the active object use the active object output on the Objects node or copy the active object from the active view layer",
|
||||
}
|
||||
|
||||
class SN_PT_data_search(bpy.types.Panel):
|
||||
bl_space_type = 'PREFERENCES'
|
||||
bl_region_type = 'WINDOW'
|
||||
bl_label = "Display"
|
||||
bl_options = {'HIDE_HEADER'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.scene.sn.hide_preferences
|
||||
|
||||
def should_draw(self, item, search_value, filters):
|
||||
if search_value.lower() in item["name"].lower():
|
||||
return item["type"] in filters
|
||||
return False
|
||||
|
||||
def draw_item(self, layout, item):
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
|
||||
if not item["has_properties"]:
|
||||
row.scale_y = 0.75
|
||||
else:
|
||||
op = row.operator("sn.expand_data", text="", icon="TRIA_DOWN" if item["expanded"] else "TRIA_RIGHT", emboss=False)
|
||||
op.path = item["path"]
|
||||
|
||||
subrow = row.row(align=True)
|
||||
has_filters = item["data_search"] != "" or item["data_filter"] != filter_defaults
|
||||
op = subrow.operator("sn.filter_data", text="", icon="FILTER", emboss=has_filters, depress=has_filters)
|
||||
op.path = item["path"]
|
||||
if has_filters:
|
||||
op = subrow.operator("sn.reset_item_filters", text="", icon="LOOP_BACK", depress=True)
|
||||
op.path = item["path"]
|
||||
|
||||
row.label(text=item["name"])
|
||||
|
||||
icon = property_icons[item["type"]] if item["type"] in property_icons else "ERROR"
|
||||
subrow = row.row()
|
||||
subrow.enabled = False
|
||||
subrow.label(text=item["type"], icon=icon)
|
||||
|
||||
if bpy.context.scene.sn.show_path:
|
||||
subrow = row.row()
|
||||
subrow.enabled = False
|
||||
subrow.label(text=item["path"])
|
||||
|
||||
if item["has_properties"]:
|
||||
op = row.operator("sn.reload_item_data", text="", icon="FILE_REFRESH", emboss=False)
|
||||
op.path = item["path"]
|
||||
|
||||
op = row.operator("sn.copy_data_path", text="", icon="COPYDOWN", emboss=False)
|
||||
op.path = item["path"]
|
||||
op.type = item["type"]
|
||||
op.required = item["required"]
|
||||
|
||||
if item["expanded"]:
|
||||
row = box.row()
|
||||
split = row.split(factor=0.015)
|
||||
split.label(text="")
|
||||
col = split.column(align=True)
|
||||
|
||||
if item["path"] in path_notes:
|
||||
box = col.box()
|
||||
box.scale_y = 0.75
|
||||
box.label(text=path_notes[item["path"]], icon="INFO")
|
||||
|
||||
is_empty = True
|
||||
for key in item["properties"].keys():
|
||||
sub_item = item["properties"][key]
|
||||
if self.should_draw(sub_item, item["data_search"], item["data_filter"]):
|
||||
self.draw_item(col, sub_item)
|
||||
if sub_item["clamped"]:
|
||||
box = col.box()
|
||||
box.scale_y = 0.75
|
||||
box.label(text="... Shortened because of too many items", icon="PLUS")
|
||||
col.separator()
|
||||
is_empty = False
|
||||
|
||||
if is_empty:
|
||||
col.label(text="No Items for these filters!", icon="INFO")
|
||||
|
||||
def draw_operator_category(self, layout, category):
|
||||
sn = bpy.context.scene.sn
|
||||
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
op = row.operator("sn.expand_data", text="", icon="TRIA_DOWN" if sn.ops_items["operators"][category]["expanded"] else "TRIA_RIGHT", emboss=False)
|
||||
op.path = f"bpy.ops.{category}"
|
||||
if category == "sn":
|
||||
row.label(text="Serpens")
|
||||
elif category == "sna":
|
||||
row.label(text="Serpens Addon")
|
||||
else:
|
||||
row.label(text=category.replace("_", " ").title())
|
||||
|
||||
if sn.ops_items["operators"][category]["expanded"]:
|
||||
row = box.row()
|
||||
split = row.split(factor=0.015)
|
||||
split.label(text="")
|
||||
col = split.column(align=True)
|
||||
|
||||
for operator in sn.ops_items["operators"][category]["items"]:
|
||||
if operator["operator"] in sn.ops_items["filtered"][category]:
|
||||
path = f"bpy.ops.{category}.{operator['operator']}()"
|
||||
box = col.box()
|
||||
box.scale_y = 0.75
|
||||
row = box.row()
|
||||
row.label(text=operator["name"])
|
||||
|
||||
if bpy.context.scene.sn.show_path:
|
||||
subrow = row.row()
|
||||
subrow.enabled = False
|
||||
subrow.label(text=path)
|
||||
|
||||
op = row.operator("sn.copy_python_name", text="", icon="COPYDOWN", emboss=False)
|
||||
op.name = path
|
||||
|
||||
def draw_global_search(self, layout):
|
||||
sn = bpy.context.scene.sn
|
||||
|
||||
def is_section_in_search(section):
|
||||
if sn.discover_search.startswith(section) or \
|
||||
sn.discover_search.endswith(section) or \
|
||||
f",{section}," in sn.discover_search:
|
||||
return True
|
||||
return False
|
||||
|
||||
col = layout.column(align=True)
|
||||
for path in bpy.context.scene.sn.discover_data["items"]:
|
||||
item = global_search.data_flat[path]
|
||||
|
||||
box = col.box()
|
||||
row = box.row()
|
||||
|
||||
subrow = row.row(align=True)
|
||||
subrow.alignment = "LEFT"
|
||||
for section in path.split("."):
|
||||
if not section == "bpy":
|
||||
display = section.replace("_", " ").title()
|
||||
if "[" in display and "]" in display:
|
||||
display = display.split("[")[0] + ": " + display.split("[")[1].replace("]", "")
|
||||
subrow.operator("sn.add_to_search", text=display, emboss=not is_section_in_search(section)).section = section
|
||||
|
||||
row.label(text="")
|
||||
|
||||
if bpy.context.scene.sn.show_path:
|
||||
subcol = row.column()
|
||||
subcol.enabled = False
|
||||
subcol.label(text=path)
|
||||
|
||||
row.label(text="")
|
||||
|
||||
op = row.operator("sn.copy_python_name", text="", icon="COPYDOWN", emboss=False)
|
||||
op.name = path
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
sn = context.scene.sn
|
||||
|
||||
col = layout.column(align=True)
|
||||
|
||||
if sn.data_category == "discover":
|
||||
self.draw_global_search(col)
|
||||
|
||||
else:
|
||||
is_empty = True
|
||||
if sn.data_category == "ops":
|
||||
row = col.row()
|
||||
row.label(text="Use property functions instead of operators when possible!", icon="INFO")
|
||||
row.operator("sn.expand_operators", text="", icon="FULLSCREEN_ENTER", emboss=False)
|
||||
col.separator()
|
||||
for cat in sn.ops_items["operators"].keys():
|
||||
if cat in sn.ops_items["filtered"].keys():
|
||||
self.draw_operator_category(col, cat)
|
||||
is_empty = False
|
||||
else:
|
||||
for key in sn.data_items[sn.data_category].keys():
|
||||
item = sn.data_items[sn.data_category][key]
|
||||
if self.should_draw(item, sn.data_search, sn.data_filter):
|
||||
self.draw_item(col, item)
|
||||
is_empty = False
|
||||
|
||||
if is_empty:
|
||||
layout.label(text="No Items for these filters!", icon="INFO")
|
||||
@@ -0,0 +1,16 @@
|
||||
import bpy
|
||||
import subprocess
|
||||
|
||||
|
||||
|
||||
class SN_OT_OpenExplorer(bpy.types.Operator):
|
||||
bl_idname = "sn.open_explorer"
|
||||
bl_label = "Open Explorer"
|
||||
bl_description = "Open the explorer"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
path: bpy.props.StringProperty({"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
subprocess.Popen(f'explorer /select,"{self.path}"')
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,67 @@
|
||||
import bpy
|
||||
from .graph_ui_list import get_selected_graph, get_selected_graph_offset
|
||||
|
||||
|
||||
class SN_OT_GetPythonName(bpy.types.Operator):
|
||||
bl_idname = "sn.get_python_name"
|
||||
bl_label = "Get Python Name"
|
||||
bl_description = "Get the python name for this element"
|
||||
bl_options = {"REGISTER","UNDO","INTERNAL"}
|
||||
|
||||
to_copy: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
bpy.context.window_manager.clipboard = self.to_copy
|
||||
self.report({"INFO"},message="Python path copied")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SN_PT_GraphPanel(bpy.types.Panel):
|
||||
bl_idname = "SN_PT_GraphPanel"
|
||||
bl_label = ""
|
||||
bl_space_type = 'NODE_EDITOR'
|
||||
bl_region_type = 'UI'
|
||||
bl_category = "Serpens"
|
||||
bl_order = 0
|
||||
bl_options = {"HEADER_LAYOUT_EXPAND"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.space_data.tree_type == "ScriptingNodesTree" and context.space_data.node_tree
|
||||
|
||||
def draw_header(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="Node Trees")
|
||||
layout.operator("wm.url_open", text="", icon="QUESTION", emboss=False).url = "https://joshuaknauber.notion.site/Workflow-Introduction-d235d03178124dc9b752088d75a25192"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
sn = context.scene.sn
|
||||
|
||||
tree = get_selected_graph()
|
||||
before = get_selected_graph_offset(-1)
|
||||
after = get_selected_graph_offset(1)
|
||||
|
||||
row = layout.row(align=False)
|
||||
col = row.column(align=True)
|
||||
|
||||
if sn.show_graph_categories:
|
||||
subrow = col.row(align=True)
|
||||
subrow.prop(sn, "active_graph_category", text="")
|
||||
subrow.operator("sn.edit_graph_categories", text="", icon="GREASEPENCIL")
|
||||
|
||||
col.template_list("SN_UL_GraphList", "Graphs", bpy.data, "node_groups", sn, "node_tree_index", rows=4)
|
||||
|
||||
col = row.column(align=True)
|
||||
col.operator("sn.add_graph", text="", icon="ADD")
|
||||
col.operator("sn.append_graph", text="", icon="APPEND_BLEND")
|
||||
subrow = col.row(align=True)
|
||||
subrow.enabled = tree != None
|
||||
subrow.operator("sn.remove_graph", text="", icon="REMOVE")
|
||||
col.separator()
|
||||
subrow = col.row(align=True)
|
||||
subrow.enabled = tree != None and before != None
|
||||
subrow.operator("sn.move_node_tree", text="", icon="TRIA_UP").move_up = True
|
||||
subrow = col.row(align=True)
|
||||
subrow.enabled = tree != None and after != None
|
||||
subrow.operator("sn.move_node_tree", text="", icon="TRIA_DOWN").move_up = False
|
||||
@@ -0,0 +1,110 @@
|
||||
import bpy
|
||||
|
||||
|
||||
def get_selected_graph():
|
||||
sn = bpy.context.scene.sn
|
||||
if sn.node_tree_index < len(bpy.data.node_groups):
|
||||
ntree = bpy.data.node_groups[sn.node_tree_index]
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
cat_list = list(map(lambda cat: cat.name, sn.graph_categories))
|
||||
|
||||
if sn.active_graph_category == "ALL":
|
||||
return ntree
|
||||
elif sn.active_graph_category == "OTHER":
|
||||
if ntree.category == "OTHER" or not ntree.category or not ntree.category in cat_list:
|
||||
return ntree
|
||||
elif ntree.category == sn.active_graph_category:
|
||||
return ntree
|
||||
return None
|
||||
|
||||
|
||||
filtered_cache = {}
|
||||
|
||||
|
||||
def get_filtered_graphs():
|
||||
sn = bpy.context.scene.sn
|
||||
key = "|".join(list(map(lambda ntree: getattr(ntree, "category", "SHADER") + "," + str(getattr(ntree, "index", 0)), bpy.data.node_groups))) + "|" + bpy.context.scene.sn.active_graph_category
|
||||
if key in filtered_cache:
|
||||
return filtered_cache[key]
|
||||
filtered = []
|
||||
cat_list = list(map(lambda cat: cat.name, sn.graph_categories))
|
||||
for ntree in bpy.data.node_groups:
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
if sn.active_graph_category == "ALL":
|
||||
filtered.append(ntree)
|
||||
elif sn.active_graph_category == "OTHER":
|
||||
if ntree.category == "OTHER" or not ntree.category or not ntree.category in cat_list:
|
||||
filtered.append(ntree)
|
||||
elif ntree.category == sn.active_graph_category:
|
||||
filtered.append(ntree)
|
||||
|
||||
filtered = list(sorted(filtered, key=lambda n: n.index))
|
||||
filtered_cache[key] = filtered
|
||||
return filtered
|
||||
|
||||
|
||||
def get_selected_graph_offset(offset):
|
||||
global filtered_cache
|
||||
selected = get_selected_graph()
|
||||
filtered = get_filtered_graphs()
|
||||
if selected:
|
||||
if not selected in filtered:
|
||||
filtered_cache = {}
|
||||
filtered = get_filtered_graphs()
|
||||
i = filtered.index(selected)
|
||||
i += offset
|
||||
if i >= 0 and i < len(filtered):
|
||||
return filtered[i]
|
||||
return None
|
||||
|
||||
|
||||
class SN_UL_GraphList(bpy.types.UIList):
|
||||
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||
row = layout.row()
|
||||
row.label(text="", icon="SCRIPT")
|
||||
row.prop(item, "name", emboss=False, text="")
|
||||
if context.scene.sn.show_graph_categories:
|
||||
row.operator("sn.move_graph_category", text="",
|
||||
icon="FORWARD", emboss=False).index = index
|
||||
|
||||
def filter_items(self, context, data, propname):
|
||||
sn = context.scene.sn
|
||||
node_trees = getattr(data, propname)
|
||||
helper_funcs = bpy.types.UI_UL_list
|
||||
|
||||
_sort = [(idx, frame)
|
||||
for idx, frame in enumerate(bpy.data.node_groups)]
|
||||
flt_neworder = helper_funcs.sort_items_helper(
|
||||
_sort, lambda e: getattr(e[1], "index", 0), False)
|
||||
|
||||
if sn.active_graph_category == "ALL":
|
||||
flt_flags = helper_funcs.filter_items_by_name(
|
||||
"ScriptingNodesTree", self.bitflag_filter_item, node_trees, "bl_idname", reverse=False)
|
||||
|
||||
elif sn.active_graph_category == "OTHER":
|
||||
flt_flags = []
|
||||
cat_list = list(map(lambda cat: cat.name, sn.graph_categories))
|
||||
for tree in node_trees:
|
||||
if not hasattr(tree, "category"):
|
||||
flt_flags.append(0)
|
||||
elif tree.category == "OTHER" or not tree.category or not tree.category in cat_list:
|
||||
flt_flags.append(self.bitflag_filter_item)
|
||||
else:
|
||||
flt_flags.append(0)
|
||||
|
||||
else:
|
||||
flt_flags = []
|
||||
for tree in node_trees:
|
||||
if not hasattr(tree, "category"):
|
||||
flt_flags.append(0)
|
||||
elif tree.category == sn.active_graph_category:
|
||||
flt_flags.append(self.bitflag_filter_item)
|
||||
else:
|
||||
flt_flags.append(0)
|
||||
|
||||
for i in range(len(node_trees)):
|
||||
if self.filter_name and not self.filter_name.lower() in node_trees[i].name.lower():
|
||||
flt_flags[i] = 0
|
||||
|
||||
return flt_flags, flt_neworder
|
||||
@@ -0,0 +1,205 @@
|
||||
import bpy
|
||||
from ...extensions import package_ops
|
||||
from ...extensions import snippet_ops
|
||||
|
||||
|
||||
class SN_PT_ExtensionsPanel(bpy.types.Panel):
|
||||
bl_idname = "SN_PT_ExtensionsPanel"
|
||||
bl_label = ""
|
||||
bl_space_type = "NODE_EDITOR"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Serpens"
|
||||
bl_order = 6
|
||||
bl_options = {"DEFAULT_CLOSED", "HEADER_LAYOUT_EXPAND"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (
|
||||
context.space_data.tree_type == "ScriptingNodesTree"
|
||||
and context.space_data.node_tree
|
||||
)
|
||||
|
||||
def draw_header(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="Node Extensions")
|
||||
layout.operator("wm.url_open", text="", icon="QUESTION", emboss=False).url = (
|
||||
"https://joshuaknauber.notion.site/Packages-Snippets-5fc9492b640146a2bcafb269d4a9e876"
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
|
||||
class SN_PT_SnippetsPanel(bpy.types.Panel):
|
||||
bl_parent_id = "SN_PT_ExtensionsPanel"
|
||||
bl_idname = "SN_PT_SnippetsPanel"
|
||||
bl_label = ""
|
||||
bl_space_type = "NODE_EDITOR"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Serpens"
|
||||
bl_options = {"HEADER_LAYOUT_EXPAND"}
|
||||
bl_order = 1
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (
|
||||
context.space_data.tree_type == "ScriptingNodesTree"
|
||||
and context.space_data.node_tree
|
||||
)
|
||||
|
||||
def draw_header(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="Snippets")
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
row = layout.row()
|
||||
row.scale_y = 1.1
|
||||
row.operator(
|
||||
"sn.open_preferences", text="Get Snippets", icon="URL"
|
||||
).navigation = "MARKET"
|
||||
|
||||
node = context.space_data.node_tree.nodes.active
|
||||
row = layout.row()
|
||||
row.scale_y = 1.1
|
||||
if (
|
||||
node
|
||||
and node.select
|
||||
and node.bl_idname
|
||||
in ["SN_RunFunctionNode", "SN_RunInterfaceFunctionNodeNew"]
|
||||
):
|
||||
if getattr(node, "ref_SN_FunctionNode", None) or getattr(
|
||||
node, "ref_SN_RunInterfaceFunctionNodeNew", None
|
||||
):
|
||||
op = row.operator(
|
||||
"sn.draw_export_snippet",
|
||||
text="Export Snippet",
|
||||
icon="EXPORT",
|
||||
depress=True,
|
||||
)
|
||||
op.node = node.name
|
||||
op.tree = node.node_tree.name
|
||||
|
||||
else:
|
||||
box = row.box()
|
||||
box.label(
|
||||
text="Select a valid Run Function node to export a snippet",
|
||||
icon="EXPORT",
|
||||
)
|
||||
else:
|
||||
box = row.box()
|
||||
box.label(
|
||||
text="Select Run Function node to export a snippet", icon="EXPORT"
|
||||
)
|
||||
layout.separator()
|
||||
|
||||
row = layout.row()
|
||||
row.scale_y = 1.1
|
||||
row.operator("sn.install_snippet", text="Install Snippets", icon="FILE_FOLDER")
|
||||
|
||||
for i, snippet in enumerate(snippet_ops.loaded_snippets):
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
if type(snippet) == str:
|
||||
row.label(text=snippet.split(".")[0])
|
||||
row.operator(
|
||||
"sn.uninstall_snippet", text="", icon="PANEL_CLOSE", emboss=False
|
||||
).index = i
|
||||
else:
|
||||
cat = context.scene.sn.snippet_categories.get(snippet["name"])
|
||||
row.prop(
|
||||
cat,
|
||||
"expand",
|
||||
text="",
|
||||
emboss=False,
|
||||
icon=(
|
||||
"DISCLOSURE_TRI_DOWN" if cat.expand else "DISCLOSURE_TRI_RIGHT"
|
||||
),
|
||||
)
|
||||
row.label(text=snippet["name"])
|
||||
row.operator(
|
||||
"sn.uninstall_snippet", text="", icon="PANEL_CLOSE", emboss=False
|
||||
).index = i
|
||||
if cat.expand:
|
||||
row = box.row()
|
||||
split = row.split(factor=0.1)
|
||||
split.label(text="")
|
||||
col = split.column(align=True)
|
||||
col.enabled = False
|
||||
for name in snippet["snippets"]:
|
||||
col.label(text=name.split(".")[0])
|
||||
|
||||
if not snippet_ops.loaded_snippets:
|
||||
box = layout.box()
|
||||
box.label(text="No snippets installed!", icon="INFO")
|
||||
|
||||
|
||||
class SN_PT_PackagesPanel(bpy.types.Panel):
|
||||
bl_parent_id = "SN_PT_ExtensionsPanel"
|
||||
bl_idname = "SN_PT_PackagesPanel"
|
||||
bl_label = ""
|
||||
bl_space_type = "NODE_EDITOR"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Serpens"
|
||||
bl_options = {"DEFAULT_CLOSED", "HEADER_LAYOUT_EXPAND"}
|
||||
bl_order = 2
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (
|
||||
context.space_data.tree_type == "ScriptingNodesTree"
|
||||
and context.space_data.node_tree
|
||||
)
|
||||
|
||||
def draw_header(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="Packages")
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
row = layout.row()
|
||||
row.scale_y = 1.1
|
||||
row.operator(
|
||||
"sn.open_preferences", text="Get Packages", icon="URL"
|
||||
).navigation = "MARKET"
|
||||
layout.separator()
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 1.1
|
||||
row.operator("sn.install_package", text="Install Package", icon="FILE_FOLDER")
|
||||
row.operator("sn.reload_packages", text="", icon="FILE_REFRESH")
|
||||
|
||||
for i, package in enumerate(package_ops.loaded_packages):
|
||||
box = layout.box()
|
||||
col = box.column(align=True)
|
||||
row = col.row()
|
||||
row.label(text=package["name"])
|
||||
if package["wiki"]:
|
||||
row.operator("wm.url_open", text="", icon="URL", emboss=False).url = (
|
||||
package["wiki"]
|
||||
)
|
||||
row.operator(
|
||||
"sn.uninstall_package", text="", icon="PANEL_CLOSE", emboss=False
|
||||
).index = i
|
||||
|
||||
if package["description"]:
|
||||
row = col.row()
|
||||
row.enabled = False
|
||||
row.label(text=package["description"])
|
||||
if package["author"]:
|
||||
row = col.row()
|
||||
row.enabled = False
|
||||
row.label(text="By: " + package["author"])
|
||||
if package["version"]:
|
||||
row = col.row()
|
||||
row.enabled = False
|
||||
row.label(text=package["version"])
|
||||
|
||||
if not package_ops.loaded_packages:
|
||||
box = layout.box()
|
||||
box.label(text="No packages installed!", icon="INFO")
|
||||
|
||||
if package_ops.require_reload:
|
||||
row = layout.row()
|
||||
row.alert = True
|
||||
row.label(text="Restart blender to see package!", icon="INFO")
|
||||
@@ -0,0 +1,93 @@
|
||||
import bpy
|
||||
from .property_ui_list import get_selected_property, get_selected_property_offset
|
||||
|
||||
|
||||
|
||||
class SN_PT_PropertyPanel(bpy.types.Panel):
|
||||
bl_idname = "SN_PT_PropertyPanel"
|
||||
bl_label = ""
|
||||
bl_space_type = 'NODE_EDITOR'
|
||||
bl_region_type = 'UI'
|
||||
bl_category = "Serpens"
|
||||
bl_order = 1
|
||||
bl_options = {"HEADER_LAYOUT_EXPAND"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.space_data.tree_type == "ScriptingNodesTree" and context.space_data.node_tree
|
||||
|
||||
def draw_header(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="Properties")
|
||||
layout.operator("wm.url_open", text="", icon="QUESTION", emboss=False).url = "https://joshuaknauber.notion.site/Properties-6f7567be7bff4256b9bb0311e8d79f9d"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
sn = context.scene.sn
|
||||
prop = get_selected_property()
|
||||
before = get_selected_property_offset(-1)
|
||||
after = get_selected_property_offset(1)
|
||||
|
||||
# draw property ui list
|
||||
row = layout.row(align=False)
|
||||
col = row.column(align=True)
|
||||
|
||||
if sn.show_property_categories:
|
||||
subrow = col.row(align=True)
|
||||
subrow.prop(sn, "active_prop_category", text="")
|
||||
subrow.operator("sn.edit_property_categories", text="", icon="GREASEPENCIL")
|
||||
|
||||
col.template_list("SN_UL_PropertyList", "Properties", sn, "properties", sn, "property_index", rows=5)
|
||||
col.operator("sn.add_property_node_popup", text="Add Node", icon="ADD")
|
||||
col = row.column(align=True)
|
||||
col.operator("sn.add_property", text="", icon="ADD")
|
||||
col.operator("sn.find_property", text="", icon="VIEWZOOM")
|
||||
subrow = col.row(align=True)
|
||||
subrow.enabled = prop != None
|
||||
subrow.operator("sn.remove_property", text="", icon="REMOVE")
|
||||
col.separator()
|
||||
|
||||
subrow = col.row(align=True)
|
||||
subrow.enabled = prop != None
|
||||
op = subrow.operator("sn.duplicate_property", text="", icon="DUPLICATE")
|
||||
|
||||
col.separator()
|
||||
subrow = col.row(align=True)
|
||||
subrow.enabled = before != None and prop != None
|
||||
op = subrow.operator("sn.move_property", text="", icon="TRIA_UP")
|
||||
op.move_up = True
|
||||
subrow = col.row(align=True)
|
||||
subrow.enabled = after != None and prop != None
|
||||
op = subrow.operator("sn.move_property", text="", icon="TRIA_DOWN")
|
||||
op.move_up = False
|
||||
|
||||
|
||||
|
||||
if prop:
|
||||
# draw property debug
|
||||
if sn.debug_python_properties:
|
||||
box = layout.box()
|
||||
col = box.column(align=True)
|
||||
row = col.row()
|
||||
row.enabled = False
|
||||
row.label(text="Register")
|
||||
for line in prop.register_code.split("\n"):
|
||||
col.label(text=line)
|
||||
box = layout.box()
|
||||
col = box.column(align=True)
|
||||
row = col.row()
|
||||
row.enabled = False
|
||||
row.label(text="Unregister")
|
||||
for line in prop.unregister_code.split("\n"):
|
||||
col.label(text=line)
|
||||
|
||||
col = layout.column()
|
||||
col.use_property_split = True
|
||||
col.use_property_decorate = False
|
||||
|
||||
# draw general property settings
|
||||
prop.draw(context, col)
|
||||
|
||||
# draw property specific settings
|
||||
col.separator()
|
||||
prop.settings.draw(context, col)
|
||||
@@ -0,0 +1,86 @@
|
||||
import bpy
|
||||
|
||||
|
||||
def get_filtered_properties():
|
||||
filtered = []
|
||||
sn = bpy.context.scene.sn
|
||||
for prop in sn.properties:
|
||||
cat_list = list(map(lambda cat: cat.name, sn.property_categories))
|
||||
if sn.active_prop_category == "ALL":
|
||||
filtered.append(prop)
|
||||
elif sn.active_prop_category == "OTHER":
|
||||
if prop.category == "OTHER" or not prop.category or not prop.category in cat_list:
|
||||
filtered.append(prop)
|
||||
elif prop.category == sn.active_prop_category:
|
||||
filtered.append(prop)
|
||||
return filtered
|
||||
|
||||
def get_selected_property():
|
||||
sn = bpy.context.scene.sn
|
||||
if sn.property_index < len(sn.properties):
|
||||
prop = sn.properties[sn.property_index]
|
||||
cat_list = list(map(lambda cat: cat.name, sn.property_categories))
|
||||
|
||||
if sn.active_prop_category == "ALL":
|
||||
return prop
|
||||
elif sn.active_prop_category == "OTHER":
|
||||
if prop.category == "OTHER" or not prop.category or not prop.category in cat_list:
|
||||
return prop
|
||||
elif prop.category == sn.active_prop_category:
|
||||
return prop
|
||||
return None
|
||||
|
||||
def get_selected_property_offset(offset):
|
||||
selected = get_selected_property()
|
||||
filtered = get_filtered_properties()
|
||||
if selected:
|
||||
i = filtered.index(selected)
|
||||
i += offset
|
||||
if i >= 0 and i < len(filtered):
|
||||
return filtered[i]
|
||||
return None
|
||||
|
||||
|
||||
class SN_UL_PropertyList(bpy.types.UIList):
|
||||
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||
row = layout.row()
|
||||
row.label(text="", icon=item.icon)
|
||||
row.prop(item, "name", emboss=False, text="")
|
||||
if not item.property_type == "Group":
|
||||
row.operator("sn.copy_python_name", text="", icon="COPYDOWN", emboss=False).name = item.data_path
|
||||
if context.scene.sn.show_property_categories and item.prop_collection_origin == context.scene.sn:
|
||||
row.operator("sn.move_property_category", text="", icon="FORWARD", emboss=False).index = index
|
||||
|
||||
def filter_items(self, context, data, propname):
|
||||
sn = context.scene.sn
|
||||
helper_funcs = bpy.types.UI_UL_list
|
||||
|
||||
if sn.active_prop_category == "ALL" or data != context.scene.sn:
|
||||
flt_flags = helper_funcs.filter_items_by_name(self.filter_name, self.bitflag_filter_item, sn.properties, "name", reverse=False)
|
||||
return flt_flags, []
|
||||
|
||||
elif sn.active_prop_category == "OTHER":
|
||||
flt_flags = []
|
||||
cat_list = list(map(lambda cat: cat.name, sn.property_categories))
|
||||
for prop in sn.properties:
|
||||
if prop.category == "OTHER" or not prop.category or not prop.category in cat_list:
|
||||
if not self.filter_name or self.filter_name.lower() in prop.name.lower():
|
||||
flt_flags.append(self.bitflag_filter_item)
|
||||
else:
|
||||
flt_flags.append(0)
|
||||
else:
|
||||
flt_flags.append(0)
|
||||
return flt_flags, []
|
||||
|
||||
else:
|
||||
flt_flags = []
|
||||
for prop in sn.properties:
|
||||
if prop.category == sn.active_prop_category:
|
||||
if not self.filter_name or self.filter_name.lower() in prop.name.lower():
|
||||
flt_flags.append(self.bitflag_filter_item)
|
||||
else:
|
||||
flt_flags.append(0)
|
||||
else:
|
||||
flt_flags.append(0)
|
||||
return flt_flags, []
|
||||
@@ -0,0 +1,10 @@
|
||||
import bpy
|
||||
|
||||
|
||||
class SN_UL_VariableList(bpy.types.UIList):
|
||||
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||
row = layout.row()
|
||||
row.label(text="", icon=item.icon)
|
||||
row.prop(item, "name", emboss=False, text="")
|
||||
row.operator("sn.copy_python_name", text="", icon="COPYDOWN", emboss=False).name = item.data_path
|
||||
@@ -0,0 +1,79 @@
|
||||
import bpy
|
||||
|
||||
|
||||
|
||||
class SN_PT_VariablePanel(bpy.types.Panel):
|
||||
bl_parent_id = "SN_PT_GraphPanel"
|
||||
bl_idname = "SN_PT_VariablePanel"
|
||||
bl_label = ""
|
||||
bl_space_type = 'NODE_EDITOR'
|
||||
bl_region_type = 'UI'
|
||||
bl_category = "Serpens"
|
||||
bl_options = {"DEFAULT_CLOSED", "HEADER_LAYOUT_EXPAND"}
|
||||
bl_order = 1
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.space_data.tree_type == "ScriptingNodesTree" and context.space_data.node_tree
|
||||
|
||||
def draw_header(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="Variables")
|
||||
layout.operator("wm.url_open", text="", icon="QUESTION", emboss=False).url = "https://joshuaknauber.notion.site/Variables-ff5e8ae2e4154c8fa9eed43ecaa0c165"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
sn = context.scene.sn
|
||||
ntree = context.space_data.node_tree
|
||||
|
||||
# draw variable ui list
|
||||
row = layout.row(align=False)
|
||||
col = row.column(align=True)
|
||||
|
||||
if sn.overwrite_variable_graph:
|
||||
col.prop(sn, "variable_graph", text="")
|
||||
ntree = bpy.data.node_groups[sn.variable_graph]
|
||||
|
||||
col.template_list("SN_UL_VariableList", "Variables", ntree, "variables", ntree, "variable_index", rows=4)
|
||||
|
||||
op = col.operator("sn.add_variable_node_popup", text="Add Node", icon="ADD")
|
||||
op.node_tree = ntree.name
|
||||
|
||||
col = row.column(align=True)
|
||||
col.operator("sn.add_variable", text="", icon="ADD").node_tree = ntree.name
|
||||
col.operator("sn.find_variable", text="", icon="VIEWZOOM").node_tree = ntree.name
|
||||
col.operator("sn.remove_variable", text="", icon="REMOVE").node_tree = ntree.name
|
||||
|
||||
col.separator()
|
||||
subrow = col.row(align=True)
|
||||
subrow.enabled = ntree.variable_index > 0
|
||||
op = subrow.operator("sn.move_variable", text="", icon="TRIA_UP")
|
||||
op.move_up = True
|
||||
op.node_tree = ntree.name
|
||||
subrow = col.row(align=True)
|
||||
subrow.enabled = ntree.variable_index < len(ntree.variables)-1
|
||||
op = subrow.operator("sn.move_variable", text="", icon="TRIA_DOWN")
|
||||
op.move_up = False
|
||||
op.node_tree = ntree.name
|
||||
layout.separator()
|
||||
|
||||
if ntree.variable_index < len(ntree.variables):
|
||||
var = ntree.variables[ntree.variable_index]
|
||||
col = layout.column()
|
||||
col.use_property_split = True
|
||||
col.use_property_decorate = False
|
||||
|
||||
col.prop(var, "variable_type")
|
||||
|
||||
if var.variable_type == "String":
|
||||
col.separator()
|
||||
col.prop(var, "string_default")
|
||||
elif var.variable_type == "Boolean":
|
||||
col.separator()
|
||||
col.prop(var, "boolean_default")
|
||||
elif var.variable_type == "Float":
|
||||
col.separator()
|
||||
col.prop(var, "float_default")
|
||||
elif var.variable_type == "Integer":
|
||||
col.separator()
|
||||
col.prop(var, "integer_default")
|
||||
@@ -0,0 +1,10 @@
|
||||
import bpy
|
||||
|
||||
|
||||
def append_warning(self, context):
|
||||
if context.space_data.node_tree and context.space_data.node_tree.bl_idname == "ScriptingNodesTree":
|
||||
layout = self.layout
|
||||
row = layout.row()
|
||||
row.alert = True
|
||||
|
||||
row.label(text="Do not edit these settings!", icon="ERROR")
|
||||
@@ -0,0 +1,66 @@
|
||||
import bpy
|
||||
|
||||
|
||||
# keymaps
|
||||
addon_keymaps = {}
|
||||
|
||||
|
||||
def get_shortcut(idname):
|
||||
""" Returns the shortcut struct for the given idname """
|
||||
return bpy.context.window_manager.keyconfigs.user.keymaps["Node Editor"].keymap_items[idname]
|
||||
|
||||
|
||||
def register_keymaps():
|
||||
# registers the visual scripting keymaps
|
||||
|
||||
# create keymap
|
||||
global addon_keymaps
|
||||
|
||||
wm = bpy.context.window_manager
|
||||
kc = wm.keyconfigs.addon
|
||||
|
||||
km = kc.keymaps.new(name="Node Editor", space_type="NODE_EDITOR")
|
||||
|
||||
# shortcut for compiling
|
||||
kmi = km.keymap_items.new(
|
||||
idname="sn.force_compile",
|
||||
type="R",
|
||||
value="PRESS",
|
||||
shift=True,
|
||||
ctrl=False,
|
||||
alt=False,
|
||||
)
|
||||
addon_keymaps["compile"] = (km, kmi)
|
||||
|
||||
# shortcut for docs
|
||||
kmi = km.keymap_items.new(
|
||||
idname="sn.open_node_docs",
|
||||
type="F1",
|
||||
value="PRESS",
|
||||
shift=False,
|
||||
ctrl=False,
|
||||
alt=False,
|
||||
)
|
||||
addon_keymaps["docs"] = (km, kmi)
|
||||
|
||||
# shortcut for adding a node from copied path
|
||||
kmi = km.keymap_items.new(
|
||||
idname="sn.add_copied_node",
|
||||
type="V",
|
||||
value="PRESS",
|
||||
shift=True,
|
||||
ctrl=False,
|
||||
alt=False,
|
||||
)
|
||||
addon_keymaps["copied"] = (km, kmi)
|
||||
|
||||
|
||||
def unregister_keymaps():
|
||||
# unregister visual scripting keymaps
|
||||
global addon_keymaps
|
||||
|
||||
for key in addon_keymaps:
|
||||
km, kmi = addon_keymaps[ key ]
|
||||
km.keymap_items.remove(kmi)
|
||||
|
||||
addon_keymaps.clear()
|
||||
@@ -0,0 +1,61 @@
|
||||
import bpy
|
||||
|
||||
|
||||
class SN_OT_OpenNodeDocs(bpy.types.Operator):
|
||||
bl_idname = "sn.open_node_docs"
|
||||
bl_label = "Open Node Docs"
|
||||
bl_description = "Open Node Documentation"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
if getattr(context.space_data.node_tree.nodes.active, "is_sn", False):
|
||||
if context.space_data.node_tree.nodes.active.select:
|
||||
bpy.ops.wm.url_open(url="https://joshuaknauber.notion.site/555efb921f50426ea4d5812f1aa3e462?v=d781b590cc8f47449cb20812deab0cc6")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_MT_AddOperatorMenu(bpy.types.Menu):
|
||||
bl_idname = "SN_MT_AddOperatorMenu"
|
||||
bl_label = "Add Operator Node"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout.menu_pie()
|
||||
layout.operator("sn.add_copied_operator_node", text="Run Operator", icon="POSE_HLT").is_button = False
|
||||
layout.operator("sn.add_copied_operator_node", text="Button", icon="MOUSE_LMB").is_button = True
|
||||
|
||||
|
||||
class SN_OT_AddCopiedNode(bpy.types.Operator):
|
||||
bl_idname = "sn.add_copied_node"
|
||||
bl_label = "Add Copied Node"
|
||||
bl_description = "Adds a node from the copied path for operators and properties"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
if "bpy." in context.window_manager.clipboard and ".ops." in context.window_manager.clipboard:
|
||||
bpy.ops.wm.call_menu_pie(name="SN_MT_AddOperatorMenu")
|
||||
elif "bpy." in context.window_manager.clipboard and not ".ops." in context.window_manager.clipboard:
|
||||
bpy.ops.node.add_node("INVOKE_DEFAULT", type="SN_BlenderPropertyNode", use_transform=True)
|
||||
node = context.space_data.node_tree.nodes.active
|
||||
bpy.ops.sn.paste_data_path(node=node.name, node_tree=context.space_data.node_tree.name)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SN_OT_AddCopiedOperatorNode(bpy.types.Operator):
|
||||
bl_idname = "sn.add_copied_operator_node"
|
||||
bl_label = "Add Copied Operator Node"
|
||||
bl_description = "Adds a node from the copied path for operators"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
is_button: bpy.props.BoolProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
if "bpy." in context.window_manager.clipboard and ".ops." in context.window_manager.clipboard:
|
||||
if self.is_button:
|
||||
bpy.ops.node.add_node("INVOKE_DEFAULT", type="SN_ButtonNodeNew", use_transform=True)
|
||||
else:
|
||||
bpy.ops.node.add_node("INVOKE_DEFAULT", type="SN_RunOperatorNode", use_transform=True)
|
||||
node = context.space_data.node_tree.nodes.active
|
||||
node.source_type = "BLENDER"
|
||||
node.pasted_operator = context.window_manager.clipboard
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,37 @@
|
||||
import bpy
|
||||
|
||||
owner = object()
|
||||
|
||||
|
||||
def name_change_callback(cls):
|
||||
for ntree in bpy.data.node_groups:
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
key = (
|
||||
cls.collection_key_overwrite
|
||||
if cls.collection_key_overwrite
|
||||
else cls.bl_idname
|
||||
)
|
||||
for ref in ntree.node_collection(key).refs:
|
||||
node = ref.node
|
||||
if node and node.name != ref.name:
|
||||
ref.name = node.name
|
||||
node.on_node_name_change()
|
||||
node._evaluate(bpy.context)
|
||||
return
|
||||
|
||||
|
||||
def subscribe_to_name_change():
|
||||
unsubscribe_from_name_change()
|
||||
for cls in bpy.types.Node.__subclasses__():
|
||||
if getattr(cls, "is_sn", False):
|
||||
subscribe_to = (cls, "name")
|
||||
bpy.msgbus.subscribe_rna(
|
||||
key=subscribe_to,
|
||||
owner=owner,
|
||||
args=(cls,),
|
||||
notify=name_change_callback,
|
||||
)
|
||||
|
||||
|
||||
def unsubscribe_from_name_change():
|
||||
bpy.msgbus.clear_by_owner(owner)
|
||||
@@ -0,0 +1,147 @@
|
||||
from bpy_extras.io_utils import ExportHelper
|
||||
import bpy
|
||||
import os
|
||||
import shutil
|
||||
from ...nodes.compiler import (
|
||||
format_blender_manifest,
|
||||
format_multifile,
|
||||
format_single_file,
|
||||
)
|
||||
from ...utils import normalize_code
|
||||
|
||||
|
||||
class SN_OT_ExportAddon(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "sn.export_addon"
|
||||
bl_label = "Export Addon"
|
||||
bl_description = "Exports this addon to an installable zip file"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
filepath: bpy.props.StringProperty(
|
||||
name="File Path",
|
||||
description="Filepath used for exporting the file",
|
||||
maxlen=1024,
|
||||
subtype="FILE_PATH",
|
||||
)
|
||||
|
||||
filename_ext = ".zip"
|
||||
filter_glob: bpy.props.StringProperty(default="*.zip", options={"HIDDEN"})
|
||||
|
||||
def add_easy_bpy(self, path, code):
|
||||
"""Adds the easybpy file to the addon if needed"""
|
||||
if "easybpy" in code and bpy.context.scene.sn.easy_bpy_path:
|
||||
shutil.copyfile(
|
||||
src=bpy.context.scene.sn.easy_bpy_path,
|
||||
dst=os.path.join(path, "easybpy.py"),
|
||||
)
|
||||
|
||||
def add_assets(self, asset_path):
|
||||
"""Adds the addon assets to the folder"""
|
||||
for asset in bpy.context.scene.sn.assets:
|
||||
if os.path.exists(asset.path):
|
||||
if os.path.isdir(asset.path):
|
||||
dirname = os.path.basename(asset.path)
|
||||
if not dirname:
|
||||
dirname = os.path.basename(os.path.dirname(asset.path))
|
||||
shutil.copytree(
|
||||
asset.path,
|
||||
os.path.join(asset_path, dirname),
|
||||
dirs_exist_ok=True,
|
||||
)
|
||||
else:
|
||||
shutil.copy(
|
||||
asset.path,
|
||||
os.path.join(asset_path, os.path.basename(asset.path)),
|
||||
)
|
||||
|
||||
def add_icons(self, icon_path):
|
||||
"""Adds the icons to the folder"""
|
||||
for ntree in bpy.data.node_groups:
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
for node in ntree.nodes:
|
||||
if node.bl_idname == "SN_IconNode":
|
||||
if node.icon_source == "CUSTOM" and node.icon_file:
|
||||
img_path = bpy.path.abspath(node.icon_file.filepath)
|
||||
if os.path.exists(img_path):
|
||||
filepath = os.path.join(
|
||||
icon_path, os.path.basename(img_path)
|
||||
)
|
||||
shutil.copy(img_path, filepath)
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
f"Could not find the icon file at {icon_path}"
|
||||
)
|
||||
|
||||
def add_code(self, path):
|
||||
"""Creates the index file"""
|
||||
for ntree in bpy.data.node_groups:
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
ntree.reevaluate()
|
||||
if bpy.context.scene.sn.multifile:
|
||||
files = format_multifile()
|
||||
else:
|
||||
files = {
|
||||
"__init__.py": format_single_file(),
|
||||
"blender_manifest.toml": format_blender_manifest(),
|
||||
}
|
||||
for name in files.keys():
|
||||
with open(os.path.join(path, name), "a") as code_file:
|
||||
code = files[name]
|
||||
code = code.replace("from easybpy import", "from .easybpy import")
|
||||
code_file.write(code)
|
||||
for ntree in bpy.data.node_groups:
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
ntree.reevaluate()
|
||||
return code
|
||||
|
||||
def create_files(self, path):
|
||||
"""Creates the addon files in the folder structure"""
|
||||
self.add_assets(os.path.join(path, "assets"))
|
||||
self.add_icons(os.path.join(path, "icons"))
|
||||
code = self.add_code(path)
|
||||
self.add_easy_bpy(path, code)
|
||||
|
||||
def create_structure(self, path):
|
||||
"""Sets up the addons folder structure at the given filepath"""
|
||||
os.mkdir(path)
|
||||
baseDir = os.path.join(path, bpy.context.scene.sn.module_name)
|
||||
os.mkdir(baseDir)
|
||||
os.mkdir(os.path.join(baseDir, "assets"))
|
||||
os.mkdir(os.path.join(baseDir, "icons"))
|
||||
return baseDir
|
||||
|
||||
def zip_addon(self, path):
|
||||
"""Zips the given path"""
|
||||
shutil.make_archive(path, "zip", root_dir=path)
|
||||
try:
|
||||
shutil.rmtree(path)
|
||||
except OSError as e:
|
||||
self.report({"WARNING"}, message=f"Error: {e.filename} - {e.strerror}.")
|
||||
|
||||
def execute(self, context):
|
||||
bpy.context.scene.sn.is_exporting = True
|
||||
context.window_manager.progress_begin(0, 100)
|
||||
try:
|
||||
name, _ = os.path.splitext(self.filepath)
|
||||
if os.path.exists(name):
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
message=f"Please delete the '{os.path.basename(name)}' folder before exporting.",
|
||||
)
|
||||
else:
|
||||
baseDir = self.create_structure(name)
|
||||
context.window_manager.progress_update(30)
|
||||
self.create_files(baseDir)
|
||||
context.window_manager.progress_update(90)
|
||||
self.zip_addon(name)
|
||||
bpy.ops.sn.export_to_marketplace("INVOKE_DEFAULT")
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, message=f"Error: {e}")
|
||||
bpy.context.scene.sn.is_exporting = False
|
||||
context.window_manager.progress_end()
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
version = ".".join([str(i) for i in context.scene.sn.version])
|
||||
self.filepath = f"{context.scene.sn.module_name}_{version}.blend"
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
@@ -0,0 +1,135 @@
|
||||
import bpy
|
||||
from ...interface.panels.graph_ui_list import get_selected_graph
|
||||
|
||||
|
||||
class SN_GraphCategory(bpy.types.PropertyGroup):
|
||||
|
||||
def set_name(self, value):
|
||||
for ntree in bpy.data.node_groups:
|
||||
if hasattr(ntree, "category"):
|
||||
if ntree.category and ntree.category == self.name:
|
||||
ntree.category = value
|
||||
self["name"] = value
|
||||
|
||||
def get_name(self):
|
||||
return self.get("name", "New Category")
|
||||
|
||||
name: bpy.props.StringProperty(name="Name", default="New Category",
|
||||
description="The name of this graph category",
|
||||
set=set_name, get=get_name)
|
||||
|
||||
|
||||
class SN_OT_AddGraphCategory(bpy.types.Operator):
|
||||
bl_idname = "sn.add_graph_category"
|
||||
bl_label = "Add Graph Category"
|
||||
bl_description = "Adds a graph category"
|
||||
bl_options = {"REGISTER", "INTERNAL", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.sn.graph_categories.add()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_RemoveGraphCategory(bpy.types.Operator):
|
||||
bl_idname = "sn.remove_graph_category"
|
||||
bl_label = "Remove Graph Category"
|
||||
bl_description = "Removes a graph category"
|
||||
bl_options = {"REGISTER", "INTERNAL", "UNDO"}
|
||||
|
||||
index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.sn.graph_categories.remove(self.index)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_EditGraphCategories(bpy.types.Operator):
|
||||
bl_idname = "sn.edit_graph_categories"
|
||||
bl_label = "Edit Graph Categories"
|
||||
bl_description = "Edit the addon graph categories"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="Categories")
|
||||
for i, cat in enumerate(context.scene.sn.graph_categories):
|
||||
row = layout.row()
|
||||
row.scale_y = 1.2
|
||||
row.prop(cat, "name", text="")
|
||||
row.operator("sn.remove_graph_category", text="", icon="REMOVE", emboss=False).index = i
|
||||
|
||||
if not context.scene.sn.graph_categories:
|
||||
row = layout.row()
|
||||
row.enabled = False
|
||||
row.label(text="No categories added", icon="ERROR")
|
||||
|
||||
row = layout.row()
|
||||
row.scale_y = 1.2
|
||||
row.operator("sn.add_graph_category", text="Add Category", icon="ADD")
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_popup(self, width=250)
|
||||
|
||||
|
||||
|
||||
class SN_OT_MoveGraphToCategory(bpy.types.Operator):
|
||||
bl_idname = "sn.move_graph_to_category"
|
||||
bl_label = "Move Graph Category"
|
||||
bl_description = "Move the selected graph to a different category"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
category: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
ntree = get_selected_graph()
|
||||
if ntree:
|
||||
if self.category == -1:
|
||||
ntree.category = "OTHER"
|
||||
else:
|
||||
ntree.category = context.scene.sn.graph_categories[self.category].name
|
||||
context.area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_MoveGraphCategory(bpy.types.Operator):
|
||||
bl_idname = "sn.move_graph_category"
|
||||
bl_label = "Move Graph Category"
|
||||
bl_description = "Move the selected graph to a different category"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
ntree = get_selected_graph()
|
||||
|
||||
layout.label(text="Categories")
|
||||
for i, cat in enumerate(context.scene.sn.graph_categories):
|
||||
row = layout.row()
|
||||
row.enabled = ntree != None and ntree.category != cat.name
|
||||
row.scale_y = 1.2
|
||||
row.operator("sn.move_graph_to_category", text=f"Move to '{cat.name}'", icon="FORWARD").category = i
|
||||
|
||||
row = layout.row()
|
||||
row.enabled = ntree != None and ntree.category and ntree.category != "OTHER"
|
||||
row.scale_y = 1.2
|
||||
row.operator("sn.move_graph_to_category", text=f"Remove Category", icon="REMOVE").category = -1
|
||||
|
||||
if not len(context.scene.sn.graph_categories):
|
||||
row = layout.row()
|
||||
row.enabled = False
|
||||
row.label(text="No categories added", icon="ERROR")
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.scene.sn.node_tree_index = self.index
|
||||
return context.window_manager.invoke_popup(self, width=250)
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
import bpy
|
||||
import os
|
||||
|
||||
from ...interface.panels.graph_ui_list import get_selected_graph, get_selected_graph_offset
|
||||
from ...nodes.compiler import unregister_addon, compile_addon
|
||||
|
||||
|
||||
|
||||
def get_serpens_graphs():
|
||||
graphs = []
|
||||
for group in bpy.data.node_groups:
|
||||
if group.bl_idname == "ScriptingNodesTree":
|
||||
graphs.append(group)
|
||||
return graphs
|
||||
|
||||
|
||||
def reassign_tree_indices():
|
||||
trees = []
|
||||
for ngroup in bpy.data.node_groups:
|
||||
if ngroup.bl_idname == "ScriptingNodesTree":
|
||||
trees.append(ngroup)
|
||||
trees = sorted(trees, key=lambda tree: tree.index)
|
||||
|
||||
for i in range(len(trees)):
|
||||
trees[i].index = i
|
||||
return trees
|
||||
|
||||
|
||||
|
||||
class SN_OT_AddGraph(bpy.types.Operator):
|
||||
bl_idname = "sn.add_graph"
|
||||
bl_label = "Add Node Tree"
|
||||
bl_description = "Adds a node tree to the addon"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
sn = context.scene.sn
|
||||
trees = reassign_tree_indices()
|
||||
|
||||
curr_index = 0
|
||||
if sn.node_tree_index < len(bpy.data.node_groups) and bpy.data.node_groups[sn.node_tree_index].bl_idname == "ScriptingNodesTree":
|
||||
curr_index = bpy.data.node_groups[sn.node_tree_index].index
|
||||
for i in range(curr_index+1, len(trees)):
|
||||
trees[i].index += 1
|
||||
|
||||
graph = bpy.data.node_groups.new("NodeTree", "ScriptingNodesTree")
|
||||
graph.index = curr_index - 1
|
||||
if sn.active_graph_category != "ALL":
|
||||
graph.category = sn.active_graph_category
|
||||
|
||||
for i, group in enumerate(bpy.data.node_groups):
|
||||
if group == graph:
|
||||
sn.node_tree_index = i
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_RemoveGraph(bpy.types.Operator):
|
||||
bl_idname = "sn.remove_graph"
|
||||
bl_label = "Remove Node Tree"
|
||||
bl_description = "Removes this node tree from the addon"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if context.scene.sn.node_tree_index < len(bpy.data.node_groups):
|
||||
return bpy.data.node_groups[context.scene.sn.node_tree_index].bl_idname == "ScriptingNodesTree"
|
||||
|
||||
def execute(self, context):
|
||||
sn = context.scene.sn
|
||||
group = bpy.data.node_groups[sn.node_tree_index]
|
||||
curr_index = group.index
|
||||
bpy.data.node_groups.remove(group)
|
||||
|
||||
trees = reassign_tree_indices()
|
||||
for tree in trees:
|
||||
if tree.index == curr_index:
|
||||
for i, ntree in enumerate(bpy.data.node_groups):
|
||||
if ntree == tree:
|
||||
sn.node_tree_index = i
|
||||
break
|
||||
elif tree.index == curr_index - 1:
|
||||
for i, ntree in enumerate(bpy.data.node_groups):
|
||||
if ntree == tree:
|
||||
sn.node_tree_index = i
|
||||
break
|
||||
else:
|
||||
sn.node_tree_index = 0
|
||||
|
||||
|
||||
compile_addon()
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_confirm(self, event)
|
||||
|
||||
|
||||
|
||||
class SN_OT_AppendGraph(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "sn.append_graph"
|
||||
bl_label = "Append Node Tree"
|
||||
bl_description = "Appends a node tree from another file to this addon"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
filter_glob: bpy.props.StringProperty( default='*.blend', options={'HIDDEN'} )
|
||||
|
||||
def execute(self, context):
|
||||
_, extension = os.path.splitext(self.filepath)
|
||||
if extension == ".blend":
|
||||
bpy.ops.sn.append_popup("INVOKE_DEFAULT", path=self.filepath)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_AppendPopup(bpy.types.Operator):
|
||||
bl_idname = "sn.append_popup"
|
||||
bl_label = "Append Node Tree"
|
||||
bl_description = "Appends this node tree from the addon"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def get_graph_items(self, context):
|
||||
""" Returns all node trees that can be found in the selected file """
|
||||
items = []
|
||||
with bpy.data.libraries.load(self.path) as (data_from, _):
|
||||
for group in data_from.node_groups:
|
||||
items.append((group, group, group))
|
||||
if not items:
|
||||
items = [("NONE", "NONE", "NONE")]
|
||||
return items
|
||||
|
||||
path: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
|
||||
graph: bpy.props.EnumProperty(name="Node Tree",
|
||||
description="Node Tree to import",
|
||||
items=get_graph_items,
|
||||
options={"HIDDEN", "SKIP_SAVE"})
|
||||
|
||||
def execute(self, context):
|
||||
if self.graph != "NONE":
|
||||
# save previous groups
|
||||
prev_groups = bpy.data.node_groups.values()
|
||||
|
||||
# append node group
|
||||
with bpy.data.libraries.load(self.path) as (_, data_to):
|
||||
data_to.node_groups = [self.graph]
|
||||
|
||||
# register new graph
|
||||
new_groups = set(prev_groups) ^ set(bpy.data.node_groups.values())
|
||||
for group in new_groups:
|
||||
context.scene.sn.node_tree_index = bpy.data.node_groups.values().index(group)
|
||||
compile_addon()
|
||||
|
||||
# redraw screen
|
||||
context.area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
if self.graph == "NONE":
|
||||
self.layout.label(text="No Node Trees found in this blend file",icon="ERROR")
|
||||
else:
|
||||
self.layout.prop(self, "graph", text="Node Tree")
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
|
||||
|
||||
class SN_OT_ForceCompile(bpy.types.Operator):
|
||||
bl_idname = "sn.force_compile"
|
||||
bl_label = "This might be slow for large addons!"
|
||||
bl_description = "Forces all node trees to compile"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
def fix_compile_order(self, refs):
|
||||
for node in refs.nodes:
|
||||
if node.order == 0:
|
||||
node.order = 3
|
||||
|
||||
def execute(self, context):
|
||||
for ntree in bpy.data.node_groups:
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
for refs in ntree.node_refs:
|
||||
refs.clear_unused_refs()
|
||||
refs.fix_ref_names()
|
||||
if refs.name == "SN_OnKeypressNode":
|
||||
self.fix_compile_order(refs)
|
||||
ntree.reevaluate()
|
||||
compile_addon()
|
||||
self.report({"INFO"}, message="Compiled successfully!")
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_confirm(self, event)
|
||||
|
||||
|
||||
|
||||
class SN_OT_ForceUnregister(bpy.types.Operator):
|
||||
bl_idname = "sn.force_unregister"
|
||||
bl_label = "Force Unregister"
|
||||
bl_description = "Forces all node trees to unregister"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
unregister_addon()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_MoveNodeTree(bpy.types.Operator):
|
||||
bl_idname = "sn.move_node_tree"
|
||||
bl_label = "Move Node Tree"
|
||||
bl_description = "Moves this node tree in the list"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
move_up: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
|
||||
|
||||
def execute(self, context):
|
||||
reassign_tree_indices()
|
||||
|
||||
ntree = get_selected_graph()
|
||||
before = get_selected_graph_offset(-1)
|
||||
after = get_selected_graph_offset(1)
|
||||
|
||||
# move trees
|
||||
if ntree:
|
||||
if self.move_up and before:
|
||||
temp_index = ntree.index
|
||||
ntree.index = before.index
|
||||
before.index = temp_index
|
||||
elif not self.move_up and after:
|
||||
temp_index = ntree.index
|
||||
ntree.index = after.index
|
||||
after.index = temp_index
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,138 @@
|
||||
import bpy
|
||||
import json
|
||||
|
||||
|
||||
class SN_OT_CopyCommand(bpy.types.Operator):
|
||||
bl_idname = "sn.copy_command"
|
||||
bl_label = "Copy Command"
|
||||
bl_description = "Copies the command to post in the discord server"
|
||||
bl_options = {"REGISTER","UNDO","INTERNAL"}
|
||||
|
||||
url: bpy.props.StringProperty(default="",options={"SKIP_SAVE"})
|
||||
price: bpy.props.StringProperty(default="Free",options={"SKIP_SAVE"})
|
||||
blender: bpy.props.BoolProperty(default=False,options={"SKIP_SAVE"})
|
||||
|
||||
def execute(self, context):
|
||||
sn = bpy.context.scene.sn
|
||||
|
||||
addon_info = {
|
||||
"name": sn.addon_name,
|
||||
"description": sn.description,
|
||||
"category": sn.category if not sn.category == 'CUSTOM' else sn.custom_category,
|
||||
"author": sn.author,
|
||||
"blender_version": list(tuple(sn.blender)),
|
||||
"addon_version": list(tuple(sn.version)),
|
||||
"external": self.url != "",
|
||||
"url": self.url,
|
||||
"price": self.price,
|
||||
"blend": self.blender,
|
||||
"blend_url": "",
|
||||
"user": 0,
|
||||
"serpens_version": 3
|
||||
}
|
||||
|
||||
if self.url == "":
|
||||
addon_info["url"] = ""
|
||||
addon_info["price"] = ""
|
||||
|
||||
bpy.context.window_manager.clipboard = json.dumps(addon_info)
|
||||
|
||||
self.report({"INFO"},message="Copied successfully!")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SN_OT_ExportToMarketplaceAddon(bpy.types.Operator):
|
||||
bl_idname = "sn.export_to_marketplace"
|
||||
bl_label = "Export Addon To Marketplace"
|
||||
bl_description = "Exports the active node tree to the marketplace"
|
||||
bl_options = {"REGISTER","UNDO","INTERNAL"}
|
||||
|
||||
url: bpy.props.StringProperty(default="",name="Addon URL",description="Enter the url to your addon here")
|
||||
price: bpy.props.StringProperty(default="Free",name="Addon Price",description="Enter the price of your addon here")
|
||||
blender: bpy.props.BoolProperty(default=False,options={"SKIP_SAVE"})
|
||||
|
||||
upload_type: bpy.props.EnumProperty(name="Upload Type",items=[("DIRECT","Direct Upload","Upload the addon directly"),("URL","External Link","Provide an external url for your addon")])
|
||||
|
||||
expand_1: bpy.props.BoolProperty(default=True,name="Expand")
|
||||
expand_2: bpy.props.BoolProperty(default=False,name="Expand")
|
||||
expand_3: bpy.props.BoolProperty(default=False,name="Expand")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self,context):
|
||||
box = self.layout.box()
|
||||
box.label(text="If you think your addon is interesting to others you can share it!",icon="FUND")
|
||||
|
||||
self.layout.separator()
|
||||
|
||||
self.layout.prop(self,"upload_type",expand=True,text=" ")
|
||||
|
||||
self.layout.separator()
|
||||
|
||||
box = self.layout.box()
|
||||
box.prop(self,"expand_1",text="Step 1",emboss=False,toggle=True,icon="DISCLOSURE_TRI_DOWN" if self.expand_1 else "DISCLOSURE_TRI_RIGHT")
|
||||
if self.expand_1:
|
||||
col = box.column(align=True)
|
||||
col.label(text=" • Set your addons name, description, version, ...")
|
||||
col.label(text=" • If you want to update an addon use the same name")
|
||||
col.label(text=" • Select if you want to upload your blend file with the node tree")
|
||||
col.separator()
|
||||
row = col.row(align=True)
|
||||
row.label(icon="BLANK1")
|
||||
row.prop(self,"blender", text="Upload blend file")
|
||||
row.label(icon="BLANK1")
|
||||
|
||||
self.layout.separator()
|
||||
|
||||
box = self.layout.box()
|
||||
box.prop(self,"expand_2",text="Step 2",emboss=False,toggle=True,icon="DISCLOSURE_TRI_DOWN" if self.expand_2 else "DISCLOSURE_TRI_RIGHT")
|
||||
if self.expand_2:
|
||||
col = box.column(align=False)
|
||||
|
||||
if self.upload_type == "DIRECT":
|
||||
col.label(text=" • Go in the #marketplace channel on discord and post the following:")
|
||||
row = col.row(align=True)
|
||||
split = row.split(factor=0.03)
|
||||
split.label(text=" ")
|
||||
op = split.operator("sn.copy_command",text="Click To Copy!",icon="COPYDOWN")
|
||||
op.blender = self.blender
|
||||
row.operator("wm.url_open",text="",icon_value=bpy.context.scene.sn_icons[ "discord" ].icon_id).url = "https://discord.com/invite/NK6kyae"
|
||||
row.label(text=" ")
|
||||
col.separator()
|
||||
col.label(text=" • You will be asked to upload your addon. Export it and do so.")
|
||||
|
||||
elif self.upload_type == "URL":
|
||||
row = col.row()
|
||||
row.label(text=" • Paste the link to your addon in here:")
|
||||
row.prop(self,"url", text="")
|
||||
col.separator()
|
||||
row = col.row()
|
||||
row.label(text=" • Enter the price of your addon here:")
|
||||
row.prop(self,"price", text="")
|
||||
|
||||
if self.upload_type == "URL":
|
||||
self.layout.separator()
|
||||
|
||||
box = self.layout.box()
|
||||
box.prop(self,"expand_3",text="Step 3",emboss=False,toggle=True,icon="DISCLOSURE_TRI_DOWN" if self.expand_3 else "DISCLOSURE_TRI_RIGHT")
|
||||
if self.expand_3:
|
||||
col = box.column(align=False)
|
||||
col.label(text=" • Go in the #marketplace channel on discord and post the following:")
|
||||
row = col.row(align=True)
|
||||
split = row.split(factor=0.03)
|
||||
split.label(text=" ")
|
||||
op = split.operator("sn.copy_command",text="Click To Copy!",icon="COPYDOWN")
|
||||
op.url = self.url
|
||||
op.price = self.price
|
||||
op.blender = self.blender
|
||||
row.operator("wm.url_open",text="",icon_value=bpy.context.scene.sn_icons[ "discord" ].icon_id).url = "https://discord.com/invite/NK6kyae"
|
||||
row.label(text=" ")
|
||||
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_popup(self, width=500)
|
||||
@@ -0,0 +1,85 @@
|
||||
import bpy
|
||||
|
||||
|
||||
|
||||
node_cache = {} # stores a cache of the nodes with key f"{node_tree.name};{node.static_uid}"
|
||||
|
||||
class NodeRef(bpy.types.PropertyGroup):
|
||||
|
||||
@property
|
||||
def node(self):
|
||||
node_tree = self.id_data
|
||||
# retrieve node from cache
|
||||
if f"{node_tree.name};{self.uid}" in node_cache:
|
||||
return node_cache[f"{node_tree.name};{self.uid}"]
|
||||
# save node to cache
|
||||
for node in node_tree.nodes:
|
||||
if getattr(node, "static_uid", None) == self.uid:
|
||||
node_cache[f"{node_tree.name};{node.static_uid}"] = node
|
||||
return node
|
||||
return None
|
||||
|
||||
def set_name(self, value):
|
||||
prev_name = self.get("name", "")
|
||||
self["name"] = value
|
||||
|
||||
# update references
|
||||
if prev_name:
|
||||
ref_node = self.node
|
||||
for ntree in bpy.data.node_groups:
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
for node in ntree.nodes:
|
||||
if getattr(node, "ref_ntree", None) == ref_node.node_tree:
|
||||
# update specific node type references
|
||||
if getattr(node, f"ref_{ref_node.collection_key}", None) == prev_name:
|
||||
setattr(node, f"ref_{ref_node.collection_key}", value)
|
||||
# update node names for any type of node
|
||||
elif getattr(node, "from_node", None) == prev_name:
|
||||
setattr(node, f"from_node", value)
|
||||
|
||||
def get_name(self):
|
||||
return self.get("name", "")
|
||||
|
||||
uid: bpy.props.StringProperty(name="UID",
|
||||
description="The static_uid of the node that belongs to this reference")
|
||||
|
||||
name: bpy.props.StringProperty(name="Name",
|
||||
set=set_name,
|
||||
get=get_name,
|
||||
description="The name of the node this reference belongs to")
|
||||
|
||||
|
||||
|
||||
class NodeRefCollection(bpy.types.PropertyGroup):
|
||||
|
||||
name: bpy.props.StringProperty(name="Node Name",
|
||||
description="The idname of the nodes in this collection")
|
||||
|
||||
refs: bpy.props.CollectionProperty(type=NodeRef,
|
||||
name="References",
|
||||
description="References to the nodes of this type")
|
||||
|
||||
def get_ref_by_uid(self, uid):
|
||||
for ref in self.refs:
|
||||
if ref.uid == uid:
|
||||
return ref
|
||||
return None
|
||||
|
||||
def clear_unused_refs(self):
|
||||
""" Removes all references that don't match a node """
|
||||
for i in range(len(self.refs)-1, -1, -1):
|
||||
if not self.refs[i].node:
|
||||
self.refs.remove(i)
|
||||
|
||||
def fix_ref_names(self):
|
||||
""" Makes sure all ref names match the node names """
|
||||
for ref in self.refs:
|
||||
if ref.get("name") != ref.node.name:
|
||||
ref["name"] = ref.node.name
|
||||
ref.node.on_node_name_change()
|
||||
# ref.node._evaluate(bpy.context)
|
||||
|
||||
@property
|
||||
def nodes(self):
|
||||
""" Returns all the nodes for this collection """
|
||||
return [ref.node for ref in self.refs]
|
||||
@@ -0,0 +1,295 @@
|
||||
import bpy
|
||||
from ..sockets.conversions import CONVERSIONS
|
||||
from .node_refs import NodeRefCollection
|
||||
from ...addon.variables.variables import SN_VariableProperties
|
||||
from ...utils import unique_collection_name, get_python_name
|
||||
|
||||
|
||||
class ScriptingNodesTree(bpy.types.NodeTree):
|
||||
bl_idname = "ScriptingNodesTree"
|
||||
bl_label = "Visual Scripting Editor"
|
||||
bl_icon = "FILE_SCRIPT"
|
||||
is_sn = True
|
||||
type: bpy.props.EnumProperty(items=[("SCRIPTING", "Scripting", "Scripting")
|
||||
],
|
||||
name="Type")
|
||||
|
||||
index: bpy.props.IntProperty(
|
||||
default=0,
|
||||
description="The index of this node tree in the node tree list",
|
||||
name="Index",
|
||||
)
|
||||
|
||||
category: bpy.props.StringProperty(
|
||||
name="Category",
|
||||
default="OTHER",
|
||||
description="The category this property is displayed in",
|
||||
)
|
||||
|
||||
link_cache = (
|
||||
{}
|
||||
) # stores cache of the links from the previous update for all node trees based on their memory adress
|
||||
|
||||
variables: bpy.props.CollectionProperty(
|
||||
type=SN_VariableProperties,
|
||||
name="Variables",
|
||||
description="The variables of this node tree",
|
||||
)
|
||||
|
||||
variable_index: bpy.props.IntProperty(
|
||||
name="Variable Index",
|
||||
min=0,
|
||||
description="Index of the selected variable")
|
||||
|
||||
node_refs: bpy.props.CollectionProperty(
|
||||
type=NodeRefCollection,
|
||||
name="Node References",
|
||||
description=
|
||||
"A collection of groups that hold references to nodes of a specific idname",
|
||||
)
|
||||
|
||||
show_debug: bpy.props.BoolProperty(
|
||||
name="Show Debug",
|
||||
default=True,
|
||||
description="Show node tree data in the debug panel",
|
||||
)
|
||||
|
||||
# cache python names so they only have to be generated once
|
||||
cached_python_names = {}
|
||||
cached_python_name: bpy.props.StringProperty()
|
||||
cached_human_name: bpy.props.StringProperty()
|
||||
|
||||
@property
|
||||
def python_name(self):
|
||||
if self.name == self.cached_human_name and self.cached_python_name:
|
||||
return self.cached_python_name
|
||||
if self.name in self.cached_python_names:
|
||||
return self.cached_python_names[self.name]
|
||||
|
||||
names = []
|
||||
for ntree in bpy.data.node_groups:
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
if ntree == self:
|
||||
break
|
||||
names.append(ntree.python_name)
|
||||
|
||||
name = unique_collection_name(
|
||||
f"{get_python_name(self.name, 'node_tree')}", "node_tree", names,
|
||||
"_")
|
||||
try:
|
||||
self.cached_python_name = name
|
||||
self.cached_human_name = self.name
|
||||
except AttributeError:
|
||||
pass
|
||||
self.cached_python_names[self.name] = name
|
||||
return name
|
||||
|
||||
def node_collection(self, idname):
|
||||
"""Returns the collection for the given node idname refs in this node trees"""
|
||||
if idname in self.node_refs:
|
||||
return self.node_refs[idname]
|
||||
return self.node_refs["empty"]
|
||||
|
||||
def _map_link_to_sockets(self, link):
|
||||
"""Maps the given link to a tuple of the from socket, to socket and the link itself"""
|
||||
from_real = None
|
||||
if getattr(link.to_socket, "is_sn", False):
|
||||
from_real = link.to_socket.from_socket()
|
||||
return (link.from_socket, link.to_socket, from_real, link)
|
||||
|
||||
def is_valid_connection(self, from_out, to_inp):
|
||||
"""Check if a connection between the given sockets would be valid"""
|
||||
if from_out and from_out.is_program == to_inp.is_program:
|
||||
# check if multiple program sockets are connected
|
||||
if to_inp.is_program:
|
||||
to_sockets = from_out.to_sockets(check_validity=False)
|
||||
if from_out.bl_label == to_inp.bl_label:
|
||||
# check if first same program socket
|
||||
for socket in to_sockets:
|
||||
if socket.bl_label == to_inp.bl_label:
|
||||
if socket == to_inp:
|
||||
return True
|
||||
else:
|
||||
break
|
||||
return False
|
||||
# data types are the same
|
||||
elif from_out.bl_label == to_inp.bl_label:
|
||||
return True
|
||||
# check if data types are convertible
|
||||
else:
|
||||
if not to_inp.convert_data:
|
||||
return True
|
||||
if from_out.bl_label in CONVERSIONS:
|
||||
if to_inp.bl_label in CONVERSIONS[from_out.bl_label]:
|
||||
return True
|
||||
return False
|
||||
return False
|
||||
|
||||
def is_valid_link(self, link):
|
||||
"""Checks if the given link is valid"""
|
||||
# all links connected to reroutes inputs are valid
|
||||
if not getattr(link.to_socket, "is_sn", False):
|
||||
return True
|
||||
|
||||
# get the sockets and return their validity
|
||||
to_inp = link.to_socket
|
||||
from_out = link.to_socket.from_socket()
|
||||
return self.is_valid_connection(from_out, to_inp)
|
||||
|
||||
def _insert_define_data_nodes(self, links):
|
||||
"""Inserts define data nodes for all links invalid links"""
|
||||
for link in links:
|
||||
# add define data node
|
||||
if not getattr(link.from_socket, "changeable", False):
|
||||
node = self.nodes.new("SN_DefineDataType")
|
||||
if link.to_socket.bl_idname in list(
|
||||
map(lambda item: item[0],
|
||||
node.get_data_items(bpy.context))):
|
||||
node.convert_to = link.to_socket.bl_idname
|
||||
node.location = (
|
||||
(link.from_node.location[0] + link.to_node.location[0]) /
|
||||
2,
|
||||
(link.from_node.location[1] + link.to_node.location[1]) /
|
||||
2,
|
||||
)
|
||||
self.links.new(link.from_socket, node.inputs[0])
|
||||
self.links.new(node.outputs[0], link.to_socket)
|
||||
# change data output
|
||||
elif link.from_socket.bl_label == "Data":
|
||||
to_sockets = link.from_socket.to_sockets(False)
|
||||
if not link.from_socket.dynamic and len(to_sockets) == 1:
|
||||
to_socket = to_sockets[0]
|
||||
if to_socket.bl_idname in list(
|
||||
map(
|
||||
lambda item: item[0],
|
||||
link.from_socket.get_data_type_items(
|
||||
bpy.context),
|
||||
)):
|
||||
link.from_socket.data_type = to_socket.bl_idname
|
||||
# link.from_socket.subtype = to_socket.subtype
|
||||
|
||||
def _update_post(self):
|
||||
"""Only do visual aspects in here as this is run after evaluating the nodes"""
|
||||
# mark links as invalid
|
||||
data_links = []
|
||||
for link in self.links:
|
||||
if not self.is_valid_link(link):
|
||||
link.is_valid = False
|
||||
if link.from_socket.bl_label == "Data":
|
||||
data_links.append(link)
|
||||
self._insert_define_data_nodes(data_links)
|
||||
|
||||
def _find_node_from_socket(self, socket):
|
||||
for node in self.nodes:
|
||||
for s in [*node.inputs, *node.outputs]:
|
||||
if s == socket:
|
||||
return node
|
||||
return None
|
||||
|
||||
def _update_changed_links(self, links):
|
||||
"""Forces the affected nodes to update depending on if it's a program or data socket"""
|
||||
for from_out, to_inp, _, _ in links:
|
||||
to_inp_node = self._find_node_from_socket(to_inp)
|
||||
from_out_node = self._find_node_from_socket(from_out)
|
||||
# update data sockets
|
||||
try:
|
||||
if (getattr(to_inp, "is_sn", False) and to_inp_node
|
||||
and not to_inp.is_program):
|
||||
to_inp.force_update()
|
||||
# update program sockets
|
||||
elif (from_out_node and getattr(from_out, "is_sn", False)
|
||||
and from_out.is_program):
|
||||
from_out.force_update()
|
||||
except:
|
||||
pass
|
||||
|
||||
def _call_link_inserts(self, added):
|
||||
"""Calls link_insert for all new links"""
|
||||
for from_inp, to_inp, from_real, _ in added:
|
||||
if from_real:
|
||||
from_real.node.link_insert(from_real, to_inp, is_output=True)
|
||||
to_inp.node.link_insert(from_real, to_inp, is_output=False)
|
||||
elif (from_inp and getattr(from_inp.node, "is_sn", False)
|
||||
and to_inp and getattr(to_inp.node, "is_sn", False)):
|
||||
from_inp.node.link_insert(from_inp, to_inp, is_output=True)
|
||||
|
||||
def _call_link_removes(self, removed):
|
||||
"""Calls link_remove for all removed links"""
|
||||
for _, to_inp, from_real, _ in removed:
|
||||
if from_real:
|
||||
node = self._find_node_from_socket(from_real)
|
||||
if node:
|
||||
node.link_remove(from_real, to_inp, is_output=True)
|
||||
node = self._find_node_from_socket(to_inp)
|
||||
if node:
|
||||
node.link_remove(from_real, to_inp, is_output=False)
|
||||
|
||||
def _update_added_links(self, added):
|
||||
"""Triggers an update on the given links data outputs and program inputs to update the affected program"""
|
||||
self._update_changed_links(added)
|
||||
self._call_link_inserts(added)
|
||||
|
||||
def _update_removed_links(self, removed):
|
||||
"""Triggers an update on the given links data inputs and program outputs to update the affected program"""
|
||||
self._update_changed_links(removed)
|
||||
self._call_link_removes(removed)
|
||||
|
||||
def _update_tree_links(self):
|
||||
"""Finds all changed node links and updates the connections"""
|
||||
# get current links
|
||||
curr_links = list(map(self._map_link_to_sockets, self.links.values()))
|
||||
|
||||
if id(self) in self.link_cache:
|
||||
# update added links
|
||||
added = list(set(curr_links) - set(self.link_cache[id(self)]))
|
||||
self._update_added_links(added)
|
||||
# update removed links
|
||||
removed = list(set(self.link_cache[id(self)]) - set(curr_links))
|
||||
self._update_removed_links(removed)
|
||||
|
||||
# update cached current links
|
||||
self.link_cache[id(self)] = curr_links
|
||||
|
||||
# calls a function after the links are realized
|
||||
bpy.app.timers.register(self._update_post, first_interval=0.001)
|
||||
|
||||
def _update_reroutes(self):
|
||||
"""Updates all inputs and display shapes of the reroutes in this node tree"""
|
||||
for reroute in self.nodes:
|
||||
if reroute.bl_idname == "NodeReroute":
|
||||
try:
|
||||
connections_left = [
|
||||
x.from_socket for x in reroute.inputs[0].links
|
||||
]
|
||||
connections_right = [
|
||||
x.to_socket for x in reroute.outputs[0].links
|
||||
]
|
||||
if reroute.inputs[0].bl_idname != "SN_RerouteSocket":
|
||||
reroute.inputs.remove(reroute.inputs[0])
|
||||
reroute.outputs.remove(reroute.outputs[0])
|
||||
i = reroute.inputs.new("SN_RerouteSocket", "Input")
|
||||
o = reroute.outputs.new("SN_RerouteSocket", "Output")
|
||||
for c in connections_left:
|
||||
self.links.new(c, i)
|
||||
for c in connections_right:
|
||||
self.links.new(c, o)
|
||||
reroute.inputs[0].display_shape = (
|
||||
connections_left[0].display_shape
|
||||
if connections_left else "CIRCLE")
|
||||
reroute.outputs[0].display_shape = (
|
||||
connections_left[0].display_shape
|
||||
if connections_left else "CIRCLE")
|
||||
except:
|
||||
pass
|
||||
|
||||
def update(self):
|
||||
# update tree links
|
||||
self._update_tree_links()
|
||||
self._update_reroutes()
|
||||
|
||||
def reevaluate(self):
|
||||
"""Reevaluates all nodes in this node tree"""
|
||||
# evaluate all nodes
|
||||
for node in self.nodes:
|
||||
if getattr(node, "is_sn", False):
|
||||
node._evaluate(bpy.context)
|
||||
@@ -0,0 +1,69 @@
|
||||
import bpy
|
||||
|
||||
|
||||
class SN_OT_FindReferencingNodes(bpy.types.Operator):
|
||||
bl_idname = "sn.find_referencing_nodes"
|
||||
bl_label = "Find Referencing Nodes"
|
||||
bl_description = "Find all nodes that reference this node"
|
||||
bl_options = {'REGISTER', 'UNDO', "INTERNAL"}
|
||||
|
||||
node: bpy.props.StringProperty(name="Node", options={'HIDDEN', 'SKIP_SAVE'})
|
||||
add_node: bpy.props.StringProperty(name="Add Node", options={'HIDDEN', 'SKIP_SAVE'})
|
||||
|
||||
references = {}
|
||||
|
||||
def execute(self, context):
|
||||
pass
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
if not self.node in context.space_data.node_tree.nodes: return
|
||||
ref_node = context.space_data.node_tree.nodes[self.node]
|
||||
|
||||
for key in self.references:
|
||||
layout.label(text=key)
|
||||
for ref in self.references[key]:
|
||||
op = layout.operator("sn.find_node", text=ref, icon="RESTRICT_SELECT_OFF")
|
||||
op.node_tree = key
|
||||
op.node = ref
|
||||
layout.separator()
|
||||
|
||||
if not self.references:
|
||||
layout.label(text="No references found", icon="INFO")
|
||||
|
||||
if self.add_node:
|
||||
op = layout.operator("sn.add_referencing_node", text="Add Node", icon="FORWARD")
|
||||
op.idname = self.add_node
|
||||
key = ref_node.bl_idname if not ref_node.collection_key_overwrite else ref_node.collection_key_overwrite
|
||||
op.ref_attr = f"ref_{key}"
|
||||
op.node = ref_node.name
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.references = {}
|
||||
ref_node = context.space_data.node_tree.nodes[self.node]
|
||||
for ngroup in bpy.data.node_groups:
|
||||
for node in ngroup.nodes:
|
||||
idname = ref_node.collection_key_overwrite if ref_node.collection_key_overwrite else ref_node.bl_idname
|
||||
if getattr(node, f"ref_{idname}", None) == ref_node.name and getattr(node, "ref_ntree", None) == ref_node.node_tree:
|
||||
if not ngroup.name in self.references:
|
||||
self.references[ngroup.name] = []
|
||||
self.references[ngroup.name].append(node.name)
|
||||
return context.window_manager.invoke_popup(self, width=250)
|
||||
|
||||
|
||||
|
||||
class SN_OT_AddReferencingNode(bpy.types.Operator):
|
||||
bl_idname = "sn.add_referencing_node"
|
||||
bl_label = "Add Node"
|
||||
bl_description = "Adds the referenced node to the node tree"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
idname: bpy.props.StringProperty(name="ID Name", options={'HIDDEN', 'SKIP_SAVE'})
|
||||
ref_attr: bpy.props.StringProperty(name="Attribute", options={'HIDDEN', 'SKIP_SAVE'})
|
||||
node: bpy.props.StringProperty(name="Node", options={'HIDDEN', 'SKIP_SAVE'})
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.node.add_node("INVOKE_DEFAULT", type=self.idname, use_transform=True)
|
||||
node = context.space_data.node_tree.nodes.active
|
||||
setattr(node, self.ref_attr, self.node)
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,178 @@
|
||||
from .. import auto_load
|
||||
import os
|
||||
import inspect
|
||||
import bpy
|
||||
from ..extensions import snippet_ops
|
||||
|
||||
|
||||
def flatten_snippets(data):
|
||||
flat = []
|
||||
if type(data) == str:
|
||||
flat.append(data)
|
||||
else:
|
||||
cat = bpy.context.scene.sn.snippet_categories[data["name"]]
|
||||
for item in data["snippets"]:
|
||||
flat.extend(
|
||||
list(map(lambda x: os.path.join(cat.path, x), flatten_snippets(item)))
|
||||
)
|
||||
return flat
|
||||
|
||||
|
||||
def get_snippet_list():
|
||||
flat_snippets = []
|
||||
for snippet in snippet_ops.loaded_snippets:
|
||||
flat_snippets.extend(flatten_snippets(snippet))
|
||||
flat_snippets = list(set(flat_snippets))
|
||||
|
||||
for i, snippet in enumerate(flat_snippets):
|
||||
if os.path.basename(snippet) == snippet:
|
||||
flat_snippets[i] = os.path.join(
|
||||
os.path.dirname(os.path.dirname(__file__)),
|
||||
"extensions",
|
||||
"snippets",
|
||||
snippet,
|
||||
)
|
||||
return flat_snippets
|
||||
|
||||
|
||||
class SN_MT_LayoutMenu(bpy.types.Menu):
|
||||
bl_idname = "SN_MT_LayoutMenu"
|
||||
bl_label = "Layout"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
op = layout.operator("node.add_node", text="Frame")
|
||||
op.type = "NodeFrame"
|
||||
op.use_transform = True
|
||||
|
||||
op = layout.operator("node.add_node", text="Portal")
|
||||
op.type = "SN_PortalNode"
|
||||
op.use_transform = True
|
||||
|
||||
op = layout.operator("node.add_node", text="Reroute")
|
||||
op.type = "NodeReroute"
|
||||
op.use_transform = True
|
||||
|
||||
|
||||
_node_categories = {}
|
||||
|
||||
|
||||
def get_node_categories():
|
||||
global _node_categories
|
||||
if _node_categories:
|
||||
return _node_categories
|
||||
else:
|
||||
node_categories = {}
|
||||
|
||||
for cls in auto_load.ordered_classes:
|
||||
if cls.bl_rna.base and cls.bl_rna.base.identifier == "Node":
|
||||
path = os.path.dirname(inspect.getfile(cls))
|
||||
dirs = path.split(os.sep)
|
||||
|
||||
if "nodes" in dirs:
|
||||
node_path = dirs[dirs.index("nodes") + 1 :]
|
||||
parent = node_categories
|
||||
for dir in node_path:
|
||||
if not dir in parent:
|
||||
parent[dir] = {}
|
||||
parent = parent[dir]
|
||||
if not "nodes" in parent:
|
||||
parent["nodes"] = []
|
||||
parent["nodes"].append(cls)
|
||||
|
||||
_node_categories = node_categories
|
||||
return node_categories
|
||||
|
||||
|
||||
blocklist = ["nodes", "Snippets", "Layout", "Legacy"]
|
||||
_registered_menus = []
|
||||
|
||||
|
||||
def register_node_menus():
|
||||
categories = get_node_categories()
|
||||
for cat in sorted(categories.keys()):
|
||||
if not cat in blocklist:
|
||||
register_menu(cat, cat)
|
||||
register_category_menus(categories[cat], cat)
|
||||
|
||||
|
||||
def register_category_menus(category, path):
|
||||
for cat in sorted(category.keys()):
|
||||
if not cat in blocklist:
|
||||
register_menu(cat, f"{path}.{cat}")
|
||||
register_category_menus(category[cat], f"{path}.{cat}")
|
||||
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.space_data.tree_type == "ScriptingNodesTree"
|
||||
|
||||
|
||||
def register_menu(name, path):
|
||||
menu_type = type(
|
||||
"SN_MT_category_" + name.replace(" ", "_"),
|
||||
(bpy.types.Menu,),
|
||||
{
|
||||
"bl_space_type": "NODE_EDITOR",
|
||||
"bl_label": name.replace("_", " ").title(),
|
||||
"path": path,
|
||||
"poll": poll,
|
||||
"draw": draw_submenu,
|
||||
},
|
||||
)
|
||||
bpy.utils.register_class(menu_type)
|
||||
_registered_menus.append(menu_type)
|
||||
|
||||
|
||||
def unregister_node_menus():
|
||||
for menu in _registered_menus:
|
||||
try:
|
||||
bpy.utils.unregister_class(menu)
|
||||
except:
|
||||
pass
|
||||
_registered_menus.clear()
|
||||
|
||||
|
||||
def draw_submenu(self, context):
|
||||
layout = self.layout
|
||||
|
||||
category = get_node_categories()
|
||||
for path in self.path.split("."):
|
||||
category = category[path]
|
||||
|
||||
for cat in sorted(category.keys()):
|
||||
if not cat in blocklist:
|
||||
layout.menu(
|
||||
"SN_MT_category_" + cat.replace(" ", "_"),
|
||||
text=cat.replace("_", " ").title(),
|
||||
)
|
||||
|
||||
if "nodes" in category and len(category["nodes"]) and len(category.keys()) > 1:
|
||||
layout.separator()
|
||||
|
||||
if "nodes" in category:
|
||||
for node in sorted(category["nodes"], key=lambda n: n.bl_label):
|
||||
op = layout.operator("node.add_node", text=node.bl_label)
|
||||
op.type = node.bl_idname
|
||||
op.use_transform = True
|
||||
|
||||
|
||||
def draw_node_menu(self, context):
|
||||
if context.space_data.tree_type != "ScriptingNodesTree":
|
||||
return
|
||||
categories = get_node_categories()
|
||||
layout = self.layout
|
||||
|
||||
layout.separator()
|
||||
for cat in sorted(categories.keys()):
|
||||
if not cat in blocklist:
|
||||
layout.menu(
|
||||
"SN_MT_category_" + cat.replace(" ", "_"),
|
||||
text=cat.replace("_", " ").title(),
|
||||
)
|
||||
|
||||
layout.menu("SN_MT_LayoutMenu", text="Layout")
|
||||
|
||||
layout.separator()
|
||||
layout.menu("SN_MT_PresetMenu", text="Presets")
|
||||
layout.menu("SN_MT_SnippetsMenu", text="Snippets")
|
||||
@@ -0,0 +1,498 @@
|
||||
import bpy
|
||||
from .conversions import CONVERSIONS
|
||||
from ...addon.properties.settings.settings import property_icons
|
||||
|
||||
import time
|
||||
|
||||
|
||||
class ScriptingSocket:
|
||||
### SOCKET GENERAL
|
||||
is_sn = True
|
||||
output_limit = 9999
|
||||
# OVERWRITE
|
||||
socket_shape = "CIRCLE" # CIRCLE | SQUARE | DIAMOND
|
||||
|
||||
def update_socket_name(self, context):
|
||||
self.node.on_socket_name_change(self)
|
||||
|
||||
name: bpy.props.StringProperty(name="Socket Name",
|
||||
description="Name of this socket",
|
||||
update=update_socket_name)
|
||||
|
||||
### SOCKET OPTIONS
|
||||
# OVERWRITE
|
||||
is_program = False # Only Interface and Execute sockets are programs
|
||||
dynamic: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
name="Dynamic",
|
||||
description="If this socket adds another socket when connected",
|
||||
)
|
||||
prev_dynamic: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
name="Previously Dynamic",
|
||||
description=
|
||||
"True if this socket was previously dynamic and can now be removed",
|
||||
)
|
||||
|
||||
def update_conversion(self, context):
|
||||
if self.is_linked:
|
||||
from_out = self.links[0].from_socket
|
||||
self.node.node_tree.links.remove(self.links[0])
|
||||
self.node.node_tree.links.new(from_out, self)
|
||||
|
||||
convert_data: bpy.props.BoolProperty(
|
||||
default=True,
|
||||
name="Convert Data",
|
||||
description="Convert the incoming data to this sockets type",
|
||||
update=update_conversion,
|
||||
)
|
||||
|
||||
def update_disabled(self, context):
|
||||
self.force_update()
|
||||
|
||||
disabled: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
name="Disabled",
|
||||
description="Disable this socket for this node",
|
||||
update=update_disabled,
|
||||
)
|
||||
|
||||
can_be_disabled: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
name="Can Be Hidden",
|
||||
description=
|
||||
"Lets the user disable this socket which can be used for evaluation",
|
||||
)
|
||||
|
||||
# OVERWRITE
|
||||
subtypes = [
|
||||
"NONE"
|
||||
] # possible subtypes for this data socket. Vector sockets should be seperate socket types, not subtypes (their size is a subtype)!
|
||||
subtype_values = {
|
||||
"NONE": "default_value"
|
||||
} # the matching propertie names for this data sockets subtype
|
||||
|
||||
def on_subtype_update(self):
|
||||
pass
|
||||
|
||||
def get_subtype_items(self, _):
|
||||
return [(name, name, name) for name in self.subtypes]
|
||||
|
||||
def update_subtype(self, _):
|
||||
self.force_update()
|
||||
self.on_subtype_update()
|
||||
self.node.location = self.node.location
|
||||
|
||||
subtype: bpy.props.EnumProperty(
|
||||
name="Subtype",
|
||||
description="The subtype of this socket",
|
||||
items=get_subtype_items,
|
||||
update=update_subtype,
|
||||
)
|
||||
|
||||
@property
|
||||
def subtype_attr(self):
|
||||
return self.subtype_values[self.subtype]
|
||||
|
||||
# INDEXING OPTIONS
|
||||
def set_hide(self, value):
|
||||
"""Sets the hide value of this socket and disconnects all links if hidden"""
|
||||
if value:
|
||||
for link in self.links:
|
||||
self.node.node_tree.links.remove(link)
|
||||
self.hide = value
|
||||
|
||||
def update_index_type(self, context):
|
||||
if self.indexable and self.bl_idname != self.node.socket_names[
|
||||
self.index_type]:
|
||||
# hide all index sockets before blend data input
|
||||
hide = self.index_type == "Property"
|
||||
for inp in self.node.inputs:
|
||||
if inp == self:
|
||||
hide = False
|
||||
if inp.indexable:
|
||||
inp.set_hide(hide)
|
||||
# convert socket
|
||||
self.node.convert_socket(self,
|
||||
self.node.socket_names[self.index_type])
|
||||
|
||||
indexable: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
name="Indexable",
|
||||
description=
|
||||
"If this socket is indexable. Switches between String, Integer and Blend Data",
|
||||
)
|
||||
|
||||
index_type: bpy.props.EnumProperty(
|
||||
name="Index Type",
|
||||
description="The type of index this socket indexes the property with",
|
||||
items=[
|
||||
("String", "Name", "Name", "SYNTAX_OFF", 0),
|
||||
("Integer", "Index", "Index", "DRIVER_TRANSFORM", 1),
|
||||
("Property", "Property", "Property", "MONKEY", 2),
|
||||
],
|
||||
update=update_index_type,
|
||||
)
|
||||
|
||||
def update_data_type(self, context):
|
||||
if self.changeable and self.data_type != self.bl_idname:
|
||||
self.node.convert_socket(self, self.data_type)
|
||||
|
||||
def get_data_type_items(self, context):
|
||||
items = []
|
||||
used_idnames = []
|
||||
for name in list(self.node.socket_names.keys())[2:]:
|
||||
if not self.node.socket_names[name] in used_idnames:
|
||||
items.append((
|
||||
self.node.socket_names[name],
|
||||
name,
|
||||
name,
|
||||
property_icons[name],
|
||||
len(items),
|
||||
))
|
||||
used_idnames.append(self.node.socket_names[name])
|
||||
return items
|
||||
|
||||
changeable: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
name="Changeable",
|
||||
description="If this data socket type can be changed",
|
||||
)
|
||||
|
||||
data_type: bpy.props.EnumProperty(
|
||||
name="The type this socket has right now",
|
||||
update=update_data_type,
|
||||
items=get_data_type_items,
|
||||
)
|
||||
|
||||
# VARIABLE SOCKET OPTIONS
|
||||
|
||||
is_variable: bpy.props.BoolProperty(
|
||||
name="Is Variable",
|
||||
description="If this socket is a variable socket that can be renamed",
|
||||
)
|
||||
|
||||
### DRAW SOCKET
|
||||
# OVERWRITE
|
||||
def draw_socket(self, context, layout, node, text, minimal=False):
|
||||
pass
|
||||
|
||||
def _draw_removable_socket(self, layout, node):
|
||||
"""Draws the operators for removable sockets"""
|
||||
op = layout.operator("sn.remove_socket",
|
||||
text="",
|
||||
emboss=False,
|
||||
icon="REMOVE")
|
||||
op.node = node.name
|
||||
op.is_output = self.is_output
|
||||
op.index = self.index
|
||||
|
||||
def _draw_dynamic_socket(self, layout, node, text):
|
||||
"""Draws the operators for dynamic sockets"""
|
||||
# draw socket label
|
||||
if self.is_output:
|
||||
layout.label(text=text)
|
||||
|
||||
# draw add operator
|
||||
op = layout.operator("sn.add_dynamic",
|
||||
text="",
|
||||
emboss=False,
|
||||
icon="ADD")
|
||||
op.node = node.name
|
||||
op.is_output = self.is_output
|
||||
op.insert_above = False
|
||||
op.index = self.index
|
||||
|
||||
# draw socket label
|
||||
if not self.is_output:
|
||||
layout.label(text=text)
|
||||
|
||||
def _draw_prev_dynamic_socket(self, context, layout, node):
|
||||
"""Draws the operators for previously dynamic sockets"""
|
||||
# draw remove socket
|
||||
self._draw_removable_socket(layout, node)
|
||||
|
||||
# draw add above operator
|
||||
if context.scene.sn.insert_sockets:
|
||||
op = layout.operator("sn.add_dynamic",
|
||||
text="",
|
||||
emboss=False,
|
||||
icon="TRIA_UP")
|
||||
op.node = node.name
|
||||
op.is_output = self.is_output
|
||||
op.insert_above = True
|
||||
op.index = self.index
|
||||
|
||||
def draw(self, context, layout, node, text):
|
||||
"""Draws this socket"""
|
||||
sn = context.scene.sn
|
||||
text = self.name
|
||||
# draw debug text for sockets
|
||||
if sn.debug_python_sockets and self.python_value:
|
||||
if not sn.debug_selected_only or (sn.debug_selected_only
|
||||
and self.node.select):
|
||||
text = self.python_value.replace("\n", " || ")
|
||||
# draw dynamic sockets
|
||||
if self.dynamic:
|
||||
self._draw_dynamic_socket(layout, node, text)
|
||||
# draw variable socket
|
||||
elif self.is_variable:
|
||||
# draw previously dynamic socket (with insert socket)
|
||||
if not self.is_output and self.prev_dynamic:
|
||||
self._draw_prev_dynamic_socket(context, layout, node)
|
||||
layout.prop(self, "name", text="")
|
||||
self.draw_socket(context, layout, node, "", minimal=True)
|
||||
# draw changeable socket
|
||||
if self.changeable:
|
||||
layout.separator()
|
||||
layout.prop(self, "data_type", icon_only=True)
|
||||
# draw previously dynamic socket (with insert socket)
|
||||
if self.is_output and self.prev_dynamic:
|
||||
self._draw_prev_dynamic_socket(context, layout, node)
|
||||
# draw normal socket
|
||||
else:
|
||||
# draw output
|
||||
if self.is_output:
|
||||
self.draw_socket(context, layout, node, text)
|
||||
# draw changeable socket
|
||||
if self.changeable:
|
||||
layout.separator()
|
||||
layout.prop(self, "data_type", icon_only=True)
|
||||
# draw previously dynamic socket (with insert socket)
|
||||
if self.prev_dynamic:
|
||||
self._draw_prev_dynamic_socket(context, layout, node)
|
||||
# draw inputs
|
||||
if not self.is_output:
|
||||
# draw disable icon
|
||||
if self.can_be_disabled:
|
||||
layout.prop(
|
||||
self,
|
||||
"disabled",
|
||||
icon_only=True,
|
||||
icon="HIDE_ON" if self.disabled else "HIDE_OFF",
|
||||
emboss=False,
|
||||
)
|
||||
layout = layout.row()
|
||||
layout.enabled = not self.disabled
|
||||
# draw disabled socket
|
||||
if self.can_be_disabled and self.disabled:
|
||||
layout.label(text=text)
|
||||
# draw enabled socket
|
||||
else:
|
||||
self.draw_socket(context, layout, node, text)
|
||||
# draw indexable socket
|
||||
if self.indexable:
|
||||
layout.prop(self, "index_type", icon_only=True)
|
||||
# draw changeable socket
|
||||
if self.changeable:
|
||||
layout.separator()
|
||||
layout.prop(self, "data_type", icon_only=True)
|
||||
|
||||
### SOCKET COLOR
|
||||
# OVERWRITE
|
||||
def get_color(self, context, node):
|
||||
return (0, 0, 0)
|
||||
|
||||
def draw_color(self, context, node):
|
||||
"""Draws the color of this node based on the get_color function and the status of this socket"""
|
||||
c = self.get_color(context, node)
|
||||
alpha = 1
|
||||
# if self.dynamic:
|
||||
# alpha = 0
|
||||
return (c[0], c[1], c[2], alpha)
|
||||
|
||||
### PASS CODE AND DATA
|
||||
# OVERWRITE
|
||||
default_python_value = "None"
|
||||
default_prop_value = ""
|
||||
|
||||
def get_python_repr(self):
|
||||
return "None"
|
||||
|
||||
def reset_value(self):
|
||||
"""Resets this sockets python value back to the default"""
|
||||
self.python_value = self.default_python_value
|
||||
|
||||
def _get_python(self):
|
||||
"""Returns the python value for this socket"""
|
||||
if self.is_program:
|
||||
if self.is_output:
|
||||
# returns the connected program inputs python value or this sockets default
|
||||
to_socket = self.to_sockets()
|
||||
if to_socket:
|
||||
return to_socket[0].python_value
|
||||
return self.get("python_value", self.default_python_value)
|
||||
else:
|
||||
# returns this program inputs python value or its default
|
||||
return self.get("python_value", self.default_python_value)
|
||||
else:
|
||||
if self.is_output:
|
||||
# returns this data outputs current python value or its default
|
||||
return self.get("python_value", self.default_python_value)
|
||||
else:
|
||||
# returns the connected data outputs current python value or the python representation for this input
|
||||
from_out = self.from_socket()
|
||||
if from_out:
|
||||
value = from_out.python_value
|
||||
if self.convert_data:
|
||||
# convert different socket types
|
||||
if from_out.bl_label != self.bl_label:
|
||||
value = CONVERSIONS[from_out.bl_label][
|
||||
self.bl_label](from_out, self)
|
||||
# convert convertable subtypes of the same socket
|
||||
elif from_out.subtype != self.subtype:
|
||||
if from_out.subtype in CONVERSIONS[
|
||||
from_out.bl_label]:
|
||||
if (self.subtype in CONVERSIONS[
|
||||
from_out.bl_label][from_out.subtype]):
|
||||
value = CONVERSIONS[from_out.bl_label][
|
||||
from_out.subtype][self.subtype](
|
||||
from_out, self)
|
||||
return value
|
||||
return self.get_python_repr()
|
||||
|
||||
def _set_python(self, value):
|
||||
"""Sets the python value of this socket if it has changed and triggers an update"""
|
||||
if self.get("python_value") == None or value != self["python_value"]:
|
||||
self["python_value"] = value
|
||||
self._trigger_update()
|
||||
|
||||
def _trigger_update(self):
|
||||
"""Triggers node evaluation depending on the type of this socket"""
|
||||
if self.is_program:
|
||||
# evaluate this node if this is a program output
|
||||
if self.is_output:
|
||||
self.node._evaluate(bpy.context)
|
||||
# evaluate all connected nodes if this is a program input
|
||||
else:
|
||||
from_socket = self.from_socket()
|
||||
if from_socket:
|
||||
from_socket.node._evaluate(bpy.context)
|
||||
else:
|
||||
# evaluate all connected nodes if this is a data output
|
||||
if self.is_output:
|
||||
for socket in self.to_sockets():
|
||||
socket.node._evaluate(bpy.context)
|
||||
# evaluate this node if this is a data input
|
||||
else:
|
||||
self.node._evaluate(bpy.context)
|
||||
|
||||
python_value: bpy.props.StringProperty(
|
||||
name="Python Value",
|
||||
description="Python representation of this sockets value",
|
||||
get=_get_python,
|
||||
set=_set_python,
|
||||
)
|
||||
|
||||
def _get_value(self):
|
||||
"""Returns the current value of this socket"""
|
||||
return self.get(self.subtype_attr, self.default_prop_value)
|
||||
|
||||
def _set_value(self, value):
|
||||
"""Sets the default value depending on the current subtype and updates the python value"""
|
||||
self[self.subtype_attr] = value
|
||||
self.python_value = self._get_python()
|
||||
|
||||
def _update_value(self, _):
|
||||
"""Update function for the subtype properties to force an update on the node"""
|
||||
self.force_update()
|
||||
|
||||
# OVERWRITE
|
||||
default_value: bpy.props.StringProperty(name="Value",
|
||||
description="Value of this socket",
|
||||
get=_get_value,
|
||||
set=_set_value)
|
||||
|
||||
def force_update(self):
|
||||
"""Triggers an update to the connected sockets, for both data and program sockets. Used to pretend the data of this node changed"""
|
||||
self._trigger_update()
|
||||
|
||||
### CONNECTED SOCKETS
|
||||
def _get_to_sockets(self, socket, check_validity=True):
|
||||
"""Recursively returns the inputs connected to the given output, skipping over reroutes"""
|
||||
to_sockets = []
|
||||
# recursively find all sockets when splitting at reroutes
|
||||
if socket.node.bl_idname == "NodeReroute":
|
||||
for link in socket.node.outputs[0].links:
|
||||
to_sockets += self._get_to_sockets(link.to_socket,
|
||||
check_validity)
|
||||
else:
|
||||
# check validity of connection
|
||||
if not check_validity or self.node.node_tree.is_valid_connection(
|
||||
self, socket):
|
||||
to_sockets.append(socket)
|
||||
return to_sockets
|
||||
|
||||
def to_sockets(self, check_validity=True):
|
||||
"""Returns all inputs connected to this output, skipping over reroutes"""
|
||||
sockets = []
|
||||
for link in self.links:
|
||||
sockets += self._get_to_sockets(link.to_socket, check_validity)
|
||||
return sockets
|
||||
|
||||
def from_socket(self, check_validity=True):
|
||||
"""Returns the socket this input comes from skipping over reroutes"""
|
||||
if len(self.links) > 0:
|
||||
from_out = self.links[0].from_socket
|
||||
# find the first socket that is not a reroute
|
||||
while from_out.node.bl_idname == "NodeReroute":
|
||||
if len(from_out.node.inputs[0].links) > 0:
|
||||
from_out = from_out.node.inputs[0].links[0].from_socket
|
||||
else:
|
||||
return None
|
||||
# check connection validity
|
||||
if not check_validity or self.node.node_tree.is_valid_connection(
|
||||
from_out, self):
|
||||
return from_out
|
||||
return None
|
||||
|
||||
### DYNAMIC SOCKETS
|
||||
@property
|
||||
def index(self):
|
||||
"""Returns the index of this socket on the node or -1 if it can't be found"""
|
||||
for index, socket in enumerate(
|
||||
self.node.outputs if self.is_output else self.node.inputs):
|
||||
if socket == self:
|
||||
return index
|
||||
return -1
|
||||
|
||||
def trigger_dynamic(self, insert_above=False):
|
||||
"""Adds another socket like this one after itself and turns itself into a normal socket"""
|
||||
if self.dynamic or self.prev_dynamic:
|
||||
# add new socket
|
||||
if self.is_output:
|
||||
socket = self.node._add_output(self.bl_idname, self.name)
|
||||
# move socket
|
||||
self.node.outputs.move(
|
||||
len(self.node.outputs) - 1,
|
||||
self.index + 1 if not insert_above else self.index,
|
||||
)
|
||||
else:
|
||||
socket = self.node._add_input(self.bl_idname, self.name)
|
||||
# move socket
|
||||
self.node.inputs.move(
|
||||
len(self.node.inputs) - 1,
|
||||
self.index + 1 if not insert_above else self.index,
|
||||
)
|
||||
self.node.location = self.node.location
|
||||
|
||||
# set new socket
|
||||
socket.dynamic = self.dynamic
|
||||
socket.prev_dynamic = self.prev_dynamic
|
||||
socket.subtype = self.subtype
|
||||
socket.changeable = self.changeable
|
||||
socket.is_variable = self.is_variable
|
||||
socket.data_type = self.data_type
|
||||
if hasattr(socket, "passthrough_layout_type"):
|
||||
socket.passthrough_layout_type = self.passthrough_layout_type
|
||||
|
||||
# set this socket
|
||||
self.dynamic = False
|
||||
self.prev_dynamic = True
|
||||
|
||||
if socket.dynamic:
|
||||
self.node.on_dynamic_socket_add(self)
|
||||
else:
|
||||
self.node.on_dynamic_socket_add(socket)
|
||||
|
||||
socket.node._evaluate(bpy.context)
|
||||
@@ -0,0 +1,36 @@
|
||||
import bpy
|
||||
from .base_socket import ScriptingSocket
|
||||
|
||||
|
||||
|
||||
class SN_BooleanSocket(bpy.types.NodeSocket, ScriptingSocket):
|
||||
|
||||
bl_idname = "SN_BooleanSocket"
|
||||
group = "DATA"
|
||||
bl_label = "Boolean"
|
||||
|
||||
|
||||
default_python_value = "False"
|
||||
default_prop_value = False
|
||||
|
||||
def get_python_repr(self):
|
||||
return f"{self.default_value}"
|
||||
|
||||
default_value: bpy.props.BoolProperty(name="Value",
|
||||
default=False,
|
||||
description="Value of this socket",
|
||||
get=ScriptingSocket._get_value,
|
||||
set=ScriptingSocket._set_value)
|
||||
|
||||
subtypes = ["NONE"]
|
||||
subtype_values = {"NONE": "default_value"}
|
||||
|
||||
|
||||
def get_color(self, context, node):
|
||||
return (0.95, 0.73, 1)
|
||||
|
||||
def draw_socket(self, context, layout, node, text, minimal=False):
|
||||
if self.is_output or self.is_linked:
|
||||
layout.label(text=text)
|
||||
else:
|
||||
layout.prop(self, self.subtype_attr, text=text)
|
||||
@@ -0,0 +1,68 @@
|
||||
import bpy
|
||||
import mathutils
|
||||
from .base_socket import ScriptingSocket
|
||||
|
||||
|
||||
|
||||
class SN_BooleanVectorSocket(bpy.types.NodeSocket, ScriptingSocket):
|
||||
|
||||
bl_idname = "SN_BooleanVectorSocket"
|
||||
group = "DATA"
|
||||
bl_label = "Boolean Vector"
|
||||
|
||||
|
||||
default_python_value = "(False, False, False)"
|
||||
default_prop_value = tuple([False]*32)
|
||||
|
||||
def get_python_repr(self):
|
||||
return f"{tuple(getattr(self, self.subtype_attr))[:self.size]}"
|
||||
|
||||
|
||||
def _get_value(self):
|
||||
value = ScriptingSocket._get_value(self)
|
||||
value = tuple(map(lambda x: bool(x), value))
|
||||
return tuple(value)
|
||||
|
||||
def _set_value(self, value):
|
||||
value = list(value)
|
||||
while len(value) < 32:
|
||||
value.append(False)
|
||||
ScriptingSocket._set_value(self, tuple(value))
|
||||
|
||||
def update_size(self, context):
|
||||
self.default_python_value = str(tuple([False]*self.size))
|
||||
self._set_value(self.default_value)
|
||||
self.node.on_socket_type_change(self)
|
||||
|
||||
size: bpy.props.IntProperty(default=3, min=2, max=32,
|
||||
name="Size",
|
||||
description="Size of this boolean vector",
|
||||
update=update_size)
|
||||
|
||||
size_editable: bpy.props.BoolProperty(default=False,
|
||||
name="Size Editable",
|
||||
description="Let's you edit the vectors size on the socket")
|
||||
|
||||
default_value: bpy.props.BoolVectorProperty(name="Value",
|
||||
size=32,
|
||||
description="Value of this socket",
|
||||
get=_get_value,
|
||||
set=_set_value)
|
||||
|
||||
subtypes = ["NONE"]
|
||||
subtype_values = {"NONE": "default_value"}
|
||||
|
||||
|
||||
def get_color(self, context, node):
|
||||
return (0.38, 0.34, 0.84)
|
||||
|
||||
def draw_socket(self, context, layout, node, text, minimal=False):
|
||||
if self.is_output or self.is_linked:
|
||||
layout.label(text=text)
|
||||
else:
|
||||
col = layout.column(heading=text, align=True)
|
||||
for i in range(self.size):
|
||||
col.prop(self, self.subtype_attr, index=i, text=str(getattr(self, self.subtype_attr)[i]), toggle=True)
|
||||
|
||||
if self.size_editable:
|
||||
layout.prop(self, "size", text="")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user