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