Selaa lähdekoodia

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 kuukautta sitten
vanhempi
commit
6c6c12bf2a
4 muutettua tiedostoa jossa 501 lisäystä ja 556 poistoa
  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_MINOR=10
-MANTIS_VERSION_SUB=9
+MANTIS_VERSION_SUB=11
 
 classLists = [module.TellClasses() for module in [
  link_definitions,

+ 135 - 153
deformer_containers.py

@@ -1,5 +1,5 @@
 from .node_container_common import *
-from .xForm_containers import xFormGeometryObject
+from .xForm_containers import xFormGeometryObject, xFormObjectInstance
 from .misc_nodes import InputExistingGeometryObject
 from .base_definitions import MantisNode, MantisSocketTemplate
 from .utilities import (prRed, prGreen, prPurple, prWhite, prOrange,
@@ -19,26 +19,17 @@ def TellClasses():
              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):
-    from .xForm_containers import xFormGeometryObject
-    from .misc_nodes import InputExistingGeometryObject
-    from bpy.types import Object
     if (trace := trace_single_line(nc, socket)[0] ) :
         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()
         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):
     def __init__(self, signature : tuple,
                  base_tree : NodeTree,
@@ -46,6 +37,7 @@ class MantisDeformerNode(MantisNode):
         super().__init__(signature, base_tree, socket_templates)
         self.node_type = 'LINK'
         self.prepared = True
+        self.bObject=[]
     # we need evaluate_input to have the same behaviour as links.
     def evaluate_input(self, input_name, index=0):
         if ('Target' in input_name):
@@ -56,6 +48,16 @@ class MantisDeformerNode(MantisNode):
             
         else:
             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):
     '''A node representing an armature deformer'''
@@ -88,7 +90,7 @@ class DeformerArmature(MantisDeformerNode):
 
     def GetxForm(self, socket="Deformer"):
         if socket == "Deformer":
-            return GetxForm(self)
+            return super().GetxForm()
         else:
             trace_xForm_back(self, socket)
     
@@ -113,8 +115,8 @@ class DeformerArmature(MantisDeformerNode):
     def bExecute(self, bContext = None,):
         self.executed = True
     
-    def initialize_vgroups(self,):
-        ob = self.GetxForm().bGetObject()
+    def initialize_vgroups(self, xf):
+        ob = xf.bGetObject()
         armOb = self.bGetParentArmature()
         for b in armOb.data.bones:
             if b.use_deform == False:
@@ -125,10 +127,10 @@ class DeformerArmature(MantisDeformerNode):
                 num_verts = len(ob.data.vertices)
                 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
         import bpy
-        ob = self.GetxForm().bGetObject()
+        ob = xf.bGetObject()
         try:
             copy_from = self.GetxForm(socket="Copy Skin Weights From")
         except GraphError:
@@ -162,64 +164,55 @@ class DeformerArmature(MantisDeformerNode):
     def bFinalize(self, bContext=None):
         prGreen("Executing Armature Deform Node")
         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):
@@ -278,12 +271,6 @@ class DeformerHook(MantisDeformerNode):
                 var1['channel']="SCALE_"+axes[i]
                 driver['vars'].append(var1)
         CreateDrivers([driver])
-
-    def GetxForm(self, socket="Deformer"):
-        if socket == "Deformer":
-            return GetxForm(self)
-        else:
-            trace_xForm_back(self, socket)
             
     def bExecute(self, bContext = None,):
         self.executed = True
@@ -299,62 +286,62 @@ class DeformerHook(MantisDeformerNode):
         props_sockets = self.gen_property_socket_map()
         if isinstance(target, Bone) or isinstance(target, PoseBone):
             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)
         # 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...
@@ -385,7 +372,7 @@ class DeformerMorphTarget(MantisDeformerNode):
     def GetxForm(self, trace_input="Object"):
         trace = trace_single_line(self, trace_input)
         for node in trace[0]:
