2025-07-01
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
import bpy
|
||||
from ..keymaps.keymap import get_shortcut
|
||||
from .preset_data import PresetData
|
||||
|
||||
|
||||
|
||||
class SN_PT_MarketFilters(bpy.types.Panel):
|
||||
bl_idname = "SN_PT_MarketFilters"
|
||||
bl_label = "Filter"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "WINDOW"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
addon_prefs = bpy.context.preferences.addons[__name__.partition('.')[ 0]].preferences
|
||||
layout.prop(addon_prefs, "only_serpens_3")
|
||||
|
||||
|
||||
|
||||
class SN_AddonPreferences(bpy.types.AddonPreferences):
|
||||
|
||||
bl_idname = __name__.partition('.')[0]
|
||||
|
||||
navigation: bpy.props.EnumProperty(name="Navigation",
|
||||
description="Preferences Navigation",
|
||||
items=[("SETTINGS", "Settings", "Serpens settings", "PREFERENCES", 0),
|
||||
("MARKET", "Marketplace", "Get things from the marketplace", "OUTLINER_OB_GROUP_INSTANCE", 1),
|
||||
("CUSTOM", "Custom", "Preview your addons preferences", "FILE_SCRIPT", 2)])
|
||||
|
||||
market_navigation: bpy.props.EnumProperty(name="Navigation",
|
||||
description="Marketplace Navigation",
|
||||
items=[("PACKAGES", "Packages", "Get packages for Serpens"),
|
||||
("SNIPPETS", "Snippets", "Get snippets made with and for Serpens"),
|
||||
("ADDONS", "Addons", "Get addons made with Serpens")])
|
||||
|
||||
check_for_updates: bpy.props.BoolProperty(name="Check For Updates",
|
||||
description="Check for updates online when loading the addon",
|
||||
default=True)
|
||||
|
||||
use_colors: bpy.props.BoolProperty(name="Use Colored Nodes",
|
||||
description="Color nodes to match their category. Does not apply to existing nodes",
|
||||
default=True)
|
||||
|
||||
keep_last_error_file: bpy.props.BoolProperty(name="Keep Error File",
|
||||
description="Keeps a copy of any compiled file that threw an error as 'serpens_error' in the text editor",
|
||||
default=False)
|
||||
|
||||
search_addons: bpy.props.StringProperty(name="Search",
|
||||
description="Search through the loaded addons",
|
||||
options={"TEXTEDIT_UPDATE"})
|
||||
|
||||
search_packages: bpy.props.StringProperty(name="Search",
|
||||
description="Search through the loaded packages",
|
||||
options={"TEXTEDIT_UPDATE"})
|
||||
|
||||
search_snippets: bpy.props.StringProperty(name="Search",
|
||||
description="Search through the loaded snippets",
|
||||
options={"TEXTEDIT_UPDATE"})
|
||||
|
||||
only_serpens_3: bpy.props.BoolProperty(name="Only Serpens 3",
|
||||
description="Hide all results from previous serpens versions")
|
||||
|
||||
presets: bpy.props.CollectionProperty(name="Presets",
|
||||
description="Preset nodes",
|
||||
type=PresetData)
|
||||
|
||||
|
||||
def draw_serpens_prefs(self, context, layout):
|
||||
row = layout.row()
|
||||
|
||||
col = row.column(heading="General")
|
||||
col.prop(self, "check_for_updates")
|
||||
col.prop(self, "use_colors")
|
||||
col.prop(get_shortcut("sn.force_compile"), "type", full_event=True, text="Force Compile")
|
||||
col.prop(get_shortcut("sn.open_node_docs"), "type", full_event=True, text="Node Docs")
|
||||
col.prop(get_shortcut("sn.add_copied_node"), "type", full_event=True, text="Add Node From Copied")
|
||||
|
||||
col = row.column(heading="Debugging")
|
||||
col.prop(self, "keep_last_error_file")
|
||||
|
||||
|
||||
def draw_market_addon(self, addon_data):
|
||||
if not addon_data.name == "placeholder" and \
|
||||
(not self.only_serpens_3 or self.only_serpens_3 and addon_data.serpens_version == 3) and \
|
||||
(self.search_addons.lower() in addon_data.name.lower() or \
|
||||
self.search_addons.lower() in addon_data.description.lower() or \
|
||||
self.search_addons.lower() in addon_data.author.lower() or \
|
||||
self.search_addons.lower() in addon_data.category.lower()):
|
||||
box = self.layout.box()
|
||||
row = box.row()
|
||||
row.label(text=f"{addon_data.category}: {addon_data.name}")
|
||||
subrow = row.row()
|
||||
subrow.alignment = "RIGHT"
|
||||
subrow.label(text=addon_data.author)
|
||||
row = box.row()
|
||||
row.enabled = False
|
||||
row.label(text=addon_data.description)
|
||||
row = box.row()
|
||||
row.enabled = False
|
||||
row.label(text="Blender: " + ".".join(list(map(lambda i: str(i), list(addon_data.blender_version)))))
|
||||
row.label(text="Addon: " + ".".join(list(map(lambda i: str(i), list(addon_data.addon_version)))))
|
||||
row = box.row()
|
||||
row.operator("wm.url_open", text=addon_data.price if addon_data.price else "Free", icon="URL" if addon_data.is_external else "IMPORT").url = addon_data.addon_url
|
||||
if addon_data.has_blend:
|
||||
serpens_version = "" if addon_data.serpens_version == 3 else " (Serpens 2)"
|
||||
row.operator("wm.url_open", text=f"Download .blend{serpens_version}", icon="IMPORT").url = addon_data.blend_url
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def draw_market_package(self, package_data):
|
||||
if not package_data.name == "placeholder" and \
|
||||
(not self.only_serpens_3 or self.only_serpens_3 and package_data.serpens_version != 2) and \
|
||||
(self.search_packages.lower() in package_data.name.lower() or \
|
||||
self.search_packages.lower() in package_data.description.lower() or \
|
||||
self.search_packages.lower() in package_data.author.lower()):
|
||||
box = self.layout.box()
|
||||
row = box.row()
|
||||
row.label(text=package_data.name)
|
||||
subrow = row.row()
|
||||
subrow.alignment = "RIGHT"
|
||||
subrow.label(text=package_data.author)
|
||||
row = box.row()
|
||||
row.enabled = False
|
||||
row.label(text=package_data.description)
|
||||
serpens_version = "" if package_data.serpens_version != 2 else " (Serpens 2)"
|
||||
box.operator("wm.url_open", text=f"{package_data.price}{serpens_version}" if package_data.price else f"Free {serpens_version}", icon="URL").url = package_data.url
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def draw_market_snippet(self, snippet_data):
|
||||
if not snippet_data.name == "placeholder" and \
|
||||
(not self.only_serpens_3 or self.only_serpens_3 and snippet_data.serpens_version == 3) and \
|
||||
(self.search_snippets.lower() in snippet_data.name.lower() or \
|
||||
self.search_snippets.lower() in snippet_data.description.lower() or \
|
||||
self.search_snippets.lower() in snippet_data.author.lower()):
|
||||
box = self.layout.box()
|
||||
row = box.row()
|
||||
row.label(text=snippet_data.name)
|
||||
subrow = row.row()
|
||||
subrow.alignment = "RIGHT"
|
||||
subrow.label(text=snippet_data.author)
|
||||
row = box.row()
|
||||
row.enabled = False
|
||||
row.label(text=snippet_data.description)
|
||||
row = box.row()
|
||||
serpens_version = "" if snippet_data.serpens_version == 3 else " (Serpens 2)"
|
||||
row.operator("wm.url_open", text=f"{snippet_data.price}{serpens_version}" if snippet_data.price else f"Free {serpens_version}", icon="URL").url = snippet_data.url
|
||||
if snippet_data.blend_url:
|
||||
row.operator("wm.url_open", text=f"Download .blend{serpens_version}", icon="IMPORT").url = snippet_data.blend_url
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def draw_serpens_market(self, layout):
|
||||
sn = bpy.context.scene.sn
|
||||
row = layout.row()
|
||||
row.prop(self, "market_navigation", expand=True)
|
||||
row = layout.row()
|
||||
row.scale_y = 1
|
||||
found_results = False
|
||||
if self.market_navigation == "PACKAGES":
|
||||
found_results = not sn.packages
|
||||
if sn.packages:
|
||||
row.prop(self, "search_packages", text="", icon="VIEWZOOM")
|
||||
row.popover("SN_PT_MarketFilters", text="", icon="FILTER")
|
||||
row.operator("sn.load_packages", text="Load Packages" if not sn.packages else "Reload", icon="FILE_REFRESH")
|
||||
for package in sn.packages:
|
||||
found_results = self.draw_market_package(package) or found_results
|
||||
elif self.market_navigation == "SNIPPETS":
|
||||
found_results = not sn.snippets
|
||||
if sn.snippets:
|
||||
row.prop(self, "search_snippets", text="", icon="VIEWZOOM")
|
||||
row.popover("SN_PT_MarketFilters", text="", icon="FILTER")
|
||||
row.operator("sn.load_snippets", text="Load Snippets" if not sn.snippets else "Reload", icon="FILE_REFRESH")
|
||||
for snippet in sn.snippets:
|
||||
found_results = self.draw_market_snippet(snippet) or found_results
|
||||
elif self.market_navigation == "ADDONS":
|
||||
found_results = not sn.addons
|
||||
if sn.addons:
|
||||
row.prop(self, "search_addons", text="", icon="VIEWZOOM")
|
||||
row.popover("SN_PT_MarketFilters", text="", icon="FILTER")
|
||||
row.operator("sn.load_addons", text="Load Addons" if not sn.addons else "Reload", icon="FILE_REFRESH")
|
||||
for addon in sn.addons:
|
||||
found_results = self.draw_market_addon(addon) or found_results
|
||||
if not found_results:
|
||||
layout.label(text="No results found!")
|
||||
|
||||
|
||||
def draw_custom_prefs(self, context, layout):
|
||||
if context.scene.sn.preferences:
|
||||
layout.label(text="This will be shown in your preferences:", icon="INFO")
|
||||
context.scene.sn.preferences[0](layout)
|
||||
else:
|
||||
layout.label(text="No preferences node added to your addon.", icon="ERROR")
|
||||
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
row = layout.row()
|
||||
row.scale_y = 1.2
|
||||
row.prop(self, "navigation", expand=True)
|
||||
|
||||
if self.navigation == "SETTINGS":
|
||||
self.draw_serpens_prefs(context, layout)
|
||||
if self.navigation == "MARKET":
|
||||
self.draw_serpens_market(layout)
|
||||
elif self.navigation == "CUSTOM":
|
||||
self.draw_custom_prefs(context, layout)
|
||||
|
||||
|
||||
|
||||
# addon_prefs = bpy.context.preferences.addons[__name__.partition('.')[ 0]].preferences
|
||||
@@ -0,0 +1,674 @@
|
||||
import bpy
|
||||
from bl_ui import space_userpref
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
from .data_properties import get_data_items, filter_items, filter_defaults
|
||||
from .handle_script_changes import unwatch_script_changes, watch_script_changes
|
||||
from ..addon.properties.properties import SN_GeneralProperties
|
||||
from ..addon.properties.property_category import SN_PropertyCategory
|
||||
from ..node_tree.graphs.graph_category_ops import SN_GraphCategory
|
||||
from ..addon.assets.assets import SN_AssetProperties
|
||||
from ..utils import get_python_name
|
||||
from .load_markets import SN_Addon, SN_Package, SN_Snippet
|
||||
from ..extensions.snippet_ops import SN_BoolCollection, SN_SnippetCategory
|
||||
from ..nodes.compiler import compile_addon
|
||||
from . import global_search
|
||||
|
||||
|
||||
_item_map = dict()
|
||||
|
||||
|
||||
class SN_AddonProperties(bpy.types.PropertyGroup):
|
||||
# stores the unregister function for the addon when its compiled
|
||||
addon_unregister = []
|
||||
addon_modules = []
|
||||
|
||||
# stores the preferences draw function while compiling the addon to draw in the serpens preferences
|
||||
preferences = []
|
||||
|
||||
# stores the custom icon property collections while developing an addon
|
||||
preview_collections = {}
|
||||
|
||||
# stores functions that need to be called during developement
|
||||
function_store = {}
|
||||
|
||||
# stores the module store for the current addon
|
||||
module_store = []
|
||||
|
||||
compile_time: bpy.props.FloatProperty(
|
||||
name="Compile Time", description="Time the addon took to compile"
|
||||
)
|
||||
|
||||
@property
|
||||
def module_name(self):
|
||||
return get_python_name(
|
||||
bpy.context.scene.sn.addon_name,
|
||||
replacement=f"addon_{uuid4().hex[:5].upper()}",
|
||||
).replace("__", "_")
|
||||
|
||||
def update_reregister(self, context):
|
||||
if not self.pause_reregister:
|
||||
compile_addon()
|
||||
|
||||
pause_reregister: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
name="Pause Reregistering",
|
||||
description="Pauses reregistering the addon when changes are made",
|
||||
update=update_reregister,
|
||||
)
|
||||
|
||||
snippet_vars_customizable: bpy.props.CollectionProperty(
|
||||
type=SN_BoolCollection,
|
||||
name="Variables Customizable",
|
||||
description="Saves customizable setting of snippet variables",
|
||||
)
|
||||
|
||||
snippet_props_customizable: bpy.props.CollectionProperty(
|
||||
type=SN_BoolCollection,
|
||||
name="Properties Customizable",
|
||||
description="Saves customizable setting of snippet properties",
|
||||
)
|
||||
|
||||
is_exporting: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
name="Is Exporting",
|
||||
description="Saves the current status of exporting to evaluate nodes differently",
|
||||
)
|
||||
|
||||
picker_active: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
name="Picker Is Active",
|
||||
description="This is enabled when a location picker for panels or similar is active",
|
||||
)
|
||||
|
||||
last_copied_datatype: bpy.props.StringProperty(
|
||||
default="",
|
||||
name="Last Copied Data Type",
|
||||
description="The type of data last copied with the copy property button",
|
||||
)
|
||||
|
||||
last_copied_datapath: bpy.props.StringProperty(
|
||||
default="",
|
||||
name="Last Copied Data Path",
|
||||
description="The path of data last copied with the copy property button",
|
||||
)
|
||||
|
||||
last_copied_required: bpy.props.StringProperty(
|
||||
default="",
|
||||
name="Last Copied Required Properties",
|
||||
description="The identifiers of the last copied required properties separated by ;",
|
||||
)
|
||||
|
||||
copied_context = []
|
||||
|
||||
show_wrap_settings: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
name="Show Wrap Settings",
|
||||
description="If this is enabled, the wrapping settings for the text in these panels are show.",
|
||||
)
|
||||
|
||||
line_length: bpy.props.IntProperty(
|
||||
default=40,
|
||||
min=10,
|
||||
name="Line Wrap",
|
||||
description="The amount of characters shown in a single line in the panel.",
|
||||
)
|
||||
|
||||
has_update: bpy.props.BoolProperty(
|
||||
name="Has Update",
|
||||
description="If Serpens has an available update or not. This is set on file load.",
|
||||
default=False,
|
||||
)
|
||||
|
||||
def update_recompile(self, context):
|
||||
bpy.ops.sn.force_compile()
|
||||
|
||||
debug_python_nodes: bpy.props.BoolProperty(
|
||||
default=False, name="Debug Nodes", description="Debug internal node code"
|
||||
)
|
||||
|
||||
debug_python_sockets: bpy.props.BoolProperty(
|
||||
default=False, name="Debug Sockets", description="Debug internal socket code"
|
||||
)
|
||||
|
||||
debug_selected_only: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
name="Debug Selected Only",
|
||||
description="Debug only selected nodes",
|
||||
)
|
||||
|
||||
debug_code: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
name="Keep Code",
|
||||
update=update_recompile,
|
||||
description="Keeps a python file in the text editor when the code changes",
|
||||
)
|
||||
|
||||
debug_python_properties: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
name="Debug Properties",
|
||||
description="Debug internal property code",
|
||||
)
|
||||
|
||||
debug_compile_time: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
name="Debug Compile Time",
|
||||
description="Prints the time it takes to compile the code",
|
||||
)
|
||||
|
||||
insert_sockets: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
name="Insert Socket Buttons",
|
||||
description="Show an insert button on dynamic sockets to insert a new socket above",
|
||||
)
|
||||
|
||||
compile_on_load: bpy.props.BoolProperty(
|
||||
default=True,
|
||||
name="Compile On Load",
|
||||
description="Compile this addon when the file is loaded",
|
||||
)
|
||||
|
||||
easy_bpy_path: bpy.props.StringProperty(
|
||||
default="",
|
||||
name="Easy BPY Path",
|
||||
description="Gets set when a file is loaded. Set to the easy bpy file path.",
|
||||
)
|
||||
|
||||
def update_node_tree_index(self, context):
|
||||
if len(bpy.data.node_groups):
|
||||
if hasattr(bpy.context.space_data, "node_tree"):
|
||||
bpy.context.space_data.node_tree = bpy.data.node_groups[
|
||||
self.node_tree_index
|
||||
]
|
||||
|
||||
node_tree_index: bpy.props.IntProperty(
|
||||
default=0,
|
||||
min=0,
|
||||
name="Active Node Tree",
|
||||
description="The node tree you're currently editing",
|
||||
update=update_node_tree_index,
|
||||
)
|
||||
|
||||
properties: bpy.props.CollectionProperty(type=SN_GeneralProperties)
|
||||
|
||||
property_index: bpy.props.IntProperty(
|
||||
default=0,
|
||||
min=0,
|
||||
name="Active Property",
|
||||
description="The property you're currently editing",
|
||||
)
|
||||
|
||||
def update_show_property_categories(self, context):
|
||||
self.active_prop_category = "ALL"
|
||||
|
||||
show_property_categories: bpy.props.BoolProperty(
|
||||
name="Show Property Categories",
|
||||
description="Show categories for your addon properties",
|
||||
default=False,
|
||||
update=update_show_property_categories,
|
||||
)
|
||||
|
||||
property_categories: bpy.props.CollectionProperty(type=SN_PropertyCategory)
|
||||
|
||||
def prop_category_items(self, context):
|
||||
cat_list = list(map(lambda cat: cat.name, self.property_categories))
|
||||
no_cat = 0
|
||||
for prop in self.properties:
|
||||
if (
|
||||
not prop.category
|
||||
or prop.category == "OTHER"
|
||||
or not prop.category in cat_list
|
||||
):
|
||||
no_cat += 1
|
||||
|
||||
items = [
|
||||
("ALL", f"All Properties ({len(self.properties)})", "Show all properties"),
|
||||
(
|
||||
"OTHER",
|
||||
f"Uncategorized Properties ({no_cat})",
|
||||
"Properties without a category",
|
||||
),
|
||||
]
|
||||
|
||||
for item in self.property_categories:
|
||||
amount = 0
|
||||
for prop in self.properties:
|
||||
if prop.category and prop.category == item.name:
|
||||
amount += 1
|
||||
items.append(
|
||||
(item.name if item.name else "-", f"{item.name} ({amount})", item.name)
|
||||
)
|
||||
return items
|
||||
|
||||
active_prop_category: bpy.props.EnumProperty(
|
||||
name="Category", description="The properties shown", items=prop_category_items
|
||||
)
|
||||
|
||||
assets: bpy.props.CollectionProperty(type=SN_AssetProperties)
|
||||
|
||||
asset_index: bpy.props.IntProperty(
|
||||
default=0,
|
||||
min=0,
|
||||
name="Active Asset",
|
||||
description="The asset you're currently editing",
|
||||
)
|
||||
|
||||
addon_name: bpy.props.StringProperty(
|
||||
default="My Addon", name="Addon Name", description="The name of the addon"
|
||||
)
|
||||
|
||||
description: bpy.props.StringProperty(
|
||||
default="", name="Description", description="The description of the addon"
|
||||
)
|
||||
|
||||
author: bpy.props.StringProperty(
|
||||
default="Your Name", name="Author", description="The author of this addon"
|
||||
)
|
||||
|
||||
version: bpy.props.IntVectorProperty(
|
||||
default=(1, 0, 0),
|
||||
size=3,
|
||||
min=0,
|
||||
name="Version",
|
||||
description="The author of this addon",
|
||||
)
|
||||
|
||||
blender: bpy.props.IntVectorProperty(
|
||||
default=(4, 2, 0),
|
||||
size=3,
|
||||
min=0,
|
||||
name="Minimum Blender",
|
||||
description="Minimum blender version required for this addon",
|
||||
)
|
||||
|
||||
location: bpy.props.StringProperty(
|
||||
default="",
|
||||
name="Location",
|
||||
description="Describes where the addons functionality can be found",
|
||||
)
|
||||
|
||||
warning: bpy.props.StringProperty(
|
||||
default="",
|
||||
name="Warning",
|
||||
description="Used if there is a bug or a problem that the user should be aware of",
|
||||
)
|
||||
|
||||
doc_url: bpy.props.StringProperty(
|
||||
default="", name="Doc URL", description="URL to the addons documentation"
|
||||
)
|
||||
|
||||
tracker_url: bpy.props.StringProperty(
|
||||
default="", name="Tracker URL", description="URL to the addons bug tracker"
|
||||
)
|
||||
|
||||
def get_categories(self, context):
|
||||
categories = [
|
||||
"3D View",
|
||||
"Add Mesh",
|
||||
"Add Curve",
|
||||
"Animation",
|
||||
"Compositing",
|
||||
"Development",
|
||||
None,
|
||||
"Game Engine",
|
||||
"Import-Export",
|
||||
"Lighting",
|
||||
"Material",
|
||||
"Mesh",
|
||||
"Node",
|
||||
None,
|
||||
"Object",
|
||||
"Paint",
|
||||
"Physics",
|
||||
"Render",
|
||||
"Rigging",
|
||||
"Scene",
|
||||
None,
|
||||
"Sequencer",
|
||||
"System",
|
||||
"Text Editor",
|
||||
"UV",
|
||||
"User Interface",
|
||||
]
|
||||
items = []
|
||||
for cat in categories:
|
||||
if cat:
|
||||
items.append((cat, cat, cat))
|
||||
else:
|
||||
items.append(("", "", ""))
|
||||
return items + [("CUSTOM", "- Custom Category -", "Add your own category")]
|
||||
|
||||
category: bpy.props.EnumProperty(
|
||||
items=get_categories,
|
||||
name="Category",
|
||||
description="The category the addon will be displayed in",
|
||||
)
|
||||
|
||||
custom_category: bpy.props.StringProperty(
|
||||
default="My Category",
|
||||
name="Custom Category",
|
||||
description="Your custom category",
|
||||
)
|
||||
|
||||
data_items = {"app": {}, "context": {}, "data": {}}
|
||||
ops_items = {"operators": {}, "filtered": {}}
|
||||
|
||||
def overwrite_data_items(self, data):
|
||||
self.data_items["data"] = data["data"]
|
||||
self.data_items["context"] = data["context"]
|
||||
self.data_items["app"] = data["app"]
|
||||
|
||||
def reload_data_category(self, category):
|
||||
"""Reloads the basic data for a category"""
|
||||
if category != "context":
|
||||
self.data_items[category] = get_data_items(
|
||||
f"bpy.{category}", getattr(bpy, category)
|
||||
)
|
||||
else:
|
||||
ctxt = self.copied_context[0] if self.copied_context else bpy.context.copy()
|
||||
self.data_items[category] = get_data_items(f"bpy.context", ctxt)
|
||||
|
||||
def refresh_filtered_ops(self):
|
||||
"""Sets the filtered operators"""
|
||||
filtered = {}
|
||||
for cat in self.ops_items["operators"]:
|
||||
cat_ops = []
|
||||
for op in self.ops_items["operators"][cat]["items"]:
|
||||
if (
|
||||
self.data_search.lower() in op["name"].lower()
|
||||
or self.data_search.lower() in op["operator"].lower()
|
||||
):
|
||||
cat_ops.append(op["operator"])
|
||||
if cat_ops:
|
||||
filtered[cat] = cat_ops
|
||||
self.ops_items["filtered"] = filtered
|
||||
|
||||
def get_category_ops(self, category, cat_name):
|
||||
"""Gets the operators for a category"""
|
||||
ops = []
|
||||
for op_name in dir(category):
|
||||
if op_name[0].isalpha():
|
||||
try:
|
||||
op = eval(f"bpy.ops.{cat_name}.{op_name}")
|
||||
except:
|
||||
op = None
|
||||
if op:
|
||||
rna = op.get_rna_type()
|
||||
ops.append(
|
||||
{
|
||||
"name": (
|
||||
getattr(rna, "name", op_name)
|
||||
if getattr(rna, "name", op_name)
|
||||
else op_name.replace("_", " ").title()
|
||||
),
|
||||
"operator": op_name,
|
||||
}
|
||||
)
|
||||
return ops
|
||||
|
||||
def load_operators(self):
|
||||
"""Reloads the list of operators"""
|
||||
self.ops_items["operators"] = {}
|
||||
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:
|
||||
self.ops_items["operators"][cat_name] = {
|
||||
"expanded": False,
|
||||
"items": self.get_category_ops(cat, cat_name),
|
||||
}
|
||||
self.refresh_filtered_ops()
|
||||
|
||||
def load_categories(self):
|
||||
"""Loads the data for the bpy categories"""
|
||||
self.reload_data_category("app")
|
||||
self.reload_data_category("context")
|
||||
self.reload_data_category("data")
|
||||
self.load_operators()
|
||||
|
||||
def update_hide_preferences(self, context):
|
||||
for cls in space_userpref.classes:
|
||||
try:
|
||||
if self.hide_preferences:
|
||||
bpy.utils.unregister_class(cls)
|
||||
else:
|
||||
bpy.utils.register_class(cls)
|
||||
except:
|
||||
pass
|
||||
if self.hide_preferences:
|
||||
self.load_categories()
|
||||
|
||||
hide_preferences: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
name="Hide Preferences",
|
||||
description="Hides all panels in the preferences window",
|
||||
update=update_hide_preferences,
|
||||
)
|
||||
|
||||
global_search_active: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
name="Global Search",
|
||||
description="If the global search is active",
|
||||
)
|
||||
|
||||
def make_enum_item(self, _id, name, descr, preview_id, uid):
|
||||
lookup = (
|
||||
str(_id)
|
||||
+ "\\0"
|
||||
+ str(name)
|
||||
+ "\\0"
|
||||
+ str(descr)
|
||||
+ "\\0"
|
||||
+ str(preview_id)
|
||||
+ "\\0"
|
||||
+ str(uid)
|
||||
)
|
||||
if not lookup in _item_map:
|
||||
_item_map[lookup] = (_id, name, descr, preview_id, uid)
|
||||
return _item_map[lookup]
|
||||
|
||||
def get_categories(self, context):
|
||||
ctxt = "Preferences"
|
||||
if context.scene.sn.copied_context:
|
||||
ctxt = f"{self.copied_context[0]['area'].type.replace('_', ' ').title()} {self.copied_context[0]['region'].type.replace('_', ' ').title()}"
|
||||
items = [
|
||||
self.make_enum_item("app", "App", "bpy.app", 0, 0),
|
||||
self.make_enum_item("context", f"Context ({ctxt})", "bpy.context", 0, 1),
|
||||
self.make_enum_item("data", "Data", "bpy.data", 0, 2),
|
||||
self.make_enum_item("ops", "Operators", "bpy.ops", 0, 3),
|
||||
self.make_enum_item(
|
||||
"discover", "Discover", "Discover items in the global search", 0, 4
|
||||
),
|
||||
]
|
||||
return items
|
||||
|
||||
def update_data_search(self, context):
|
||||
self.refresh_filtered_ops()
|
||||
|
||||
def update_categories(self, context):
|
||||
if self.data_category == "discover":
|
||||
global_search.start_get_data(context)
|
||||
|
||||
discover_data = {"items": [], "full_matches": 0}
|
||||
|
||||
def update_discover(self, context):
|
||||
counted_paths = []
|
||||
full_matches = 0
|
||||
queries = self.discover_search.lower().replace(", ", ",").split(",")
|
||||
|
||||
for path in global_search.data_flat.keys():
|
||||
matches = 0
|
||||
for query in queries:
|
||||
if query in path.lower():
|
||||
matches += 1
|
||||
if matches == len(queries):
|
||||
full_matches += 1
|
||||
counted_paths.append((path, matches))
|
||||
elif not self.discover_full_only:
|
||||
counted_paths.append((path, matches))
|
||||
|
||||
ordered_paths = list(
|
||||
map(lambda x: x[0], sorted(counted_paths, key=lambda x: x[1], reverse=True))
|
||||
)
|
||||
ordered_paths = ordered_paths[: self.discover_show_amount]
|
||||
|
||||
self.discover_data["items"] = ordered_paths
|
||||
self.discover_data["full_matches"] = full_matches
|
||||
|
||||
discover_search: bpy.props.StringProperty(
|
||||
name="Search",
|
||||
description="Searches for items in the global search (Separate multiple queries with commas)",
|
||||
default="",
|
||||
update=update_discover,
|
||||
)
|
||||
|
||||
discover_show_amount: bpy.props.IntProperty(
|
||||
name="Show Amount",
|
||||
description="The amount of items to show in the discover tab",
|
||||
default=100,
|
||||
min=1,
|
||||
update=update_discover,
|
||||
)
|
||||
|
||||
discover_full_only: bpy.props.BoolProperty(
|
||||
name="Full Matches Only",
|
||||
description="Only show items that match the full search",
|
||||
default=True,
|
||||
update=update_discover,
|
||||
)
|
||||
|
||||
data_category: bpy.props.EnumProperty(
|
||||
name="Category",
|
||||
items=get_categories,
|
||||
update=update_categories,
|
||||
description="Category of blend data",
|
||||
)
|
||||
|
||||
data_filter: bpy.props.EnumProperty(
|
||||
name="Type",
|
||||
options={"ENUM_FLAG"},
|
||||
description="Filter by data type",
|
||||
items=filter_items,
|
||||
default=filter_defaults,
|
||||
)
|
||||
|
||||
data_search: bpy.props.StringProperty(
|
||||
name="Search",
|
||||
description="Search data",
|
||||
options={"TEXTEDIT_UPDATE"},
|
||||
update=update_data_search,
|
||||
)
|
||||
|
||||
show_path: bpy.props.BoolProperty(
|
||||
name="Show Path", description="Show python path of properties", default=False
|
||||
)
|
||||
|
||||
addons: bpy.props.CollectionProperty(type=SN_Addon)
|
||||
|
||||
packages: bpy.props.CollectionProperty(type=SN_Package)
|
||||
|
||||
snippets: bpy.props.CollectionProperty(type=SN_Snippet)
|
||||
|
||||
snippet_categories: bpy.props.CollectionProperty(type=SN_SnippetCategory)
|
||||
|
||||
def update_show_graph_categories(self, context):
|
||||
self.active_graph_category = "ALL"
|
||||
|
||||
show_graph_categories: bpy.props.BoolProperty(
|
||||
name="Show Graph Categories",
|
||||
description="Show categories for your addon graphs",
|
||||
default=False,
|
||||
update=update_show_graph_categories,
|
||||
)
|
||||
|
||||
graph_categories: bpy.props.CollectionProperty(type=SN_GraphCategory)
|
||||
|
||||
def graph_category_items(self, context):
|
||||
cat_list = list(map(lambda cat: cat.name, self.graph_categories))
|
||||
no_cat = 0
|
||||
ntree_amount = 0
|
||||
for ntree in bpy.data.node_groups:
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
ntree_amount += 1
|
||||
if (
|
||||
not ntree.category
|
||||
or ntree.category == "OTHER"
|
||||
or not ntree.category in cat_list
|
||||
):
|
||||
no_cat += 1
|
||||
|
||||
items = [
|
||||
("ALL", f"All Graphs ({ntree_amount})", "Show all graphs"),
|
||||
("OTHER", f"Uncategorized Graphs ({no_cat})", "Graphs without a category"),
|
||||
]
|
||||
|
||||
for item in self.graph_categories:
|
||||
amount = 0
|
||||
for ntree in bpy.data.node_groups:
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
if ntree.category and ntree.category == item.name:
|
||||
amount += 1
|
||||
items.append(
|
||||
(item.name if item.name else "-", f"{item.name} ({amount})", item.name)
|
||||
)
|
||||
return items
|
||||
|
||||
active_graph_category: bpy.props.EnumProperty(
|
||||
name="Category", description="The graphs shown", items=graph_category_items
|
||||
)
|
||||
|
||||
overwrite_variable_graph: bpy.props.BoolProperty(
|
||||
name="Overwrite Variable Graph",
|
||||
description="Let's you pick a graph to show the variable list from",
|
||||
default=False,
|
||||
)
|
||||
|
||||
def get_variable_graph_items(self, context):
|
||||
items = []
|
||||
for ntree in bpy.data.node_groups:
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
items.append((ntree.name, ntree.name, ntree.name))
|
||||
return items
|
||||
|
||||
variable_graph: bpy.props.EnumProperty(
|
||||
name="Variable Graph",
|
||||
description="Graph to display variables from",
|
||||
items=get_variable_graph_items,
|
||||
)
|
||||
|
||||
remove_duplicate_code: bpy.props.BoolProperty(
|
||||
name="Remove Duplicate Code",
|
||||
description="Removes duplicate code in the generated code (small performance impact for large addons)",
|
||||
default=True,
|
||||
update=update_recompile,
|
||||
)
|
||||
|
||||
format_code: bpy.props.BoolProperty(
|
||||
name="Format Code",
|
||||
description="Formats linebreaks in the generated code (small performance impact for large addons)",
|
||||
default=True,
|
||||
update=update_recompile,
|
||||
)
|
||||
|
||||
def update_watch_scripts(self, context):
|
||||
if self.watch_script_changes:
|
||||
watch_script_changes()
|
||||
else:
|
||||
unwatch_script_changes()
|
||||
|
||||
watch_script_changes: bpy.props.BoolProperty(
|
||||
name="Watch Script Changes",
|
||||
description="Will watch for changes in the scripts of your run script nodes and recompile the addon when you save the file",
|
||||
default=False,
|
||||
update=update_watch_scripts,
|
||||
)
|
||||
|
||||
multifile: bpy.props.BoolProperty(
|
||||
name="Multifile",
|
||||
description="Export the separate node trees as separate python files",
|
||||
default=False,
|
||||
)
|
||||
@@ -0,0 +1,378 @@
|
||||
import bpy
|
||||
from ..addon.properties.settings.settings import property_icons
|
||||
|
||||
|
||||
def is_valid_attribute(attr):
|
||||
ignore_attributes = ["rna_type", "original",
|
||||
"bl_rna", "evaluated_depsgraph_get"]
|
||||
return not attr in ignore_attributes and not attr[0] == "_"
|
||||
|
||||
|
||||
filter_items = [("Pointer", "Pointer", "Pointer", property_icons["Property"], 1),
|
||||
("Collection", "Collection", "Collection",
|
||||
property_icons["Collection"], 2),
|
||||
("List", "List", "List", property_icons["List"], 4),
|
||||
("String", "String/Enum", "Strings and Enums",
|
||||
property_icons["String"], 8),
|
||||
("Enum Set", "Enum Set", "Enum Set",
|
||||
property_icons["Enum Set"], 32),
|
||||
("Boolean", "Boolean", "Boolean",
|
||||
property_icons["Boolean"], 64),
|
||||
("Boolean Vector", "Boolean Vector",
|
||||
"Boolean Vector", property_icons["Boolean"], 128),
|
||||
("Integer", "Integer", "Integer",
|
||||
property_icons["Integer"], 256),
|
||||
("Integer Vector", "Integer Vector",
|
||||
"Integer Vector", property_icons["Integer"], 512),
|
||||
("Float", "Float", "Float", property_icons["Float"], 1024),
|
||||
("Float Vector", "Float Vector",
|
||||
"Float Vector", property_icons["Float"], 2048),
|
||||
("Function", "Function", "Function", property_icons["Function"], 4096)]
|
||||
|
||||
|
||||
filter_defaults = {"Pointer", "Collection", "List", "String", "Enum Set", "Boolean", "Boolean Vector",
|
||||
"Integer", "Integer Vector", "Float", "Float Vector", "Function"}
|
||||
|
||||
|
||||
def is_iterable(data):
|
||||
if hasattr(data, "keys") and hasattr(data, "values"):
|
||||
return hasattr(data, "__getitem__")
|
||||
return False
|
||||
|
||||
|
||||
def add_additional_data(data_dict, path):
|
||||
if ".outputs[" in path:
|
||||
data_dict["links"] = list(eval(path+".links"))
|
||||
return data_dict
|
||||
|
||||
|
||||
def get_data_items(path, data):
|
||||
data_items = {}
|
||||
|
||||
# get attributes
|
||||
data_dict = validate_data_dict(data) if type(
|
||||
data) == dict else data_to_dict(data)
|
||||
data_dict = add_additional_data(data_dict, path)
|
||||
for key in data_dict.keys():
|
||||
item = get_data_item(data, data_dict[key], path, key)
|
||||
if item:
|
||||
data_items[key] = item
|
||||
|
||||
# get items for iterable
|
||||
if is_iterable(data) and not type(data) == dict:
|
||||
# get keyed items
|
||||
if len(data.keys()) == len(data.values()):
|
||||
for key in data.keys():
|
||||
if hasattr(data[key], "bl_rna"):
|
||||
item = get_data_item(data, data[key], path, f"'{key}'")
|
||||
if item:
|
||||
data_items[f"'{key}'"] = item
|
||||
# get indexed items
|
||||
else:
|
||||
max_items = 21
|
||||
for i in range(min(len(data.values()), max_items)):
|
||||
item = get_data_item(data, data[i], path, f"{i}")
|
||||
if item:
|
||||
data_items[f"{i}"] = item
|
||||
if i == max_items-1 and len(data.values()) > max_items:
|
||||
data_items[f"{i}"]["clamped"] = True
|
||||
|
||||
# sort items
|
||||
sorted_keys = sorted(
|
||||
data_items.keys(), key=lambda s: data_items[s]["type"])
|
||||
sorted_keys = sorted(
|
||||
sorted_keys, key=lambda s: data_items[s]["has_properties"], reverse=True)
|
||||
sorted_items = {}
|
||||
for key in sorted_keys:
|
||||
sorted_items[key] = data_items[key]
|
||||
return sorted_items
|
||||
|
||||
|
||||
def validate_data_dict(data):
|
||||
""" Removes all attributes that aren't valid """
|
||||
to_delete = []
|
||||
for key in data.keys():
|
||||
if not is_valid_attribute(key):
|
||||
to_delete.append(key)
|
||||
for key in to_delete:
|
||||
del data[key]
|
||||
return data
|
||||
|
||||
|
||||
def data_to_dict(data):
|
||||
""" Converts the given data to a dictionary with keys for the path section and the data """
|
||||
data_dict = {}
|
||||
attributes = dir(data)
|
||||
if hasattr(data, "keyframe_insert"):
|
||||
attributes += dir(bpy.types.Struct)
|
||||
attributes.sort()
|
||||
for attribute in attributes:
|
||||
if is_valid_attribute(attribute):
|
||||
data_dict[attribute] = getattr(data, attribute)
|
||||
return data_dict
|
||||
|
||||
|
||||
def get_data_item(parent_data, data, path, attribute):
|
||||
""" Returns a data object for the given data its path and the datas attribute """
|
||||
try:
|
||||
data
|
||||
has_properties = hasattr(
|
||||
data, "bl_rna") or "bpy_prop_collection" in str(type(data))
|
||||
if (attribute[0] == "'" and attribute[-1] == "'") or attribute.isdigit():
|
||||
new_path = f"{path}[{attribute}]"
|
||||
has_properties = True
|
||||
else:
|
||||
new_path = f"{path}.{attribute}"
|
||||
|
||||
data_item = {
|
||||
"name": get_data_name(data, attribute),
|
||||
"path": new_path,
|
||||
"required": "",
|
||||
"type": get_item_type(data),
|
||||
"data": data,
|
||||
"data_search": "",
|
||||
"data_filter": filter_defaults,
|
||||
"expanded": False,
|
||||
"has_properties": has_properties,
|
||||
"properties": {},
|
||||
"clamped": False,
|
||||
}
|
||||
if data_item["type"] == "Function":
|
||||
data_item["path"] += get_function_parameters(
|
||||
parent_data, attribute)
|
||||
data_item["required"] += get_required_function_parameters(
|
||||
parent_data, attribute)
|
||||
return data_item
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def get_data_name(data, attribute):
|
||||
""" Returns a name based on the given attribute name """
|
||||
if (attribute[0] == "'" and attribute[-1] == "'") or attribute.isdigit():
|
||||
return f"[{attribute}]"
|
||||
name = attribute.replace("_", " ").title()
|
||||
if hasattr(data, "bl_rna"):
|
||||
name = f"{name} ({data.bl_rna.name})"
|
||||
return name
|
||||
|
||||
|
||||
def get_item_type(data):
|
||||
""" Returns the item type for the given data """
|
||||
item_type = str(type(data))
|
||||
if "bpy_prop_collection" in item_type:
|
||||
item_type = "Collection"
|
||||
elif "bpy_types" in item_type:
|
||||
item_type = "Pointer"
|
||||
elif "bpy.types" in item_type:
|
||||
item_type = "Pointer"
|
||||
elif hasattr(type(data), "bl_rna"):
|
||||
item_type = "Pointer"
|
||||
elif "None" in item_type:
|
||||
item_type = "Pointer"
|
||||
elif "bpy_func" in item_type or "builtin_function_or_method" in item_type:
|
||||
item_type = "Function"
|
||||
elif "set" in item_type:
|
||||
item_type = "Enum Set"
|
||||
elif "str" in item_type:
|
||||
item_type = "String"
|
||||
elif "bool" in item_type:
|
||||
item_type = "Boolean"
|
||||
elif "float" in item_type:
|
||||
item_type = "Float"
|
||||
elif "int" in item_type:
|
||||
item_type = "Integer"
|
||||
elif "Color" in item_type:
|
||||
item_type = "Float Vector"
|
||||
elif "Euler" in item_type:
|
||||
item_type = "Float Vector"
|
||||
elif "Quaternion" in item_type:
|
||||
item_type = "Float Vector"
|
||||
elif "Matrix" in item_type:
|
||||
item_type = "List"
|
||||
elif "list" in item_type:
|
||||
item_type = "List"
|
||||
elif ("tuple" in item_type or "Vector" in item_type or "bpy_prop_array" in item_type) and len(data):
|
||||
if "float" in str(type(data[0])):
|
||||
item_type = "Float Vector"
|
||||
elif "int" in str(type(data[0])):
|
||||
item_type = "Integer Vector"
|
||||
elif "bool" in str(type(data[0])):
|
||||
item_type = "Boolean Vector"
|
||||
else:
|
||||
item_type = "List"
|
||||
return item_type
|
||||
|
||||
|
||||
def get_special_function_params(attribute):
|
||||
""" Returns the special parameters for the given function """
|
||||
if attribute == "keyframe_insert":
|
||||
return "data_path: String, index: Integer, frame: Integer, group: String, options: Enum Set['INSERTKEY_NEEDED','INSERTKEY_VISUAL','INSERTKEY_XYZ_TO_RGB','INSERTKEY_REPLACE','INSERTKEY_AVAILABLE','INSERTKEY_CYCLE_AWARE'], "
|
||||
elif attribute == "keyframe_delete":
|
||||
return "data_path: String, index: Integer, frame: Integer, group: String, "
|
||||
elif attribute == "driver_add":
|
||||
return "path*: String, index*: Integer, "
|
||||
elif attribute == "driver_remove":
|
||||
return "path*: String, index*: Integer, "
|
||||
return ""
|
||||
|
||||
|
||||
def get_special_function_outputs(attribute):
|
||||
""" Returns the special outputs for the given function """
|
||||
if attribute == "as_pointer":
|
||||
return "adress: Integer, "
|
||||
elif attribute == "keyframe_insert":
|
||||
return "success: Boolean, "
|
||||
elif attribute == "keyframe_delete":
|
||||
return "success: Boolean, "
|
||||
elif attribute == "driver_add":
|
||||
return "fcurve: Data, "
|
||||
elif attribute == "driver_remove":
|
||||
return "success: Boolean, "
|
||||
return ""
|
||||
|
||||
|
||||
def get_special_required_parameters(attribute):
|
||||
""" Returns the special required parameters for the given function """
|
||||
if attribute == "keyframe_insert":
|
||||
return "data_path"
|
||||
elif attribute == "keyframe_delete":
|
||||
return "data_path"
|
||||
elif attribute == "driver_add":
|
||||
return "path*"
|
||||
elif attribute == "driver_remove":
|
||||
return "path*"
|
||||
return ""
|
||||
|
||||
|
||||
def get_function_parameters(parent_data, name):
|
||||
""" Returns a dictionary with function parameters """
|
||||
params = ""
|
||||
outputs = ""
|
||||
if hasattr(parent_data, "bl_rna") and hasattr(parent_data.bl_rna, "functions"):
|
||||
if name in parent_data.bl_rna.functions:
|
||||
for param in parent_data.bl_rna.functions[name].parameters:
|
||||
param_type = param.type.title()
|
||||
if param_type == "Str":
|
||||
param_type = "String"
|
||||
elif param_type == "Int":
|
||||
param_type = "Integer"
|
||||
if getattr(param, "is_array", False):
|
||||
param_type += " Vector"
|
||||
if param_type == "Enum":
|
||||
if param.is_enum_flag:
|
||||
param_type = "Enum Set"
|
||||
items = ",".join(
|
||||
list(map(lambda item: f"'{item.identifier}'", param.enum_items_static)))
|
||||
param_type += f"[{items}]"
|
||||
if param.is_output:
|
||||
outputs += f"{param.identifier}: {param_type}, "
|
||||
else:
|
||||
params += f"{param.identifier}: {param_type}, "
|
||||
else:
|
||||
params = get_special_function_params(name)
|
||||
outputs = get_special_function_outputs(name)
|
||||
if params:
|
||||
params = params[:-2]
|
||||
if outputs:
|
||||
outputs = outputs[:-2]
|
||||
params = f"({params})"
|
||||
if outputs:
|
||||
params += f" = {outputs}"
|
||||
return params
|
||||
|
||||
|
||||
def get_required_function_parameters(parent_data, name):
|
||||
""" Returns a string separated by ; with the required function parameters """
|
||||
required = ""
|
||||
if hasattr(parent_data, "bl_rna") and hasattr(parent_data.bl_rna, "functions"):
|
||||
if name in parent_data.bl_rna.functions:
|
||||
for param in parent_data.bl_rna.functions[name].parameters:
|
||||
if param.is_required:
|
||||
required += param.identifier if not required else f";{param.identifier}"
|
||||
else:
|
||||
required = get_special_required_parameters(name)
|
||||
return required
|
||||
|
||||
|
||||
def item_from_path(data, path):
|
||||
""" Returns the item in the data for the given path. Works for anything above bpy.xyz """
|
||||
# after bpy.xyz
|
||||
if len(path.split(".")) > 2:
|
||||
path_sections = bpy_to_path_sections(path, False)
|
||||
curr_item = data[path_sections[0]][path_sections[1]]
|
||||
for key in path_sections[2:]:
|
||||
curr_item = curr_item["properties"][key]
|
||||
return curr_item
|
||||
# bpy
|
||||
elif len(path.split(".")) == 1:
|
||||
return bpy
|
||||
# bpy.xyz
|
||||
else:
|
||||
return data[path.split(".")[-1]]
|
||||
|
||||
|
||||
def bpy_to_path_sections(path, keep_brackets=True):
|
||||
""" Takes a blender python data path and converts it to json path sections """
|
||||
if len(path) >= 4 and path[:4] == "bpy.":
|
||||
path = path[4:]
|
||||
|
||||
sections = []
|
||||
curr_section = ""
|
||||
bracket_level = 0
|
||||
in_string = False
|
||||
for char in path:
|
||||
if in_string:
|
||||
curr_section += char
|
||||
if char == "'" or char == '"':
|
||||
in_string = False
|
||||
else:
|
||||
if char == "." and bracket_level == 0:
|
||||
sections.append(curr_section)
|
||||
curr_section = ""
|
||||
elif char == "'" or char == '"':
|
||||
in_string = True
|
||||
curr_section += char
|
||||
elif char == "[":
|
||||
if bracket_level == 0:
|
||||
sections.append(curr_section)
|
||||
curr_section = "[" if keep_brackets else ""
|
||||
bracket_level = 1
|
||||
else:
|
||||
bracket_level += 1
|
||||
curr_section += "["
|
||||
elif char == "]":
|
||||
bracket_level -= 1
|
||||
if keep_brackets or bracket_level > 0:
|
||||
curr_section += "]"
|
||||
else:
|
||||
curr_section += char
|
||||
sections.append(curr_section)
|
||||
sections = list(filter(lambda item: item, sections))
|
||||
return sections
|
||||
|
||||
|
||||
def bpy_to_indexed_sections(path):
|
||||
""" Takes a blender python data path and converts it to indexed path sections """
|
||||
# combine indexed sections
|
||||
combined = ["bpy"]
|
||||
for section in bpy_to_path_sections(path):
|
||||
if (section[0] == "[" and section[-1] == "]") and not combined[-1][-1] == "]":
|
||||
combined[-1] += section
|
||||
else:
|
||||
combined.append(section)
|
||||
|
||||
if not "bpy." in path:
|
||||
combined = combined[1:]
|
||||
return combined
|
||||
|
||||
|
||||
def join_sections(sections):
|
||||
""" Returns the given sections joined to a valid data path """
|
||||
data_path = ""
|
||||
for i, section in enumerate(sections):
|
||||
if i == 0 or section[0] == "[":
|
||||
data_path += section
|
||||
else:
|
||||
data_path += f".{section}"
|
||||
return data_path
|
||||
@@ -0,0 +1,13 @@
|
||||
import bpy
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
|
||||
def check_easy_bpy_install():
|
||||
bpy.context.scene.sn.easy_bpy_path = ""
|
||||
for path in sys.path:
|
||||
if os.path.exists(path):
|
||||
for filename in os.listdir(path):
|
||||
if filename == "easybpy.py":
|
||||
bpy.context.scene.sn.easy_bpy_path = os.path.join(path, filename)
|
||||
@@ -0,0 +1,110 @@
|
||||
import bpy
|
||||
import threading
|
||||
|
||||
thread = None
|
||||
data_flat = {}
|
||||
visited_ids = {}
|
||||
|
||||
def is_iterable(data):
|
||||
if hasattr(data, "keys") and hasattr(data, "values"):
|
||||
return hasattr(data, "__getitem__")
|
||||
return False
|
||||
|
||||
def is_valid_key(key):
|
||||
if key.startswith("_"):
|
||||
return False
|
||||
if key in ["rna_type", "bl_rna", "depsgraph", "original", "get", "find", "items", "keys", "values", "foreach_get", "foreach_set", "id_data", "tag", "pop", "keyconfigs"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_valid_path(path, screen):
|
||||
# NOTE blender bug where it crashes when you access data from another screen
|
||||
if ".screens[" in path and not screen.name in path:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_data_info(data, key):
|
||||
info = {
|
||||
"name": key,
|
||||
"type": type(data).__name__,
|
||||
}
|
||||
return info
|
||||
|
||||
def get_data(data, path, screen, depth):
|
||||
global data_flat
|
||||
global visited_ids
|
||||
|
||||
if depth > 10:
|
||||
return
|
||||
|
||||
data_id = data
|
||||
|
||||
if data_id in visited_ids and visited_ids[data_id]["depth"] <= depth:
|
||||
return
|
||||
visited_ids[data_id] = {
|
||||
"path": path,
|
||||
"depth": depth
|
||||
}
|
||||
|
||||
keys = dir(data)
|
||||
if hasattr(data, "keyframe_insert"):
|
||||
keys += dir(bpy.types.Struct)
|
||||
|
||||
for key in keys:
|
||||
child_path = f"{path}.{key}"
|
||||
if is_valid_key(key) and is_valid_path(child_path, screen):
|
||||
try:
|
||||
try:
|
||||
if data.bl_rna.properties[key].type == "ENUM":
|
||||
if data.bl_rna.properties[key].is_enum_flag:
|
||||
child_data = set()
|
||||
else:
|
||||
child_data = ""
|
||||
else:
|
||||
child_data = getattr(data, key)
|
||||
except Exception as e:
|
||||
child_data = getattr(data, key)
|
||||
data_flat[child_path] = get_data_info(child_data, key)
|
||||
if hasattr(child_data, "bl_rna"):
|
||||
get_data(child_data, child_path, screen, depth+1)
|
||||
|
||||
max_items = 20
|
||||
if is_iterable(child_data):
|
||||
# keyed data
|
||||
if len(child_data.keys()) == len(child_data.values()):
|
||||
for i, child_key in enumerate(list(child_data.keys())):
|
||||
get_data(child_data[child_key], f"{child_path}['{child_key}']", screen, depth+1)
|
||||
# indexed data
|
||||
else:
|
||||
for i in range(min(len(child_data.values()), max_items)):
|
||||
get_data(child_data[i], f"{child_path}[{i}]", screen, depth+1)
|
||||
except: pass
|
||||
|
||||
|
||||
def timer():
|
||||
global thread
|
||||
global data_flat
|
||||
if not thread.is_alive():
|
||||
keys = list(data_flat.keys())
|
||||
keys.sort(key=len)
|
||||
new_data = {}
|
||||
for key in keys:
|
||||
new_data[key] = data_flat[key]
|
||||
data_flat = new_data
|
||||
bpy.context.scene.sn.discover_search = bpy.context.scene.sn.discover_search
|
||||
return None
|
||||
return 0.1
|
||||
|
||||
|
||||
def start_get_data(context):
|
||||
global thread
|
||||
global data_flat
|
||||
global visited_ids
|
||||
|
||||
data_flat = {}
|
||||
visited_ids = {}
|
||||
|
||||
thread = threading.Thread(target=get_data, args=(bpy.data, "bpy.data", context.screen, 1))
|
||||
thread.start()
|
||||
|
||||
bpy.app.timers.register(timer, first_interval=0.1)
|
||||
@@ -0,0 +1,47 @@
|
||||
import bpy
|
||||
import os
|
||||
|
||||
|
||||
def watch_script_changes():
|
||||
bpy.app.timers.register(timer_script_update)
|
||||
|
||||
|
||||
def unwatch_script_changes():
|
||||
if bpy.app.timers.is_registered(timer_script_update):
|
||||
bpy.app.timers.unregister(timer_script_update)
|
||||
|
||||
|
||||
last_updates = {
|
||||
# '{static_uid;filename}': 'last_save_time'
|
||||
}
|
||||
|
||||
def update_script_nodes(update_blender_always=False):
|
||||
for ntree in bpy.data.node_groups:
|
||||
if ntree.bl_idname == "ScriptingNodesTree":
|
||||
for node in ntree.node_collection("SN_RunScriptNode").nodes:
|
||||
path = ""
|
||||
if node.source == "BLENDER":
|
||||
if node.script and update_blender_always:
|
||||
node._evaluate(bpy.context)
|
||||
elif node.script and node.script.filepath:
|
||||
path = node.script.filepath
|
||||
elif node.source == "EXTERNAL":
|
||||
node_path = eval(node.inputs['Script Path'].python_value)
|
||||
if node_path:
|
||||
path = bpy.path.abspath(node_path)
|
||||
|
||||
if path and os.path.exists(path):
|
||||
key = f"{node.static_uid};{path}"
|
||||
time = os.path.getmtime(path)
|
||||
if not key in last_updates:
|
||||
last_updates[key] = time
|
||||
node._evaluate(bpy.context)
|
||||
else:
|
||||
if last_updates[key] != time:
|
||||
last_updates[key] = time
|
||||
node._evaluate(bpy.context)
|
||||
|
||||
|
||||
def timer_script_update():
|
||||
update_script_nodes()
|
||||
return 2
|
||||
@@ -0,0 +1,135 @@
|
||||
import bpy
|
||||
import requests
|
||||
from random import shuffle
|
||||
|
||||
|
||||
|
||||
class SN_Addon(bpy.types.PropertyGroup):
|
||||
|
||||
name: bpy.props.StringProperty()
|
||||
description: bpy.props.StringProperty()
|
||||
price: bpy.props.StringProperty()
|
||||
category: bpy.props.StringProperty()
|
||||
author: bpy.props.StringProperty()
|
||||
blender_version: bpy.props.IntVectorProperty(size=3)
|
||||
addon_version: bpy.props.IntVectorProperty(size=3)
|
||||
is_external: bpy.props.BoolProperty()
|
||||
has_blend: bpy.props.BoolProperty()
|
||||
addon_url: bpy.props.StringProperty()
|
||||
blend_url: bpy.props.StringProperty()
|
||||
serpens_version: bpy.props.IntProperty()
|
||||
|
||||
class SN_Package(bpy.types.PropertyGroup):
|
||||
|
||||
name: bpy.props.StringProperty()
|
||||
description: bpy.props.StringProperty()
|
||||
price: bpy.props.StringProperty()
|
||||
author: bpy.props.StringProperty()
|
||||
url: bpy.props.StringProperty()
|
||||
serpens_version: bpy.props.IntProperty()
|
||||
|
||||
class SN_Snippet(bpy.types.PropertyGroup):
|
||||
|
||||
name: bpy.props.StringProperty()
|
||||
description: bpy.props.StringProperty()
|
||||
price: bpy.props.StringProperty()
|
||||
author: bpy.props.StringProperty()
|
||||
url: bpy.props.StringProperty()
|
||||
blend_url: bpy.props.StringProperty()
|
||||
serpens_version: bpy.props.IntProperty()
|
||||
|
||||
|
||||
|
||||
class SN_OT_LoadAddons(bpy.types.Operator):
|
||||
bl_idname = "sn.load_addons"
|
||||
bl_label = "Load Addons"
|
||||
bl_description = "Loads the addons from the marketplace"
|
||||
bl_options = {"REGISTER","INTERNAL","UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
url = "https://raw.githubusercontent.com/joshuaKnauber/serpens_addon_market/main/addons.json"
|
||||
|
||||
try:
|
||||
content = requests.get(url).json()
|
||||
addons = content["addons"]
|
||||
shuffle(addons)
|
||||
|
||||
context.scene.sn.addons.clear()
|
||||
for addon in addons:
|
||||
item = context.scene.sn.addons.add()
|
||||
item.name = addon["name"]
|
||||
if addon["price"]: item.price = addon["price"]
|
||||
item.description = addon["description"]
|
||||
item.category = addon["category"]
|
||||
item.author = addon["author"]
|
||||
item.blender_version = addon["blender_version"]
|
||||
item.addon_version = addon["addon_version"]
|
||||
item.is_external = addon["external"]
|
||||
item.has_blend = addon["blend"]
|
||||
item.addon_url = addon["url"]
|
||||
item.blend_url = addon["blend_url"]
|
||||
item.serpens_version = 2 if not "serpens_version" in addon else 3
|
||||
except:
|
||||
self.report({"ERROR"}, message="Couldn't load addons!")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_LoadPackages(bpy.types.Operator):
|
||||
bl_idname = "sn.load_packages"
|
||||
bl_label = "Load Packages"
|
||||
bl_description = "Loads the packages from the marketplace"
|
||||
bl_options = {"REGISTER","INTERNAL","UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
url = "https://raw.githubusercontent.com/joshuaKnauber/serpens_addon_market/main/packages.json"
|
||||
|
||||
try:
|
||||
content = requests.get(url).json()
|
||||
packages = content["packages"]
|
||||
shuffle(packages)
|
||||
|
||||
context.scene.sn.packages.clear()
|
||||
for package in packages:
|
||||
item = context.scene.sn.packages.add()
|
||||
item.name = package["title"]
|
||||
item.description = package["description"]
|
||||
item.price = package["price"]
|
||||
item.url = package["url"]
|
||||
item.author = package["author"]
|
||||
item.serpens_version = 2 if not "version" in package else 3
|
||||
|
||||
except:
|
||||
print("Couldn't load packages!")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class SN_OT_LoadSnippets(bpy.types.Operator):
|
||||
bl_idname = "sn.load_snippets"
|
||||
bl_label = "Load Snippets"
|
||||
bl_description = "Loads the snippets from the marketplace"
|
||||
bl_options = {"REGISTER","INTERNAL","UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
url = "https://raw.githubusercontent.com/joshuaKnauber/serpens_addon_market/main/snippets.json"
|
||||
|
||||
try:
|
||||
content = requests.get(url).json()
|
||||
snippets = content["snippets"]
|
||||
shuffle(snippets)
|
||||
|
||||
context.scene.sn.snippets.clear()
|
||||
for snippet in snippets:
|
||||
item = context.scene.sn.snippets.add()
|
||||
item.name = snippet["title"]
|
||||
item.description = snippet["description"]
|
||||
item.price = snippet["price"]
|
||||
item.url = snippet["url"]
|
||||
item.blend_url = snippet["blend_url"]
|
||||
item.author = snippet["author"]
|
||||
item.serpens_version = 2 if not "serpens_version" in snippet else snippet["serpens_version"]
|
||||
|
||||
except:
|
||||
print("Couldn't load snippets!")
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,13 @@
|
||||
import bpy
|
||||
|
||||
|
||||
class PresetData(bpy.types.PropertyGroup):
|
||||
|
||||
name: bpy.props.StringProperty(name="Name",
|
||||
description="Name of this preset node")
|
||||
|
||||
idname: bpy.props.StringProperty(name="Idname",
|
||||
description="The idname of this node")
|
||||
|
||||
data: bpy.props.StringProperty(name="Data",
|
||||
description="The stringified json data used to recreate this node")
|
||||
@@ -0,0 +1,91 @@
|
||||
import bpy
|
||||
import os
|
||||
import requests
|
||||
import json
|
||||
from datetime import date
|
||||
|
||||
|
||||
message = []
|
||||
|
||||
|
||||
class SN_OT_MessageUpdate(bpy.types.Operator):
|
||||
bl_idname = "sn.update_message"
|
||||
bl_label = "- - - - - - - - - - - - - - - -"
|
||||
bl_description = "Update Message"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
update_log()
|
||||
bpy.context.scene.sn.has_update = False
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
global message
|
||||
layout = self.layout
|
||||
|
||||
layout.label(text="Serpens Update available!", icon="FUND")
|
||||
layout.label(text="A free update is out.")
|
||||
layout.label(text="You can go and download it right now!")
|
||||
|
||||
if message:
|
||||
layout.label(text="This update includes:")
|
||||
for m in message:
|
||||
layout.label(text=" • " + m)
|
||||
layout.label(text="- - - - - - - - - - - - - - - -")
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
|
||||
def update_log():
|
||||
with open(os.path.join(os.path.dirname(__file__), "update_log.json"), "w") as log:
|
||||
log.write(json.dumps({"last_check": str(date.today())}, indent=4))
|
||||
|
||||
|
||||
def should_update():
|
||||
should_update = False
|
||||
addon_prefs = bpy.context.preferences.addons[__name__.partition('.')[ 0]].preferences
|
||||
if not addon_prefs.check_for_updates:
|
||||
should_update = False
|
||||
|
||||
log_path = os.path.join(os.path.dirname(__file__), "update_log.json")
|
||||
if os.path.exists(log_path):
|
||||
with open(log_path, "r") as log:
|
||||
log = json.loads(log.read())
|
||||
if not log["last_check"] == str(date.today()):
|
||||
should_update = True
|
||||
else:
|
||||
update_log()
|
||||
return should_update
|
||||
|
||||
|
||||
def exists_newer_version(version, current):
|
||||
needs_update = False
|
||||
if version[0] > current[0]:
|
||||
needs_update = True
|
||||
elif version[0] == current[0]:
|
||||
if version[1] > current[1]:
|
||||
needs_update = True
|
||||
elif version[1] == current[1]:
|
||||
if version[2] > current[2]:
|
||||
needs_update = True
|
||||
return needs_update
|
||||
|
||||
|
||||
def check_serpens_updates(current_version):
|
||||
global message
|
||||
bpy.context.scene.sn.has_update = False
|
||||
if should_update():
|
||||
url = "https://raw.githubusercontent.com/joshuaKnauber/serpens_addon_market/main/version.json"
|
||||
|
||||
try:
|
||||
content = requests.get(url).json()
|
||||
version = tuple(content["version"])
|
||||
if exists_newer_version(version, current_version):
|
||||
message = content["content"]
|
||||
bpy.context.scene.sn.has_update = True
|
||||
else:
|
||||
update_log()
|
||||
|
||||
except:
|
||||
print("Couldn't check for Serpens updates!")
|
||||
Reference in New Issue
Block a user