1
0

2 Commits b4ad5bfeff ... 17c6920b48

Autor SHA1 Mensaje Fecha
  Joseph Brandenburg 17c6920b48 Compatibility improvements for 5.0 hace 3 meses
  Joseph Brandenburg 5233a77c94 Custom Properties Nodes (for all xForms!) hace 3 meses

+ 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,
 ]