1
0

12 Коммиты 555c99e212 ... 9fba5be67f

Автор SHA1 Сообщение Дата
  Joseph Brandenburg 9fba5be67f Big Fix Cleanup: Mantis Socket Fillers 4 недель назад
  Joseph Brandenburg 0cebd85655 Array properties can be given single driver now 4 недель назад
  Joseph Brandenburg b9c844629e fix: bone display update 1 месяц назад
  Joseph Brandenburg f9dc2cc911 fix: s.sync error when tree empty 1 месяц назад
  Joseph Brandenburg fd30f33afc Fix: armature and fcurve versioning 1 месяц назад
  Joseph Brandenburg 6eb28ad9e9 use generated prop_sockets map in Armature Deformer 1 месяц назад
  Joseph Brandenburg ccba82dce3 Fix: selection sync for pre-5.0 blender, deselect 1 месяц назад
  Joseph Brandenburg be810b0d49 Fix: Bone Collection Versioning 1 месяц назад
  Joseph Brandenburg 30dc2159da Fix Armature and FCurve versioning still broken 1 месяц назад
  Joseph Brandenburg c9c5bdd677 Fix: bone Collection Update broke old trees 1 месяц назад
  Joseph Brandenburg f36f1322f9 Add versioning disable command line switch 1 месяц назад
  Joseph Brandenburg 6555bf8414 Fix: Custom Property Versioning Links Broken 1 месяц назад

+ 7 - 1
__init__.py

@@ -54,6 +54,9 @@ if environ.get("ENABLEVIS"):
 DOERROR = False
 if environ.get("DOERROR"):
     DOERROR = True
+DOVERSIONING = True
+if environ.get("DOVERSIONING") in ["0", "False", "false", "off"]:
+    DOVERSIONING = False
 
 import nodeitems_utils
 from nodeitems_utils import NodeCategory, NodeItem
@@ -325,7 +328,10 @@ def version_update_handler(filename):
                 node_tree.is_exporting=False; node_tree.is_executing=False
     for node_tree in bpy.data.node_groups:
         if node_tree.bl_idname in ["MantisTree", "SchemaTree"]:
-            if (node_tree.mantis_version[0] < MANTIS_VERSION_MAJOR) or \
+            if DOVERSIONING == False:
+                version = node_tree.mantis_version
+                print (f"{node_tree.name}:  v{version[0], version[1], version[2]}")
+            elif (node_tree.mantis_version[0] < MANTIS_VERSION_MAJOR) or \
                (node_tree.mantis_version[1] < MANTIS_VERSION_MINOR) or \
                (node_tree.mantis_version[2] < MANTIS_VERSION_SUB):
                 do_version_update(node_tree)

+ 17 - 3
base_definitions.py

@@ -782,16 +782,30 @@ class MantisNode:
         for key, value in additional_parameters.items():
             self.parameters[key]=value
 
-    def gen_property_socket_map(self) -> dict:
+    def gen_property_socket_map(self, prop_owner_group=None) -> dict:
+        from .mantis_dataclasses import MantisSocketFiller
+        from dataclasses import replace
         props_sockets = {}
         for s_template in self.socket_templates:
+            if prop_owner_group is not None and \
+                    s_template.prop_owner_group != prop_owner_group:
+                continue # for composite objects like bones
             if not s_template.blender_property:
                 continue
+            filler = MantisSocketFiller(socket_name=s_template.name)
             if isinstance(s_template.blender_property, str):
-                props_sockets[s_template.blender_property]=(s_template.name, s_template.default_value)
+                filler = replace(filler)
+                filler.property_name = s_template.blender_property
+                filler.default_value=s_template.default_value
+                props_sockets[s_template.blender_property]=filler
             elif isinstance(s_template.blender_property, (tuple, list)):
                 for index, sub_prop in enumerate(s_template.blender_property):
-                    props_sockets[sub_prop]=( (s_template.name, index),s_template.default_value[index] )
+                    filler = replace(filler)
+                    filler.property_name = sub_prop
+                    filler.value_index=index
+                    filler.default_value = s_template.default_value[index]
+                    filler.combo_property=True
+                    props_sockets[sub_prop]= filler
         return props_sockets
 
     def set_traverse(self, traversal_pairs = [(str, str)]) -> None:

+ 15 - 13
deformer_nodes.py

@@ -265,14 +265,7 @@ class DeformerArmature(MantisDeformerNode):
                 raise RuntimeError(f"Modifier was not created in node {self} -- the object is invalid.")
             self.bObject.append(d)
             d.object = self.bGetParentArmature()
-            props_sockets = {
-            'vertex_group'               : ("Blend Vertex Group", ""),
-            'invert_vertex_group'        : ("Invert Vertex Group", ""),
-            'use_deform_preserve_volume' : ("Preserve Volume", False),
-            'use_multi_modifier'         : ("Use Multi Modifier", False),
-            'use_bone_envelopes'         : ("Use Envelopes", False),
-            'use_vertex_groups'          : ("Use Vertex Groups", False),
-            }
+            props_sockets = self.gen_property_socket_map()
             evaluate_sockets(self, d, props_sockets)
             #
             if (skin_method := self.evaluate_input("Skinning Method")) == "AUTOMATIC_HEAT":
@@ -568,7 +561,13 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
             sk.slider_min = -10
             sk.slider_max = 10
             keys[mt_name]=sk
-            props_sockets[mt_name]= ("Value."+str(i).zfill(3), 1.0)
+            from .mantis_dataclasses import MantisSocketFiller
+            socket_value = MantisSocketFiller(
+                property_name=mt_name,
+                socket_name = "Value."+str(i).zfill(3),
+                default_value= 1.0,
+            )
+            props_sockets[mt_name]= socket_value
         for i, t in enumerate(targets):
             mt_node = t.links[0].from_node; sk_ob = mt_node.GetxForm().bGetObject()
             if sk_ob is None: continue
@@ -581,9 +580,6 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
         finish_drivers(self)
         prWhite(f"Initializing morph target took {time() -start_time} seconds")
 
-
-        
-
     def gen_shape_key(self, xf, context):
         # TODO: make this a feature of the node definition that appears only when there are no prior deformers - and shows a warning!
         # TODO: the below works well, but it is quite slow. It does not seem to have better performence. Its only advantage is export to FBX.
@@ -659,7 +655,13 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
             sk.slider_min = -10
             sk.slider_max = 10
             keys[mt_name]=sk
-            props_sockets[mt_name]= ("Value."+str(i).zfill(3), 1.0)
+            from .mantis_dataclasses import MantisSocketFiller
+            socket_value = MantisSocketFiller(
+                property_name=mt_name,
+                socket_name = "Value."+str(i).zfill(3),
+                default_value= 1.0,
+            )
+            props_sockets[mt_name]= socket_value
         for i, t in enumerate(targets):
             mt_node = t.links[0].from_node; sk_ob = mt_node.GetxForm().bGetObject()
             if sk_ob is None: continue

+ 13 - 6
deformer_socket_templates.py