-            if (node.__class__ in [xFormGeometryObject, InputExistingGeometryObject]):
+            if (isinstance(node, deformable_types)):
                 return node
         raise GraphError("%s is not connected to an upstream xForm" % self)
 
@@ -436,17 +423,12 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
         self.node_type = "LINK"
         self.prepared = True
         self.executed = True
-        self.bObject = None
         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.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
         targets = []
         for k,v in self.inputs.items():
@@ -459,15 +441,15 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
         from .geometry_node_graphgen import gen_morph_target_nodes
         m, props_sockets = gen_morph_target_nodes(
                             self.evaluate_input("Name"),
-                            self.GetxForm().bGetObject(),
+                            xf.bGetObject(),
                             targets,
                             context,
                             use_offset=self.evaluate_input("Use Offset"))
-        self.bObject = m
+        self.bObject.append(m)
         evaluate_sockets(self, m, props_sockets)
         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.
         # there are a number of things I need to fix here
         #   - reuse shape keys if possible
@@ -483,7 +465,6 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
         from time import time
         start_time = time()
         from bpy import data
-        xf = self.GetxForm()
         ob = xf.bGetObject()
         dg = context.view_layer.depsgraph
         dg.update()
@@ -541,7 +522,7 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
                 sk = keys.get(mt_name)
                 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)
         finish_drivers(self)
         prWhite(f"Initializing morph target took {time() -start_time} seconds")
@@ -549,9 +530,9 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
 
     def bFinalize(self, bContext=None):
         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
         # 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 self.inputs.get("Deformer"): # I guess this isn't available in some node group contexts... bad. FIXME
                 if (links := self.inputs["Deformer"].links):
@@ -560,7 +541,8 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
                     elif links[0].from_node.parameters.get("Use Shape Key") == False:
                         use_shape_keys = False
         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)
         self.node_type = 'LINK'
         self.prepared = True
+        self.bObject=[]
 
     def evaluate_input(self, input_name, index=0):
         # should catch 'Target', 'Pole Target' and ArmatureConstraint targets, too
@@ -81,6 +82,16 @@ class MantisLinkNode(MantisNode):
             else:
                 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):
         finish_drivers(self)
 
@@ -88,12 +99,6 @@ class MantisLinkNode(MantisNode):
 # 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):
     '''A node representing inheritance'''
@@ -104,7 +109,7 @@ class LinkInherit(MantisLinkNode):
         self.set_traverse([('Parent', 'Inheritance')])
         self.executed = True
     
-    def GetxForm(self): # DUPLICATED, TODO fix this
+    def GetxForm(self):
         # I think this is only run in display update.
         trace = trace_single_line_up(self, "Inheritance")
         for node in trace[0]:
@@ -121,23 +126,21 @@ class LinkCopyLocation(MantisLinkNode):
         additional_parameters = { "Name":None }
         self.init_parameters(additional_parameters=additional_parameters)
         self.set_traverse([("Input Relationship", "Output Relationship")])
-        
-    def GetxForm(self):
-        return GetxForm(self)
 
     def bExecute(self, context):
         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
         
 class LinkCopyRotation(MantisLinkNode):
@@ -148,32 +151,30 @@ class LinkCopyRotation(MantisLinkNode):
         additional_parameters = { "Name":None }
         self.init_parameters(additional_parameters=additional_parameters)
         self.set_traverse([("Input Relationship", "Output Relationship")])
-        
-    def GetxForm(self):
-        return GetxForm(self)
 
     def bExecute(self, context):
         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'
-        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
         
 class LinkCopyScale(MantisLinkNode):
@@ -184,36 +185,34 @@ class LinkCopyScale(MantisLinkNode):
         additional_parameters = { "Name":None }
         self.init_parameters(additional_parameters=additional_parameters)
         self.set_traverse([("Input Relationship", "Output Relationship")])
-    
-    def GetxForm(self):
-        return GetxForm(self)
 
     def bExecute(self, context):
         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 
             
 
@@ -225,23 +224,21 @@ class LinkCopyTransforms(MantisLinkNode):
         additional_parameters = { "Name":None }
         self.init_parameters(additional_parameters=additional_parameters)
         self.set_traverse([("Input Relationship", "Output Relationship")])
