2025-07-01

This commit is contained in:
2026-03-17 14:30:01 -06:00
parent f9a22056dd
commit 62b5978595
4579 changed files with 1257472 additions and 0 deletions
@@ -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")