@@ -10,15 +10,22 @@ Target = ST(name="Target", bl_idname='xFormSocket',
 
 DeformArmatureSockets = [ 
     ArmatureObject := replace(Target, name="Armature Object"),
-    BlendVertexGroup := ST(name="Blend Vertex Group", bl_idname="StringSocket", is_input=True),
-    InvertBlendVertexGroup := ST(name="Invert Vertex Group", bl_idname="BooleanSocket", is_input=True),
+    BlendVertexGroup := ST(name="Blend Vertex Group", bl_idname="StringSocket", is_input=True,
+                           blender_property='vertex_group'),
+    InvertBlendVertexGroup := ST(name="Invert Vertex Group", bl_idname="BooleanSocket", is_input=True,
+                           blender_property='invert_vertex_group'),
     # QuatBlendVertexGroup := ST(name="Preserve Volume Vertex Group", bl_idname="StringSocket", is_input=True)
     # ^ TODO figure out the right UX for automatic dual-quat blending
-    PreserveVolume := ST(name="Preserve Volume", bl_idname="BooleanSocket", is_input=True),
+    PreserveVolume := ST(name="Preserve Volume", bl_idname="BooleanSocket", is_input=True,
+                           blender_property='use_deform_preserve_volume'),
     # ^ TODO: make the above controlled by a vertex group instead.
-    UseMultiModifier := ST(name="Use Multi Modifier", bl_idname="BooleanSocket", is_input=True), # might just set this auto
-    UseEnvelopes := ST(name="Use Envelopes", bl_idname="BooleanSocket", is_input=True, default_value=False),
-    UseVertexGroups := ST(name="Use Vertex Groups", bl_idname="BooleanSocket", is_input=True, default_value=True),
+    # might just set this below to auto (I don't evern understand it)
+    UseMultiModifier := ST(name="Use Multi Modifier", bl_idname="BooleanSocket", is_input=True,
+                           blender_property='use_multi_modifier'),
+    UseEnvelopes := ST(name="Use Envelopes", bl_idname="BooleanSocket", is_input=True, default_value=False,
+                           blender_property='use_bone_envelopes'),
+    UseVertexGroups := ST(name="Use Vertex Groups", bl_idname="BooleanSocket", is_input=True, default_value=True,
+                           blender_property='use_vertex_groups'),
     DeformerInput := ST(name="Deformer", bl_idname="DeformerSocket", is_input=True),
     CopySkinWeightsFrom := ST(name="Copy Skin Weights From", bl_idname="xFormSocket", is_input=True, hide=True),
     SkinningMethod := ST(name="Skinning Method", bl_idname="EnumSkinning", is_input=True),

+ 24 - 7
link_nodes.py

@@ -65,7 +65,7 @@ class MantisLinkNode(MantisNode):
             return None
 
         else:
-            return super().evaluate_input(input_name)
+            return super().evaluate_input(input_name, index)
 
     def gen_property_socket_map(self) -> dict:
         props_sockets = super().gen_property_socket_map()
@@ -319,17 +319,21 @@ class LinkTransformation(MantisLinkNode):
                 for axis in ['x', 'y', 'z']:
                     stub='from_min_'+axis
                     props_sockets[stub+from_suffix]=props_sockets[stub]
+                    props_sockets[stub+from_suffix].property_name = stub+from_suffix
                     del props_sockets[stub]
                     stub='from_max_'+axis
                     props_sockets[stub+from_suffix]=props_sockets[stub]
+                    props_sockets[stub+from_suffix].property_name = stub+from_suffix
                     del props_sockets[stub]
             if to_replace:
                 for axis in ['x', 'y', 'z']:
                     stub='to_min_'+axis
                     props_sockets[stub+to_replace]=props_sockets[stub]
+                    props_sockets[stub+from_suffix].property_name = stub+to_replace
                     del props_sockets[stub]
                     stub='to_max_'+axis
                     props_sockets[stub+to_replace]=props_sockets[stub]
+                    props_sockets[stub+from_suffix].property_name = stub+to_replace
                     del props_sockets[stub]
             evaluate_sockets(self, c, props_sockets)
         self.executed = True
@@ -766,8 +770,11 @@ class LinkDrivenParameter(MantisLinkNode):
                 def_value=[0.0]*length
                 self.parameters["Value"] = tuple( 0.0 if i != index else value for i in range(length))
 
+            from .mantis_dataclasses import MantisSocketFiller
             props_sockets = {
-                prop: ("Value", def_value)
+                prop: MantisSocketFiller( property_name=prop,
+                                          socket_name="Value",
+                                          default_value=def_value)
             }
             evaluate_sockets(self, ob, props_sockets)
 
@@ -814,6 +821,7 @@ class LinkArmature(MantisLinkNode):
             #       are all in the same armature
             # Mantis should intelligently provide a working armature constraint
             # even if the user supplies targets that would not work in vanilla Blender
+            # TODO find out this is reading the arrays in reverse order? is that bad?
             targets_weights = {}
             for i in range(num_targets):
                 target = c.targets.new()
@@ -822,7 +830,17 @@ class LinkArmature(MantisLinkNode):
                 if not isinstance(weight_value, float):
                     weight_value=0
                 targets_weights[i]=weight_value
-                props_sockets["targets[%d].weight" % i] = ("Weights", i)
+                from .mantis_dataclasses import MantisSocketFiller
+                default_value = 1.0
+                if self.inputs['Weights'].links:
+                    default_value = 1/len(self.inputs['Weights'].links)
+                filler = MantisSocketFiller(
+                    property_name= (prop_name := "targets[%d].weight" % i),
+                    socket_name="Weights",
+                    default_value=default_value,
+                    array_index=i,
+                )
+                props_sockets[prop_name] = filler
             evaluate_sockets(self, c, props_sockets)
             for target, value in targets_weights.items():
                 c.targets[target].weight=value
@@ -904,14 +922,13 @@ class LinkShrinkWrap(MantisLinkNode):
                 wrapGreen(" Constraint for bone: ") +
                 wrapOrange(xf.bGetObject().name))
             c = xf.bGetObject().constraints.new('SHRINKWRAP')
+            self.bObject.append(c)
             self.get_target_and_subtarget(c)
             if constraint_name := self.evaluate_input("Name"):
                 c.name = constraint_name
             props_sockets = self.gen_property_socket_map()
-            print (props_sockets.keys())
-            self.bObject.append(c)
-            # self.set_custom_space() # this needs to be overridden for me TODO
             evaluate_sockets(self, c, props_sockets)
+            # self.set_custom_space() # this needs to be overridden for me TODO
         self.executed = True
 
 
@@ -931,10 +948,10 @@ class LinkGeometryAttribute(MantisLinkNode):
         for xf in self.GetxForm():
             print
             c = xf.bGetObject().constraints.new('GEOMETRY_ATTRIBUTE')
+            self.bObject.append(c)
             self.get_target_and_subtarget(c)
             if constraint_name := self.evaluate_input("Name"):
                 c.name = constraint_name
             props_sockets = self.gen_property_socket_map()
-            self.bObject.append(c)
             evaluate_sockets(self, c, props_sockets)
         self.executed = True

+ 21 - 2
mantis_dataclasses.py

@@ -1,5 +1,5 @@
 from dataclasses import dataclass, field
-from typing import List, Any, Tuple
+from typing import List, Any, Tuple, Optional
 
 @dataclass
 class MantisSocketTemplate():
@@ -19,6 +19,7 @@ class MantisSocketTemplate():
     #  where multiple blender objects own the properties, like e.g. bone/posebone
     prop_owner_group : str = field(default="")
     # do? add versioning fields for Mantis, Blender version min/max
+    is_animatable    : bool = field(default=True) # controls whether a driver is legal or not
 
 @dataclass
 class xForm_info():
@@ -78,4 +79,22 @@ class MantisBBoneTransform():
     # Custom Handles
     use_bbone_custom_handles: bool = field(default=False)
     bbone_custom_handle_start: str = field(default=None)
-    bbone_custom_handle_end: str = field(default=None)
+    bbone_custom_handle_end: str = field(default=None)
+
+# used to apply values or drivers to blender properties in a generic way
+@dataclass
+class MantisSocketFiller():
+    socket_name: str = field(default="")
+    property_name : str = field(default="")
+    default_value: Any = field(default=None)
+    array_index: int = field(default=0) # for multi-input array sockets
+    value_index: Optional[int] = field(default=None) # for vector properties
+    # A Combo Property is a property that jams a bunch of blender properties into one socket
+    combo_property: bool = field(default=False)
+
+    def __repr__(self):
+        return f"socket_name: {self.socket_name}, " \
+               f"property_name: {self.property_name}, " \
+               f"default_value: {self.default_value}, " \
+               f"array_index: {self.array_index or 0}, " \
+               f"value_index: {self.value_index or None}" 

+ 4 - 3
misc_nodes_ui.py

@@ -534,10 +534,11 @@ class UtilityCustomProperty(Node, MantisMiscNode):
     socket_templates=UtilityCustomPropertySockets
     
     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")
+        if context: # I need to be able to call this without context in versioing.
+            if mantis_node := parsed_tree.get(get_signature_from_edited_tree(self, context)):
+                # 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')

+ 66 - 54
timers.py

@@ -62,60 +62,71 @@ for space in spaces:
 #  - easy to fix, will do it when clearing TODO's here.
 
 def timer_update_selection_sync():
-    if not ENABLED: return None
-    root_tree = None
-    tree_path = None
-    parsed_tree = None
-    spaces = get_visible_space_data()
-    if not spaces:
-        return SELECTION_POLL_RATE
-
-    selection_state_start = cached_selection_state.copy()
-    selection_state = cached_selection_state.copy()
-    current_tree_selected_total = 0
-
-    # we can't get UI context in this function so we have to checc all of them
-    #maybe the tree itself can load the timer when it is opened in the node editor?
-    for space in spaces:
-        # if we are in the node editor, manage node selection
-        if space.type == "NODE_EDITOR":
-            root_tree = space.path[0].node_tree
-            parsed_tree = root_tree.parsed_tree
-            tree_path = [path_item.node_tree for path_item in space.path]
-            if root_tree is None:
-                return SELECTION_POLL_RATE
-            if root_tree.bl_idname != 'MantisTree':
-                return SELECTION_POLL_RATE
-            active_ui_signature = (get_signature_from_space(tree_path[-1].nodes.active, space))
-            for n in tree_path[-1].nodes:
-                if n.bl_idname not in WATCH_FOR_SELECTION: continue
-                selection_state[ui_signature := get_signature_from_space(n, space)] = n.select
-                current_tree_selected_total += int(n.select)
-    
-    if parsed_tree is None:
-        return SELECTION_POLL_RATE
-    cached_selection_state.update(selection_state)
-    # We only fire the update if things have changed.
-    if selection_state != selection_state_start:
+    try:
+        if not ENABLED: return None
+        root_tree = None
+        tree_path = None
+        parsed_tree = None
+        spaces = get_visible_space_data()
+        if not spaces:
+            return SELECTION_POLL_RATE
+
+        selection_state_start = cached_selection_state.copy()
+        selection_state = cached_selection_state.copy()
+        current_tree_selected_total = 0
+
+        # we can't get UI context in this function so we have to checc all of them
+        #maybe the tree itself can load the timer when it is opened in the node editor?
+        for space in spaces:
+            # if we are in the node editor, manage node selection
+            if space.type == "NODE_EDITOR":
+                if not space.path:
+                    return SELECTION_POLL_RATE # empty tree
+                root_tree = space.path[0].node_tree
+                parsed_tree = root_tree.parsed_tree
+                tree_path = [path_item.node_tree for path_item in space.path]
+                if root_tree is None:
+                    return SELECTION_POLL_RATE
+                if root_tree.bl_idname != 'MantisTree':
+                    return SELECTION_POLL_RATE
+                active_ui_signature = (get_signature_from_space(tree_path[-1].nodes.active, space))
+                for n in tree_path[-1].nodes:
+                    if n.bl_idname not in WATCH_FOR_SELECTION: continue
+                    selection_state[ui_signature := get_signature_from_space(n, space)] = n.select
+                    current_tree_selected_total += int(n.select)
         
-        from bpy import context
-        view_layer = context.view_layer
-        deselect = not bool(current_tree_selected_total)
-        try:
-            update_scene_selection_from_node_graph_selection_sync(
-                active_ui_signature, view_layer,
-                selection_state, deselect)
-        except Exception as e:
-            prRed (e)
+        if parsed_tree is None:
             return SELECTION_POLL_RATE
+        cached_selection_state.update(selection_state)
+        # We only fire the update if things have changed.
+        if selection_state != selection_state_start:
+            from bpy import context
+            view_layer = context.view_layer
+            deselect = not bool(current_tree_selected_total)
+            try:
+                update_scene_selection_from_node_graph_selection_sync(
+                    active_ui_signature, view_layer,
+                    selection_state, deselect)
+            except Exception as e:
+                prRed (e)
+                return SELECTION_POLL_RATE
+    except Exception as e:
+        print ("Error in Selection Sync")
+        print (e)
     return SELECTION_POLL_RATE
 
+def set_bone_selection(pose_bone, selection_state):
+    # in some older blender versions, select was a bone property
+    if hasattr(pose_bone, 'select'):
+        pose_bone.select = selection_state
+    elif hasattr(pose_bone.bone, 'select'):
+        pose_bone.bone.select = selection_state
+
 def update_scene_selection_from_node_graph_selection_sync(
         active_ui_signature, view_layer,
         selection_state, deselect ):
-    from .base_definitions import ( selection_sync_node_to_object,
-                                    selection_sync_object_to_node, )
-    prWhite("Updating Scene Selection from Node Graph Selection")
+    from .base_definitions import selection_sync_node_to_object
+    # prWhite("Updating Scene Selection from Node Graph Selection")
     deselect_do_once = []
     for ui_signature, selected in selection_state.items():
         this_one_is_active = False
@@ -123,15 +134,16 @@ def update_scene_selection_from_node_graph_selection_sync(
         for object_key in selection_sync_node_to_object.get(ui_signature, []):
             id_data = bpy.data.objects.get(object_key[0])
             if object_key[1]: # we have a sub_value, it's a bone
-                if deselect and object_key[0] not in deselect_do_once:
-                    for pb in id_data.pose.bones:
-                        pb.select = False
-                    deselect_do_once.append(object_key[0])
+                if deselect:
+                    if object_key[0] not in deselect_do_once:
+                        for pb in id_data.pose.bones:
+                            set_bone_selection(pb, False)
+                        deselect_do_once.append(object_key[0])
                 else:
-                    ob = id_data.pose.bones[object_key[1]]
-                    ob.select = selected
+                    pb = id_data.pose.bones[object_key[1]]
+                    set_bone_selection(pb, selected)
                     if this_one_is_active: # set the active bone
-                        id_data.data.bones.active = ob.bone
+                        id_data.data.bones.active = pb.bone
                     # now do its parent object...
                     if selected:
                         id_data.select_set(True, view_layer=view_layer)

+ 91 - 86
utilities/drivers.py

@@ -52,102 +52,107 @@ def CreateDrivers(drivers):
     def brackets(s):
         return "[\""+s+"\"]"
     from bpy.types import Object, Key
+    fcurves = []
     for driver in drivers:
         if (isinstance(driver["owner"], Object)):
             ob = driver["owner"]
         else: # Pose Bone:
             ob = driver["owner"].id_data
         if isinstance(driver["owner"], Key):
-            fc = ob.driver_add(driver["prop"])
+            fcurves = ob.driver_add(driver["prop"])
         else:
-            fc = ob.driver_add(driver["owner"].path_from_id(driver["prop"]), driver["ind"])
-        drv = fc.driver
-        try: # annoyingly, this initializes with a modifier
-            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
-
-        fc.extrapolation = "CONSTANT"
-        if (extrapolation_mode := driver.get("extrapolation")) in ("CONSTANT", "LINEAR"):
-            fc.extrapolation = extrapolation_mode
-        else:
-            prRed(f"Extrapolation Mode in driver has incorrect data: {extrapolation_mode}")
+            fcurves = ob.driver_add(driver["owner"].path_from_id(driver["prop"]), driver["ind"])
+        if not isinstance(fcurves, list):
+            fcurves = [fcurves] # ususally it is a single fCurve, sometimes it is a list
+            # for e.g. array properties if driver_add is given -1 as index
+        for fc in fcurves:
+            drv = fc.driver
+            try: # annoyingly, this initializes with a modifier
+                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
+
+            fc.extrapolation = "CONSTANT"
+            if (extrapolation_mode := driver.get("extrapolation")) in ("CONSTANT", "LINEAR"):
+                fc.extrapolation = extrapolation_mode
+            else:
+                prRed(f"Extrapolation Mode in driver has incorrect data: {extrapolation_mode}")
 
-        # logic for handling type can go here
+            # logic for handling type can go here
 
-        # start by clearing
-        while (len(drv.variables) > 0):
-            v = drv.variables[0]
-            dVar = drv.variables.remove(v)
+            # start by clearing
+            while (len(drv.variables) > 0):
+                v = drv.variables[0]
+                dVar = drv.variables.remove(v)
 
-        for v in driver["vars"]:
-            pose_bone = False
-            bone = ''; target2bone = ''
-            vob, target2ob = None, None
-            if (isinstance(v["owner"], Object)):
-                vob = v["owner"]
-            else:
-                pose_bone = True
-                vob = v["owner"].id_data
-                bone = v["owner"].name
-            #
-
-            if "xForm 2" in v.keys() and v["xForm 2"]:
-                if (isinstance(v["xForm 2"], Object)):
-                    target2ob = v["xForm 2"]
+            for v in driver["vars"]:
+                pose_bone = False
+                bone = ''; target2bone = ''
+                vob, target2ob = None, None
+                if (isinstance(v["owner"], Object)):
+                    vob = v["owner"]
                 else:
-                    target2ob = v["xForm 2"].id_data
-                    target2bone = v["xForm 2"].name
-
-            dVar = drv.variables.new()
-
-
-            dVar.name = v["name"]
-            dVar.type = v["type"]
-            #for now, assume this is always true:
-            #dVar.targets[0].id_type = "OBJECT"
-            #it's possible to use other datablocks, but this is not commonly done
-            #actually, it looks like Blender figures this out for me.
-
-            dVar.targets[0].id = vob
-            dVar.targets[0].bone_target = bone
-            if len(dVar.targets) > 1:
-                dVar.targets[1].id = target2ob
-                dVar.targets[1].bone_target = target2bone
-
-            if (dVar.type == "TRANSFORMS"):
-                dVar.targets[0].transform_space = v["space"]
-                dVar.targets[0].transform_type = v["channel"]
-            if (dVar.type == 'SINGLE_PROP'):
-                if pose_bone:
-                    stub = "pose.bones[\""+v["owner"].name+"\"]"
-                    dVar.targets[0].data_path = stub + brackets(v["prop"])
-                    if (hasattr( v["owner"], v["prop"] )):
-                        dVar.targets[0].data_path = stub + "."+ (v["prop"])
-                    # else: # the property may be added later.
-                    # TODO BUG I want a guarantee that this property is already there.
+                    pose_bone = True
+                    vob = v["owner"].id_data
+                    bone = v["owner"].name
+                #
+
+                if "xForm 2" in v.keys() and v["xForm 2"]:
+                    if (isinstance(v["xForm 2"], Object)):
+                        target2ob = v["xForm 2"]
+                    else:
+                        target2ob = v["xForm 2"].id_data
+                        target2bone = v["xForm 2"].name
+
+                dVar = drv.variables.new()
+
+
+                dVar.name = v["name"]
+                dVar.type = v["type"]
+                #for now, assume this is always true:
+                #dVar.targets[0].id_type = "OBJECT"
+                #it's possible to use other datablocks, but this is not commonly done
+                #actually, it looks like Blender figures this out for me.
+
+                dVar.targets[0].id = vob
+                dVar.targets[0].bone_target = bone
+                if len(dVar.targets) > 1:
+                    dVar.targets[1].id = target2ob
+                    dVar.targets[1].bone_target = target2bone
+
+                if (dVar.type == "TRANSFORMS"):
+                    dVar.targets[0].transform_space = v["space"]
+                    dVar.targets[0].transform_type = v["channel"]
+                if (dVar.type == 'SINGLE_PROP'):
+                    if pose_bone:
+                        stub = "pose.bones[\""+v["owner"].name+"\"]"
+                        dVar.targets[0].data_path = stub + brackets(v["prop"])
+                        if (hasattr( v["owner"], v["prop"] )):
+                            dVar.targets[0].data_path = stub + "."+ (v["prop"])
+                        # else: # the property may be added later.
+                        # TODO BUG I want a guarantee that this property is already there.
 
-                else:
-                    if (hasattr( v["owner"], v["prop"] )):
-                        dVar.targets[0].data_path = (v["prop"])
                     else:
-                        dVar.targets[0].data_path = brackets(v["prop"])
-        # setup keyframe points
-        kp = fc.keyframe_points
-        for key in driver["keys"]:
-            k = kp.insert(frame=key["co"][0], value = key["co"][1],)
-
-            k.interpolation     = key["interpolation"]
-            if (key["interpolation"] == 'BEZIER'):
-                k.handle_left_type  = key["handle_left_type" ]
-                k.handle_right_type = key["handle_right_type"]
-                if (k.handle_left_type in ("ALIGNED", "VECTOR", "FREE")):
-                    k.handle_left       = (k.co[0] + key["handle_left"][0], k.co[1] + key["handle_left"][1])
-                if (k.handle_right_type in ("ALIGNED", "VECTOR", "FREE")):
-                    k.handle_right      = (k.co[0] + key["handle_right"][0], k.co[1] + key["handle_right"][1])
-            k.type = key["type"]
+                        if (hasattr( v["owner"], v["prop"] )):
+                            dVar.targets[0].data_path = (v["prop"])
+                        else:
+                            dVar.targets[0].data_path = brackets(v["prop"])
+            # setup keyframe points
+            kp = fc.keyframe_points
+            for key in driver["keys"]:
+                k = kp.insert(frame=key["co"][0], value = key["co"][1],)
+
+                k.interpolation     = key["interpolation"]
+                if (key["interpolation"] == 'BEZIER'):
+                    k.handle_left_type  = key["handle_left_type" ]
+                    k.handle_right_type = key["handle_right_type"]
+                    if (k.handle_left_type in ("ALIGNED", "VECTOR", "FREE")):
+                        k.handle_left       = (k.co[0] + key["handle_left"][0], k.co[1] + key["handle_left"][1])
+                    if (k.handle_right_type in ("ALIGNED", "VECTOR", "FREE")):
+                        k.handle_right      = (k.co[0] + key["handle_right"][0], k.co[1] + key["handle_right"][1])
+                k.type = key["type"]

+ 7 - 1
utilities/geometry_node_graphgen.py

@@ -79,7 +79,13 @@ def gen_morph_target_nodes(mod_name, mod_ob, targets, context, use_offset=True):
             ng.links.new(input=inp.outputs[mt_name+" Value"], output=scale1.inputs["Scale"])
         add_these.append(scale1)
         object_map["Socket_"+str((i+1)*2)]=mt_node.GetxForm().bGetObject()
-        props_sockets["Socket_"+str((i+1)*2+1)]= ("Value."+str(i).zfill(3), 1.0)
+        from ..mantis_dataclasses import MantisSocketFiller
+        socket_value = MantisSocketFiller(
+            property_name = (prop := "Socket_"+str((i+1)*2+1)),
+            socket_name = (socket_name := "Value."+str(i).zfill(3)),
+            default_value = 1.0
+        )
+        props_sockets[prop] = socket_value
     
     set_position = ng.nodes.new("GeometryNodeSetPosition")
     bake = ng.nodes.new("GeometryNodeBake")

+ 89 - 49
utilities/node_common.py

@@ -124,15 +124,14 @@ def setup_custom_property_inputs_outputs(mantis_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 allowed for class {mantis_node.__class__.__name__}"))
         return
     else:
-        ui_node = get_ui_node(mantis_node.signature, mantis_node.base_tree)
+        ui_node = get_ui_node(mantis_node.ui_signature, mantis_node.base_tree)
     if ui_node:
         setup_custom_props_from_np(mantis_node, ui_node)
     else:
-        prRed("Failed to setup custom properties for: mantis_node")
+        prRed(f"Failed to setup custom properties for: {mantis_node.ui_signature}")
 
 def setup_custom_props_from_np(mantis_node, ui_node):
     for inp in ui_node.inputs:
@@ -162,13 +161,27 @@ def prepare_parameters(mantis_node):
             mantis_node.parameters[s_name] = sock.links[0].from_node.parameters[sock.name]
     # should work, this is ugly.
 
-def check_for_driver(mantis_node, input_name, index = None):
-    prop = mantis_node.evaluate_input(input_name)
-    if (index is not None):
-        prop = prop[index]
+def check_for_driver(mantis_node, input_name, value_index = None, array_index = 0, combo_property=False):
+    prop = mantis_node.evaluate_input(input_name, array_index)
+    if (value_index is not None):
+        try:
+            prop = prop[value_index]
+        except KeyError: # We'll try to use the driver for all three channels.
+            pass
+        # TODO an Index or Key error here should have a reasonable error message
     return (prop.__class__.__name__ == 'MantisDriver')
 
-# TODO: this should handle sub-properties better
+def print_driver_item(mantis_node, b_object, original_prop):
+    if mantis_node.node_type in ['LINK',]:
+        printname  = wrapOrange(b_object.id_data.name)
+    elif mantis_node.node_type in ['XFORM',]:
+        printname  = wrapOrange(mantis_node.bGetObject().name)
+    else:
+        printname = wrapOrange(mantis_node)
+    print("Adding driver %s to %s in %s" % (wrapPurple(original_prop), wrapWhite(mantis_node.signature[-1]), printname))
+
+# HACK This is the most hideous voodoo black magic fucntion in the entire program
+#      but everything depends on it and it works 
 def evaluate_sockets(mantis_node, b_object, props_sockets,):
     # this is neccesary because some things use dict properties for dynamic properties and setattr doesn't work
     def safe_setattr(ob, att_name, val):
@@ -181,20 +194,25 @@ def evaluate_sockets(mantis_node, b_object, props_sockets,):
             # it is of the form prop[int].prop2
             prop=att_name.split('[')[0]
             prop1=att_name.split('.')[1]
-            index = int(att_name.split('[')[1][0])
-            setattr(getattr(b_object, prop)[index], prop1, val)
+            get_index = int(att_name.split('[')[1][0])
+            setattr(getattr(b_object, prop)[get_index], prop1, val)
         else:
             try:
                 setattr(ob, att_name, val)
             except Exception as e:
-                prRed(ob, att_name, val); raise e
-    for prop, (sock, default) in props_sockets.items():
-        # annoyingly, sometimes the socket is an array
-        index = None
-        if isinstance(sock, tuple):
-            index = sock[1]; sock = sock[0]
-        if (check_for_driver(mantis_node, sock, index)):
-            sock = (sock, index)
+                print ("ob, att_name, val")
+                prRed(ob, att_name, val)
+                raise e
+    for prop, filler_item in props_sockets.items():
+        if isinstance(filler_item, tuple):
+            raise NotImplementedError(mantis_node)
+        sock = filler_item.socket_name
+        default = filler_item.default_value
+        value_index = filler_item.value_index
+        array_index = filler_item.array_index
+        combo_property = filler_item.combo_property
+        driver_key = (sock, value_index, array_index)
+        if (check_for_driver(mantis_node, sock, value_index, array_index, combo_property)):
             original_prop = prop
             # TODO: deduplicate this terrible hack
             if ("." in prop) and not b_object.__class__.__name__ in ["Key"]: # this is a property of a property...
@@ -204,31 +222,25 @@ def evaluate_sockets(mantis_node, b_object, props_sockets,):
                     prop = split_prop[1]
                     sub_prop = (split_prop[0])
                     if ("[" in sub_prop):
-                        sub_prop, index = sub_prop.split("[")
-                        index = int(index[0])
-                        sub_props.append(getattr(sub_props[-1], sub_prop)[index])
+                        sub_prop, sub_index = sub_prop.split("[")
+                        sub_prop_index = int(sub_index[0])
+                        sub_props.append(getattr(sub_props[-1], sub_prop)[sub_prop_index])
                     else:
                         sub_props.append(getattr(sub_props[-1], sub_prop))
                 safe_setattr(sub_props[-1], prop, default)
             # this is really stupid
             else:
                 safe_setattr(b_object, prop, default)
-            if mantis_node.node_type in ['LINK',]:
-                printname  = wrapOrange(b_object.id_data.name)
-            elif mantis_node.node_type in ['XFORM',]:
-                printname  = wrapOrange(mantis_node.bGetObject().name)
-            else:
-                printname = wrapOrange(mantis_node)
-            print("Adding driver %s to %s in %s" % (wrapPurple(original_prop), wrapWhite(mantis_node.signature[-1]), printname))
+            print_driver_item( mantis_node, b_object, original_prop)
             if b_object.__class__.__name__ in ["NodesModifier"]:
-                mantis_node.drivers[sock] = "[\""+original_prop+"\"]" # lol. It is a dict element not a "true" property
+                mantis_node.drivers[driver_key] = "[\""+original_prop+"\"]" # lol. It is a dict element not a "true" property
             elif b_object.__class__.__name__ in ["Key"]:
-                mantis_node.drivers[sock] = "key_blocks[\""+original_prop+"\"].value"
+                mantis_node.drivers[driver_key] = "key_blocks[\""+original_prop+"\"].value"
             else:
-                mantis_node.drivers[sock] = original_prop
+                mantis_node.drivers[driver_key] = original_prop
         else: # here we can do error checking for the socket if needed
-            if (index is not None):
-                safe_setattr(b_object, prop, mantis_node.evaluate_input(sock)[index])
+            if (value_index is not None):
+                safe_setattr(b_object, prop, mantis_node.evaluate_input(sock)[value_index])
             else:                    # 'mute' is better than 'enabled'
                 # UGLY HACK          # because it is available in older
                 if (prop == 'mute'): # Blenders.
@@ -237,9 +249,6 @@ def evaluate_sockets(mantis_node, b_object, props_sockets,):
                     safe_setattr(b_object, prop, bool(mantis_node.evaluate_input(sock)))
                 else:
                     try:
-                        # prRed(b_object.name, mantis_node, prop, mantis_node.evaluate_input(sock) )
-                        # print( mantis_node.evaluate_input(sock))
-                    # value_eval = mantis_node.evaluate_input(sock)
                     # just wanna see if we are dealing with some collection
                     # check hasattr in case it is one of those ["such-and-such"] props, and ignore those
                         if hasattr(b_object, prop) and (not isinstance(getattr(b_object, prop), str)) and hasattr(getattr(b_object, prop), "__getitem__"):
@@ -248,7 +257,7 @@ def evaluate_sockets(mantis_node, b_object, props_sockets,):
                                 # assume this will work, both because val should have the right number of elements, and because this should be the right data type.
                                 from .drivers import MantisDriver
                                 if isinstance(value, MantisDriver):
-                                    getattr(b_object,prop)[val_index] =  default[val_index]
+                                    getattr(b_object,prop)[val_index] = default[val_index]
                                     print("Adding driver %s to %s in %s" % (wrapPurple(prop), wrapWhite(mantis_node.signature[-1]), mantis_node))
                                     try:
                                         mantis_node.drivers[sock].append((prop, val_index))
@@ -257,24 +266,50 @@ def evaluate_sockets(mantis_node, b_object, props_sockets,):
                                 else:
                                     getattr(b_object,prop)[val_index] =  value
                         else:
-                            # prOrange("Skipping the Thing", getattr(b_object, prop))
                             safe_setattr(b_object, prop, mantis_node.evaluate_input(sock))
                     except Exception as e:
                         prRed(b_object, mantis_node, prop, sock, mantis_node.evaluate_input(sock))
                         raise e
 
 def finish_driver(mantis_node, b_object, driver_item, prop):
-    # prWhite(mantis_node, prop)
-    index = driver_item[1]; driver_sock = driver_item[0]
-    driver_trace = trace_single_line(mantis_node, driver_sock)
+    try:
+        driver_sock = driver_item[0]; index = driver_item[1]; array_index =driver_item[2]
+    except Exception as e:
+        print (wrapGreen(mantis_node), wrapOrange(b_object), wrapRed(driver_item), wrapWhite(prop),)
+        raise e
+    driver_trace = trace_single_line(mantis_node, driver_sock, array_index)
     driver_provider, driver_socket = driver_trace[0][-1], driver_trace[1]
+    
+    combo_property = False
+    # try to detect combo properties
+    if mantis_node.socket_templates:
+        for st in mantis_node.socket_templates:
+            if st.name == driver_sock:
+                if isinstance(st.blender_property, tuple):
+                    combo_property = True
+                break
+
     if index is not None:
-        driver = driver_provider.parameters[driver_socket.name][index].copy()
+        try:
+            driver = driver_provider.parameters[driver_socket.name][index].copy()
+        except KeyError as e: # try single driver, it should still work fine.
+            if combo_property:
+                driver = driver_provider.parameters[driver_socket.name].copy()
+            else:
+                raise e
         # this is harmless and necessary for the weird ones where the property is a vector too
-        driver["ind"] = index
+        if not combo_property:
+            driver["ind"] = index
     else:
         driver = driver_provider.parameters[driver_socket.name].copy()
     if driver:
+        socket_template = None
+        if mantis_node.socket_templates:
+            for socket_template in mantis_node.socket_templates:
+                if socket_template.name == driver_sock: break
+        if socket_template:
+            if socket_template.is_animatable == False:
+                raise RuntimeError(f"Driver is not allowed for {driver_sock} in node {mantis_node}")
         if ("." in prop) and mantis_node.__class__.__name__ != "DeformerMorphTargetDeform": # this is a property of a property...
             sub_props = [b_object]
             while ("." in prop):
@@ -291,11 +326,14 @@ def finish_driver(mantis_node, b_object, driver_item, prop):
         elif mantis_node.node_type in ['XFORM',] and mantis_node.__class__.__name__ in ['xFormBone']:
             # TODO: I really shouldn't have to hardcode this. Look into better solutions.
             # NOTE: VERSIONING in Blender 5.0 we have to use pose bone for this, too.
-            if bpy_version <= (5,0,0) and prop  == 'hide': # older blendders, we need the bone
+            # default to pose bone, it's usually what we want
+            bone_col = mantis_node.bGetParentArmature().pose.bones
+            from ..xForm_socket_templates import attributes_shared_by_bone_and_pose_bone
+            if socket_template.prop_owner_group == 'bone':
                 bone_col = mantis_node.bGetParentArmature().data.bones
-            elif prop  == 'show_wire': # TODO check if the bug that forced the above affects this
-                bone_col = mantis_node.bGetParentArmature().data.bones
-            else:
+            # this is a little bit clumsy. TODO clean up here
+            if prop in attributes_shared_by_bone_and_pose_bone:
+                # we want to use the pose bone for this...only if it has a driver
                 bone_col = mantis_node.bGetParentArmature().pose.bones
             driver["owner"] = bone_col[b_object] # we use "unsafe" brackets instead of get() because we want to see any errors that occur
         # HACK having special cases here is indicitave of a deeper problem that should be refactored
@@ -323,11 +361,13 @@ def finish_drivers(mantis_node):
         if mantis_node.node_type == 'LINK':
             b_objects = mantis_node.bObject # this is already a list
         for b_object in b_objects:
+            # Mutating the driver_item here is the WORST THING I HAVE EVER SEEN
+            # HACK UGLY BUG TERRIBLE BAD CODE
             if isinstance(prop, list):
                 for sub_item in prop:
-                        drivers.append(finish_driver(mantis_node, b_object, (driver_item, sub_item[1]), sub_item[0]))
+                        drivers.append(finish_driver(mantis_node, b_object, (driver_item, sub_item[1], 0), sub_item[0]))
                 else:
-                    drivers.append(finish_driver(mantis_node, b_object, (driver_item, sub_item[1]), sub_item[0]))
+                    drivers.append(finish_driver(mantis_node, b_object, (driver_item, sub_item[1], 0), sub_item[0]))
             else:
                 drivers.append(finish_driver(mantis_node, b_object, driver_item, prop))
     from .drivers import CreateDrivers

+ 149 - 68
versioning.py

@@ -7,7 +7,20 @@ from bpy.app import version as bpy_version
 from .utilities.utilities import (prRed, prGreen, prPurple, prWhite,
                               prOrange,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
-                              wrapOrange,)
+                              wrapOrange,
+                              get_socket_maps, do_relink, )
+
+
+# CHECKLIST FOR DUMMIES:
+# Did you make sure to turn off and re-enable tree updates?
+# Did you restore connections to the node socket after changing things?
+# Did you test FUNCTIONALITY in your test rigs instead of just checking if the tree
+#    would generate? CHANGED FUNCTIONALITY IS STILL A BUG!!
+# Did you make sure to toggle the is_updating flag for ANY node that is touched by
+#    the edits you are doing to the current node (example: removing a socket causes
+#    linked group and schema nodes to update)
+# (add more whenever a weird or difficult to debug problem occurs)
+# Did you check the update script on a tree that doesn't need it?
 
 def version_upgrade_very_old(*args, **kwargs):
     node = kwargs['node']
@@ -78,12 +91,10 @@ def version_upgrade_very_old(*args, **kwargs):
                 node.inputs.move(len(node.inputs)-1, index)
         socket_map = None
         if rename_jobs:
-            from .utilities.utilities import get_socket_maps
             socket_maps = get_socket_maps(node)
         for socket, socket_index, new_bl_idname, new_name, multi in rename_jobs:
             old_id = socket.identifier
             print (f"Renaming socket {socket.identifier} to {new_name} in node {node.name}")
-            from .utilities.utilities import do_relink
             if socket.is_output:
                 index = 1
                 in_out = "OUTPUT"
@@ -105,11 +116,13 @@ def version_upgrade_very_old(*args, **kwargs):
         prRed(f"Error updating version in node: {node.id_data.name}::{node.name}; see error:")
         print(e)
 
-
 def version_upgrade_bone_0_12_0_from_older(*args, **kwargs):
     # we need to check if it has an array collection input and a color input
     # then we need to solve each task
     node = kwargs['node']
+    live_updates = node.id_data.do_live_update
+    node.id_data.do_live_update = False
+
     current_major_version = node.id_data.mantis_version[0]
     current_minor_version = node.id_data.mantis_version[1]
     if  current_major_version > 0: # major version must be 0
@@ -117,8 +130,14 @@ def version_upgrade_bone_0_12_0_from_older(*args, **kwargs):
     if current_minor_version >= 12: # minor version must be 11 or less
         return
     # sub version doesn't matter since any subversion of 11 should trigger this task
+    collection_input_is_array = False
     try:
         collection_input_is_array = node.inputs['Bone Collection'].is_multi_input
+    except KeyError: # I suppose this happens in very, very old trees
+        pass         # but it is extremely unlikely
+    toggle_updating = []
+    was_updating = {}
+    try:
         if not collection_input_is_array: # it must be made into an array!
             prPurple(f"Updating \"Bone Collection\" Socket in {node.name}")
             from .utilities.utilities import get_socket_maps
@@ -126,23 +145,19 @@ def version_upgrade_bone_0_12_0_from_older(*args, **kwargs):
             socket_map = socket_maps[0]
             for i, socket in enumerate(node.inputs):
                 if socket.name == 'Bone Collection': break
-            old_id = socket.identifier
-            # it is an input
+            # Always guard socket removal in case of connection to e.g. schema
+            for socket_name, links in socket_map.items():
+                if isinstance(links, list):
+                    for link in links:
+                        if hasattr(link, 'node') and hasattr(link.node, 'is_updating'):
+                            toggle_updating.append(link.node)
+                            was_updating[link.node.name] = link.node.is_updating
+                            link.node.is_updating = True
             node.inputs.remove(socket)
             s = node.inputs.new('BoneCollectionSocket', 'Bone Collection',
                                 identifier='Bone Collection', use_multi_input=True)
             node.inputs.move(len(node.inputs)-1, i)
-            socket_map_from_old_socket = socket_map[old_id]
-            # there seems to be an error in do_relink
-            # gonna do it directly instead
-            if isinstance(socket_map_from_old_socket, list):
-                for map_info in socket_map_from_old_socket:
-                    if isinstance(map_info, Node ):
-                        l = node.id_data.links.new(input=map_info.outputs[0], output=s)
-                    elif isinstance(map_info, NodeSocket):
-                        l = node.id_data.links.new(input=map_info, output=s)
-            else:
-                s.default_value = socket_map_from_old_socket
+            do_relink(node, s, socket_map, in_out='INPUT')
         if node.inputs.get('Color') is None:
             prPurple(f"Adding \"Color\" Socket to {node.name}")
             s = node.inputs.new('ColorSetSocket', 'Color',)
@@ -150,7 +165,11 @@ def version_upgrade_bone_0_12_0_from_older(*args, **kwargs):
     except Exception as e:
         prRed(f"Error updating version in node: {node.id_data.name}::{node.name}; see error:")
         print(e)
-
+    finally:
+        # Now we have to clean up by removing the unneeded sockets and repositioning.
+        for other_node in toggle_updating:
+            other_node.is_updating = was_updating[other_node.name]
+        node.id_data.do_live_update = live_updates
 
 def up_0_12_1_add_inherit_color(*args, **kwargs):
     # add an inherit color input.
@@ -178,7 +197,6 @@ def up_0_12_1_add_inherit_color(*args, **kwargs):
         print(e)
 
 def up_0_12_13_add_widget_scale(*args, **kwargs):
-    # add an inherit color input.
     node = kwargs['node']
     current_major_version = node.id_data.mantis_version[0]
     current_minor_version = node.id_data.mantis_version[1]
@@ -198,34 +216,7 @@ def up_0_12_13_add_widget_scale(*args, **kwargs):
         prRed(f"Error updating version in node: {node.id_data.name}::{node.name}; see error:")
         print(e)
 
-# versioning tasks that involve Blender versions rather than Mantis versions:
-error_description_4_5_0_LTS = 'There is a bug in Blender 4.5.0 LTS, that is why these sockets are blue.'\
-                             ' This will be fixed in future Blender versions.'
-def cleanup_4_5_0_LTS_interface_workaround(*args, **kwargs):
-    # this is a function for cleaning up the workaround up above
-    tree = kwargs['tree']
-    if bpy_version == (4,5,0): return
-    if not hasattr(tree, "interface_helper"):
-        return
-    import json
-    try:
-        interface_helper = json.loads(tree.interface_helper)
-    except:
-        return # this should mean it was never a problem to begin with.
-    prPurple(f"Restoring Tree Interface for {tree.name}.")
-    for interface_item in tree.interface.items_tree:
-        if interface_item.item_type == 'PANEL': continue
-        # now we need to restore the socket types, which are in the interface helper
-        # keep the interface stuff around, I'll use it for marking arrays in the future
-        int_info = interface_helper[interface_item.identifier]
-        socket_type = int_info['bl_socket_idname']
-        interface_item.socket_type = socket_type
-        if interface_item.description == error_description_4_5_0_LTS:
-            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']
     current_major_version = node.id_data.mantis_version[0]
     current_minor_version = node.id_data.mantis_version[1]
@@ -233,7 +224,6 @@ def up_0_12_25_replace_floor_offset_type(*args, **kwargs):
     if  current_major_version > 0: return# major version must be 0
     if current_minor_version > 12: return# minor version must be 12 or less
     if current_minor_version == 12 and current_sub_version > 24: return 
-    from .utilities.utilities import get_socket_maps, do_relink
     socket_maps = get_socket_maps(node)
     prPurple(f"Fixing \"Offset\" socket in {node.name}")
     try:
@@ -246,13 +236,12 @@ 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
+def get_absolute_location(node): # using 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']
@@ -262,8 +251,9 @@ def up_0_13_0_new_custom_prop_node(*args, **kwargs):
     if current_minor_version >= 13: return# minor version must be 12 or less
     remove_me, prop_nodes = [], []
     from .xForm_nodes_ui import xFormBoneNode
+    socket_map_in, socket_map_out = get_socket_maps(node)
     if isinstance(node, xFormBoneNode):
-        for custom_input in node.inputs:
+        for custom_input in reversed(list(node.inputs)):
             if 'Parameter' not in custom_input.bl_idname:
                 continue
             custom_prop_node = tree.nodes.new('UtilityCustomProperty')
@@ -288,10 +278,13 @@ def up_0_13_0_new_custom_prop_node(*args, **kwargs):
             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")
+                try:
+                    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")
+                except AttributeError: # I don't know if I just never implemented these or if it is version specific
+                    pass # anyways this isn't critical and should not trigger even a warning
             elif custom_input.bl_idname == 'ParameterStringSocket':
                 prop_type = 'STRING'
                 custom_prop_node.inputs['Default (String)'].default_value = getattr(custom_input, "default_value")
@@ -299,6 +292,28 @@ def up_0_13_0_new_custom_prop_node(*args, **kwargs):
             custom_prop_node.inputs['Type'].default_value = prop_type
             prop_nodes.append(custom_prop_node)
             remove_me.append(custom_input)
+            # now we need to relink things, because the custom property socket could have had an
+            # input supplying the default value
+            relink_value = socket_map_in.get(custom_input.identifier, None)
+            if isinstance(relink_value, list): # this indicates it is a list of links
+                # CRITICALLY, we need to force display update before doing this.
+                custom_prop_node.display_update({}, None) # parsed_tree and context, not needed
+                input_socket = None
+                match prop_type:
+                    case 'BOOL':
+                        input_socket = "Default (Bool)"
+                    case 'INT':
+                        input_socket = "Default (Int)"
+                    case 'FLOAT':
+                        input_socket = "Default (Float)"
+                    case 'VECTOR':
+                        input_socket = "Default (Vector)"
+                    case 'STRING':
+                        input_socket = "Default (String)"
+                    case _: # Should never never happen
+                        raise RuntimeError("Unexpected Code Path Reached, this is a bug.")
+                do_relink(custom_prop_node, custom_prop_node.inputs[input_socket],
+                          {input_socket:relink_value})
         while remove_me:
             custom_input = remove_me.pop()
             node.inputs.remove(custom_input)
@@ -313,7 +328,6 @@ def up_0_13_0_new_custom_prop_node(*args, **kwargs):
         new_input = node.inputs.new("CustomPropSocket", "Custom Properties", use_multi_input=True)
 
 def schema_enable_custom_interface_types(*args, **kwargs):
-    # return
     tree = kwargs['tree']
     current_major_version = tree.mantis_version[0]
     current_minor_version = tree.mantis_version[1]
@@ -352,37 +366,50 @@ def schema_enable_custom_interface_types(*args, **kwargs):
                 n.is_updating = False
         tree.do_live_update = True
 
-
-
-# since this is basically the same operation, just do it all together
 def update_fcurve_and_armature_node_to_multi_socket(*args, **kwargs):
+    from .base_definitions import SchemaUINode
     node = kwargs['node']
     live_updates = node.id_data.do_live_update
     node.id_data.do_live_update = False
     # First we have to get the socket maps and re-init the sockets
-    from .utilities.utilities import get_socket_maps, do_relink
     convert_keys = {'UtilityFCurve' : [ 'Keyframe', ],
                     'LinkArmature'  : [ 'Target', 'Weight', ], }
     my_convert_keys = convert_keys[node.bl_idname]
     sockets_in, _sockets_out = get_socket_maps(node,)
-    node.inputs.clear()
-    input_templates = [st for st in node.socket_templates if st.is_input == True]
-    node.init_sockets(input_templates)
+    # NOTE IMPORTANT BUG INFO
+    # I was using node.inputs.clear() here before unguarded...
+    # this caused connected Schema nodes to update and mutate the socket map
+    # which, in turn, connected the new socket to the wildcard and extended the schema
+    # resulting in trees that fail to run. This was particularly bad because it was
+    # so hard to debug (due to spooky action at a distance mutation)
+    # So we have to mark the connected nodes as "updating" before we modify the tree.
+    # TODO: should I just mark all nodes as updating during versioning?
+    toggle_updating = []
+    was_updating = {}
+    for socket_name, links in sockets_in.items():
+        if isinstance(links, list):
+            for link in links:
+                if hasattr(link, 'node') and hasattr(link.node, 'is_updating'):
+                    toggle_updating.append(link.node)
+                    was_updating[link.node.name] = link.node.is_updating
+                    link.node.is_updating = True
+    node.inputs.clear() # this is VERY DANGEROUS without the above
+    add_me_sockets = [st for st in node.socket_templates if st.is_input == True]
+    node.init_sockets(add_me_sockets)
     try:
         new_sockets_in = {}
         for convert_key in my_convert_keys:
             for socket_name, links in sockets_in.items():
                 if links is None: continue
                 if convert_key+'.' in socket_name:
-                    print (socket_name, links)
                     if new_sockets_in.get(convert_key+'s'):
                                                     # has to be in this order to come back alright.
                                                     # assuming, that is, that the keys are sorted. they should be.
                         new_sockets_in[convert_key+'s'] = sockets_in[socket_name] + new_sockets_in[convert_key+'s']
                     else:
                         new_sockets_in[convert_key+'s'] = sockets_in[socket_name]
-                    prWhite (new_sockets_in[convert_key+'s'])
-            do_relink(node, node.inputs[convert_key+'s'], new_sockets_in, in_out="INPUT",)
+            if new_sockets_in:
+                do_relink(node, node.inputs[convert_key+'s'], new_sockets_in, in_out="INPUT",)
         for socket_name, links in sockets_in.items():
             proceed = True
             for convert_key in my_convert_keys:
@@ -390,10 +417,65 @@ def update_fcurve_and_armature_node_to_multi_socket(*args, **kwargs):
                     proceed = False
             if proceed:
                 do_relink(node, node.inputs[socket_name], sockets_in, in_out='INPUT')
-
     finally:
+        # Now we have to clean up by removing the unneeded sockets and repositioning.
+        for other_node in toggle_updating:
+            other_node.is_updating = was_updating[other_node.name]
         node.id_data.do_live_update = live_updates
 
+# These BBone Scale-In, Scale-Out properties had the wrong defaults
+# now that they are implemented properly, 
+# since they were not implemented before, we'll do all of them at once.
+# Normally it is wrong to set things directly like this, but they didn't
+# do anything before. Oh well.
+def fix_bbone_default_scale_in_and_out(*args, **kwargs):
+    tree = kwargs['tree']
+    current_major_version = tree.mantis_version[0]
+    current_minor_version = tree.mantis_version[1]
+    current_sub_version = tree.mantis_version[2]
+    if  current_major_version > 0: return# major version must be 0
+    if current_minor_version >= 13: return# minor version must be 12 or less
+    if current_minor_version == 13 and current_sub_version >= 5: return# sub version must be 4 or less
+    prPurple(f"Fixing BBone Settings for {tree.name}")
+    live_updates = tree.do_live_update
+    bones = []
+    for node in tree.nodes:
+        if hasattr(node, 'is_updating'):
+            node.is_updating = True
+        if node.bl_idname == 'xFormBoneNode':
+            bones.append(node)
+    try:
+        for bone_node in bones:
+            bone_node.inputs['BBone Scale-In'].default_value = (1,1,1)
+            bone_node.inputs['BBone Scale-Out'].default_value = (1,1,1)
+    finally:
+        for node in tree.nodes:
+            if hasattr(node, 'is_updating'):
+                node.is_updating = False
+
+# versioning tasks that involve Blender versions rather than Mantis versions:
+error_description_4_5_0_LTS = 'There is a bug in Blender 4.5.0 LTS, that is why these sockets are blue.'\
+                             ' This will be fixed in future Blender versions.'
+def cleanup_4_5_0_LTS_interface_workaround(*args, **kwargs):
+    # this is a function for cleaning up the workaround up above
+    tree = kwargs['tree']
+    if bpy_version == (4,5,0): return
+    if not hasattr(tree, "interface_helper"): return
+    import json
+    try:
+        interface_helper = json.loads(tree.interface_helper)
+    except:
+        return # this should mean it was never a problem to begin with.
+    prPurple(f"Restoring Tree Interface for {tree.name}.")
+    for interface_item in tree.interface.items_tree:
+        if interface_item.item_type == 'PANEL': continue
+        # now we need to restore the socket types, which are in the interface helper
+        # keep the interface stuff around, I'll use it for marking arrays in the future
+        int_info = interface_helper[interface_item.identifier]
+        socket_type = int_info['bl_socket_idname']
+        interface_item.socket_type = socket_type
+        if interface_item.description == error_description_4_5_0_LTS:
+            interface_item.description = ''
 
 versioning_tasks = [
     # node bl_idname    task                required keyword arguments
@@ -407,11 +489,10 @@ versioning_tasks = [
     (['SchemaTree'], schema_enable_custom_interface_types, ['tree']),
     (['UtilityFCurve', 'LinkArmature'],
            update_fcurve_and_armature_node_to_multi_socket, ['node']),
+    (['MantisTree', 'SchemaTree'], fix_bbone_default_scale_in_and_out, ['tree']),
 ]
 
 
-
-
 # WORKAROUNDS for bugs should go here:
 # 4.5.0 LTS valid socket types bug:
 def workaround_4_5_0_interface_update(tree, name, in_out, sock_type, parent_name, do_parent=False):

+ 66 - 136
xForm_nodes.py

@@ -76,6 +76,26 @@ class xFormNode(MantisNode):
     def reset_execution(self):
         super().reset_execution()
         self.prepared=False
+
+    def check_drivers(self):
+        for st in self.socket_templates:
+            if st.is_animatable == False:
+                socket = st.name
+                multi = st.use_multi_input
+                vector = isinstance(st.default_value, tuple)
+                # if single property
+                if not multi and not vector:
+                    has_driver = check_for_driver(self, socket)
+                elif not multi and vector:
+                    for i in range(len(st.default_value)):
+                        has_driver = check_for_driver(self, socket, i)
+                        if has_driver: break
+                elif multi:
+                    for i in range(len(self.inputs[socket].links)):
+                        has_driver = check_for_driver(self, socket, 0, i)
+                        if has_driver: break
+                if has_driver:
+                    raise GraphError(f"Error: Socket \"{socket}\" is not animatable and cannot be driven.")
     
     def setup_custom_props(self, ob, input):
         from .mantis_dataclasses import custom_prop_template
@@ -149,9 +169,13 @@ class xFormArmature(xFormNode):
 
         #check if an object by the name exists
         if (name) and (ob := bpy.data.objects.get(name)):
+            # Clear the drivers from BOTH the object/pose and the skeleton
             if (ob.animation_data):
                 while (ob.animation_data.drivers):
                         ob.animation_data.drivers.remove(ob.animation_data.drivers[-1])
+            if (ob.data.animation_data):
+                while (ob.data.animation_data.drivers):
+                        ob.data.animation_data.drivers.remove(ob.data.animation_data.drivers[-1])
             for pb in ob.pose.bones:
                 # clear it, even after deleting the edit bones,
                 #  if we create them again the pose bones will be reused
@@ -269,6 +293,7 @@ class xFormBone(xFormNode):
             eb.inherit_scale = parent_mantis_node.evaluate_input("Inherit Scale")
 
     def bPrepare(self, bContext=None):
+        self.check_drivers()
         self.parameters['Matrix'] = get_matrix(self)
         self.prepared = True
 
@@ -341,13 +366,9 @@ class xFormBone(xFormNode):
             raise RuntimeError(wrapRed(f"Could not create edit bone: {name} because bone head was located in the same place as bone tail."))
 
         # Setup Deform attributes...
-        eb.use_deform            = self.evaluate_input("Deform")
-        eb.envelope_distance     = self.evaluate_input("Envelope Distance")
-        eb.envelope_weight       = self.evaluate_input("Envelope Weight")
-        eb.use_envelope_multiply = self.evaluate_input("Envelope Multiply")
-        eb.head_radius           = self.evaluate_input("Envelope Head Radius")
-        eb.tail_radius           = self.evaluate_input("Envelope Tail Radius")
-
+        eb_props_sockets = self.gen_property_socket_map(prop_owner_group="edit_bone")
+        evaluate_sockets(self, eb, eb_props_sockets)
+        
         print( wrapGreen("Created Bone: ") + wrapOrange(eb.name) + wrapGreen(" in ") + wrapWhite(self.bGetParentArmature().name))
 
         parent_xForm_info = get_parent_xForm_info(self)
@@ -360,7 +381,6 @@ class xFormBone(xFormNode):
             self_edit_name=eb.name,
         )
         self.parameters['xForm Out'] = my_info
-
         self.executed = True
 
     def set_bone_color(self, b, inherit_color, bContext):
@@ -401,155 +421,68 @@ class xFormBone(xFormNode):
 
 
     def bFinalize(self, bContext = None):
+
         b = self.bGetParentArmature().data.bones[self.bObject]
         # let's do bone colors first
         inherit_color = self.evaluate_input("Inherit Color")
         if len(self.inputs['Color'].links) > 0:
             inherit_color = False # use the link instead
-        # try: # just in case, this shouldn't cause a failure
         self.set_bone_color(b, inherit_color, bContext)
-        # except Exception as e:
-        #     prRed("WARNING: failed to set color because of error, see report below:")
-        #     prOrange(e)
-        #
-        do_bb=False
-        b.bbone_x = self.evaluate_input("BBone X Size"); b.bbone_x = max(b.bbone_x, 0.0002)
-        b.bbone_z = self.evaluate_input("BBone Z Size"); b.bbone_z = max(b.bbone_z, 0.0002)
-        if (segs := self.evaluate_input("BBone Segments")) > 1:
-            do_bb=True
-            b.bbone_segments = segs
-            b.bbone_x = self.evaluate_input("BBone X Size")
-            b.bbone_z = self.evaluate_input("BBone Z Size")
-            if self.evaluate_input("BBone HQ Deformation"):
-                b.bbone_mapping_mode = "CURVED"
-            # 'bbone_handle_type_start'    : ("BBone Start Handle Type", "AUTO"),
-            # 'bbone_handle_type_end'      : ("BBone End Handle Type", "AUTO"),
-            # 'bbone_custom_handle_start'  : ("BBone Custom Start Handle", "AUTO"),
-            # 'bbone_custom_handle_end'    : ("BBone Custom End Handle", "AUTO"),
-            if handle_type := self.evaluate_input("BBone Start Handle Type"):
-                b.bbone_handle_type_start = handle_type
-            if handle_type := self.evaluate_input("BBone End Handle Type"):
-                b.bbone_handle_type_end = handle_type
-
-            try:
-                if (custom_handle := self.evaluate_input("BBone Custom Start Handle")):
-                    b.bbone_custom_handle_start = self.bGetParentArmature().data.bones[custom_handle]
-                if (custom_handle := self.evaluate_input("BBone Custom End Handle")):
-                    b.bbone_custom_handle_end = self.bGetParentArmature().data.bones[custom_handle]
-            except KeyError:
-                prRed("Warning: BBone start or end handle not set because of missing bone in armature.")
-
-            bone_props_socket= {
-                'bbone_curveinx'     : ("BBone X Curve-In", 0.0),
-                'bbone_curveinz'     : ("BBone Z Curve-In", 0.0),
-                'bbone_curveoutx'    : ("BBone X Curve-Out", 0.0),
-                'bbone_curveoutz'    : ("BBone Z Curve-Out", 0.0),
-            }
-            evaluate_sockets(self, b, bone_props_socket)
-            # TODO this section should be done with props-socket thing
-            b.bbone_handle_use_scale_start = self.evaluate_input("BBone Start Handle Scale")
-            b.bbone_handle_use_scale_end = self.evaluate_input("BBone End Handle Scale")
-
+        if self.evaluate_input("BBone HQ Deformation"):
+            b.bbone_mapping_mode = "CURVED"
+        if handle_type := self.evaluate_input("BBone Start Handle Type"):
+            b.bbone_handle_type_start = handle_type
+        if handle_type := self.evaluate_input("BBone End Handle Type"):
+            b.bbone_handle_type_end = handle_type
+        try:
+            if (custom_handle := self.evaluate_input("BBone Custom Start Handle")):
+                b.bbone_custom_handle_start = self.bGetParentArmature().data.bones[custom_handle]
+            if (custom_handle := self.evaluate_input("BBone Custom End Handle")):
+                b.bbone_custom_handle_end = self.bGetParentArmature().data.bones[custom_handle]
+        except KeyError:
+            prRed("Warning: BBone start or end handle not set because of missing bone in armature.")
 
         import bpy
-
         pb = self.bGetParentArmature().pose.bones[self.bObject]
-        rotation_mode = self.evaluate_input("Rotation Order")
-        if rotation_mode == "AUTO": rotation_mode = "XYZ"
-        pb.rotation_mode = rotation_mode
-        pb.id_properties_clear()
-        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
-            props_sockets = {
-            'ik_stretch'          : ("IK Stretch", 0),
-            'lock_ik_x'           : (("Lock IK", 0), False),
-            'lock_ik_y'           : (("Lock IK", 1), False),
-            'lock_ik_z'           : (("Lock IK", 2), False),
-            'ik_stiffness_x'      : (("IK Stiffness", 0), 0.0),
-            'ik_stiffness_y'      : (("IK Stiffness", 1), 0.0),
-            'ik_stiffness_z'      : (("IK Stiffness", 2), 0.0),
-            'use_ik_limit_x'      : (("Limit IK", 0), False),
-            'use_ik_limit_y'      : (("Limit IK", 1), False),
-            'use_ik_limit_z'      : (("Limit IK", 2), False),
-            'ik_min_x'            : ("X Min", 0),
-            'ik_max_x'            : ("X Max", 0),
-            'ik_min_y'            : ("Y Min", 0),
-            'ik_max_y'            : ("Y Max", 0),
-            'ik_min_z'            : ("Z Min", 0),
-            'ik_max_z'            : ("Z Max", 0),
-            }
+        if check_for_driver(self, 'Rotation Order'):
+            # this is stupid and a HACK
+            from .mantis_dataclasses import MantisSocketFiller
+            props_sockets = {"rotation_mode": MantisSocketFiller( socket_name="Rotation Order",
+                property_name="rotation_mode", default_value='XYZ',)}
             evaluate_sockets(self, pb, props_sockets)
-        if do_bb:
-            props_sockets = {
-            'bbone_curveinx'             : ("BBone X Curve-In", pb.bone.bbone_curveinx),
-            'bbone_curveinz'             : ("BBone Z Curve-In", pb.bone.bbone_curveinz),
-            'bbone_curveoutx'            : ("BBone X Curve-Out", pb.bone.bbone_curveoutx),
-            'bbone_curveoutz'            : ("BBone Z Curve-Out", pb.bone.bbone_curveoutz),
-            'bbone_easein'               : ("BBone Ease-In", 0),
-            'bbone_easeout'              : ("BBone Ease-Out", 0),
-            'bbone_rollin'               : ("BBone Roll-In", 0),
-            'bbone_rollout'              : ("BBone Roll-Out", 0),
-            'bbone_scalein'              : ("BBone Scale-In", (1,1,1)),
-            'bbone_scaleout'             : ("BBone Scale-Out", (1,1,1)),
-            }
-            evaluate_sockets(self, pb, props_sockets)
-            # we need to clear this stuff since our only real goal was to get some drivers from the above
-            for attr_name in props_sockets.keys():
-                try:
-                    setattr(pb, attr_name, 0) # just clear it
-                except (ValueError, TypeError): # Blender 5.2 raises TypeError for scalar -> vector
-                    setattr(pb, attr_name, (1.0,1.0,1.0)) # scale needs to be set to 1
-
+        else: # This is stupid, but I did it to myself and it will be annoying to fix
+            rotation_mode = self.evaluate_input("Rotation Order")
+            if rotation_mode == "AUTO": rotation_mode = "XYZ"
+            pb.rotation_mode = rotation_mode
+            
+            
 
-            # important TODO... all of the drivers and stuff should be handled this way, right?
-        # time to set up drivers!
-
-        # just gonna add this to the end and build off it I guess
-        props_sockets = {
-            "lock_location"               : ("Lock Location", [False, False, False]),
-            "lock_rotation"               : ("Lock Rotation", [False, False, False]),
-            "lock_scale"                  : ("Lock Scale", [False, False, False]),
-            'custom_shape_scale_xyz'      : ("Custom Object Scale",  (0.0,0.0,0.0) ),
-            'custom_shape_translation'    : ("Custom Object Translation",  (0.0,0.0,0.0) ),
-            'custom_shape_rotation_euler' : ("Custom Object Rotation",  (0.0,0.0,0.0) ),
-            'use_custom_shape_bone_size'  : ("Custom Object Scale to Bone Length",  True,),
-        }
-        # in 5.0.0 and newer I have to do pose bone, this is an error in < 5.0
-        if bpy_version >=(5,0,0):
-            props_sockets['hide'] = ("Hide", False)
+        pb.id_properties_clear()
+        self.setup_custom_props(pb, "Custom Properties")
+        pb_props_sockets = self.gen_property_socket_map(prop_owner_group='pose_bone')
+        evaluate_sockets(self, pb, pb_props_sockets)
 
-        evaluate_sockets(self, pb, props_sockets)
+        bone_props_socket = self.gen_property_socket_map(prop_owner_group='bone')
+        evaluate_sockets(self, pb.bone, bone_props_socket)
 
-        # this could probably be moved to bTransformPass
-        # TODO VERSIONING: newer (5.0+ ?) Blenders need to use PoseBone for this
-        props_sockets = {
-            'hide'      : ("Hide", False),
-            'show_wire' : ("Custom Object Wireframe", False),
-        }
-        evaluate_sockets(self, pb.bone, props_sockets)
+        # and in edit mode we need to explicitly set the bbone properties
+        # I don't like this HACK but there seems to be no other way...
+        # Better to ask forgiveness than permission. If anyone wants control over this, I'll solve the problem then
+        # IN the near future, in fact, I think I'll make a BBone Rest Pose matrix thing
+        # and allow users to set it from bezier curves
+        pb.bone.bbone_easein = 1; pb.bone.bbone_easeout = 1
 
-        # whatever I was doing there.... was stupid. CLEAN UP TODO
-        # this is the right thing to do.
         finish_drivers(self)
-        #
-        # OK, visual settings
-        #
-        # Get the override xform's bone:
-
-
 
         pb.custom_shape_transform = None
         pb.custom_shape = None
-
         if len(self.inputs["Custom Object xForm Override"].links) > 0:
             trace = trace_single_line(self, "Custom Object xForm Override")
             try:
                 pb.custom_shape_transform = trace[0][1].bGetObject()
             except AttributeError:
                 pass
-
         if len(self.inputs["Custom Object"].links) > 0:
             trace = trace_single_line(self, "Custom Object")
             try:
@@ -668,10 +601,7 @@ class xFormGeometryObject(xFormNode):
         self.has_shape_keys = False
         # putting this in bTransformPass simply prevents it from being run more than once.
         # maybe I should do that with the rest of bPrepare, too.
-        props_sockets = {
-            'hide_viewport'    : ("Hide in Viewport", False),
-            'hide_render'      : ("Hide in Render", False),
-        }
+        props_sockets = self.gen_property_socket_map()
         evaluate_sockets(self, self.bObject, props_sockets)
         self.executed = True
 

+ 17 - 8
xForm_nodes_ui.py

@@ -145,15 +145,24 @@ class xFormBoneNode(Node, xFormNode):
             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 and mantis_node.prepared:
+            mantis_node_ready = mantis_node and mantis_node.prepared
+            if self.inputs['Custom Object'].is_linked and mantis_node_ready:
+                self.display_vp_settings = mantis_node.evaluate_input("Custom Object")
+            if self.inputs['Deform'].is_linked and mantis_node_ready:
+                self.display_def_settings = mantis_node.evaluate_input("Deform")
+            else:
+                self.display_def_settings = self.inputs['Deform'].default_value
+            if self.inputs['BBone Segments'].is_linked and mantis_node_ready:
+                segs = mantis_node.evaluate_input("BBone Segments")
                 try:
-                    self.inputs['Name'].display_text = mantis_node.evaluate_input("Name")
-                    self.display_vp_settings = mantis_node.inputs["Custom Object"].is_connected
-                    self.display_def_settings = mantis_node.evaluate_input("Deform")
-                    self.display_bb_settings = mantis_node.evaluate_input("BBone Segments") > 1
-                except KeyError:
-                    return # the tree isn't ready yet.
-            
+                    self.display_bb_settings = segs > 1
+                except TypeError:
+                    self.display_bb_settings = True # it is probably a driver
+            else:
+                self.display_bb_settings = self.inputs['BBone Segments'].default_value > 1
+            if self.inputs['Name'].is_linked and mantis_node_ready:
+                self.inputs['Name'].display_text = mantis_node.evaluate_input("Name")
+
             for template in self.socket_templates:
                 name = template.name
                 category = template.category

+ 160 - 77
xForm_socket_templates.py

@@ -1,6 +1,7 @@
 from .mantis_dataclasses import MantisSocketTemplate as SockTemplate
 from .misc_nodes_socket_templates import SplineIndexTemplate
 from dataclasses import replace
+from bpy.app import version as bpy_version
 
 xFormArmatureSockets=[
     NameTemplate := SockTemplate(
@@ -16,7 +17,7 @@ xFormArmatureSockets=[
         name="Relationship", is_input=True,  bl_idname='RelationshipSocket', ),
     CustomPropsTemplate := SockTemplate(
         name="Custom Properties", is_input=True,  bl_idname='CustomPropSocket',
-            use_multi_input=True, ),
+            use_multi_input=True, is_animatable=False, ),
     xFormOutTemplate := SockTemplate(
         name="xForm Out", is_input=False,  bl_idname='xFormSocket', ),
 ]
@@ -84,151 +85,233 @@ xFormGetBoneSockets = [
 ]
 
 
+# Some (especially BBone) properties are good to treat differently
+#  based on bone/posebone or value/driver
+# I'll use these lists to make decisions about that sort of thing
+attributes_shared_by_bone_and_pose_bone = [
+    "bbone_curveinx",
+    "bbone_curveinz",
+    "bbone_curveoutx",
+    "bbone_curveoutz",
+    "bbone_custom_handle_end",
+    "bbone_custom_handle_start",
+    "bbone_easein",
+    "bbone_easeout",
+    "bbone_rollin",
+    "bbone_rollout",
+    "bbone_scalein",
+    "bbone_scaleout",
+]
+
+# these need to be 1 in edit mode and 0 in pose mode, by default. Pain.
+need_different_defaults = [
+    ("bbone_easein", 1, 0 ),
+    ("bbone_easeout", 1, 0),
+]
+
+
+# VERSIONING for Bone
+bone_hide_prop_group='bone'
+if bpy_version >= (5,0,0):
+    bone_hide_prop_group='pose_bone'
+bone_hide_template = replace(HideTemplate, name='Hide', category='always_show',
+                            blender_property='hide', default_value=False,
+                            prop_owner_group = bone_hide_prop_group)
+
+bbone_template = SockTemplate( name="<BBone Template>",
+        default_value = 0.0, blender_property=None, is_input=True,
+        bl_idname='FloatSocket', prop_owner_group='bone',)
+
+# And there is some serious confusing stuff regarding BBones so I'll explain...
+# Blender has duplicated properties for Bone and Pose Bone
+# When there is a driver, you usually want the Pose Bone to hold it
+# but when there isn't you probably want it on the Bone since it isn't animated
+
 # and bones! this one is a bit much...
 from math import pi
 xFormBoneSockets = [
-    replace(NameTemplate, default_value='Bone', category='always_show'),
-    replace(RotationOrderTemplate, category='always_show'),
-    BoneMatrixTemplate := replace(ObjectMatrixTemplate,
-            blender_property='matrix', category='always_show'),
-    replace(RelationshipInTemplate, category='always_show'),
+    replace(NameTemplate, default_value='Bone', category='always_show',
+            is_animatable=False,),
+    replace(RotationOrderTemplate, category='always_show',),
+    BoneMatrixTemplate := replace(ObjectMatrixTemplate, is_animatable=False,
+            blender_property='matrix', category='always_show',),
+    replace(RelationshipInTemplate, category='always_show', is_animatable=False,),
     # IK
     IKStretch := SockTemplate(
         name="IK Stretch", is_input=True,  bl_idname='FloatFactorSocket',
-        default_value=0, blender_property='ik_stretch', category='IK'),
+        default_value=0, blender_property='ik_stretch', category='IK',
+        prop_owner_group='pose_bone',),
     IKLock := SockTemplate(
         name="Lock IK", is_input=True,  bl_idname='BooleanThreeTupleSocket',
         default_value=(False, False, False), category='IK',
-        blender_property=('lock_ik_x', 'lock_ik_y', 'lock_ik_z'),),
+        blender_property=('lock_ik_x', 'lock_ik_y', 'lock_ik_z'),
+        prop_owner_group='pose_bone', ),
     IKStiffness := SockTemplate(
         name="IK Stiffness", is_input=True,  bl_idname='BooleanThreeTupleSocket',
         default_value=(False, False, False), category='IK',
-        blender_property=('ik_stiffness_x', 'ik_stiffness_y', 'ik_stiffness_z')),
+        blender_property=('ik_stiffness_x', 'ik_stiffness_y', 'ik_stiffness_z'),
+        prop_owner_group='pose_bone', ),
     IKLimit := SockTemplate(
         name="Limit IK", is_input=True,  bl_idname='BooleanThreeTupleSocket',
         default_value=(False, False, False), category='IK',
-        blender_property=('use_ik_limit_x', 'use_ik_limit_y', 'use_ik_limit_z')),
+        blender_property=('use_ik_limit_x', 'use_ik_limit_y', 'use_ik_limit_z'),
+        prop_owner_group='pose_bone', ),
     IKXMin := SockTemplate(
         name="X Min", is_input=True,  bl_idname='NodeSocketFloatAngle',
-        default_value=-pi, blender_property='ik_min_x', category='IK'),
+        default_value=-pi, blender_property='ik_min_x', category='IK',
+        prop_owner_group='pose_bone', ),
     IKXMax := replace(IKXMin, name="X Max", default_value=pi,
-                      blender_property='ik_max_x'),
+                      blender_property='ik_max_x',),
     IKYMax := replace(IKXMin, name="Y Min", blender_property='ik_min_y'),
     IKYMax := replace(IKXMax, name="Y Max", blender_property='ik_max_y'),
     IKZMax := replace(IKXMin, name="Z Min", blender_property='ik_min_z'),
     IKZMax := replace(IKXMax, name="Z Max", blender_property='ik_max_z'),
     # Visual Stuff
     BoneCollectionTemplate := SockTemplate(name="Bone Collection", is_input=True,
-            bl_idname='BoneCollectionSocket', category='Display', use_multi_input=True),
+            bl_idname='BoneCollectionSocket', category='Display', use_multi_input=True,
+            is_animatable=False,),
     CustomObjectTemplate := replace(SourcexFormTemplate, name='Custom Object',
-                                    category='Display'),
-    OverrideXFormTemplate := replace(SourcexFormTemplate,
-                    name='Custom Object xForm Override', category='Display'),
+        category='Display', is_animatable=False,),
+    OverrideXFormTemplate := replace(SourcexFormTemplate, is_animatable=False,
+        name='Custom Object xForm Override', category='Display',),
     CustomObjectScaleToBoneTemplate := replace(HideRenderTemplate,
-            name='Custom Object Scale to Bone Length', category='Display',
-            blender_property='use_custom_shape_bone_size',
-            default_value=True,),
+        name='Custom Object Scale to Bone Length', category='Display',
+        blender_property='use_custom_shape_bone_size',
+        default_value=True, prop_owner_group='pose_bone', ),
     CustomObjectWireframeTemplate := replace(HideRenderTemplate,
             name='Custom Object Wireframe', category='Display',
-            blender_property='show_wire', default_value=True,),
+            blender_property='show_wire', default_value=True,
+            prop_owner_group='bone', ),
     CustomObjectScaleTemplate := SockTemplate(name="Custom Object Scale",
             is_input=True, bl_idname='VectorScaleSocket',
-            category='Display', default_value=(1.0,1.0,1.0),),
+            category='Display', default_value=(1.0,1.0,1.0),
+            blender_property = 'custom_shape_scale_xyz',
+            prop_owner_group='pose_bone', ),
     CustomObjectTranslationTemplate := SockTemplate(name="Custom Object Translation",
             is_input=True, bl_idname='VectorSocket',
-            category='Display', default_value=(0.0,0.0,0.0),),
+            category='Display', default_value=(0.0,0.0,0.0),
+            blender_property = 'custom_shape_translation',
+            prop_owner_group='pose_bone', ),
     CustomObjectRotationTemplate := SockTemplate(name="Custom Object Rotation",
             is_input=True, bl_idname='VectorEulerSocket',
-            category='Display', default_value=(0.0,0.0,0.0),),
+            category='Display', default_value=(0.0,0.0,0.0),
+            blender_property = 'custom_shape_rotation_euler',
+            prop_owner_group='pose_bone', ),
     CustomColor := SockTemplate(name="Color",
             is_input=True, bl_idname='ColorSetSocket',
-            category='Display',),
+            category='Display', is_animatable=False,),
     InheritColor := SockTemplate(name="Inherit Color",
-            is_input=True, bl_idname='BooleanSocket',
-            category='Display', default_value=True),
+            is_input=True, bl_idname='BooleanSocket', is_animatable=False,
+            category='Display', default_value=True, prop_owner_group='pose_bone', ),
     # Deform Stuff
     BoneDeformTemplate := replace(HideRenderTemplate, name='Deform',
-        category='Deform', blender_property='use_deform', default_value=False,),
+        category='Deform', blender_property='use_deform', default_value=False,
+        prop_owner_group='edit_bone', is_animatable=False,),
     EnvelopeDistanceTemplate := SockTemplate( name='Envelope Distance',
-            bl_idname='FloatPositiveSocket', category='Deform',
-            blender_property='envelope_distance', default_value=0,
-            is_input=True,),
-    EnvelopeWeightTemplate := replace(EnvelopeDistanceTemplate,
-            name='Envelope Weight', bl_idname = 'FloatFactorSocket',
-            blender_property='envelope_weight', default_value=1.0,),
-    EnvelopeWeightTemplate := replace(EnvelopeDistanceTemplate,
-            name='Envelope Multiply', bl_idname = 'BooleanSocket',
-            blender_property='use_envelope_multiply', default_value=False,),
+        bl_idname='FloatPositiveSocket', category='Deform',
+        blender_property='envelope_distance', default_value=0,
+        is_input=True, prop_owner_group='edit_bone', is_animatable=False,),
     EnvelopeWeightTemplate := replace(EnvelopeDistanceTemplate,
+        name='Envelope Weight', bl_idname = 'FloatFactorSocket',
+        blender_property='envelope_weight', default_value=1.0,
+        prop_owner_group='edit_bone', is_animatable=False,),
+    EnvelopeMultiplyTemplate := replace(EnvelopeDistanceTemplate,
+        name='Envelope Multiply', bl_idname = 'BooleanSocket',
+        blender_property='use_envelope_multiply', default_value=False,
+        prop_owner_group='edit_bone', is_animatable=False,),
+    EnvelopeHeadRadiusTemplate := replace(EnvelopeDistanceTemplate,
         name='Envelope Head Radius', blender_property='head_radius', default_value=0.0,),
