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,186 @@
import bpy
from bpy_extras.io_utils import ImportHelper
import os
class SN_OT_AddAsset(bpy.types.Operator):
bl_idname = "sn.add_asset"
bl_label = "Add Asset"
bl_description = "Adds a asset to the addon"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
add_type: bpy.props.EnumProperty(default="FILE",
items=[("FILE", "File", "Import a single file"),
("DIRECTORY", "Directory", "Import a full directory")],
name="Type",
description="Add this directory or this file as an asset",
options={"SKIP_SAVE"})
def execute(self, context):
bpy.ops.sn.load_asset("INVOKE_DEFAULT", add_type=self.add_type)
return {"FINISHED"}
def draw(self, context):
layout = self.layout
layout.prop(self, "add_type", expand=True)
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=200)
class SN_OT_LoadAsset(bpy.types.Operator, ImportHelper):
bl_idname = "sn.load_asset"
bl_label = "Add Asset"
bl_description = "Adds a asset to the addon"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
add_type: bpy.props.EnumProperty(default="FILE",
items=[("FILE", "File", "Import a single file"),
("DIRECTORY", "Directory", "Import a full directory")],
name="Type",
description="Add this directory or this file as an asset",
options={"SKIP_SAVE"})
def execute(self, context):
sn = context.scene.sn
new_asset = sn.assets.add()
if self.add_type == "DIRECTORY":
new_asset.path = os.path.dirname(self.filepath)
else:
new_asset.path = self.filepath
for index, asset in enumerate(sn.assets):
if asset == new_asset:
sn.asset_index = index
return {"FINISHED"}
class SN_OT_RemoveAsset(bpy.types.Operator):
bl_idname = "sn.remove_asset"
bl_label = "Remove Asset"
bl_description = "Removes this asset from the addon"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context):
return context.scene.sn.asset_index < len(context.scene.sn.assets)
def execute(self, context):
sn = context.scene.sn
asset = sn.assets[sn.asset_index]
# remove removed from asset nodes
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
for node in ntree.nodes:
if node.bl_idname == "SN_AssetNode":
if node.asset == asset.name:
node.asset = ""
# remove asset
sn.assets.remove(sn.asset_index)
sn.asset_index -= 1
return {"FINISHED"}
class SN_OT_FindNode(bpy.types.Operator):
bl_idname = "sn.find_node"
bl_label = "Find Node"
bl_description = "Find Node"
bl_options = {"REGISTER", "INTERNAL"}
node_tree: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
node: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
def execute(self, context):
ntree = bpy.data.node_groups[self.node_tree]
# set active graph and select
context.space_data.node_tree = ntree
for index, group in enumerate(bpy.data.node_groups):
if group == ntree:
context.scene.sn.node_tree_index = index
# select node and frame
for node in ntree.nodes:
node.select = node.name == self.node
bpy.ops.node.view_selected("INVOKE_DEFAULT")
return {"FINISHED"}
class SN_OT_FindAsset(bpy.types.Operator):
bl_idname = "sn.find_asset"
bl_label = "Find Asset"
bl_description = "Finds this asset in the addon"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
return {"FINISHED"}
def draw(self, context):
layout = self.layout
# init asset nodes
empty_nodes = []
asset_nodes = []
asset = None
if context.scene.sn.asset_index < len(context.scene.sn.assets):
asset = context.scene.sn.assets[context.scene.sn.asset_index]
# find assets nodes
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
for node in ntree.nodes:
if node.bl_idname == "SN_AssetNode":
if asset and node.asset == asset.name:
asset_nodes.append(node)
elif not node.asset:
empty_nodes.append(node)
# draw nodes for selected asset
if context.scene.sn.asset_index < len(context.scene.sn.assets):
col = layout.column()
row = col.row()
row.enabled = False
row.label(text=f"Asset: {asset.name}")
for node in asset_nodes:
op = col.operator("sn.find_node", text=node.name, icon="RESTRICT_SELECT_OFF")
op.node_tree = node.node_tree.name
op.node = node.name
if not asset_nodes:
col.label(text="No nodes found for this asset", icon="INFO")
# draw nodes with empty asset
col = layout.column()
row = col.row()
row.label(text="Empty Asset Nodes")
row.enabled = False
for node in empty_nodes:
op = col.operator("sn.find_node", text=node.name, icon="RESTRICT_SELECT_OFF")
op.node_tree = node.node_tree.name
op.node = node.name
if not empty_nodes:
col.label(text="No empty asset nodes found", icon="INFO")
def invoke(self, context, event):
return context.window_manager.invoke_popup(self, width=250)
class SN_OT_AddAssetNode(bpy.types.Operator):
bl_idname = "sn.add_asset_node"
bl_label = "Add Asset Node"
bl_description = "Adds an asset node"
bl_options = {"REGISTER", "INTERNAL"}
def execute(self, context):
bpy.ops.node.add_node("INVOKE_DEFAULT", type="SN_AssetNode", use_transform=True)
node = context.space_data.node_tree.nodes.active
if context.scene.sn.asset_index < len(context.scene.sn.assets):
node.asset = context.scene.sn.assets[context.scene.sn.asset_index].name
return {"FINISHED"}
@@ -0,0 +1,47 @@
import bpy
import os
class SN_AssetProperties(bpy.types.PropertyGroup):
def get_to_update(self):
to_update = []
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
for node in ntree.nodes:
if node.bl_idname == "SN_AssetNode":
if node.asset == self.name:
to_update.append(node)
return to_update
def update_file_path(self, context):
if not self.path == bpy.path.abspath(self.path):
self["path"] = bpy.path.abspath(self.path)
else:
if self.name == "Asset" and self.path:
self.name = os.path.basename(self.path)
for node in self.get_to_update():
node._evaluate(context)
def get_asset_name(self):
return self.get("name", "Asset")
def set_asset_name(self, new_name):
# update asset nodes that had this asset
to_update = self.get_to_update()
self["name"] = new_name
for node in to_update:
node.asset = new_name
name: bpy.props.StringProperty(name="Name",
description="Name of the asset",
default="Asset",
get=get_asset_name,
set=set_asset_name)
path: bpy.props.StringProperty(name="Path",
description="Path to the asset file",
subtype="FILE_PATH",
update=update_file_path)
@@ -0,0 +1,31 @@
import bpy
from .property_utils import get_sorted_props
def property_imperative_code():
""" Returns the imperative code for all properties """
props = get_sorted_props(bpy.context.scene.sn.properties.values())
imperative = ""
for prop in props:
imperative += prop.imperative_code + "\n"
return imperative
def property_register_code():
""" Returns the register code for all properties """
props = get_sorted_props(bpy.context.scene.sn.properties.values())
register = ""
for prop in props:
register += prop.register_code + "\n"
return register
def property_unregister_code():
""" Returns the unregister code for all properties """
props = get_sorted_props(bpy.context.scene.sn.properties.values())
props.reverse()
unregister = ""
for prop in props:
unregister += prop.unregister_code + "\n"
return unregister
@@ -0,0 +1,115 @@
import bpy
from ...nodes.compiler import compile_addon
from .property_basic import BasicProperty
from .settings.settings import id_items, id_data, property_icons
from .settings.group import SN_PT_GroupProperty
class FullBasicProperty(BasicProperty):
property_type: bpy.props.EnumProperty(name="Type",
description="The type of data this property can store",
update=BasicProperty.trigger_reference_update,
items=[("String", "String", "Stores text, can display a text input or a filepath field", property_icons["String"], 0),
("Boolean", "Boolean", "Stores True or False, can be used for a checkbox", property_icons["Boolean"], 1),
("Float", "Float", "Stores a decimal number or a vector", property_icons["Float"], 2),
("Integer", "Integer", "Stores an integer number or a vector", property_icons["Integer"], 3),
("Enum", "Enum", "Stores multiple entries to be used as dropdowns", property_icons["Enum"], 4),
("Pointer", "Pointer", "Stores a reference to certain types of blend data, collection or group properties", property_icons["Pointer"], 5),
("Collection", "Collection", "Stores a list of certain blend data or property groups to be displayed in lists", property_icons["Collection"], 6),
("Group", "Group", "Stores multiple properties to be used in a collection or pointer property", property_icons["Group"], 7)])
@property
def settings(self):
return {
"String": self.stngs_string,
"Boolean": self.stngs_boolean,
"Float": self.stngs_float,
"Integer": self.stngs_integer,
"Enum": self.stngs_enum,
"Pointer": self.stngs_pointer,
"Collection": self.stngs_collection,
"Group": self.stngs_group,
}[self.property_type]
stngs_group: bpy.props.PointerProperty(type=SN_PT_GroupProperty)
class SN_GeneralProperties(FullBasicProperty, bpy.types.PropertyGroup):
is_scene_prop = True
def draw(self, context, layout):
""" Draws the general property settings """
row = layout.row()
row.prop(self, "property_type")
row.operator("sn.tooltip", text="", emboss=False, icon="QUESTION").text = self.settings.type_description
if not self.property_type == "Group":
layout.prop(self, "attach_to")
layout.prop(self, "description")
if self.property_type in {"Float", "Integer", "Boolean"}:
layout.prop_enum(self, "prop_options", "ANIMATABLE", text="Animatable")
elif self.property_type == "String":
layout.prop_enum(self, "prop_options", "TEXTEDIT_UPDATE", text="Textedit Update")
@property
def data_path(self):
return f"{self.attach_to.upper()}_PLACEHOLDER.{self.python_name}"
@property
def register_code(self):
self.python_name
# register non group properties
if not self.property_type == "Group":
code = f"bpy.types.{self.attach_to}.{self.python_name} = bpy.props.{self.settings.prop_type_name}(name='{self.name}', description='{self.description}',{self.get_prop_options} {self.settings.register_options})"
# register group properties
else:
code = f"bpy.utils.register_class(SNA_GROUP_{self.python_name})"
return code
@property
def unregister_code(self):
# unregister non group properties
if not self.property_type == "Group":
return f"del bpy.types.{self.attach_to}.{self.python_name}"
# unregister group properties
else:
return f"bpy.utils.unregister_class(SNA_GROUP_{self.python_name})"
@property
def imperative_code(self):
if hasattr(self.settings, "imperative_code"):
return self.settings.imperative_code()
return ""
def compile(self, context=None):
""" Registers the property and unregisters previous version """
# print(f"Serpens Log: Property {self.name} received an update")
compile_addon()
def get_attach_to_items(self, context):
items = []
for item in id_items:
items.append((item, item, item))
return items
def get_attach_data(self):
return id_data[self.attach_to]
attach_to: bpy.props.EnumProperty(name="Attach To",
description="The type of blend data to attach this property to",
items=get_attach_to_items,
update=FullBasicProperty.trigger_reference_update)
def copy(self):
new_prop = super().copy()
new_prop.attach_to = self.attach_to
return new_prop
@@ -0,0 +1,292 @@
import bpy
from ...utils import get_python_name, unique_collection_name
from .settings.settings import property_icons
from .settings.string import SN_PT_StringProperty
from .settings.boolean import SN_PT_BooleanProperty
from .settings.float import SN_PT_FloatProperty
from .settings.integer import SN_PT_IntegerProperty
from .settings.enum import SN_PT_EnumProperty
from .settings.pointer import SN_PT_PointerProperty
from .settings.collection import SN_PT_CollectionProperty
_prop_collection_cache = {} # stores key, value of prop.as_pointer, prop collection
_prop_origin_cache = {} # stores key, value of prop.as_pointer, prop collection origin
class BasicProperty():
def draw(self, context, layout):
""" Draws the general property settings """
row = layout.row()
row.prop(self, "property_type")
row.operator("sn.tooltip", text="", emboss=False, icon="QUESTION").text = self.settings.type_description
layout.prop(self, "description")
col = layout.column(align=True)
col.prop_enum(self, "prop_options", "HIDDEN", text="Hidden")
col.prop_enum(self, "prop_options", "SKIP_SAVE", text="Skip Save")
if self.property_type in {"Float", "Integer", "Boolean"}:
col.prop_enum(self, "prop_options", "ANIMATABLE", text="Animatable")
if self.property_type == "String":
col.prop_enum(self, "prop_options", "TEXTEDIT_UPDATE", text="Textedit Update")
# cache python names so they only have to be generated once
cached_python_names = {}
cached_python_name: bpy.props.StringProperty()
cached_human_name: bpy.props.StringProperty()
@property
def python_name(self):
if self.name == self.cached_human_name and self.cached_python_name: return self.cached_python_name
if self.name in self.cached_python_names: return self.cached_python_names[self.name]
names = []
for prop in self.prop_collection:
if prop == self:
break
names.append(prop.python_name)
name = unique_collection_name(f"sna_{get_python_name(self.name, 'new_property')}", "new_property", names, "_")
try:
self.cached_python_name = name
self.cached_human_name = self.name
except AttributeError: pass
self.cached_python_names[self.name] = name
return name
@property
def get_prop_options(self):
options = ""
if self.prop_options:
options = " options={" + ", ".join(map(lambda option: f"'{option}'", list(self.prop_options))) + "},"
return options
@property
def register_code(self):
code = f"{self.python_name}: bpy.props.{self.settings.prop_type_name}(name='{self.name}', description='{self.description}',{self.get_prop_options} {self.settings.register_options})"
if hasattr(self.settings, "register_code"):
return self.settings.register_code(code)
return code
@property
def prop_collection(self):
""" Returns the collection this property lives in """
if self.id_data.bl_rna.identifier == "ScriptingNodesTree":
# find property in nodes to return
if not str(self.as_pointer()) in _prop_collection_cache:
for node in self.id_data.nodes:
if hasattr(node, "properties"):
for prop in node.properties:
if prop == self:
_prop_collection_cache[str(self.as_pointer())] = node.properties
break
elif prop.property_type == "Group":
for subprop in prop.settings.properties:
if subprop == self:
_prop_collection_cache[str(self.as_pointer())] = prop.settings.properties
break
return _prop_collection_cache[str(self.as_pointer())]
else:
path = "[".join(repr(self.path_resolve("name", False)).split("[")[:-1])
if path.endswith("scenes"):
path = "bpy.context.scene.sn.properties"
coll = eval(path)
return coll
@property
def prop_collection_origin(self):
""" Returns the source where the main property collection lives """
if self.id_data.bl_rna.identifier == "ScriptingNodesTree":
# find property in nodes to return
if not str(self.as_pointer()) in _prop_origin_cache:
for node in self.id_data.nodes:
if hasattr(node, "properties"):
for prop in node.properties:
if prop == self:
_prop_origin_cache[str(self.as_pointer())] = node
break
elif prop.property_type == "Group":
for subprop in prop.settings.properties:
if subprop == self:
_prop_origin_cache[str(self.as_pointer())] = node
break
return _prop_origin_cache[str(self.as_pointer())]
else:
parent_path = repr(self.path_resolve("name", False)).split("properties")[0][:-1]
parent = eval(parent_path)
return parent
@property
def full_prop_path(self):
""" Returns the full data path for this property """
main_prop_path = f"{repr(self.prop_collection_origin)}.properties"
if hasattr(self, "group_prop_parent"):
main_prop_path += f"['{self.group_prop_parent.name}'].settings.properties"
main_prop_path += f"['{self.name}']"
return main_prop_path
def _compile(self, context=None):
""" Update the property with the parent classes compile function """
if hasattr(self, "compile"):
self.compile(context)
def get_name(self):
return self.get("name", "Prop Default")
def get_unique_name(self, value):
names = list(map(lambda item: item.name, list(filter(lambda item: item!=self, self.prop_collection))))
return unique_collection_name(value, "New Property", names, " ")
def set_name(self, value):
value = self.get_unique_name(value)
# get nodes to update references
to_update_nodes = []
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
for node in ntree.nodes:
if getattr(node, "prop_name", None) == self.name or getattr(node, "prop_group", None) == self.name:
if self.property_type == "Group":
if hasattr(node, "get_prop_source") and node.get_prop_source() == self.settings:
to_update_nodes.append((node, "prop_group"))
else:
if hasattr(node, "get_prop_source") and node.get_prop_source() and node.get_prop_source().properties == self.prop_collection and self.name in node.get_prop_source().properties:
to_update_nodes.append((node, "prop_name"))
# get properties to update references
to_update_props = []
if self.property_type == "Group":
for prop in self.prop_collection:
if prop.property_type in ["Pointer", "Collection"] and prop.settings.prop_group == self.name:
to_update_props.append(prop)
elif prop.property_type == "Group" and prop != self:
for subprop in prop.settings.properties:
if subprop.property_type in ["Pointer", "Collection"] and subprop.settings.prop_group == self.name:
to_update_props.append(subprop)
# set value
self["name"] = value
# update property references
for prop in to_update_props:
prop.settings.prop_group = value
for node, key in to_update_nodes:
setattr(node, key, value)
name: bpy.props.StringProperty(name="Property Name",
description="Name of this property",
default="Prop Default",
get=get_name,
set=set_name,
update=_compile)
description: bpy.props.StringProperty(name="Description",
description="The description of this property, shown in tooltips",
update=_compile)
@property
def icon(self):
return property_icons[self.property_type]
def trigger_reference_update(self, context):
# get nodes to update references
to_update_nodes = []
key = "prop_group" if self.property_type == "Group" else "prop_name"
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
for node in ntree.nodes:
if hasattr(node, key) and getattr(node, key) == self.name:
to_update_nodes.append((node, key))
for node, key in to_update_nodes:
# trigger an update on the affected nodes
setattr(node, key, self.name)
self._compile()
def get_types(self, context):
items = [("String", "String", "Stores text, can display a text input or a filepath field", property_icons["String"], 0),
("Boolean", "Boolean", "Stores True or False, can be used for a checkbox", property_icons["Boolean"], 1),
("Float", "Float", "Stores a decimal number or a vector", property_icons["Float"], 2),
("Integer", "Integer", "Stores an integer number or a vector", property_icons["Integer"], 3),
("Enum", "Enum", "Stores multiple entries to be used as dropdowns", property_icons["Enum"], 4),
("Pointer", "Pointer", "Stores a reference to certain types of blend data, collection or group properties", property_icons["Pointer"], 5),
("Collection", "Collection", "Stores a list of certain blend data or property groups to be displayed in lists", property_icons["Collection"], 6)]
if not self.allow_pointers:
items.pop(5)
return items
property_type: bpy.props.EnumProperty(name="Type",
description="The type of data this property can store",
update=trigger_reference_update,
items=get_types)
allow_pointers: bpy.props.BoolProperty(default=True)
def get_prop_option_items(self, context):
items = [("HIDDEN", "Hidden", "Hide property from operator popups"),
("SKIP_SAVE", "Skip Save", "Don't save this property between calls"),
("ANIMATABLE", "Animatable", "Enable if this property should be animatable"),
("TEXTEDIT_UPDATE", "Textedit Update", "Calls the update function every time the property is edited (Only string properties; not operator popups)")]
return items
prop_options: bpy.props.EnumProperty(name="Options",
description="Options for this property",
options={"ENUM_FLAG"},
items=get_prop_option_items,
update=_compile)
@property
def settings(self):
return {
"String": self.stngs_string,
"Boolean": self.stngs_boolean,
"Float": self.stngs_float,
"Integer": self.stngs_integer,
"Enum": self.stngs_enum,
"Pointer": self.stngs_pointer,
"Collection": self.stngs_collection,
}[self.property_type]
stngs_string: bpy.props.PointerProperty(type=SN_PT_StringProperty)
stngs_boolean: bpy.props.PointerProperty(type=SN_PT_BooleanProperty)
stngs_float: bpy.props.PointerProperty(type=SN_PT_FloatProperty)
stngs_integer: bpy.props.PointerProperty(type=SN_PT_IntegerProperty)
stngs_enum: bpy.props.PointerProperty(type=SN_PT_EnumProperty)
stngs_pointer: bpy.props.PointerProperty(type=SN_PT_PointerProperty)
stngs_collection: bpy.props.PointerProperty(type=SN_PT_CollectionProperty)
category: bpy.props.StringProperty(name="Category", default="OTHER",
description="The category this property is displayed in")
def match_settings(self, new_prop):
new_prop["name"] = self.get_unique_name(self.get("name"))
new_prop["property_type"] = self.get("property_type")
new_prop["description"] = self.get("description")
new_prop["allow_pointers"] = self.get("allow_pointers")
new_prop["prop_options"] = self.get("prop_options")
new_prop["category"] = self.get("category")
for attr in self.settings.copy_attributes:
new_prop.settings[attr] = self.settings.get(attr)
self.settings.copy(new_prop.settings)
def copy(self):
new_prop = self.prop_collection.add()
self.match_settings(new_prop)
return new_prop
@@ -0,0 +1,135 @@
import bpy
from ...interface.panels.property_ui_list import get_selected_property
class SN_PropertyCategory(bpy.types.PropertyGroup):
def set_name(self, value):
for prop in bpy.context.scene.sn.properties:
if prop.category and prop.category == self.name:
prop.category = value
self["name"] = value
def get_name(self):
return self.get("name", "New Category")
name: bpy.props.StringProperty(name="Name", default="New Category",
description="The name of this property category",
set=set_name, get=get_name)
class SN_OT_AddPropertyCategory(bpy.types.Operator):
bl_idname = "sn.add_property_category"
bl_label = "Add Property Category"
bl_description = "Adds a property category"
bl_options = {"REGISTER", "INTERNAL", "UNDO"}
def execute(self, context):
context.scene.sn.property_categories.add()
return {"FINISHED"}
class SN_OT_RemovePropertyCategory(bpy.types.Operator):
bl_idname = "sn.remove_property_category"
bl_label = "Remove Property Category"
bl_description = "Removes a property category"
bl_options = {"REGISTER", "INTERNAL", "UNDO"}
index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
context.scene.sn.property_categories.remove(self.index)
return {"FINISHED"}
class SN_OT_EditPropertyCategories(bpy.types.Operator):
bl_idname = "sn.edit_property_categories"
bl_label = "Edit Property Categories"
bl_description = "Edit the addon property categories"
bl_options = {"REGISTER", "INTERNAL"}
def execute(self, context):
return {"FINISHED"}
def draw(self, context):
layout = self.layout
layout.label(text="Categories")
for i, cat in enumerate(context.scene.sn.property_categories):
row = layout.row()
row.scale_y = 1.2
row.prop(cat, "name", text="")
row.operator("sn.remove_property_category", text="", icon="REMOVE", emboss=False).index = i
if not context.scene.sn.property_categories:
row = layout.row()
row.enabled = False
row.label(text="No categories added", icon="ERROR")
row = layout.row()
row.scale_y = 1.2
row.operator("sn.add_property_category", text="Add Category", icon="ADD")
def invoke(self, context, event):
return context.window_manager.invoke_popup(self, width=250)
class SN_OT_MovePropertyToCategory(bpy.types.Operator):
bl_idname = "sn.move_property_to_category"
bl_label = "Move Property Category"
bl_description = "Move the selected property to a different category"
bl_options = {"REGISTER", "INTERNAL"}
category: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
prop = get_selected_property()
if prop:
if self.category == -1:
prop.category = "OTHER"
else:
prop.category = context.scene.sn.property_categories[self.category].name
context.area.tag_redraw()
return {"FINISHED"}
class SN_OT_MovePropertyCategory(bpy.types.Operator):
bl_idname = "sn.move_property_category"
bl_label = "Move Property Category"
bl_description = "Move the selected property to a different category"
bl_options = {"REGISTER", "INTERNAL"}
index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
return {"FINISHED"}
def draw(self, context):
layout = self.layout
prop = get_selected_property()
layout.label(text="Categories")
for i, cat in enumerate(context.scene.sn.property_categories):
row = layout.row()
row.enabled = prop != None and prop.category != cat.name
row.scale_y = 1.2
row.operator("sn.move_property_to_category", text=f"Move to '{cat.name}'", icon="FORWARD").category = i
row = layout.row()
row.enabled = prop != None and prop.category and prop.category != "OTHER"
row.scale_y = 1.2
row.operator("sn.move_property_to_category", text=f"Remove Category", icon="REMOVE").category = -1
if not len(context.scene.sn.property_categories):
row = layout.row()
row.enabled = False
row.label(text="No categories added", icon="ERROR")
def invoke(self, context, event):
context.scene.sn.property_index = self.index
return context.window_manager.invoke_popup(self, width=250)
@@ -0,0 +1,333 @@
import bpy
from ...nodes.compiler import compile_addon
from ...interface.panels.property_ui_list import get_selected_property, get_selected_property_offset
class SN_OT_AddProperty(bpy.types.Operator):
bl_idname = "sn.add_property"
bl_label = "Add Property"
bl_description = "Adds a property to the addon"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
sn = context.scene.sn
new_prop = sn.properties.add()
new_prop.name = "New Property"
if sn.active_prop_category: new_prop.category = sn.active_prop_category
for index, property in enumerate(sn.properties):
if property == new_prop:
sn.property_index = index
return {"FINISHED"}
class SN_OT_RemoveProperty(bpy.types.Operator):
bl_idname = "sn.remove_property"
bl_label = "Remove Property"
bl_description = "Removes this property from the addon"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context):
return context.scene.sn.property_index < len(context.scene.sn.properties)
def execute(self, context):
sn = context.scene.sn
sn.properties.remove(sn.property_index)
sn.property_index -= 1
compile_addon()
return {"FINISHED"}
class SN_OT_RemoveGroupProperty(bpy.types.Operator):
bl_idname = "sn.remove_group_property"
bl_label = "Remove Property"
bl_description = "Removes this property from the addon"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
group_items_path: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
items = eval(self.group_items_path)
items.remove(self.index)
return {"FINISHED"}
class SN_OT_MoveProperty(bpy.types.Operator):
bl_idname = "sn.move_property"
bl_label = "Move Property"
bl_description = "Moves this property"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
move_up: bpy.props.BoolProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
sn = context.scene.sn
if self.move_up:
before = get_selected_property_offset(-1)
new_index = list(sn.properties).index(before)
sn.properties.move(sn.property_index, new_index)
sn.property_index = new_index
else:
after = get_selected_property_offset(1)
new_index = list(sn.properties).index(after)
sn.properties.move(sn.property_index, new_index)
sn.property_index = new_index
return {"FINISHED"}
class SN_OT_DuplicateProperty(bpy.types.Operator):
bl_idname = "sn.duplicate_property"
bl_label = "Duplicate Property"
bl_description = "Duplicates the selected property"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
sn = context.scene.sn
prop = get_selected_property()
if prop:
prop.copy()
sn.properties.move(len(sn.properties)-1, sn.property_index+1)
sn.property_index += 1
return {"FINISHED"}
class SN_OT_MoveGroupProperty(bpy.types.Operator):
bl_idname = "sn.move_group_property"
bl_label = "Move Property"
bl_description = "Moves this property"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
group_items_path: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
move_up: bpy.props.BoolProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
items = eval(self.group_items_path)
if self.move_up:
items.move(self.index, self.index - 1)
else:
items.move(self.index, self.index + 1)
return {"FINISHED"}
class SN_OT_CopyPythonName(bpy.types.Operator):
bl_idname = "sn.copy_python_name"
bl_label = "Copy Python Name"
bl_description = "Copies the python name of this item to use in scripts"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
name: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
context.window_manager.clipboard = self.name
self.report({"INFO"}, message="Copied!")
return {"FINISHED"}
class SN_OT_AddEnumItem(bpy.types.Operator):
bl_idname = "sn.add_enum_item"
bl_label = "Add Enum Item"
bl_description = "Adds an enum item to this property"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
item_data_path: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
items = eval(self.item_data_path)
item = items.add()
item.update(context)
return {"FINISHED"}
class SN_OT_RemoveEnumItem(bpy.types.Operator):
bl_idname = "sn.remove_enum_item"
bl_label = "Remove Enum Item"
bl_description = "Removes an enum item from this property"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
settings_data_path: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
item_index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
settings = eval(self.settings_data_path)
settings.items.remove(self.item_index)
settings.compile(context)
return {"FINISHED"}
class SN_OT_MoveEnumItem(bpy.types.Operator):
bl_idname = "sn.move_enum_item"
bl_label = "Move Enum Item"
bl_description = "Moves this enum item"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
settings_data_path: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
item_index: bpy.props.IntProperty(options={"SKIP_SAVE", "HIDDEN"})
move_up: bpy.props.BoolProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
settings = eval(self.settings_data_path)
if self.move_up:
settings.items.move(self.item_index, self.item_index-1)
else:
settings.items.move(self.item_index, self.item_index+1)
settings.compile(context)
return {"FINISHED"}
class SN_OT_AddPropertyItem(bpy.types.Operator):
bl_idname = "sn.add_property_item"
bl_label = "Add Property"
bl_description = "Adds a property to this group"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
group_data_path: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
prop = eval(self.group_data_path)
new_prop = prop.settings.properties.add()
new_prop.name = "New Property"
return {"FINISHED"}
class SN_OT_AddPropertyNodePopup(bpy.types.Operator):
bl_idname = "sn.add_property_node_popup"
bl_label = "Add Property Node Popup"
bl_description = "Opens a popup to let you choose a property node"
bl_options = {"REGISTER", "INTERNAL"}
node: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
return {"FINISHED"}
def draw(self, context):
layout = self.layout
col = layout.column(align=True)
col.scale_y = 1.5
op = col.operator("sn.add_property_node", text="Property", icon="ADD")
op.type = "SN_SerpensPropertyNode"
op.node = self.node
op = col.operator("sn.add_property_node", text="Display Property", icon="ADD")
op.type = "SN_DisplayPropertyNodeNew"
op.node = self.node
op = col.operator("sn.add_property_node", text="Set Property", icon="ADD")
op.type = "SN_SetPropertyNode"
op.node = self.node
op = col.operator("sn.add_property_node", text="On Property Update", icon="ADD")
op.type = "SN_OnPropertyUpdateNode"
op.node = self.node
def invoke(self, context, event):
return context.window_manager.invoke_popup(self)
class SN_OT_AddPropertyNode(bpy.types.Operator):
bl_idname = "sn.add_property_node"
bl_label = "Add Property Node"
bl_description = "Adds this node to the editor"
bl_options = {"REGISTER", "INTERNAL"}
type: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
node: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
bpy.ops.node.add_node("INVOKE_DEFAULT", type=self.type, use_transform=True)
node = context.space_data.node_tree.nodes.active
prop = None
if self.node:
prop_node = context.space_data.node_tree.nodes[self.node]
if prop_node.property_index < len(prop_node.properties):
prop = prop_node.properties[prop_node.property_index]
elif context.scene.sn.property_index < len(context.scene.sn.properties):
prop = context.scene.sn.properties[context.scene.sn.property_index]
if prop:
if self.type in ["SN_SerpensPropertyNode", "SN_OnPropertyUpdateNode"]:
if self.node:
node.prop_source = "NODE"
node.from_node = self.node
node.prop_name = prop.name
return {"FINISHED"}
class SN_OT_FindProperty(bpy.types.Operator):
bl_idname = "sn.find_property"
bl_label = "Find Property"
bl_description = "Finds this property in the addon"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
return {"FINISHED"}
def draw(self, context):
layout = self.layout
# init property nodes
empty_nodes = []
property_nodes = []
property = None
if context.scene.sn.property_index < len(context.scene.sn.properties):
property = context.scene.sn.properties[context.scene.sn.property_index]
# find property nodes
for ngroup in bpy.data.node_groups:
if ngroup.bl_idname == "ScriptingNodesTree":
for node in ngroup.nodes:
if node.bl_idname == "SN_SerpensPropertyNode":
prop_src = node.get_prop_source()
if prop_src and node.prop_name in prop_src.properties:
prop = prop_src.properties[node.prop_name]
if prop == property:
property_nodes.append(node)
elif not prop_src or not node.prop_name:
empty_nodes.append(node)
# draw nodes for selected property
if context.scene.sn.property_index < len(context.scene.sn.properties):
col = layout.column()
row = col.row()
row.enabled = False
row.label(text=f"Property: {property.name}")
for node in property_nodes:
op = col.operator("sn.find_node", text=node.name, icon="RESTRICT_SELECT_OFF")
op.node_tree = node.node_tree.name
op.node = node.name
if not property_nodes:
col.label(text="No nodes found for this property", icon="INFO")
# draw nodes with empty property
col = layout.column()
row = col.row()
row.label(text="Empty Propert Nodes")
row.enabled = False
for node in empty_nodes:
op = col.operator("sn.find_node", text=node.name, icon="RESTRICT_SELECT_OFF")
op.node_tree = node.node_tree.name
op.node = node.name
if not empty_nodes:
col.label(text="No empty property nodes found", icon="INFO")
def invoke(self, context, event):
return context.window_manager.invoke_popup(self, width=250)
@@ -0,0 +1,21 @@
def get_sorted_props(prop_list):
""" Returns a list of the properties, sorted for registering """
# sort groups to the top of the list
prop_list.sort(key=lambda prop: prop.property_type == "Group", reverse=True)
# split property groups with collections or pointers with use prop group enabled
prop_groups = list(filter(lambda prop: prop.property_type == "Group", prop_list))
other_props = list(filter(lambda prop: not prop in prop_groups, prop_list))
ref_prop_groups = list(filter(_is_propgroup_with_references, prop_groups))
prop_groups = list(filter(lambda prop: not prop in ref_prop_groups, prop_groups))
# TODO sort ref_prop_groups -> may fail if a prop group has a prop with a collection or pointer to another prop group with the same
return prop_groups + ref_prop_groups + other_props
def _is_propgroup_with_references(prop):
""" Returns if the given property group has pointer or collection props with references to other prop groups """
for subprop in prop.settings.properties:
if subprop.property_type == "Collection" and subprop.settings.prop_group in prop.prop_collection_origin.properties:
return True
elif subprop.property_type == "Pointer" and subprop.settings.use_prop_group and subprop.settings.prop_group in prop.prop_collection_origin.properties:
return True
return False
@@ -0,0 +1,64 @@
import bpy
from .settings import PropertySettings
class SN_PT_BooleanProperty(PropertySettings, bpy.types.PropertyGroup):
type_description = "Boolean properties can hold a value of True or False.\n" \
+ "They can also be turned into a vector which holds multiple of these.\n" \
+ "\n" \
+ "Booleans are displayed as checkboxes or toggles in the UI."
copy_attributes = ["default", "is_vector", "size", "vector_default"]
def draw(self, context, layout):
""" Draws the settings for this property type """
row = layout.row(heading="Default")
row.enabled = not self.is_vector
row.prop(self, "default", toggle=True, text=str(self.default))
layout.separator()
layout.prop(self, "is_vector")
col = layout.column()
col.enabled = self.is_vector
col.prop(self, "size")
sub_col = col.column(align=True, heading="Default")
for i in range(self.size):
sub_col.prop(self, "vector_default", index=i, text=str(self.vector_default[i]), toggle=True)
@property
def prop_type_name(self):
if self.is_vector:
return "BoolVectorProperty"
return "BoolProperty"
@property
def register_options(self):
if self.is_vector:
options = f"size={self.size}, default={tuple(list(self.vector_default)[:self.size])}"
else:
options = f"default={self.default}"
return options + self.update_option
default: bpy.props.BoolProperty(name="Default",
description="Default value of this property (This may not reset automatically for existing attached items)",
update=PropertySettings.compile)
def update_vector(self, context):
self.prop.trigger_reference_update(context)
is_vector: bpy.props.BoolProperty(name="Is Vector",
description="If this property is a vector",
update=update_vector)
size: bpy.props.IntProperty(name="Vector Size", min=2, max=32, default=3,
description="Length of the vector property",
update=PropertySettings.compile)
vector_default: bpy.props.BoolVectorProperty(name="Default",
description="Default value of this property (This may not reset automatically for existing attached items)",
size=32,
update=PropertySettings.compile)
@@ -0,0 +1,47 @@
import bpy
from .settings import PropertySettings
class SN_PT_CollectionProperty(PropertySettings, bpy.types.PropertyGroup):
type_description = "Integer properties can hold decimal number.\n" \
+ "They can also be turned into a vector which holds multiple of these.\n" \
+ "\n" \
+ "Integers are displayed as number inputs."
copy_attributes = ["prop_group"]
def draw(self, context, layout):
""" Draws the settings for this property type """
src = context.scene.sn
layout.prop_search(self, "prop_group", src, "properties")
row = layout.row()
row.alert = True
if self.prop_group in src.properties:
if not src.properties[self.prop_group].property_type == "Group":
row.label(text="The selected property is not a group!", icon="ERROR")
elif hasattr(self.prop, "group_prop_parent") and self.prop.group_prop_parent.name == self.prop_group:
row.label(text="Can't use self reference for this collection!", icon="ERROR")
else:
row.label(text="There is no valid property group selected!", icon="ERROR")
@property
def prop_type_name(self):
return "CollectionProperty"
@property
def register_options(self):
src = self.prop.prop_collection_origin
if self.prop_group in src.properties and src.properties[self.prop_group].property_type == "Group":
if not hasattr(self.prop, "group_prop_parent") or (hasattr(self.prop, "group_prop_parent") and self.prop.group_prop_parent.name != self.prop_group):
return f"type=SNA_GROUP_{src.properties[self.prop_group].python_name}"
return "type=bpy.types.PropertyGroup.__subclasses__()[0]"
prop_group: bpy.props.StringProperty(name="Property Group",
description="The property group you want to point to",
update=PropertySettings.compile)
@@ -0,0 +1,180 @@
import bpy
from ....utils import normalize_code
from .settings import PropertySettings
_enum_prop_cache = {} # stores key, value of enum.as_pointer, prop
class EnumItem(bpy.types.PropertyGroup):
@property
def prop(self):
if self.id_data.bl_rna.identifier == "ScriptingNodesTree":
# find property in nodes to return
if not str(self.as_pointer()) in _enum_prop_cache:
for node in self.id_data.nodes:
if hasattr(node, "properties"):
for prop in node.properties:
if prop.property_type == "Enum":
for item in prop.settings.items:
if item == self:
_enum_prop_cache[str(self.as_pointer())] = prop
break
elif prop.property_type == "Group":
for subprop in prop.settings.properties:
if subprop.property_type == "Enum":
for item in subprop.settings.items:
if item == self:
_enum_prop_cache[str(self.as_pointer())] = prop
break
return _enum_prop_cache[str(self.as_pointer())]
else:
path = ".".join(repr(self.path_resolve("name", False)).split(".")[:-2])
prop = eval(path)
return prop
def update(self, context):
self.prop.compile()
name: bpy.props.StringProperty(name="Name", default="New Item",
description="Name of this enum item",
update=update)
description: bpy.props.StringProperty(name="Description",
description="Description of this enum item",
update=update)
icon: bpy.props.IntProperty(name="Icon", default=0, min=0,
description="Icon value of this enum item",
update=update)
class SN_PT_EnumProperty(PropertySettings, bpy.types.PropertyGroup):
type_description = "Enum properties can hold multiple items with a name and description.\n" \
+ "\n" \
+ "Enum properties are displayed as dropdowns or a list of toggles.\n" \
+ "Dynamic enums can be used to display custom icons such as a list of asset images."
copy_attributes = ["enum_flag", "is_dynamic"]
def copy(self, new_settings):
for item in self.items:
new = new_settings.items.add()
new.name = item.name
new.description = item.description
new.icon = item.icon
def draw(self, context, layout):
""" Draws the settings for this property type """
layout.prop(self, "enum_flag")
layout.prop(self, "is_dynamic")
layout.separator()
row = layout.row()
row.scale_y = 1.2
if not self.is_dynamic:
op = row.operator("sn.add_enum_item", text="Add Item", icon="ADD")
op.item_data_path = f"{self.prop.full_prop_path}.settings.items"
for i, item in enumerate(self.items):
box = layout.box()
col = box.column()
box.use_property_split = False
row = col.row()
subrow = row.row(align=True)
subrow.prop(item, "name", text="")
op = subrow.operator("sn.select_icon", icon_value=item.icon if item.icon != 0 else 101, text="", emboss=item.icon==0)
op.icon_data_path = f"{self.prop.full_prop_path}.settings.items[{i}]"
subrow = row.row(align=True)
subcol = subrow.column(align=True)
subcol.enabled = i > 0
op = subcol.operator("sn.move_enum_item", text="", icon="TRIA_UP", emboss=False)
op.settings_data_path = f"{self.prop.full_prop_path}.settings"
op.item_index = i
op.move_up = True
subcol = subrow.column(align=True)
subcol.enabled = i < len(self.items)-1
op = subcol.operator("sn.move_enum_item", text="", icon="TRIA_DOWN", emboss=False)
op.settings_data_path = f"{self.prop.full_prop_path}.settings"
op.item_index = i
op.move_up = False
op = subrow.operator("sn.remove_enum_item", text="", icon="PANEL_CLOSE", emboss=False)
op.settings_data_path = f"{self.prop.full_prop_path}.settings"
op.item_index = i
col.prop(item, "description")
else:
op = row.operator("sn.add_generate_items_node", text="Generate Items", icon="ADD")
@property
def prop_type_name(self):
return "EnumProperty"
@property
def item_func_name(self):
name = f"{self.prop.python_name}_enum_items"
if hasattr(self.prop, "group_prop_parent"):
return f"{self.prop.group_prop_parent.python_name}_{name}"
return name
@property
def register_options(self):
options = ""
if not self.is_dynamic:
items = []
for i, item in enumerate(self.items):
if self.enum_flag:
i = 2 ** i
items.append(f"('{item.name}', '{item.name}', '{item.description}', {item.icon}, {i})")
options = f"items=[{', '.join(items)}]"
else:
options = f"items={self.item_func_name}"
if self.enum_flag:
options += ", options={'ENUM_FLAG'}"
return options + self.update_option
def imperative_code(self):
# node exists for this property
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
for ref in ntree.node_collection("SN_GenerateEnumItemsNode").refs:
node = ref.node
enum_src = node.get_prop_source()
if enum_src and node.prop_name in enum_src.properties and enum_src.properties[node.prop_name] == self.prop:
return ""
code = f"""
def {self.item_func_name}(self, context):
return [("No Items", "No Items", "No generate enum items node found to create items!", "ERROR", 0)]
"""
return normalize_code(code) + "\n" + self.update_function
def update_enum_flag(self, context):
self.prop.trigger_reference_update(context)
self.compile(context)
enum_flag: bpy.props.BoolProperty(name="Select Multiple (Enum Set)",
description="Lets you select multiple options from this property",
update=update_enum_flag)
is_dynamic: bpy.props.BoolProperty(name="Dynamic Items",
description="The items are generated with a function and aren't predefined",
update=PropertySettings.compile)
items: bpy.props.CollectionProperty(type=EnumItem,
name="Items",
description="Enum Items")
@@ -0,0 +1,206 @@
import bpy
from .settings import PropertySettings
class SN_PT_FloatProperty(PropertySettings, bpy.types.PropertyGroup):
type_description = "Float properties can hold decimal number.\n" \
+ "They can also be turned into a vector which holds multiple of these.\n" \
+ "\n" \
+ "Floats are displayed as number inputs or sliders.\n" \
+ "Float vectors can be used with subtypes, for example to make a color input."
copy_attributes = ["is_vector", "default", "subtype", "unit", "use_min", "min", "use_max", "use_soft_min",
"soft_min", "max", "use_soft_max", "soft_max", "step", "precision", "size", "vector_default"]
def draw(self, context, layout):
""" Draws the settings for this property type """
layout.prop(self, "subtype")
layout.prop(self, "unit")
row = layout.row()
row.enabled = not self.is_vector
row.prop(self, "default")
layout.separator()
layout.prop(self, "step")
layout.prop(self, "precision")
layout.separator()
row = layout.row(heading="Minimum")
row.prop(self, "use_min", text="")
sub_row = row.row()
sub_row.enabled = self.use_min
sub_row.prop(self, "min")
row = layout.row(heading="Maximum")
row.prop(self, "use_max", text="")
sub_row = row.row()
sub_row.enabled = self.use_max
sub_row.prop(self, "max")
row = layout.row(heading="Soft Minimum")
row.prop(self, "use_soft_min", text="")
sub_row = row.row()
sub_row.enabled = self.use_soft_min
sub_row.prop(self, "soft_min")
row = layout.row(heading="Soft Maximum")
row.prop(self, "use_soft_max", text="")
sub_row = row.row()
sub_row.enabled = self.use_soft_max
sub_row.prop(self, "soft_max")
layout.separator()
layout.prop(self, "is_vector")
col = layout.column()
col.enabled = self.is_vector
col.prop(self, "size")
row = col.row()
split = row.split(factor=0.4)
split.alignment = "RIGHT"
split.label(text="Default")
sub_col = split.column(align=True)
for i in range(self.size):
sub_col.prop(self, "vector_default", index=i, text="")
@property
def prop_type_name(self):
if self.is_vector:
return "FloatVectorProperty"
return "FloatProperty"
@property
def register_options(self):
if self.is_vector:
options = f"size={self.size}, default={tuple(list(self.vector_default)[:self.size])}"
else:
options = f"default={self.default}"
options += f", subtype='{self.subtype}'"
options += f", unit='{self.unit}'"
if self.use_min: options += f", min={self.min}"
if self.use_soft_min: options += f", soft_min={self.soft_min}"
if self.use_max: options += f", max={self.max}"
if self.use_soft_max: options += f", soft_max={self.soft_max}"
options += f", step={self.step}"
options += f", precision={self.precision}"
return options + self.update_option
default: bpy.props.FloatProperty(name="Default",
description="Default value of this property (This may not reset automatically for existing attached items)",
update=PropertySettings.compile)
def get_subtype_items(self, context):
items = [("NONE", "None", "No subtype, just a default float input"),
("PIXEL", "Pixel", "Pixel"),
("UNSIGNED", "Unsigned", "Unsigned"),
("PERCENTAGE", "Percentage", "Percentage"),
("FACTOR", "Factor", "Factor"),
("ANGLE", "Angle", "Angle"),
("TIME", "Time", "Time"),
("DISTANCE", "Distance", "Distance"),
("DISTANCE_CAMERA", "Distance Camera", "Distance Camera"),
("POWER", "Power", "Power"),
("TEMPERATURE", "Temperature", "Temperature")]
if self.is_vector:
items = [("NONE", "None", "No subtype, just a default float vector input"),
("COLOR", "Color", "Color"),
("TRANSLATION", "Translation", "Translation"),
("DIRECTION", "Direction", "Direction"),
("VELOCITY", "Velocity", "Velocity"),
("ACCELERATION", "Acceleration", "Acceleration"),
("MATRIX", "Matrix", "Matrix"),
("EULER", "Euler", "Euler"),
("QUATERNION", "Quaternion", "Quaternion"),
("AXISANGLE", "Axisangle", "Axisangle"),
("XYZ", "XYZ", "XYZ"),
("XYZ_LENGTH", "XYZ Length", "XYZ Length"),
("COLOR_GAMMA", "Color Gamma", "Color Gamma"),
("COORDINATES", "Coordinates", "Coordinates"),
("LAYER", "Layer", "Layer"),
("LAYER_MEMBER", "Layer Member", "Layer Member"),]
return items
subtype: bpy.props.EnumProperty(name="Subtype",
description="The subtype of this property. This changes how the property is displayed",
update=PropertySettings.compile,
items=get_subtype_items)
unit: bpy.props.EnumProperty(name="Unit",
description="The unit of this property. This changes how the property is displayed",
update=PropertySettings.compile,
items=[("NONE", "None", "No unit, just a default float input"),
("LENGTH", "Length", "Length"),
("AREA", "Area", "Area"),
("VOLUME", "Volume", "Volume"),
("ROTATION", "Rotation", "Rotation"),
("TIME", "Time", "Time"),
("VELOCITY", "Velocity", "Velocity"),
("ACCELERATION", "Acceleration", "Acceleration"),
("MASS", "Mass", "Mass"),
("CAMERA", "Camera", "Camera"),
("POWER", "Power", "Power")])
use_min: bpy.props.BoolProperty(name="Minimum",
description="Use a minimum property value",
update=PropertySettings.compile)
min: bpy.props.FloatProperty(name="Minimum", default=-0,
description="The minimum value of this property",
update=PropertySettings.compile)
use_max: bpy.props.BoolProperty(name="Maximum",
description="Use a maximum property value",
update=PropertySettings.compile)
use_soft_min: bpy.props.BoolProperty(name="Soft Minimum",
description="Use a soft minimum property value",
update=PropertySettings.compile)
soft_min: bpy.props.FloatProperty(name="Soft Minimum", default=-0,
description="The soft minimum value of this property",
update=PropertySettings.compile)
max: bpy.props.FloatProperty(name="Maximum", default=1,
description="The maximum value of this property",
update=PropertySettings.compile)
use_soft_max: bpy.props.BoolProperty(name="Soft Maximum",
description="Use a soft maximum property value",
update=PropertySettings.compile)
soft_max: bpy.props.FloatProperty(name="Soft Maximum", default=1,
description="The soft maximum value of this property",
update=PropertySettings.compile)
step: bpy.props.IntProperty(name="Step", min=1, max=100, default=3,
description="Step of increment/decrement in the UI",
update=PropertySettings.compile)
precision: bpy.props.IntProperty(name="Precision", min=0, max=6, default=6,
description="Maximum number of decimal digits to display",
update=PropertySettings.compile)
def update_vector(self, context):
self.prop.trigger_reference_update(context)
is_vector: bpy.props.BoolProperty(name="Is Vector",
description="If this property is a vector",
update=update_vector)
size: bpy.props.IntProperty(name="Vector Size", min=2, max=32, default=3,
description="Length of the vector property",
update=PropertySettings.compile)
vector_default: bpy.props.FloatVectorProperty(name="Default",
description="Default value of this property (This may not reset automatically for existing attached items)",
size=32,
update=PropertySettings.compile)
@@ -0,0 +1,120 @@
import bpy
from .settings import PropertySettings
from ..property_basic import BasicProperty
_group_prop_cache = {} # stores key, value of prop.as_pointer, prop
class SN_SimpleProperty(BasicProperty, bpy.types.PropertyGroup):
expand: bpy.props.BoolProperty(default=True, name="Expand", description="Expand this property")
@property
def group_prop_parent(self):
""" Returns the parent of the property collection this property lives in """
if self.id_data.bl_rna.identifier == "ScriptingNodesTree":
# find property in nodes to return
if not str(self.as_pointer()) in _group_prop_cache:
for node in self.id_data.nodes:
if hasattr(node, "properties"):
for prop in node.properties:
if prop.property_type == "Group":
for subprop in prop.settings.properties:
if subprop == self:
_group_prop_cache[str(self.as_pointer())] = prop
break
return _group_prop_cache[str(self.as_pointer())]
else:
coll_path = "[".join(repr(self.path_resolve("name", False)).split("[")[:-1])
parent_path = coll_path.split("stngs_group")[0][:-1]
return eval(parent_path)
@property
def python_name(self):
return super().python_name[4:] # cut of sna_ for props in prop group (mainly for name prop)
def compile(self, context=None):
self.group_prop_parent.compile()
class SN_PT_GroupProperty(PropertySettings, bpy.types.PropertyGroup):
type_description = "Group properties can hold multiple other properties.\n" \
+ "They are used in combination with a pointer or collection property.\n" \
+ "Use a property called 'Name' to find properties in a collection.\n" \
+ "\n" \
+ "A common use for group properties is to group your addons settings together."
copy_attributes = []
def copy(self, new_settings):
for prop in self.properties:
new_prop = new_settings.properties.add()
prop.match_settings(new_prop)
def draw(self, context, layout):
""" Draws the settings for this property type """
row = layout.row()
row.scale_y = 1.2
op = row.operator("sn.add_property_item", text="Add Property", icon="ADD")
op.group_data_path = f"{self.prop.full_prop_path}"
for i, prop in enumerate(self.properties):
box = layout.box()
row = box.row()
subrow = row.row()
subrow.prop(prop, "expand", text="", icon="DISCLOSURE_TRI_DOWN" if prop.expand else "DISCLOSURE_TRI_RIGHT", emboss=False)
row.prop(prop, "name", text="")
subrow = row.row(align=True)
subcol = subrow.column(align=True)
subcol.enabled = i > 0
op = subcol.operator("sn.move_group_property", text="", icon="TRIA_UP")
op.group_items_path = f"{self.prop.full_prop_path}.settings.properties"
op.index = i
op.move_up = True
subcol = subrow.column(align=True)
subcol.enabled = i < len(self.properties)-1
op = subcol.operator("sn.move_group_property", text="", icon="TRIA_DOWN")
op.group_items_path = f"{self.prop.full_prop_path}.settings.properties"
op.index = i
op.move_up = False
op = row.operator("sn.remove_group_property", text="", icon="TRASH", emboss=False)
op.group_items_path = f"{self.prop.full_prop_path}.settings.properties"
op.index = i
row.operator("sn.copy_python_name", text="", icon="COPYDOWN", emboss=False).name = "PROP_PATH_PLACEHOLDER."+prop.python_name
if prop.expand:
prop.draw(context, box)
box.separator()
prop.settings.draw(context, box)
@property
def prop_type_name(self):
return f"SNA_GROUP_{self.prop.python_name}"
@property
def register_options(self):
return f""
def imperative_code(self):
code = f"class {self.prop_type_name}(bpy.types.PropertyGroup):\n\n"
for prop in self.properties:
for line in prop.register_code.split("\n"):
code += " "*4 + line + "\n"
code += "\n"
if not len(self.properties):
code += " "*4 + "pass\n\n"
return code
properties: bpy.props.CollectionProperty(type=SN_SimpleProperty)
@@ -0,0 +1,172 @@
import bpy
from .settings import PropertySettings
class SN_PT_IntegerProperty(PropertySettings, bpy.types.PropertyGroup):
type_description = "Integer properties can hold decimal number.\n" \
+ "They can also be turned into a vector which holds multiple of these.\n" \
+ "\n" \
+ "Integers are displayed as number inputs."
copy_attributes = ["is_vector", "default", "subtype", "use_min", "min", "use_max", "use_soft_min",
"soft_min", "max", "use_soft_max", "soft_max", "step", "size", "vector_default"]
def draw(self, context, layout):
""" Draws the settings for this property type """
layout.prop(self, "subtype")
row = layout.row()
row.enabled = not self.is_vector
row.prop(self, "default")
layout.separator()
row = layout.row(heading="Minimum")
row.prop(self, "use_min", text="")
sub_row = row.row()
sub_row.enabled = self.use_min
sub_row.prop(self, "min")
row = layout.row(heading="Maximum")
row.prop(self, "use_max", text="")
sub_row = row.row()
sub_row.enabled = self.use_max
sub_row.prop(self, "max")
row = layout.row(heading="Soft Minimum")
row.prop(self, "use_soft_min", text="")
sub_row = row.row()
sub_row.enabled = self.use_soft_min
sub_row.prop(self, "soft_min")
row = layout.row(heading="Soft Maximum")
row.prop(self, "use_soft_max", text="")
sub_row = row.row()
sub_row.enabled = self.use_soft_max
sub_row.prop(self, "soft_max")
layout.separator()
layout.prop(self, "is_vector")
col = layout.column()
col.enabled = self.is_vector
col.prop(self, "size")
row = col.row()
split = row.split(factor=0.4)
split.alignment = "RIGHT"
split.label(text="Default")
sub_col = split.column(align=True)
for i in range(self.size):
sub_col.prop(self, "vector_default", index=i, text="")
@property
def prop_type_name(self):
if self.is_vector:
return "IntVectorProperty"
return "IntProperty"
@property
def register_options(self):
if self.is_vector:
options = f"size={self.size}, default={tuple(list(self.vector_default)[:self.size])}"
else:
options = f"default={self.default}"
options += f", subtype='{self.subtype}'"
if self.use_min: options += f", min={self.min}"
if self.use_soft_min: options += f", soft_min={self.soft_min}"
if self.use_max: options += f", max={self.max}"
if self.use_soft_max: options += f", soft_max={self.soft_max}"
return options + self.update_option
default: bpy.props.IntProperty(name="Default",
description="Default value of this property (This may not reset automatically for existing attached items)",
update=PropertySettings.compile)
def get_subtype_items(self, context):
items = [("NONE", "None", "No subtype, just a default float input"),
("PIXEL", "Pixel", "Pixel"),
("UNSIGNED", "Unsigned", "Unsigned"),
("PERCENTAGE", "Percentage", "Percentage"),
("FACTOR", "Factor", "Factor"),
("ANGLE", "Angle", "Angle"),
("TIME", "Time", "Time"),
("DISTANCE", "Distance", "Distance"),
("DISTANCE_CAMERA", "Distance Camera", "Distance Camera"),
("POWER", "Power", "Power"),
("TEMPERATURE", "Temperature", "Temperature")]
if self.is_vector:
items = [("NONE", "None", "No subtype, just a default float vector input"),
("COLOR", "Color", "Color"),
("TRANSLATION", "Translation", "Translation"),
("DIRECTION", "Direction", "Direction"),
("VELOCITY", "Velocity", "Velocity"),
("ACCELERATION", "Acceleration", "Acceleration"),
("MATRIX", "Matrix", "Matrix"),
("EULER", "Euler", "Euler"),
("QUATERNION", "Quaternion", "Quaternion"),
("AXISANGLE", "Axisangle", "Axisangle"),
("XYZ", "XYZ", "XYZ"),
("XYZ_LENGTH", "XYZ Length", "XYZ Length"),
("COLOR_GAMMA", "Color Gamma", "Color Gamma"),
("COORDINATES", "Coordinates", "Coordinates"),
("LAYER", "Layer", "Layer"),
("LAYER_MEMBER", "Layer Member", "Layer Member"),]
return items
subtype: bpy.props.EnumProperty(name="Subtype",
description="The subtype of this property. This changes how the property is displayed",
update=PropertySettings.compile,
items=get_subtype_items)
use_min: bpy.props.BoolProperty(name="Minimum",
description="Use a minimum property value",
update=PropertySettings.compile)
min: bpy.props.IntProperty(name="Minimum", default=-0,
description="The minimum value of this property",
update=PropertySettings.compile)
use_max: bpy.props.BoolProperty(name="Maximum",
description="Use a maximum property value",
update=PropertySettings.compile)
use_soft_min: bpy.props.BoolProperty(name="Soft Minimum",
description="Use a soft minimum property value",
update=PropertySettings.compile)
soft_min: bpy.props.IntProperty(name="Soft Minimum", default=-0,
description="The soft minimum value of this property",
update=PropertySettings.compile)
max: bpy.props.IntProperty(name="Maximum", default=1,
description="The maximum value of this property",
update=PropertySettings.compile)
use_soft_max: bpy.props.BoolProperty(name="Soft Maximum",
description="Use a soft maximum property value",
update=PropertySettings.compile)
soft_max: bpy.props.IntProperty(name="Soft Maximum", default=1,
description="The soft maximum value of this property",
update=PropertySettings.compile)
def update_vector(self, context):
self.prop.trigger_reference_update(context)
is_vector: bpy.props.BoolProperty(name="Is Vector",
description="If this property is a vector",
update=update_vector)
size: bpy.props.IntProperty(name="Vector Size", min=2, max=32, default=3,
description="Length of the vector property",
update=PropertySettings.compile)
vector_default: bpy.props.IntVectorProperty(name="Default",
description="Default value of this property (This may not reset automatically for existing attached items)",
size=32,
update=PropertySettings.compile)
@@ -0,0 +1,75 @@
import bpy
from .settings import PropertySettings, id_items
class SN_PT_PointerProperty(PropertySettings, bpy.types.PropertyGroup):
type_description = "Pointer properties can point to specific types of blend data or property groups.\n" \
+ "\n" \
+ "They are often used to point to your addons settings, which could live grouped\n" \
+ "in a property group and be attached to the scene.\n" \
+ "\n" \
+ "When used with blend data, you can use pointers to let the user select the data\n" \
+ "from a dropdown and get the blend data from the property."
copy_attributes = ["data_type", "use_prop_group", "prop_group"]
def draw(self, context, layout):
""" Draws the settings for this property type """
src = context.scene.sn
layout.prop(self, "use_prop_group")
if not self.use_prop_group:
layout.prop(self, "data_type")
else:
layout.prop_search(self, "prop_group", src, "properties")
row = layout.row()
row.alert = True
if self.prop_group and self.prop_group in src.properties:
if not src.properties[self.prop_group].property_type == "Group":
row.label(text="The selected property is not a group!", icon="ERROR")
elif hasattr(self.prop, "group_prop_parent") and self.prop.group_prop_parent.name == self.prop_group:
row.label(text="Can't use self reference for this collection!", icon="ERROR")
else:
row.label(text="There is no valid property group selected!", icon="ERROR")
@property
def prop_type_name(self):
return "PointerProperty"
@property
def register_options(self):
if not self.use_prop_group:
data_type = "bpy.types."+self.data_type
else:
src = self.prop.prop_collection_origin
data_type = "bpy.types.Scene"
if self.prop_group in src.properties and src.properties[self.prop_group].property_type == "Group":
if not hasattr(self.prop, "group_prop_parent") or (hasattr(self.prop, "group_prop_parent") and self.prop.group_prop_parent.name != self.prop_group):
data_type = f"SNA_GROUP_{bpy.context.scene.sn.properties[self.prop_group].python_name}"
return f"type={data_type}{self.update_option}"
def get_data_items(self, context):
items = []
for item in id_items:
items.append((item, item, item))
return items
data_type: bpy.props.EnumProperty(name="Data Type",
description="The type of blend data to have this property point to",
items=get_data_items,
update=PropertySettings.compile)
use_prop_group: bpy.props.BoolProperty(name="Use Property Group",
description="Point to a custom property group you created",
update=PropertySettings.compile)
prop_group: bpy.props.StringProperty(name="Property Group",
description="The property group you want to point to",
update=PropertySettings.compile)
@@ -0,0 +1,149 @@
import bpy
id_items = ["Scene", "Action", "Armature", "Brush", "CacheFile", "Camera",
"Collection", "Curve", "FreestyleLineStyle", "GreasePencil",
"Image", "Key", "Lattice", "Library", "Light", "LightProbe",
"Mask", "Material", "Mesh", "MetaBall", "MovieClip", "NodeTree",
"Object", "PaintCurve", "Palette", "ParticleSettings",
"Screen", "Sound", "Speaker", "Text", "Texture", "VectorFont",
"Volume", "WindowManager", "WorkSpace", "World"]
id_data = {"Scene": "scenes", "Action":"actions", "Armature":"armatures",
"Brush":"bruhes", "CacheFile":"cache_files", "Camera":"cameras",
"Collection":"collections", "Curve":"curves", "FreestyleLineStyle":"linestyles",
"GreasePencil":"grease_pencils", "Image": "images", "Key": "shape_keys",
"Lattice": "lattices", "Library": "libraries", "Light": "lights",
"LightProbe": "lightprobes", "Mask": "masks", "Material": "materials",
"Mesh": "meshes", "MetaBall": "metaballs", "MovieClip": "movieclips",
"NodeTree": "node_groups", "Object": "objects", "PaintCurve": "paint_curves",
"Palette": "palettes", "ParticleSettings": "particles", "Screen": "screens",
"Sound": "sounds", "Speaker": "speakers", "Text": "texts", "Texture": "textures",
"VectorFont": "fonts", "Volume": "volumes", "WindowManager": "window_managers",
"WorkSpace": "workspaces", "World": "worlds"}
property_icons = {
"String": "SYNTAX_OFF",
"Boolean": "FORCE_CHARGE",
"Boolean Vector": "FORCE_CHARGE",
"Float": "CON_TRANSLIKE",
"Float Vector": "CON_TRANSLIKE",
"Integer": "DRIVER_TRANSFORM",
"Integer Vector": "DRIVER_TRANSFORM",
"Enum": "PRESET",
"Enum Set": "PRESET",
"Pointer": "MONKEY",
"Property": "MONKEY",
"Collection": "ASSET_MANAGER",
"Collection Property": "ASSET_MANAGER",
"Group": "FILEBROWSER",
"List": "LONGDISPLAY",
"Data": "OBJECT_DATA",
"Icon": "DRIVER_TRANSFORM",
"Function": "FILE_SCRIPT",
"Built In Function": "SCRIPTPLUGINS",
}
property_socket = {
"String": "String",
"Boolean": "Boolean",
"Float": "Float",
"Integer": "Integer",
"Enum": "Enum",
"Pointer": "Property",
"Collection": "Collection Property",
"Group": "Data",
}
def prop_to_socket(prop):
socket_name = property_socket[prop.property_type]
if getattr(prop.settings, "enum_flag", False):
socket_name = "Enum Set"
if getattr(prop.settings, "is_vector", False):
socket_name += " Vector"
return socket_name
_prop_cache = {} # stores key, value of settings.as_pointer with prop for settings
class PropertySettings:
dummy: bpy.props.StringProperty(name="DUMMY", description="Dummy prop for resolving path")
copy_attributes = []
def copy(self, new_settings): pass
@property
def prop(self):
""" Returns the property these settings belong to """
if self.id_data.bl_rna.identifier == "ScriptingNodesTree":
# find property in nodes to return
if not str(self.as_pointer()) in _prop_cache:
for node in self.id_data.nodes:
if hasattr(node, "properties"):
for prop in node.properties:
if prop.settings == self:
_prop_cache[str(self.as_pointer())] = prop
break
elif prop.property_type == "Group":
for subprop in prop.settings.properties:
if subprop.settings == self:
_prop_cache[str(self.as_pointer())] = subprop
break
return _prop_cache[str(self.as_pointer())]
else:
path = ".".join(repr(self.path_resolve("dummy", False)).split(".")[:-2])
prop = eval(path)
return prop
def compile(self, context=None):
""" Compile the property for these settings """
self.prop.compile()
def imperative_code(self):
return self.update_function
def _update_function_names(self):
""" Returns the code for the on property update function """
updates = []
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
for node in ntree.node_collection("SN_OnPropertyUpdateNode").nodes:
prop_src = node.get_prop_source()
if prop_src and node.prop_name in prop_src.properties:
prop = prop_src.properties[node.prop_name]
if prop.name == self.prop.name:
updates.append((node.update_func_name(prop), node.order))
return list(map(lambda item: item[0], sorted(updates, key=lambda i: i[1])))
@property
def update_function(self):
""" Returns the code for the update function """
update_names = self._update_function_names()
if len(update_names) < 2:
return ""
else:
code = f"def sna_update_{self.prop.python_name}(self, context):\n"
for func in update_names:
code += " "*4 + f"{func}(self, context)\n"
return code
@property
def update_option(self):
""" Returns the code for the update function option """
update_names = self._update_function_names()
if len(update_names) == 0 or self.prop.property_type in ["Group", "Collection"]:
return ""
elif len(update_names) == 1:
return f", update={update_names[0]}"
return f", update=sna_update_{self.prop.python_name}"
@@ -0,0 +1,18 @@
import operator
import bpy
class SN_OT_AddGenerateItemsNode(bpy.types.Operator):
bl_idname = "sn.add_generate_items_node"
bl_label = "Generate Items"
bl_description = "Adds a node to generate dynamic enum items"
bl_options = {"REGISTER", "INTERNAL", "UNDO"}
def execute(self, context):
bpy.ops.node.add_node("INVOKE_DEFAULT", type="SN_GenerateEnumItemsNode", use_transform=True)
node = context.space_data.node_tree.nodes.active
if context.scene.sn.property_index < len(context.scene.sn.properties):
prop = context.scene.sn.properties[context.scene.sn.property_index]
node.prop_name = prop.name
return {"FINISHED"}
@@ -0,0 +1,54 @@
import bpy
from .settings import PropertySettings
class SN_PT_StringProperty(PropertySettings, bpy.types.PropertyGroup):
type_description = "String properties can hold a line of text.\n" \
+ "\n" \
+ "String properties are displayed as text inputs in the UI. \n" \
+ "There are subtypes to add a file selector to the string property."
copy_attributes = ["default", "subtype", "maxlen"]
def draw(self, context, layout):
""" Draws the settings for this property type """
layout.prop(self, "subtype")
layout.prop(self, "default")
layout.separator()
layout.prop(self, "maxlen")
@property
def prop_type_name(self):
return "StringProperty"
@property
def register_options(self):
return f"default='{self.default}', subtype='{self.subtype}', maxlen={self.maxlen}{self.update_option}"
default: bpy.props.StringProperty(name="Default",
description="Default value of this property (This may not reset automatically for existing attached items)",
update=PropertySettings.compile)
subtype: bpy.props.EnumProperty(name="Subtype",
description="The subtype of this property. This changes how the property is displayed",
update=PropertySettings.compile,
items=[("NONE", "None", "No subtype, just a default string input"),
("FILE_PATH", "File Path", "Display this property as a file path"),
("DIR_PATH", "Directory Path", "Display this property as a directory path"),
# ("FILE_NAME", "File Name", "Display that property as a file name"),
("BYTE_STRING", "Byte String", "Stores the string as a UTF-8 encoded byte string"),
("PASSWORD", "Password", "Displays asterisks in the UI to hide the typed string")])
maxlen: bpy.props.IntProperty(name="Max Length",
description="The maximum length of the string (0 is unlimited)",
min=0,
default=0,
update=PropertySettings.compile)
@@ -0,0 +1,18 @@
import bpy
def ntree_variable_register_code(ntree):
if len(ntree.variables) == 0: return ""
code = f"{ntree.python_name} = {{"
for var in ntree.variables:
code += f"'{var.python_name}': {var.var_default}, "
code += "}\n"
return code
def variable_register_code():
code = ""
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
code += ntree_variable_register_code(ntree)
return code
@@ -0,0 +1,177 @@
import bpy
from ...nodes.compiler import compile_addon
class SN_OT_AddVariable(bpy.types.Operator):
bl_idname = "sn.add_variable"
bl_label = "Add Variable"
bl_description = "Adds a variable to the addon"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
ntree = bpy.data.node_groups[self.node_tree]
new_var = ntree.variables.add()
new_var.name = "New Variable"
ntree.variables.move(len(ntree.variables)-1, ntree.variable_index+1)
ntree.variable_index += 1
ntree.variable_index = min(ntree.variable_index, len(ntree.variables)-1)
return {"FINISHED"}
class SN_OT_RemoveVariable(bpy.types.Operator):
bl_idname = "sn.remove_variable"
bl_label = "Remove Variable"
bl_description = "Removes this variable from the addon"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
ntree = bpy.data.node_groups[self.node_tree]
ntree.variables.remove(ntree.variable_index)
ntree.variable_index -= 1
compile_addon()
return {"FINISHED"}
class SN_OT_MoveVariable(bpy.types.Operator):
bl_idname = "sn.move_variable"
bl_label = "Move Variable"
bl_description = "Moves this variable"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
move_up: bpy.props.BoolProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
ntree = bpy.data.node_groups[self.node_tree]
if self.move_up:
ntree.variables.move(ntree.variable_index, ntree.variable_index - 1)
ntree.variable_index -= 1
else:
ntree.variables.move(ntree.variable_index, ntree.variable_index + 1)
ntree.variable_index += 1
return {"FINISHED"}
class SN_OT_AddVariableNodePopup(bpy.types.Operator):
bl_idname = "sn.add_variable_node_popup"
bl_label = "Add Variable Node Popup"
bl_description = "Opens a popup to let you choose a variable node"
bl_options = {"REGISTER", "INTERNAL"}
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
return {"FINISHED"}
def draw(self, context):
layout = self.layout
col = layout.column(align=True)
col.scale_y = 1.5
op = col.operator("sn.add_variable_node", text="Get Variable", icon="ADD")
op.type = "SN_GetVariableNode"
op.node_tree = self.node_tree
op = col.operator("sn.add_variable_node", text="Set Variable", icon="ADD")
op.type = "SN_SetVariableNode"
op.node_tree = self.node_tree
op = col.operator("sn.add_variable_node", text="Reset Variable", icon="ADD")
op.type = "SN_ResetVariableNode"
op.node_tree = self.node_tree
def invoke(self, context, event):
return context.window_manager.invoke_popup(self)
class SN_OT_AddVariableNode(bpy.types.Operator):
bl_idname = "sn.add_variable_node"
bl_label = "Add Variable Node"
bl_description = "Adds this node to the editor"
bl_options = {"REGISTER", "INTERNAL"}
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
type: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
bpy.ops.node.add_node("INVOKE_DEFAULT", type=self.type, use_transform=True)
node = context.space_data.node_tree.nodes.active
ntree = bpy.data.node_groups[self.node_tree]
if ntree.variable_index < len(ntree.variables):
var = ntree.variables[ntree.variable_index]
node.ref_ntree = ntree
node.var_name = var.name
return {"FINISHED"}
class SN_OT_FindVariable(bpy.types.Operator):
bl_idname = "sn.find_variable"
bl_label = "Find Variable"
bl_description = "Finds this variable in the addon"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
node_tree: bpy.props.StringProperty(options={"SKIP_SAVE", "HIDDEN"})
def execute(self, context):
return {"FINISHED"}
def draw(self, context):
layout = self.layout
ntree = bpy.data.node_groups[self.node_tree]
# init variable nodes
empty_nodes = []
variable_nodes = []
variable = None
if ntree.variable_index < len(ntree.variables):
variable = ntree.variables[ntree.variable_index]
# find variable nodes
for ngroup in bpy.data.node_groups:
if ngroup.bl_idname == "ScriptingNodesTree":
for node in ngroup.nodes:
if hasattr(node, "var_name") and hasattr(node, "ref_ntree"):
if variable and node.var_name == variable.name and node.ref_ntree == ntree:
variable_nodes.append(node)
elif not node.var_name or not node.ref_ntree:
empty_nodes.append(node)
# draw nodes for selected variable
if ntree.variable_index < len(ntree.variables):
col = layout.column()
row = col.row()
row.enabled = False
row.label(text=f"Variable: {variable.name}")
for node in variable_nodes:
op = col.operator("sn.find_node", text=node.name, icon="RESTRICT_SELECT_OFF")
op.node_tree = node.node_tree.name
op.node = node.name
if not variable_nodes:
col.label(text="No nodes found for this variable", icon="INFO")
# draw nodes with empty variable
col = layout.column()
row = col.row()
row.label(text="Empty Variable Nodes")
row.enabled = False
for node in empty_nodes:
op = col.operator("sn.find_node", text=node.name, icon="RESTRICT_SELECT_OFF")
op.node_tree = node.node_tree.name
op.node = node.name
if not empty_nodes:
col.label(text="No empty variable nodes found", icon="INFO")
def invoke(self, context, event):
return context.window_manager.invoke_popup(self, width=250)
@@ -0,0 +1,126 @@
import bpy
from ...utils import get_python_name, unique_collection_name
from ..properties.settings.settings import property_icons
from ...nodes.compiler import compile_addon
class SN_VariableProperties(bpy.types.PropertyGroup):
@property
def node_tree(self):
return self.id_data
# cache python names so they only have to be generated once
cached_python_names = {}
cached_python_name: bpy.props.StringProperty()
cached_human_name: bpy.props.StringProperty()
@property
def python_name(self):
if self.name == self.cached_human_name and self.cached_python_name: return self.cached_python_name
if self.name in self.cached_python_names: return self.cached_python_names[self.name]
names = []
for var in self.node_tree.variables:
if var == self:
break
names.append(var.python_name)
name = unique_collection_name(f"sna_{get_python_name(self.name, 'sna_new_variable')}", "sna_new_variable", names, "_")
try:
self.cached_python_name = name
self.cached_human_name = self.name
except AttributeError: pass
self.cached_python_names[self.name] = name
return name
@property
def data_path(self):
return f"{self.node_tree.python_name}['{self.python_name}']"
@property
def icon(self):
return property_icons[self.variable_type]
def compile(self, context=None):
""" Registers the variable and unregisters previous version """
# print(f"Serpens Log: Variable {self.name} received an update")
compile_addon()
def get_name(self):
return self.get("name", "Variable Default")
def get_to_update_nodes(self):
to_update_nodes = []
for ntree in bpy.data.node_groups:
if ntree.bl_idname == "ScriptingNodesTree":
for node in ntree.nodes:
if getattr(node, "var_name", None) == self.name:
to_update_nodes.append(node)
return to_update_nodes
def set_name(self, value):
names = list(map(lambda item: item.name, list(filter(lambda item: item!=self, self.node_tree.variables))))
value = unique_collection_name(value, "New Variable", names, " ")
to_update = self.get_to_update_nodes()
# set value
self["name"] = value
self.compile()
# update node references
for node in to_update:
node.var_name = value
name: bpy.props.StringProperty(name="Variable Name",
description="Name of this variable",
default="Variable Default",
get=get_name,
set=set_name,
update=compile)
def update_variable_type(self, context):
for node in self.get_to_update_nodes():
if hasattr(node, "on_var_changed"):
node.on_var_changed()
self.compile()
variable_type: bpy.props.EnumProperty(name="Type",
description="The type of data this variable stores",
update=update_variable_type,
items=[("Data", "Data", "Stores any type of data", property_icons["Data"], 0),
("String", "String", "Stores a string of characters", property_icons["String"], 1),
("Boolean", "Boolean", "Stores True or False", property_icons["Boolean"], 2),
("Float", "Float", "Stores a decimal number", property_icons["Float"], 3),
("Integer", "Integer", "Stores an integer number", property_icons["Integer"], 4),
("List", "List", "Stores a list of data", property_icons["List"], 5),
("Pointer", "Pointer", "Stores a reference to certain types of blend data, collection or group properties", property_icons["Pointer"], 6),
("Collection", "Collection", "Stores a list of certain blend data or property groups to be displayed in lists", property_icons["Collection"], 7)])
string_default: bpy.props.StringProperty(name="Default", description="Default value for the variable", update=compile)
boolean_default: bpy.props.BoolProperty(name="Default", description="Default value for the variable", update=compile)
float_default: bpy.props.FloatProperty(name="Default", description="Default value for the variable", update=compile)
integer_default: bpy.props.IntProperty(name="Default", description="Default value for the variable", update=compile)
@property
def var_default(self):
return {
"Data": None,
"String": f"'{self.string_default}'",
"Boolean": self.boolean_default,
"Float": self.float_default,
"Integer": self.integer_default,
"List": [],
"Pointer": None,
"Collection": None,
}[self.variable_type]