-        
-    def GetxForm(self):
-        return GetxForm(self)
 
     def bExecute(self, context):
         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
 
 class LinkTransformation(MantisLinkNode):
@@ -251,51 +248,49 @@ class LinkTransformation(MantisLinkNode):
         super().__init__(signature, base_tree, LinkTransformationSockets)
         self.init_parameters(additional_parameters={"Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
-        
-    def GetxForm(self):
-        return GetxForm(self)
 
     def bExecute(self, context):
         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
 
 class LinkLimitLocation(MantisLinkNode):
@@ -303,24 +298,20 @@ class LinkLimitLocation(MantisLinkNode):
         super().__init__(signature, base_tree, LinkLimitLocationScaleSockets)
         self.init_parameters(additional_parameters={ "Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
-        
-    def GetxForm(self):
-        return GetxForm(self)
 
     def bExecute(self, context):
         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
         
 class LinkLimitRotation(MantisLinkNode):
@@ -328,23 +319,21 @@ class LinkLimitRotation(MantisLinkNode):
         super().__init__(signature, base_tree, LinkLimitRotationSockets)
         self.init_parameters(additional_parameters={ "Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
-        
-    def GetxForm(self):
-        return GetxForm(self)
 
     def bExecute(self, context):
         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
 
 class LinkLimitScale(MantisLinkNode):
@@ -352,23 +341,21 @@ class LinkLimitScale(MantisLinkNode):
         super().__init__(signature, base_tree, LinkLimitLocationScaleSockets)
         self.init_parameters(additional_parameters={ "Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
-    
-    def GetxForm(self):
-        return GetxForm(self)
 
     def bExecute(self, context):
         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
  
 class LinkLimitDistance(MantisLinkNode):
@@ -376,23 +363,21 @@ class LinkLimitDistance(MantisLinkNode):
         super().__init__(signature, base_tree, LinkLimitDistanceSockets)
         self.init_parameters(additional_parameters={ "Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
-        
-    def GetxForm(self):
-        return GetxForm(self)
 
     def bExecute(self, context):
         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
 
 # Tracking
@@ -402,26 +387,24 @@ class LinkStretchTo(MantisLinkNode):
         super().__init__(signature, base_tree, LinkStretchToSockets)
         self.init_parameters(additional_parameters={ "Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
-        
-    def GetxForm(self):
-        return GetxForm(self)
 
     def bExecute(self, context):
         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
 
 class LinkDampedTrack(MantisLinkNode):
@@ -430,21 +413,19 @@ class LinkDampedTrack(MantisLinkNode):
         self.init_parameters(additional_parameters={ "Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
 
-    def GetxForm(self):
-        return GetxForm(self)
-
     def bExecute(self, context):
         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
 
 class LinkLockedTrack(MantisLinkNode):
@@ -453,21 +434,19 @@ class LinkLockedTrack(MantisLinkNode):
         self.init_parameters(additional_parameters={"Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
 
-    def GetxForm(self):
-        return GetxForm(self)
-
     def bExecute(self, context):
         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
 
 class LinkTrackTo(MantisLinkNode):
@@ -476,21 +455,19 @@ class LinkTrackTo(MantisLinkNode):
         self.init_parameters(additional_parameters={"Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
 
-    def GetxForm(self):
-        return GetxForm(self)
-
     def bExecute(self, context):
         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
 
 
@@ -500,33 +477,28 @@ class LinkInheritConstraint(MantisLinkNode):
         self.init_parameters(additional_parameters={"Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
 
-    def GetxForm(self):
-        return GetxForm(self)
-
     def bExecute(self, context):
         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):
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree, LinkInverseKinematicsSockets)
         self.init_parameters(additional_parameters={"Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
-        
-    def GetxForm(self):
-        return GetxForm(self)
     
     def get_base_ik_bone(self, ik_bone):
         chain_length : int = (self.evaluate_input("Chain Length"))
@@ -545,10 +517,10 @@ class LinkInverseKinematics(MantisLinkNode):
     # be clamped in that range.
     # so we simply wrap the value.
     # 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 .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):
         """
@@ -631,7 +603,7 @@ class LinkInverseKinematics(MantisLinkNode):
             dot_after=current_knee_direction.dot(knee_direction)
             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.")
-                angle = -angle; self.set_pole_angle(angle)
+                angle = -angle; self.set_pole_angle(c, angle)
                 dg.update()
 
         # now we can do a bisect search to find the best value.
@@ -656,7 +628,7 @@ class LinkInverseKinematics(MantisLinkNode):
                 break
             # get the center-point betweeen the bounds
             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
             error = signed_angle((base_ik_bone.tail-center_point), knee_direction, ik_axis)
             error_identical+= int(error == prev_error)
@@ -666,39 +638,41 @@ class LinkInverseKinematics(MantisLinkNode):
 
     def bExecute(self, context):
         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
 
     def bFinalize(self, bContext = None):
         # adding a test here
         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)
         
 
@@ -727,38 +701,35 @@ class LinkDrivenParameter(MantisLinkNode):
         self.init_parameters(additional_parameters={ "Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
 
-    def GetxForm(self):
-        return GetxForm(self)
-
     def bExecute(self, bContext = None,):
         prepare_parameters(self)
         prGreen("Executing Driven Parameter node")
         prop = self.evaluate_input("Parameter")
         index = self.evaluate_input("Index")
         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
 
@@ -785,35 +756,35 @@ class LinkArmature(MantisLinkNode):
         self.set_traverse([("Input Relationship", "Output Relationship")])
         setup_custom_props(self) # <-- this takes care of the runtime-added sockets
 
-    def GetxForm(self):
-        return GetxForm(self)
-
     def bExecute(self, bContext = None,):
-        prGreen("Creating Armature Constraint for bone: \""+ self.GetxForm().bGetObject().name + "\"")
         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
 
 class LinkSplineIK(MantisLinkNode):
@@ -824,25 +795,22 @@ class LinkSplineIK(MantisLinkNode):
         self.init_parameters(additional_parameters={"Name":None })
         self.set_traverse([("Input Relationship", "Output Relationship")])
 
-    def GetxForm(self):
-        return GetxForm(self)
-
     def bExecute(self, bContext = None,):
         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

+ 36 - 41
node_container_common.py

@@ -8,7 +8,6 @@ from collections.abc import Callable
 # the x_containers files import * from this file
 # so all the top-level imports are carried over
 
-
 #TODO: refactor the socket definitions so this becomes unnecessary.
 def get_socket_value(node_socket):
     value = None
@@ -18,8 +17,6 @@ def get_socket_value(node_socket):
         value =  node_socket.TellValue()
     return value
 
-
-
 # TODO: modify this to work with multi-input nodes
 def trace_single_line(node_container, input_name, link_index=0):
     # DO: refactor this for new link class
@@ -39,7 +36,6 @@ def trace_single_line(node_container, input_name, link_index=0):
                 break
     return nodes, socket
 
-
 # this is same as the other, just flip from/to and in/out
 def trace_single_line_up(node_container, output_name,):
     """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
                         if break_condition(socket.node):
                             leaf_nodes.append(socket.node)
-                        if socket.can_traverse:
+                        elif socket.can_traverse:
                             check_sockets.append(socket.traverse_target)
                         else: # this is an input.
                             leaf_nodes.append(socket.node)
@@ -137,14 +133,13 @@ def check_for_driver(node_container, input_name, index = None):
         prop = prop[index]
     return (prop.__class__.__name__ == 'MantisDriver')
 
-
 # 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
     def safe_setattr(ob, att_name, val):
         if ob.__class__.__name__ in ["NodesModifier"]:
             ob[att_name]=val
-        elif c.__class__.__name__ in ["Key"]:
+        elif b_object.__class__.__name__ in ["Key"]:
             if not val: val=0
             ob.key_blocks[att_name].value=val
         elif "]." in att_name:
@@ -152,14 +147,13 @@ def evaluate_sockets(nc, c, props_sockets):
             prop=att_name.split('[')[0]
             prop1=att_name.split('.')[1]
             index = int(att_name.split('[')[1][0])
-            setattr(getattr(c, prop)[index], prop1, val)
+            setattr(getattr(b_object, prop)[index], prop1, val)
         else:
             try:
                 setattr(ob, att_name, val)
             except Exception as e:
                 prRed(ob, att_name, val); raise e
     for prop, (sock, default) in props_sockets.items():
-        # c = nc.bObject
         # annoyingly, sometimes the socket is an array
         index = None
         if isinstance(sock, tuple):
@@ -168,8 +162,8 @@ def evaluate_sockets(nc, c, props_sockets):
             sock = (sock, index)
             original_prop = prop
             # 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):
                     split_prop = prop.split(".")
                     prop = split_prop[1]
@@ -183,59 +177,58 @@ def evaluate_sockets(nc, c, props_sockets):
                 safe_setattr(sub_props[-1], prop, default)
             # this is really stupid
             else:
-                safe_setattr(c, prop, default)
+                safe_setattr(b_object, prop, default)
             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',]:
                 printname  = wrapOrange(nc.bGetObject().name)
             else:
                 printname = wrapOrange(nc)
             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
-            elif c.__class__.__name__ in ["Key"]:
+            elif b_object.__class__.__name__ in ["Key"]:
                 nc.drivers[sock] = "key_blocks[\""+original_prop+"\"].value"
             else:
                 nc.drivers[sock] = original_prop
         else: # here we can do error checking for the socket if needed
             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'
                 # UGLY HACK          # because it is available in older
                 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.
-                    safe_setattr(c, prop, bool(nc.evaluate_input(sock)))
+                    safe_setattr(b_object, prop, bool(nc.evaluate_input(sock)))
                 else:
                     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))
                     # value_eval = nc.evaluate_input(sock)
                     # just wanna see if we are dealing with some collection
                     # check hasattr in case it is one of those ["such-and-such"] props, and ignore those
-                        if hasattr(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")
                             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.
                                 from .drivers import 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))
                                     try:
                                         nc.drivers[sock].append((prop, val_index))
                                     except:
                                         nc.drivers[sock] = [(prop, val_index)]
                                 else:
-                                    getattr(c,prop)[val_index] =  value
+                                    getattr(b_object,prop)[val_index] =  value
                         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:
-                        prRed(c, nc, prop, sock, nc.evaluate_input(sock))
+                        prRed(b_object, nc, prop, sock, nc.evaluate_input(sock))
                         raise e
 
-
-def finish_driver(nc, driver_item, prop):
+def finish_driver(nc, b_object, driver_item, prop):
     # prWhite(nc, prop)
     index = driver_item[1]; driver_sock = driver_item[0]
     driver_trace = trace_single_line(nc, driver_sock)
@@ -247,12 +240,8 @@ def finish_driver(nc, driver_item, prop):
     else:
         driver = driver_provider.parameters[driver_socket.name].copy()
     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...
-            sub_props = [c]
+            sub_props = [b_object]
             while ("." in prop):
                 split_prop = prop.split(".")
                 prop = split_prop[1]
@@ -270,13 +259,13 @@ def finish_driver(nc, driver_item, prop):
                 bone_col = nc.bGetParentArmature().data.bones
             else:
                 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
         elif nc.__class__.__name__ in ['xFormCurvePin'] and \
                       prop in ['offset_factor', 'forward_axis', 'up_axis']:
-                driver["owner"] = nc.bObject.constraints['Curve Pin']
+                driver["owner"] = b_object.constraints['Curve Pin']
         else:
-            driver["owner"] = nc.bObject
+            driver["owner"] = b_object
         driver["prop"] = prop
         return driver
     else:
@@ -292,10 +281,16 @@ def finish_drivers(nc):
     if not hasattr(nc, "drivers"):
         return # HACK
     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
     CreateDrivers(drivers)