-    EnvelopeWeightTemplate := replace(EnvelopeDistanceTemplate,
+    EnvelopeTailRadiusTemplate := replace(EnvelopeDistanceTemplate,
         name='Envelope Tail Radius', blender_property='tail_radius', default_value=0.0,),
     # BBone Stuff
     BBoneSegmentsTemplate := SockTemplate(name="BBone Segments", is_input=True,
         bl_idname='UnsignedIntSocket', category = 'bbone',
-        blender_property='bbone_segments', default_value=1 ),
+        blender_property='bbone_segments', default_value=1,
+        prop_owner_group='bone', ),
     BBoneXSizeTemplate := replace(BBoneSegmentsTemplate, name='BBone X Size',
-        bl_idname='FloatSocket', blender_property='bbone_x', default_value=0.0025, ),
+        bl_idname='FloatSocket', blender_property='bbone_x', default_value=0.0025,
+        prop_owner_group='bone',),
     BBoneZSizeTemplate := replace(BBoneXSizeTemplate, name='BBone Z Size',
         blender_property='bbone_z', ),
     BBoneHQDeformation := replace(BBoneSegmentsTemplate, name='BBone HQ Deformation',
-        bl_idname='BooleanSocket', blender_property='', default_value=None ),
+        bl_idname='BooleanSocket', blender_property='', default_value=None, is_animatable=False, ),
     BBoneXCurveInTemplate := replace( BBoneXSizeTemplate, name="BBone X Curve-In",
-        bl_idname='FloatSocket', blender_property='bbone_curveinx', default_value=0.0, ),
+        bl_idname='FloatSocket', blender_property='bbone_curveinx', default_value=0.0,
+        prop_owner_group='bone',),
     BBoneZCurveInTemplate := replace(BBoneXCurveInTemplate, name="BBone Z Curve-In",
-        blender_property='bbone_curveinz', ),
+        blender_property='bbone_curveinz', prop_owner_group='bone', ),
     BBoneXCurveOutTemplate := replace(BBoneXCurveInTemplate, name="BBone X Curve-Out",
-        blender_property='bbone_curveoutx', ),
+                                      default_value=0.0,
+        blender_property='bbone_curveoutx', prop_owner_group='bone', ),
     BBoneZCurveOutTemplate := replace(BBoneXCurveInTemplate, name="BBone Z Curve-Out",
-        blender_property='bbone_curveoutz', ),  # I'm tired of assigning variables, not gonna bother anymore lol
-                                                # it's just a conincidence that a lot of these are also unimplemented
-    replace(BBoneXCurveInTemplate, name="BBone Roll-In", blender_property='bbone_rollin', ), # CURRENTLY UNIMPLEMENTED
-    replace(BBoneXCurveInTemplate, name="BBone Roll-Out", blender_property='bbone_rollout', ), # CURRENTLY UNIMPLEMENTED
-    replace(BBoneXCurveInTemplate, name="BBone Inherit End Roll",
-            bl_idname='BooleanSocket', default_value=False,), # CURRENTLY UNIMPLEMENTED
-    replace(BBoneXCurveInTemplate, name="BBone Scale-In",
-            bl_idname='VectorSocket', default_value=(0.0,0.0,0.0),), # CURRENTLY DOESN'T WORK
-    replace(BBoneXCurveInTemplate, name="BBone Scale-Out",
-            bl_idname='VectorSocket', default_value=(0.0,0.0,0.0)), # CURRENTLY DOESN'T WORK
-    replace(BBoneXCurveInTemplate, name="BBone Ease-In",), # CURRENTLY DOESN'T WORK
-    replace(BBoneXCurveInTemplate, name="BBone Ease-Out",), # CURRENTLY DOESN'T WORK
-    replace(BBoneXCurveInTemplate, name="BBone Easing",
-            bl_idname='BooleanSocket', default_value=False,), # CURRENTLY DOESN'T WORK
-    replace(BBoneXCurveInTemplate, name="BBone Start Handle Type",
+        blender_property='bbone_curveoutz', prop_owner_group='bone', ), 
+    replace(bbone_template, name="BBone Roll-In",
+            default_value = 0.0,
+            blender_property='bbone_rollin',),
+    replace(bbone_template, name="BBone Roll-Out", blender_property='bbone_rollout', ),
+    replace(bbone_template, name="BBone Inherit End Roll",
+            bl_idname='BooleanSocket', default_value=False,
+            blender_property = 'use_endroll_as_inroll',
+            is_animatable=False, prop_owner_group='bone',),
+    replace(bbone_template, name="BBone Scale-In",
+            bl_idname='VectorSocket', default_value=(1.0,1.0,1.0),
+            blender_property = 'bbone_scalein'),
+    replace(bbone_template, name="BBone Scale-Out",
+            bl_idname='VectorSocket', default_value=(1.0,1.0,1.0),
+            blender_property = 'bbone_scaleout'),
+    replace(bbone_template, name="BBone Ease-In",
+            blender_property = 'bbone_easein'),
+    replace(bbone_template, name="BBone Ease-Out",
+            blender_property = 'bbone_easeout'),
+    replace(bbone_template, name="BBone Easing",
+            bl_idname='BooleanSocket', default_value=False,
+            blender_property='use_scale_easing',  prop_owner_group='bone',),
+    replace(bbone_template, name="BBone Start Handle Type",
             bl_idname="EnumBBoneHandleType",
-            blender_property='bbone_handle_type_start', default_value=None, ),
-    replace(BBoneXCurveInTemplate, name="BBone Custom Start Handle",
+            blender_property='bbone_handle_type_start', default_value=None,
+            prop_owner_group='bone', is_animatable=False,),
+    replace(bbone_template, name="BBone Custom Start Handle",
             bl_idname="StringSocket",
-            blender_property='bbone_custom_handle_start', default_value=None, ),
-    replace(BBoneXCurveInTemplate, name="BBone Start Handle Scale",
+            blender_property=None, # not handled by this system
+            default_value=None, is_animatable=False,),
+    replace(bbone_template, name="BBone Start Handle Scale",
             bl_idname="BooleanThreeTupleSocket",
-            blender_property='bbone_handle_use_scale_start', default_value=None, ),
-    replace(BBoneXCurveInTemplate, name="BBone Start Handle Ease",
-            bl_idname='BooleanSocket', default_value=False),# CURRENTLY DOESN'T WORK),
-    replace(BBoneXCurveInTemplate, name="BBone End Handle Type",
+            default_value = (False, False, False),
+            blender_property='bbone_handle_use_scale_start',
+            prop_owner_group='bone', ),
+    replace(bbone_template, name="BBone Start Handle Ease",
+            bl_idname='BooleanSocket', default_value=False,
+            blender_property = 'bbone_handle_use_ease_start',
+            prop_owner_group='bone',),
+    replace(bbone_template, name="BBone End Handle Type",
             bl_idname="EnumBBoneHandleType",
-            blender_property='bbone_handle_type_end', default_value=None, ),
-    replace(BBoneXCurveInTemplate, name="BBone Custom End Handle",
+            blender_property='bbone_handle_type_end', default_value=None,
+            prop_owner_group='bone', is_animatable=False,),
+    replace(bbone_template, name="BBone Custom End Handle",
             bl_idname="StringSocket",
-            blender_property='bbone_custom_handle_end', default_value=None, ),
-    replace(BBoneXCurveInTemplate, name="BBone End Handle Scale",
+            blender_property=None, # not handled by this system
+            default_value=None, is_animatable=False,),
+    replace(bbone_template, name="BBone End Handle Scale",
             bl_idname="BooleanThreeTupleSocket",
+            default_value= (False, False, False),
             blender_property='bbone_handle_use_scale_end',
-            default_value= (False, False, False)),
-    replace(BBoneXCurveInTemplate, name="BBone End Handle Ease",
-            bl_idname='BooleanSocket', default_value=False),# CURRENTLY DOESN'T WORK),
+            prop_owner_group='bone',),
+    replace(bbone_template, name="BBone End Handle Ease",
+            bl_idname='BooleanSocket', default_value=False,
+            blender_property = 'bbone_handle_use_ease_end',
+            prop_owner_group='bone',),
     # locks
     LockLocationTemplate := SockTemplate(name="Lock Location",
         is_input=True, bl_idname='BooleanThreeTupleSocket', category = 'lock',
-        blender_property='lock_location', default_value=[True, True, True] ),
+        blender_property='lock_location', default_value=[True, True, True],
+        prop_owner_group='pose_bone', ),
     LockRotationTemplate := replace(LockLocationTemplate, name="Lock Rotation",
         blender_property='lock_rotation',),
     LockRotationTemplate := replace(LockLocationTemplate, name="Lock Scale",
         blender_property='lock_scale',),
     # hide
-    replace(HideTemplate, name='Hide', category='always_show',
-            blender_property='hide', default_value=False,),
+    bone_hide_template,
     CustomPropsTemplate,
     # Output
     xFormOutTemplate,
-]
+]