Browse Source

All Link nodes now apply to all outgoing xForm connections

so from the beginning my intention was for Link nodes to add
constraints to every xForm node connected downstream
however, up until now it was too hard to figure out or I just didnt
notice it. anyways it only affected the first xForm in any case

now it affects all xForm nodes, whether a constraint or deformer.

the result is now I no longer need redundant node paths when e.g.
a pair of bones have the same constraints or two meshes have the
same deformers.
Joseph Brandenburg 6 months ago
parent
commit
6c6c12bf2a
4 changed files with 501 additions and 556 deletions
  1. 1 1
      __init__.py
  2. 135 153
      deformer_containers.py
  3. 329 361
      link_containers.py
  4. 36 41
      node_container_common.py

+ 1 - 1
__init__.py

@@ -16,7 +16,7 @@ from .utilities import prRed
 
 
 MANTIS_VERSION_MAJOR=0
 MANTIS_VERSION_MAJOR=0
 MANTIS_VERSION_MINOR=10
 MANTIS_VERSION_MINOR=10
-MANTIS_VERSION_SUB=9
+MANTIS_VERSION_SUB=11
 
 
 classLists = [module.TellClasses() for module in [
 classLists = [module.TellClasses() for module in [
  link_definitions,
  link_definitions,

+ 135 - 153
deformer_containers.py

@@ -1,5 +1,5 @@
 from .node_container_common import *
 from .node_container_common import *
-from .xForm_containers import xFormGeometryObject
+from .xForm_containers import xFormGeometryObject, xFormObjectInstance
 from .misc_nodes import InputExistingGeometryObject
 from .misc_nodes import InputExistingGeometryObject
 from .base_definitions import MantisNode, MantisSocketTemplate
 from .base_definitions import MantisNode, MantisSocketTemplate
 from .utilities import (prRed, prGreen, prPurple, prWhite, prOrange,
 from .utilities import (prRed, prGreen, prPurple, prWhite, prOrange,
@@ -19,26 +19,17 @@ def TellClasses():
              DeformerMorphTargetDeform,
              DeformerMorphTargetDeform,
            ]
            ]
 
 
+# object instance probably can't use the deformer but it doesn't hurt to try.
+deformable_types= (xFormGeometryObject, InputExistingGeometryObject, xFormObjectInstance)
 
 
 def trace_xForm_back(nc, socket):
 def trace_xForm_back(nc, socket):
-    from .xForm_containers import xFormGeometryObject
-    from .misc_nodes import InputExistingGeometryObject
-    from bpy.types import Object
     if (trace := trace_single_line(nc, socket)[0] ) :
     if (trace := trace_single_line(nc, socket)[0] ) :
         for i in range(len(trace)): # have to look in reverse, actually
         for i in range(len(trace)): # have to look in reverse, actually
-            if ( isinstance(trace[ i ], xFormGeometryObject ) ) or ( isinstance(trace[ i ], InputExistingGeometryObject ) ):
+            if ( isinstance(trace[ i ], deformable_types ) ):
                 return trace[ i ].bGetObject()
                 return trace[ i ].bGetObject()
         raise GraphError(wrapRed(f"No other object found for {nc}."))
         raise GraphError(wrapRed(f"No other object found for {nc}."))
 
 
 
 
-# semi-duplicated from link_containers
-def GetxForm(nc):
-    trace = trace_single_line_up(nc, "Deformer")
-    for node in trace[0]:
-        if (node.__class__ in [xFormGeometryObject, InputExistingGeometryObject]):
-            return node
-    raise GraphError("%s is not connected to a downstream xForm" % nc)
-
 class MantisDeformerNode(MantisNode):
 class MantisDeformerNode(MantisNode):
     def __init__(self, signature : tuple,
     def __init__(self, signature : tuple,
                  base_tree : NodeTree,
                  base_tree : NodeTree,
@@ -46,6 +37,7 @@ class MantisDeformerNode(MantisNode):
         super().__init__(signature, base_tree, socket_templates)
         super().__init__(signature, base_tree, socket_templates)
         self.node_type = 'LINK'
         self.node_type = 'LINK'
         self.prepared = True
         self.prepared = True
+        self.bObject=[]
     # we need evaluate_input to have the same behaviour as links.
     # we need evaluate_input to have the same behaviour as links.
     def evaluate_input(self, input_name, index=0):
     def evaluate_input(self, input_name, index=0):
         if ('Target' in input_name):
         if ('Target' in input_name):
@@ -56,6 +48,16 @@ class MantisDeformerNode(MantisNode):
             
             
         else:
         else:
             return super().evaluate_input(input_name, index)
             return super().evaluate_input(input_name, index)
+    
+    def GetxForm(nc, output_name="Deformer"):
+        break_condition= lambda node : node.__class__ in deformable_types
+        xforms = trace_line_up_branching(nc, output_name, break_condition)
+        return_me=[]
+        for xf in xforms:
+            if xf.node_type != 'XFORM':
+                continue
+            return_me.append(xf)
+        return return_me
 
 
 class DeformerArmature(MantisDeformerNode):
 class DeformerArmature(MantisDeformerNode):
     '''A node representing an armature deformer'''
     '''A node representing an armature deformer'''
@@ -88,7 +90,7 @@ class DeformerArmature(MantisDeformerNode):
 
 
     def GetxForm(self, socket="Deformer"):
     def GetxForm(self, socket="Deformer"):
         if socket == "Deformer":
         if socket == "Deformer":
-            return GetxForm(self)
+            return super().GetxForm()
         else:
         else:
             trace_xForm_back(self, socket)
             trace_xForm_back(self, socket)
     
     
@@ -113,8 +115,8 @@ class DeformerArmature(MantisDeformerNode):
     def bExecute(self, bContext = None,):
     def bExecute(self, bContext = None,):
         self.executed = True
         self.executed = True
     
     
-    def initialize_vgroups(self,):
-        ob = self.GetxForm().bGetObject()
+    def initialize_vgroups(self, xf):
+        ob = xf.bGetObject()
         armOb = self.bGetParentArmature()
         armOb = self.bGetParentArmature()
         for b in armOb.data.bones:
         for b in armOb.data.bones:
             if b.use_deform == False:
             if b.use_deform == False:
@@ -125,10 +127,10 @@ class DeformerArmature(MantisDeformerNode):
                 num_verts = len(ob.data.vertices)
                 num_verts = len(ob.data.vertices)
                 vg.add(range(num_verts), 0, 'REPLACE')
                 vg.add(range(num_verts), 0, 'REPLACE')
     
     
-    def copy_weights(self):
+    def copy_weights(self, xf):
         # we'll use modifiers for this, maybe use GN for it in the future tho
         # we'll use modifiers for this, maybe use GN for it in the future tho
         import bpy
         import bpy
-        ob = self.GetxForm().bGetObject()
+        ob = xf.bGetObject()
         try:
         try:
             copy_from = self.GetxForm(socket="Copy Skin Weights From")
             copy_from = self.GetxForm(socket="Copy Skin Weights From")
         except GraphError:
         except GraphError:
@@ -162,64 +164,55 @@ class DeformerArmature(MantisDeformerNode):
     def bFinalize(self, bContext=None):
     def bFinalize(self, bContext=None):
         prGreen("Executing Armature Deform Node")
         prGreen("Executing Armature Deform Node")
         mod_name = self.evaluate_input("Name")
         mod_name = self.evaluate_input("Name")
-        d = self.GetxForm().bGetObject().modifiers.new(mod_name, type='ARMATURE')
-        if d is None:
-            raise RuntimeError(f"Modifier was not created in node {self} -- the object is invalid.")
-        self.bObject = 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),
-        }
-        evaluate_sockets(self, d, props_sockets)
-        #
-        if (skin_method := self.evaluate_input("Skinning Method")) == "AUTOMATIC_HEAT":
-            # This is bad and leads to somewhat unpredictable
-            #  behaviour, e.g. what object will be selected? What mode?
-            # also bpy.ops is ugly and prone to error when used in
-            #  scripts. I don't intend to use bpy.ops when I can avoid it.
-            import bpy
-            self.initialize_vgroups()
-            bContext.view_layer.depsgraph.update()
-            ob = self.GetxForm().bGetObject()
-            armOb = self.bGetParentArmature()
-            deform_bones = []
-            for pb in armOb.pose.bones:
-                if pb.bone.use_deform == True:
-                    deform_bones.append(pb)
-            
-            context_override = {
-                                  'active_object':ob,
-                                  'selected_objects':[ob, armOb],
-                                  'active_pose_bone':deform_bones[0],
-                                  'selected_pose_bones':deform_bones,}
+        for xf in self.GetxForm():
+            ob = xf.bGetObject()
+            d = ob.modifiers.new(mod_name, type='ARMATURE')
+            if d is None:
+                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),
+            }
+            evaluate_sockets(self, d, props_sockets)
             #
             #
-            # with bContext.temp_override(**{'active_object':armOb}):
-            #     bpy.ops.object.mode_set(mode='POSE')
-            #     bpy.ops.pose.select_all(action='SELECT')
-            for b in armOb.data.bones:
-                b.select = True
-            with bContext.temp_override(**context_override):
-                bpy.ops.paint.weight_paint_toggle()
-                bpy.ops.paint.weight_from_bones(type='AUTOMATIC')
-                bpy.ops.paint.weight_paint_toggle()
-            for b in armOb.data.bones:
-                b.select = False
-                #
-            # with bContext.temp_override(**{'active_object':armOb}):
-            #     bpy.ops.object.mode_set(mode='POSE')
-            #     bpy.ops.pose.select_all(action='DESELECT') 
-            #     bpy.ops.object.mode_set(mode='OBJECT')
-            # TODO: modify Blender to make this available as a Python API function.
-        elif skin_method == "EXISTING_GROUPS":
-            pass
-        elif skin_method == "COPY_FROM_OBJECT":
-            self.initialize_vgroups()
-            self.copy_weights()
+            if (skin_method := self.evaluate_input("Skinning Method")) == "AUTOMATIC_HEAT":
+                # This is bad and leads to somewhat unpredictable
+                #  behaviour, e.g. what object will be selected? What mode?
+                # also bpy.ops is ugly and prone to error when used in
+                #  scripts. I don't intend to use bpy.ops when I can avoid it.
+                import bpy
+                self.initialize_vgroups(xf)
+                bContext.view_layer.depsgraph.update()
+                armOb = self.bGetParentArmature()
+                deform_bones = []
+                for pb in armOb.pose.bones:
+                    if pb.bone.use_deform == True:
+                        deform_bones.append(pb)
+                context_override = {
+                                    'active_object':ob,
+                                    'selected_objects':[ob, armOb],
+                                    'active_pose_bone':deform_bones[0],
+                                    'selected_pose_bones':deform_bones,}
+                for b in armOb.data.bones:
+                    b.select = True
+                with bContext.temp_override(**context_override):
+                    bpy.ops.paint.weight_paint_toggle()
+                    bpy.ops.paint.weight_from_bones(type='AUTOMATIC')
+                    bpy.ops.paint.weight_paint_toggle()
+                for b in armOb.data.bones:
+                    b.select = False
+                # TODO: modify Blender to make this available as a Python API function.
+            elif skin_method == "COPY_FROM_OBJECT":
+                self.initialize_vgroups(xf)
+                self.copy_weights(xf)
+            # elif skin_method == "EXISTING_GROUPS":
+            #     pass
 
 
 
 
 class DeformerHook(MantisDeformerNode):
 class DeformerHook(MantisDeformerNode):
@@ -278,12 +271,6 @@ class DeformerHook(MantisDeformerNode):
                 var1['channel']="SCALE_"+axes[i]
                 var1['channel']="SCALE_"+axes[i]
                 driver['vars'].append(var1)
                 driver['vars'].append(var1)
         CreateDrivers([driver])
         CreateDrivers([driver])
-
-    def GetxForm(self, socket="Deformer"):
-        if socket == "Deformer":
-            return GetxForm(self)
-        else:
-            trace_xForm_back(self, socket)
             
             
     def bExecute(self, bContext = None,):
     def bExecute(self, bContext = None,):
         self.executed = True
         self.executed = True
@@ -299,62 +286,62 @@ class DeformerHook(MantisDeformerNode):
         props_sockets = self.gen_property_socket_map()
         props_sockets = self.gen_property_socket_map()
         if isinstance(target, Bone) or isinstance(target, PoseBone):
         if isinstance(target, Bone) or isinstance(target, PoseBone):
             subtarget = target.name; target = target.id_data
             subtarget = target.name; target = target.id_data
-        ob=self.GetxForm().bGetObject()
-
-        if ob.type == 'CURVE':
-            spline_index = self.evaluate_input("Spline Index")
-            from .utilities import get_extracted_spline_object
-            ob = get_extracted_spline_object(ob, spline_index, self.mContext)
-        
-        reuse = False
-        for m in ob.modifiers:
-            if  m.type == 'HOOK' and m.object == target and m.subtarget == subtarget:
-                if self.evaluate_input("Influence") != m.strength:
-                    continue # make a new modifier so they can have different strengths
-                if ob.animation_data: # this can be None
-                    drivers = ob.animation_data.drivers
-                    for k in props_sockets.keys():
-                        if driver := drivers.find(k):
-                            # TODO: I should check to see if the drivers are the same...
-                            break # continue searching for an equivalent modifier
-                    else: # There was no driver - use this one.
-                        d = m; reuse = True; break
-                else: # use this one, there can't be drivers without animation_data.
-                    d = m; reuse = True; break
-        else:
-            d = ob.modifiers.new(mod_name, type='HOOK')
-            if d is None:
-                raise RuntimeError(f"Modifier was not created in node {self} -- the object is invalid.")
-        self.bObject = d
-        self.get_target_and_subtarget(d, input_name="Hook Target")
-        vertices_used=[]
-        if reuse: # Get the verts in the list... filter out all the unneeded 0's
-            vertices_used = list(d.vertex_indices)
-            include_0 = 0 in vertices_used
-            vertices_used = list(filter(lambda a : a != 0, vertices_used))
-            if include_0: vertices_used.append(0)
-        # now we add the selected vertex to the list, too
-        vertex = self.evaluate_input("Point Index")
-        if ob.type == 'CURVE' and ob.data.splines[0].type == 'BEZIER' and auto_bezier:
-            if affect_radius:
-                self.driver_for_radius(ob, target_node.bGetObject(), vertex, d.strength)
-            vertex*=3
-            vertices_used.extend([vertex, vertex+1, vertex+2])
-        else:
-            vertices_used.append(vertex)
-        # if we have a curve and it is NOT using auto-bezier for the verts..
-        if ob.type == 'CURVE' and ob.data.splines[0].type == 'BEZIER' and affect_radius and not auto_bezier:
-            print (f"WARN: {self}: \"Affect Radius\" may not behave as expected"
-                    " when used on Bezier curves without Auto-Bezier")
-            #bezier point starts at 1, and then every third vert, so 4, 7, 10...
-            if vertex%3==1:
-                self.driver_for_radius(ob, target_node.bGetObject(), vertex, d.strength)
-        if ob.type == 'CURVE' and ob.data.splines[0].type != 'BEZIER' and \
-                    affect_radius:
-            self.driver_for_radius(ob, target_node.bGetObject(), vertex, d.strength, bezier=False)
+        for xf in self.GetxForm():
+            ob=xf.bGetObject()
+            if ob.type == 'CURVE':
+                spline_index = self.evaluate_input("Spline Index")
+                from .utilities import get_extracted_spline_object
+                ob = get_extracted_spline_object(ob, spline_index, self.mContext)
             
             
-        d.vertex_indices_set(vertices_used)
-        evaluate_sockets(self, d, props_sockets)
+            reuse = False
+            for m in ob.modifiers:
+                if  m.type == 'HOOK' and m.object == target and m.subtarget == subtarget:
+                    if self.evaluate_input("Influence") != m.strength:
+                        continue # make a new modifier so they can have different strengths
+                    if ob.animation_data: # this can be None
+                        drivers = ob.animation_data.drivers
+                        for k in props_sockets.keys():
+                            if driver := drivers.find(k):
+                                # TODO: I should check to see if the drivers are the same...
+                                break # continue searching for an equivalent modifier
+                        else: # There was no driver - use this one.
+                            d = m; reuse = True; break
+                    else: # use this one, there can't be drivers without animation_data.
+                        d = m; reuse = True; break
+            else:
+                d = ob.modifiers.new(mod_name, type='HOOK')
+                if d is None:
+                    raise RuntimeError(f"Modifier was not created in node {self} -- the object is invalid.")
+            self.bObject.append(d)
+            self.get_target_and_subtarget(d, input_name="Hook Target")
+            vertices_used=[]
+            if reuse: # Get the verts in the list... filter out all the unneeded 0's
+                vertices_used = list(d.vertex_indices)
+                include_0 = 0 in vertices_used
+                vertices_used = list(filter(lambda a : a != 0, vertices_used))
+                if include_0: vertices_used.append(0)
+            # now we add the selected vertex to the list, too
+            vertex = self.evaluate_input("Point Index")
+            if ob.type == 'CURVE' and ob.data.splines[0].type == 'BEZIER' and auto_bezier:
+                if affect_radius:
+                    self.driver_for_radius(ob, target_node.bGetObject(), vertex, d.strength)
+                vertex*=3
+                vertices_used.extend([vertex, vertex+1, vertex+2])
+            else:
+                vertices_used.append(vertex)
+            # if we have a curve and it is NOT using auto-bezier for the verts..
+            if ob.type == 'CURVE' and ob.data.splines[0].type == 'BEZIER' and affect_radius and not auto_bezier:
+                print (f"WARN: {self}: \"Affect Radius\" may not behave as expected"
+                        " when used on Bezier curves without Auto-Bezier")
+                #bezier point starts at 1, and then every third vert, so 4, 7, 10...
+                if vertex%3==1:
+                    self.driver_for_radius(ob, target_node.bGetObject(), vertex, d.strength)
+            if ob.type == 'CURVE' and ob.data.splines[0].type != 'BEZIER' and \
+                        affect_radius:
+                self.driver_for_radius(ob, target_node.bGetObject(), vertex, d.strength, bezier=False)
+                
+            d.vertex_indices_set(vertices_used)
+            evaluate_sockets(self, d, props_sockets)
         finish_drivers(self)
         finish_drivers(self)
         # todo: this node should be able to take many indices in the future.
         # todo: this node should be able to take many indices in the future.
         # Also: I have a Geometry Nodes implementation of this I can use... maybe...
         # Also: I have a Geometry Nodes implementation of this I can use... maybe...
@@ -385,7 +372,7 @@ class DeformerMorphTarget(MantisDeformerNode):
     def GetxForm(self, trace_input="Object"):
     def GetxForm(self, trace_input="Object"):
         trace = trace_single_line(self, trace_input)
         trace = trace_single_line(self, trace_input)
         for node in trace[0]:
         for node in trace[0]:
-            if (node.__class__ in [xFormGeometryObject, InputExistingGeometryObject]):
+            if (isinstance(node, deformable_types)):
                 return node
                 return node
         raise GraphError("%s is not connected to an upstream xForm" % self)
         raise GraphError("%s is not connected to an upstream xForm" % self)
 
 
@@ -436,17 +423,12 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
         self.node_type = "LINK"
         self.node_type = "LINK"
         self.prepared = True
         self.prepared = True
         self.executed = True
         self.executed = True
-        self.bObject = None
         setup_custom_props(self)
         setup_custom_props(self)
-
-    def GetxForm(self):
-        return GetxForm(self)
     
     
     # bpy.data.node_groups["Morph Deform.045"].nodes["Named Attribute.020"].data_type = 'FLOAT_VECTOR'
     # bpy.data.node_groups["Morph Deform.045"].nodes["Named Attribute.020"].data_type = 'FLOAT_VECTOR'
     # bpy.context.object.add_rest_position_attribute = True
     # bpy.context.object.add_rest_position_attribute = True
 
 
-
-    def gen_morph_target_modifier(self, context):
+    def gen_morph_target_modifier(self, xf, context):
         # first let's see if this is a no-op
         # first let's see if this is a no-op
         targets = []
         targets = []
         for k,v in self.inputs.items():
         for k,v in self.inputs.items():
@@ -459,15 +441,15 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
         from .geometry_node_graphgen import gen_morph_target_nodes
         from .geometry_node_graphgen import gen_morph_target_nodes
         m, props_sockets = gen_morph_target_nodes(
         m, props_sockets = gen_morph_target_nodes(
                             self.evaluate_input("Name"),
                             self.evaluate_input("Name"),
-                            self.GetxForm().bGetObject(),
+                            xf.bGetObject(),
                             targets,
                             targets,
                             context,
                             context,
                             use_offset=self.evaluate_input("Use Offset"))
                             use_offset=self.evaluate_input("Use Offset"))
-        self.bObject = m
+        self.bObject.append(m)
         evaluate_sockets(self, m, props_sockets)
         evaluate_sockets(self, m, props_sockets)
         finish_drivers(self)
         finish_drivers(self)
 
 
-    def gen_shape_key(self, context): # TODO: make this a feature of the node definition that appears only when there are no prior deformers - and shows a warning!
+    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.
         # 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.
         # there are a number of things I need to fix here
         # there are a number of things I need to fix here
         #   - reuse shape keys if possible
         #   - reuse shape keys if possible
@@ -483,7 +465,6 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
         from time import time
         from time import time
         start_time = time()
         start_time = time()
         from bpy import data
         from bpy import data
-        xf = self.GetxForm()
         ob = xf.bGetObject()
         ob = xf.bGetObject()
         dg = context.view_layer.depsgraph
         dg = context.view_layer.depsgraph
         dg.update()
         dg.update()
@@ -541,7 +522,7 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
                 sk = keys.get(mt_name)
                 sk = keys.get(mt_name)
                 sk.relative_key = keys.get(rel)
                 sk.relative_key = keys.get(rel)
         
         
-        self.bObject = sk.id_data
+        self.bObject.append(sk.id_data)
         evaluate_sockets(self, sk.id_data, props_sockets)
         evaluate_sockets(self, sk.id_data, props_sockets)
         finish_drivers(self)
         finish_drivers(self)
         prWhite(f"Initializing morph target took {time() -start_time} seconds")
         prWhite(f"Initializing morph target took {time() -start_time} seconds")
@@ -549,9 +530,9 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
 
 
     def bFinalize(self, bContext=None):
     def bFinalize(self, bContext=None):
         prGreen(f"Executing Morph Deform node {self}")
         prGreen(f"Executing Morph Deform node {self}")
+        use_shape_keys = self.evaluate_input("Use Shape Key")
         # if there is a not a prior deformer then there should be an option to use plain 'ol shape keys
         # if there is a not a prior deformer then there should be an option to use plain 'ol shape keys
         # GN is always desirable as an option though because it can be baked & many other reasons
         # GN is always desirable as an option though because it can be baked & many other reasons
-        use_shape_keys = self.evaluate_input("Use Shape Key")
         if use_shape_keys: # check and see if we can.
         if use_shape_keys: # check and see if we can.
             if self.inputs.get("Deformer"): # I guess this isn't available in some node group contexts... bad. FIXME
             if self.inputs.get("Deformer"): # I guess this isn't available in some node group contexts... bad. FIXME
                 if (links := self.inputs["Deformer"].links):
                 if (links := self.inputs["Deformer"].links):
@@ -560,7 +541,8 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
                     elif links[0].from_node.parameters.get("Use Shape Key") == False:
                     elif links[0].from_node.parameters.get("Use Shape Key") == False:
                         use_shape_keys = False
                         use_shape_keys = False
         self.parameters["Use Shape Key"] = use_shape_keys
         self.parameters["Use Shape Key"] = use_shape_keys
-        if use_shape_keys:
-            self.gen_shape_key(bContext)
-        else:
-            self.gen_morph_target_modifier(bContext)
+        for xf in self.GetxForm():
+            if use_shape_keys:
+                self.gen_shape_key(xf, bContext)
+            else:
+                self.gen_morph_target_modifier(xf, bContext)

+ 329 - 361
link_containers.py

@@ -44,6 +44,7 @@ class MantisLinkNode(MantisNode):
         super().__init__(signature, base_tree, socket_templates)
         super().__init__(signature, base_tree, socket_templates)
         self.node_type = 'LINK'
         self.node_type = 'LINK'
         self.prepared = True
         self.prepared = True
+        self.bObject=[]
 
 
     def evaluate_input(self, input_name, index=0):
     def evaluate_input(self, input_name, index=0):
         # should catch 'Target', 'Pole Target' and ArmatureConstraint targets, too
         # should catch 'Target', 'Pole Target' and ArmatureConstraint targets, too
@@ -81,6 +82,16 @@ class MantisLinkNode(MantisNode):
             else:
             else:
                 c.space_object=xf
                 c.space_object=xf
     
     
+    def GetxForm(nc, output_name="Output Relationship"):
+        break_condition= lambda node : node.node_type=='XFORM'
+        xforms = trace_line_up_branching(nc, output_name, break_condition)
+        return_me=[]
+        for xf in xforms:
+            if xf.node_type != 'XFORM':
+                continue
+            return_me.append(xf)
+        return return_me
+    
     def bFinalize(self, bContext=None):
     def bFinalize(self, bContext=None):
         finish_drivers(self)
         finish_drivers(self)
 
 
@@ -88,12 +99,6 @@ class MantisLinkNode(MantisNode):
 # L I N K   N O D E S
 # L I N K   N O D E S
 #*#-------------------------------#++#-------------------------------#*#
 #*#-------------------------------#++#-------------------------------#*#
 
 
-def GetxForm(nc):
-    trace = trace_single_line_up(nc, "Output Relationship")
-    for node in trace[0]:
-        if (node.node_type == 'XFORM'):
-            return node
-    raise GraphError("%s is not connected to a downstream xForm" % nc)
 
 
 class LinkInherit(MantisLinkNode):
 class LinkInherit(MantisLinkNode):
     '''A node representing inheritance'''
     '''A node representing inheritance'''
@@ -104,7 +109,7 @@ class LinkInherit(MantisLinkNode):
         self.set_traverse([('Parent', 'Inheritance')])
         self.set_traverse([('Parent', 'Inheritance')])
         self.executed = True
         self.executed = True
     
     
-    def GetxForm(self): # DUPLICATED, TODO fix this
+    def GetxForm(self):
         # I think this is only run in display update.
         # I think this is only run in display update.
         trace = trace_single_line_up(self, "Inheritance")
         trace = trace_single_line_up(self, "Inheritance")
         for node in trace[0]:
         for node in trace[0]:
@@ -121,23 +126,21 @@ class LinkCopyLocation(MantisLinkNode):
         additional_parameters = { "Name":None }
         additional_parameters = { "Name":None }
         self.init_parameters(additional_parameters=additional_parameters)
         self.init_parameters(additional_parameters=additional_parameters)
         self.set_traverse([("Input Relationship", "Output Relationship")])
         self.set_traverse([("Input Relationship", "Output Relationship")])
-        
-    def GetxForm(self):
-        return GetxForm(self)
 
 
     def bExecute(self, context):
     def bExecute(self, context):
         prepare_parameters(self)
         prepare_parameters(self)
-        c = self.GetxForm().bGetObject().constraints.new('COPY_LOCATION')
-        self.get_target_and_subtarget(c)
-        print(wrapGreen("Creating ")+wrapWhite("Copy Location")+
-             wrapGreen(" Constraint for bone: ") +
-             wrapOrange(self.GetxForm().bGetObject().name))
-        if constraint_name := self.evaluate_input("Name"):
-            c.name = constraint_name
-        self.bObject = c
-        self.set_custom_space()
-        props_sockets = self.gen_property_socket_map()
-        evaluate_sockets(self, c, props_sockets)
+        for xf in self.GetxForm():
+            c = xf.bGetObject().constraints.new('COPY_LOCATION')
+            self.get_target_and_subtarget(c)
+            print(wrapGreen("Creating ")+wrapWhite("Copy Location")+
+                wrapGreen(" Constraint for bone: ") +
+                wrapOrange(xf.bGetObject().name))
+            if constraint_name := self.evaluate_input("Name"):
+                c.name = constraint_name
+            self.bObject.append(c)
+            self.set_custom_space()
+            props_sockets = self.gen_property_socket_map()
+            evaluate_sockets(self, c, props_sockets)
         self.executed = True
         self.executed = True
         
         
 class LinkCopyRotation(MantisLinkNode):
 class LinkCopyRotation(MantisLinkNode):
@@ -148,32 +151,30 @@ class LinkCopyRotation(MantisLinkNode):
         additional_parameters = { "Name":None }
         additional_parameters = { "Name":None }
         self.init_parameters(additional_parameters=additional_parameters)
         self.init_parameters(additional_parameters=additional_parameters)
         self.set_traverse([("Input Relationship", "Output Relationship")])
         self.set_traverse([("Input Relationship", "Output Relationship")])
-        
-    def GetxForm(self):
-        return GetxForm(self)
 
 
     def bExecute(self, context):
     def bExecute(self, context):
         prepare_parameters(self)
         prepare_parameters(self)
-        c = self.GetxForm().bGetObject().constraints.new('COPY_ROTATION')
-        self.get_target_and_subtarget(c)
-        print(wrapGreen("Creating ")+wrapWhite("Copy Rotation")+
-             wrapGreen(" Constraint for bone: ") +
-             wrapOrange(self.GetxForm().bGetObject().name))
-        
-        rotation_order = self.evaluate_input("RotationOrder")
-        if ((rotation_order == 'QUATERNION') or (rotation_order == 'AXIS_ANGLE')):
-            c.euler_order = 'AUTO'
-        else:
-            try:
-                c.euler_order = rotation_order
-            except TypeError: # it's a driver or incorrect
+        for xf in self.GetxForm():
+            c = xf.bGetObject().constraints.new('COPY_ROTATION')
+            self.get_target_and_subtarget(c)
+            print(wrapGreen("Creating ")+wrapWhite("Copy Rotation")+
+                wrapGreen(" Constraint for bone: ") +
+                wrapOrange(xf.bGetObject().name))
+            
+            rotation_order = self.evaluate_input("RotationOrder")
+            if ((rotation_order == 'QUATERNION') or (rotation_order == 'AXIS_ANGLE')):
                 c.euler_order = 'AUTO'
                 c.euler_order = 'AUTO'
-        if constraint_name := self.evaluate_input("Name"):
-            c.name = constraint_name
-        self.bObject = c
-        self.set_custom_space()
-        props_sockets = self.gen_property_socket_map()
-        evaluate_sockets(self, c, props_sockets)
+            else:
+                try:
+                    c.euler_order = rotation_order
+                except TypeError: # it's a driver or incorrect
+                    c.euler_order = 'AUTO'
+            if constraint_name := self.evaluate_input("Name"):
+                c.name = constraint_name
+            self.bObject.append(c)
+            self.set_custom_space()
+            props_sockets = self.gen_property_socket_map()
+            evaluate_sockets(self, c, props_sockets)
         self.executed = True
         self.executed = True
         
         
 class LinkCopyScale(MantisLinkNode):
 class LinkCopyScale(MantisLinkNode):
@@ -184,36 +185,34 @@ class LinkCopyScale(MantisLinkNode):
         additional_parameters = { "Name":None }
         additional_parameters = { "Name":None }
         self.init_parameters(additional_parameters=additional_parameters)
         self.init_parameters(additional_parameters=additional_parameters)
         self.set_traverse([("Input Relationship", "Output Relationship")])
         self.set_traverse([("Input Relationship", "Output Relationship")])
-    
-    def GetxForm(self):
-        return GetxForm(self)
 
 
     def bExecute(self, context):
     def bExecute(self, context):
         prepare_parameters(self)
         prepare_parameters(self)
-        c = self.GetxForm().bGetObject().constraints.new('COPY_SCALE')
-        self.get_target_and_subtarget(c)
-        print(wrapGreen("Creating ")+wrapWhite("Copy Scale")+
-             wrapGreen(" Constraint for bone: ") +
-             wrapOrange(self.GetxForm().bGetObject().name))
-        if constraint_name := self.evaluate_input("Name"):
-            c.name = constraint_name
-        self.bObject = c
-        if self.inputs["Owner Space"].is_connected and self.inputs["Owner Space"].links[0].from_node.node_type == 'XFORM':
-            c.owner_space='CUSTOM'
-            xf = self.inputs["Owner Space"].links[0].from_node.bGetObject(mode="OBJECT")
-            if isinstance(xf, Bone):
-                c.space_object=self.inputs["Owner Space"].links[0].from_node.bGetParentArmature(); c.space_subtarget=xf.name
-            else:
-                c.space_object=xf
-        if self.inputs["Target Space"].is_connected and self.inputs["Target Space"].links[0].from_node.node_type == 'XFORM':
-            c.target_space='CUSTOM'
-            xf = self.inputs["Target Space"].links[0].from_node.bGetObject(mode="OBJECT")
-            if isinstance(xf, Bone):
-                c.space_object=self.inputs["Owner Space"].links[0].from_node.bGetParentArmature(); c.space_subtarget=xf.name
-            else:
-                c.space_object=xf
-        props_sockets = self.gen_property_socket_map()
-        evaluate_sockets(self, c, props_sockets)   
+        for xf in self.GetxForm():
+            c = xf.bGetObject().constraints.new('COPY_SCALE')
+            self.get_target_and_subtarget(c)
+            print(wrapGreen("Creating ")+wrapWhite("Copy Scale")+
+                wrapGreen(" Constraint for bone: ") +
+                wrapOrange(xf.bGetObject().name))
+            if constraint_name := self.evaluate_input("Name"):
+                c.name = constraint_name
+            self.bObject.append(c)
+            if self.inputs["Owner Space"].is_connected and self.inputs["Owner Space"].links[0].from_node.node_type == 'XFORM':
+                c.owner_space='CUSTOM'
+                xf = self.inputs["Owner Space"].links[0].from_node.bGetObject(mode="OBJECT")
+                if isinstance(xf, Bone):
+                    c.space_object=self.inputs["Owner Space"].links[0].from_node.bGetParentArmature(); c.space_subtarget=xf.name
+                else:
+                    c.space_object=xf
+            if self.inputs["Target Space"].is_connected and self.inputs["Target Space"].links[0].from_node.node_type == 'XFORM':
+                c.target_space='CUSTOM'
+                xf = self.inputs["Target Space"].links[0].from_node.bGetObject(mode="OBJECT")
+                if isinstance(xf, Bone):
+                    c.space_object=self.inputs["Owner Space"].links[0].from_node.bGetParentArmature(); c.space_subtarget=xf.name
+                else:
+                    c.space_object=xf
+            props_sockets = self.gen_property_socket_map()
+            evaluate_sockets(self, c, props_sockets)   
         self.executed = True 
         self.executed = True 
             
             
 
 
@@ -225,23 +224,21 @@ class LinkCopyTransforms(MantisLinkNode):
         additional_parameters = { "Name":None }
         additional_parameters = { "Name":None }
         self.init_parameters(additional_parameters=additional_parameters)
         self.init_parameters(additional_parameters=additional_parameters)
         self.set_traverse([("Input Relationship", "Output Relationship")])
         self.set_traverse([("Input Relationship", "Output Relationship")])
-        
-    def GetxForm(self):
-        return GetxForm(self)
 
 
     def bExecute(self, context):
     def bExecute(self, context):
         prepare_parameters(self)
         prepare_parameters(self)
-        c = self.GetxForm().bGetObject().constraints.new('COPY_TRANSFORMS')
-        self.get_target_and_subtarget(c)
-        print(wrapGreen("Creating ")+wrapWhite("Copy Transforms")+
-             wrapGreen(" Constraint for bone: ") +
-             wrapOrange(self.GetxForm().bGetObject().name))
-        if constraint_name := self.evaluate_input("Name"):
-            c.name = constraint_name
-        self.bObject = c
-        self.set_custom_space()
-        props_sockets = self.gen_property_socket_map()
-        evaluate_sockets(self, c, props_sockets)  
+        for xf in self.GetxForm():
+            c = xf.bGetObject().constraints.new('COPY_TRANSFORMS')
+            self.get_target_and_subtarget(c)
+            print(wrapGreen("Creating ")+wrapWhite("Copy Transforms")+
+                wrapGreen(" Constraint for bone: ") +
+                wrapOrange(xf.bGetObject().name))
+            if constraint_name := self.evaluate_input("Name"):
+                c.name = constraint_name
+            self.bObject.append(c)
+            self.set_custom_space()
+            props_sockets = self.gen_property_socket_map()
+            evaluate_sockets(self, c, props_sockets)  
         self.executed = True
         self.executed = True
 
 
 class LinkTransformation(MantisLinkNode):
 class LinkTransformation(MantisLinkNode):
@@ -251,51 +248,49 @@ class LinkTransformation(MantisLinkNode):
         super().__init__(signature, base_tree, LinkTransformationSockets)
         super().__init__(signature, base_tree, LinkTransformationSockets)
         self.init_parameters(additional_parameters={"Name":None })
         self.init_parameters(additional_parameters={"Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
         self.set_traverse([("Input Relationship", "Output Relationship")])
-        
-    def GetxForm(self):
-        return GetxForm(self)
 
 
     def bExecute(self, context):
     def bExecute(self, context):
         prepare_parameters(self)
         prepare_parameters(self)
-        c = self.GetxForm().bGetObject().constraints.new('TRANSFORM')
-        self.get_target_and_subtarget(c)
-        print(wrapGreen("Creating ")+wrapWhite("Transformation")+
-             wrapGreen(" Constraint for bone: ") +
-             wrapOrange(self.GetxForm().bGetObject().name))
-        if constraint_name := self.evaluate_input("Name"):
-            c.name = constraint_name
-        self.bObject = c
-        self.set_custom_space()
-        props_sockets = self.gen_property_socket_map()
-        # we have to fix the blender-property for scale/rotation
-        # because Blender stores these separately.
-        # I do not care that this code is ugly.
-        from_replace, to_replace = '', ''
-        if self.evaluate_input("Map From") == 'ROTATION':
-            from_replace='_rot'
-        elif self.evaluate_input("Map From") == 'SCALE':
-            from_replace='_scale'
-        if self.evaluate_input("Map To") == 'ROTATION':
-            to_replace='_rot'
-        elif self.evaluate_input("Map To") == 'SCALE':
-            to_replace='_scale'
-        if from_replace:
-            for axis in ['x', 'y', 'z']:
-                stub='from_min_'+axis
-                props_sockets[stub+from_replace]=props_sockets[stub]
-                del props_sockets[stub]
-                stub='from_max_'+axis
-                props_sockets[stub+from_replace]=props_sockets[stub]
-                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]
-                del props_sockets[stub]
-                stub='to_max_'+axis
-                props_sockets[stub+to_replace]=props_sockets[stub]
-                del props_sockets[stub]
-        evaluate_sockets(self, c, props_sockets)  
+        for xf in self.GetxForm():
+            c = xf.bGetObject().constraints.new('TRANSFORM')
+            self.get_target_and_subtarget(c)
+            print(wrapGreen("Creating ")+wrapWhite("Transformation")+
+                wrapGreen(" Constraint for bone: ") +
+                wrapOrange(xf.bGetObject().name))
+            if constraint_name := self.evaluate_input("Name"):
+                c.name = constraint_name
+            self.bObject.append(c)
+            self.set_custom_space()
+            props_sockets = self.gen_property_socket_map()
+            # we have to fix the blender-property for scale/rotation
+            # because Blender stores these separately.
+            # I do not care that this code is ugly.
+            from_replace, to_replace = '', ''
+            if self.evaluate_input("Map From") == 'ROTATION':
+                from_replace='_rot'
+            elif self.evaluate_input("Map From") == 'SCALE':
+                from_replace='_scale'
+            if self.evaluate_input("Map To") == 'ROTATION':
+                to_replace='_rot'
+            elif self.evaluate_input("Map To") == 'SCALE':
+                to_replace='_scale'
+            if from_replace:
+                for axis in ['x', 'y', 'z']:
+                    stub='from_min_'+axis
+                    props_sockets[stub+from_replace]=props_sockets[stub]
+                    del props_sockets[stub]
+                    stub='from_max_'+axis
+                    props_sockets[stub+from_replace]=props_sockets[stub]
+                    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]
+                    del props_sockets[stub]
+                    stub='to_max_'+axis
+                    props_sockets[stub+to_replace]=props_sockets[stub]
+                    del props_sockets[stub]
+            evaluate_sockets(self, c, props_sockets)  
         self.executed = True
         self.executed = True
 
 
 class LinkLimitLocation(MantisLinkNode):
 class LinkLimitLocation(MantisLinkNode):
@@ -303,24 +298,20 @@ class LinkLimitLocation(MantisLinkNode):
         super().__init__(signature, base_tree, LinkLimitLocationScaleSockets)
         super().__init__(signature, base_tree, LinkLimitLocationScaleSockets)
         self.init_parameters(additional_parameters={ "Name":None })
         self.init_parameters(additional_parameters={ "Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
         self.set_traverse([("Input Relationship", "Output Relationship")])
-        
-    def GetxForm(self):
-        return GetxForm(self)
 
 
     def bExecute(self, context):
     def bExecute(self, context):
         prepare_parameters(self)
         prepare_parameters(self)
-        c = self.GetxForm().bGetObject().constraints.new('LIMIT_LOCATION')
-        #
-        print(wrapGreen("Creating ")+wrapWhite("Limit Location")+
-             wrapGreen(" Constraint for bone: ") +
-             wrapOrange(self.GetxForm().bGetObject().name))
-        
-        if constraint_name := self.evaluate_input("Name"):
-            c.name = constraint_name
-        self.bObject = c
-        self.set_custom_space()
-        props_sockets = self.gen_property_socket_map()
-        evaluate_sockets(self, c, props_sockets)
+        for xf in self.GetxForm():
+            c = xf.bGetObject().constraints.new('LIMIT_LOCATION')
+            print(wrapGreen("Creating ")+wrapWhite("Limit Location")+
+                wrapGreen(" Constraint for bone: ") +
+                wrapOrange(xf.bGetObject().name))
+            if constraint_name := self.evaluate_input("Name"):
+                c.name = constraint_name
+            self.bObject.append(c)
+            self.set_custom_space()
+            props_sockets = self.gen_property_socket_map()
+            evaluate_sockets(self, c, props_sockets)
         self.executed = True
         self.executed = True
         
         
 class LinkLimitRotation(MantisLinkNode):
 class LinkLimitRotation(MantisLinkNode):
@@ -328,23 +319,21 @@ class LinkLimitRotation(MantisLinkNode):
         super().__init__(signature, base_tree, LinkLimitRotationSockets)
         super().__init__(signature, base_tree, LinkLimitRotationSockets)
         self.init_parameters(additional_parameters={ "Name":None })
         self.init_parameters(additional_parameters={ "Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
         self.set_traverse([("Input Relationship", "Output Relationship")])
-        
-    def GetxForm(self):
-        return GetxForm(self)
 
 
     def bExecute(self, context):
     def bExecute(self, context):
         prepare_parameters(self)
         prepare_parameters(self)
-        c = self.GetxForm().bGetObject().constraints.new('LIMIT_ROTATION')
-        print(wrapGreen("Creating ")+wrapWhite("Limit Rotation")+
-             wrapGreen(" Constraint for bone: ") +
-             wrapOrange(self.GetxForm().bGetObject().name))
-        
-        if constraint_name := self.evaluate_input("Name"):
-            c.name = constraint_name
-        self.bObject = c
-        self.set_custom_space()
-        props_sockets = self.gen_property_socket_map()
-        evaluate_sockets(self, c, props_sockets)
+        for xf in self.GetxForm():
+            c = xf.bGetObject().constraints.new('LIMIT_ROTATION')
+            print(wrapGreen("Creating ")+wrapWhite("Limit Rotation")+
+                wrapGreen(" Constraint for bone: ") +
+                wrapOrange(xf.bGetObject().name))
+            
+            if constraint_name := self.evaluate_input("Name"):
+                c.name = constraint_name
+            self.bObject.append(c)
+            self.set_custom_space()
+            props_sockets = self.gen_property_socket_map()
+            evaluate_sockets(self, c, props_sockets)
         self.executed = True
         self.executed = True
 
 
 class LinkLimitScale(MantisLinkNode):
 class LinkLimitScale(MantisLinkNode):
@@ -352,23 +341,21 @@ class LinkLimitScale(MantisLinkNode):
         super().__init__(signature, base_tree, LinkLimitLocationScaleSockets)
         super().__init__(signature, base_tree, LinkLimitLocationScaleSockets)
         self.init_parameters(additional_parameters={ "Name":None })
         self.init_parameters(additional_parameters={ "Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
         self.set_traverse([("Input Relationship", "Output Relationship")])
-    
-    def GetxForm(self):
-        return GetxForm(self)
 
 
     def bExecute(self, context):
     def bExecute(self, context):
         prepare_parameters(self)
         prepare_parameters(self)
-        c = self.GetxForm().bGetObject().constraints.new('LIMIT_SCALE')
-        print(wrapGreen("Creating ")+wrapWhite("Limit Scale")+
-             wrapGreen(" Constraint for bone: ") +
-             wrapOrange(self.GetxForm().bGetObject().name))
-        
-        if constraint_name := self.evaluate_input("Name"):
-            c.name = constraint_name
-        self.bObject = c
-        self.set_custom_space()
-        props_sockets = self.gen_property_socket_map()
-        evaluate_sockets(self, c, props_sockets)
+        for xf in self.GetxForm():
+            c = xf.bGetObject().constraints.new('LIMIT_SCALE')
+            print(wrapGreen("Creating ")+wrapWhite("Limit Scale")+
+                wrapGreen(" Constraint for bone: ") +
+                wrapOrange(xf.bGetObject().name))
+            
+            if constraint_name := self.evaluate_input("Name"):
+                c.name = constraint_name
+            self.bObject.append(c)
+            self.set_custom_space()
+            props_sockets = self.gen_property_socket_map()
+            evaluate_sockets(self, c, props_sockets)
         self.executed = True
         self.executed = True
  
  
 class LinkLimitDistance(MantisLinkNode):
 class LinkLimitDistance(MantisLinkNode):
@@ -376,23 +363,21 @@ class LinkLimitDistance(MantisLinkNode):
         super().__init__(signature, base_tree, LinkLimitDistanceSockets)
         super().__init__(signature, base_tree, LinkLimitDistanceSockets)
         self.init_parameters(additional_parameters={ "Name":None })
         self.init_parameters(additional_parameters={ "Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
         self.set_traverse([("Input Relationship", "Output Relationship")])
-        
-    def GetxForm(self):
-        return GetxForm(self)
 
 
     def bExecute(self, context):
     def bExecute(self, context):
         prepare_parameters(self)
         prepare_parameters(self)
-        print(wrapGreen("Creating ")+wrapWhite("Limit Distance")+
-             wrapGreen(" Constraint for bone: ") +
-             wrapOrange(self.GetxForm().bGetObject().name))
-        c = self.GetxForm().bGetObject().constraints.new('LIMIT_DISTANCE')
-        self.get_target_and_subtarget(c)
-        if constraint_name := self.evaluate_input("Name"):
-            c.name = constraint_name
-        self.bObject = c
-        self.set_custom_space()
-        props_sockets = self.gen_property_socket_map()
-        evaluate_sockets(self, c, props_sockets)
+        for xf in self.GetxForm():
+            print(wrapGreen("Creating ")+wrapWhite("Limit Distance")+
+                wrapGreen(" Constraint for bone: ") +
+                wrapOrange(xf.bGetObject().name))
+            c = xf.bGetObject().constraints.new('LIMIT_DISTANCE')
+            self.get_target_and_subtarget(c)
+            if constraint_name := self.evaluate_input("Name"):
+                c.name = constraint_name
+            self.bObject.append(c)
+            self.set_custom_space()
+            props_sockets = self.gen_property_socket_map()
+            evaluate_sockets(self, c, props_sockets)
         self.executed = True
         self.executed = True
 
 
 # Tracking
 # Tracking
@@ -402,26 +387,24 @@ class LinkStretchTo(MantisLinkNode):
         super().__init__(signature, base_tree, LinkStretchToSockets)
         super().__init__(signature, base_tree, LinkStretchToSockets)
         self.init_parameters(additional_parameters={ "Name":None })
         self.init_parameters(additional_parameters={ "Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
         self.set_traverse([("Input Relationship", "Output Relationship")])
-        
-    def GetxForm(self):
-        return GetxForm(self)
 
 
     def bExecute(self, context):
     def bExecute(self, context):
         prepare_parameters(self)
         prepare_parameters(self)
-        print(wrapGreen("Creating ")+wrapWhite("Stretch-To")+
-             wrapGreen(" Constraint for bone: ") +
-             wrapOrange(self.GetxForm().bGetObject().name))
-        c = self.GetxForm().bGetObject().constraints.new('STRETCH_TO')
-        self.get_target_and_subtarget(c)
-        if constraint_name := self.evaluate_input("Name"):
-            c.name = constraint_name
-        self.bObject = c
-        props_sockets = self.gen_property_socket_map()
-        evaluate_sockets(self, c, props_sockets)
-        
-        if (self.evaluate_input("Original Length") == 0):
-            # this is meant to be set automatically.
-            c.rest_length = self.GetxForm().bGetObject().bone.length
+        for xf in self.GetxForm():
+            print(wrapGreen("Creating ")+wrapWhite("Stretch-To")+
+                wrapGreen(" Constraint for bone: ") +
+                wrapOrange(xf.bGetObject().name))
+            c = xf.bGetObject().constraints.new('STRETCH_TO')
+            self.get_target_and_subtarget(c)
+            if constraint_name := self.evaluate_input("Name"):
+                c.name = constraint_name
+            self.bObject.append(c)
+            props_sockets = self.gen_property_socket_map()
+            evaluate_sockets(self, c, props_sockets)
+            
+            if (self.evaluate_input("Original Length") == 0):
+                # this is meant to be set automatically.
+                c.rest_length = xf.bGetObject().bone.length
         self.executed = True
         self.executed = True
 
 
 class LinkDampedTrack(MantisLinkNode):
 class LinkDampedTrack(MantisLinkNode):
@@ -430,21 +413,19 @@ class LinkDampedTrack(MantisLinkNode):
         self.init_parameters(additional_parameters={ "Name":None })
         self.init_parameters(additional_parameters={ "Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
         self.set_traverse([("Input Relationship", "Output Relationship")])
 
 
-    def GetxForm(self):
-        return GetxForm(self)
-
     def bExecute(self, context):
     def bExecute(self, context):
         prepare_parameters(self)
         prepare_parameters(self)
-        print(wrapGreen("Creating ")+wrapWhite("Damped Track")+
-             wrapGreen(" Constraint for bone: ") +
-             wrapOrange(self.GetxForm().bGetObject().name))
-        c = self.GetxForm().bGetObject().constraints.new('DAMPED_TRACK')
-        self.get_target_and_subtarget(c)
-        if constraint_name := self.evaluate_input("Name"):
-            c.name = constraint_name
-        self.bObject = c
-        props_sockets = self.gen_property_socket_map()
-        evaluate_sockets(self, c, props_sockets)
+        for xf in self.GetxForm():
+            print(wrapGreen("Creating ")+wrapWhite("Damped Track")+
+                wrapGreen(" Constraint for bone: ") +
+                wrapOrange(xf.bGetObject().name))
+            c = xf.bGetObject().constraints.new('DAMPED_TRACK')
+            self.get_target_and_subtarget(c)
+            if constraint_name := self.evaluate_input("Name"):
+                c.name = constraint_name
+            self.bObject.append(c)
+            props_sockets = self.gen_property_socket_map()
+            evaluate_sockets(self, c, props_sockets)
         self.executed = True
         self.executed = True
 
 
 class LinkLockedTrack(MantisLinkNode):
 class LinkLockedTrack(MantisLinkNode):
@@ -453,21 +434,19 @@ class LinkLockedTrack(MantisLinkNode):
         self.init_parameters(additional_parameters={"Name":None })
         self.init_parameters(additional_parameters={"Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
         self.set_traverse([("Input Relationship", "Output Relationship")])
 
 
-    def GetxForm(self):
-        return GetxForm(self)
-
     def bExecute(self, context):
     def bExecute(self, context):
         prepare_parameters(self)
         prepare_parameters(self)
-        print(wrapGreen("Creating ")+wrapWhite("Locked Track")+
-             wrapGreen(" Constraint for bone: ") +
-             wrapOrange(self.GetxForm().bGetObject().name))
-        c = self.GetxForm().bGetObject().constraints.new('LOCKED_TRACK')
-        self.get_target_and_subtarget(c)
-        if constraint_name := self.evaluate_input("Name"):
-            c.name = constraint_name
-        self.bObject = c
-        props_sockets = self.gen_property_socket_map()
-        evaluate_sockets(self, c, props_sockets)
+        for xf in self.GetxForm():
+            print(wrapGreen("Creating ")+wrapWhite("Locked Track")+
+                wrapGreen(" Constraint for bone: ") +
+                wrapOrange(xf.bGetObject().name))
+            c = xf.bGetObject().constraints.new('LOCKED_TRACK')
+            self.get_target_and_subtarget(c)
+            if constraint_name := self.evaluate_input("Name"):
+                c.name = constraint_name
+            self.bObject.append(c)
+            props_sockets = self.gen_property_socket_map()
+            evaluate_sockets(self, c, props_sockets)
         self.executed = True
         self.executed = True
 
 
 class LinkTrackTo(MantisLinkNode):
 class LinkTrackTo(MantisLinkNode):
@@ -476,21 +455,19 @@ class LinkTrackTo(MantisLinkNode):
         self.init_parameters(additional_parameters={"Name":None })
         self.init_parameters(additional_parameters={"Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
         self.set_traverse([("Input Relationship", "Output Relationship")])
 
 
-    def GetxForm(self):
-        return GetxForm(self)
-
     def bExecute(self, context):
     def bExecute(self, context):
         prepare_parameters(self)
         prepare_parameters(self)
-        print(wrapGreen("Creating ")+wrapWhite("Track-To")+
-             wrapGreen(" Constraint for bone: ") +
-             wrapOrange(self.GetxForm().bGetObject().name))
-        c = self.GetxForm().bGetObject().constraints.new('TRACK_TO')
-        self.get_target_and_subtarget(c)
-        if constraint_name := self.evaluate_input("Name"):
-            c.name = constraint_name
-        self.bObject = c
-        props_sockets = self.gen_property_socket_map()
-        evaluate_sockets(self, c, props_sockets)
+        for xf in self.GetxForm():
+            print(wrapGreen("Creating ")+wrapWhite("Track-To")+
+                wrapGreen(" Constraint for bone: ") +
+                wrapOrange(xf.bGetObject().name))
+            c = xf.bGetObject().constraints.new('TRACK_TO')
+            self.get_target_and_subtarget(c)
+            if constraint_name := self.evaluate_input("Name"):
+                c.name = constraint_name
+            self.bObject.append(c)
+            props_sockets = self.gen_property_socket_map()
+            evaluate_sockets(self, c, props_sockets)
         self.executed = True
         self.executed = True
 
 
 
 
@@ -500,33 +477,28 @@ class LinkInheritConstraint(MantisLinkNode):
         self.init_parameters(additional_parameters={"Name":None })
         self.init_parameters(additional_parameters={"Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
         self.set_traverse([("Input Relationship", "Output Relationship")])
 
 
-    def GetxForm(self):
-        return GetxForm(self)
-
     def bExecute(self, context):
     def bExecute(self, context):
         prepare_parameters(self)
         prepare_parameters(self)
-        print(wrapGreen("Creating ")+wrapWhite("Child-Of")+
-             wrapGreen(" Constraint for bone: ") +
-             wrapOrange(self.GetxForm().bGetObject().name))
-        c = self.GetxForm().bGetObject().constraints.new('CHILD_OF')
-        self.get_target_and_subtarget(c)
-        if constraint_name := self.evaluate_input("Name"):
-            c.name = constraint_name
-        self.bObject = c
-        
-        props_sockets = self.gen_property_socket_map()
-        evaluate_sockets(self, c, props_sockets)
-        c.set_inverse_pending
-        self.executed = True
+        for xf in self.GetxForm():
+            print(wrapGreen("Creating ")+wrapWhite("Child-Of")+
+                wrapGreen(" Constraint for bone: ") +
+                wrapOrange(xf.bGetObject().name))
+            c = xf.bGetObject().constraints.new('CHILD_OF')
+            self.get_target_and_subtarget(c)
+            if constraint_name := self.evaluate_input("Name"):
+                c.name = constraint_name
+            self.bObject.append(c)
+            
+            props_sockets = self.gen_property_socket_map()
+            evaluate_sockets(self, c, props_sockets)
+            c.set_inverse_pending
+            self.executed = True
 
 
 class LinkInverseKinematics(MantisLinkNode):
 class LinkInverseKinematics(MantisLinkNode):
     def __init__(self, signature, base_tree):
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree, LinkInverseKinematicsSockets)
         super().__init__(signature, base_tree, LinkInverseKinematicsSockets)
         self.init_parameters(additional_parameters={"Name":None })
         self.init_parameters(additional_parameters={"Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
         self.set_traverse([("Input Relationship", "Output Relationship")])
-        
-    def GetxForm(self):
-        return GetxForm(self)
     
     
     def get_base_ik_bone(self, ik_bone):
     def get_base_ik_bone(self, ik_bone):
         chain_length : int = (self.evaluate_input("Chain Length"))
         chain_length : int = (self.evaluate_input("Chain Length"))
@@ -545,10 +517,10 @@ class LinkInverseKinematics(MantisLinkNode):
     # be clamped in that range.
     # be clamped in that range.
     # so we simply wrap the value.
     # so we simply wrap the value.
     # not very efficient but it's OK
     # not very efficient but it's OK
-    def set_pole_angle(self, angle: float) -> None:
+    def set_pole_angle(self, constraint, angle: float) -> None:
         from math import pi
         from math import pi
         from .utilities import wrap
         from .utilities import wrap
-        self.bObject.pole_angle = wrap(-pi, pi, angle)
+        constraint.pole_angle = wrap(-pi, pi, angle)
     
     
     def calc_pole_angle_pre(self, c, ik_bone):
     def calc_pole_angle_pre(self, c, ik_bone):
         """
         """
@@ -631,7 +603,7 @@ class LinkInverseKinematics(MantisLinkNode):
             dot_after=current_knee_direction.dot(knee_direction)
             dot_after=current_knee_direction.dot(knee_direction)
             if dot_after < dot_before: # they are somehow less aligned
             if dot_after < dot_before: # they are somehow less aligned
                 prPurple("Mantis has gone down an unexpected code path. Please report this as a bug.")
                 prPurple("Mantis has gone down an unexpected code path. Please report this as a bug.")
-                angle = -angle; self.set_pole_angle(angle)
+                angle = -angle; self.set_pole_angle(c, angle)
                 dg.update()
                 dg.update()
 
 
         # now we can do a bisect search to find the best value.
         # now we can do a bisect search to find the best value.
@@ -656,7 +628,7 @@ class LinkInverseKinematics(MantisLinkNode):
                 break
                 break
             # get the center-point betweeen the bounds
             # get the center-point betweeen the bounds
             try_angle = lower_bounds + (upper_bounds-lower_bounds)/2
             try_angle = lower_bounds + (upper_bounds-lower_bounds)/2
-            self.set_pole_angle(try_angle); dg.update()
+            self.set_pole_angle(c, try_angle); dg.update()
             prev_error = error
             prev_error = error
             error = signed_angle((base_ik_bone.tail-center_point), knee_direction, ik_axis)
             error = signed_angle((base_ik_bone.tail-center_point), knee_direction, ik_axis)
             error_identical+= int(error == prev_error)
             error_identical+= int(error == prev_error)
@@ -666,39 +638,41 @@ class LinkInverseKinematics(MantisLinkNode):
 
 
     def bExecute(self, context):
     def bExecute(self, context):
         prepare_parameters(self)
         prepare_parameters(self)
-        print(wrapGreen("Creating ")+wrapOrange("Inverse Kinematics")+
-             wrapGreen(" Constraint for bone: ") +
-             wrapOrange(self.GetxForm().bGetObject().name))
-        ik_bone = self.GetxForm().bGetObject()
-        c = self.GetxForm().bGetObject().constraints.new('IK')
-        self.get_target_and_subtarget(c)
-        self.get_target_and_subtarget(c, input_name = 'Pole Target')
-        if constraint_name := self.evaluate_input("Name"):
-            c.name = constraint_name
-        
-        self.bObject = c
-        c.chain_count = 1 # so that, if there are errors, this doesn't print
-        #  a whole bunch of circular dependency crap from having infinite chain length
-        if (c.pole_target):
-            self.set_pole_angle(self.calc_pole_angle_pre(c, ik_bone))
-
-        props_sockets = self.gen_property_socket_map()
-        evaluate_sockets(self, c, props_sockets)
-        c.use_location   = self.evaluate_input("Position") > 0
-        c.use_rotation   = self.evaluate_input("Rotation") > 0
+        for xf in self.GetxForm():
+            print(wrapGreen("Creating ")+wrapOrange("Inverse Kinematics")+
+                wrapGreen(" Constraint for bone: ") +
+                wrapOrange(xf.bGetObject().name))
+            ik_bone = xf.bGetObject()
+            c = xf.bGetObject().constraints.new('IK')
+            self.get_target_and_subtarget(c)
+            self.get_target_and_subtarget(c, input_name = 'Pole Target')
+            if constraint_name := self.evaluate_input("Name"):
+                c.name = constraint_name
+            
+            self.bObject.append(c)
+            c.chain_count = 1 # so that, if there are errors, this doesn't print
+            #  a whole bunch of circular dependency crap from having infinite chain length
+            if (c.pole_target):
+                self.set_pole_angle(c, self.calc_pole_angle_pre(c, ik_bone))
+
+            props_sockets = self.gen_property_socket_map()
+            evaluate_sockets(self, c, props_sockets)
+            c.use_location   = self.evaluate_input("Position") > 0
+            c.use_rotation   = self.evaluate_input("Rotation") > 0
         self.executed = True
         self.executed = True
 
 
     def bFinalize(self, bContext = None):
     def bFinalize(self, bContext = None):
         # adding a test here
         # adding a test here
         if bContext:
         if bContext:
-            ik_bone = self.GetxForm().bGetObject(mode='POSE')
-            if self.bObject.pole_target:
-                prWhite(f"Fine-tuning IK Pole Angle for {self}")
-                # make sure to enable it first
-                enabled_before = self.bObject.mute
-                self.bObject.mute = False
-                self.calc_pole_angle_post(self.bObject, ik_bone, bContext)
-                self.bObject.mute = enabled_before
+            for i, constraint in enumerate(self.bObject):
+                ik_bone = self.GetxForm()[i].bGetObject(mode='POSE')
+                if constraint.pole_target:
+                    prWhite(f"Fine-tuning IK Pole Angle for {self}")
+                    # make sure to enable it first
+                    enabled_before = constraint.mute
+                    constraint.mute = False
+                    self.calc_pole_angle_post(constraint, ik_bone, bContext)
+                    constraint.mute = enabled_before
         super().bFinalize(bContext)
         super().bFinalize(bContext)
         
         
 
 
@@ -727,38 +701,35 @@ class LinkDrivenParameter(MantisLinkNode):
         self.init_parameters(additional_parameters={ "Name":None })
         self.init_parameters(additional_parameters={ "Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
         self.set_traverse([("Input Relationship", "Output Relationship")])
 
 
-    def GetxForm(self):
-        return GetxForm(self)
-
     def bExecute(self, bContext = None,):
     def bExecute(self, bContext = None,):
         prepare_parameters(self)
         prepare_parameters(self)
         prGreen("Executing Driven Parameter node")
         prGreen("Executing Driven Parameter node")
         prop = self.evaluate_input("Parameter")
         prop = self.evaluate_input("Parameter")
         index = self.evaluate_input("Index")
         index = self.evaluate_input("Index")
         value = self.evaluate_input("Value")
         value = self.evaluate_input("Value")
-        xf = self.GetxForm()
-        ob = xf.bGetObject(mode="POSE")
-        # IMPORTANT: this node only works on pose bone attributes.
-        self.bObject = ob
-        length=1
-        if hasattr(ob, prop):
-            try:
-                length = len(getattr(ob, prop))
-            except TypeError:
-                pass
-            except AttributeError:
-                pass
-        else:
-            raise AttributeError(f"Cannot Set value {prop} on object because it does not exist.")
-        def_value = 0.0
-        if length>1:
-            def_value=[0.0]*length
-            self.parameters["Value"] = tuple( 0.0 if i != index else value for i in range(length))
+        for xf in self.GetxForm():
+            ob = xf.bGetObject(mode="POSE")
+            # IMPORTANT: this node only works on pose bone attributes.
+            self.bObject.append(ob)
+            length=1
+            if hasattr(ob, prop):
+                try:
+                    length = len(getattr(ob, prop))
+                except TypeError:
+                    pass
+                except AttributeError:
+                    pass
+            else:
+                raise AttributeError(f"Cannot Set value {prop} on object because it does not exist.")
+            def_value = 0.0
+            if length>1:
+                def_value=[0.0]*length
+                self.parameters["Value"] = tuple( 0.0 if i != index else value for i in range(length))
 
 
-        props_sockets = {
-            prop: ("Value", def_value)
-        }
-        evaluate_sockets(self, ob, props_sockets)
+            props_sockets = {
+                prop: ("Value", def_value)
+            }
+            evaluate_sockets(self, ob, props_sockets)
 
 
         self.executed = True
         self.executed = True
 
 
@@ -785,35 +756,35 @@ class LinkArmature(MantisLinkNode):
         self.set_traverse([("Input Relationship", "Output Relationship")])
         self.set_traverse([("Input Relationship", "Output Relationship")])
         setup_custom_props(self) # <-- this takes care of the runtime-added sockets
         setup_custom_props(self) # <-- this takes care of the runtime-added sockets
 
 
-    def GetxForm(self):
-        return GetxForm(self)
-
     def bExecute(self, bContext = None,):
     def bExecute(self, bContext = None,):
-        prGreen("Creating Armature Constraint for bone: \""+ self.GetxForm().bGetObject().name + "\"")
         prepare_parameters(self)
         prepare_parameters(self)
-        c = self.GetxForm().bGetObject().constraints.new('ARMATURE')
-        if constraint_name := self.evaluate_input("Name"):
-            c.name = constraint_name
-        self.bObject = c
-        # get number of targets
-        num_targets = len( list(self.inputs.values())[6:] )//2
-        
-        props_sockets = self.gen_property_socket_map()
-        targets_weights = {}
-        for i in range(num_targets):
-            target = c.targets.new()
-            target_input_name = list(self.inputs.keys())[i*2+6  ]
-            weight_input_name = list(self.inputs.keys())[i*2+6+1]
-            self.get_target_and_subtarget(target, target_input_name)
-            weight_value=self.evaluate_input(weight_input_name)
-            if not isinstance(weight_value, float):
-                weight_value=0
-            targets_weights[i]=weight_value
-            props_sockets["targets[%d].weight" % i] = (weight_input_name, 0)
-            # targets_weights.append({"weight":(weight_input_name, 0)})
-        evaluate_sockets(self, c, props_sockets)
-        for target, value in targets_weights.items():
-            c.targets[target].weight=value
+        for xf in self.GetxForm():
+            print(wrapGreen("Creating ")+wrapOrange("Armature")+
+                wrapGreen(" Constraint for bone: ") +
+                wrapOrange(xf.bGetObject().name))
+            c = xf.bGetObject().constraints.new('ARMATURE')
+            if constraint_name := self.evaluate_input("Name"):
+                c.name = constraint_name
+            self.bObject.append(c)
+            # get number of targets
+            num_targets = len( list(self.inputs.values())[6:] )//2
+            
+            props_sockets = self.gen_property_socket_map()
+            targets_weights = {}
+            for i in range(num_targets):
+                target = c.targets.new()
+                target_input_name = list(self.inputs.keys())[i*2+6  ]
+                weight_input_name = list(self.inputs.keys())[i*2+6+1]
+                self.get_target_and_subtarget(target, target_input_name)
+                weight_value=self.evaluate_input(weight_input_name)
+                if not isinstance(weight_value, float):
+                    weight_value=0
+                targets_weights[i]=weight_value
+                props_sockets["targets[%d].weight" % i] = (weight_input_name, 0)
+                # targets_weights.append({"weight":(weight_input_name, 0)})
+            evaluate_sockets(self, c, props_sockets)
+            for target, value in targets_weights.items():
+                c.targets[target].weight=value
         self.executed = True
         self.executed = True
 
 
 class LinkSplineIK(MantisLinkNode):
 class LinkSplineIK(MantisLinkNode):
@@ -824,25 +795,22 @@ class LinkSplineIK(MantisLinkNode):
         self.init_parameters(additional_parameters={"Name":None })
         self.init_parameters(additional_parameters={"Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
         self.set_traverse([("Input Relationship", "Output Relationship")])
 
 
-    def GetxForm(self):
-        return GetxForm(self)
-
     def bExecute(self, bContext = None,):
     def bExecute(self, bContext = None,):
         prepare_parameters(self)
         prepare_parameters(self)
-        if not self.inputs['Target'].is_linked:
-            print(f"INFO: {self} is not connected to any target, it will not be generated.")
-            return
-        prGreen("Creating Spline-IK Constraint for bone: \""+ self.GetxForm().bGetObject().name + "\"")
-        c = self.GetxForm().bGetObject().constraints.new('SPLINE_IK')
-        # set the spline - we need to get the right one 
-        spline_index = self.evaluate_input("Spline Index")
-        from .utilities import get_extracted_spline_object
-        proto_curve = self.inputs['Target'].links[0].from_node.bGetObject()
-        curve = get_extracted_spline_object(proto_curve, spline_index, self.mContext)
-        c.target=curve
-        if constraint_name := self.evaluate_input("Name"):
-            c.name = constraint_name
-        self.bObject = c
-        props_sockets = self.gen_property_socket_map()
-        evaluate_sockets(self, c, props_sockets)
+        for xf in self.GetxForm():
+            print(wrapGreen("Creating ")+wrapOrange("Spline-IK")+
+                wrapGreen(" Constraint for bone: ") +
+                wrapOrange(xf.bGetObject().name))
+            c = xf.bGetObject().constraints.new('SPLINE_IK')
+            # set the spline - we need to get the right one 
+            spline_index = self.evaluate_input("Spline Index")
+            from .utilities import get_extracted_spline_object
+            proto_curve = self.inputs['Target'].links[0].from_node.bGetObject()
+            curve = get_extracted_spline_object(proto_curve, spline_index, self.mContext)
+            c.target=curve
+            if constraint_name := self.evaluate_input("Name"):
+                c.name = constraint_name
+            self.bObject.append(c)
+            props_sockets = self.gen_property_socket_map()
+            evaluate_sockets(self, c, props_sockets)
         self.executed = True
         self.executed = True

+ 36 - 41
node_container_common.py

@@ -8,7 +8,6 @@ from collections.abc import Callable
 # the x_containers files import * from this file
 # the x_containers files import * from this file
 # so all the top-level imports are carried over
 # so all the top-level imports are carried over
 
 
-
 #TODO: refactor the socket definitions so this becomes unnecessary.
 #TODO: refactor the socket definitions so this becomes unnecessary.
 def get_socket_value(node_socket):
 def get_socket_value(node_socket):
     value = None
     value = None
@@ -18,8 +17,6 @@ def get_socket_value(node_socket):
         value =  node_socket.TellValue()
         value =  node_socket.TellValue()
     return value
     return value
 
 
-
-
 # TODO: modify this to work with multi-input nodes
 # TODO: modify this to work with multi-input nodes
 def trace_single_line(node_container, input_name, link_index=0):
 def trace_single_line(node_container, input_name, link_index=0):
     # DO: refactor this for new link class
     # DO: refactor this for new link class
@@ -39,7 +36,6 @@ def trace_single_line(node_container, input_name, link_index=0):
                 break
                 break
     return nodes, socket
     return nodes, socket
 
 
-
 # this is same as the other, just flip from/to and in/out
 # this is same as the other, just flip from/to and in/out
 def trace_single_line_up(node_container, output_name,):
 def trace_single_line_up(node_container, output_name,):
     """I use this to get the xForm from a link node."""
     """I use this to get the xForm from a link node."""
@@ -82,7 +78,7 @@ def trace_line_up_branching(node : MantisNode, output_name : str,
                         socket = other
                         socket = other
                         if break_condition(socket.node):
                         if break_condition(socket.node):
                             leaf_nodes.append(socket.node)
                             leaf_nodes.append(socket.node)
-                        if socket.can_traverse:
+                        elif socket.can_traverse:
                             check_sockets.append(socket.traverse_target)
                             check_sockets.append(socket.traverse_target)
                         else: # this is an input.
                         else: # this is an input.
                             leaf_nodes.append(socket.node)
                             leaf_nodes.append(socket.node)
@@ -137,14 +133,13 @@ def check_for_driver(node_container, input_name, index = None):
         prop = prop[index]
         prop = prop[index]
     return (prop.__class__.__name__ == 'MantisDriver')
     return (prop.__class__.__name__ == 'MantisDriver')
 
 
-
 # TODO: this should handle sub-properties better
 # TODO: this should handle sub-properties better
-def evaluate_sockets(nc, c, props_sockets):
+def evaluate_sockets(nc, b_object, props_sockets,):
     # this is neccesary because some things use dict properties for dynamic properties and setattr doesn't work
     # this is neccesary because some things use dict properties for dynamic properties and setattr doesn't work
     def safe_setattr(ob, att_name, val):
     def safe_setattr(ob, att_name, val):
         if ob.__class__.__name__ in ["NodesModifier"]:
         if ob.__class__.__name__ in ["NodesModifier"]:
             ob[att_name]=val
             ob[att_name]=val
-        elif c.__class__.__name__ in ["Key"]:
+        elif b_object.__class__.__name__ in ["Key"]:
             if not val: val=0
             if not val: val=0
             ob.key_blocks[att_name].value=val
             ob.key_blocks[att_name].value=val
         elif "]." in att_name:
         elif "]." in att_name:
@@ -152,14 +147,13 @@ def evaluate_sockets(nc, c, props_sockets):
             prop=att_name.split('[')[0]
             prop=att_name.split('[')[0]
             prop1=att_name.split('.')[1]
             prop1=att_name.split('.')[1]
             index = int(att_name.split('[')[1][0])
             index = int(att_name.split('[')[1][0])
-            setattr(getattr(c, prop)[index], prop1, val)
+            setattr(getattr(b_object, prop)[index], prop1, val)
         else:
         else:
             try:
             try:
                 setattr(ob, att_name, val)
                 setattr(ob, att_name, val)
             except Exception as e:
             except Exception as e:
                 prRed(ob, att_name, val); raise e
                 prRed(ob, att_name, val); raise e
     for prop, (sock, default) in props_sockets.items():
     for prop, (sock, default) in props_sockets.items():
-        # c = nc.bObject
         # annoyingly, sometimes the socket is an array
         # annoyingly, sometimes the socket is an array
         index = None
         index = None
         if isinstance(sock, tuple):
         if isinstance(sock, tuple):
@@ -168,8 +162,8 @@ def evaluate_sockets(nc, c, props_sockets):
             sock = (sock, index)
             sock = (sock, index)
             original_prop = prop
             original_prop = prop
             # TODO: deduplicate this terrible hack
             # TODO: deduplicate this terrible hack
-            if ("." in prop) and not c.__class__.__name__ in ["Key"]: # this is a property of a property...
-                sub_props = [c]
+            if ("." in prop) and not b_object.__class__.__name__ in ["Key"]: # this is a property of a property...
+                sub_props = [b_object]
                 while ("." in prop):
                 while ("." in prop):
                     split_prop = prop.split(".")
                     split_prop = prop.split(".")
                     prop = split_prop[1]
                     prop = split_prop[1]
@@ -183,59 +177,58 @@ def evaluate_sockets(nc, c, props_sockets):
                 safe_setattr(sub_props[-1], prop, default)
                 safe_setattr(sub_props[-1], prop, default)
             # this is really stupid
             # this is really stupid
             else:
             else:
-                safe_setattr(c, prop, default)
+                safe_setattr(b_object, prop, default)
             if nc.node_type in ['LINK',]:
             if nc.node_type in ['LINK',]:
-                printname  = wrapOrange(nc.GetxForm().bGetObject().name)
+                printname  = wrapOrange(b_object.id_data.name)
             elif nc.node_type in ['XFORM',]:
             elif nc.node_type in ['XFORM',]:
                 printname  = wrapOrange(nc.bGetObject().name)
                 printname  = wrapOrange(nc.bGetObject().name)
             else:
             else:
                 printname = wrapOrange(nc)
                 printname = wrapOrange(nc)
             print("Adding driver %s to %s in %s" % (wrapPurple(original_prop), wrapWhite(nc.signature[-1]), printname))
             print("Adding driver %s to %s in %s" % (wrapPurple(original_prop), wrapWhite(nc.signature[-1]), printname))
-            if c.__class__.__name__ in ["NodesModifier"]:
+            if b_object.__class__.__name__ in ["NodesModifier"]:
                 nc.drivers[sock] = "[\""+original_prop+"\"]" # lol. It is a dict element not a "true" property
                 nc.drivers[sock] = "[\""+original_prop+"\"]" # lol. It is a dict element not a "true" property
-            elif c.__class__.__name__ in ["Key"]:
+            elif b_object.__class__.__name__ in ["Key"]:
                 nc.drivers[sock] = "key_blocks[\""+original_prop+"\"].value"
                 nc.drivers[sock] = "key_blocks[\""+original_prop+"\"].value"
             else:
             else:
                 nc.drivers[sock] = original_prop
                 nc.drivers[sock] = original_prop
         else: # here we can do error checking for the socket if needed
         else: # here we can do error checking for the socket if needed
             if (index is not None):
             if (index is not None):
-                safe_setattr(c, prop, nc.evaluate_input(sock)[index])
+                safe_setattr(b_object, prop, nc.evaluate_input(sock)[index])
             else:                    # 'mute' is better than 'enabled'
             else:                    # 'mute' is better than 'enabled'
                 # UGLY HACK          # because it is available in older
                 # UGLY HACK          # because it is available in older
                 if (prop == 'mute'): # Blenders.
                 if (prop == 'mute'): # Blenders.
-                    safe_setattr(c, prop, not bool(nc.evaluate_input(sock)))
+                    safe_setattr(b_object, prop, not bool(nc.evaluate_input(sock)))
                 elif (prop == 'hide'): # this will not cast it for me, annoying.
                 elif (prop == 'hide'): # this will not cast it for me, annoying.
-                    safe_setattr(c, prop, bool(nc.evaluate_input(sock)))
+                    safe_setattr(b_object, prop, bool(nc.evaluate_input(sock)))
                 else:
                 else:
                     try:
                     try:
-                        # prRed(c.name, nc, prop, nc.evaluate_input(sock) )
+                        # prRed(b_object.name, nc, prop, nc.evaluate_input(sock) )
                         # print( nc.evaluate_input(sock))
                         # print( nc.evaluate_input(sock))
                     # value_eval = nc.evaluate_input(sock)
                     # value_eval = nc.evaluate_input(sock)
                     # just wanna see if we are dealing with some collection
                     # 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
                     # check hasattr in case it is one of those ["such-and-such"] props, and ignore those
-                        if hasattr(c, prop) and (not isinstance(getattr(c, prop), str)) and hasattr(getattr(c, prop), "__getitem__"):
+                        if hasattr(b_object, prop) and (not isinstance(getattr(b_object, prop), str)) and hasattr(getattr(b_object, prop), "__getitem__"):
                             # prGreen("Doing the thing")
                             # prGreen("Doing the thing")
                             for val_index, value in enumerate(nc.evaluate_input(sock)):
                             for val_index, value in enumerate(nc.evaluate_input(sock)):
                                 # assume this will work, both because val should have the right number of elements, and because this should be the right data type.
                                 # 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
                                 from .drivers import MantisDriver
                                 if isinstance(value, MantisDriver):
                                 if isinstance(value, MantisDriver):
-                                    getattr(c,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(nc.signature[-1]), nc))
                                     print("Adding driver %s to %s in %s" % (wrapPurple(prop), wrapWhite(nc.signature[-1]), nc))
                                     try:
                                     try:
                                         nc.drivers[sock].append((prop, val_index))
                                         nc.drivers[sock].append((prop, val_index))
                                     except:
                                     except:
                                         nc.drivers[sock] = [(prop, val_index)]
                                         nc.drivers[sock] = [(prop, val_index)]
                                 else:
                                 else:
-                                    getattr(c,prop)[val_index] =  value
+                                    getattr(b_object,prop)[val_index] =  value
                         else:
                         else:
-                            # prOrange("Skipping the Thing", getattr(c, prop))
-                            safe_setattr(c, prop, nc.evaluate_input(sock))
+                            # prOrange("Skipping the Thing", getattr(b_object, prop))
+                            safe_setattr(b_object, prop, nc.evaluate_input(sock))
                     except Exception as e:
                     except Exception as e:
-                        prRed(c, nc, prop, sock, nc.evaluate_input(sock))
+                        prRed(b_object, nc, prop, sock, nc.evaluate_input(sock))
                         raise e
                         raise e
 
 
-
-def finish_driver(nc, driver_item, prop):
+def finish_driver(nc, b_object, driver_item, prop):
     # prWhite(nc, prop)
     # prWhite(nc, prop)
     index = driver_item[1]; driver_sock = driver_item[0]
     index = driver_item[1]; driver_sock = driver_item[0]
     driver_trace = trace_single_line(nc, driver_sock)
     driver_trace = trace_single_line(nc, driver_sock)
@@ -247,12 +240,8 @@ def finish_driver(nc, driver_item, prop):
     else:
     else:
         driver = driver_provider.parameters[driver_socket.name].copy()
         driver = driver_provider.parameters[driver_socket.name].copy()
     if driver:
     if driver:
-        # todo: deduplicate this terrible hack
-        c = None # no idea what this c and sub_prop thing is, HACK?
-        if hasattr(nc, "bObject"):
-            c = nc.bObject # STUPID                 # stupid and bad HACK here too
         if ("." in prop) and nc.__class__.__name__ != "DeformerMorphTargetDeform": # this is a property of a property...
         if ("." in prop) and nc.__class__.__name__ != "DeformerMorphTargetDeform": # this is a property of a property...
-            sub_props = [c]
+            sub_props = [b_object]
             while ("." in prop):
             while ("." in prop):
                 split_prop = prop.split(".")
                 split_prop = prop.split(".")
                 prop = split_prop[1]
                 prop = split_prop[1]
@@ -270,13 +259,13 @@ def finish_driver(nc, driver_item, prop):
                 bone_col = nc.bGetParentArmature().data.bones
                 bone_col = nc.bGetParentArmature().data.bones
             else:
             else:
                 bone_col = nc.bGetParentArmature().pose.bones
                 bone_col = nc.bGetParentArmature().pose.bones
-            driver["owner"] = bone_col[nc.bObject] # we use "unsafe" brackets instead of get() because we want to see any errors that occur
+            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
         # HACK having special cases here is indicitave of a deeper problem that should be refactored
         elif nc.__class__.__name__ in ['xFormCurvePin'] and \
         elif nc.__class__.__name__ in ['xFormCurvePin'] and \
                       prop in ['offset_factor', 'forward_axis', 'up_axis']:
                       prop in ['offset_factor', 'forward_axis', 'up_axis']:
-                driver["owner"] = nc.bObject.constraints['Curve Pin']
+                driver["owner"] = b_object.constraints['Curve Pin']
         else:
         else:
-            driver["owner"] = nc.bObject
+            driver["owner"] = b_object
         driver["prop"] = prop
         driver["prop"] = prop
         return driver
         return driver
     else:
     else:
@@ -292,10 +281,16 @@ def finish_drivers(nc):
     if not hasattr(nc, "drivers"):
     if not hasattr(nc, "drivers"):
         return # HACK
         return # HACK
     for driver_item, prop in nc.drivers.items():
     for driver_item, prop in nc.drivers.items():
-        if isinstance(prop, list):
-            for sub_item in prop:
-                drivers.append(finish_driver(nc, (driver_item, sub_item[1]), sub_item[0]))
-        else:
-            drivers.append(finish_driver(nc, driver_item, prop))
+        b_objects = [nc.bObject]
+        if nc.node_type == 'LINK':
+            b_objects = nc.bObject # this is already a list
+        for b_object in b_objects:
+            if isinstance(prop, list):
+                for sub_item in prop:
+                        drivers.append(finish_driver(nc, b_object, (driver_item, sub_item[1]), sub_item[0]))
+                else:
+                    drivers.append(finish_driver(nc, b_object, (driver_item, sub_item[1]), sub_item[0]))
+            else:
+                drivers.append(finish_driver(nc, b_object, driver_item, prop))
     from .drivers import CreateDrivers
     from .drivers import CreateDrivers
     CreateDrivers(drivers)
     CreateDrivers(drivers)