1
0

2 Commits b4ad5bfeff ... 17c6920b48

Autor SHA1 Mensagem Data
  Joseph Brandenburg 17c6920b48 Compatibility improvements for 5.0 há 10 meses atrás
  Joseph Brandenburg 5233a77c94 Custom Properties Nodes (for all xForms!) há 10 meses atrás

+ 8 - 3
__init__.py

@@ -17,8 +17,8 @@ from .ops_generate_tree import GenerateMantisTree
 from .utilities import prRed
 
 MANTIS_VERSION_MAJOR=0
-MANTIS_VERSION_MINOR=12
-MANTIS_VERSION_SUB=28
+MANTIS_VERSION_MINOR=13
+MANTIS_VERSION_SUB=0
 
 classLists = [module.TellClasses() for module in [
  link_nodes_ui,
@@ -120,6 +120,7 @@ xForm_category = [
         NodeItem("xFormCurvePin"),
     ]
 driver_category = [
+        NodeItem("UtilityCustomProperty"),
         NodeItem("LinkDrivenParameter"),
         NodeItem("UtilityFCurve"),
         NodeItem("UtilityBoneProperties"),
@@ -268,13 +269,17 @@ def execute_handler(scene):
 
 from .versioning import versioning_tasks
 def node_version_update(node, do_once):
+    from .xForm_nodes_ui import xFormNode
     for bl_idname, task, required_kwargs in versioning_tasks:
         arg_map = {}
         if 'node' in required_kwargs:
             arg_map['node']=node
         if 'node_tree' in required_kwargs:
             arg_map['node_tree']=node.id_data
-        if ('ALL' in bl_idname) or node.bl_idname in bl_idname:
+        # we'll match all, or categories, or specific bl_idname as needed.
+        if ('ALL' in bl_idname) or \
+                ('XFORM' in bl_idname) and isinstance(node, xFormNode) or\
+                node.bl_idname in bl_idname:
             if do_once:
                 print (f"Updating tree {node.id_data.name} to "
                        f"{MANTIS_VERSION_MAJOR}.{MANTIS_VERSION_MINOR}.{MANTIS_VERSION_SUB}")

+ 6 - 20
base_definitions.py

@@ -11,6 +11,7 @@ from .utilities import (prRed, prGreen, prPurple, prWhite,
                               wrapOrange,)
 
 from .utilities import get_socket_maps, relink_socket_map, do_relink
+from .mantis_dataclasses import MantisSocketTemplate
 
 FLOAT_EPSILON=0.0001 # used to check against floating point inaccuracy
 
@@ -203,24 +204,6 @@ class SchemaTree(NodeTree):
             fix_reroute_colors(self)
 
 
-from dataclasses import dataclass, field
-from typing import Any
-
-@dataclass
-class MantisSocketTemplate():
-    name             : str = field(default="")
-    bl_idname        : str = field(default="")
-    traverse_target  : str = field(default="")
-    identifier       : str = field(default="")
-    display_shape    : str = field(default="") # for arrays
-    category         : str = field(default="") # for use in display update
-    blender_property : str | tuple[str] = field(default="") # for props_sockets -> evaluate sockets
-    is_input         : bool = field(default=False)
-    hide             : bool = field(default=False)
-    use_multi_input  : bool = field(default=False)
-    default_value    : Any = field(default=None)
-    
-
 
 #TODO: do a better job explaining how MantisNode and MantisUINode relate.
 
@@ -279,7 +262,7 @@ class MantisUINode:
             socket.hide= template.hide
             if template.category:
                 # a custom property for the UI functions to use.
-                socket['category'] = template.category
+                socket.category = template.category
             if template.default_value is not None:
                 socket.default_value = template.default_value
                 # this can throw a TypeError - it is the caller's
@@ -643,7 +626,8 @@ replace_types = ["NodeGroupInput", "NodeGroupOutput", "SchemaIncomingConnection"
 
 # anything that gets properties added in the graph... this is a clumsy approach but I need to watch for this
 #   in schema generation and this is the easiest way to do it for now.
-custom_props_types = ["LinkArmature", "UtilityKeyframe", "UtilityFCurve", "UtilityDriver", "xFormBone"]
+custom_props_types = ["LinkArmature", "UtilityKeyframe", "UtilityFCurve", "UtilityDriver", "xFormBone",
+                      "xFormArmature", "xFormGeometryObject", "xFormObjectInstance", "xFormCurvePin",]
 
 # filters for determining if a link is a hierarchy link or a non-hierarchy (cyclic) link.
 from_name_filter = ["Driver",]
@@ -700,6 +684,8 @@ class MantisExecutionContext():
         self.execution_failed=False
         self.b_objects={} # objects created by Mantis during execution
 
+from typing import Any
+
 class MantisNode:
     """
         This class contains the basic interface for a Mantis Node.

+ 3 - 2
deformer_nodes.py

@@ -1,7 +1,8 @@
 from .node_common import *
 from .xForm_nodes import xFormGeometryObject, xFormObjectInstance
 from .misc_nodes import InputExistingGeometryObject
-from .base_definitions import MantisNode, MantisSocketTemplate
+from .base_definitions import MantisNode
+from .mantis_dataclasses import MantisSocketTemplate
 from .utilities import (prRed, prGreen, prPurple, prWhite, prOrange,
                         wrapRed, wrapGreen, wrapPurple, wrapWhite,
                         wrapOrange,)
@@ -514,7 +515,7 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
         self.node_type = "LINK"
         self.prepared = True
         self.executed = True
-        setup_custom_props(self)
+        setup_custom_property_inputs_outputs(self)
     
     # bpy.data.node_groups["Morph Deform.045"].nodes["Named Attribute.020"].data_type = 'FLOAT_VECTOR'
     # bpy.context.object.add_rest_position_attribute = True

+ 1 - 1
deformer_socket_templates.py

@@ -1,4 +1,4 @@
-from .base_definitions import MantisSocketTemplate as SockTemplate
+from .mantis_dataclasses import MantisSocketTemplate as SockTemplate
 from dataclasses import replace
 
 from .misc_nodes_socket_templates import SplineIndexTemplate

+ 3 - 0
drivers.py

@@ -48,6 +48,7 @@ class MantisDriver(dict):
     pass
 
 def CreateDrivers(drivers):
+    from bpy.app import version
     def brackets(s):
         return "[\""+s+"\"]"
     from bpy.types import Object, Key
@@ -65,6 +66,8 @@ def CreateDrivers(drivers):
             fc.modifiers.remove(fc.modifiers[0])
         except IndexError: #haven't seen this happen, but should handle
             pass # perhaps this has been fixed for 3.0?
+        if version >= (5,0,0): # and now it initializes with keys. p
+            fc.keyframe_points.clear()
         drv.type = driver["type"]
         if (expr := driver.get("expression")) and isinstance(expr, str):
             drv.expression = expr

+ 1 - 1
link_nodes.py

@@ -785,7 +785,7 @@ class LinkArmature(MantisLinkNode):
         super().__init__(signature, base_tree, LinkArmatureSockets)
         self.init_parameters(additional_parameters={"Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
-        setup_custom_props(self) # <-- this takes care of the runtime-added sockets
+        setup_custom_property_inputs_outputs(self) # <-- this takes care of the runtime-added sockets
 
     def bRelationshipPass(self, bContext = None,):
         prepare_parameters(self)

+ 31 - 0
mantis_dataclasses.py

@@ -1,4 +1,20 @@
 from dataclasses import dataclass, field
+from typing import List, Any
+
+
+@dataclass
+class MantisSocketTemplate():
+    name             : str = field(default="")
+    bl_idname        : str = field(default="")
+    traverse_target  : str = field(default="")
+    identifier       : str = field(default="")
+    display_shape    : str = field(default="") # for arrays
+    category         : str = field(default="") # for use in display update
+    blender_property : str | tuple[str] = field(default="") # for props_sockets -> evaluate sockets
+    is_input         : bool = field(default=False)
+    hide             : bool = field(default=False)
+    use_multi_input  : bool = field(default=False)
+    default_value    : Any = field(default=None)
 
 @dataclass
 class xForm_info():
@@ -9,3 +25,18 @@ class xForm_info():
     self_pose_name       :  str = field(default="")
     self_edit_name      :  str = field(default="")
 # should I add node signatures to this?
+
+@dataclass
+class custom_prop_template():
+    name      : str = field(default="")
+    prop_type : str = field(default="")
+    default_value_string : str = field(default="")
+    default_value_bool   : bool = field(default=False)
+    default_value_int    : int = field(default=0)
+    default_value_float  : float = field(default=0.0)
+    default_value_vector : List[float] = field(default_factory=list)
+    min : float|int = field(default=0)
+    max : float|int = field(default=1)
+    soft_min : float|int = field(default=1)
+    soft_max : float|int = field(default=1)
+    description : str = field(default="")

+ 53 - 6
misc_nodes.py

@@ -21,6 +21,7 @@ def TellClasses():
              InputExistingGeometryData,
              InputThemeBoneColorSets,
              InputColorSetPallete,
+             UtilityCustomProperty,
              UtilityDeclareCollections,
              UtilityCollectionJoin,
              UtilityCollectionHierarchy,
@@ -297,7 +298,7 @@ class InputThemeBoneColorSets(SimpleInputNode):
 
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree)
-        from .base_definitions import MantisSocketTemplate
+        from .mantis_dataclasses import MantisSocketTemplate
         outputs = []
         for i in range(20):
             outputs.append (MantisSocketTemplate(
@@ -324,7 +325,7 @@ class InputColorSetPallete(SimpleInputNode):
         if not ui_node:
             from .utilities import get_ui_node
             ui_node = get_ui_node(self.ui_signature, self.base_tree)
-        from .base_definitions import MantisSocketTemplate
+        from .mantis_dataclasses import MantisSocketTemplate
         outputs = []
         for o in ui_node.outputs:
             outputs.append (MantisSocketTemplate( name = o.name,))
@@ -334,6 +335,53 @@ class InputColorSetPallete(SimpleInputNode):
             self.parameters[o.name] = o.default_value
         return super().fill_parameters(ui_node)
 
+
+class UtilityCustomProperty(MantisNode):
+    def __init__(self, signature, base_tree):
+        super().__init__(signature, base_tree, UtilityCustomPropertySockets)
+        self.init_parameters()
+        self.node_type = "UTILITY"
+    # this should output a dataclass containing custom prop info
+    # then the xForm will use it to assign custom properties
+
+    def bPrepare(self, bContext=None):
+        from .mantis_dataclasses import custom_prop_template
+        # it's kind of silly to do it this way, but whatever 
+        name = self.evaluate_input("Name")
+        assert name, "The custom property must have a name."
+        prop_type = self.evaluate_input("Type")
+        kwargs = {
+            "name"        : name,
+            "prop_type"   : prop_type,
+            "description" : self.evaluate_input("Description"),
+        }
+        match prop_type:
+            case 'BOOL':
+                kwargs['default_value_bool'] = self.evaluate_input("Default (Bool)")
+            case 'INT':
+                kwargs['default_value_int'] = self.evaluate_input("Default (Int)")
+                kwargs['min'] = self.evaluate_input("Min (Int)")
+                kwargs['soft_min'] = self.evaluate_input("Soft Min (Int)")
+                kwargs['max'] = self.evaluate_input("Max (Int)")
+                kwargs['soft_max'] = self.evaluate_input("Soft Max (Int)")
+            case 'FLOAT':
+                kwargs['default_value_float'] = self.evaluate_input("Default (Float)")
+                kwargs['min'] = self.evaluate_input("Min (Float)")
+                kwargs['soft_min'] = self.evaluate_input("Soft Min (Float)")
+                kwargs['max'] = self.evaluate_input("Max (Float)")
+                kwargs['soft_max'] = self.evaluate_input("Soft Max (Float)")
+            case 'VECTOR':
+                kwargs['default_value_vector'] = self.evaluate_input("Default (Vector)")
+                kwargs['min'] = self.evaluate_input("Min (Float)")
+                kwargs['soft_min'] = self.evaluate_input("Soft Min (Float)")
+                kwargs['max'] = self.evaluate_input("Max (Float)")
+                kwargs['soft_max'] = self.evaluate_input("Soft Max (Float)")
+            case 'STRING':
+                kwargs['default_value_string'] = self.evaluate_input("Default (String)")
+        my_data = custom_prop_template( **kwargs)
+        self.parameters['Custom Property'] = my_data
+        self.prepared = True; self.executed = True
+
 class UtilityMatrixFromCurve(MantisNode):
     '''Get a matrix from a curve'''
 
@@ -930,7 +978,7 @@ class UtilityKeyframe(MantisNode):
         self.outputs.init_sockets(outputs)
         self.init_parameters( additional_parameters=additional_parameters)
         self.node_type = "DRIVER" # MUST be run in Pose mode
-        setup_custom_props(self)
+        setup_custom_property_inputs_outputs(self)
 
     def bPrepare(self, bContext = None,):
         key = self.parameters["Keyframe"]
@@ -958,7 +1006,7 @@ class UtilityFCurve(MantisNode):
         self.outputs.init_sockets(outputs)
         self.init_parameters()
         self.node_type = "UTILITY"
-        setup_custom_props(self)
+        setup_custom_property_inputs_outputs(self)
         self.prepared = True
 
     def reset_execution(self):
@@ -1009,7 +1057,7 @@ class UtilityDriver(MantisNode):
         self.outputs.init_sockets(outputs)
         self.init_parameters(additional_parameters=additional_parameters)
         self.node_type = "DRIVER" # MUST be run in Pose mode
-        setup_custom_props(self)
+        setup_custom_property_inputs_outputs(self)
         self.prepared = True
 
     def reset_execution(self):
@@ -1419,7 +1467,6 @@ class UtilityDeclareCollections(MantisNode):
         if ui_node is None:
             from .utilities import get_ui_node
             ui_node = get_ui_node(self.ui_signature, self.base_tree)
-        from .base_definitions import MantisSocketTemplate as SockTemplate
         templates=[]
         for out in ui_node.outputs:
             if not (out.name in self.outputs.keys()) :

+ 48 - 1
misc_nodes_socket_templates.py

@@ -1,4 +1,4 @@
-from .base_definitions import MantisSocketTemplate as SockTemplate
+from .mantis_dataclasses import MantisSocketTemplate as SockTemplate
 from dataclasses import replace
 
 SplineIndexTemplate = SockTemplate(name="Spline Index",
@@ -129,3 +129,50 @@ CollectionHierarchySockets = [
     CollectionDeclarationOutput,
 ]
 
+
+UtilityCustomPropertySockets = [
+    Name := SockTemplate(name='Name', is_input=True,
+        bl_idname="StringSocket", category='general'),
+    
+    CustomPropType := SockTemplate(name='Type', is_input=True,
+        bl_idname="EnumCustomPropTypeSocket", category='general'),
+    # Subtype - add this later
+
+    # float properties
+    DefaultFloatValue := SockTemplate(name='Default (Float)', is_input=True,
+        bl_idname="FloatSocket",  category='float'),
+    Min := SockTemplate(name='Min (Float)', is_input=True,
+        bl_idname="FloatSocket", category='float'),
+    Max := SockTemplate(name='Max (Float)', is_input=True,
+        bl_idname="FloatSocket", category='float'),
+    SoftMin := SockTemplate(name='Soft Min (Float)', is_input=True,
+        bl_idname="FloatSocket", category='float'),
+    SoftMax := SockTemplate(name='Soft Max (Float)', is_input=True,
+        bl_idname="FloatSocket", category='float'),
+    
+    # bool properties
+    DefaultVectorValue := SockTemplate(name='Default (Bool)', is_input=True,
+        bl_idname="BooleanSocket", hide=True, category='bool'),
+    # int properties
+    DefaultIntValue := SockTemplate(name='Default (Int)', is_input=True,
+        bl_idname="IntSocket", hide=True, category='int'),
+    IntMin := SockTemplate(name='Min (Int)', is_input=True,
+        bl_idname="IntSocket", hide=True, category='int'),
+    IntMax := SockTemplate(name='Max (Int)', is_input=True,
+        bl_idname="IntSocket", hide=True, category='int'),
+    IntSoftMin := SockTemplate(name='Soft Min (Int)', is_input=True,
+        bl_idname="IntSocket", hide=True, category='int'),
+    IntSoftMax := SockTemplate(name='Soft Max (Int)', is_input=True,
+        bl_idname="IntSocket", hide=True, category='int'),
+    # vector properties
+    DefaultVectorValue := SockTemplate(name='Default (Vector)', is_input=True,
+        bl_idname="VectorSocket", hide=True, category='vector'),
+    # String Properties
+    DefaultStringValue := SockTemplate(name='Default (String)', is_input=True,
+        bl_idname="StringSocket", hide=True, category='string'),
+    # general:
+    Description := SockTemplate(name='Description', is_input=True,
+        bl_idname="StringSocket", category='general'),
+    PropertyDeclaration := SockTemplate(name='Custom Property',
+        is_input=False, bl_idname="CustomPropSocket", category='general'),
+]

+ 43 - 0
misc_nodes_ui.py

@@ -24,6 +24,7 @@ def TellClasses():
              InputExistingGeometryDataNode,
              InputThemeBoneColorSets,
              InputColorSetPallete,
+             UtilityCustomProperty,
              UtilityDeclareCollections,
              UtilityCollectionJoin,
              UtilityCollectionHierarchy,
@@ -808,6 +809,48 @@ def socket_data_from_collection_paths(root_data, root_name, path, socket_data):
         path.pop()
     return socket_data
 
+class UtilityCustomProperty(Node, MantisUINode):
+    """A declaration of a custom property."""
+    bl_idname = "UtilityCustomProperty"
+    bl_label  = "Custom Property"
+    bl_icon   = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
+    mantis_node_class_name=bl_idname
+    def init(self, context):
+        self.init_sockets(UtilityCustomPropertySockets)
+        self.initialized = True
+    
+    def display_update(self, parsed_tree, context):
+        mantis_node = parsed_tree.get(get_signature_from_edited_tree(self, context))
+        prop_type = self.inputs['Type'].default_value
+        if mantis_node: # this is done here so I don't have to define yet another custom socket.
+            prop_type = mantis_node.evaluate_input("Type")
+        visible_categories = ['general']
+        match prop_type:
+            case 'STRING': visible_categories.append('string')
+            case 'BOOL': visible_categories.append('bool')
+            case 'INT': visible_categories.append('int')
+            case 'FLOAT': visible_categories.append('float')
+            case 'VECTOR': visible_categories.append('vector')
+        for i, socket_template in enumerate(UtilityCustomPropertySockets):
+            if socket_template.is_input == False: continue
+            if socket_template.category in visible_categories:
+                self.inputs[i].hide = False
+            else:
+                self.inputs[i].hide = True
+        if prop_type == "VECTOR":
+            for i in range (4): # keep the min/max open
+                self.inputs[i+3].hide=False
+    
+
+    def draw_label(self): # this will prefer a user-set label, or return the evaluated name
+        if self.label:
+            return self.label
+        if self.inputs['Name'].display_text:
+            return self.inputs['Name'].display_text
+        return self.name
+
+
 class UtilityDeclareCollections(Node, MantisUINode):
     """A utility used to declare bone collections."""
     bl_idname = "UtilityDeclareCollections"

+ 2 - 2
node_common.py

@@ -116,13 +116,13 @@ def get_parent_xForm_info(mantis_node, socket_name = 'Relationship'):
     if parent_xForm_info is None: parent_xForm_info = xForm_info() # default to empty
     return parent_xForm_info
 
-def setup_custom_props(mantis_node):
+def setup_custom_property_inputs_outputs(mantis_node):
     from .utilities import get_ui_node
     if mantis_node.signature[0] == 'SCHEMA_AUTOGENERATED':
         from .base_definitions import custom_props_types
         if mantis_node.__class__.__name__ not in custom_props_types:
             # prRed(f"Reminder: figure out how to deal with custom property setting for Schema Node {mantis_node}")
-            raise RuntimeError(wrapRed(f"Custom Properties not set up for node {mantis_node}"))
+            raise RuntimeError(wrapRed(f"Custom Properties not allowed for class {mantis_node.__class__.__name__}"))
         return
     else:
         ui_node = get_ui_node(mantis_node.signature, mantis_node.base_tree)

+ 29 - 0
ops_nodegroup.py

@@ -1046,6 +1046,33 @@ class ImportFromComponentLibrary(Operator):
     def execute(self, context):
         return {"FINISHED"}
 
+from os import environ
+if environ.get("DOERROR"): # this should only be available to devs
+    class TestMantisVersioning(Operator):
+        """Utility Operator for testing Mantis Versioning"""
+        bl_idname = "mantis.test_versioning"
+        bl_label = "Test Mantis Versioning"
+        bl_options = {'REGISTER', 'UNDO'}
+
+        tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
+        node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
+
+        @classmethod
+        def poll(cls, context):
+            return (mantis_tree_poll_op(context))
+        
+        def invoke(self, context, event):
+            self.tree_invoked = bpy.context.space_data.path[-1].node_tree.name
+            self.node_invoked = bpy.context.space_data.path[-1].node_tree.nodes.active.name
+            return self.execute(context)
+
+        def execute(self, context):
+            tree = bpy.data.node_groups[self.tree_invoked]
+            node = tree.nodes[self.node_invoked]
+            from .versioning import up_0_13_0_new_custom_prop_node
+            print ("testing...")
+            up_0_13_0_new_custom_prop_node(node=node, tree=tree)
+            return {"FINISHED"}
 
 # this has to be down here for some reason. what a pain
 classes = [
@@ -1085,6 +1112,8 @@ classes = [
         ]
 if (bpy.app.version >= (4, 4, 0)):
     classes.append(B4_4_0_Workaround_NodeTree_Interface_Update)
+if environ.get("DOERROR"):
+    classes.append(TestMantisVersioning)
 
 def TellClasses():
     return classes

+ 1 - 1
primitives_sockets.py

@@ -1,4 +1,4 @@
-from .base_definitions import MantisSocketTemplate as SockTemplate
+from .mantis_dataclasses import MantisSocketTemplate as SockTemplate
 from dataclasses import replace
 
 

+ 57 - 2
socket_definitions.py

@@ -10,6 +10,8 @@ no_default_value= [
             'xFormSocket',
             'GeometrySocket',
             'GenericRotationSocket',
+            'EnumCustomPropTypeSocket', 
+            'CustomPropSocket',
             'FCurveSocket',
             'DriverSocket',
             'DriverVariableSocket',
@@ -25,12 +27,15 @@ if bpy_version == (4,5,0): # THere is a bug that requires the socket type to inh
     from bpy.types import NodeSocketGeometry # so we will just inherit from NodeSocketGeometry
     class MantisSocket(NodeSocketGeometry, NodeSocket): # even though that is kinda silly
         is_valid_interface_type=False
+        category : bpy.props.StringProperty()
         @property # making this a classmethod is apparently not gonna work
         def interface_type(self):
             return NodeSocketGeometry.bl_idname
+        
 else:
     class MantisSocket(NodeSocket):
         is_valid_interface_type=False
+        category : bpy.props.StringProperty()
         @property
         def interface_type(self):
             # this is stupid but it is the fastest way to implement this
@@ -185,6 +190,8 @@ def TellClasses() -> List[MantisSocket]:
              BoneCollectionSocket,
              EnumArrayGetOptions,
 
+             EnumCustomPropTypeSocket,
+             CustomPropSocket,
              xFormParameterSocket,
              ParameterBoolSocket,
              ParameterIntSocket,
@@ -1131,11 +1138,60 @@ class EnumArrayGetOptions(MantisSocket):
 #####################################################################################
 
 
+eCustomPropType = (
+        ('BOOL'   , "Boolean", "Boolean", 0),
+        ('INT'    , "Int"    , "Integer", 1),
+        ('FLOAT'  , "Float"  , "Floating Point Number", 2),
+        ('VECTOR' , "Vector" , "Vector", 3),
+        ('STRING' , "String" , "String", 4),
+    )
+
+class EnumCustomPropTypeSocket(MantisSocket):
+    """Custom Property Type"""
+    bl_idname = 'EnumCustomPropTypeSocket'
+    bl_label = "Property Type"
+    color_simple = cString
+
+    default_value: bpy.props.EnumProperty(
+        items=eCustomPropType,
+        description="Custom Property Type",
+        default = 'BOOL',
+        update = update_socket,)
+
+    color : bpy.props.FloatVectorProperty(default=cString, size=4)
+    input : bpy.props.BoolProperty(default =False,)
+
+    def draw(self, context, layout, node, text):
+        ChooseDraw(self, context, layout, node, text)
+    def draw_color(self, context, node):
+        return self.color
+    @classmethod
+    def draw_color_simple(self):
+        return self.color_simple
+# what is this one again?
+
+class CustomPropSocket(MantisSocket):
+    '''Custom Property'''
+    bl_idname = 'CustomPropSocket'
+    bl_label = "Custom Property"
+    color_simple = cString
+    color : bpy.props.FloatVectorProperty(default=cString, size=4)
+    input : bpy.props.BoolProperty(default =False,)
+
+    def init(self):
+        self.display_shape = 'CIRCLE_DOT'
+    def draw(self, context, layout, node, text):
+        ChooseDraw(self, context, layout, node, text)
+    def draw_color(self, context, node):
+        return self.color
+    @classmethod
+    def draw_color_simple(self):
+        return self.color_simple
 
 class xFormParameterSocket(MantisSocket):
     '''xFrom Parameter'''
     bl_idname = 'xFormParameterSocket'
-    bl_label = "sForm Parameter"
+    bl_label = "xForm Parameter"
     color_simple = cxForm
     color : bpy.props.FloatVectorProperty(default=cxForm, size=4)
     input : bpy.props.BoolProperty(default =False,)
@@ -2487,7 +2543,6 @@ class EnumDriverType(MantisSocket):
 # enum for interpolation type
 # eventually gonna make it to the fancy stuff
 
-
 class FloatSocket(MantisSocket):
     """Float Input socket"""
     bl_idname = 'FloatSocket'

+ 2 - 2
utilities.py

@@ -145,8 +145,8 @@ def get_socket_maps(node, force=False):
                     keep_sockets.append(other_socket)
                 map[sock.identifier]= keep_sockets
             elif hasattr(sock, "default_value"):
-                if sock.get("default_value") is not None:
-                    val = sock['default_value']
+                if getattr(sock, "default_value") is not None:
+                    val = getattr(sock, "default_value")
                 elif sock.bl_idname == "EnumCurveSocket" and sock.get("default_value") is None:
                     # HACK I need to add this special case because during file-load,
                     #  this value is None and should not be altered until it is set once.

+ 65 - 1
versioning.py

@@ -220,7 +220,6 @@ def cleanup_4_5_0_LTS_interface_workaround(*args, **kwargs):
             interface_item.description = ''
     # that should be enough!
 
-
 def up_0_12_25_replace_floor_offset_type(*args, **kwargs):
     # add an inherit color input.
     node = kwargs['node']
@@ -243,7 +242,71 @@ def up_0_12_25_replace_floor_offset_type(*args, **kwargs):
         prRed(f"Error updating version in node: {node.id_data.name}::{node.name}; see error:")
         print(e)
 
+def get_absolute_location(node): # us9ng this for version compatibility
+    location = node.location.copy() # FUTURE: deprecate old blenders and use absolute_location
+    while(node.parent):
+        node = node.parent
+        location+= node.location
+    return location
+    
 
+def up_0_13_0_new_custom_prop_node(*args, **kwargs):
+    tree, node = kwargs['node_tree'], kwargs['node']
+    current_major_version = tree.mantis_version[0]
+    if  current_major_version > 0: return# major version must be 0
+    current_minor_version = tree.mantis_version[1]
+    if current_minor_version >= 13: return# minor version must be 12 or less
+    remove_me, prop_nodes = [], []
+    from .xForm_nodes_ui import xFormBoneNode
+    if isinstance(node, xFormBoneNode):
+        for custom_input in node.inputs:
+            if 'Parameter' not in custom_input.bl_idname:
+                continue
+            custom_prop_node = tree.nodes.new('UtilityCustomProperty')
+            prop_type = None
+            if custom_input.bl_idname == 'ParameterBoolSocket':
+                prop_type = 'BOOL'
+                custom_prop_node.inputs['Default (Bool)'].default_value = getattr(custom_input, "default_value")
+            elif custom_input.bl_idname == 'ParameterIntSocket':
+                prop_type = 'INT'
+                custom_prop_node.inputs['Default (Int)'].default_value = int( getattr(custom_input, "default_value") )
+                custom_prop_node.inputs['Min (Int)'].default_value = int( getattr(custom_input, "min") )
+                custom_prop_node.inputs['Max (Int)'].default_value = int( getattr(custom_input, "max") )
+                custom_prop_node.inputs['Soft Min (Int)'].default_value = int( getattr(custom_input, "soft_min") )
+                custom_prop_node.inputs['Soft Max (Int)'].default_value = int( getattr(custom_input, "soft_max") )
+            elif custom_input.bl_idname == 'ParameterFloatSocket':
+                prop_type = 'FLOAT'
+                custom_prop_node.inputs['Default (Float)'].default_value = getattr(custom_input, "default_value")
+                custom_prop_node.inputs['Min (Float)'].default_value = getattr(custom_input, "min")
+                custom_prop_node.inputs['Max (Float)'].default_value = getattr(custom_input, "max")
+                custom_prop_node.inputs['Soft Min (Float)'].default_value = getattr(custom_input, "soft_min")
+                custom_prop_node.inputs['Soft Max (Float)'].default_value = getattr(custom_input, "soft_max")
+            elif custom_input.bl_idname == 'ParameterVectorSocket':
+                prop_type = 'VECTOR'
+                custom_prop_node.inputs['Default (Vector)'].default_value = getattr(custom_input, "default_value")
+                custom_prop_node.inputs['Min (Float)'].default_value = getattr(custom_input, "min")
+                custom_prop_node.inputs['Max (Float)'].default_value = getattr(custom_input, "max")
+                custom_prop_node.inputs['Soft Min (Float)'].default_value = getattr(custom_input, "soft_min")
+                custom_prop_node.inputs['Soft Max (Float)'].default_value = getattr(custom_input, "soft_max")
+            elif custom_input.bl_idname == 'ParameterStringSocket':
+                prop_type = 'STRING'
+                custom_prop_node.inputs['Default (String)'].default_value = getattr(custom_input, "default_value")
+            custom_prop_node.inputs['Name'].default_value = custom_input.name
+            custom_prop_node.inputs['Type'].default_value = prop_type
+            prop_nodes.append(custom_prop_node)
+            remove_me.append(custom_input)
+        while remove_me:
+            custom_input = remove_me.pop()
+            node.inputs.remove(custom_input)
+        new_input = node.inputs.new("CustomPropSocket", "Custom Properties", use_multi_input=True)
+        from mathutils import Vector
+        for i, prop_node in enumerate(prop_nodes):
+            tree.links.new(input=prop_node.outputs[0], output=new_input)
+            prop_node.location = get_absolute_location(node)
+            prop_node.location = prop_node.location - Vector((180, -260*i))
+            prop_node.select = False # it is annoying to select them all by default
+    else: # just need to add the socket.
+        new_input = node.inputs.new("CustomPropSocket", "Custom Properties", use_multi_input=True)
 
 versioning_tasks = [
     # node bl_idname    task                required keyword arguments
@@ -253,6 +316,7 @@ versioning_tasks = [
     (['MantisTree', 'SchemaTree'], cleanup_4_5_0_LTS_interface_workaround, ['tree']),
     (['InputWidget'], up_0_12_13_add_widget_scale, ['node']),
     (['LinkFloor'], up_0_12_25_replace_floor_offset_type, ['node']),
+    (['XFORM'], up_0_13_0_new_custom_prop_node, ['node', 'node_tree']),
 ]
 
 

+ 69 - 71
xForm_nodes.py

@@ -20,6 +20,7 @@ def TellClasses():
 
 def reset_object_data(ob):
     # moving this to a common function so I can figure out the details later
+    ob.id_properties_clear() # we must remove the custom properties so we can make them again.
     ob.constraints.clear()
     ob.animation_data_clear() # this is a little dangerous. TODO find a better solution since this can wipe animation the user wants to keep
     ob.modifiers.clear() # I would also like a way to copy modifiers and their settings, or bake them down. oh well
@@ -75,6 +76,52 @@ class xFormNode(MantisNode):
     def reset_execution(self):
         super().reset_execution()
         self.prepared=False
+    
+    def setup_custom_props(self, ob, input):
+        from .mantis_dataclasses import custom_prop_template
+        # detect custom inputs
+        for i, l in enumerate(self.inputs[input].links):
+            # we need to trace back all inputs
+            custom_prop_data = self.evaluate_input(input, i)
+            if not isinstance(custom_prop_data, custom_prop_template): continue
+            from .drivers import MantisDriver
+            name = custom_prop_data.name
+            if name in ob.keys():
+                raise RuntimeError("Error: cannot create a custom property with "
+                                   "the same name as an existing property.")
+            prop_type = custom_prop_data.prop_type
+            match prop_type:
+                case 'BOOL':
+                    value = custom_prop_data.default_value_bool
+                case 'INT':
+                    value = custom_prop_data.default_value_int
+                case 'FLOAT':
+                    value = custom_prop_data.default_value_float
+                case 'VECTOR':
+                    value = custom_prop_data.default_value_vector
+                case 'STRING':
+                    value = custom_prop_data.default_value_string
+            if (value is None):
+                raise RuntimeError("Could not set value of custom parameter")
+                # it creates a more confusing error later sometimes, better to catch it here.
+            ob[name] = value
+            ui_data = ob.id_properties_ui(name)
+            ui_data.update(
+                default=value,
+                description=custom_prop_data.description)
+            #if a number, set the min/max values. it may overflow on the C side
+            if type(value) in [float, int]:
+                for prop_name in ['min', 'max', 'soft_min', 'soft_max', ]:
+                    prop_value = getattr(custom_prop_data, prop_name)
+                    if type(value) == int: prop_value = int(prop_value)
+                    # DO: figure out the right way to prevent an oveflow
+                    try: # we have to do this as a keyword argument like this
+                        ui_data.update( **{prop_name:prop_value} )
+                    except OverflowError: #this occurs when the value is inf
+                        prRed(f"invalid value {prop_value} for custom prop {prop_name}"
+                                f" of type {type(value)} in {self}. It will remain unset.")
+            ob.property_overridable_library_set(f"[\"{name}\"]", True)
+            pass
 
 class xFormArmature(xFormNode):
     '''A node representing an armature object'''
@@ -83,6 +130,7 @@ class xFormArmature(xFormNode):
         super().__init__(signature, base_tree, xFormArmatureSockets)
         self.init_parameters()
         self.set_traverse([("Relationship", "xForm Out")])
+        setup_custom_property_inputs_outputs(self)
 
     def bPrepare(self, bContext=None):
         self.parameters['Matrix'] = get_matrix(self)
@@ -128,6 +176,9 @@ class xFormArmature(xFormNode):
             ob = bpy.data.objects.new(name, bpy.data.armatures.new(name)) #create ob
             if (ob.name != name):
                 raise RuntimeError("Could not create xForm object", name)
+        
+        # it seems this data sticks around even if the object was deleted, so clear it here.
+        ob.id_properties_clear()
 
         self.bObject = ob.name
         ob.matrix_world = matrix.copy()
@@ -176,6 +227,11 @@ class xFormArmature(xFormNode):
         self.parameters['xForm Out'] = my_info
 
         self.executed = True
+    
+    def bFinalize(self, bContext = None):
+        # custom properties
+        self.setup_custom_props(self.bGetObject(), "Custom Properties")
+        finish_drivers(self)
 
     def bGetObject(self, mode = ''):
         import bpy; return bpy.data.objects[self.parameters['xForm Out'].self_pose_name]
@@ -244,6 +300,7 @@ bone_inputs= [
          "Lock Location",
          "Lock Rotation",
          "Lock Scale",
+         "Custom Properties",
 ]
 class xFormBone(xFormNode):
     '''A node representing a bone in an armature'''
@@ -260,6 +317,7 @@ class xFormBone(xFormNode):
         # currently it is waiting on BBone and refactoring/cleanup.
         self.init_parameters()
         self.set_traverse([("Relationship", "xForm Out")])
+        setup_custom_property_inputs_outputs(self)
 
     def bGetParentArmature(self):
         parent_xForm_info = get_parent_xForm_info(self)
@@ -462,18 +520,6 @@ class xFormBone(xFormNode):
 
 
         import bpy
-        from .drivers import MantisDriver
-        # prevAct = bContext.view_layer.objects.active
-        # bContext.view_layer.objects.active = ob
-        # bpy.ops.object.mode_set(mode='OBJECT')
-        # bContext.view_layer.objects.active = prevAct
-        #
-        #get relationship
-        # ensure we have a pose bone...
-        # set the ik parameters
-        #
-        #
-        # Don't need to bother about whatever that was
 
         pb = self.bGetParentArmature().pose.bones[self.bObject]
         rotation_mode = self.evaluate_input("Rotation Order")
@@ -486,58 +532,8 @@ class xFormBone(xFormNode):
         driver = None
         do_prints=False
 
-        # detect custom inputs
-        for i, inp in enumerate(self.inputs.values()):
-            custom_prop=False
-            for s_template in self.socket_templates:
-                if s_template.name == inp.name:
-                    break
-            else:
-                custom_prop=True
-            if custom_prop == False: continue
-            name = inp.name
-            value = self.evaluate_input(inp.name)
-            # This may be driven, so let's do this:
-            if (isinstance(value, tuple)):
-                raise RuntimeError(f"The custom property type is not supported: {self}")
-            if (isinstance(value, MantisDriver)):
-                # the value should be the default for its socket...
-                type_val_map = {
-                    str:"",
-                    bool:False,
-                    int:0,
-                    float:0.0,
-                    bpy.types.bpy_prop_array:(0,0,0),
-                    }
-                driver = value
-                value = type_val_map[type(self.parameters[inp.name])]
-            if (value is None):
-                raise RuntimeError("Could not set value of custom parameter")
-                # it creates a more confusing error later sometimes, better to catch it here.
-
-            pb[name] = value
-            ui_data = pb.id_properties_ui(name)
-            description = ''
-            if hasattr(inp, 'description'):
-                description = inp.description
-                # i am assuming there was a reason I was not already taking inp.description
-                # So guard it a little here just to be safe and not change things too much.
-            ui_data.update(
-                default=value,
-                description=description)
-
-            #if a number, set the min/max values. it may overflow on the C side
-            if type(value) in [float, int]:
-                for prop_name in ['min', 'max', 'soft_min', 'soft_max', ]:
-                    prop_value = getattr(inp, prop_name)
-                    if type(value) == int: prop_value = int(prop_value)
-                    # DO: figure out the right way to prevent an oveflow
-                    try: # we have to do this as a keyword argument like this
-                        ui_data.update( **{prop_name:prop_value} )
-                    except OverflowError: #this occurs when the value is inf
-                        prRed(f"invalid value {prop_value} for custom prop {prop_name}"
-                             f" of type {type(value)} in {self}. It will remain unset.")
-            pb.property_overridable_library_set(f"[\"{name}\"]", True)
+        # custom properties
+        self.setup_custom_props(pb, "Custom Properties")
 
         if (pb.is_in_ik_chain):
             # this  props_socket thing wasn't really meant to work here but it does, neat
@@ -654,13 +650,6 @@ class xFormBone(xFormNode):
             prRed ("Cannot get bone for %s" % self)
             raise e
 
-    def fill_parameters(self, ui_node=None):
-        # this is the fill_parameters that is run if it isn't a schema
-        setup_custom_props(self)
-        super().fill_parameters(ui_node)
-        # otherwise we will do this from the schema
-        # LEGIBILITY TODO - why? explain this?
-
 class xFormGeometryObject(xFormNode):
     '''A node representing an armature object'''
     def __init__(self, signature, base_tree):
@@ -668,6 +657,7 @@ class xFormGeometryObject(xFormNode):
         self.init_parameters()
         self.set_traverse([("Relationship", "xForm Out")])
         self.has_shape_keys = False
+        setup_custom_property_inputs_outputs(self)
 
     def bPrepare(self, bContext = None,):
         import bpy
@@ -763,6 +753,8 @@ class xFormGeometryObject(xFormNode):
         matrix= get_matrix(self)
         set_object_parent(self)
         self.bObject.matrix_world = matrix
+        # custom properties
+        self.setup_custom_props(self.bGetObject(), "Custom Properties")
         for i, (driver_key, driver_item) in enumerate(self.drivers.items()):
             print (wrapGreen(i), wrapWhite(self), wrapPurple(driver_key))
             prOrange(driver_item)
@@ -781,6 +773,7 @@ class xFormObjectInstance(xFormNode):
         self.links = {} # leave this empty for now!
         self.set_traverse([("Relationship", "xForm Out")])
         self.has_shape_keys = False # Shape Keys will make a dupe so this is OK
+        setup_custom_property_inputs_outputs(self)
 
     def ui_modify_socket(self, ui_socket, socket_name=None):
         if ui_socket.name == 'As Instance':
@@ -860,6 +853,8 @@ class xFormObjectInstance(xFormNode):
         modifier.node_group = ng
         modifier["Socket_0"] = source_ob
         modifier["Socket_1"] = self.evaluate_input("As Instance")
+        # custom properties
+        self.setup_custom_props(self.bGetObject(), "Custom Properties")
         for i, (driver_key, driver_item) in enumerate(self.drivers.items()):
             print (wrapGreen(i), wrapWhite(self), wrapPurple(driver_key))
             prOrange(driver_item)
@@ -873,6 +868,7 @@ class xFormCurvePin(xFormNode):
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree,xFormCurvePinSockets)
         self.init_parameters(additional_parameters={"Matrix":None})
+        setup_custom_property_inputs_outputs(self)
 
     def prep_driver_values(self, constraint):
         from .misc_nodes import UtilityDriver, UtilitySwitch
@@ -992,6 +988,8 @@ class xFormCurvePin(xFormNode):
         self.prepared = True; self.executed = True
 
     def bFinalize(self, bContext = None):
+        # custom properties
+        self.setup_custom_props(self.bGetObject(), "Custom Properties")
         finish_drivers(self)
 
     def bGetObject(self, mode = 'POSE'):

+ 87 - 28
xForm_nodes_ui.py

@@ -66,6 +66,71 @@ def main_draw_label(self): # this will prefer a user-set label, or return the ev
     return self.name
 
 
+def custom_prop_display_update(self, mantis_node = None):
+    custom_props = {}
+    type_map = {
+        'BOOL'   : 'ParameterBoolSocket',
+        'INT'    : 'ParameterIntSocket',
+        'FLOAT'  : 'ParameterFloatSocket',
+        'VECTOR' : 'ParameterVectorSocket',
+        'STRING' : 'ParameterStringSocket',
+    }
+    cant_read=0
+    if not mantis_node:
+        links = [link for link in self.inputs['Custom Properties'].links]
+        links.sort(key=lambda a : -a.multi_input_sort_id)
+        for link in links:
+            if link.from_node.bl_idname == 'UtilityCustomProperty':
+                if (link.from_node.inputs['Name'].is_linked or 
+                            link.from_node.inputs['Type'].is_linked):
+                    cant_read+=1 # we can't eval this without the Mantis data
+                    continue # but we need to count it
+                else:
+                    custom_props[link.from_node.inputs['Name'].default_value] = \
+                        type_map[link.from_node.inputs['Type'].default_value]
+            else:
+                cant_read+=1
+        for name, type in custom_props.items():
+            if self.outputs.get(name): continue # already there
+            else:
+                self.outputs.new(type, name)
+    else:
+        from .misc_nodes import UtilityCustomProperty
+        from .mantis_dataclasses import custom_prop_template
+        mantis_node.inputs['Custom Properties'].flush_links() # clean/sort the links
+        
+        if len(mantis_node.inputs['Custom Properties'].links) != len(self.inputs['Custom Properties'].links):
+            return # there is a problem somewhere
+        for i, link in enumerate(mantis_node.inputs['Custom Properties'].links):
+            from .node_common import trace_single_line
+            node_line, _last_socket = trace_single_line(mantis_node, 'Custom Properties', i)
+            if not node_line[-1].prepared:
+                return
+            custom_prop_data = mantis_node.evaluate_input('Custom Property', i)
+            if isinstance(node_line[-1], UtilityCustomProperty) and \
+                    not isinstance(custom_prop_data, custom_prop_template):
+                return
+            elif not isinstance(custom_prop_data, custom_prop_template):
+                continue # wrong connection
+            name = custom_prop_data.name
+            type = custom_prop_data.prop_type
+            custom_props[name] = type_map[type]
+    from .utilities import get_socket_maps, do_relink
+    _socket_map_in, socket_map_out = get_socket_maps(self)
+    for name, type in custom_props.items():
+        if socket_map_out.get(name) is None:
+            socket_map_out[name] = None # initialize the new ones
+    # remove and replace sockets
+    if cant_read == 0:
+        remove_me = self.outputs[1:]
+        while remove_me:
+            remove_socket = remove_me.pop()
+            self.outputs.remove(remove_socket)
+        for name, type in custom_props.items():
+            socket = self.outputs.new(type, name)
+            do_relink(self, socket, socket_map_out, in_out='OUTPUT')
+
+
 # I had chat gpt flip these so they may be a little innacurate
 # always visible
 main_names = {
@@ -175,7 +240,7 @@ class xFormBoneNode(Node, xFormNode):
         for name, sock_type in ik_names.items():
             s = self.inputs.new(sock_type, name)
             s.hide = True
-
+        
         for name, sock_type in display_names.items():
             if name == 'Bone Collection': # HACK because I am not using Socket Templates yet
                 s = self.inputs.new(sock_type, name, use_multi_input=True)
@@ -201,6 +266,8 @@ class xFormBoneNode(Node, xFormNode):
             self.inputs.new(sock_type, name)
         # could probably simplify this further with iter_tools.chain() but meh
 
+        self.inputs.new("CustomPropSocket", "Custom Properties", use_multi_input=True)
+
         self.socket_count = len(self.inputs)
         #
         self.outputs.new('xFormSocket', "xForm Out")
@@ -225,29 +292,24 @@ class xFormBoneNode(Node, xFormNode):
         #
         self.initialized=True
     
-    def draw_buttons(self, context, layout):
-        # return
-        layout.operator("mantis.add_custom_property", text='+Add Custom Parameter')
-        # layout.label(text="Edit Parameter ... not implemented")
-        if (len(self.inputs) > self.socket_count):
-            layout.operator("mantis.edit_custom_property", text=' Edit Custom Parameter')
-            layout.operator("mantis.remove_custom_property", text='-Remove Custom Parameter')
-        else:
-            layout.label(text="")
-    
     def draw_label(self): # this will prefer a user-set label, or return the evaluated name
         return main_draw_label(self)
 
         
     def display_update(self, parsed_tree, context):
+        # let's setup the outputs for the custom properties:
+        # we'll start by trying to do it without the special mantis data
+        if self.id_data.mantis_version[1] < 13: return #or the old custom properties will get wiped.
+        custom_prop_display_update(self)
         if context.space_data:
             mantis_node = parsed_tree.get(get_signature_from_edited_tree(self, context))
+            if mantis_node and mantis_node.prepared:
+                custom_prop_display_update(self, mantis_node)
             self.display_ik_settings = False
             if mantis_node and (pb := mantis_node.bGetObject(mode='POSE')):
                 self.display_ik_settings = pb.is_in_ik_chain
-            
             self.inputs['Name'].display_text = ""
-            if mantis_node:
+            if mantis_node and mantis_node.prepared:
                 try:
                     self.inputs['Name'].display_text = mantis_node.evaluate_input("Name")
                     self.display_vp_settings = mantis_node.inputs["Custom Object"].is_connected
@@ -271,6 +333,15 @@ class xFormBoneNode(Node, xFormNode):
                 if name in ['BBone Segments']: continue
                 self.inputs[name].hide = not self.display_bb_settings
 
+def object_displaty_update(self, parsed_tree, context):
+    custom_prop_display_update(self)
+    if context.space_data:
+        mantis_node = parsed_tree.get(get_signature_from_edited_tree(self, context))
+        self.inputs['Name'].display_text = ""
+        if mantis_node and mantis_node.prepared:
+            self.inputs['Name'].display_text = mantis_node.evaluate_input("Name")
+            custom_prop_display_update(self, mantis_node)
+
 class xFormArmatureNode(Node, xFormNode):
     '''A node representing an Armature object node'''
     bl_idname = 'xFormArmatureNode'
@@ -289,11 +360,7 @@ class xFormArmatureNode(Node, xFormNode):
         return main_draw_label(self)
     
     def display_update(self, parsed_tree, context):
-        if context.space_data:
-            mantis_node = parsed_tree.get(get_signature_from_edited_tree(self, context))
-            self.inputs['Name'].display_text = ""
-            if mantis_node:
-                self.inputs['Name'].display_text = mantis_node.evaluate_input("Name")
+        object_displaty_update(self, parsed_tree, context)
 
 class xFormGeometryObjectNode(Node, xFormNode):
     """Represents a curve or mesh object."""
@@ -313,11 +380,7 @@ class xFormGeometryObjectNode(Node, xFormNode):
         return main_draw_label(self)
     
     def display_update(self, parsed_tree, context):
-        if context.space_data:
-            mantis_node = parsed_tree.get(get_signature_from_edited_tree(self, context))
-            self.inputs['Name'].display_text = ""
-            if mantis_node:
-                self.inputs['Name'].display_text = mantis_node.evaluate_input("Name")
+        object_displaty_update(self, parsed_tree, context)
 
 class xFormObjectInstance(Node, xFormNode):
     """Represents an instance of an existing geometry object."""
@@ -337,11 +400,7 @@ class xFormObjectInstance(Node, xFormNode):
         return main_draw_label(self)
     
     def display_update(self, parsed_tree, context):
-        if context.space_data:
-            mantis_node = parsed_tree.get(get_signature_from_edited_tree(self, context))
-            self.inputs['Name'].display_text = ""
-            if mantis_node:
-                self.inputs['Name'].display_text = mantis_node.evaluate_input("Name")
+        object_displaty_update(self, parsed_tree, context)
 
 from .xForm_nodes import xFormCurvePinSockets
 class xFormCurvePin(Node, xFormNode):

+ 8 - 1
xForm_socket_templates.py

@@ -1,4 +1,4 @@
-from .base_definitions import MantisSocketTemplate as SockTemplate
+from .mantis_dataclasses import MantisSocketTemplate as SockTemplate
 from .misc_nodes_socket_templates import SplineIndexTemplate
 from dataclasses import replace
 
@@ -14,6 +14,9 @@ xFormArmatureSockets=[
             blender_property='matrix_world' ),
     RelationshipInTemplate := SockTemplate(
         name="Relationship", is_input=True,  bl_idname='RelationshipSocket', ),
+    CustomPropsTemplate := SockTemplate(
+        name="Custom Properties", is_input=True,  bl_idname='CustomPropSocket',
+            use_multi_input=True, ),
     xFormOutTemplate := SockTemplate(
         name="xForm Out", is_input=False,  bl_idname='xFormSocket', ),
 ]
@@ -32,6 +35,7 @@ xFormGeometryObjectSockets=[
     HideRenderTemplate := SockTemplate(name="Hide in Render",
         is_input=True, bl_idname='BooleanSocket', default_value=False,
         blender_property='hide_render' ),
+    CustomPropsTemplate,
     xFormOutTemplate,
 ]
 
@@ -46,6 +50,7 @@ xFormGeometryObjectInstanceSockets=[
     DeformerInTemplate,
     HideTemplate,
     HideRenderTemplate,
+    CustomPropsTemplate,
     xFormOutTemplate,
 ]
 
@@ -66,6 +71,7 @@ xFormCurvePinSockets = [
     CurvePinDisplaySize := SockTemplate(
         name="Display Size", is_input=True,  bl_idname='FloatPositiveSocket',
         default_value=0.05, blender_property='empty_display_size'),
+    CustomPropsTemplate,
     xFormOutTemplate,
 ]
 
@@ -211,6 +217,7 @@ xFormBoneSockets = [
     # hide
     replace(HideTemplate, name='Hide', category='always_show',
             blender_property='hide', default_value=False,),
+    CustomPropsTemplate,
     # Output
     xFormOutTemplate,
 ]