Browse Source

the big one! after months of sporadic development, it's ready. Added simple math nodes, Schemas, completely rewrote grouping, tree parsing.. everything really

Joseph Brandenburg 9 months ago
parent
commit
0b8a18201f

+ 1 - 0
.gitignore

@@ -1,2 +1,3 @@
 *__pycache__*
 grandalf/*
+removed/*

+ 133 - 62
__init__.py

@@ -6,10 +6,14 @@ from . import ( ops_nodegroup,
                 nodes_generic,
                 primitives_definitions,
                 deformer_definitions,
+                math_definitions,
+                i_o,
+                schema_definitions,
               )
-from .ops_generate_tree import CreateMantisTree
+from .ops_generate_tree import GenerateMantisTree
 from bpy.types import NodeSocket
 
+from .utilities import prRed
 
 
 classLists = [module.TellClasses() for module in [
@@ -20,10 +24,13 @@ classLists = [module.TellClasses() for module in [
  socket_definitions,
  ops_nodegroup,
  primitives_definitions,
-                deformer_definitions,
+ deformer_definitions,
+ math_definitions,
+ i_o,
+ schema_definitions,
 ]]
 # lol
-classLists.append( [CreateMantisTree] )
+classLists.append( [GenerateMantisTree] )
 #
 classes = []
 while (classLists):
@@ -31,44 +38,48 @@ while (classLists):
 
 interface_classes = []
 from bpy import app
-if app.version[0]  == 3:
-    for cls in [cls for cls in socket_definitions.TellClasses() if issubclass(cls, NodeSocket)]:
-        name = cls.__name__+"Interface"
-        from bpy.types import NodeSocketInterface
-        def default_draw_color(self, context,):
-            return self.color
-        def default_draw(self, context, layout):
-            return
-        interface = type(
-                      name,
-                      (NodeSocketInterface,),
-                      {
-                          "color"            : cls.color,
-                          "draw_color"       : default_draw_color,
-                          "draw"             : default_draw,
-                          "bl_idname"        : name,
-                          "bl_socket_idname" : cls.__name__,
-                      },
-                  )
-        interface_classes.append(interface)
-
-    classes.extend(interface_classes)
+# if app.version[0]  == 3:
+#     for cls in [cls for cls in socket_definitions.TellClasses() if issubclass(cls, NodeSocket)]:
+#         name = cls.__name__+"Interface"
+#         from bpy.types import NodeSocketInterface
+#         def default_draw_color(self, context,):
+#             return self.color
+#         def default_draw(self, context, layout):
+#             return
+#         interface = type(
+#                       name,
+#                       (NodeSocketInterface,),
+#                       {
+#                           "color"            : cls.color,
+#                           "draw_color"       : default_draw_color,
+#                           "draw"             : default_draw,
+#                           "bl_idname"        : name,
+#                           "bl_socket_idname" : cls.__name__,
+#                       },
+#                   )
+#         interface_classes.append(interface)
+
+#     classes.extend(interface_classes)
 
 import nodeitems_utils
 from nodeitems_utils import NodeCategory, NodeItem
 
-class AllNodeCategory(NodeCategory):
+class MantisNodeCategory(NodeCategory):
     @classmethod
     def poll(cls, context):
-        return (context.space_data.tree_type == 'MantisTree')
+        return (context.space_data.tree_type in ['MantisTree', 'SchemaTree'])
 
+class SchemaNodeCategory(NodeCategory):
+    @classmethod
+    def poll(cls, context):
+        # doesn't seem to work tho
+        try:
+            return (context.space_data.path[len(path)-1].node_tree.bl_idname == 'SchemaTree')
+        except:
+            return True
 
 
-# THIS is stupid, should be filled out automatically
-node_categories = [
-    # identifier, label, items list
-    AllNodeCategory('INPUT', "Input", items=[
-            NodeItem("UtilityMetaRig"),
+input_category=[
             NodeItem("InputFloatNode"),
             NodeItem("InputVectorNode"),
             NodeItem("InputBooleanNode"),
@@ -76,16 +87,15 @@ node_categories = [
             # NodeItem("InputRotationOrderNode"),
             # NodeItem("InputTransformSpaceNode"),
             NodeItem("InputStringNode"),
+            NodeItem("InputIntNode"),
             # NodeItem("InputQuaternionNode"),
             # NodeItem("InputQuaternionNodeAA"),
             NodeItem("InputMatrixNode"),
             NodeItem("InputLayerMaskNode"),
             NodeItem("InputExistingGeometryObject"),
             NodeItem("InputExistingGeometryData"),
-    ]),
-    # AllNodeCategory('LINK', "Link", items=[]),
-    # AllNodeCategory('LINK_TRACKING', "Link", items=[]),
-    AllNodeCategory('LINK_TRANSFORM', "Link (Transform)", items=[
+    ]
+link_transform_category = [
         NodeItem("LinkCopyLocation"),
         NodeItem("LinkCopyRotation"),
         NodeItem("LinkCopyScale"),
@@ -95,49 +105,95 @@ node_categories = [
         NodeItem("LinkLimitRotation"),
         NodeItem("LinkLimitDistance"),
         NodeItem("LinkTransformation"),
-    ]),
-    AllNodeCategory('LINK_TRACKING', "Link (Tracking)", items=[
+    ]
+link_tracking_category = [
         NodeItem("LinkInverseKinematics"),
         NodeItem("LinkSplineIK"),
         NodeItem("LinkStretchTo"),
         NodeItem("LinkDampedTrack"),
         NodeItem("LinkLockedTrack"),
         NodeItem("LinkTrackTo"),
-    ]),
-    AllNodeCategory('LINK_RELATIONSHIP', "Link (Inheritance)", items=[
+    ]
+link_relationship_category = [
         NodeItem("linkInherit"),
         NodeItem("LinkInheritConstraint"),
         NodeItem("LinkArmature"),
-    ]),
-    AllNodeCategory('DEFORMER', "Deformer", items=[
-            NodeItem("DeformerArmature"),
-    ]),
-    AllNodeCategory('XFORM', "Transform", items=[
+    ]
+deformer_category=[NodeItem(cls.bl_idname) for cls in deformer_definitions.TellClasses()]
+xForm_category = [
          NodeItem("xFormGeometryObject"),
         # NodeItem("xFormNullNode"),
         NodeItem("xFormBoneNode"),
         NodeItem("xFormRootNode"),
         NodeItem("xFormArmatureNode"),
-    ]),
-    AllNodeCategory('DRIVER', "Driver", items=[
+    ]
+driver_category = [
+        NodeItem("LinkDrivenParameter"),
         NodeItem("UtilityFCurve"),
         NodeItem("UtilityBoneProperties"),
-        NodeItem("LinkDrivenParameter"),
         NodeItem("UtilityDriverVariable"),
         NodeItem("UtilitySwitch"),
         NodeItem("UtilityDriver"),
-    ]),
-    AllNodeCategory('GEOMETRY', "Geometry", items = [
+        NodeItem("UtilityKeyframe"),
+    ]
+geometry_category = [
         NodeItem("GeometryCirclePrimitive"),
-    ]),
-    AllNodeCategory('UTILITIES', "Utility", items=[
+    ]
+utility_category = [
+        NodeItem("MathStaticInt"),
+        NodeItem("MathStaticFloat"),
+        NodeItem("MathStaticVector"),
         NodeItem("UtilityCatStrings"),
         NodeItem("UtilityCombineThreeBool"),
         NodeItem("UtilityCombineVector"),
-    ]),
-    AllNodeCategory('GROUPS', "Groups", items=[
+        NodeItem("UtilityIntToString"),
+        NodeItem("UtilityArrayGet"),
+        NodeItem("UtilityChoose"),
+        NodeItem("UtilityCompare"),
+        NodeItem("UtilityPrint"),
+    ]
+matrix_category = [
+        NodeItem("UtilityMetaRig"),
+        NodeItem("UtilityMatrixFromCurve"),
+        NodeItem("UtilityMatricesFromCurve"),
+        NodeItem("UtilityPointFromBoneMatrix"),
+        NodeItem("UtilitySetBoneLength"),
+        NodeItem("UtilityGetBoneLength"),
+        NodeItem("UtilityBoneMatrixHeadTailFlip"),
+        NodeItem("UtilityMatrixSetLocation"),
+        NodeItem("UtilityMatrixGetLocation"),
+        NodeItem("UtilityMatrixFromXForm"),
+        NodeItem("UtilityAxesFromMatrix"),
+        NodeItem("UtilityMatrixTransform"),
+        NodeItem("UtilityTransformationMatrix"),
+    ]
+groups_category = [
         NodeItem("MantisNodeGroup"),
-    ]),
+        NodeItem("MantisSchemaGroup"),
+    ]
+
+# THIS is stupid, should be filled out automatically
+node_categories = [
+    # identifier, label, items list
+    MantisNodeCategory('INPUT', "Input", items=input_category),
+    # MantisNodeCategory('LINK', "Link", items=[]),
+    # MantisNodeCategory('LINK_TRACKING', "Link", items=[]),
+    MantisNodeCategory('LINK_TRANSFORM', "Link (Transform)", items=link_transform_category),
+    MantisNodeCategory('LINK_TRACKING', "Link (Tracking)", items=link_tracking_category),
+    MantisNodeCategory('LINK_RELATIONSHIP', "Link (Inheritance)", items=link_relationship_category),
+    MantisNodeCategory('DEFORMER', "Deformer", items=deformer_category),
+    MantisNodeCategory('XFORM', "Transform", items=xForm_category),
+    MantisNodeCategory('DRIVER', "Driver", items=driver_category),
+    MantisNodeCategory('GEOMETRY', "Geometry", items =geometry_category),
+    MantisNodeCategory('UTILITIES', "Utility", items=utility_category),
+    MantisNodeCategory('MATRIX', "Matrix", items=matrix_category),
+    MantisNodeCategory('GROUPS', "Groups", items=groups_category),
+]
+
+schema_category=[NodeItem(cls.bl_idname) for cls in schema_definitions.TellClasses()]
+
+schema_categories = [
+    SchemaNodeCategory('SCHEMA_SCHEMA', "Schema", items=schema_category),
 ]
 
 import bpy
@@ -145,27 +201,40 @@ def init_keymaps():
     kc = bpy.context.window_manager.keyconfigs.addon
     km = kc.keymaps.new(name="Node Generic", space_type='NODE_EDITOR')
     kmi = [
-        # km.keymap_items.new("sorcar.execute_node", 'E', 'PRESS'),
+        # Normal operation
         km.keymap_items.new("mantis.group_nodes", 'G', 'PRESS', ctrl=True),
         km.keymap_items.new("mantis.edit_group", 'TAB', 'PRESS'),
-        km.keymap_items.new("mantis.query_sockets", 'Q', 'PRESS'),
         km.keymap_items.new("mantis.execute_node_tree", 'E', 'PRESS'),
         km.keymap_items.new("mantis.mute_node", 'M', 'PRESS'),
+        km.keymap_items.new("mantis.nodes_cleanup", "C", 'PRESS', shift=True,),
+        # Testing
+        km.keymap_items.new("mantis.query_sockets", 'Q', 'PRESS'),
         km.keymap_items.new("mantis.test_operator", 'T', 'PRESS'),
-        km.keymap_items.new("mantis.nodes_cleanup", "C", 'PRESS', shift=True,)
+        km.keymap_items.new("mantis.visualize_output", 'V', 'PRESS'),
+        # Saving, Loading, Reloading, etc.
+        km.keymap_items.new("mantis.export_save_choose", "S", 'PRESS', alt=True,),
+        km.keymap_items.new("mantis.export_save_as", "S", 'PRESS', alt=True, shift=True),
+        km.keymap_items.new("mantis.reload_tree", "R", 'PRESS', alt=True,),
+        km.keymap_items.new("mantis.import_tree", "O", 'PRESS', ctrl=True,),
     ]
     return km, kmi
 
 addon_keymaps = []
 
+
+
 def register():
     from bpy.utils import register_class
     
     for cls in classes:
-        register_class(cls)
+        try:
+            register_class(cls)
+        except RuntimeError as e:
+            prRed(cls.__name__)
+            raise e
 
-
-    nodeitems_utils.register_node_categories('AllNodeCategories', node_categories)
+    nodeitems_utils.register_node_categories('MantisNodeCategories', node_categories)
+    nodeitems_utils.register_node_categories('SchemaNodeCategories', schema_categories)
 
 
     if (not bpy.app.background):
@@ -174,9 +243,11 @@ def register():
             k.active = True
             addon_keymaps.append((km, k))
 
+    
 
 def unregister():
-    nodeitems_utils.unregister_node_categories('AllNodeCategories')
+    nodeitems_utils.unregister_node_categories('MantisNodeCategories')
+    nodeitems_utils.unregister_node_categories('SchemaNodeCategories')
 
     from bpy.utils import unregister_class
     for cls in reversed(classes):

+ 53 - 24
add_node.sh

@@ -9,7 +9,7 @@ icon=""
 
 
 #                     title: name           menu: name,      H  W  menu size    pairs of tag,description,  trick to swap stdout and stderr
-nodetype=$(whiptail --title "add_node.sh" --menu "Node Type" 25 78 16 "xForm" "" "Link" "" "Utility" "" 3>&2 2>&1 1>&3)
+nodetype=$(whiptail --title "add_node.sh" --menu "Node Type" 25 78 16 "xForm" "" "Link" "" "Utility" "" "Math" "" 3>&2 2>&1 1>&3)
 if [[ $nodetype == "" ]]; then
     echo "Cancelled."
 else
@@ -34,6 +34,13 @@ else
         parentclass="MantisNode"
         icon="NODE"
         ;;
+
+      Math)
+        nodefile="math_definitions.py"
+        cnodefile="math_containers.py"
+        parentclass="MantisNode"
+        icon="NODE"
+        ;;
     esac
 
 
@@ -75,12 +82,29 @@ else
             cancelled=1
         fi
         cancelled=$?; check_cancelled
-    else
-        num_inputs=0
-    fi
     
-    
-    if [[ $nodetype == 'Utility' ]]; then
+    elif [[ $nodetype == 'Utility' ]]; then
+        choice=$(whiptail --title "add_node.sh"\
+          --inputbox "Number of outputs?" 8 39 "0" 3>&1 1>&2 2>&3)
+        if [[ "$choice" -eq "$choice" ]]; then
+            num_outputs=$choice
+        else
+            echo "Error, must be a number"
+            cancelled=1
+        fi
+        cancelled=$?; check_cancelled
+    elif [[ $nodetype == 'Math' ]]; then
+
+        choice=$(whiptail --title "add_node.sh"\
+          --inputbox "Number of inputs?" 8 39 "0" 3>&1 1>&2 2>&3)
+        if [[ "$choice" -eq "$choice" ]]; then
+            num_inputs=$choice
+        else
+            echo "Error, must be a number"
+            cancelled=1
+        fi
+        cancelled=$?; check_cancelled
+
         choice=$(whiptail --title "add_node.sh"\
           --inputbox "Number of outputs?" 8 39 "0" 3>&1 1>&2 2>&3)
         if [[ "$choice" -eq "$choice" ]]; then
@@ -91,9 +115,12 @@ else
         fi
         cancelled=$?; check_cancelled
     else
-        num_outputs=0
+        num_inputs=0
     fi
     
+
+
+    
     if [[ $nodetype == 'Utility' && $num_inputs == "0" && $num_outputs == "0" ]]; then
         echo "Error. The node must have at least one socket."
         exit
@@ -141,10 +168,10 @@ else
         echo "        self.inputs.new('MatrixSocket', \"Matrix\")" >> classheader.txt
         echo "        self.inputs.new('RelationshipSocket', \"Relationship\")" >> classheader.txt
         #cnode
-        echo "          \"Name\"           : NodeSocket(is_input = True, to_socket = \"Name\", to_node = self)," >> cnode_def
-        echo "          \"Rotation Order\" : NodeSocket(is_input = True, to_socket = \"Rotation Order\", to_node = self)," >> cnode_def
-        echo "          \"Matrix\"         : NodeSocket(is_input = True, to_socket = \"Matrix\", to_node = self)," >> cnode_def
-        echo "          \"Relationship\"   : NodeSocket(is_input = True, to_socket = \"Relationship\", to_node = self)," >> cnode_def
+        echo "          \"Name\"           : NodeSocket(is_input = True, name = \"Name\", node = self)," >> cnode_def
+        echo "          \"Rotation Order\" : NodeSocket(is_input = True, name = \"Rotation Order\", node = self)," >> cnode_def
+        echo "          \"Matrix\"         : NodeSocket(is_input = True, name = \"Matrix\", node = self)," >> cnode_def
+        echo "          \"Relationship\"   : NodeSocket(is_input = True, name = \"Relationship\", node = self)," >> cnode_def
         #parameters; should be identical to cnode inputs
         echo "          \"Name\":None, " >> parameters
         echo "          \"Rotation Order\":None, " >> parameters
@@ -154,7 +181,7 @@ else
         #node
         echo "        self.inputs.new (\"RelationshipSocket\", \"Input Relationship\")" >> classheader.txt
         #cnode
-        echo "        \"Input Relationship\" : NodeSocket(is_input = True, to_socket = \"Input Relationship\", to_node = self,)," >> cnode_def
+        echo "        \"Input Relationship\" : NodeSocket(is_input = True, name = \"Input Relationship\", node = self,)," >> cnode_def
         #parameters; should be identical to cnode inputs
         echo "        \"Input Relationship\":None, " >> parameters
     fi
@@ -178,9 +205,9 @@ else
           "QuaternionSocket" ""\
           "QuaternionSocketAA" ""\
           "IntSocket" ""\
-	  "GeometrySocket" ""\
+	      "GeometrySocket" ""\
           "StringSocket" ""\
-          "LayerMaskSocket" ""\
+          "BoneCollectionSocket" ""\
           "BoolUpdateParentNode" ""\
           "LabelSocket" ""\
           "IKChainLengthSocket" ""\
@@ -211,7 +238,7 @@ else
         #node
         echo "        self.inputs.new(\"$sockettype\", \"$socketname\")" >> classheader.txt
         #cnode
-        echo "          \"$socketname\"   : NodeSocket(is_input = True, to_socket = \"$socketname\", to_node = self)," >> cnode_def
+        echo "          \"$socketname\"   : NodeSocket(is_input = True, name = \"$socketname\", node = self)," >> cnode_def
         #parameters; should be identical to cnode inputs
         echo "          \"$socketname\":None, " >> parameters
     done
@@ -224,14 +251,14 @@ else
         #node
         echo "        self.outputs.new('xFormSocket', \"xForm Out\")" >> classheader.txt
         #cnode
-        echo "          \"xForm Out\" : NodeSocket(from_socket=\"xForm Out\", from_node = self), }" >> cnode_def
+        echo "          \"xForm Out\" : NodeSocket(name=\"xForm Out\", node = self), }" >> cnode_def
     elif [[ $nodetype == 'Link' ]]; then
         #node
         echo "        self.outputs.new(\"RelationshipSocket\", \"Output Relationship\")" >> classheader.txt
         #cnode
-        echo "          \"Output Relationship\" : NodeSocket(from_socket = \"Output Relationship\", from_node=self), }" >> cnode_def
+        echo "          \"Output Relationship\" : NodeSocket(name = \"Output Relationship\", node=self), }" >> cnode_def
     # New Outputs
-    elif [[ $nodetype == 'Utility' ]]; then
+    elif [[ $nodetype == 'Utility' || $nodetype == 'Math' ]]; then
         
         until [[ $num_outputs == "0" ]]; do
             sockettype=$(whiptail --title "add_node.sh" --menu "Output Socket Type" 25 78 16\
@@ -250,9 +277,9 @@ else
             "QuaternionSocket" ""\
             "QuaternionSocketAA" ""\
             "IntSocket" ""\
-	    "GeometrySocket" ""\
+	        "GeometrySocket" ""\
             "StringSocket" ""\
-            "LayerMaskSocket" ""\
+            "BoneCollectionSocket" ""\
             "BoolUpdateParentNode" ""\
             "LabelSocket" ""\
             "IKChainLengthSocket" ""\
@@ -283,7 +310,7 @@ else
             #node
             echo "        self.outputs.new(\"$sockettype\", \"$socketname\")" >> classheader.txt
             #cnode
-            echo "          \"$socketname\" : NodeSocket(from_socket = \"$socketname\", from_node=self)," >> cnode_def
+            echo "          \"$socketname\" : NodeSocket(name = \"$socketname\", node=self)," >> cnode_def
             #parameters , this time it should by the cnode outputs!
             echo "          \"$socketname\":None, " >> parameters
         done
@@ -313,13 +340,13 @@ else
     echo "        return evaluate_input(self, input_name)" >> cnode_def
     echo >> cnode_def
     echo "    def bExecute(self, bContext = None,):" >> cnode_def
-    echo "        pass" >> cnode_def
+    echo "        return" >> cnode_def
     echo >> cnode_def
     echo "    def __repr__(self):" >> cnode_def
     echo "        return self.signature.__repr__()" >> cnode_def
     echo >> cnode_def    
-    echo "    def fill_parameters(self, node_prototype):" >> cnode_def
-    echo "        fill_parameters(self, node_prototype)" >> cnode_def
+    echo "    def fill_parameters(self):" >> cnode_def
+    echo "        fill_parameters(self)" >> cnode_def
     # now it's done!
     
     cat cnode_def >> $cnodefile
@@ -377,6 +404,8 @@ else
         tc_end=$(grep -n -m 1 "AllNodeCategory('UTILITIES'" $bakfile |  cut -f1 -d:)
     elif [[ $nodetype == 'Utility' && $isinput == "1" ]]; then
         tc_end=$(grep -n -m 1 "AllNodeCategory('INPUT'" $bakfile |  cut -f1 -d:)
+    elif [[ $nodetype == 'Math' ]]; then
+        tc_end=$(grep -n -m 1 "AllNodeCategory('UTILITIES'" $bakfile |  cut -f1 -d:)
     fi
     
     # the total length of the file, in lines

+ 593 - 144
base_definitions.py

@@ -1,6 +1,6 @@
 #Mantis Nodes Base
 import bpy
-from bpy.props import BoolProperty, StringProperty, EnumProperty, CollectionProperty, IntProperty
+from bpy.props import BoolProperty, StringProperty, EnumProperty, CollectionProperty, IntProperty, PointerProperty, BoolVectorProperty
 from . import ops_nodegroup
 from bpy.types import NodeTree, Node, PropertyGroup, Operator, UIList, Panel
 
@@ -9,11 +9,21 @@ from .utilities import (prRed, prGreen, prPurple, prWhite,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
                               wrapOrange,)
 
+from .utilities import get_socket_maps, relink_socket_map, do_relink
+
 from bpy.app.handlers import persistent
 
 def TellClasses():
     #Why use a function to do this? Because I don't need every class to register.
-    return [MantisNodeGroup, MantisTree, ]
+    return [ MantisTree,
+             SchemaTree,
+            #  MantisNode,
+            #  SchemaNode,
+             MantisNodeGroup,
+             SchemaGroup,
+             MantisVisualizeTree,
+             MantisVisualizeNode,
+           ]
 
 class MantisTree(NodeTree):
     '''A custom node tree type that will show up in the editor type list'''
@@ -22,22 +32,19 @@ class MantisTree(NodeTree):
     bl_icon = 'OUTLINER_OB_ARMATURE'
     
     tree_valid:BoolProperty(default=False)
-    do_live_update:BoolProperty(default=True)
-    # use this to disable updates for e.g. scripts
-    locked:BoolProperty(default=True)
-    
+    do_live_update:BoolProperty(default=True) # use this to disable updates for e.g. scripts
     num_links:IntProperty(default=-1)
+    filepath:StringProperty(default="", subtype='FILE_PATH')
+    is_executing:BoolProperty(default=False)
+    is_exporting:BoolProperty(default=False)
+    execution_id:StringProperty(default='')
+
     
     parsed_tree={}
     
     def interface_update(self, context):
         # no idea what this does
-        print ("Update Interface function in MantisTree class")
-
-           
-    def interface_update(self, context):
-        prGreen("interface_update")
-        
+        print ("Update Interface function in MantisTree class") 
 
 
     if bpy.app.version >= (3, 2):  # in 3.1 this can lead to a crash
@@ -48,21 +55,27 @@ class MantisTree(NodeTree):
             return socket_type in Tell_bl_idnames()
             # thank you, Sverchok
             
-    def update_tree(self, context):
-        if self.do_live_update == False:
+    def update_tree(self, context = None):
+        if self.is_exporting:
             return
+        # return
+        self.is_executing = True
         from . import readtree
         prGreen("Validating Tree: %s" % self.name)
-        parsed_tree = readtree.parse_tree(self)
-        self.parsed_tree=parsed_tree
-        current_tree = bpy.context.space_data.path[-1].node_tree
-        self.tree_valid = True
-        prWhite("Number of Nodes: %s" % (len(self.parsed_tree)))
-        self.display_update(context)
+        try:
+            self.parsed_tree = readtree.parse_tree(self)
+            if context:
+                self.display_update(context)
+            self.is_executing = False
+            self.tree_valid = True
+        except GraphError as e:
+            prRed("Failed to update node tree due to error.")
+            self.tree_valid = False
+            self.is_executing = False
+            raise e
+
     
     def display_update(self, context):
-        if self.do_live_update == False:
-            return
         current_tree = bpy.context.space_data.path[-1].node_tree
         for node in current_tree.nodes:
             if hasattr(node, "display_update"):
@@ -74,181 +87,617 @@ class MantisTree(NodeTree):
         
     
     def execute_tree(self,context):
+        if self.is_exporting:
+            return
+        # return
         prGreen("Executing Tree: %s" % self.name)
+        self.is_executing = True
         from . import readtree
-        readtree.execute_tree(self.parsed_tree, self, context)
+        try:
+            readtree.execute_tree(self.parsed_tree, self, context)
+        except RecursionError as e:
+            prRed("Recursion error while parsing tree.")
+            # prRed(e); node_tree.do_live_update = False
+        # except Exception:
+        #     pass
+        finally: # first time I have ever used a finally block in my life.
+            self.is_executing = False
 
     
 
-def update_handler(scene):
-    context=bpy.context
-    if context.space_data:
-        node_tree = context.space_data.path[0].node_tree
-        if node_tree.do_live_update:
-            prev_links = node_tree.num_links
-            node_tree.num_links = len(node_tree.links)
-            if (prev_links == -1):
-                return
-            if prev_links != node_tree.num_links:
-                node_tree.tree_valid = False
-            if node_tree.tree_valid == False:
-                from . import readtree
-                node_tree.update_tree(context)
+# class SchemaPropertyGroup(bpy.types.PropertyGroup):
 
-def execute_handler(scene):
-    context = bpy.context
-    if context.space_data:
-        node_tree = context.space_data.path[0].node_tree
-        if node_tree.tree_valid and node_tree.do_live_update:
-            node_tree.execute_tree(context)
-            self.tree_valid = False
+class SchemaTree(NodeTree):
+    '''A node tree representing a schema to generate a Mantis tree'''
+    bl_idname = 'SchemaTree'
+    bl_label = "Rigging Nodes Schema"
+    bl_icon = 'RIGID_BODY_CONSTRAINT'
+
+    tree_valid:BoolProperty(default=False)
+    do_live_update:BoolProperty(default=True) # use this to disable updates for e.g. scripts
+    is_executing:BoolProperty(default=False)
+    num_links:IntProperty(default=-1)
+    # filepath:StringProperty(default="", subtype='FILE_PATH')
+    
+    parsed_tree={}
+
+    # def update(self):
+    #     for n in self.nodes:
+    #         if hasattr(n, "update"): n.update()
+
+    # def update_tree(self, context):
+    #     prRed("update tree for Schema Tree!")
+    #     # self.tree_valid = True
+    #     # return
+    #     from . import readtree
+    #     prGreen("Validating Tree: %s" % self.name)
+    #     parsed_tree = readtree.parse_tree(self)
+    #     self.parsed_tree=parsed_tree
+    #     current_tree = bpy.context.space_data.path[-1].node_tree
+    #     self.tree_valid = True
+    #     prWhite("Number of Nodes: %s" % (len(self.parsed_tree)))
+    #     self.display_update(context)
+    
+    # def display_update(self, context):
+    #     prRed("display update for Schema Tree!")
+    #     return
+    #     current_tree = bpy.context.space_data.path[-1].node_tree
+    #     for node in current_tree.nodes:
+    #         if hasattr(node, "display_update"):
+    #             try:
+    #                 node.display_update(self.parsed_tree, context)
+    #             except Exception as e:
+    #                 print("Node \"%s\" failed to update display with error: %s" %(wrapGreen(node.name), wrapRed(e)))
+    #                 # raise e
+
+    # def execute_tree(self,context):
+    #     self.is_executing = True
+    #     prRed("executing Schema Tree!")
+    #     self.tree_valid = False
+    #     self.is_executing = False
+    #     return
+    #     prGreen("Executing Tree: %s" % self.name)
+    #     from . import readtree
+    #     try:
+    #         readtree.execute_tree(self.parsed_tree, self, context)
+    #     except RecursionError as e:
+    #         prRed("Recursion error while parsing tree.")
+    #         prRed(e); node_tree.do_live_update = False
+
+    if bpy.app.version >= (3, 2):  # in 3.1 this can lead to a crash
+        @classmethod
+        def valid_socket_type(cls, socket_type: str):
+            # https://docs.blender.org/api/master/bpy.types.NodeTree.html#bpy.types.NodeTree.valid_socket_type
+            from .socket_definitions import Tell_bl_idnames
+            return socket_type in Tell_bl_idnames()
+            # thank you, Sverchok
+            
 
-# bpy.app.handlers.load_post.append(set_tree_invalid)
-bpy.app.handlers.depsgraph_update_pre.append(update_handler)
-bpy.app.handlers.depsgraph_update_post.append(execute_handler)
 
 
 class MantisNode:
-    num_links:IntProperty(default=-1)
+    # num_links:IntProperty(default=-1) # is this used anywhere?
+
+    # is_triggering_execute:BoolProperty(default=False)
+
     # do_display_update:BoolProperty(default=False)
     @classmethod
     def poll(cls, ntree):
-        return (ntree.bl_idname == 'MantisTree')
+        return (ntree.bl_idname in ['MantisTree', 'SchemaTree'])
                 
     def insert_link(self, link):
         context = bpy.context
         if context.space_data:
             node_tree = context.space_data.path[0].node_tree
             from . import readtree
-            prOrange("Updating from insert_link callback")
-            node_tree.update_tree(context)
-            if (link.to_socket.is_linked == False):
-                node_tree.num_links+=1
-            elif (link.to_socket.is_multi_input and 
-                  link.to_socket.links < link.to_socket.link_limit ):
-                node_tree.num_links+=1
+            if node_tree.do_live_update:
+                # prOrange("Updating from insert_link callback")
+                node_tree.update_tree(context)
+                if (link.to_socket.is_linked == False):
+                    node_tree.num_links+=1
+                elif (link.to_socket.is_multi_input):# and 
+                    #len(link.to_socket.links) < link.to_socket.link_limit ):
+                    # the above doesn't work and I can't be bothered to fix it right now TODO
+                    node_tree.num_links+=1
             
+class SchemaNode:
+    # is_triggering_execute:BoolProperty(default=False)
+    # do_display_update:BoolProperty(default=False)
+    @classmethod
+    def poll(cls, ntree):
+        return (ntree.bl_idname in ['SchemaTree'])
+
+
                 
 
 class LinkNode(MantisNode):
     useTarget : BoolProperty(default=False)
     @classmethod
     def poll(cls, ntree):
-        return (ntree.bl_idname == 'MantisTree')
+        return (ntree.bl_idname in ['MantisTree', 'SchemaTree'])
 
 class xFormNode(MantisNode):
     @classmethod
     def poll(cls, ntree):
-        return (ntree.bl_idname == 'MantisTree')
+        return (ntree.bl_idname in ['MantisTree', 'SchemaTree'])
 
 class DeformerNode(MantisNode):
     @classmethod
     def poll(cls, ntree):
-        return (ntree.bl_idname == 'MantisTree')
+        return (ntree.bl_idname in ['MantisTree', 'SchemaTree'])
+
+
+
+
+# from bpy.types import NodeCustomGroup
+
+def poll_node_tree(self, object):
+    if isinstance(object, MantisTree):
+        return True
+    return False
+
+
+# TODO this should check ID's instead of name
+def node_group_update(node):
+    toggle_update = node.id_data.do_live_update
+    node.id_data.do_live_update = False
+    # prWhite (node.name, len(node.inputs), len(node.outputs))
+
+    identifiers_in={socket.identifier:socket for socket in node.inputs}
+    identifiers_out={socket.identifier:socket for socket in node.outputs}
+
+    if node.node_tree is None:
+        node.inputs.clear(); node.outputs.clear()
+        node.id_data.do_live_update = toggle_update
+        return
+    found_in, found_out = [], []
+    update_input, update_output = False, False
+    for item in node.node_tree.interface.items_tree:
+        if item.item_type != "SOCKET": continue
+        if item.in_out == 'OUTPUT':
+            if s:= identifiers_out.get(item.identifier): # if the requested output doesn't exist, update
+                found_out.append(item.identifier)
+                if update_output: continue # done here
+                if s.bl_idname != item.socket_type: update_output = True; continue
+            else: update_output = True; continue # prRed(f"Not found: {item.name}"); 
+        else:
+            if s:= identifiers_in.get(item.identifier): # if the requested input doesn't exist, update
+                found_in.append(item.identifier)
+                if update_input: continue # done here
+                if s.bl_idname != item.socket_type: update_input = True; continue
+            else: update_input = True; continue # prGreen(f"Not found: {item.name}");
+    
+
+    # Schema has an extra input for Length and for Extend.
+    if node.bl_idname == 'MantisSchemaGroup':
+        found_in.extend(['Schema Length', ''])
+
+    # if we have too many elements, just get rid of the ones we don't need
+    if len(node.inputs) > len(found_in):#
+        for inp in node.inputs:
+            if inp.identifier in found_in: continue
+            node.inputs.remove(inp)
+    if len(node.outputs) > len(found_out):
+        for out in node.outputs:
+            if out.identifier in found_out: continue
+            node.outputs.remove(out)
+    #
+    if len(node.inputs) > 0 and (inp := node.inputs[-1]).bl_idname == 'WildcardSocket' and inp.is_linked:
+        # prPurple("oink! I am a piggy!")
+        update_input = True
+    if len(node.outputs) > 0 and  (out := node.outputs[-1]).bl_idname == 'WildcardSocket' and out.is_linked:
+        # prPurple("woof! I am a doggy!")
+        update_output = True
+    #
+    if not (update_input or update_output):
+        node.id_data.do_live_update = toggle_update
+        return
+
+    if update_input or update_output:
+        socket_map_in, socket_map_out = get_socket_maps(node)
 
+        if update_input :
+            if node.bl_idname == 'MantisSchemaGroup':
+                schema_length=0
+                if sl := node.inputs.get("Schema Length"):
+                    schema_length = sl.default_value
+                # sometimes this isn't available yet # TODO not happy about this solution
 
+            node.inputs.clear()
+            if node.bl_idname == 'MantisSchemaGroup':
+                node.inputs.new("IntSocket", "Schema Length", identifier='Schema Length')
+                node.inputs['Schema Length'].default_value = schema_length
+
+        if update_output: node.outputs.clear()
+
+
+        for item in node.node_tree.interface.items_tree:
+            if item.item_type != "SOCKET": continue
+            if (item.in_out == 'INPUT' and update_input):
+                relink_socket_map(node, node.inputs, socket_map_in, item)
+            if (item.in_out == 'OUTPUT' and update_output):
+                relink_socket_map(node, node.outputs, socket_map_out, item)
+        
+        # at this point there is no wildcard socket
+        if '__extend__' in socket_map_in.keys():
+            do_relink(node, None, socket_map_in, in_out='INPUT', parent_name='Constant' )
+
+
+
+        node.id_data.do_live_update = toggle_update
+
+
+
+
+def node_tree_prop_update(self, context):
+    if self.is_updating: # this looks dumb... but update() can be called from update() in a sort of accidental way.
+        return
+    # prGreen("updating me...")
+    self.is_updating = True
+    node_group_update(self)
+    self.is_updating = False
+    if self.bl_idname in ['MantisSchemaGroup'] and self.node_tree is not None:
+        if len(self.inputs) == 0:
+            self.inputs.new("IntSocket", "Schema Length", identifier='Schema Length')
+        if self.inputs[-1].bl_idname != "WildcardSocket":
+            self.inputs.new("WildcardSocket", "", identifier="__extend__")
 
 from bpy.types import NodeCustomGroup
-# TODO: make this one's traverse() function actually work
-def poll_node_tree(self, tree):
-    return True #TODO: prevent circular group ofc
-class MantisNodeGroup(NodeCustomGroup, MantisNode):
+
+class MantisNodeGroup(Node, MantisNode):
     bl_idname = "MantisNodeGroup"
     bl_label = "Node Group"
+
+    node_tree:PointerProperty(type=NodeTree, poll=poll_node_tree, update=node_tree_prop_update,)
+    is_updating:BoolProperty(default=False)
     
-    node_tree_updater : bpy.props.PointerProperty(type=bpy.types.NodeTree, poll=poll_node_tree)
-    # def poll_node_tree(self, object):
-        # if object.bl_idname not in "MantisTree":
-            # return False
-        # context=bpy.context
-        # context = bpy.context
-        # if context.space_data:
-            # used_trees = [ pathitem.node_tree for pathitem in context.space_data.path]
-            # if object in used_trees:
-                # return False
-    # node_tree:bpy.props.PointerProperty(type=bpy.types.NodeTree, poll=poll_node_tree)
-    
-    # def init(self, context):
-        # pass
-        
-        
-    def socket_value_update(self, context):
-        prGreen("updating...")
-    
-    
-    # this is a total HACK
     def update(self):
-        if self.node_tree_updater is not self.node_tree:
-            self.update_node_tree()
-            self.node_tree_updater = self.node_tree
-    
-    
-    def update_node_tree(self):
-        self.inputs.clear()
-        self.outputs.clear()
-        for item in self.node_tree.interface.items_tree:
-            if item.item_type != "SOCKET":
-                continue
-            s = None
-            if item.in_out == 'OUTPUT':
-                s = self.outputs.new(type=item.socket_type, name=item.name, identifier=item.identifier)
-            else:
-                s = self.inputs.new(type=item.socket_type, name=item.name, identifier=item.identifier)
-    
+        if self.is_updating: # this looks dumb... but update() can be called from update() in a sort of accidental way.
+            return
+        self.is_updating = True
+        node_group_update(self)
+        self.is_updating = False
+
     def draw_buttons(self, context, layout):
         row = layout.row(align=True)
         row.prop(self, "node_tree", text="")
         row.operator("mantis.edit_group", text="", icon='NODETREE', emboss=True)
         
-# # I don't remember why I need this?
-# class GroupOutputDummySocket:
-    # # a dummy class for acting like a socket that is coming from every
-    # #  group output node
-    # def __init__(self, tree, identifier, is_input=True):
-        # #
-        # # So, we need to go through the node tree and find all 
-        # #   the Group Input sockets that match this
-        # #   socket's identifier
-        # #
-        # sockets = []
-        # s = None
-        # for node in tree.nodes:
-            # if (is_input):
-                # if node.bl_idname == 'NodeGroupInput':
-                    # # Group Inputs have outputs... confusing.
-                    # for s in node.outputs:
-                        # if (s.identifier == identifier):
-                            # sockets.append(s)
-            # else:
-                # if node.bl_idname == 'NodeGroupOutput':
-                    # for s in node.inputs:
-                        # if (s.identifier == identifier):
-                            # sockets.append(s)
-        # sock = sockets[-1]
-        # # whatever the last socket is should be OK for most of this stuff
-        # self.bl_idname=sock.bl_idname
-        # self.identifier = identifier
-        # self.name = sock.name
-        # is_linked = False
-        # for s in sockets:
-            # if s.is_linked:
-                # is_linked = True; break
-        # self.is_linked = is_linked
-        # self.is_output = not is_input
-        # # hopefully this doesn't matter, since it is a group node...
-        # self.node = sock.node
-        # self.links = []
-        # for s in sockets:
-            # self.links.extend(s.links)
-        # # seems to werk
 
 class CircularDependencyError(Exception):
     pass
 class GraphError(Exception):
     pass
 
-def get_signature_from_edited_tree(self, context):
+def get_signature_from_edited_tree(node, context):
     sig_path=[None,]
     for item in context.space_data.path[:-1]:
         sig_path.append(item.node_tree.nodes.active.name)
-    return tuple(sig_path+[self.name])
+    return tuple(sig_path+[node.name])
+
+def poll_node_tree_schema(self, object):
+    if isinstance(object, SchemaTree):
+        return True
+    return False
+
+# def update_schema_length(self, context):
+#     pass # for now
+
+
+# TODO tiny UI problem - inserting new links into the tree will not place them in the right place.
+
+#this is a schema node in a mantis tree... kinda confusing
+class SchemaGroup(Node, MantisNode):
+    bl_idname = "MantisSchemaGroup"
+    bl_label = "Node Schema"
+    
+    node_tree:PointerProperty(type=NodeTree, poll=poll_node_tree_schema, update=node_tree_prop_update,)
+    # schema_length:IntProperty(default=5, update=update_schema_length)
+    is_updating:BoolProperty(default=False)
+
+
+    # incoming link
+    # from-node = name
+    # from socket = index or identifier or something
+    # property is unset as soon as it is used
+    # so the update function checks this property and handles the incoming link in an allowed place
+    # and actually the handle-link function can work as a switch - when the property is unset, it allows new links
+    # otherwise it unsets the property and returns.
+    
+    # def init(self, context):
+    #     self.inputs.new("IntSocket", "Schema Length")
+    #     self.inputs.new("WildcardSocket", "")
+
+    def draw_buttons(self, context, layout):
+        row = layout.row(align=True)
+        row.prop(self, "node_tree", text="")
+        row.operator("mantis.edit_group", text="", icon='NODETREE', emboss=True)
+        # layout.prop(self, "schema_length", text="n=")
+    
+    # WHAT IF:
+    #   - the function that creates the input/output map returns to a property in the node
+    #   - then the node handles the update in its update function.
+
+    def update(self):
+        if self.is_updating: # this looks dumb... but update() can be called from update() in a sort of accidental way.
+            return
+        self.is_updating = True
+        node_group_update(self)
+        # kinda dumb but necessary since update doesn't always fix this
+        if self.node_tree:
+            if len(self.inputs) == 0:
+                self.inputs.new("IntSocket", "Schema Length", identifier='Schema Length')
+            if self.inputs[-1].bl_idname != "WildcardSocket":
+                self.inputs.new("WildcardSocket", "", identifier="__extend__")
+        self.is_updating = False
+
+    # def insert_link(self, link):
+    #     if self.node_tree is None:
+    #         link.is_valid = False
+    #         return
+    #     sock_type = link.from_socket.bl_idname
+    #     for i, sock in enumerate(self.inputs):
+    #         if sock == link.to_socket: # dumb but whatever
+    #             identifier = link.to_socket.identifier
+    #             if sock.bl_idname not in ["WildcardSocket"]:
+    #                 if sock.is_linked == True:
+    #                     links = [ getattr(l, "from_socket") for l in sock.links ]
+    #                     name = sock.name
+    #                     # self.inputs.remove(sock)
+    #                     # new_input = self.inputs.new(sock_type, name, identifier=identifier, use_multi_input=True); self.inputs.move(-1, i)
+    #                     sock.display_shape = 'SQUARE_DOT'
+    #                     interface_item = self.node_tree.interface.items_tree[name]
+    #                     if not (interface_parent := self.node_tree.interface.items_tree.get('Array')):
+    #                         interface_parent = self.node_tree.interface.new_panel(name='Array')
+    #                     self.node_tree.interface.move_to_parent(interface_item, interface_parent, len(interface_parent.interface_items))
+    #                     # sock.link_limit = self.schema_length TODO this will be very hard to get at this point
+    #                     # self.id_data.links.new()
+    #             else: #if link.to_socket  == self.inputs[-1]:
+    #                     self.inputs.remove(sock)#self.inputs[-1])
+    #                     #
+    #                     name_stem = link.from_socket.bl_idname.replace('Socket',''); num=0
+    #                     if hasattr(link.from_socket, "default_value"):
+    #                         name_stem = type(link.from_socket.default_value).__name__
+    #                     for n in self.inputs:
+    #                         if name_stem in n.name: num+=1
+    #                     name = name_stem + '.' + str(num).zfill(3)
+    #                     #
+    #                     new_input = self.inputs.new(sock_type, name, identifier=identifier, use_multi_input=False); self.inputs.move(-1, i+1)
+    #                     new_input.link_limit = 1
+    #                     # link.to_socket = new_input
+    #                     # this seems to work
+
+    #                     self.inputs.new("WildcardSocket", "")
+    #                     if not (interface_parent := self.node_tree.interface.items_tree.get('Constant')):
+    #                         interface_parent = self.node_tree.interface.new_panel(name='Constant')
+    #                     self.node_tree.interface.new_socket(name=name,in_out='INPUT', socket_type=sock_type, parent=interface_parent)
+    #                     return
+            
+    # TODO: investigate whether this is necessary
+    # @classmethod
+    # def poll(cls, ntree):
+    #     return (ntree.bl_idname in ['MantisTree'])
+
+
+
+
+# handlers!
+
+#annoyingly these have to be persistent
+@persistent
+def update_handler(scene):
+    context=bpy.context
+    if context.space_data:
+        if not hasattr(context.space_data, "path"):
+            return
+        trees = [p.node_tree for p in context.space_data.path]
+        if not trees: return
+        if (node_tree := trees[0]).bl_idname in ['MantisTree']:
+            if node_tree.do_live_update and not (node_tree.is_executing or node_tree.is_exporting):
+                prev_links = node_tree.num_links
+                node_tree.num_links = len(node_tree.links)
+                if (prev_links == -1):
+                    return
+                if prev_links != node_tree.num_links:
+                    node_tree.tree_valid = False
+                if node_tree.tree_valid == False:
+                        node_tree.update_tree(context)
+
+@persistent
+def execute_handler(scene):
+    context = bpy.context
+    if context.space_data:
+        if not hasattr(context.space_data, "path"):
+            return
+        trees = [p.node_tree for p in context.space_data.path]
+        if not trees: return
+        if (node_tree := trees[0]).bl_idname in ['MantisTree']:
+            if node_tree.tree_valid and node_tree.do_live_update and not (node_tree.is_executing or node_tree.is_exporting):
+                node_tree.execute_tree(context)
+                node_tree.tree_valid = False
+
+# @persistent
+# def load_post_handler(scene):
+#     print ("cuss and darn")
+
+#     # import bpy
+#     import sys
+
+#     def wrapRed(skk):    return "\033[91m{}\033[00m".format(skk)
+#     def intercept(fn, *args):
+#         print(wrapRed("Intercepting:"), fn)
+#         sys.stdout.flush()
+
+#         fn(*args)
+
+#         print(wrapRed("... done"))
+#         sys.stdout.flush()
+
+#     for attr in dir(bpy.app.handlers):
+#         if attr.startswith("_"):
+#             continue
+
+#         handler_list = getattr(bpy.app.handlers, attr)
+#         if attr =='load_post_handler':
+#             continue
+#         if not isinstance(handler_list, list):
+#             continue
+#         if not handler_list:
+#             continue
+
+#         print("Intercept Setup:", attr)
+
+#         handler_list[:] = [lambda *args: intercept(fn, *args) for fn in handler_list]
+
+#         # import cProfile
+#         from os import environ
+#         do_profile=False
+#         print (environ.get("DOPROFILE"))
+#         if environ.get("DOPROFILE"):
+#             do_profile=True
+#         if do_profile:
+#             # cProfile.runctx("tree.update_tree(context)", None, locals())
+#             # cProfile.runctx("tree.execute_tree(context)", None, locals())
+#             import hunter
+#             hunter.trace(stdlib=False, action=hunter.CallPrinter(force_colors=False))
+# #     def msgbus_callback(*args):
+# #         # print("something changed!")
+# #         print("Something changed!", args)
+# #     owner = object()
+
+#     subscribe_to = (bpy.types.Node, "location")
+#     subscribe_to = (bpy.types.Node, "color")
+#     subscribe_to = (bpy.types.Node, "dimensions")
+#     subscribe_to = (bpy.types.Node, "height")
+#     subscribe_to = (bpy.types.Node, "width")
+#     subscribe_to = (bpy.types.Node, "inputs")
+#     subscribe_to = (bpy.types.Node, "outputs")
+#     subscribe_to = (bpy.types.Node, "select")
+#     subscribe_to = (bpy.types.Node, "name")
+#     subscribe_to = (bpy.types.NodeSocket, "name")
+#     subscribe_to = (bpy.types.NodeSocket, "display_shape")
+
+
+#     bpy.msgbus.subscribe_rna(
+#         key=subscribe_to,
+#         owner=owner,
+#         args=(1, 2, 3),
+#         notify=msgbus_callback,
+#     )
+
+
+# print ("cuss and darn")
+
+# bpy.app.handlers.load_post.append(set_tree_invalid)
+bpy.app.handlers.depsgraph_update_pre.append(update_handler)
+bpy.app.handlers.depsgraph_update_post.append(execute_handler)
+# bpy.app.handlers.load_post.append(load_post_handler)
+
+# # import bpy
+# import sys
+
+# def wrapRed(skk):    return "\033[91m{}\033[00m".format(skk)
+# def intercept(fn, *args):
+#     print(wrapRed("Intercepting:"), fn)
+#     sys.stdout.flush()
+
+#     fn(*args)
+
+#     print(wrapRed("... done"))
+#     sys.stdout.flush()
+
+# for attr in dir(bpy.app.handlers):
+#     if attr.startswith("_"):
+#         continue
+
+#     handler_list = getattr(bpy.app.handlers, attr)
+#     if attr =='load_post_handler':
+#         continue
+#     if not isinstance(handler_list, list):
+#         continue
+#     if not handler_list:
+#         continue
+
+#     print("Intercept Setup:", attr)
+
+#     handler_list[:] = [lambda *args: intercept(fn, *args) for fn in handler_list]
+
+class MantisVisualizeTree(NodeTree):
+    '''A custom node tree type that will show up in the editor type list'''
+    bl_idname = 'MantisVisualizeTree'
+    bl_label = "mantis output"
+    bl_icon = 'HIDE_OFF'
+
+
+class MantisVisualizeNode(Node):
+    bl_idname = "MantisVisualizeNode"
+    bl_label = "Node"
+    @classmethod
+    def poll(cls, ntree):
+        return (ntree.bl_idname in ['MantisVisualizeTree'])
+    
+    def init(self, context):
+        pass
+    
+    def gen_data(self, nc, np = None):
+        self.use_custom_color = True
+        if nc.node_type == 'XFORM':
+            self.color = (1.0 ,0.5, 0.0)
+        if nc.node_type == 'LINK':
+            self.color = (0.4 ,0.2, 1.0)
+        if nc.node_type == 'UTILITY':
+            self.color = (0.2 ,0.2, 0.2)
+        if nc.node_type == 'SCHEMA':
+            self.color = (0.85 ,0.95, 0.9)
+        if nc.node_type == 'DUMMY':
+            self.color = (0.05 ,0.05, 0.15)
+        self.name = ''.join(nc.signature[1:])
+
+        if np:
+            if np.label:
+                self.label=np.label
+            else:
+                self.label=np.name
+            for inp in nc.inputs:
+                s = self.inputs.new('WildcardSocket', inp)
+                try:
+                    if sock := np.inputs.get(inp):
+                        s.color = inp.color
+                except AttributeError: #default bl_idname types like Float and Vector, no biggie
+                    pass
+                except KeyError:
+                    pass
+            for out in nc.outputs:
+                s = self.outputs.new('WildcardSocket', out)
+                try:
+                    if sock := np.outputs.get(out):
+                        s.color = out.color
+                except AttributeError: #default bl_idname types like Float and Vector, no biggie
+                    pass
+                except KeyError:
+                    pass
+            self.location = np.location # will get overwritten by Grandalf later.
+        else:
+            self.label = nc.signature[-1] # which is be the unique name.
+            for inp in nc.inputs:
+                self.inputs.new('WildcardSocket', inp)
+            for out in nc.outputs:
+                self.outputs.new('WildcardSocket', out)
+
+                
+# replace names with bl_idnames for reading the tree and solving schemas.
+replace_types = ["NodeGroupInput", "NodeGroupOutput", "SchemaIncomingConnection",
+                 "SchemaArrayInput", "SchemaConstInput", "SchemaConstOutput", "SchemaIndex",
+                 "SchemaOutgoingConnection", "SchemaConstantOutput", "SchemaArrayOutput",
+                 "SchemaArrayInputGet",]
+
+# anything that gets properties added in the graph... this is a clumsy approach but I need to watch for this
+#   in schema generation and this is the easiest way to do it for now.
+custom_props_types = ["LinkArmature", "UtilityKeyframe", "UtilityFCurve", "UtilityDriver", "xFormBone"]
+
+from_name_filter = ["Driver", ]
+to_name_filter = [
+                   "Custom Object xForm Override",
+                   "Custom Object",
+                   "Deform Bones",
+                 ]

+ 6 - 7
blender_manifest.toml

@@ -3,7 +3,7 @@ schema_version = "1.0.0"
 # Example of manifest file for a Blender extension
 # Change the values according to your extension
 id = "mantis"
-version = "0.0.0"
+version = "0.9.0"
 name = "Mantis"
 tagline = "Rigging Nodes"
 maintainer = "Nodespaghetti <josephbburg@protonmail.com>"
@@ -34,16 +34,15 @@ license = [
 # ]
 
 # Optional list of supported platforms. If omitted, the extension will be available in all operating systems.
-# platforms = ["windows-x64", "macos-arm64", "linux-x64"]
-platforms = ["linux-x64"]
+platforms = ["windows-x64", "macos-arm64", "linux-x64"]
+# platforms = ["linux-x64"]
 # Other supported platforms: "windows-arm64", "macos-x64"
 
 # Optional: bundle 3rd party Python modules.
 # https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html
-# wheels = [
-#   "./wheels/hexdump-3.3-py3-none-any.whl",
-#   "./wheels/jsmin-3.0.1-py3-none-any.whl",
-# ]
+wheels = [
+  "./wheels/grandalf-0.8-py3-none-any.whl",
+]
 
 # # Optional: add-ons can list which resources they will require:
 # # * files (for access of any filesystem operations)

+ 434 - 52
deformer_containers.py

@@ -1,12 +1,19 @@
 from .node_container_common import *
 from .xForm_containers import xFormGeometryObject
+from .misc_containers import InputExistingGeometryObject
 from bpy.types import Node
 from .base_definitions import MantisNode
 
+from .utilities import (prRed, prGreen, prPurple, prWhite, prOrange,
+                        wrapRed, wrapGreen, wrapPurple, wrapWhite,
+                        wrapOrange,)
+
 def TellClasses():
              
     return [ 
              DeformerArmature,
+             DeformerMorphTarget,
+             DeformerMorphTargetDeform,
            ]
 
 
@@ -28,7 +35,7 @@ def default_evaluate_input(nc, input_name):
 def GetxForm(nc):
     trace = trace_single_line_up(nc, "Deformer")
     for node in trace[0]:
-        if (node.__class__ in [xFormGeometryObject]):
+        if (node.__class__ in [xFormGeometryObject, InputExistingGeometryObject]):
             return node
     raise GraphError("%s is not connected to a downstream xForm" % nc)
 
@@ -39,73 +46,139 @@ class DeformerArmature:
         self.base_tree=base_tree
         self.signature = signature
         self.inputs = {
-          "Input Relationship" : NodeSocket(is_input = True, name = "Input Relationship", node = self,),
-          "Target"             : NodeSocket(is_input = True, name = "Target", node = self,),
-          "Vertex Group"       : NodeSocket(is_input = True, name = "Vertex Group", node = self),
-          "Preserve Volume"    : NodeSocket(is_input = True, name = "Preserve Volume", node = self),
-          "Use Multi Modifier" : NodeSocket(is_input = True, name = "Use Multi Modifier", node = self),
-          "Use Envelopes"      : NodeSocket(is_input = True, name = "Use Envelopes", node = self),
-          "Use Vertex Groups"  : NodeSocket(is_input = True, name = "Use Vertex Groups", node = self),
-          "Skinning Method"    : NodeSocket(is_input = True, name = "Skinning Method", node = self),
+          "Input Relationship"     : NodeSocket(is_input = True, name = "Input Relationship", node = self,),
+          "Armature Object"        : NodeSocket(is_input = True, name = "Armature Object", node = self,),
+          "Blend Vertex Group"     : NodeSocket(is_input = True, name = "Blend Vertex Group", node = self),
+          "Invert Vertex Group"    : NodeSocket(is_input = True, name = "Invert Vertex Group", node = self),
+          "Preserve Volume"        : NodeSocket(is_input = True, name = "Preserve Volume", node = self),
+          "Use Multi Modifier"     : NodeSocket(is_input = True, name = "Use Multi Modifier", node = self),
+          "Use Envelopes"          : NodeSocket(is_input = True, name = "Use Envelopes", node = self),
+          "Use Vertex Groups"      : NodeSocket(is_input = True, name = "Use Vertex Groups", node = self),
+          "Skinning Method"        : NodeSocket(is_input = True, name = "Skinning Method", node = self),
+          "Deformer"               : NodeSocket(is_input = True, name = "Deformer", node = self),
+          "Copy Skin Weights From" : NodeSocket(is_input = True, name = "Copy Skin Weights From", node = self),
         }
         self.outputs = {
           "Deformer" : NodeSocket(is_input = False, name = "Deformer", node=self), }
         self.parameters = {
-          "Name"               : None,
-          "Target"             : None,
-          "Vertex Group"       : None,
-          "Preserve Volume"    : None,
-          "Use Multi Modifier" : None,
-          "Use Envelopes"      : None,
-          "Use Vertex Groups"  : None,
-          "Skinning Method"    : None,
+          "Name"                   : None,
+          "Armature Object"        : None,
+          "Blend Vertex Group"     : None,
+          "Invert Vertex Group"    : None,
+          "Preserve Volume"        : None,
+          "Use Multi Modifier"     : None,
+          "Use Envelopes"          : None,
+          "Use Vertex Groups"      : None,
+          "Skinning Method"        : None,
+          "Deformer"               : None,
+          "Copy Skin Weights From" : None,
         }
         # now set up the traverse target...
-        self.inputs["Input Relationship"].set_traverse_target(self.outputs["Deformer"])
-        self.outputs["Deformer"].set_traverse_target(self.inputs["Input Relationship"])
+        self.inputs["Deformer"].set_traverse_target(self.outputs["Deformer"])
+        self.outputs["Deformer"].set_traverse_target(self.inputs["Deformer"])
         self.node_type = "LINK"
+        self.hierarchy_connections, self.connections = [], []
+        self.hierarchy_dependencies, self.dependencies = [], []
+        self.prepared = True
+        self.executed = False
 
     def evaluate_input(self, input_name):
         return default_evaluate_input(self, input_name)
 
-    def GetxForm(self):
-        return GetxForm(self)
+    def GetxForm(self, socket="Deformer"):
+        if socket == "Deformer":
+            return GetxForm(self)
+        else:
+            from .xForm_containers import xFormGeometryObject
+            from .misc_containers import InputExistingGeometryObject
+            from bpy.types import Object
+            if (trace := trace_single_line(self, socket)[0] ) :
+                for i in range(len(trace)): # have to look in reverse, actually
+                    if ( isinstance(trace[ i ], xFormGeometryObject ) ) or ( isinstance(trace[ i ], InputExistingGeometryObject ) ):
+                        return trace[ i ].bGetObject()
+                raise GraphError(wrapRed(f"No other object found for {self}."))
+    
+    # DUPLICATED FROM xForm_containers::xFormBone 
+    # DEDUP HACK HACK HACK HACK HACK
+    def bGetParentArmature(self):
+        from .xForm_containers import xFormArmature
+        from .misc_containers import InputExistingGeometryObject
+        from bpy.types import Object
+        if (trace := trace_single_line(self, "Armature Object")[0] ) :
+            for i in range(len(trace)):
+                # have to look in reverse, actually
+                if ( isinstance(trace[ i ], xFormArmature ) ):
+                    return trace[ i ].bGetObject()
+                elif ( isinstance(trace[i], InputExistingGeometryObject)):
+                    if (ob := trace[i].bGetObject()).type == "ARMATURE":
+                        return ob
+
+        return None
+        #should do the trick...
 
     def bExecute(self, bContext = None,):
-        prGreen("Executing Armature Deform Node\n")
-        print(self.GetxForm())
-        prOrange("My object: %s\n" % (self.GetxForm().bGetObject()))
-        prepare_parameters(self)
-        d = self.GetxForm().bGetObject().modifiers.new(self.evaluate_input("Name"), type='ARMATURE')
-        self.bObject = d
-        get_target_and_subtarget(self, d)
-        props_sockets = {
-        'vertex_group'       : ("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)   
+        self.executed = True
     
-    def initialize_vgroups(self, use_existing = False):
+    def initialize_vgroups(self,):
         ob = self.GetxForm().bGetObject()
-        if use_existing == False:
-            ob.vertex_groups.clear()
-        armOb = self.evaluate_input("Target").bGetObject()
-        deform_bones = []
+        armOb = self.bGetParentArmature()
         for b in armOb.data.bones:
-            if b.use_deform == True:
-                deform_bones.append(b)
-        for b in deform_bones:
+            if b.use_deform == False:
+                continue
             vg = ob.vertex_groups.get(b.name)
             if not vg:
                 vg = ob.vertex_groups.new(name=b.name)
                 num_verts = len(ob.data.vertices)
                 vg.add(range(num_verts), 0, 'REPLACE')
-            
+    
+    def copy_weights(self):
+        # we'll use modifiers for this, maybe use GN for it in the future tho
+        import bpy
+        ob = self.GetxForm().bGetObject()
+        copy_from = self.GetxForm(socket="Copy Skin Weights From")
+        m = ob.modifiers.new(type="DATA_TRANSFER", name="Mantis_temp_data_transfer")
+        m.object = None; m.use_vert_data = True
+        m.data_types_verts = {'VGROUP_WEIGHTS'}
+        m.vert_mapping = 'POLYINTERP_NEAREST'
+        m.layers_vgroup_select_src = 'ALL'
+        m.layers_vgroup_select_dst = 'NAME'
+        m.object = copy_from
+        ob.modifiers.move(len(ob.modifiers)-1, 0)
+
+        # ob.data = ob.data.copy()
+        if False: #MAYBE the mouse needs to be in the 3D viewport, no idea how to set this in an override
+            # TODO: figure out how to apply this, context is incorrect because armature is still in pose mode
+            original_active = bpy.context.active_object
+            original_mode = original_active.mode
+            bpy.ops.object.mode_set(mode='OBJECT')
+            with bpy.context.temp_override(**{'active_object':ob, 'selected_objects':[ob, copy_from]}):
+                # bpy.ops.object.datalayout_transfer(modifier=m.name) # note: this operator is used by the modifier or stand-alone in the UI
+                # the poll for this operator is defined in blender/source/blender/editors/object/object_data_transfer.cc
+                # and blender/source/blender/editors/object/object_modifier.cc
+                # bpy.ops.object.modifier_apply(modifier=m.name, single_user=True)
+                bpy.ops.object.datalayout_transfer(data_type='VGROUP_WEIGHTS')
+                bpy.ops.object.data_transfer(data_type='VGROUP_WEIGHTS')
+            bpy.ops.object.mode_set(mode=original_mode)
     
     def bFinalize(self, bContext=None):
+        prGreen("Executing Armature Deform Node")
+        mod_name = self.evaluate_input("Name")
+        try:
+            d = self.GetxForm().bGetObject().modifiers[mod_name]
+        except KeyError:
+            d = self.GetxForm().bGetObject().modifiers.new(mod_name, type='ARMATURE')
+        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 reatarded and leads to somewhat unpredictable
             #  behaviour, e.g. what object will be selected? What mode?
@@ -115,7 +188,7 @@ class DeformerArmature:
             self.initialize_vgroups()
             bContext.view_layer.depsgraph.update()
             ob = self.GetxForm().bGetObject()
-            armOb = self.evaluate_input("Target").bGetObject()
+            armOb = self.bGetParentArmature()
             deform_bones = []
             for pb in armOb.pose.bones:
                 if pb.bone.use_deform == True:
@@ -140,12 +213,321 @@ class DeformerArmature:
                 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.
-        if skin_method == "EXISTING_GROUPS":
-            self.initialize_vgroups(use_existing = True)
+        elif skin_method == "EXISTING_GROUPS":
+            pass
+        elif skin_method == "COPY_FROM_OBJECT":
+            self.copy_weights()
+            # self.initialize_vgroups(use_existing = True)
+
+
+class DeformerMorphTarget:
+    '''A node representing an armature deformer'''
+    def __init__(self, signature, base_tree):
+        self.base_tree=base_tree
+        self.signature = signature
+        self.inputs = {
+          "Relative to"  : NodeSocket(is_input = True, name = "Relative To", node = self,),
+          "Object"       : NodeSocket(is_input = True, name = "Object", node = self,),
+          "Deformer"     : NodeSocket(is_input = True, name = "Deformer", node = self),
+          "Vertex Group" : NodeSocket(is_input = True, name = "Vertex Group", node = self),
+        }
+        self.outputs = {
+          "Deformer" : NodeSocket(is_input = False, name = "Deformer", node=self),
+          "Morph Target" : NodeSocket(is_input = False, name = "Morph Target", node=self), }
+        self.parameters = {
+          "Name"               : None,
+          "Relative to"        : None,
+          "Object"             : None,
+          "Morph Target"       : None,
+          "Deformer"           : None,
+          "Vertex Group"       : None,
+        }
+        # now set up the traverse target...
+        self.inputs["Deformer"].set_traverse_target(self.outputs["Deformer"])
+        self.outputs["Deformer"].set_traverse_target(self.inputs["Deformer"])
+        self.node_type = "LINK"
+        self.hierarchy_connections, self.connections = [], []
+        self.hierarchy_dependencies, self.dependencies = [], []
+        self.prepared = True
+        self.executed = False
+    
+    def GetxForm(self, trace_input="Object"):
+        trace = trace_single_line(self, trace_input)
+        for node in trace[0]:
+            if (node.__class__ in [xFormGeometryObject, InputExistingGeometryObject]):
+                return node
+        raise GraphError("%s is not connected to an upstream xForm" % self)
+
+
+    def bExecute(self, bContext = None,):
+        prGreen("Executing Morph Target Node")
+        name = ''
+
+        ob = None; relative = None
+        try:
+            ob = self.GetxForm().bGetObject().name 
+        except Exception as e: # this will and should throw an error if it fails
+            prRed(f"Execution failed at {self}: no object found for morph target.")
+            raise e
+        if self.inputs["Relative to"].is_linked:
+            try:
+                relative = self.GetxForm("Relative to").bGetObject().name
+            except Exception as e: # same here
+                prRed(f"Execution failed at {self}: no relative object found for morph target, despite link existing.")
+                raise e
+
+        vg = self.evaluate_input("Vertex Group") if self.evaluate_input("Vertex Group") else "" # just make sure it is a string
+        
+        mt={"object":ob, "vertex_group":vg, "relative_shape":relative}
+
+        self.parameters["Morph Target"] = mt
+        self.executed = True
+
+
+
+class DeformerMorphTargetDeform:
+    '''A node representing an armature deformer'''
+
+    def __init__(self, signature, base_tree):
+        self.base_tree=base_tree
+        self.signature = signature
+        self.inputs = {
+          "Deformer"            : NodeSocket(is_input = True, name = "Deformer", node = self),
+        }
+        self.outputs = {
+          "Deformer" : NodeSocket(is_input = False, name = "Deformer", node=self), }
+        self.parameters = {
+          "Name"                : None,
+          "Deformer"            : None,}
+        # now set up the traverse target...
+        self.inputs["Deformer"].set_traverse_target(self.outputs["Deformer"])
+        self.outputs["Deformer"].set_traverse_target(self.inputs["Deformer"])
+        self.node_type = "LINK"
+        self.hierarchy_connections, self.connections = [], []
+        self.hierarchy_dependencies, self.dependencies = [], []
+        self.prepared = True
+        self.executed = True
+        self.bObject = None
+        setup_custom_props(self)
+
+    def GetxForm(self):
+        return GetxForm(self)
+    
+
+    def gen_morph_target_modifier(self):
+        mod_name = self.evaluate_input("Name")
+        # self.GetxForm().bGetObject().add_rest_position_attribute = True # this ended up being unnecessary
+        try:
+            m = self.GetxForm().bGetObject().modifiers[mod_name]
+        except KeyError:
+            m = self.GetxForm().bGetObject().modifiers.new(mod_name, type='NODES')
+        self.bObject = m
+        # at this point we make the node tre
+        from bpy import data
+        ng = data.node_groups.new(mod_name, "GeometryNodeTree")
+        m.node_group = ng
+        ng.interface.new_socket("Geometry", in_out="INPUT", socket_type="NodeSocketGeometry")
+        ng.interface.new_socket("Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
+        inp = ng.nodes.new("NodeGroupInput")
+        out = ng.nodes.new("NodeGroupOutput")
+        # TODO CLEANUP here
+        if (position := ng.nodes.get("Position")) is None: position = ng.nodes.new("GeometryNodeInputPosition")
+        if (index := ng.nodes.get("Index")) is None: index = ng.nodes.new("GeometryNodeInputIndex")
+        # if (rest_position := ng.nodes.get("Rest Position")) is None: rest_position = ng.nodes.new("GeometryNodeInputNamedAttribute"); rest_position.name = "Rest Position"
+        # rest_position.data_type = "FLOAT_VECTOR"; rest_position.inputs["Name"].default_value = "rest_position"
+        rest_position = position
+        add_these = []
+
+        props_sockets={}
+        object_map = {}
+
+        targets = []
+        for k,v in self.inputs.items():
+            if "Target" in k:
+                targets.append(v)
+        for i, t in enumerate(targets):
+            mt_node = t.links[0].from_node
+            # mt_name = "Morph Target."+str(i).zfill(3)
+            mt_name = mt_node.GetxForm().bGetObject().name
+            vg = mt_node.parameters["Morph Target"]["vertex_group"]
+            if vg: mt_name = mt_name+"."+vg
+            try:
+                ob_relative = t.links[0].from_node.inputs["Relative to"].links[0].from_node.bGetObject()
+            except IndexError:
+                ob_relative = None
+            
+            ng.interface.new_socket(mt_name, in_out = "INPUT", socket_type="NodeSocketObject")
+            ng.interface.new_socket(mt_name+" Value", in_out = "INPUT", socket_type="NodeSocketFloat")
+            ob_node = ng.nodes.new("GeometryNodeObjectInfo")
+            sample_index = ng.nodes.new("GeometryNodeSampleIndex"); sample_index.data_type = 'FLOAT_VECTOR'
+            # if (rest_position := ng.nodes.get("Rest Position")) is None: rest_position = ng.nodes.new("GeometryNodeInputNamedAttribute"); rest_position.name = "Rest Position"
+            # rest_position.data_type = "FLOAT_VECTOR"; rest_position.inputs["Name"].default_value = "rest_position"
+            subtract = ng.nodes.new("ShaderNodeVectorMath"); subtract.operation="SUBTRACT"
+            scale1 = ng.nodes.new("ShaderNodeVectorMath"); scale1.operation="SCALE"
+            
+
+            ng.links.new(input=inp.outputs[mt_name], output=ob_node.inputs["Object"])
+            ng.links.new(input=index.outputs["Index"], output=sample_index.inputs["Index"])
+            ng.links.new(input=position.outputs["Position"], output=sample_index.inputs["Value"])
+            ng.links.new(input=sample_index.outputs["Value"], output=subtract.inputs[0])
+            ng.links.new(input=ob_node.outputs["Geometry"], output=sample_index.inputs["Geometry"])
+
+            if ob_relative: # TODO: this should also be exposed as an input
+                ob_node1 = ng.nodes.new("GeometryNodeObjectInfo"); ob_node1.inputs["Object"].default_value = ob_relative
+                sample_index1 = ng.nodes.new("GeometryNodeSampleIndex"); sample_index1.data_type = 'FLOAT_VECTOR'
+                ng.links.new(input=index.outputs["Index"], output=sample_index1.inputs["Index"])
+                ng.links.new(input=position.outputs["Position"], output=sample_index1.inputs["Value"])
+                ng.links.new(input=ob_node1.outputs["Geometry"], output=sample_index1.inputs["Geometry"])
+                ng.links.new(input=sample_index1.outputs["Value"], output=subtract.inputs[1])
+            else:
+                # ng.links.new(input=rest_position.outputs["Attribute"], output=subtract.inputs[1])
+                ng.links.new(input=rest_position.outputs["Position"], output=subtract.inputs[1])
+
+            # IMPORTANT TODO (?):
+               # relative objects can be recursive! Need to go back and back and back as long as we have relative objects!
+               # in reality I am not sure haha
+
+            ng.links.new(input=subtract.outputs["Vector"], output=scale1.inputs[0])
+
+            # TODO: this should be exposed as a node tree input
+            if vg:= mt_node.evaluate_input("Vertex Group"): # works
+                vg_att = ng.nodes.new("GeometryNodeInputNamedAttribute"); vg_att.inputs["Name"].default_value=vg
+                multiply = ng.nodes.new("ShaderNodeMath"); multiply.operation = "MULTIPLY"
+                ng.links.new(input=vg_att.outputs["Attribute"], output=multiply.inputs[1])
+                ng.links.new(input=inp.outputs[mt_name+" Value"], output=multiply.inputs[0])
+                ng.links.new(input=multiply.outputs[0], output=scale1.inputs["Scale"])
+            else:
+                ng.links.new(input=inp.outputs[mt_name+" Value"], output=scale1.inputs["Scale"])
+            add_these.append(scale1)
+            object_map["Socket_"+str((i+1)*2)]=mt_node.GetxForm().bGetObject()
+            props_sockets["Socket_"+str((i+1)*2+1)]= ("Value."+str(i).zfill(3), 1.0)
+        
+        set_position = ng.nodes.new("GeometryNodeSetPosition")
+        ng.links.new(inp.outputs["Geometry"], output=set_position.inputs["Geometry"])
+        ng.links.new(set_position.outputs["Geometry"], output=out.inputs["Geometry"])
+
+        
+        prev_node = rest_position
+        for i, node in enumerate(add_these):
+            add = ng.nodes.new("ShaderNodeVectorMath"); add.operation="ADD"
+            ng.links.new(prev_node.outputs[0], output=add.inputs[0])
+            ng.links.new(node.outputs[0], output=add.inputs[1])
+            prev_node = add
+        ng.links.new(add.outputs[0], output=set_position.inputs["Position"])
+        
+        from .utilities import SugiyamaGraph
+        SugiyamaGraph(ng, 12)
+
+
+        # for k,v in props_sockets.items():
+        #     print(wrapWhite(k), wrapOrange(v))
+        evaluate_sockets(self, m, props_sockets)
+        for socket, ob in object_map.items():
+            m[socket]=ob
+        finish_drivers(self)
+
+    def gen_shape_key(self): # TODO: make this a feature of the node definition that appears only when there are no prior deformers - and shows a warning!
+        self.gen_morph_target_modifier()
+        return
+        # 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
+        #   - figure out how to make this a lot faster
+        #   - edit the xForm stuff to delete drivers from shape key ID's, since they belong to the Key, not the Object.
+        from time import time
+        start_time = time()
+        from bpy import data
+        ob = self.GetxForm().bGetObject()
+        m = data.meshes.new_from_object(ob, preserve_all_data_layers=True)
+        ob.data = m
+        ob.add_rest_position_attribute = True
+        ob.shape_key_clear()
+
+        targets = []
+        for k,v in self.inputs.items():
+            if "Target" in k:
+                targets.append(v)
+        for i, t in enumerate(targets):
+            mt_node = t.links[0].from_node
+            mt_name = "Morph Target."+str(i).zfill(3)
+        
+        # using the built-in shapekey feature is actually a lot harder in terms of programming because I need...
+            # min/max, as it is just not a feature of the GN version
+            # to carry info from the morph target node regarding relative shapes and vertex groups and all that
+            # the drivers may be more difficult to apply, too.
+            # hafta make new geometry for the object and add shape keys and all that
+            # the benefit to all this being maybe better performence and exporting to game engines via .fbx
+
+        #
+        # first make a basis shape key
+        ob.shape_key_add(name='Basis', from_mix=False)
+        keys={}
+        props_sockets={}
+        for i, t in enumerate(targets):
+            mt_node = t.links[0].from_node
+            # mt_name = "Morph Target."+str(i).zfill(3)
+            sk_ob = mt_node.GetxForm().bGetObject()
+            mt_name = sk_ob.name
+            vg = mt_node.parameters["Morph Target"]["vertex_group"]
+            if vg: mt_name = mt_name+"."+vg
+            
+            sk = ob.shape_key_add(name=mt_name, from_mix=False)
+            # the shapekey data is absolute point data for each vertex, in order, very simple
+            for j in range(len(m.vertices)):
+                sk.data[j].co = sk_ob.data.vertices[j].co # assume they match
+            sk.vertex_group = vg
+            sk.slider_min = -10
+            sk.slider_max = 10
+            keys[mt_name]=sk
+            props_sockets[mt_name]= ("Value."+str(i).zfill(3), 1.0)
+        for i, t in enumerate(targets):
+            mt_node = t.links[0].from_node
+            # mt_name = "Morph Target."+str(i).zfill(3)
+            sk_ob = mt_node.GetxForm().bGetObject()
+            mt_name = sk_ob.name
+            vg = mt_node.parameters["Morph Target"]["vertex_group"]
+            if vg: mt_name = mt_name+"."+vg
+            if rel := mt_node.parameters["Morph Target"]["relative_shape"]:
+                sk = keys.get(mt_name)
+                sk.relative_key = keys.get(rel)
+        
+        # for k,v in props_sockets.items():
+        #     print(wrapWhite(k), wrapOrange(v), wrapRed(self.evaluate_input(v)))
+        self.bObject = sk.id_data
+        evaluate_sockets(self, sk.id_data, props_sockets)
+        finish_drivers(self)
+            
+        
+        prWhite(f"Initializing morph target took {time() -start_time} seconds")
+        
+        
+        
+            
+
+            
+
+        # then we need to get all the data from the morph targets, pull all the relative shapes first and add them, vertex groups and properties
+        # next we add all the shape keys that are left, and their vertex groups
+        # set the slider ranges to -10 and 10
+        # then set up the drivers
+        
+
+    def bFinalize(self, bContext=None):
+        # let's find out if there is a prior deformer.
+        # if not, 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.
+        if self.inputs["Deformer"].is_linked:
+            self.gen_morph_target_modifier()
+        else:
+            # for now we'll just do it this way.
+            self.gen_shape_key()
+
+
+        
+        
+            
 
         
-    def __repr__(self):
-        return self.signature.__repr__()
 
-    def fill_parameters(self):
-        fill_parameters(self)
+for c in TellClasses():
+    setup_container(c)

+ 124 - 7
deformer_definitions.py

@@ -1,11 +1,17 @@
 import bpy
 from bpy.types import NodeTree, Node, NodeSocket
-from .base_definitions import MantisNode, DeformerNode
+from .base_definitions import MantisNode, DeformerNode, get_signature_from_edited_tree
+
+from .utilities import (prRed, prGreen, prPurple, prWhite, prOrange,
+                        wrapRed, wrapGreen, wrapPurple, wrapWhite,
+                        wrapOrange,)
 
 
 def TellClasses():
     return [
              DeformerArmatureNode,
+             DeformerMorphTargetDeform,
+             DeformerMorphTarget,
            ]
 icons = (
           'NONE', 'QUESTION', 'ERROR', 'CANCEL', 'TRIA_RIGHT',
@@ -257,8 +263,8 @@ icons = (
 
 def default_traverse(self, socket):
         if (socket == self.outputs["Deformer"]):
-            return self.inputs["Input Relationship"]
-        if (socket == self.inputs["Input Relationship"]):
+            return self.inputs["Deformer"]
+        if (socket == self.inputs["Deformer"]):
             return self.outputs["Deformer"]
         return None
 
@@ -267,11 +273,14 @@ class DeformerArmatureNode(Node, DeformerNode):
     bl_idname = 'DeformerArmature'
     bl_label = "Armature Deform"
     bl_icon = 'MOD_ARMATURE'
+    initialized : bpy.props.BoolProperty(default = False)
 
     def init(self, context):
         # self.inputs.new ("RelationshipSocket", "Input Relationship")
-        self.inputs.new('xFormSocket', "Target")
-        self.inputs.new('StringSocket', "Vertex Group")
+        self.inputs.new('xFormSocket', "Armature Object")
+        self.inputs.new('StringSocket', "Blend Vertex Group")
+        # self.inputs.new('StringSocket', "Preserve Volume Vertex Group") #TODO figure out the right UX for automatic dual-quat blending
+        self.inputs.new('BooleanSocket', "Invert Vertex Group")
         
         self.inputs.new('BooleanSocket', "Preserve Volume")
         # TODO: make the above controlled by a vertex group instead.
@@ -279,9 +288,117 @@ class DeformerArmatureNode(Node, DeformerNode):
         self.inputs.new('BooleanSocket', "Use Envelopes")
         self.inputs.new('BooleanSocket', "Use Vertex Groups")
         
+        self.inputs.new("DeformerSocket", "Deformer")
+
+        s = self.inputs.new("xFormSocket", "Copy Skin Weights From")
+        s.hide = True
         self.inputs.new("EnumSkinning", "Skinning Method")
         
         self.outputs.new('DeformerSocket', "Deformer")
+        self.initialized = True
+    
+    def display_update(self, parsed_tree, context):
+        self.inputs["Copy Skin Weights From"].hide = True
+        node_tree = context.space_data.path[0].node_tree
+        nc = parsed_tree.get(get_signature_from_edited_tree(self, context))
+        if nc:
+            if nc.evaluate_input("Skinning Method") == "COPY_FROM_OBJECT":
+                self.inputs["Copy Skin Weights From"].hide = False
+
+from .utilities import get_socket_maps, relink_socket_map
+
+# TODO this should probably not be in this file but intstead in Utilities or something
+def simple_do_relink(node, map, in_out='INPUT'):
+    from bpy.types import NodeSocket
+    for key, val in map.items():
+        s = node.inputs.get(key) if in_out == "INPUT" else node.outputs.get(key)
+        if s is None:
+            if in_out == "INPUT":
+                if node.num_targets > 0:
+                    s = node.inputs["Target."+str(node.num_targets-1).zfill(3)]
+                else:
+                    continue
+        if isinstance(val, list):
+            for sub_val in val:
+                if isinstance(sub_val, NodeSocket):
+                    if in_out =='INPUT':
+                        node.id_data.links.new(input=sub_val, output=s)
+                    else:
+                        node.id_data.links.new(input=s, output=sub_val)
+        else:
+            try:
+                s.default_value = val
+            except (AttributeError, ValueError, TypeError): # must be readonly or maybe it doesn't have a d.v.. TypeError if the d.v. is None at this point
+                pass
+
+
+# Dynamic
+#   - each Morph Target gets a MT input
+#   - each Morph Target gets an influence input
+# this node creates a GN deformer that ADDS the position deltas (from the base position)
+# Value has to scale the delta
+class DeformerMorphTargetDeform(Node, DeformerNode):
+    '''A node representing a Morph Target Deformer'''
+    bl_idname = 'DeformerMorphTargetDeform'
+    bl_label = "Morph Deform"
+    bl_icon = 'MOD_ARMATURE'
+    initialized : bpy.props.BoolProperty(default = False)
+    num_targets : bpy.props.IntProperty(default = 0)
+
+
+    def init(self, context):
+        self.id_data.do_live_update = False
+        self.inputs.new('DeformerSocket', 'Previous Deformer', )
+        self.inputs.new('WildcardSocket', '', identifier='__extend__')
+        self.outputs.new('DeformerSocket', "Deformer")
+        self.update()
+
+    def update(self):
+        if self.id_data.is_executing:
+            return # so that we don't update it while saving/loading the tree
+        self.initialized = False
+        input_map = get_socket_maps(self)[0]
+        # checc to see if targets have been removed... then modify the input map if necessary
+        targets_deleted = 0 # this should usually be either 0 or 1
+        for i in range(self.num_targets):
+            name = "Target."+str(i).zfill(3); inf_name = "Value."+str(i).zfill(3)
+            if self.inputs[name].is_linked == False:
+                del input_map[name]; del input_map[inf_name]
+                targets_deleted+=1
+            elif targets_deleted: # move it back
+                new_name = "Target."+str(i-targets_deleted).zfill(3); new_inf_name = "Value."+str(i-targets_deleted).zfill(3)
+                input_map[new_name] = input_map[name]; input_map[new_inf_name] = input_map[inf_name]
+                del input_map[name]; del input_map[inf_name]
+        self.num_targets-=targets_deleted
+        if self.inputs[-1].is_linked and self.inputs[-1].bl_idname == 'WildcardSocket':
+            self.num_targets+=1
+        self.inputs.clear()
+        self.inputs.new('DeformerSocket', 'Previous Deformer', )
+        # have to do this manually to avoid making things harder elsewhere
+        # input_map
+        for i in range(self.num_targets):
+            self.inputs.new("MorphTargetSocket", "Target."+str(i).zfill(3))
+            self.inputs.new("FloatSocket", "Value."+str(i).zfill(3))
+        # if self.num_targets > 0:
+        simple_do_relink(self, input_map, in_out='INPUT')
+        if len(self.inputs)<1 or self.inputs[-1].bl_idname not in ["WildcardSocket"]:
+            self.inputs.new('WildcardSocket', '', identifier='__extend__')
+        self.initialized = True
+
+# TODO: there is no reason for this to be a separate node!
+class DeformerMorphTarget(Node, DeformerNode):
+    '''A node representing a single Morph Target'''
+    bl_idname = 'DeformerMorphTarget'
+    bl_label = "Morph Target"
+    bl_icon = 'SHAPEKEY_DATA'
+    initialized : bpy.props.BoolProperty(default = False)
+    num_targets : bpy.props.IntProperty(default = 0)
+
+    def init(self, context):
+        self.inputs.new('xFormSocket', "Relative to")
+        self.inputs.new('xFormSocket', "Object")
+        self.inputs.new('StringSocket', "Vertex Group")
+        self.outputs.new('MorphTargetSocket', "Morph Target")
 
-    def traverse(self, socket):
-        return default_traverse(self,socket)
+        self.initialized = True
+    

+ 18 - 3
drivers.py

@@ -55,14 +55,16 @@ class MantisDriver(dict):
 def CreateDrivers(drivers):
     def brackets(s):
         return "[\""+s+"\"]"
-    from bpy.types import Object
+    from bpy.types import Object, Key
     for driver in drivers:
-        # print (driver)
         if (isinstance(driver["owner"], Object)):
             ob = driver["owner"]
         else: # Pose Bone:
             ob = driver["owner"].id_data
-        fc = ob.driver_add(driver["owner"].path_from_id(driver["prop"]), driver["ind"])
+        if isinstance(driver["owner"], Key):
+            fc = ob.driver_add(driver["prop"])
+        else:
+            fc = ob.driver_add(driver["owner"].path_from_id(driver["prop"]), driver["ind"])
         drv = fc.driver
         try: # annoyingly, this initializes with a modifier
             fc.modifiers.remove(fc.modifiers[0])
@@ -80,11 +82,21 @@ def CreateDrivers(drivers):
             dVar = drv.variables.remove(v)
             
         for v in driver["vars"]:
+            bone = ''; target2bone = ''
+            vob, target2ob = None, None
             if (isinstance(v["owner"], Object)):
                 vob = v["owner"]
             else:
                 vob = v["owner"].id_data
                 bone = v["owner"].name
+            #
+            
+            if "xForm 2" in v.keys() and v["xForm 2"]:
+                if (isinstance(v["xForm 2"], Object)):
+                    target2ob = v["xForm 2"]
+                else:
+                    target2ob = v["xForm 2"].id_data
+                    target2bone = v["xForm 2"].name
             
             dVar = drv.variables.new()
             
@@ -98,6 +110,9 @@ def CreateDrivers(drivers):
             
             dVar.targets[0].id = vob
             dVar.targets[0].bone_target = bone
+            if len(dVar.targets) > 1:
+                dVar.targets[1].id = target2ob
+                dVar.targets[1].bone_target = target2bone
             
             if (dVar.type == "TRANSFORMS"):
                 dVar.targets[0].transform_space = v["space"]

+ 8 - 4
f_nodegraph.py

@@ -1,3 +1,8 @@
+# TODO FIXME UNBREAK
+# why in the hell does this file even exist
+
+
+
 #Node Graph Functions
 
         
@@ -9,13 +14,12 @@ def SeekNodePathUntil(node, input_name, nodeType, direction = 'BACK'):
     else: # 'FORWARD'
         return trace_single_line_up(node, input_name)
     
-
+# may not work anymore but that should be OK
 def get_node_container(node, context):
-    from .utilities import parse_node_tree, print_lines
+    # from .utilities import parse_node_tree, print_lines
     base_tree = context.space_data.path[0].node_tree
     from .readtree import get_tree_data
-    parsed_tree = parse_node_tree(base_tree)
-    nodes = get_tree_data(parsed_tree, base_tree)
+    nodes = base_tree.parsed_tree
     node_container = None
     if (node.id_data != context.space_data.path[-1].node_tree):
         return None, None

+ 0 - 1
grandalf/__init__.py

@@ -1 +0,0 @@
-__all__ = ["graphs", "layouts", "routing", "utils"]

+ 0 - 879
grandalf/graphs.py

@@ -1,879 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""
-.. _graphs:
-
-graphs.py
-=========
-This module implements essential graph classes for representing
-vertices (nodes), edges (links), and graphs.
-
-"""
-
-# This code is part of Grandalf
-# Copyright (C) 2008-2011 Axel Tillequin (bdcht3@gmail.com)
-# published under GPLv2 license or EPLv1 license
-
-from mantis.grandalf.utils import Poset
-
-# ------------------------------------------------------------------------------
-
-
-class vertex_core(object):
-    """ The Vertex essentials attributes and methods.
-
-        Attributes:
-            e (list[Edge]): list of edges associated with this vertex.
-
-        Methods:
-            deg() : degree of the vertex (number of edges).
-            e_in() : list of edges directed toward this vertex.
-            e_out(): list of edges directed outward this vertex.
-            e_dir(int): either e_in, e_out or all edges depending on
-               provided direction parameter (>0 means outward).
-            N(f_io=0): list of neighbor vertices in all directions (default)
-               or in filtered f_io direction (>0 means outward).
-            e_to(v): returns the Edge from this vertex directed toward vertex v.
-            e_from(v): returns the Edge from vertex v directed toward this vertex.
-            e_with(v): return the Edge with both this vertex and vertex v
-            detach(): removes this vertex from all its edges and returns this list
-               of edges.
-    """
-
-    def __init__(self):
-        # will hold list of edges for this vertex (adjacency list)
-        self.e = []
-
-    def deg(self):
-        return len(self.e)
-
-    def e_in(self):
-        return list(filter((lambda e: e.v[1] == self), self.e))
-
-    def e_out(self):
-        return list(filter((lambda e: e.v[0] == self), self.e))
-
-    def e_dir(self, dir):
-        if dir > 0:
-            return self.e_out()
-        if dir < 0:
-            return self.e_in()
-        return self.e
-
-    def N(self, f_io=0):
-        N = []
-        if f_io <= 0:
-            N += [e.v[0] for e in self.e_in()]
-        if f_io >= 0:
-            N += [e.v[1] for e in self.e_out()]
-        return N
-
-    def e_to(self, y):
-        for e in self.e_out():
-            if e.v[1] == y:
-                return e
-        return None
-
-    def e_from(self, x):
-        for e in self.e_in():
-            if e.v[0] == x:
-                return e
-        return None
-
-    def e_with(self, v):
-        for e in self.e:
-            if v in e.v:
-                return e
-        return None
-
-    def detach(self):
-        E = self.e[:]
-        for e in E:
-            e.detach()
-        assert self.deg() == 0
-        return E
-
-
-# ------------------------------------------------------------------------------
-
-
-class edge_core(object):
-    """The Edge essentials attributes.
-
-       Attributes:
-          v (list[Vertex]): list of vertices associated with this edge.
-          deg (int): degree of the edge (number of unique vertices).
-    """
-
-    def __init__(self, x, y):
-        self.deg = 0 if x == y else 1
-        self.v = (x, y)
-
-
-# ------------------------------------------------------------------------------
-
-
-class Vertex(vertex_core):
-    """Vertex class enhancing a vertex_core with graph-related features.
-       
-       Attributes:
-          c (graph_core): the component of connected vertices that contains this vertex.
-             By default a vertex belongs no component but when it is added in a
-             graph, c points to the connected component in this graph.
-          data (object) : an object associated with the vertex.
-    """
-
-    def __init__(self, data=None):
-        super().__init__()
-        # by default, a new vertex belongs to its own component
-        # but when the vertex is added to a graph, c points to the
-        # connected component where it belongs.
-        self.c = None
-        self.data = data
-        self.__index = None
-
-    @property
-    def index(self):
-        if self.__index:
-            return self.__index
-        elif isinstance(self.c, graph_core):
-            self.__index = self.c.sV.index(self)
-            return self.__index
-        else:
-            return None
-
-    def __lt__(self, v):
-        return 0
-
-    def __gt__(self, v):
-        return 0
-
-    def __le__(self, v):
-        return 0
-
-    def __ge__(self, v):
-        return 0
-
-    def __getstate__(self):
-        return (self.index, self.data)
-
-    def __setstate__(self, state):
-        self.__index, self.data = state
-        self.c = None
-        self.e = []
-
-
-# ------------------------------------------------------------------------------
-
-
-class Edge(edge_core):
-    """Edge class enhancing edge_core with attributes and methods related to the graph.
-
-       Attributes:
-         w (int): a weight associated with the edge (default 1) used by Dijkstra to
-           find min-flow paths.
-         data (object): an object associated with the edge.
-         feedback (bool): indicates if the edge has been marked as a *feeback* edge
-           by the Tarjan algorithm which means that it is part of a cycle and that
-           inverting this edge would remove this cycle.
-
-       Methods:
-         attach(): add this edge in its vertices edge lists.
-         detach(): remove this edge from its vertices edge lists.
-    """
-
-    def __init__(self, x, y, w=1, data=None, connect=False):
-        super().__init__(x, y)
-        # w is an optional weight associated with the edge.
-        self.w = w
-        self.data = data
-        self.feedback = False
-        if connect and (x.c is None or y.c is None):
-            c = x.c or y.c
-            c.add_edge(self)
-
-    def attach(self):
-        if not self in self.v[0].e:
-            self.v[0].e.append(self)
-        if not self in self.v[1].e:
-            self.v[1].e.append(self)
-
-    def detach(self):
-        if self.deg == 1:
-            assert self in self.v[0].e
-            assert self in self.v[1].e
-            self.v[0].e.remove(self)
-            self.v[1].e.remove(self)
-        else:
-            if self in self.v[0].e:
-                self.v[0].e.remove(self)
-            assert self not in self.v[0].e
-        return [self]
-
-    def __lt__(self, v):
-        return 0
-
-    def __gt__(self, v):
-        return 0
-
-    def __le__(self, v):
-        return 0
-
-    def __ge__(self, v):
-        return 0
-
-    def __getstate__(self):
-        xi, yi = (self.v[0].index, self.v[1].index)
-        return (xi, yi, self.w, self.data, self.feedback)
-
-    def __setstate__(self, state):
-        xi, yi, self.w, self.data, self.feedback = state
-        self._v = [xi, yi]
-        self.deg = 0 if xi == yi else 1
-
-
-# ------------------------------------------------------------------------------
-
-
-class graph_core(object):
-    """A connected graph of Vertex/Edge objects. A graph_core is a *component*
-       of a Graph that contains a connected set of Vertex and Edges.
-
-       Attributes:
-         sV (poset[Vertex]): the partially ordered set of vertices of the graph.
-         sE (poset[Edge]): the partially ordered set of edges of the graph.
-         degenerated_edges (set[Edge]): the set of *degenerated* edges (of degree 0).
-         directed (bool): indicates if the graph is considered *oriented* or not.
-
-       Methods:
-         V(cond=None): generates an iterator over vertices, with optional filter
-         E(cond=None): generates an iterator over edges, with optional filter
-         M(cond=None): returns the associativity matrix of the graph component
-         order(): the order of the graph (number of vertices)
-         norm(): the norm of the graph (number of edges)
-         deg_min(): the minimum degree of vertices
-         deg_max(): the maximum degree of vertices
-         deg_avg(): the average degree of vertices
-         eps(): the graph epsilon value (norm/order), average number of edges per vertex. 
-         path(x,y,f_io=0,hook=None): shortest path between vertices x and y by breadth-first descent,
-           contrained by f_io direction if provided. The path is returned as a list of Vertex objects.
-           If a *hook* function is provided, it is called at every vertex added to the path, passing
-           the vertex object as argument.
-         roots(): returns the list of *roots* (vertices with no inward edges).
-         leaves(): returns the list of *leaves* (vertices with no outward edges).
-         add_single_vertex(v): allow a graph_core to hold a single vertex.
-         add_edge(e): add edge e. At least one of its vertex must belong to the graph,
-           the other being added automatically.
-         remove_edge(e): remove Edge e, asserting that the resulting graph is still connex.
-         remove_vertex(x): remove Vertex x and all associated edges.
-         dijkstra(x,f_io=0,hook=None): shortest weighted-edges paths between x and all other vertices
-           by dijkstra's algorithm with heap used as priority queue.
-         get_scs_with_feedback(): returns the set of strongly connected components
-           ("scs") by using Tarjan algorithm.
-           These are maximal sets of vertices such that there is a path from each
-           vertex to every other vertex.
-           The algorithm performs a DFS from the provided list of root vertices.
-           A cycle is of course a strongly connected component,
-           but a strongly connected component can include several cycles.
-           The Feedback Acyclic Set of edge to be removed/reversed is provided by
-           marking the edges with a "feedback" flag.
-           Complexity is O(V+E).
-         partition(): returns a *partition* of the connected graph as a list of lists.
-         N(v): returns neighbours of a vertex v.
-    """
-
-    def __init__(self, V=None, E=None, directed=True):
-        if V is None:
-            V = []
-        if E is None:
-            E = []
-        self.directed = directed
-        self.sV = Poset(V)
-        self.sE = Poset([])
-
-        self.degenerated_edges = set()
-
-        if len(self.sV) == 1:
-            v = self.sV[0]
-            v.c = self
-            for e in v.e:
-                e.detach()
-            return
-
-        for e in E:
-            x = self.sV.get(e.v[0])
-            y = self.sV.get(e.v[1])
-            if x is None or y is None:
-                raise ValueError("unknown Vertex (%s or %s)" % e.v)
-            e.v = (x, y)
-            if e.deg == 0:
-                self.degenerated_edges.add(e)
-            e = self.sE.add(e)
-            e.attach()
-            if x.c is None:
-                x.c = Poset([x])
-            if y.c is None:
-                y.c = Poset([y])
-            if id(x.c) != id(y.c):
-                x, y = (x, y) if len(x.c) > len(y.c) else (y, x)
-                x.c.update(y.c)
-                for v in y.c:
-                    v.c = x.c
-            s = x.c
-        # check if graph is connected:
-        for v in self.V():
-            if v.c is None or (v.c != s):
-                raise ValueError("unconnected Vertex %s" % v.data)
-            else:
-                v.c = self
-
-    def roots(self):
-        return list(filter(lambda v: len(v.e_in()) == 0, self.sV))
-
-    def leaves(self):
-        return list(filter(lambda v: len(v.e_out()) == 0, self.sV))
-
-    def add_single_vertex(self, v):
-        if len(self.sE) == 0 and len(self.sV) == 0:
-            v = self.sV.add(v)
-            v.c = self
-            return v
-        return None
-
-    def add_edge(self, e):
-        if e in self.sE:
-            return self.sE.get(e)
-        x = e.v[0]
-        y = e.v[1]
-        if not ((x in self.sV) or (y in self.sV)):
-            raise ValueError("unconnected edge")
-        x = self.sV.add(x)
-        y = self.sV.add(y)
-        e.v = (x, y)
-        e.attach()
-        e = self.sE.add(e)
-        x.c = self
-        y.c = self
-        if e.deg == 0:
-            self.degenerated_edges.add(e)
-        return e
-
-    def remove_edge(self, e):
-        if not e in self.sE:
-            return
-        e.detach()
-        # check if still connected (path is not oriented here):
-        if e.deg == 1 and not self.path(e.v[0], e.v[1]):
-            # return to inital state by reconnecting everything:
-            e.attach()
-            # exit with exception!
-            raise ValueError(e)
-        else:
-            e = self.sE.remove(e)
-            if e in self.degenerated_edges:
-                self.degenerated_edges.remove(e)
-            return e
-
-    def remove_vertex(self, x):
-        if x not in self.sV:
-            return
-        V = x.N()  # get all neighbor vertices to check paths
-        E = x.detach()  # remove the edges from x and neighbors list
-        # now we need to check if all neighbors are still connected,
-        # and it is sufficient to check if one of them is connected to
-        # all others:
-        v0 = V.pop(0)
-        for v in V:
-            if not self.path(v0, v):
-                # repair everything and raise exception if not connected:
-                for e in E:
-                    e.attach()
-                raise ValueError(x)
-        # remove edges and vertex from internal sets:
-        for e in E:
-            self.sE.remove(e)
-        x = self.sV.remove(x)
-        x.c = None
-        return x
-
-    def V(self, cond=None):
-        V = self.sV
-        if cond is None:
-            cond = lambda x: True
-        for v in V:
-            if cond(v):
-                yield v
-
-    def E(self, cond=None):
-        E = self.sE
-        if cond is None:
-            cond = lambda x: True
-        for e in E:
-            if cond(e):
-                yield e
-
-    def M(self, cond=None):
-        from array import array
-
-        mat = []
-        for v in self.V(cond):
-            vec = array("b", [0] * self.order())
-            mat.append(vec)
-            for e in v.e_in():
-                v0 = e.v[0]
-                if v0.index == v.index:
-                    continue
-                vec[v0.index] = -e.w
-            for e in v.e_out():
-                v1 = e.v[1]
-                vec[v1.index] = e.w
-        return mat
-
-    def order(self):
-        return len(self.sV)
-
-    def norm(self):
-        return len(self.sE)
-
-    def deg_min(self):
-        return min([v.deg() for v in self.sV])
-
-    def deg_max(self):
-        return max([v.deg() for v in self.sV])
-
-    def deg_avg(self):
-        return sum([v.deg() for v in self.sV]) / float(self.order())
-
-    def eps(self):
-        return float(self.norm()) / self.order()
-
-    def path(self, x, y, f_io=0, hook=None):
-        assert x in self.sV
-        assert y in self.sV
-        x = self.sV.get(x)
-        y = self.sV.get(y)
-        if x == y:
-            return []
-        if f_io != 0:
-            assert self.directed == True
-        # path:
-        p = None
-        if hook is None:
-            hook = lambda x: False
-        # apply hook:
-        hook(x)
-        # visisted:
-        v = {x: None}
-        # queue:
-        q = [x]
-        while (not p) and len(q) > 0:
-            c = q.pop(0)
-            for n in c.N(f_io):
-                if not n in v:
-                    hook(n)
-                    v[n] = c
-                    if n == y:
-                        p = [n]
-                    q.append(n)
-                if p:
-                    break
-        # now we fill the path p backward from y to x:
-        while p and p[0] != x:
-            p.insert(0, v[p[0]])
-        return p
-
-    def dijkstra(self, x, f_io=0, hook=None):
-        from collections import defaultdict
-        from heapq import heappop, heappush
-
-        if x not in self.sV:
-            return None
-        if f_io != 0:
-            assert self.directed == True
-        # initiate with path to itself...
-        v = self.sV.get(x)
-        # D is the returned vector of distances:
-        D = defaultdict(lambda: None)
-        D[v] = 0.0
-        L = [(D[v], v)]
-        while len(L) > 0:
-            l, u = heappop(L)
-            for e in u.e_dir(f_io):
-                v = e.v[0] if (u is e.v[1]) else e.v[1]
-                Dv = l + e.w
-                if D[v] != None:
-                    # check if heap/D needs updating:
-                    # ignore if a shorter path was found already...
-                    if Dv < D[v]:
-                        for i, t in enumerate(L):
-                            if t[1] is v:
-                                L.pop(i)
-                                break
-                        D[v] = Dv
-                        heappush(L, (Dv, v))
-                else:
-                    D[v] = Dv
-                    heappush(L, (Dv, v))
-        return D
-
-    def get_scs_with_feedback(self, roots=None):
-        from sys import getrecursionlimit, setrecursionlimit
-
-        limit = getrecursionlimit()
-        N = self.norm() + 10
-        if N > limit:
-            setrecursionlimit(N)
-
-        def _visit(v, L):
-            v.ind = v.ncur
-            v.lowlink = v.ncur
-            Vertex.ncur += 1
-            self.tstack.append(v)
-            v.mark = True
-            for e in v.e_out():
-                w = e.v[1]
-                if w.ind == 0:
-                    _visit(w, L)
-                    v.lowlink = min(v.lowlink, w.lowlink)
-                elif w.mark:
-                    e.feedback = True
-                if w in self.tstack:
-                    v.lowlink = min(v.lowlink, w.ind)
-            if v.lowlink == v.ind:
-                l = [self.tstack.pop()]
-                while l[0] != v:
-                    l.insert(0, self.tstack.pop())
-                # print "unstacked %s"%('-'.join([x.data[1:13] for x in l]))
-                L.append(l)
-            v.mark = False
-
-        if roots is None:
-            roots = self.roots()
-        self.tstack = []
-        scs = []
-        Vertex.ncur = 1
-        for v in self.sV:
-            v.ind = 0
-        # start exploring tree from roots:
-        for v in roots:
-            v = self.sV.get(v)
-            if v.ind == 0:
-                _visit(v, scs)
-        # now possibly unvisited vertices:
-        for v in self.sV:
-            if v.ind == 0:
-                _visit(v, scs)
-        # clean up Tarjan-specific data:
-        for v in self.sV:
-            del v.ind
-            del v.lowlink
-            del v.mark
-        del Vertex.ncur
-        del self.tstack
-        setrecursionlimit(limit)
-        return scs
-
-    def partition(self):
-        V = self.sV.copy()
-        R = self.roots()
-        for r in R:
-            V.remove(r)
-        parts = []
-        while len(R) > 0:
-            v = R.pop(0)
-            p = Poset([v])
-            l = v.N(+1)
-            while len(l) > 0:
-                x = l.pop(0)
-                if x in p:
-                    continue
-                if all([(y in p) for y in x.N(-1)]):
-                    p.add(x)
-                    if x in R:
-                        R.remove(x)
-                    else:
-                        V.remove(x)
-                    l.extend(x.N(+1))
-                else:
-                    if x in V:
-                        V.remove(x)
-                        R.append(x)
-            parts.append(list(p))
-        return parts
-
-    def N(self, v, f_io=0):
-        return v.N(f_io)
-
-    # general graph properties:
-    # -------------------------
-
-    # returns True iff
-    #  - o is a subgraph of self, or
-    #  - o is a vertex in self, or
-    #  - o is an edge in self
-    def __contains__(self, o):
-        try:
-            return o.sV.issubset(self.sV) and o.sE.issubset(self.sE)
-        except AttributeError:
-            return (o in self.sV) or (o in self.sE)
-
-    # merge graph_core G into self
-    def union_update(self, G):
-        for v in G.sV:
-            v.c = self
-        self.sV.update(G.sV)
-        self.sE.update(G.sE)
-
-    # derivated graphs:
-    # -----------------
-
-    # returns subgraph spanned by vertices V
-    def spans(self, V):
-        raise NotImplementedError
-
-    # returns join of G (if disjoint)
-    def __mul__(self, G):
-        raise NotImplementedError
-
-    # returns complement of a graph G
-    def complement(self, G):
-        raise NotImplementedError
-
-    # contraction G\e
-    def contract(self, e):
-        raise NotImplementedError
-
-    def __getstate__(self):
-        V = [v for v in self.sV]
-        E = [e for e in self.sE]
-        return (V, E, self.directed)
-
-    def __setstate__(self, state):
-        V, E, directed = state
-        for e in E:
-            e.v = [V[x] for x in e._v]
-            del e._v
-        graph_core.__init__(self, V, E, directed)
-
-
-# ------------------------------------------------------------------------------
-
-
-class Graph(object):
-    """Disjoint-set Graph.
-       The graph is stored in disjoint-sets holding each connex component
-       in self.C as a list of graph_core objects.
-
-       Attributes:
-          C (list[graph_core]): list of graph_core components.
-
-       Methods:
-          add_vertex(v): add vertex v into the Graph as a new component
-          add_edge(e): add edge e and its vertices into the Graph possibly merging the
-            associated graph_core components
-          get_vertices_count(): see order()
-          V(): see graph_core
-          E(): see graph_core
-          remove_edge(e): remove edge e possibly spawning two new cores
-            if the graph_core that contained e gets disconnected.
-          remove_vertex(v): remove vertex v and all its edges.
-          order(): the order of the graph (number of vertices)
-          norm(): the norm of the graph (number of edges)
-          deg_min(): the minimum degree of vertices
-          deg_max(): the maximum degree of vertices
-          deg_avg(): the average degree of vertices
-          eps(): the graph epsilon value (norm/order), average number of edges per vertex. 
-          connected(): returns True if the graph is connected (i.e. it has only one component).
-          components(): returns self.C
-    """
-
-    component_class = graph_core
-
-    def __init__(self, V=None, E=None, directed=True):
-        if V is None:
-            V = []
-        if E is None:
-            E = []
-        self.directed = directed
-        # tag connex set of vertices:
-        # at first, every vertex is its own component
-        for v in V:
-            v.c = Poset([v])
-        CV = [v.c for v in V]
-        # then pass through edges and union associated vertices such that
-        # CV finally holds only connected sets:
-        for e in E:
-            x = e.v[0]
-            y = e.v[1]
-            assert x in V
-            assert y in V
-            assert x.c in CV
-            assert y.c in CV
-            e.attach()
-            if x.c != y.c:
-                # merge y.c into x.c :
-                x.c.update(y.c)
-                # update set list (MUST BE DONE BEFORE UPDATING REFS!)
-                CV.remove(y.c)
-                # update reference:
-                for z in y.c:
-                    z.c = x.c
-        # now create edge sets from connected vertex sets and
-        # make the graph_core connected graphs for this component :
-        self.C = []
-        for c in CV:
-            s = set()
-            for v in c:
-                s.update(v.e)
-            self.C.append(self.component_class(c, s, directed))
-
-    def add_vertex(self, v):
-        for c in self.C:
-            if v in c.sV:
-                return c.sV.get(v)
-        g = self.component_class(directed=self.directed)
-        v = g.add_single_vertex(v)
-        self.C.append(g)
-        return v
-
-    def add_edge(self, e):
-        # take vertices:
-        x = e.v[0]
-        y = e.v[1]
-        x = self.add_vertex(x)
-        y = self.add_vertex(y)
-        # take respective graph_cores:
-        cx = x.c
-        cy = y.c
-        # add edge:
-        e = cy.add_edge(e)
-        # connect (union) the graphs:
-        if cx != cy:
-            cx.union_update(cy)
-            self.C.remove(cy)
-        return e
-
-    def get_vertices_count(self):
-        return sum([c.order() for c in self.C])
-
-    def V(self):
-        for c in self.C:
-            V = c.sV
-            for v in V:
-                yield v
-
-    def E(self):
-        for c in self.C:
-            E = c.sE
-            for e in E:
-                yield e
-
-    def remove_edge(self, e):
-        # get the graph_core:
-        c = e.v[0].c
-        assert c == e.v[1].c
-        if not c in self.C:
-            return None
-        # remove edge in graph_core and replace it with two new cores
-        # if removing edge disconnects the graph_core:
-        try:
-            e = c.remove_edge(e)
-        except ValueError:
-            e = c.sE.remove(e)
-            e.detach()
-            self.C.remove(c)
-            tmpg = type(self)(c.sV, c.sE, self.directed)
-            assert len(tmpg.C) == 2
-            self.C.extend(tmpg.C)
-        return e
-
-    def remove_vertex(self, x):
-        # get the graph_core:
-        c = x.c
-        if not c in self.C:
-            return None
-        try:
-            x = c.remove_vertex(x)
-            if c.order() == 0:
-                self.C.remove(c)
-        except ValueError:
-            for e in x.detach():
-                c.sE.remove(e)
-            x = c.sV.remove(x)
-            self.C.remove(c)
-            tmpg = type(self)(c.sV, c.sE, self.directed)
-            assert len(tmpg.C) == 2
-            self.C.extend(tmpg.C)
-        return x
-
-    def order(self):
-        return sum([c.order() for c in self.C])
-
-    def norm(self):
-        return sum([c.norm() for c in self.C])
-
-    def deg_min(self):
-        return min([c.deg_min() for c in self.C])
-
-    def deg_max(self):
-        return max([c.deg_max() for c in self.C])
-
-    def deg_avg(self):
-        t = 0.0
-        for c in self.C:
-            t += sum([v.deg() for v in c.sV])
-        return t / float(self.order())
-
-    def eps(self):
-        return float(self.norm()) / self.order()
-
-    def path(self, x, y, f_io=0, hook=None):
-        if x == y:
-            return []
-        if x.c != y.c:
-            return None
-        # path:
-        return x.c.path(x, y, f_io, hook)
-
-    def N(self, v, f_io=0):
-        return v.N(f_io)
-
-    def __contains__(self, G):
-        r = False
-        for c in self.C:
-            r |= G in c
-        return r
-
-    def connected(self):
-        return len(self.C) == 1
-
-    # returns connectivity (kappa)
-    def connectivity(self):
-        raise NotImplementedError
-
-    # returns edge-connectivity (lambda)
-    def e_connectivity(self):
-        raise NotImplementedError
-
-    # returns the list of graphs components
-    def components(self):
-        return self.C
-
-    # derivated graphs:
-    # -----------------
-
-    # returns subgraph spanned by vertices V
-    def spans(self, V):
-        raise NotImplementedError
-
-    # returns join of G (if disjoint)
-    def __mul__(self, G):
-        raise NotImplementedError
-
-    # returns complement of a graph G
-    def complement(self, G):
-        raise NotImplementedError
-
-    # contraction G\e
-    def contract(self, e):
-        raise NotImplementedError

+ 0 - 1083
grandalf/layouts.py

@@ -1,1083 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""
-.. _layouts:
-
-layouts.py
-==========
-Layouts are classes that provide graph drawing algorithms.
-
-These classes all take a :class:`graph_core` argument. The graph
-topology will never be permanently modified by the drawing algorithm:
-e.g. "dummy" node insertion, edge reversal for making the graph
-acyclic and so on, are all kept inside the layout object.
-"""
-
-# This code is part of Grandalf
-# Copyright (C) 2010-2012 Axel Tillequin (bdcht3@gmail.com)
-# published under the GPLv2 license or EPLv1 license
-
-import importlib
-from bisect import bisect
-from sys import getrecursionlimit, setrecursionlimit
-
-from mantis.grandalf.utils import *
-
-# ------------------------------------------------------------------------------
-
-
-class VertexViewer(object):
-    """
-    The VertexViewer class is used as the default provider of
-    Vertex dimensions (w,h) and position (xy).
-    In most cases it should be replaced by *view* instances associated
-    with a ui widgets library, allowing to get dimensions and 
-    set position directly on the widget.
-    """
-
-    def __init__(self, w=2, h=2, data=None):
-        self.w = w
-        self.h = h
-        self.data = data
-        self.xy = None
-
-    def __str__(self, *args, **kwargs):
-        return "VertexViewer (xy: %s) w: %s h: %s" % (self.xy, self.w, self.h)
-
-
-# ------------------------------------------------------------------------------
-
-
-class _sugiyama_vertex_attr(object):
-    """
-    The sugiyama layout adds new attributes to vertices.
-    These attributes are stored in an internal _sugimyama_vertex_attr object.
-
-    Attributes:
-        rank (int): the rank number is the index of the layer that
-                    contains this vertex.
-        dummy (0/1): a flag indicating if the vertex is *dummy*
-        pos (int): the index of the vertex in the layer
-        x (list(float)): the list of computed horizontal coordinates of the vertex
-        bar (float): the current *barycenter* of the vertex 
-    """
-
-    def __init__(self, r=None, d=0):
-        self.rank = r
-        self.dummy = d
-        self.pos = None
-        self.x = 0
-        self.bar = None
-
-    def __str__(self):
-        s = "(%3d,%3d) x=%s" % (self.rank, self.pos, str(self.x))
-        if self.dummy:
-            s = "[d] %s" % s
-        return s
-
-    # def __eq__(self,x):
-    #    return self.bar == x.bar
-    # def __ne__(self,x):
-    #    return self.bar != x.bar
-    # def __lt__(self,x):
-    #    return self.bar < x.bar
-    # def __le__(self,x):
-    #    return self.bar <= x.bar
-    # def __gt__(self,x):
-    #    return self.bar > x.bar
-    # def __ge__(self,x):
-    #    return self.bar >= x.bar
-
-
-# ------------------------------------------------------------------------------
-
-
-class DummyVertex(_sugiyama_vertex_attr):
-    """
-    The DummyVertex class is used by the sugiyama layout to represent
-    *long* edges, i.e. edges that span over several ranks.
-    For these edges, a DummyVertex is inserted in every inner layer.
-
-    Attributes:
-        view (viewclass): since a DummyVertex is acting as a Vertex, it
-                          must have a view.
-        ctrl (list[_sugiyama_attr]): the list of associated dummy vertices
-
-    Methods:
-        N(dir): reflect the Vertex method and returns the list of adjacent
-                 vertices (possibly dummy) in the given direction.
-        inner(dir): return True if a neighbor in the given direction is *dummy*.
-    """
-
-    def __init__(self, r=None, viewclass=VertexViewer):
-        self.view = viewclass()
-        self.ctrl = None
-        super().__init__(r, d=1)
-
-    def N(self, dir):
-        assert dir == +1 or dir == -1
-        v = self.ctrl.get(self.rank + dir, None)
-        return [v] if v is not None else []
-
-    def inner(self, dir):
-        assert dir == +1 or dir == -1
-        try:
-            return any([x.dummy == 1 for x in self.N(dir)])
-        except KeyError:
-            return False
-        except AttributeError:
-            return False
-
-    def __str__(self):
-        s = "(%3d,%3d) x=%s" % (self.rank, self.pos, str(self.x))
-        if self.dummy:
-            s = "[d] %s" % s
-        return s
-
-
-# ------------------------------------------------------------------------------
-
-
-class Layer(list):
-    """
-    Layer is where Sugiyama layout organises vertices in hierarchical lists.
-    The placement of a vertex is done by the Sugiyama class, but it highly relies on
-    the *ordering* of vertices in each layer to reduce crossings.
-    This ordering depends on the neighbors found in the upper or lower layers.
-
-    Attributes:
-        layout (SugiyamaLayout): a reference to the sugiyama layout instance that
-                                 contains this layer
-        upper (Layer): a reference to the *upper* layer (rank-1)
-        lower (Layer): a reference to the *lower* layer (rank+1)
-        ccount (int) : number of crossings detected in this layer
-
-    Methods:
-        setup (layout): set initial attributes values from provided layout
-        nextlayer(): returns *next* layer in the current layout's direction parameter.
-        prevlayer(): returns *previous* layer in the current layout's direction parameter.
-        order(): compute *optimal* ordering of vertices within the layer.
-    """
-
-    __r = None
-    layout = None
-    upper = None
-    lower = None
-    __x = 1.0
-    ccount = None
-
-    def __eq__(self, other):
-        return super().__eq__(other)
-
-    def __str__(self):
-        s = "<Layer %d" % self.__r
-        s += ", len=%d" % len(self)
-        xc = self.ccount or "?"
-        s += ", crossings=%s>" % xc
-        return s
-
-    def setup(self, layout):
-        self.layout = layout
-        r = layout.layers.index(self)
-        self.__r = r
-        if len(self) > 1:
-            self.__x = 1.0 / (len(self) - 1)
-        for i, v in enumerate(self):
-            assert layout.grx[v].rank == r
-            layout.grx[v].pos = i
-            layout.grx[v].bar = i * self.__x
-        if r > 0:
-            self.upper = layout.layers[r - 1]
-        if r < len(layout.layers) - 1:
-            self.lower = layout.layers[r + 1]
-
-    def nextlayer(self):
-        return self.lower if self.layout.dirv == -1 else self.upper
-
-    def prevlayer(self):
-        return self.lower if self.layout.dirv == +1 else self.upper
-
-    def order(self):
-        sug = self.layout
-        sug._edge_inverter()
-        c = self._cc()
-        if c > 0:
-            for v in self:
-                sug.grx[v].bar = self._meanvalueattr(v)
-            # now resort layers l according to bar value:
-            self.sort(key=lambda x: sug.grx[x].bar)
-            # reduce & count crossings:
-            c = self._ordering_reduce_crossings()
-            # assign new position in layer l:
-            for i, v in enumerate(self):
-                sug.grx[v].pos = i
-                sug.grx[v].bar = i * self.__x
-        sug._edge_inverter()
-        self.ccount = c
-        return c
-
-    def _meanvalueattr(self, v):
-        """
-        find new position of vertex v according to adjacency in prevlayer.
-        position is given by the mean value of adjacent positions.
-        experiments show that meanvalue heuristic performs better than median.
-        """
-        sug = self.layout
-        if not self.prevlayer():
-            return sug.grx[v].bar
-        bars = [sug.grx[x].bar for x in self._neighbors(v)]
-        return sug.grx[v].bar if len(bars) == 0 else float(sum(bars)) / len(bars)
-
-    def _medianindex(self, v):
-        """
-        find new position of vertex v according to adjacency in layer l+dir.
-        position is given by the median value of adjacent positions.
-        median heuristic is proven to achieve at most 3 times the minimum
-        of crossings (while barycenter achieve in theory the order of |V|)
-        """
-        assert self.prevlayer() != None
-        N = self._neighbors(v)
-        g = self.layout.grx
-        pos = [g[x].pos for x in N]
-        lp = len(pos)
-        if lp == 0:
-            return []
-        pos.sort()
-        pos = pos[:: self.layout.dirh]
-        i, j = divmod(lp - 1, 2)
-        return [pos[i]] if j == 0 else [pos[i], pos[i + j]]
-
-    def _neighbors(self, v):
-        """
-        neighbors refer to upper/lower adjacent nodes.
-        Note that v.N() provides neighbors of v in the graph, while
-        this method provides the Vertex and DummyVertex adjacent to v in the
-        upper or lower layer (depending on layout.dirv state).
-        """
-        assert self.layout.dag
-        dirv = self.layout.dirv
-        grxv = self.layout.grx[v]
-        try:  # (cache)
-            return grxv.nvs[dirv]
-        except AttributeError:
-            grxv.nvs = {-1: v.N(-1), +1: v.N(+1)}
-            if grxv.dummy:
-                return grxv.nvs[dirv]
-            # v is real, v.N are graph neigbors but we need layers neighbors
-            for d in (-1, +1):
-                tr = grxv.rank + d
-                for i, x in enumerate(v.N(d)):
-                    if self.layout.grx[x].rank == tr:
-                        continue
-                    e = v.e_with(x)
-                    dum = self.layout.ctrls[e][tr]
-                    grxv.nvs[d][i] = dum
-            return grxv.nvs[dirv]
-
-    def _crossings(self):
-        """
-        counts (inefficently but at least accurately) the number of
-        crossing edges between layer l and l+dirv.
-        P[i][j] counts the number of crossings from j-th edge of vertex i.
-        The total count of crossings is the sum of flattened P:
-        x = sum(sum(P,[]))
-        """
-        g = self.layout.grx
-        P = []
-        for v in self:
-            P.append([g[x].pos for x in self._neighbors(v)])
-        for i, p in enumerate(P):
-            candidates = sum(P[i + 1 :], [])
-            for j, e in enumerate(p):
-                p[j] = len(filter((lambda nx: nx < e), candidates))
-            del candidates
-        return P
-
-    def _cc(self):
-        """
-        implementation of the efficient bilayer cross counting by insert-sort
-        (see Barth & Mutzel paper "Simple and Efficient Bilayer Cross Counting")
-        """
-        g = self.layout.grx
-        P = []
-        for v in self:
-            P.extend(sorted([g[x].pos for x in self._neighbors(v)]))
-        # count inversions in P:
-        s = []
-        count = 0
-        for i, p in enumerate(P):
-            j = bisect(s, p)
-            if j < i:
-                count += i - j
-            s.insert(j, p)
-        return count
-
-    def _ordering_reduce_crossings(self):
-        assert self.layout.dag
-        g = self.layout.grx
-        N = len(self)
-        X = 0
-        for i, j in zip(range(N - 1), range(1, N)):
-            vi = self[i]
-            vj = self[j]
-            ni = [g[v].bar for v in self._neighbors(vi)]
-            Xij = Xji = 0
-            for nj in [g[v].bar for v in self._neighbors(vj)]:
-                x = len([nx for nx in ni if nx > nj])
-                Xij += x
-                Xji += len(ni) - x
-            if Xji < Xij:
-                self[i] = vj
-                self[j] = vi
-                X += Xji
-            else:
-                X += Xij
-        return X
-
-
-# ------------------------------------------------------------------------------
-
-
-class SugiyamaLayout(object):
-    """
-    The Sugiyama layout is the traditional "layered" graph layout called
-    *dot* in graphviz. This layout is quite efficient but heavily relies 
-    on drawing heuristics. Adaptive drawing is limited to
-    extending the leaves only, but since the algorithm is quite fast
-    redrawing the entire graph (up to about a thousand nodes) gives
-    usually good results in less than a second.
-
-    The Sugiyama Layout Class takes as input a core_graph object and implements
-    an efficient drawing algorithm based on nodes dimensions provided through
-    a user-defined *view* property in each vertex.
-
-    Attributes:
-        dirvh (int): the current aligment state
-                     for alignment policy:
-                     dirvh=0 -> dirh=+1, dirv=-1: leftmost upper
-                     dirvh=1 -> dirh=-1, dirv=-1: rightmost upper
-                     dirvh=2 -> dirh=+1, dirv=+1: leftmost lower
-                     dirvh=3 -> dirh=-1, dirv=+1: rightmost lower
-        order_inter (int): the default number of layer placement iterations
-        order_attr (str): set attribute name used for layer ordering
-        xspace (int): horizontal space between vertices in a layer
-        yspace (int): vertical space between layers
-        dw (int): default width of a vertex
-        dh (int): default height of a vertex
-        g (graph_core): the graph component reference
-        layers (list[Layer]): the list of layers
-        grx (dict): associate vertex (possibly dummy) with their sugiyama attributes
-        ctrls (dict): associate edge with all its vertices (including dummies)
-        dag (bool): the current acyclic state 
-        initdone (bool): True if state is initialized (see init_all).
-    """
-
-    def __init__(self, g):
-        from mantis.grandalf.utils.geometry import median_wh
-
-        # drawing parameters:
-        self.dirvh = 0
-        self.order_iter = 8
-        self.order_attr = "pos"
-        self.xspace = 20
-        self.yspace = 20
-        self.dw = 10
-        self.dh = 10
-        # For layered graphs, vertices and edges need to have some additional
-        # attributes that make sense only for this kind of layout:
-        # update graph struct:
-        self.g = g
-        self.layers = []
-        self.grx = {}
-        self.ctrls = {}
-        self.dag = False
-        for v in self.g.V():
-            assert hasattr(v, "view")
-            self.grx[v] = _sugiyama_vertex_attr()
-        self.dw, self.dh = median_wh([v.view for v in self.g.V()])
-        self.initdone = False
-
-    def init_all(self, roots=None, inverted_edges=None, optimize=False):
-        """initializes the layout algorithm by computing roots (unless provided),
-           inverted edges (unless provided), vertices ranks and creates all dummy
-           vertices and layers. 
-             
-             Parameters:
-                roots (list[Vertex]): set *root* vertices (layer 0)
-                inverted_edges (list[Edge]): set edges to invert to have a DAG.
-                optimize (bool): optimize ranking if True (default False)
-        """
-        if self.initdone:
-            return
-        # For layered sugiyama algorithm, the input graph must be acyclic,
-        # so we must provide a list of root nodes and a list of inverted edges.
-        if roots is None:
-            roots = [v for v in self.g.sV if len(v.e_in()) == 0]
-        if inverted_edges is None:
-            _ = self.g.get_scs_with_feedback(roots)
-            inverted_edges = [x for x in self.g.sE if x.feedback]
-        self.alt_e = inverted_edges
-        # assign rank to all vertices:
-        self.rank_all(roots, optimize)
-        # add dummy vertex/edge for 'long' edges:
-        for e in self.g.E():
-            self.setdummies(e)
-        # precompute some layers values:
-        for l in self.layers:
-            l.setup(self)
-        self.initdone = True
-
-    def draw(self, N=1.5):
-        """compute every node coordinates after converging to optimal ordering by N
-           rounds, and finally perform the edge routing.
-        """
-        while N > 0.5:
-            for (l, mvmt) in self.ordering_step():
-                pass
-            N = N - 1
-        if N > 0:
-            for (l, mvmt) in self.ordering_step(oneway=True):
-                pass
-        self.setxy()
-        self.draw_edges()
-
-    def _edge_inverter(self):
-        for e in self.alt_e:
-            x, y = e.v
-            e.v = (y, x)
-        self.dag = not self.dag
-        if self.dag:
-            for e in self.g.degenerated_edges:
-                e.detach()
-                self.g.sE.remove(e)
-        else:
-            for e in self.g.degenerated_edges:
-                self.g.add_edge(e)
-
-    @property
-    def dirvh(self):
-        return self.__dirvh
-
-    @property
-    def dirv(self):
-        return self.__dirv
-
-    @property
-    def dirh(self):
-        return self.__dirh
-
-    @dirvh.setter
-    def dirvh(self, dirvh):
-        assert dirvh in range(4)
-        self.__dirvh = dirvh
-        self.__dirh, self.__dirv = {0: (1, -1),
-                                    1: (-1, -1),
-                                    2: (1, 1),
-                                    3: (-1, 1)}[dirvh]
-
-    @dirv.setter
-    def dirv(self, dirv):
-        assert dirv in (-1, +1)
-        dirvh = (dirv + 1) + (1 - self.__dirh) // 2
-        self.dirvh = dirvh
-
-    @dirh.setter
-    def dirh(self, dirh):
-        assert dirh in (-1, +1)
-        dirvh = (self.__dirv + 1) + (1 - dirh) // 2
-        self.dirvh = dirvh
-
-    def rank_all(self, roots, optimize=False):
-        """Computes rank of all vertices.
-        add provided roots to rank 0 vertices,
-        otherwise update ranking from provided roots.
-        The initial rank is based on precedence relationships,
-        optimal ranking may be derived from network flow (simplex).
-        """
-        self._edge_inverter()
-        r = [x for x in self.g.sV if (len(x.e_in()) == 0 and x not in roots)]
-        self._rank_init(roots + r)
-        if optimize:
-            self._rank_optimize()
-        self._edge_inverter()
-
-    def _rank_init(self, unranked):
-        """Computes rank of provided unranked list of vertices and all
-           their children. A vertex will be asign a rank when all its 
-           inward edges have been *scanned*. When a vertex is asigned
-           a rank, its outward edges are marked *scanned*.
-        """
-        assert self.dag
-        scan = {}
-        # set rank of unranked based on its in-edges vertices ranks:
-        while len(unranked) > 0:
-            l = []
-            for v in unranked:
-                self.setrank(v)
-                # mark out-edges has scan-able:
-                for e in v.e_out():
-                    scan[e] = True
-                # check if out-vertices are rank-able:
-                for x in v.N(+1):
-                    if not (False in [scan.get(e, False) for e in x.e_in()]):
-                        if x not in l:
-                            l.append(x)
-            unranked = l
-
-    def _rank_optimize(self):
-        """optimize ranking by pushing long edges toward lower layers as much as possible.
-        see other interersting network flow solver to minimize total edge length
-        (http://jgaa.info/accepted/2005/EiglspergerSiebenhallerKaufmann2005.9.3.pdf)
-        """
-        assert self.dag
-        for l in reversed(self.layers):
-            for v in l:
-                gv = self.grx[v]
-                for x in v.N(-1):
-                    if all((self.grx[y].rank >= gv.rank for y in x.N(+1))):
-                        gx = self.grx[x]
-                        self.layers[gx.rank].remove(x)
-                        gx.rank = gv.rank - 1
-                        self.layers[gv.rank - 1].append(x)
-
-    def setrank(self, v):
-        """set rank value for vertex v and add it to the corresponding layer.
-           The Layer is created if it is the first vertex with this rank.
-        """
-        assert self.dag
-        r = max([self.grx[x].rank for x in v.N(-1)] + [-1]) + 1
-        self.grx[v].rank = r
-        # add it to its layer:
-        try:
-            self.layers[r].append(v)
-        except IndexError:
-            assert r == len(self.layers)
-            self.layers.append(Layer([v]))
-
-    def dummyctrl(self, r, ctrl):
-        """creates a DummyVertex at rank r inserted in the ctrl dict
-           of the associated edge and layer.
-
-           Arguments:
-              r (int): rank value
-              ctrl (dict): the edge's control vertices
-           
-           Returns:
-              DummyVertex : the created DummyVertex.
-        """
-        dv = DummyVertex(r)
-        dv.view.w, dv.view.h = self.dw, self.dh
-        self.grx[dv] = dv
-        dv.ctrl = ctrl
-        ctrl[r] = dv
-        self.layers[r].append(dv)
-        return dv
-
-    def setdummies(self, e):
-        """creates and defines all needed dummy vertices for edge e.
-        """
-        v0, v1 = e.v
-        r0, r1 = self.grx[v0].rank, self.grx[v1].rank
-        if r0 > r1:
-            assert e in self.alt_e
-            v0, v1 = v1, v0
-            r0, r1 = r1, r0
-        if (r1 - r0) > 1:
-            # "dummy vertices" are stored in the edge ctrl dict,
-            # keyed by their rank in layers.
-            ctrl = self.ctrls[e] = {}
-            ctrl[r0] = v0
-            ctrl[r1] = v1
-            for r in range(r0 + 1, r1):
-                self.dummyctrl(r, ctrl)
-
-    def draw_step(self):
-        """iterator that computes all vertices coordinates and edge routing after
-           just one step (one layer after the other from top to bottom to top).
-           Purely inefficient ! Use it only for "animation" or debugging purpose.
-        """
-        ostep = self.ordering_step()
-        for s in ostep:
-            self.setxy()
-            self.draw_edges()
-            yield s
-
-    def ordering_step(self, oneway=False):
-        """iterator that computes all vertices ordering in their layers
-           (one layer after the other from top to bottom, to top again unless
-           oneway is True).
-        """
-        self.dirv = -1
-        crossings = 0
-        for l in self.layers:
-            mvmt = l.order()
-            crossings += mvmt
-            yield (l, mvmt)
-        if oneway or (crossings == 0):
-            return
-        self.dirv = +1
-        while l:
-            mvmt = l.order()
-            yield (l, mvmt)
-            l = l.nextlayer()
-
-    def setxy(self):
-        """computes all vertex coordinates (x,y) using
-        an algorithm by Brandes & Kopf.
-        """
-        self._edge_inverter()
-        self._detect_alignment_conflicts()
-        inf = float("infinity")
-        # initialize vertex coordinates attributes:
-        for l in self.layers:
-            for v in l:
-                self.grx[v].root = v
-                self.grx[v].align = v
-                self.grx[v].sink = v
-                self.grx[v].shift = inf
-                self.grx[v].X = None
-                self.grx[v].x = [0.0] * 4
-        curvh = self.dirvh  # save current dirvh value
-        for dirvh in range(4):
-            self.dirvh = dirvh
-            self._coord_vertical_alignment()
-            self._coord_horizontal_compact()
-        self.dirvh = curvh  # restore it
-        # vertical coordinate assigment of all nodes:
-        Y = 0
-        for l in self.layers:
-            dY = max([v.view.h / 2.0 for v in l])
-            for v in l:
-                vx = sorted(self.grx[v].x)
-                # mean of the 2 medians out of the 4 x-coord computed above:
-                avgm = (vx[1] + vx[2]) / 2.0
-                # final xy-coordinates :
-                v.view.xy = (avgm, Y + dY)
-            Y += 2 * dY + self.yspace
-        self._edge_inverter()
-
-    def _detect_alignment_conflicts(self):
-        """mark conflicts between edges:
-        inner edges are edges between dummy nodes
-        type 0 is regular crossing regular (or sharing vertex)
-        type 1 is inner crossing regular (targeted crossings)
-        type 2 is inner crossing inner (avoided by reduce_crossings phase)
-        """
-        curvh = self.dirvh  # save current dirvh value
-        self.dirvh = 0
-        self.conflicts = []
-        for L in self.layers:
-            last = len(L) - 1
-            prev = L.prevlayer()
-            if not prev:
-                continue
-            k0 = 0
-            k1_init = len(prev) - 1
-            l = 0
-            for l1, v in enumerate(L):
-                if not self.grx[v].dummy:
-                    continue
-                if l1 == last or v.inner(-1):
-                    k1 = k1_init
-                    if v.inner(-1):
-                        k1 = self.grx[v.N(-1)[-1]].pos
-                    for vl in L[l : l1 + 1]:
-                        for vk in L._neighbors(vl):
-                            k = self.grx[vk].pos
-                            if k < k0 or k > k1:
-                                self.conflicts.append((vk, vl))
-                    l = l1 + 1
-                    k0 = k1
-        self.dirvh = curvh  # restore it
-
-    def _coord_vertical_alignment(self):
-        """performs vertical alignment according to current dirvh internal state.
-        """
-        dirh, dirv = self.dirh, self.dirv
-        g = self.grx
-        for l in self.layers[::-dirv]:
-            if not l.prevlayer():
-                continue
-            r = None
-            for vk in l[::dirh]:
-                for m in l._medianindex(vk):
-                    # take the median node in dirv layer:
-                    um = l.prevlayer()[m]
-                    # if vk is "free" align it with um's root
-                    if g[vk].align is vk:
-                        if dirv == 1:
-                            vpair = (vk, um)
-                        else:
-                            vpair = (um, vk)
-                        # if vk<->um link is used for alignment
-                        if (vpair not in self.conflicts) and (
-                            (r is None) or (dirh * r < dirh * m)
-                        ):
-                            g[um].align = vk
-                            g[vk].root = g[um].root
-                            g[vk].align = g[vk].root
-                            r = m
-
-    def _coord_horizontal_compact(self):
-        limit = getrecursionlimit()
-        N = len(self.layers) + 10
-        if N > limit:
-            setrecursionlimit(N)
-        dirh, dirv = self.dirh, self.dirv
-        g = self.grx
-        L = self.layers[::-dirv]
-        # recursive placement of blocks:
-        for l in L:
-            for v in l[::dirh]:
-                if g[v].root is v:
-                    self.__place_block(v)
-        setrecursionlimit(limit)
-        # mirror all nodes if right-aligned:
-        if dirh == -1:
-            for l in L:
-                for v in l:
-                    x = g[v].X
-                    if x:
-                        g[v].X = -x
-        # then assign x-coord of its root:
-        inf = float("infinity")
-        rb = inf
-        for l in L:
-            for v in l[::dirh]:
-                g[v].x[self.dirvh] = g[g[v].root].X
-                rs = g[g[v].root].sink
-                s = g[rs].shift
-                if s < inf:
-                    g[v].x[self.dirvh] += dirh * s
-                rb = min(rb, g[v].x[self.dirvh])
-        # normalize to 0, and reinit root/align/sink/shift/X
-        for l in self.layers:
-            for v in l:
-                # g[v].x[dirvh] -= rb
-                g[v].root = g[v].align = g[v].sink = v
-                g[v].shift = inf
-                g[v].X = None
-
-    # TODO: rewrite in iterative form to avoid recursion limit...
-    def __place_block(self, v):
-        g = self.grx
-        if g[v].X is None:
-            # every block is initially placed at x=0
-            g[v].X = 0.0
-            # place block in which v belongs:
-            w = v
-            while 1:
-                j = g[w].pos - self.dirh  # predecessor in rank must be placed
-                r = g[w].rank
-                if 0 <= j < len(self.layers[r]):
-                    wprec = self.layers[r][j]
-                    delta = (
-                        self.xspace + (wprec.view.w + w.view.w) / 2.0
-                    )  # abs positive minimum displ.
-                    # take root and place block:
-                    u = g[wprec].root
-                    self.__place_block(u)
-                    # set sink as sink of prec-block root
-                    if g[v].sink is v:
-                        g[v].sink = g[u].sink
-                    if g[v].sink != g[u].sink:
-                        s = g[u].sink
-                        newshift = g[v].X - (g[u].X + delta)
-                        g[s].shift = min(g[s].shift, newshift)
-                    else:
-                        g[v].X = max(g[v].X, (g[u].X + delta))
-                # take next node to align in block:
-                w = g[w].align
-                # quit if self aligned
-                if w is v:
-                    break
-
-    def draw_edges(self):
-        """Basic edge routing applied only for edges with dummy points.
-        Enhanced edge routing can be performed by using the apropriate
-        *route_with_xxx* functions from :ref:routing_ in the edges' view.
-        """
-        for e in self.g.E():
-            if hasattr(e, "view"):
-                l = []
-                if e in self.ctrls:
-                    D = self.ctrls[e]
-                    r0, r1 = self.grx[e.v[0]].rank, self.grx[e.v[1]].rank
-                    if r0 < r1:
-                        ranks = range(r0 + 1, r1)
-                    else:
-                        ranks = range(r0 - 1, r1, -1)
-                    l = [D[r].view.xy for r in ranks]
-                l.insert(0, e.v[0].view.xy)
-                l.append(e.v[1].view.xy)
-                try:
-                    self.route_edge(e, l)
-                except AttributeError:
-                    pass
-                e.view.setpath(l)
-
-
-#  DIRECTED GRAPH WITH CONSTRAINTS LAYOUT
-# ------------------------------------------------------------------------------
-
-
-class DigcoLayout(object):
-    linalg = importlib.import_module("mantis.grandalf.utils.geometry")
-
-    def __init__(self, g):
-        # drawing parameters:
-        self.xspace = 10
-        self.yspace = 10
-        self.dr = 10
-        self.debug = False
-
-        self.g = g
-        self.levels = []
-        for i, v in enumerate(self.g.V()):
-            assert hasattr(v, "view")
-            v.i = i
-            self.dr = max((self.dr, v.view.w, v.view.h))
-        # solver parameters:
-        self._cg_max_iter = g.order()
-        self._cg_tolerance = 1.0e-6
-        self._eps = 1.0e-5
-        self._cv_max_iter = self._cg_max_iter
-
-    def init_all(self, alpha=0.1, beta=0.01):
-        y = None
-        if self.g.directed:
-            # partition g in hierarchical levels:
-            y = self.part_to_levels(alpha, beta)
-        # initiate positions (y and random in x):
-        self.Z = self._xyinit(y)
-
-    def draw(self, N=None):
-        if N is None:
-            N = self._cv_max_iter
-        self.Z = self._optimize(self.Z, limit=N)
-        # set view xy from near-optimal coords matrix:
-        for v in self.g.V():
-            v.view.xy = (self.Z[v.i][0, 0] * self.dr, self.Z[v.i][0, 1] * self.dr)
-        self.draw_edges()
-
-    def draw_step(self):
-        for x in range(self._cv_max_iter):
-            self.draw(N=1)
-            self.draw_edges()
-            yield
-
-    # Basic edge routing with segments
-    def draw_edges(self):
-        for e in self.g.E():
-            if hasattr(e, "view"):
-                l = [e.v[0].view.xy, e.v[1].view.xy]
-                try:
-                    self.route_edge(e, l)
-                except AttributeError:
-                    pass
-                e.view.setpath(l)
-
-    # partition the nodes into levels:
-    def part_to_levels(self, alpha, beta):
-        opty, err = self.optimal_arrangement()
-        ordering = list(zip(opty, self.g.sV))
-        eps = alpha * (opty.max() - opty.min()) / (len(opty) - 1)
-        eps = max(beta, eps)
-        sorted(ordering, reverse=True)
-        l = []
-        self.levels.append(l)
-        for i in range(len(list(ordering)) - 1):
-            y, v = ordering[i]
-            l.append(v)
-            v.level = self.levels.index(l)
-            if (y - ordering[i + 1][0]) > eps:
-                l = []
-                self.levels.append(l)
-        y, v = ordering[-1]
-        l.append(v)
-        v.level = self.levels.index(l)
-        return opty
-
-    def optimal_arrangement(self):
-        b = self.balance()
-        y = DigcoLayout.linalg.rand_ortho1(self.g.order())
-        return self._conjugate_gradient_L(y, b)
-
-    # balance vector is assembled in finite-element way...
-    # this is faster than computing b[i] for each i.
-    def balance(self):
-        b = DigcoLayout.linalg.array([0.0] * self.g.order(), dtype=float)
-        for e in self.g.E():
-            s = e.v[0]
-            d = e.v[1]
-            q = e.w * (self.yspace + (s.view.h + d.view.h) / 2.0)
-            b[s.i] += q
-            b[d.i] -= q
-        return b
-
-    # We compute the solution Y of L.Y = b by conjugate gradient method
-    # (L is semi-definite positive so Y is unique and convergence is O(n))
-    # note that only arrays are involved here...
-    def _conjugate_gradient_L(self, y, b):
-        Lii = self.__Lii_()
-        r = b - self.__L_pk(Lii, y)
-        p = DigcoLayout.linalg.array(r, copy=True)
-        rr = sum(r * r)
-        for k in range(self._cg_max_iter):
-            try:
-                Lp = self.__L_pk(Lii, p)
-                alpha = rr / sum(p * Lp)
-                y += alpha / p
-                r -= alpha * Lp
-                newrr = sum(r * r)
-                beta = newrr / rr
-                rr = newrr
-                if rr < self._cg_tolerance:
-                    break
-                p = r + beta * p
-            except ZeroDivisionError:
-                return (None, rr)
-        return (y, rr)
-
-    # _xyinit can use diagonally scaled initial vertices positioning to provide
-    # better convergence in constrained stress majorization
-    def _xyinit(self, y=None):
-        if y is None:
-            y = DigcoLayout.linalg.rand_ortho1(self.g.order())
-        x = DigcoLayout.linalg.rand_ortho1(self.g.order())
-        # translate and normalize:
-        x = x - x[0]
-        y = y - y[0]
-        sfactor = 1.0 / max(list(map(abs, y)) + list(map(abs, x)))
-        return DigcoLayout.linalg.matrix(list(zip(x * sfactor, y * sfactor)))
-
-    # provide the diagonal of the Laplacian matrix of g
-    # the rest of L (sparse!) is already stored in every edges.
-    def __Lii_(self):
-        Lii = []
-        for v in self.g.V():
-            Lii.append(sum([e.w for e in v.e]))
-        return DigcoLayout.linalg.array(Lii, dtype=float)
-
-    # we don't compute the L.Pk matrix/vector product here since
-    # L is sparse (order of |E| not |V|^2 !) so we let each edge
-    # contribute to the resulting L.Pk vector in a FE assembly way...
-    def __L_pk(self, Lii, pk):
-        y = Lii * pk
-        for e in self.g.sE:
-            i1 = e.v[0].i
-            i2 = e.v[1].i
-            y[i1] -= e.w * pk[i2]
-            y[i2] -= e.w * pk[i1]
-        return y
-
-    # conjugate_gradient with given matrix Lw:
-    # it is assumed that b is not a multivector,
-    # so _cg_Lw should be called in all directions separately.
-    # note that everything is a matrix here, (arrays are row vectors only)
-    def _cg_Lw(self, Lw, z, b):
-        scal = lambda U, V: float(U.transpose() * V)
-        r = b - Lw * z
-        p = r.copy()
-        rr = scal(r, r)
-        for k in range(self._cg_max_iter):
-            if rr < self._cg_tolerance:
-                break
-            Lp = Lw * p
-            alpha = rr / scal(p, Lp)
-            z = z + alpha * p
-            r = r - alpha * Lp
-            newrr = scal(r, r)
-            beta = newrr / rr
-            rr = newrr
-            p = r + beta * p
-        return (z, rr)
-
-    def __Dij_(self):
-        Dji = []
-        for v in self.g.V():
-            wd = self.g.dijkstra(v)
-            Di = [wd[w] for w in self.g.V()]
-            Dji.append(Di)
-        # at this point  D is stored by rows,
-        # but anymway it's a symmetric matrix
-        return DigcoLayout.linalg.matrix(Dji, dtype=float)
-
-    # returns matrix -L^w
-    def __Lij_w_(self):
-        self.Dij = self.__Dij_()  # we keep D also for L^Z computations
-        Lij = self.Dij.copy()
-        n = self.g.order()
-        for i in range(n):
-            d = 0
-            for j in range(n):
-                if j == i:
-                    continue
-                Lij[i, j] = 1.0 / self.Dij[i, j] ** 2
-                d += Lij[i, j]
-            Lij[i, i] = -d
-        return Lij
-
-    # returns vector -L^Z.Z:
-    def __Lij_Z_Z(self, Z):
-        n = self.g.order()
-        # init:
-        lzz = Z.copy() * 0.0  # lzz has dim Z (n x 2)
-        liz = DigcoLayout.linalg.matrix([0.0] * n)  # liz is a row of L^Z (size n)
-        # compute lzz = L^Z.Z while assembling L^Z by row (liz):
-        for i in range(n):
-            iterk_except_i = (k for k in range(n) if k != i)
-            for k in iterk_except_i:
-                v = Z[i] - Z[k]
-                liz[0, k] = 1.0 / (
-                    self.Dij[i, k] * DigcoLayout.linalg.sqrt(v * v.transpose())
-                )
-            liz[0, i] = 0.0  # forced, otherwise next liz.sum() is wrong !
-            liz[0, i] = -liz.sum()
-            # now that we have the i-th row of L^Z, just dotprod with Z:
-            lzz[i] = liz * Z
-        return lzz
-
-    def _optimize(self, Z, limit=100):
-        Lw = self.__Lij_w_()
-        K = self.g.order() * (self.g.order() - 1.0) / 2.0
-        stress = float("inf")
-        count = 0
-        deep = 0
-        b = self.__Lij_Z_Z(Z)
-        while count < limit:
-            if self.debug:
-                print("count %d" % count)
-                print("Z = ", Z)
-                print("b = ", b)
-            # find next Z by solving Lw.Z = b in every direction:
-            x, xerr = self._cg_Lw(Lw[1:, 1:], Z[1:, 0], b[1:, 0])
-            y, yerr = self._cg_Lw(Lw[1:, 1:], Z[1:, 1], b[1:, 1])
-            Z[1:, 0] = x
-            Z[1:, 1] = y
-            if self.debug:
-                print(" cg -> ")
-                print(Z, xerr, yerr)
-            # compute new stress:
-            FZ = K - float(x.transpose() * b[1:, 0] + y.transpose() * b[1:, 1])
-            # precompute new b:
-            b = self.__Lij_Z_Z(Z)
-            # update new stress:
-            FZ += 2 * float(x.transpose() * b[1:, 0] + y.transpose() * b[1:, 1])
-            # test convergence:
-            print("stress=%.10f" % FZ)
-            if stress == 0.0:
-                break
-            elif abs((stress - FZ) / stress) < self._eps:
-                if deep == 2:
-                    break
-                else:
-                    deep += 1
-            stress = FZ
-            count += 1
-        return Z
-
-
-# ------------------------------------------------------------------------------
-class DwyerLayout(DigcoLayout):
-    pass

+ 0 - 132
grandalf/routing.py

@@ -1,132 +0,0 @@
-# This code is part of Grandalf
-#  Copyright (C) 2011 Axel Tillequin (bdcht3@gmail.com) and others
-# published under GPLv2 license or EPLv1 license
-# Contributor(s): Axel Tillequin, Fabio Zadrozny
-
-#  Edge routing algorithms.
-#  These are mosty helpers for routing an edge 'e' through
-#  points pts with various tweaks like moving the starting point
-#  to the intersection with the bounding box and taking some constraints
-#  into account, and/or moving the head also to its prefered position.
-#  Of course, since gandalf only works with bounding boxes, the exact
-#  shape of the nodes are not known and the edge drawing inside the bb
-#  shall be performed by the drawing engine associated with 'views'.
-#  (e.g. look at intersectC when the node shape is a circle)
-
-from mantis.grandalf.utils.geometry import intersectR, getangle, sqrt
-
-# ------------------------------------------------------------------------------
-class EdgeViewer(object):
-    def setpath(self, pts):
-        self._pts = pts
-
-
-# ------------------------------------------------------------------------------
-#  basic edge routing with lines : nothing to do for routing
-#  since the layout engine has already provided to list of points through which
-#  the edge shall be drawn. We just compute the position where to adjust the
-#  tail and head.
-def route_with_lines(e, pts):
-    assert hasattr(e, "view")
-    tail_pos = intersectR(e.v[0].view, topt=pts[1])
-    head_pos = intersectR(e.v[1].view, topt=pts[-2])
-    pts[0] = tail_pos
-    pts[-1] = head_pos
-    e.view.head_angle = getangle(pts[-2], pts[-1])
-
-
-# ------------------------------------------------------------------------------
-#  enhanced edge routing where 'corners' of the above polyline route are
-#  rounded with a bezier curve.
-def route_with_splines(e, pts):
-    from mantis.grandalf.utils.geometry import setroundcorner
-
-    route_with_lines(e, pts)
-    splines = setroundcorner(e, pts)
-    e.view.splines = splines
-
-
-def _gen_point(p1, p2, new_distance):
-    from mantis.grandalf.utils.geometry import new_point_at_distance
-
-    initial_distance = distance = sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2)
-    if initial_distance < 1e-10:
-        return None
-    if distance > new_distance:
-        distance = distance - new_distance
-    else:
-        return None
-    angle = getangle(p1, p2)
-    new = new_point_at_distance(p1, distance, angle)
-    return new
-
-
-def _gen_smoother_middle_points_from_3_points(pts, initial):
-    p1 = pts[0]
-    p2 = pts[1]
-    p3 = pts[2]
-    distance1 = sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2)
-    distance2 = sqrt((p3[0] - p1[0]) ** 2 + (p3[1] - p1[1]) ** 2)
-    if distance1 < 1e-10 or distance2 < 1e-10:
-        yield p2
-    else:
-        if distance1 < initial or distance2 < initial:
-            yield p2
-        else:
-            p2a = _gen_point(p1, p2, initial)
-            p2b = _gen_point(p3, p2, initial)
-            if p2a is None or p2b is None:
-                yield p2
-            else:
-                yield p2a
-                yield p2b
-
-
-# Future work: possibly work better when we already have 4 points?
-# maybe: http://stackoverflow.com/questions/1251438/catmull-rom-splines-in-python
-def _round_corners(pts, round_at_distance):
-    if len(pts) > 2:
-        calc_with_distance = round_at_distance
-        while calc_with_distance > 0.5:
-            new_lst = [pts[0]]
-            for i, curr in enumerate(pts[1:-1]):
-                i += 1
-                p1 = pts[i - 1]
-                p2 = curr
-                p3 = pts[i + 1]
-                if len(pts) > 3:
-                    # i.e.: at least 4 points
-                    if sqrt((p3[0] - p2[0]) ** 2 + (p3[1] - p2[1]) ** 2) < (
-                        2 * calc_with_distance
-                    ):
-                        # prevent from crossing over.
-                        new_lst.append(p2)
-                        continue
-                generated = _gen_smoother_middle_points_from_3_points(
-                    [p1, p2, p3], calc_with_distance
-                )
-                for j in generated:
-                    new_lst.append(j)
-            new_lst.append(pts[-1])
-            pts = new_lst
-            calc_with_distance /= 2.0
-    return pts
-
-
-# ------------------------------------------------------------------------------
-# Routing with a custom algorithm to round corners
-# It works by generating new points up to a distance from where an edge is
-# found (and then iteratively refining based on that).
-# This is a custom implementation as this interpolation method worked
-# well for me where others weren't so great.
-
-# This is the point where it'll start rounding from an edge.
-# (can be changed to decide up to which distance it starts
-# rounding from an edge).
-ROUND_AT_DISTANCE = 40
-
-
-def route_with_rounded_corners(e, pts):
-    route_with_lines(e, pts)
-    new_pts = _round_corners(pts, round_at_distance=ROUND_AT_DISTANCE)
-    pts[:] = new_pts[:]

+ 0 - 3
grandalf/utils/__init__.py

@@ -1,3 +0,0 @@
-from .poset import *
-from .dot import *
-from .nx import *

+ 0 - 401
grandalf/utils/dot.py

@@ -1,401 +0,0 @@
-# This code is part of Grandalf
-# Copyright (C) 2008 Axel Tillequin (bdcht3@gmail.com) and others
-# published under GPLv2 license or EPLv1 license
-# Contributor(s): Axel Tillequin
-
-try:
-    import ply.lex as lex
-    import ply.yacc as yacc
-
-    _has_ply = True
-except ImportError:
-    _has_ply = False
-
-__all__ = ["_has_ply", "Dot"]
-
-# ------------------------------------------------------------------------------
-# LALR(1) parser for Graphviz dot file format.
-class Dot:
-
-    _reserved = (
-        "strict",
-        "graph",
-        "digraph",
-        "subgraph",
-        "node",
-        "edge",
-    )
-    _tokens = ("regulars", "string", "html", "comment",) + _reserved
-
-    _literals = [",", ";", "-", ">", "=", ":", "[", "]", "{", "}"]
-
-    class Lexer(object):
-        def __init__(self):
-            self.whitespace = "\0\t\n\f\r "
-            self.reserved = Dot._reserved
-            self.tokens = Dot._tokens
-            self.literals = Dot._literals
-            self.t_ignore = self.whitespace
-
-        def t_regulars(self, t):
-            r"[-]?[\w.]+"
-            v = t.value.lower()
-            if v in self.reserved:
-                t.type = v
-                return t
-            # check numeric string
-            if v[0].isdigit() or v[0] in ["-", "."]:
-                try:
-                    float(v)
-                except ValueError:
-                    print("invalid numeral token: %s" % v)
-                    raise SyntaxError
-            elif "." in v:  # forbidden in non-numeric
-                raise SyntaxError
-            return t
-
-        def t_comment_online(self, t):
-            r"(//(.*)\n)|\\\n"
-            pass
-
-        def t_comment_macro(self, t):
-            r"(\#(.*)\n)"
-            pass
-
-        def t_comment_multline(self, t):
-            r"(/\*)"
-            start = t.lexer.lexpos
-            t.lexer.lexpos = t.lexer.lexdata.index("*/", start) + 2
-
-        def t_string(self, t):
-            r'"'
-            start = t.lexer.lexpos - 1
-            i = t.lexer.lexdata.index('"', start + 1)
-            while t.lexer.lexdata[i - 1] == "\\":
-                i = t.lexer.lexdata.index('"', i + 1)
-            t.value = t.lexer.lexdata[start : i + 1]
-            t.lexer.lexpos = i + 1
-            return t
-
-        def t_html(self, t):
-            r"<"
-            start = t.lexer.lexpos - 1
-            level = 1
-            i = start + 1
-            while level > 0:
-                c = t.lexer.lexdata[i]
-                if c == "<":
-                    level += 1
-                if c == ">":
-                    level -= 1
-                i += 1
-            t.value = t.lexer.lexdata[start:i]
-            t.lexer.lexpos = i
-            return t
-
-        def t_ANY_error(self, t):
-            print("Illegal character '%s'" % t.value[0])
-            t.lexer.skip(1)
-
-        def build(self, **kargs):
-            if _has_ply:
-                self._lexer = lex.lex(module=self, **kargs)
-
-        def test(self, data):
-            self._lexer.input(data)
-            while 1:
-                tok = self._lexer.token()
-                if not tok:
-                    break
-                print(tok)
-
-    # Classes for the AST returned by Parser:
-    class graph(object):
-        def __init__(self, name, data, strict=None, direct=None):
-            self.name = name
-            self.strict = strict
-            self.direct = direct
-            self.nodes = {}
-            self.edges = []
-            self.subgraphs = []
-            self.attr = {}
-            eattr = {}
-            nattr = {}
-            for x in data:  # data is a statements (list of stmt)
-                # x is a stmt, ie one of:
-                # a graph object (subgraph)
-                # a attr object (graph/node/edge attributes)
-                # a dict object (ID=ID)
-                # a node object
-                # a list of edges
-                if isinstance(x, Dot.graph):
-                    self.subgraphs.append(x)
-                elif isinstance(x, Dot.attr):
-                    if x.type == "graph":
-                        self.attr.update(x.D)
-                    elif x.type == "node":
-                        nattr.update(x.D)
-                    elif x.type == "edge":
-                        eattr.update(x.D)
-                    else:
-                        raise TypeError("invalid attribute type")
-                elif isinstance(x, dict):
-                    self.attr.update(x)
-                elif isinstance(x, Dot.node):
-                    x.attr.update(nattr)
-                    self.nodes[x.name] = x
-                else:
-                    for e in x:
-                        e.attr.update(eattr)
-                        self.edges.append(e)
-                        for n in [e.n1, e.n2]:
-                            if isinstance(n, Dot.graph):
-                                continue
-                            if n.name not in self.nodes:
-                                n.attr.update(nattr)
-                                self.nodes[n.name] = n
-
-        def __repr__(self):
-            u = "<%s instance at %x, name: %s, %d nodes>" % (
-                self.__class__,
-                id(self),
-                self.name,
-                len(self.nodes),
-            )
-            return u
-
-    class attr(object):
-        def __init__(self, type, D):
-            self.type = type
-            self.D = D
-
-    class edge(object):
-        def __init__(self, n1, n2):
-            self.n1 = n1
-            self.n2 = n2
-            self.attr = {}
-
-    class node(object):
-        def __init__(self, name, port=None):
-            self.name = name
-            self.port = port
-            self.attr = {}
-
-    class Parser(object):
-        def __init__(self):
-            self.tokens = Dot._tokens
-
-        def __makelist(self, p):
-            N = len(p)
-            if N > 2:
-                L = p[1]
-                L.append(p[N - 1])
-            else:
-                L = []
-                if N > 1:
-                    L.append(p[N - 1])
-            p[0] = L
-
-        def p_Data(self, p):
-            """Data : Data Graph
-                    | Graph"""
-            self.__makelist(p)
-
-        def p_Graph_strict(self, p):
-            """Graph : strict graph name Block"""
-            p[0] = Dot.graph(name=p[3], data=p[4], strict=1, direct=0)
-            # print 'Dot.Parser: graph object %s created'%p[0].name
-
-        def p_Graph_graph(self, p):
-            """Graph : graph name Block"""
-            p[0] = Dot.graph(name=p[2], data=p[3], strict=0, direct=0)
-
-        def p_Graph_strict_digraph(self, p):
-            """Graph : strict digraph name Block"""
-            p[0] = Dot.graph(name=p[3], data=p[4], strict=1, direct=1)
-
-        def p_Graph_digraph(self, p):
-            """Graph : digraph name Block"""
-            p[0] = Dot.graph(name=p[2], data=p[3], strict=0, direct=1)
-
-        def p_ID(self, p):
-            """ID : regulars
-                  | string
-                  | html """
-            p[0] = p[1]
-
-        def p_name(self, p):
-            """name : ID
-                    | """
-            if len(p) == 1:
-                p[0] = ""
-            else:
-                p[0] = p[1]
-
-        def p_Block(self, p):
-            """Block : '{' statements '}' """
-            p[0] = p[2]
-
-        def p_statements(self, p):
-            """statements : statements stmt
-                          | stmt
-                          | """
-            self.__makelist(p)
-
-        def p_stmt(self, p):
-            """stmt : stmt ';' """
-            p[0] = p[1]
-
-        def p_comment(self, p):
-            """stmt : comment"""
-            pass  # comment tokens are not outputed by lexer anyway
-
-        def p_stmt_sub(self, p):
-            """stmt : sub"""
-            p[0] = p[1]
-
-        def p_subgraph(self, p):
-            """sub : subgraph name Block
-                   | Block """
-            N = len(p)
-            if N > 2:
-                ID = p[2]
-            else:
-                ID = ""
-            p[0] = Dot.graph(name=ID, data=p[N - 1], strict=0, direct=0)
-
-        def p_stmt_assign(self, p):
-            """stmt : affect """
-            p[0] = p[1]
-
-        def p_affect(self, p):
-            """affect : ID '=' ID """
-            p[0] = dict([(p[1], p[3])])
-
-        def p_stmt_lists(self, p):
-            """stmt : graph attrs
-                    | node  attrs
-                    | edge  attrs """
-            p[0] = Dot.attr(p[1], p[2])
-
-        def p_attrs(self, p):
-            """attrs : attrs attrl
-                     | attrl """
-            if len(p) == 3:
-                p[1].update(p[2])
-            p[0] = p[1]
-
-        def p_attrl(self, p):
-            """attrl : '[' alist ']' """
-            L = {}
-            for a in p[2]:
-                if isinstance(a, dict):
-                    L.update(a)
-                else:
-                    L[a] = "true"
-            p[0] = L
-
-        def p_alist_comma(self, p):
-            """alist : alist ',' alist """
-            p[1].extend(p[3])
-            p[0] = p[1]
-
-        def p_alist_affect(self, p):
-            """alist : alist affect
-                     | alist ID
-                     | affect
-                     | ID
-                     | """
-            self.__makelist(p)
-
-        def p_stmt_E_attrs(self, p):
-            """stmt : E attrs """
-            for e in p[1]:
-                e.attr = p[2]
-            p[0] = p[1]
-
-        def p_stmt_N_attrs(self, p):
-            """stmt : N attrs """
-            p[1].attr = p[2]
-            p[0] = p[1]
-
-        def p_stmt_EN(self, p):
-            """stmt : E
-                    | N """
-            p[0] = p[1]
-
-        def p_E(self, p):
-            """E : E   link
-                 | elt link """
-            try:
-                L = p[1]
-                L.append(Dot.edge(L[-1].n2, p[2]))
-            except Exception:
-                L = []
-                L.append(Dot.edge(p[1], p[2]))
-            p[0] = L
-
-        def p_elt(self, p):
-            """elt : N
-                   | sub """
-            p[0] = p[1]
-
-        def p_link(self, p):
-            """link : '-' '>' elt
-                    | '-' '-' elt """
-            p[0] = p[3]
-
-        def p_N_port(self, p):
-            """N : ID port """
-            p[0] = Dot.node(p[1], port=p[2])
-
-        def p_N(self, p):
-            """N : ID """
-            p[0] = Dot.node(p[1])
-
-        def p_port(self, p):
-            """port : ':' ID """
-            p[0] = p[2]
-
-        def p_port2(self, p):
-            """port : port port"""
-            assert p[2] in ["n", "ne", "e", "se", "s", "sw", "w", "nw", "c", "_"]
-            p[0] = "%s:%s" % (p[1], p[2])
-
-        def p_error(self, p):
-            print("Syntax Error: %s" % (p,))
-            self._parser.restart()
-
-        def build(self, **kargs):
-            opt = dict(debug=0, write_tables=0)
-            opt.update(**kargs)
-            if _has_ply:
-                self._parser = yacc.yacc(module=self, **opt)
-
-    def __init__(self, **kargs):
-        self.lexer = Dot.Lexer()
-        self.parser = Dot.Parser()
-        if not _has_ply:
-            print("warning: Dot parser not supported (install python-ply)")
-
-    def parse(self, data):
-        try:
-            self.parser._parser.restart()
-        except AttributeError:
-            self.lexer.build(reflags=lex.re.UNICODE)
-            self.parser.build()
-        except Exception:
-            print("unexpected error")
-            return None
-        try:
-            s = data.decode("utf-8")
-        except UnicodeDecodeError:
-            s = data
-        L = self.parser._parser.parse(s, lexer=self.lexer._lexer)
-        return L
-
-    def read(self, filename):
-        f = open(
-            filename, "rb"
-        )  # As it'll try to decode later on with utf-8, read it binary at this point.
-        return self.parse(f.read())

+ 0 - 214
grandalf/utils/geometry.py

@@ -1,214 +0,0 @@
-#!/usr/bin/env python
-#
-# This code is part of Grandalf
-# Copyright (C) 2008 Axel Tillequin (bdcht3@gmail.com) and others
-# published under GPLv2 license or EPLv1 license
-# Contributor(s): Axel Tillequin, Fabio Zadrozny
-from .poset import *
-from .dot import *
-
-from math import atan2, sqrt
-from random import SystemRandom
-
-# ------------------------------------------------------------------------------
-def intersect2lines(xy1, xy2, xy3, xy4):
-    (x1, y1) = xy1
-    (x2, y2) = xy2
-    (x3, y3) = xy3
-    (x4, y4) = xy4
-    b = (x2 - x1, y2 - y1)
-    d = (x4 - x3, y4 - y3)
-    det = b[0] * d[1] - b[1] * d[0]
-    if det == 0:
-        return None
-    c = (x3 - x1, y3 - y1)
-    t = float(c[0] * b[1] - c[1] * b[0]) / (det * 1.0)
-    if t < 0.0 or t > 1.0:
-        return None
-    t = float(c[0] * d[1] - c[1] * d[0]) / (det * 1.0)
-    if t < 0.0 or t > 1.0:
-        return None
-    x = x1 + t * b[0]
-    y = y1 + t * b[1]
-    return (x, y)
-
-
-# ------------------------------------------------------------------------------
-#  intersectR returns the intersection point between the Rectangle
-#  (w,h) that characterize the view object and the line that goes
-#  from the views' object center to the 'topt' point.
-def intersectR(view, topt):
-    # we compute intersection in local views' coord:
-    # center of view is obviously :
-    x1, y1 = 0, 0
-    # endpoint in view's coord:
-    x2, y2 = topt[0] - view.xy[0], topt[1] - view.xy[1]
-    # bounding box:
-    bbx2 = view.w // 2
-    bbx1 = -bbx2
-    bby2 = view.h // 2
-    bby1 = -bby2
-    # all 4 segments of the bb:
-    S = [
-        ((x1, y1), (x2, y2), (bbx1, bby1), (bbx2, bby1)),
-        ((x1, y1), (x2, y2), (bbx2, bby1), (bbx2, bby2)),
-        ((x1, y1), (x2, y2), (bbx1, bby2), (bbx2, bby2)),
-        ((x1, y1), (x2, y2), (bbx1, bby2), (bbx1, bby1)),
-    ]
-    # check intersection with each seg:
-    for segs in S:
-        xy = intersect2lines(*segs)
-        if xy != None:
-            x, y = xy
-            # return global coord:
-            x += view.xy[0]
-            y += view.xy[1]
-            return (x, y)
-    # there can't be no intersection unless the endpoint was
-    # inside the bb !
-    raise ValueError(
-        "no intersection found (point inside ?!). view: %s topt: %s" % (view, topt)
-    )
-
-
-# ------------------------------------------------------------------------------
-def getangle(p1, p2):
-    x1, y1 = p1
-    x2, y2 = p2
-    theta = atan2(y2 - y1, x2 - x1)
-    return theta
-
-
-# ------------------------------------------------------------------------------
-def median_wh(views):
-    mw = [v.w for v in views]
-    mh = [v.h for v in views]
-    mw.sort()
-    mh.sort()
-    return (mw[len(mw) // 2], mh[len(mh) // 2])
-
-
-# ------------------------------------------------------------------------------
-try:
-    from numpy import array, matrix, cos, sin
-
-    has_numpy = True
-except ImportError:
-    has_numpy = False
-    from math import cos, sin, pi
-    from .linalg import array, matrix
-
-#  rand_ortho1 returns a numpy.array representing
-#  a random normalized n-dimension vector orthogonal to (1,1,1,...,1).
-def rand_ortho1(n):
-    r = SystemRandom()
-    pos = [r.random() for x in range(n)]
-    s = sum(pos)
-    v = array(pos, dtype=float) - (s / float(n))
-    norm = sqrt(sum(v * v))
-    return v / norm
-
-
-# ------------------------------------------------------------------------------
-#  intersectC returns the intersection point between the Circle
-#  of radius r and centered on views' position with the line
-#  to the 'topt' point.
-def intersectC(view, r, topt):
-    theta = getangle(view.xy, topt)
-    x = int(cos(theta) * r)
-    y = int(sin(theta) * r)
-    return (x, y)
-
-
-# ------------------------------------------------------------------------------
-#  setcurve returns the spline curve that path through the list of points P.
-#  The spline curve is a list of cubic bezier curves (nurbs) that have
-#  matching tangents at their extreme points.
-#  The method considered here is taken from "The NURBS book" (Les A. Piegl,
-#  Wayne Tiller, Springer, 1997) and implements a local interpolation rather
-#  than a global interpolation.
-def setcurve(e, pts, tgs=None):
-    P = list(map(array, pts))
-    n = len(P)
-    # tangent estimation
-    if tgs:
-        assert len(tgs) == n
-        T = list(map(array, tgs))
-        Q = [P[k + 1] - P[k] for k in range(0, n - 1)]
-    else:
-        Q, T = tangents(P, n)
-    splines = []
-    for k in range(n - 1):
-        t = T[k] + T[k + 1]
-        a = 16.0 - (t.dot(t))
-        b = 12.0 * (Q[k].dot(t))
-        c = -36.0 * Q[k].dot(Q[k])
-        D = (b * b) - 4.0 * a * c
-        assert D >= 0
-        sd = sqrt(D)
-        s1, s2 = (-b - sd) / (2.0 * a), (-b + sd) / (2.0 * a)
-        s = s2
-        if s1 >= 0:
-            s = s1
-        C0 = tuple(P[k])
-        C1 = tuple(P[k] + (s / 3.0) * T[k])
-        C2 = tuple(P[k + 1] - (s / 3.0) * T[k + 1])
-        C3 = tuple(P[k + 1])
-        splines.append([C0, C1, C2, C3])
-    return splines
-
-
-# ------------------------------------------------------------------------------
-def tangents(P, n):
-    assert n >= 2
-    Q = []
-    T = []
-    for k in range(0, n - 1):
-        q = P[k + 1] - P[k]
-        t = q / sqrt(q.dot(q))
-        Q.append(q)
-        T.append(t)
-    T.append(t)
-    return (Q, T)
-
-
-# ------------------------------------------------------------------------------
-def setroundcorner(e, pts):
-    P = list(map(array, pts))
-    n = len(P)
-    Q, T = tangents(P, n)
-    c0 = P[0]
-    t0 = T[0]
-    k0 = 0
-    splines = []
-    k = 1
-    while k < n:
-        z = abs(t0[0] * T[k][1] - (t0[1] * T[k][0]))
-        if z < 1.0e-6:
-            k += 1
-            continue
-        if (k - 1) > k0:
-            splines.append([c0, P[k - 1]])
-        if (k + 1) < n:
-            splines.extend(setcurve(e, [P[k - 1], P[k + 1]], tgs=[T[k - 1], T[k + 1]]))
-        else:
-            splines.extend(setcurve(e, [P[k - 1], P[k]], tgs=[T[k - 1], T[k]]))
-            break
-        if (k + 2) < n:
-            c0 = P[k + 1]
-            t0 = T[k + 1]
-            k0 = k + 1
-            k += 2
-        else:
-            break
-    return splines or [[P[0], P[-1]]]
-
-
-# ------------------------------------------------------------------------------
-def new_point_at_distance(pt, distance, angle):
-    # angle in radians
-    distance = float(distance)
-    x, y = pt[0], pt[1]
-    x += distance * cos(angle)
-    y += distance * sin(angle)
-    return x, y

+ 0 - 319
grandalf/utils/linalg.py

@@ -1,319 +0,0 @@
-from math import sqrt
-from array import array as _array
-
-constants = (int, float)
-
-
-def coerce_(types):
-    if types is None:
-        types = []
-    if str in types:
-        raise TypeError
-    if complex in types:
-        raise TypeError
-    dtype = ("i", int)
-    if float in types:
-        dtype = ("d", float)
-    return dtype
-
-
-def _mkslice(i, n):
-    if not isinstance(i, slice):
-        i = slice(i, i + 1, 1)
-    start, stop, stride = i.indices(n)
-    return slice(start, stop, stride)
-
-
-def make_ij_slices(f):
-    def wrapper(self, ij, *args):
-        # I = slice(0,self.n,1)
-        J = slice(0, self.p, 1)
-        if isinstance(ij, tuple):
-            I = _mkslice(ij[0], self.n)
-            J = _mkslice(ij[1], self.p)
-        else:
-            I = _mkslice(ij, self.n)
-        return f(self, (I, J), *args)
-
-    return wrapper
-
-
-# minimalistic numpy.array replacement class used as fallback
-# when numpy is not found in geometry module
-class array(object):
-    def __init__(self, data, dtype=None, copy=True):
-        self.dim = len(data)
-        types = None
-        if self.dim > 0:
-            types = set([type(x) for x in data])
-        if dtype is not None:
-            types = (dtype,)
-        tc, self.dtype = coerce_(types)
-        data = [self.dtype(x) for x in data]
-        if copy is True:
-            self.data = _array(tc, data)
-        else:
-            raise NotImplementedError
-
-    def coerce(self, dtype):
-        data = [dtype(x) for x in self.data]
-        tc, dtype = coerce_((dtype,))
-        self.data = _array(tc, data)
-        self.dtype = dtype
-
-    @property
-    def typecode(self):
-        return self.data.typecode
-
-    def __len__(self):
-        return self.dim
-
-    def __str__(self):
-        s = " ".join(("%.12s" % x).ljust(12) for x in self)
-        return "[%s]" % s.strip()
-
-    def copy(self):
-        return array(self.data, self.dtype)
-
-    def __add__(self, v):
-        if isinstance(v, constants):
-            v = array([v] * self.dim)
-        assert v.dim == self.dim
-        return array([x + y for (x, y) in zip(self.data, v.data)])
-
-    def __sub__(self, v):
-        if isinstance(v, constants):
-            v = array([v] * self.dim)
-        assert v.dim == self.dim
-        return array([x - y for (x, y) in zip(self.data, v.data)])
-
-    def __neg__(self):
-        return array([-x for x in self.data], dtype=self.dtype)
-
-    def __radd__(self, v):
-        return self + v
-
-    def __rsub__(self, v):
-        return (-self) + v
-
-    def dot(self, v):
-        assert v.dim == self.dim
-        return sum([x * y for (x, y) in zip(self.data, v.data)])
-
-    def __rmul__(self, k):
-        return array([k * x for x in self.data])
-
-    def __mul__(self, v):
-        if isinstance(v, constants):
-            v = array([v] * self.dim)
-        assert v.dim == self.dim
-        return array([x * y for (x, y) in zip(self.data, v.data)])
-
-    def __truediv__(self, v):
-        if isinstance(v, constants):
-            v = array([v] * self.dim)
-        assert v.dim == self.dim
-        return array([x / y for (x, y) in zip(self.data, v.data)])
-
-    __div__ = __truediv__
-
-    def __rtruediv__(self, v):
-        if isinstance(v, constants):
-            v = array([v] * self.dim)
-        assert v.dim == self.dim
-        return array([x / y for (x, y) in zip(v.data, self.data)])
-
-    __rdiv__ = __rtruediv__
-
-    def __floordiv__(self, v):
-        if isinstance(v, constants):
-            v = array([v] * self.dim)
-        assert v.dim == self.dim
-        return array([x // y for (x, y) in zip(self.data, v.data)])
-
-    def __rfloordiv__(self, v):
-        if isinstance(v, constants):
-            v = array([v] * self.dim)
-        assert v.dim == self.dim
-        return array([x // y for (x, y) in zip(v.data, self.data)])
-
-    def norm(self):
-        return sqrt(self.dot(self))
-
-    def max(self):
-        return max(self.data)
-
-    def min(self):
-        return min(self.data)
-
-    def __iter__(self):
-        for x in self.data:
-            yield x
-
-    def __setitem__(self, i, v):
-        assert isinstance(i, int)
-        self.data[i] = self.dtype(v)
-
-    def __getitem__(self, i):
-        i = _mkslice(i, self.dim)
-        res = self.data[i]
-        if len(res) == 1:
-            return res[0]
-        return array(res)
-
-    def transpose(self):
-        return matrix(self.data, self.dtype)
-
-    def __float__(self):
-        assert self.dim == 1
-        return float(self.data[0])
-
-
-# ------------------------------------------------------------------------------
-# minimalistic numpy.matrix replacement class used as fallback
-# when numpy is not found in geometry module
-class matrix(object):
-    def __init__(self, data, dtype=None, copy=True, transpose=False):
-        # check input data types:
-        types = set([type(v) for v in data])
-        if len(types) > 1:
-            raise TypeError
-        t = types.pop()
-        # import data:
-        if t in constants:
-            self.data = [array(data, dtype, copy)]
-        else:
-            if transpose:
-                data = zip(*data)
-            self.data = [array(v, dtype, copy) for v in data]
-        # define matrix sizes:
-        self.n = len(self.data)
-        sizes = set([len(v) for v in self.data])
-        if len(sizes) > 1:
-            raise ValueError
-        self.p = sizes.pop()
-        if dtype is None:
-            # coerce types of arrays of matrix:
-            types = set([v.dtype for v in self.data])
-            tc, dtype = coerce_(types)
-            for v in self.data:
-                v.coerce(dtype)
-        self.dtype = dtype
-
-    def __len__(self):
-        return self.n * self.p
-
-    def __str__(self):
-        s = "\n ".join([str(v) for v in self.data])
-        return "[%s]" % s.strip()
-
-    @property
-    def shape(self):
-        return (self.n, self.p)
-
-    def lvecs(self):
-        return self.data
-
-    def cvecs(self):
-        return [array(v, self.dtype) for v in zip(*self.data)]
-
-    def copy(self):
-        return matrix(self.data, self.dtype)
-
-    def transpose(self):
-        return matrix(self.data, dtype=self.dtype, transpose=True)
-
-    def sum(self):
-        return sum([sum(v) for v in self.data])
-
-    @make_ij_slices
-    def __getitem__(self, ij):
-        I, J = ij
-        l = self.lvecs()[I]
-        m = matrix([v[J] for v in l])
-        if m.n == 1:
-            v = m.data[0]
-            if v.dim == 1:
-                return v[0]
-            if len(l) > 1:
-                return v
-        return m
-
-    @make_ij_slices
-    def __setitem__(self, ij, v):
-        I, J = ij
-        Ri = range(I.start, I.stop, I.step)
-        Rj = range(J.start, J.stop, J.step)
-        if type(v) in constants:
-            v = (v,)
-        value = (x for x in v)
-        for i in Ri:
-            for j in Rj:
-                self.data[i][j] = next(value)
-
-    def __add__(self, m):
-        if isinstance(m, constants):
-            return matrix([u + m for u in self.data])
-        else:
-            assert self.shape == m.shape
-            return matrix([u + v for (u, v) in zip(self.data, m.data)])
-
-    def __sub__(self, m):
-        if isinstance(m, constants):
-            return matrix([u - m for u in self.data])
-        else:
-            assert self.shape == m.shape
-            return matrix([u - v for (u, v) in zip(self.data, m.data)])
-
-    def __neg__(self):
-        return matrix([-x for x in self.data], dtype=self.dtype)
-
-    def __float__(self):
-        assert self.n == 1 and self.p == 1
-        return self[0, 0]
-
-    def __radd__(self, v):
-        return self + v
-
-    def __rsub__(self, v):
-        return (-self) + v
-
-    def __rmul__(self, k):
-        if not isinstance(k, constants):
-            raise TypeError
-        return matrix([k * v for v in self.data])
-
-    def __mul__(self, X):
-        if isinstance(X, constants):
-            return X * self
-        if isinstance(X, array):
-            assert X.dim == self.p
-            return array([v.dot(X) for v in self.data])
-        if isinstance(X, matrix):
-            assert X.n == self.p
-            return matrix([self * v for v in X.cvecs()])
-
-    def __pow__(self, v):
-        S = [self] * v
-        assert len(S) > 0
-        return reduce(lambda x, y: x * y, S)
-
-    def __iter__(self):
-        for l in self.data:
-            for v in l:
-                yield v
-
-
-class SimplexMin(object):
-    def __init__(self, A, b, c):
-        self.A = A
-        self.b = b
-        self.c = c
-        self.tableau()
-
-    def tableau(self):
-        self.T = []
-
-    def setup(self):
-        self.enter = []
-        delf.outer = []

+ 0 - 39
grandalf/utils/nx.py

@@ -1,39 +0,0 @@
-# This code is part of Grandalf
-#  Copyright (C) 2014 Axel Tillequin (bdcht3@gmail.com) and others
-# published under GPLv2 license or EPLv1 license
-# Contributor(s): Fabio Zadrozny
-
-__all__ = [
-    "convert_grandalf_graph_to_networkx_graph",
-    "convert_nextworkx_graph_to_grandalf",
-]
-
-# Some utilities to interact with networkx.
-
-# Converts a grandalf graph to a networkx graph.
-# Note that the edge concept is the same, but a vertex in grandalf is called a node in networkx.
-def convert_grandalf_graph_to_networkx_graph(G):
-    from networkx import MultiDiGraph
-
-    nxg = MultiDiGraph()
-    for v in G.V():
-        nxg.add_node(v.data)
-    for e in G.E():
-        nxg.add_edge(e.v[0].data, e.v[1].data)
-    return nxg
-
-
-# Converts a networkx graph to a grandalf graph.
-# Note that the edge concept is the same, but a vertex in grandalf is called a node in networkx.
-def convert_nextworkx_graph_to_grandalf(G):
-    from grandalf.graphs import Graph, Vertex, Edge
-
-    V = []
-    data_to_V = {}
-    for x in G.nodes():
-        vertex = Vertex(x)
-        V.append(vertex)
-        data_to_V[x] = vertex
-    E = [Edge(data_to_V[xy[0]], data_to_V[xy[1]], data=xy) for xy in G.edges()]
-    g = Graph(V, E)
-    return g

+ 0 - 155
grandalf/utils/poset.py

@@ -1,155 +0,0 @@
-# This code is part of Grandalf
-# Copyright (C) 2008-2015 Axel Tillequin (bdcht3@gmail.com) and others
-# published under GPLv2 license or EPLv1 license
-# Contributor(s): Axel Tillequin
-
-from collections import OrderedDict
-
-__all__ = ["Poset"]
-
-# ------------------------------------------------------------------------------
-# Poset class implements a set but allows to interate over the elements in a
-# deterministic way and to get specific objects in the set.
-# Membership operator defaults to comparing __hash__  of objects but Poset
-# allows to check for __cmp__/__eq__ membership by using contains__cmp__(obj)
-class Poset(object):
-    def __init__(self, L):
-        self.o = OrderedDict()
-        for obj in L:
-            self.add(obj)
-
-    def __repr__(self):
-        return "Poset(%r)" % (self.o,)
-
-    def __str__(self):
-        f = "%%%dd" % len(str(len(self.o)))
-        s = []
-        for i, x in enumerate(self.o.values()):
-            s.append(f % i + ".| %s" % repr(x))
-        return "\n".join(s)
-
-    def add(self, obj):
-        if obj in self:
-            return self.get(obj)
-        else:
-            self.o[obj] = obj
-            return obj
-
-    def remove(self, obj):
-        if obj in self:
-            obj = self.get(obj)
-            del self.o[obj]
-            return obj
-        return None
-
-    def index(self, obj):
-        return list(self.o.values()).index(obj)
-
-    def get(self, obj):
-        return self.o.get(obj, None)
-
-    def __getitem__(self, i):
-        return list(self.o.values())[i]
-
-    def __len__(self):
-        return len(self.o)
-
-    def __iter__(self):
-        for obj in iter(self.o.values()):
-            yield obj
-
-    def __cmp__(self, other):
-        s1 = set(other.o.values())
-        s2 = set(self.o.values())
-        return cmp(s1, s2)
-
-    def __eq__(self, other):
-        s1 = set(other.o.values())
-        s2 = set(self.o.values())
-        return s1 == s2
-
-    def __ne__(self, other):
-        s1 = set(other.o.values())
-        s2 = set(self.o.values())
-        return s1 != s2
-
-    def copy(self):
-        return Poset(self.o.values())
-
-    __copy__ = copy
-
-    def deepcopy(self):
-        from copy import deepcopy
-
-        L = deepcopy(self.o.values())
-        return Poset(L)
-
-    def __or__(self, other):
-        return self.union(other)
-
-    def union(self, other):
-        p = Poset([])
-        p.o.update(self.o)
-        p.o.update(other.o)
-        return p
-
-    def update(self, other):
-        self.o.update(other.o)
-
-    def __and__(self, other):
-        s1 = set(self.o.values())
-        s2 = set(other.o.values())
-        return Poset(s1.intersection(s2))
-
-    def intersection(self, *args):
-        p = self
-        for other in args:
-            p = p & other
-        return p
-
-    def __xor__(self, other):
-        s1 = set(self.o.values())
-        s2 = set(other.o.values())
-        return Poset(s1.symmetric_difference(s2))
-
-    def symmetric_difference(self, *args):
-        p = self
-        for other in args:
-            p = p ^ other
-        return p
-
-    def __sub__(self, other):
-        s1 = set(self.o.values())
-        s2 = set(other.o.values())
-        return Poset(s1.difference(s2))
-
-    def difference(self, *args):
-        p = self
-        for other in args:
-            p = p - other
-        return p
-
-    def __contains__(self, obj):
-        return obj in self.o
-
-    def contains__cmp__(self, obj):
-        return obj in self.o.values()
-
-    def issubset(self, other):
-        s1 = set(self.o.values())
-        s2 = set(other.o.values())
-        return s1.issubset(s2)
-
-    def issuperset(self, other):
-        s1 = set(self.o.values())
-        s2 = set(other.o.values())
-        return s1.issuperset(s2)
-
-    __le__ = issubset
-    __ge__ = issuperset
-
-    def __lt__(self, other):
-        return self <= other and len(self) != len(other)
-
-    def __gt__(self, other):
-        return self >= other and len(self) != len(other)

+ 901 - 19
i_o.py

@@ -1,7 +1,30 @@
 # this is the I/O part of mantis. I eventually intend to make this a markup language. not right now tho lol
 
-# https://stackoverflow.com/questions/42033142/is-there-an-easy-way-to-check-if-an-object-is-json-serializable-in-python
-# thanks!
+from .utilities import (prRed, prGreen, prPurple, prWhite,
+                              prOrange,
+                              wrapRed, wrapGreen, wrapPurple, wrapWhite,
+                              wrapOrange,)
+
+from mathutils import  Vector
+
+
+
+
+# this works but it is really ugly and probably quite inneficient
+# TODO: make hotkeys for export and import and reload from file
+   # we need to give the tree a filepath attribute and update it on saving
+   # then we need to use the filepath attribute to load from
+   # finally we need to use a few operators to choose whether to open a menu or not
+   # and we need a message to display on save/load so that the user knows it is happening
+
+# TODO:
+   # Additionally export MetaRig and Curve and other referenced data
+   # Meshes can be exported as .obj and imported via GN
+
+def TellClasses():
+    return [ MantisExportNodeTreeSaveAs, MantisExportNodeTreeSave, MantisExportNodeTree, MantisImportNodeTree, MantisReloadNodeTree]
+
+# https://stackoverflow.com/questions/42033142/is-there-an-easy-way-to-check-if-an-object-is-json-serializable-in-python - thanks!
 def is_jsonable(x):
     import json
     try:
@@ -11,29 +34,888 @@ def is_jsonable(x):
         return False
 
 
+# https://stackoverflow.com/questions/295135/turn-a-stritree-into-a-valid-filename - thank you user "Sophie Gage"
+def remove_special_characters(name):
+    import re; return re.sub('[^\w_.)( -]', '', name)# re = regular expressions
+
+
+def fix_custom_parameter(n, property_definition, ):
+    if n.bl_idname in ['xFormNullNode', 'xFormBoneNode', 'xFormRootNode', 'xFormArmatureNode', 'xFormGeometryObjectNode',]:
+        prop_name = property_definition["name"]
+        prop_type = property_definition["bl_idname"]
+        
+        if prop_type in ['ParameterBoolSocket', 'ParameterIntSocket', 'ParameterFloatSocket', 'ParameterVectorSocket' ]:
+            # is it good to make both of them?
+            input = n.inputs.new( prop_type, prop_name)
+            output = n.outputs.new( prop_type, prop_name)
+            if property_definition["is_output"] == True:
+                return output
+            return input
+    
+    elif n.bl_idname in ['LinkArmature']:
+        prop_name = property_definition["name"]
+        prop_type = property_definition["bl_idname"]
+        input = n.inputs.new( prop_type, prop_name)
+        return input
+
+    return None
+    
+    
+
+
+def export_to_json(trees, path="", write_file=True, only_selected=False):
+    # ignore these because they are either unrelated python stuff or useless or borked
+    prop_ignore = [ "__dict__", "__doc__", "__module__", "__weakref__",# "name",
+                    "bl_height_default", "bl_height_max", "bl_height_min",
+                    "bl_icon", "bl_rna", "bl_static_type", "bl_description",
+                    "bl_width_default", "bl_width_max", "bl_width_min",
+                    "__annotations__", "original", "rna_type", "view_center",
+                    "links", "nodes", "internal_links", "inputs", "outputs",
+                    "__slots__", "dimensions", "type", "interface",
+                    "library_weak_reference", "parsed_tree", "node_tree_updater" ]
+    # don't ignore: "bl_idname", "bl_label",
+    # ignore the name, it's the dict - key for the node props
+     # no that's stupid don't ignore the name good grief
+
+    # I am doing this because these are interactions with other addons that cause problems and probably don't exist for any given user
+    prop_ignore.extend(['keymesh'])
+
+    export_data = {}
+    for tree in trees:
+        base_tree = False
+        if tree is trees[-1]:
+            base_tree = True
+
+        tree_info, tree_in_out = {}, {}
+        for propname  in dir(tree):
+            if (propname in prop_ignore) or ( callable(getattr(tree, propname)) ):
+                continue
+            if not is_jsonable( v := getattr(tree, propname)):
+                raise RuntimeError(f"Not JSON-able: {propname}, type: {type(v)}")
+            tree_info[propname] = v
+        tree_info["name"] = tree.name
+
+        # if only_selected:
+        #     # all in/out links, relative to the selection, should be marked and used to initialize tree properties
+        #     pass
+            
+        
+        for sock in tree.interface.items_tree:
+            sock_data={}
+
+            if sock.item_type == 'PANEL':
+                sock_data["name"] = sock.name
+                sock_data["item_type"] = sock.item_type
+                sock_data["description"] = sock.description
+                sock_data["default_closed"] = sock.default_closed
+                tree_in_out[sock.name] = sock_data
+
+            # if it is a socket....
+            else:
+                sock_parent = None
+                if sock.parent:
+                    sock_parent = sock.parent.name
+                for propname  in dir(sock):
+                    if (propname in prop_ignore) or ( callable(v) ):
+                        continue
+                    if (propname == "parent"):
+                        sock_data[propname] = sock_parent
+                        continue
+                    v = getattr(sock, propname)
+                    if not is_jsonable( v ):
+                        raise RuntimeError(f"{propname}, {type(v)}")
+                    sock_data[propname] = v
+            
+                tree_in_out[sock.identifier] = sock_data
+
+
+        nodes = {}
+        for n in tree.nodes:
+            if only_selected and n.select == False:
+                continue
+            node_props, sockets = {}, {}
+            for propname  in dir(n):
+                v = getattr(n, propname)
+                if propname in ['fake_fcurve_ob']:
+                    v=v.name
+                if (propname in prop_ignore) or ( callable(v) ):
+                    continue
+                if v.__class__.__name__ in ["Vector", "Color"]:
+                    v = tuple(v)
+                if isinstance(v, bpy.types.NodeTree):
+                    v = v.name
+                if isinstance(v, bpy.types.bpy_prop_array):
+                    v = tuple(v)
+                if propname == "parent" and v:
+                    v = v.name
+                if not is_jsonable(v):
+                    raise RuntimeError(f"Could not export...  {n.name}, {propname}, {type(v)}")
+                if v is None:
+                    continue
+
+                node_props[propname] = v
+
+                # so we have to accumulate the parent location because the location is not absolute
+                if propname == "location" and n.parent is not None:
+                    location_acc = Vector((0,0))
+                    parent = n.parent
+                    while (parent):
+                        location_acc += parent.location
+                        parent = parent.parent
+                    location_acc += getattr(n, propname)
+                    node_props[propname] = tuple(location_acc)
+                    # this works!
+
+                    # n.parent = None
+                
+                # if propname == "location":
+                #     print (v, n.location)
+                # if parent:
+                #     n.parent = parent
+            # now we need to get the sockets...
+
+            # WHY IS THIS FUNCTION DEFINED IN THIS SCOPE?
+            def socket_data(s):
+                socket = {}
+                socket["name"] = s.name
+                socket["bl_idname"] = s.bl_idname
+                socket["is_output"] = s.is_output
+                socket["is_multi_input"] = s.is_multi_input
+
+                # if s.bl_idname == 'TransformSpaceSocket':
+                #     prGreen(s.default_value)
+                
+                # here is where we'll handle a socket's special data
+                if s.bl_idname == "EnumMetaBoneSocket":
+                    socket["bone"] = s.bone
+                if s.bl_idname in ["EnumMetaBoneSocket", "EnumMetaRigSocket", "EnumCurveSocket"]:
+                    if sp := s.get("search_prop"): # may be None
+                        socket["search_prop"] = sp.name # this is an object.
+                #
+
+                # v = s.get("default_value") # this doesn't seem to work, see below
+                if hasattr(s, "default_value"):
+                    v = s.default_value
+                else:
+                    v = None
+                v_type = type(v)
+
+                # this identifies a weird bug...
+                # try:
+                #     problem = v != s.default_value
+                #     if problem:
+                #         prRed(s.node.name, s.name, s.bl_idname, s.default_value)
+                #         print(s.default_value, v)
+                # except AttributeError:
+                #     pass
+                # so it seems doing the .get thing bypassed some kind of getter that made the value "nice"
+                # so e.g. enums were using the Int identifier and bool vectors used int bitmasks
 
-def export_to_json(tree, path):
-    # takes a parsed mantis tree and returns a JSON
-    tree_data = tree.parsed_tree
+                # if s.bl_idname == 'TransformSpaceSocket':
+                #     prWhite(v)
+                if v is None:
+                    return socket # we don't need to store this.
+                if not is_jsonable(v):
+                    v = tuple(v)
+                if not is_jsonable(v):
+                    raise RuntimeError(f"Error serializing data in {s.node.name}::{s.name} for value of type {v_type}")
+                socket["default_value"] = v
+                # at this point we can get the custom parameter ui hints if we want
+                if not s.is_output:
+                    # try and get this data
+                    if v := getattr(s,'min', None):
+                        socket["min"] = v
+                    if v := getattr(s,'max', None):
+                        socket["max"] = v
+                    if v := getattr(s,'soft_min', None):
+                        socket["soft_min"] = v
+                    if v := getattr(s,'soft_max', None):
+                        socket["soft_max"] = v
+                    if v := getattr(s,'description', None):
+                        socket["description"] = v
+                # if s.bl_idname == 'TransformSpaceSocket':
+                #     prRed(socket['default_value'])
+                return socket
+                #
 
-    # we have to prepare the tree data.
-    #   SO: we can't use tuples as keys in JSON
-    #   but the tree uses a lot of tuple keys in Python dicts
-    #   so we can map the tuples to UUIDs instead
-    #  then we can store the list of nodes mapped to the same UUIDs
-    # the goal is to store the tree in a way that makes reproduction possible
+            for i, s in enumerate(n.inputs):
+                socket = socket_data(s)
+                socket["index"]=i
+                sockets[s.identifier] = socket
+            for i, s in enumerate(n.outputs):
+                socket = socket_data(s)
+                socket["index"]=i
+                sockets[s.identifier] = socket
+            # for i, s in enumerate(n.inputs):
+            #     sockets[s.identifier]["index"] = i
+            # for i, s in enumerate(n.outputs):
+            #     sockets[s.identifier]["index"] = i
+            
+            node_props["sockets"] = sockets
+            nodes[n.name] = node_props
+            
+        
+        links = []
 
+        in_sockets = {}
+        out_sockets = {}
+        # new_tree_items = {}
 
+        in_node = {"name":"MANTIS_AUTOGEN_GROUP_INPUT", "bl_idname":"NodeGroupInput", "sockets":in_sockets}
+        out_node = {"name":"MANTIS_AUTOGEN_GROUP_OUTPUT", "bl_idname":"NodeGroupOutput", "sockets":out_sockets}
+
+
+        add_input_node, add_output_node = False, False
+        
+#
+# dict_keys(['Driver Variable', 'second', 'main', 'drivers and such', 'Copy Location', 'Float', 'Reroute', 'Reroute.001', 'Reroute.002'])
+# bl_idname, name, label, location, 
+
+# Variable Type {'name': 'Variable Type', 'bl_idname': 'EnumDriverVariableType', 'is_output': False, 'is_multi_input': False, 'default_value': 'SINGLE_PROP', 'index': 0}
+# Property {'name': 'Property', 'bl_idname': 'ParameterStringSocket', 'is_output': False, 'is_multi_input': False, 'default_value': 'slide', 'index': 1}
+# Property Index {'name': 'Property Index', 'bl_idname': 'IntSocket', 'is_output': False, 'is_multi_input': False, 'default_value': 0, 'index': 2}
+# Evaluation Space {'name': 'Evaluation Space', 'bl_idname': 'EnumDriverVariableEvaluationSpace', 'is_output': False, 'is_multi_input': False, 'default_value': 'WORLD_SPACE', 'index': 3}
+# Rotation Mode {'name': 'Rotation Mode', 'bl_idname': 'EnumDriverRotationMode', 'is_output': False, 'is_multi_input': False, 'default_value': 'AUTO', 'index': 4}
+# xForm 1 {'name': 'xForm 1', 'bl_idname': 'xFormSocket', 'is_output': False, 'is_multi_input': False, 'index': 5}
+# xForm 2 {'name': 'xForm 2', 'bl_idname': 'xFormSocket', 'is_output': False, 'is_multi_input': False, 'index': 6}
+# Driver Variable {'name': 'Driver Variable', 'bl_idname': 'DriverVariableSocket', 'is_output': True, 'is_multi_input': False, 'index': 0}
+
+
+# Socket_254
+# index 16
+
+        unique_sockets_from={}
+        unique_sockets_to={}
+
+
+        for l in tree.links:
+            a, b = l.from_node.name, l.from_socket.identifier
+            c, d = l.to_node.name, l.to_socket.identifier
+
+            # get the indices of the sockets to be absolutely sure
+
+            for e, outp in enumerate(l.from_node.outputs):
+                # for some reason, 'is' does not return True no matter what...
+                # so we are gonn compare the memory address directly, this is stupid
+                if (outp.as_pointer() == l.from_socket.as_pointer()): break
+            else:
+                problem=l.from_node.name + "::" + l.from_socket.name
+                raise RuntimeError(wrapRed(f"Error saving index of socket: {problem}"))
+            for f, inp in enumerate(l.to_node.inputs):
+                if (inp.as_pointer() == l.to_socket.as_pointer()): break
+            else:
+                problem = l.to_node.name + "::" + l.to_socket.name
+                raise RuntimeError(wrapRed(f"Error saving index of socket: {problem}"))
+            g, h = l.from_socket.name, l.to_socket.name
+
+            # print (f"{a}:{b} --> {c}:{d})")
+            # this data is good enough
+            if base_tree:
+                if (only_selected and l.from_node.select) and (not l.to_node.select):
+                    # handle an output in the tree
+                    add_output_node=True
+
+
+                    if not (sock_name := unique_sockets_to.get(l.to_socket.node.name+l.to_socket.identifier)):
+                        sock_name = l.to_socket.name; name_stub = sock_name
+                        used_names = list(tree_in_out.keys()); i=0
+                        while sock_name in used_names:
+                            sock_name=name_stub+'.'+str(i).zfill(3); i+=1
+                        unique_sockets_to[l.to_socket.node.name+l.to_socket.identifier]=sock_name
+
+                    out_sock = out_sockets.get(sock_name)
+                    if not out_sock:
+                        out_sock = {}; out_sockets[sock_name] = out_sock
+                        out_sock["index"]=len(out_sockets) # zero indexed, so zero length makes zero the first index and so on, this works
+                    out_sock["name"] = sock_name
+                    out_sock["identifier"] = sock_name
+                    out_sock["bl_idname"] = l.to_socket.bl_idname
+                    out_sock["is_output"] = False
+                    out_sock["source"]=[l.to_socket.node.name,l.to_socket.identifier]
+                    out_sock["is_multi_input"] = False # this is not something I can even set on tree interface items, and this code is not intended for making Schema
+                    sock_data={}
+                    sock_data["name"] = sock_name
+                    sock_data["item_type"] = "SOCKET"
+                    sock_data["default_closed"] = False
+                    sock_data["socket_type"] = l.from_socket.bl_idname
+                    sock_data["identifier"] = sock_name
+                    sock_data["in_out"]="OUTPUT"
+                    sock_data["index"]=out_sock["index"]
+                    tree_in_out[sock_name] = sock_data
+
+                    c=out_node["name"]
+                    d=out_sock["identifier"]
+                    f=out_sock["index"]
+                    h=out_sock["name"]
+
+                elif (only_selected and (not l.from_node.select)) and l.to_node.select:
+                    add_input_node=True
+
+                    # we need to get a unique name for this
+                    # use the Tree IN/Out because we are dealing with Group in/out
+                    if not (sock_name := unique_sockets_from.get(l.from_socket.node.name+l.from_socket.identifier)):
+                        sock_name = l.from_socket.name; name_stub = sock_name
+                        used_names = list(tree_in_out.keys()); i=0
+                        while sock_name in used_names:
+                            sock_name=name_stub+'.'+str(i).zfill(3); i+=1
+                        unique_sockets_from[l.from_socket.node.name+l.from_socket.identifier]=sock_name
+
+                    in_sock = in_sockets.get(sock_name)
+                    if not in_sock:
+                        in_sock = {}; in_sockets[sock_name] = in_sock
+                        in_sock["index"]=len(in_sockets) # zero indexed, so zero length makes zero the first index and so on, this works
+                        #
+                        in_sock["name"] = sock_name
+                        in_sock["identifier"] = sock_name
+                        in_sock["bl_idname"] = l.from_socket.bl_idname
+                        in_sock["is_output"] = True
+                        in_sock["is_multi_input"] = False # this is not something I can even set on tree interface items, and this code is not intended for making Schema
+                        in_sock["source"] = [l.from_socket.node.name,l.from_socket.identifier]
+                        sock_data={}
+                        sock_data["name"] = sock_name
+                        sock_data["item_type"] = "SOCKET"
+                        sock_data["default_closed"] = False
+                        sock_data["socket_type"] = l.from_socket.bl_idname
+                        sock_data["identifier"] = sock_name
+                        sock_data["in_out"]="INPUT"
+                        sock_data["index"]=in_sock["index"]
+
+                        
+                        tree_in_out[sock_name] = sock_data
+
+                    a=in_node.get("name")
+                    b=in_sock["identifier"]
+                    e=in_sock["index"]
+                    g=in_node.get("name")
+                # parentheses matter here...
+                elif (only_selected and not (l.from_node.select and l.to_node.select)):
+                    continue
+            elif only_selected and not (l.from_node.select and l.to_node.select):
+                continue # pass if both links are not selected
+            links.append( (a,b,c,d,e,f,g,h) ) # it's a tuple
+        
+        
+        if add_input_node or add_output_node:
+            all_nodes_bounding_box=[Vector((float("inf"),float("inf"))), Vector((-float("inf"),-float("inf")))]
+            for n in nodes.values():
+                if n["location"][0] < all_nodes_bounding_box[0].x:
+                    all_nodes_bounding_box[0].x = n["location"][0]
+                if n["location"][1] < all_nodes_bounding_box[0].y:
+                    all_nodes_bounding_box[0].y = n["location"][1]
+                #
+                if n["location"][0] > all_nodes_bounding_box[1].x:
+                    all_nodes_bounding_box[1].x = n["location"][0]
+                if n["location"][1] > all_nodes_bounding_box[1].y:
+                    all_nodes_bounding_box[1].y = n["location"][1]
+
+
+        if add_input_node:
+            in_node["location"] = Vector((all_nodes_bounding_box[0].x-400, all_nodes_bounding_box[0].lerp(all_nodes_bounding_box[1], 0.5).y))
+            nodes["MANTIS_AUTOGEN_GROUP_INPUT"]=in_node
+        if add_output_node:
+            out_node["location"] = Vector((all_nodes_bounding_box[1].x+400, all_nodes_bounding_box[0].lerp(all_nodes_bounding_box[1], 0.5).y))
+            nodes["MANTIS_AUTOGEN_GROUP_OUTPUT"]=out_node
+
+        # f_curves = {}
+
+        export_data[tree.name] = (tree_info, tree_in_out, nodes, links,) # f_curves)
     import json
+
+    if not write_file:
+        return export_data # gross to have a different type of return value... but I don't care
+
     with open(path, "w") as file:
-        print("Writing mantis tree data to: ", file.name)
-        file.write( json.dumps(json_data, indent = 4) )
+        print(wrapWhite("Writing mantis tree data to: "), wrapGreen(file.name))
+        file.write( json.dumps(export_data, indent = 4) )
+    
+    # I'm gonna do this in a totally naive way, because this should already be sorted properly
+    #   for the sake of dependency satisfaction. So the current "tree" should be the "main" tree
+    tree.filepath = path
+
+    return {'FINISHED'}
+    
+
+def do_import_from_file(filepath, context):
+    import json
+
+    all_trees = [n_tree for n_tree in bpy.data.node_groups if n_tree.bl_idname in ["MantisTree", "SchemaTree"]]
+
+    for tree in all_trees:
+        tree.do_live_update = False
+
+    with open(filepath, 'r', encoding='utf-8') as f:
+        data = json.load(f)
+        do_import(data,context)
+
+        # repeat this because we left the with, this is bad and ugly but I don't care
+        for tree in all_trees:
+            tree.do_live_update = True
+
+        tree = bpy.data.node_groups[list(data.keys())[-1]]
+        try:
+            context.space_data.node_tree = tree
+        except AttributeError: # not hovering over the Node Editor
+            pass
+        return {'FINISHED'}
+    return {'CANCELLED'}
+
+def do_import(data, context):
+    trees = []
+    
+    for tree_name, tree_data in data.items():
+        print ("Importing sub-graph: %s with %s nodes" % (wrapGreen(tree_name), wrapPurple(len(tree_data[2]))) )
+        # print (tree_data)
+
+        tree_info = tree_data[0]
+        tree_in_out = tree_data[1]
+        nodes = tree_data[2]
+        links = tree_data[3]
+        
+        parent_me = []
+
+        # need to make a new tree:
+        #
+        # first, try to get it:
+        tree = bpy.data.node_groups.get(tree_info["name"])
+        if tree is None:
+            tree = bpy.data.node_groups.new(tree_info["name"], tree_info["bl_idname"])
+        tree.nodes.clear(); tree.links.clear(); tree.interface.clear()
+        # this may be a bad bad thing to do without some kind of warning TODO TODO
+
+        tree.is_executing = True
+        tree.do_live_update = False
+        trees.append(tree)
+
+        tree_sock_id_map={}
+        
+        interface_parent_me = {}
+
+        for s_name, s_props in tree_in_out.items():
+            if s_props["item_type"] == 'SOCKET':
+                if s_props["socket_type"] == "LayerMaskSocket":
+                    continue
+                if (socket_type := s_props["socket_type"]) == "NodeSocketColor":
+                    socket_type = "VectorSocket"
+                sock = tree.interface.new_socket(s_props["name"], in_out=s_props["in_out"], socket_type=socket_type)
+                tree_sock_id_map[s_name] = sock.identifier
+                    # TODO: set whatever properties are needed (default, etc)
+                if panel := s_props.get("parent"): # this get is just to maintain compatibility with an older form of this script... and it is harmless
+                    interface_parent_me[sock] = (panel, s_props["position"])
+            else: # it's a panel
+                panel = tree.interface.new_panel(s_props["name"], description=s_props.get("description"), default_closed=s_props.get("default_closed"))
     
+        for socket, (panel, index) in interface_parent_me.items():
+            tree.interface.move_to_parent(
+                                    socket,
+                                    tree.interface.items_tree.get(panel),
+                                    index,
+                                    )
+        for k,v in tree_sock_id_map.items():
+            prRed(k,v)
+        # at this point, the identifiers may not match
+        # so we have to map them back
+        
+#        from mantis.utilities import prRed, prWhite, prOrange, prGreen
+        for name, propslist in nodes.items():
+            n = tree.nodes.new(propslist["bl_idname"])
+            if propslist["bl_idname"] in ["DeformerMorphTargetDeform"]:
+                n.inputs.remove(n.inputs[1]) # get rid of the wildcard
+            # prPurple(n.bl_idname)
+
+            if n.bl_idname in [ "SchemaArrayInput",
+                                "SchemaArrayInputGet",
+                                "SchemaArrayOutput",
+                                "SchemaConstInput",
+                                "SchemaConstOutput",
+                                "SchemaOutgoingConnection",
+                                "SchemaIncomingConnection",]:
+                n.update()
+
+            
+            in_group_node = False
+            if sub_tree := propslist.get("node_tree"):
+                in_group_node = True
+                n.node_tree = bpy.data.node_groups.get(sub_tree)
+                # for s_name, s_val in propslist["sockets"].items():
+                #     print( wrapRed(s_name), wrapWhite(s_val))
+                # we have to do this first or the sockets won't exist to set their data.
+            #
+
+            for i, (s_id, s_val) in enumerate(propslist["sockets"].items()):
+                try:
+                    if s_val["is_output"]: # for some reason it thinks the index is a string?
+                        # try:
+                        if n.bl_idname == "MantisSchemaGroup":
+                            socket = n.outputs.new(s_val["bl_idname"], s_val["name"], identifier=s_id)
+                        elif n.bl_idname in ["NodeGroupInput"]:
+                            pass
+                        else:
+                            socket = n.outputs[int(s_val["index"])]
+                        # except Exception as e:
+                        #     prRed(n.name, s_id)
+                        #     raise e
+                    else:
+                        if s_val["index"] >= len(n.inputs):
+                            if n.bl_idname == "UtilityDriver":
+                                with bpy.context.temp_override(**{'node':n}):
+                                    bpy.ops.mantis.driver_node_add_variable()
+                                socket = n.inputs[int(s_val["index"])]
+                            elif n.bl_idname == "UtilityFCurve":
+                                with bpy.context.temp_override(**{'node':n}):
+                                    bpy.ops.mantis.fcurve_node_add_kf()
+                                socket = n.inputs[int(s_val["index"])]
+                            elif n.bl_idname == "MantisSchemaGroup":
+                                socket = n.inputs.new(s_val["bl_idname"], s_val["name"], identifier=s_id, use_multi_input=s_val["is_multi_input"])
+                                # for k,v in s_val.items():
+                                #     print(f"{k}:{v}")
+                                # print (s_id)
+                                # raise NotImplementedError(s_val["is_multi_input"])
+                            elif n.bl_idname in ["NodeGroupOutput"]:
+                                # print (len(n.inputs), len(n.outputs))
+                                pass
+                            elif n.bl_idname == "LinkArmature":
+                                with bpy.context.temp_override(**{'node':n}):
+                                    bpy.ops.mantis.link_armature_node_add_target()
+                                socket = n.inputs[int(s_val["index"])]
+                            elif n.bl_idname == "DeformerMorphTargetDeform": # this one doesn't use an operator since I figure out how to do dynamic node stuff
+                                socket = n.inputs.new(s_val["bl_idname"], s_val["name"], identifier=s_id)
+                            else:
+                                prRed(s_val["index"], len(n.inputs))
+                                raise NotImplementedError(wrapRed(f"{n.bl_idname} needs to be handled in JSON load."))
+                            # if n.bl_idname in ['']
+                        else: # most of the time
+                            socket = n.inputs[int(s_val["index"])]
+                except IndexError:
+                    socket = fix_custom_parameter(n, propslist["sockets"][s_id])
+                    if socket is None:
+                        is_output = "output" if {s_val["is_output"]} else "input"
+                        prRed(s_val, type(s_val))
+                        raise RuntimeError(is_output, n.name, s_val["name"], s_id, len(n.inputs))
+
+                
+#                if propslist["bl_idname"] == "UtilityMetaRig":#and i == 0:
+#                    pass#prRed (i, s_id, s_val)
+#                if propslist["bl_idname"] == "UtilityMetaRig":# and i > 0:
+#                       prRed("Not Found: %s" % (s_id))
+#                       prOrange(propslist["sockets"][s_id])
+#                       socket = fix_custom_parameter(n, propslist["sockets"][s_id]
+                
+                for s_p, s_v in s_val.items():
+                    if s_p not in ["default_value"]:
+                        if s_p == "search_prop" and n.bl_idname == 'UtilityMetaRig':
+                            socket.node.armature= s_v
+                            socket.search_prop=bpy.data.objects.get(s_v)
+                        if s_p == "search_prop" and n.bl_idname in ['UtilityMatrixFromCurve', 'UtilityMatricesFromCurve']:
+                            socket.search_prop=bpy.data.objects.get(s_v)
+                        elif s_p == "bone" and socket.bl_idname == 'EnumMetaBoneSocket':
+                            socket.bone = s_v
+                            socket.node.pose_bone = s_v
+                        continue # not editable and NOT SAFE
+                    #
+                    if socket.bl_idname in ["BooleanThreeTupleSocket"]:
+                        value = bool(s_v[0]), bool(s_v[1]), bool(s_v[2]),
+                        s_v = value
+                    try:
+                        setattr(socket, s_p , s_v)
+                    except TypeError as e:
+                        prRed("Can't set socket due to type mismatch: ", socket, s_p, s_v)
+                        # raise e
+                    except ValueError as e:
+                        prRed("Can't set socket due to type mismatch: ", socket, s_p, s_v)
+                        # raise e
+                    except AttributeError as e:
+                        prWhite("Tried to write a read-only property, ignoring...")
+                        prWhite(f"{socket.node.name}[{socket.name}].{s_p} is read only, cannot set value to {s_v}")
+                        # raise e
+                # not sure if this is true:
+                    # this can find properties that aren't node in/out
+                    # we should also be checking those above actually
+                # TODO:
+                # find out why "Bone hide" not being found
+            for p, v in propslist.items():
+                if p == "sockets": # it's the sockets dict
+                    continue
+                if p == "node_tree":
+                    continue # we've already done this # v = bpy.data.node_groups.get(v)
+                # will throw AttributeError if read-only
+                # will throw TypeError if wrong type...
+                if n.bl_idname == "NodeFrame" and p in ["width, height, location"]:
+                    continue 
+                if p == "parent" and v is not None:
+                    parent_me.append( (n.name, v) )
+                    v = None # for now) #TODO
+                try:
+                    setattr(n, p, v)
+                except Exception as e:
+                    print (p)
+                    raise e
+#        raise NotImplementedError
+
+        
+        
+#        for k,v in tree_sock_id_map.items():
+#            print (wrapGreen(k), "   ", wrapPurple(v))
+        
+        for l in links:
+            id1 = l[1]
+            id2 = l[3]
+            #
+            name1=l[6]
+            name2=l[7]
+            
+            prWhite(l[0], l[1], " --> ", l[2], l[3])
+            
+            # l has...
+            # node 1
+            # identifier 1
+            # node 2
+            # identifier 2
+            
+            from_node = tree.nodes[l[0]]
+            if hasattr(from_node, "node_tree"): # now we have to map by name actually
+                try:
+                    id1 = from_node.outputs[l[4]].identifier
+                except IndexError:
+                    prRed ("Index incorrect")
+                    id1 = None
+            elif from_node.bl_idname in ["NodeGroupInput"]:
+                id1 = tree_sock_id_map.get(l[1])
+                if id1 is None:
+                    prRed(l[1])
+#                prOrange (l[1], id1)
+            elif from_node.bl_idname in ["SchemaArrayInput", "SchemaConstInput", "SchemaIncomingConnection"]:
+                # try the index instead
+                id1 = from_node.outputs[l[4]].identifier
+            
+
+            for from_sock in from_node.outputs:
+                if from_sock.identifier == id1: break
+            else: # we can raise a runtime error here actually
+                from_sock = None
+                
+                
+            
+            to_node = tree.nodes[l[2]]
+            if hasattr(to_node, "node_tree"):
+                try:
+                    id2 = to_node.inputs[l[5]].identifier
+                except IndexError:
+                    prRed ("Index incorrect")
+                    id2 = None
+            elif to_node.bl_idname in ["NodeGroupOutput"]:
+                id2 = tree_sock_id_map.get(l[3])
+#                prPurple(to_node.name)
+#                for inp in to_node.inputs:
+#                    prPurple(inp.name, inp.identifier)
+#                prOrange (l[3], id2)
+            elif to_node.bl_idname in ["SchemaArrayOutput", "SchemaConstOutput", "SchemaOutgoingConnection"]:
+                # try the index instead
+                id2 = to_node.inputs[l[5]].identifier
+                # try to get by name
+                #id2 = to_node.inputs[name2]
+
+            for to_sock in to_node.inputs:
+                if to_sock.identifier == id2: break
+            else:
+                to_sock = None
+            
+            try:
+                link = tree.links.new(from_sock, to_sock)
+            except TypeError:
+                if ((id1 is not None) and ("Layer Mask" in id1)) or ((id2 is not None) and ("Layer Mask" in id2)):
+                    pass
+                else:
+                    prWhite(f"looking for... {name1}:{id1}, {name2}:{id2}")
+                    prRed (f"Failed: {l[0]}:{l[1]} --> {l[2]}:{l[3]}")
+                    prRed (f" got node: {from_node.name}, {to_node.name}")
+                    prRed (f" got socket: {from_sock}, {to_sock}")
+
+                    if from_sock is None:
+                        prOrange ("Candidates...")
+                        for out in from_node.outputs:
+                            prOrange("   %s, id=%s" % (out.name, out.identifier))
+                        for k, v in tree_sock_id_map.items():
+                            print (wrapOrange(k), wrapPurple(v))
+                    if to_sock is None:
+                        prOrange ("Candidates...")
+                        for inp in to_node.inputs:
+                            prOrange("   %s, id=%s" % (inp.name, inp.identifier))
+                        for k, v in tree_sock_id_map.items():
+                            print (wrapOrange(k), wrapPurple(v))
+                    raise RuntimeError
+            
+            # if at this point it doesn't work... we need to fix
+            
+        for name, p in parent_me:
+            if (n := tree.nodes.get(name)) and (p := tree.nodes.get(p)):
+                n.parent = p
+            # otherwise the frame node is missing because it was not included in the data e.g. when grouping nodes.
+
+        tree.is_executing = False
+        tree.do_live_update = True
+        
+        # try:
+        #     tree=context.space_data.path[0].node_tree
+        #     tree.update_tree(context)
+        # except: #update tree can cause all manner of errors
+        #     pass
+
+
+
+
+
+import bpy
+
+from bpy_extras.io_utils import ImportHelper, ExportHelper
+from bpy.props import StringProperty, BoolProperty, EnumProperty
+from bpy.types import Operator
+
+# Save As
+class MantisExportNodeTreeSaveAs(Operator, ExportHelper):
+    """Export a Mantis Node Tree by filename."""
+    bl_idname = "mantis.export_save_as"
+    bl_label = "Export Mantis Tree as ...(JSON)"
+
+    # ExportHelper mix-in class uses this.
+    filename_ext = ".rig"
+
+    filter_glob: StringProperty(
+        default="*.rig",
+        options={'HIDDEN'},
+        maxlen=255,  # Max internal buffer length, longer would be clamped.
+    )
+
+    @classmethod
+    def poll(cls, context):
+        return hasattr(context.space_data, 'path')
+
+    def execute(self, context):
+        # we need to get the dependent trees from self.tree...
+        # there is no self.tree
+        # how do I choose a tree?
+        
+        base_tree=context.space_data.path[-1].node_tree
+        from .utilities import all_trees_in_tree
+        trees = all_trees_in_tree(base_tree)[::-1]
+        prGreen("Exporting node graph with dependencies...")
+        for t in trees:
+            prGreen ("Node graph: \"%s\"" % (t.name))
+        return export_to_json(trees, self.filepath)
+
+# Save
+class MantisExportNodeTreeSave(Operator):
+    """Save a Mantis Node Tree to disk."""
+    bl_idname = "mantis.export_save"
+    bl_label = "Export Mantis Tree (JSON)"
+
+    @classmethod
+    def poll(cls, context):
+        return hasattr(context.space_data, 'path')
+
+    def execute(self, context):
+        
+        base_tree=context.space_data.path[-1].node_tree
+        from .utilities import all_trees_in_tree
+        trees = all_trees_in_tree(base_tree)[::-1]
+        prGreen("Exporting node graph with dependencies...")
+        for t in trees:
+            prGreen ("Node graph: \"%s\"" % (t.name))
+        return export_to_json(trees, base_tree.filepath)
+
+# Save Choose:
+class MantisExportNodeTree(Operator):
+    """Save a Mantis Node Tree to disk."""
+    bl_idname = "mantis.export_save_choose"
+    bl_label = "Export Mantis Tree (JSON)"
+
+    @classmethod
+    def poll(cls, context):
+        return hasattr(context.space_data, 'path')
+
+    def execute(self, context):
+        base_tree=context.space_data.path[-1].node_tree
+        if base_tree.filepath:
+            prRed(base_tree.filepath)
+            return bpy.ops.mantis.export_save()
+        else:
+            return bpy.ops.mantis.export_save_as('INVOKE_DEFAULT')
+
+
+
+
+# here is what needs to be done...
+#   - modify this to work with a sort of parsed-tree instead (sort of)
+#        - this needs to treat each sub-graph on its own
+#        - is this a problem? do I need to reconsider how I treat the graph data in mantis?
+#        - I should learn functional programming / currying
+#   - then the parsed-tree this builds must be executed as Blender nodes
+#   - I think... this is not important right now. not yet.
+#   -  KEEP IT SIMPLE, STUPID
+
+
+
+class MantisImportNodeTree(Operator, ImportHelper):
+    """Import a Mantis Node Tree."""
+    bl_idname = "mantis.import_tree"
+    bl_label = "Import Mantis Tree (JSON)"
+
+    # ImportHelper mixin class uses this
+    filename_ext = ".rig"
+
+    filter_glob : StringProperty(
+        default="*.rig",
+        options={'HIDDEN'},
+        maxlen=255,  # Max internal buffer length, longer would be clamped.
+    )
+
+
+    def execute(self, context):
+        return do_import_from_file(self.filepath, context)
+
+
+# this is useful:
+# https://blender.stackexchange.com/questions/73286/how-to-call-a-confirmation-dialog-box
+
+# class MantisReloadConfirmMenu(bpy.types.Panel):
+#     bl_label = "Confirm?"
+#     bl_idname = "OBJECT_MT_mantis_reload_confirm"
+
+#     def draw(self, context):
+#         layout = self.layout
+#         layout.operator("mantis.reload_tree")
+
+class MantisReloadNodeTree(Operator):
+    # """Import a Mantis Node Tree."""
+    # bl_idname = "mantis.reload_tree"
+    # bl_label = "Import Mantis Tree"
+    """Reload Mantis Tree"""
+    bl_idname = "mantis.reload_tree"
+    bl_label = "Confirm reload tree?"
+    bl_options = {'REGISTER', 'INTERNAL'}
+
+
+    @classmethod
+    def poll(cls, context):
+        if hasattr(context.space_data, 'path'):
+            return True
+        return False
+
+    def invoke(self, context, event):
+        return context.window_manager.invoke_confirm(self, event)
+
+    def execute(self, context):
+        base_tree=context.space_data.path[-1].node_tree
+        if not base_tree.filepath:
+            self.report({'ERROR'}, "Tree has not been saved - so it cannot be reloaded.")
+            return {'CANCELLED'}
+        self.report({'INFO'}, "reloading tree")
+        return do_import_from_file(base_tree.filepath, context)
+
+
+
 
-def import_from_json(json_data):
-    #reads a JSON file and makes a tree from it.
-    pass
 
-def tree_to_b_nodes(tree):
-    # creates a node tree in Blender from a mantis tree
-    pass
+# todo:
+#  - export metarig and option to import it
+#  - same with controls
+#  - it would be nice to have a library of these that can be imported alongside the mantis graph

+ 23 - 50
internal_containers.py

@@ -1,10 +1,10 @@
 from .node_container_common import *
 from bpy.types import Node
 from .base_definitions import MantisNode
-
+from uuid import uuid4
 
 class DummyNode:
-    def __init__(self, signature, base_tree, prototype = None):
+    def __init__(self, signature, base_tree, prototype = None, natural_signature=None):
         self.signature = signature
         self.base_tree = base_tree
         self.prototype = prototype
@@ -12,60 +12,33 @@ class DummyNode:
         self.outputs={}
         self.parameters = {}
         self.node_type = 'DUMMY'
+        self.prepared = True
+        if prototype.bl_idname in ["MantisSchemaGroup"]:
+            self.node_type = 'DUMMY_SCHEMA'
+            self.prepared = False
+            self.uuid = uuid4()
         if prototype:
             for sock in prototype.inputs:
-                if sock.name == "__extend__":
+                if sock.identifier == "__extend__" or sock.name == "__extend__":
                     continue
                 self.inputs[sock.identifier] = NodeSocket(is_input = True, name = sock.identifier, node = self)
             for sock in prototype.outputs:
-                if sock.name == "__extend__":
+                if sock.identifier == "__extend__" or sock.name == "__extend__":
                     continue
                 self.outputs[sock.identifier] = NodeSocket(is_input = False, name = sock.identifier, node = self)
                 self.parameters[sock.identifier]=None
+        #HACK
+        if natural_signature:
+            self.natural_signature=natural_signature
+        #HACK
+        self.hierarchy_connections = []
+        self.connections = []
+        self.hierarchy_dependencies = []
+        self.dependencies = []
+        self.executed = False
+        #
+        # 
+        self.pre_pass_done = False
+        self.execute_pass_done = False
 
-    def evaluate_input(self, input_name):
-        pass
-        # return evaluate_input(self, input_name)
-    
-    def bExecute(self, bContext = None,):
-        pass
-        
-    def bFinalize(self, bContext = None,):
-        pass
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self,):
-        pass# fill_parameters(self)
-        # I don't think I am using this but it doesn't hurt
-
-
-# a class for duplicating an existing node for e.g. temporary
-#  traces
-class DupeNode:
-    def __init__(self, signature, base_tree):
-        self.signature = signature
-        self.base_tree = base_tree
-        self.prototype = prototype
-        self.inputs={}
-        self.outputs={}
-        self.parameters = {}
-        self.node_type = 'DUMMY'
-
-    def evaluate_input(self, input_name):
-        pass
-        # return evaluate_input(self, input_name)
-    
-    def bExecute(self, bContext = None,):
-        pass
-        
-    def bFinalize(self, bContext = None,):
-        pass
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self,):
-        fill_parameters(self)
-        # I don't think I am using this but it doesn't hurt
+setup_container(DummyNode)

File diff suppressed because it is too large
+ 367 - 123
link_containers.py


+ 124 - 50
link_definitions.py

@@ -37,6 +37,15 @@ def default_traverse(self, socket):
             return self.outputs["Output Relationship"]
         return None
 
+
+from mathutils import Color
+linkColor = Color((0.028034, 0.093164, 0.070379)).from_scene_linear_to_srgb()
+inheritColor = Color((0.083213, 0.131242, 0.116497)).from_scene_linear_to_srgb()
+trackingColor = Color((0.033114, 0.049013, 0.131248)).from_scene_linear_to_srgb()
+ikColor = Color((0.131117, 0.131248, 0.006971)).from_scene_linear_to_srgb()
+driverColor = Color((0.043782, 0.014745, 0.131248,)).from_scene_linear_to_srgb()
+
+
 class LinkInheritNode(Node, LinkNode):
     '''A node representing inheritance'''
     # cuss, messed this up
@@ -58,6 +67,9 @@ class LinkInheritNode(Node, LinkNode):
         p = self.inputs.new('xFormSocket', "Parent")
         # set default values...
         self.initialized = True
+        # color
+        self.use_custom_color = True
+        self.color = inheritColor
 
     def traverse(self, socket):
         if (socket == self.outputs["Inheritance"]):
@@ -125,40 +137,18 @@ class LinkInverseKinematics(Node, LinkNode):
         self.outputs.new('RelationshipSocket', "Output Relationship")
 
         self.initialized = True
+        # color
+        self.use_custom_color = True
+        self.color = ikColor
 
 
-        
-class LinkSplineIK(Node, LinkNode):
-    '''A node representing Spline IK'''
-    bl_idname = 'LinkSplineIK'
-    bl_label = "LinkSplineIK"
-    bl_icon = 'CON_SPLINEIK'
-    useTarget : bpy.props.BoolProperty(default=True)
-    def init(self, context):
-        self.inputs.new('xFormSocket', "Parent")
-        self.inputs.new ('xFormSocket', "Target")
-        self.inputs.new ('xFormSocket', "Pole Target")
-        self.inputs.new ('BooleanSocket', "Use Tail")
-        self.inputs.new ('BooleanSocket', "Stretch")
-        self.inputs.new ('FloatFactorSocket', "Position")
-        self.inputs.new ('FloatFactorSocket', "Rotation")
-        self.inputs.new ('FloatFactorSocket', "Influence")
-        self.inputs.new ('EnableSocket', "Enable")
-        self.outputs.new('RelationshipSocket', "Inheritance")
-
-    def traverse(self, socket):
-        if (socket == self.outputs["Inheritance"]):
-            return self.inputs["Parent"]
-        if (socket == self.inputs["Parent"]):
-            return self.outputs["Inheritance"]
-        return None
-
 class LinkCopyLocationNode(Node, LinkNode):
     '''A node representing Copy Location'''
     bl_idname = 'LinkCopyLocation'
     bl_label = "Copy Location"
     bl_icon = 'CON_LOCLIKE'
     useTarget : bpy.props.BoolProperty(default=True)
+    initialized : bpy.props.BoolProperty(default = False)
 
     def init(self, context):
         self.inputs.new ('RelationshipSocket', "Input Relationship")
@@ -168,41 +158,25 @@ class LinkCopyLocationNode(Node, LinkNode):
         self.inputs.new ('BooleanThreeTupleSocket', "Invert")
         self.inputs.new ('TransformSpaceSocket', "Target Space")
         self.inputs.new ('TransformSpaceSocket', "Owner Space")
+        self.inputs.new ('BooleanSocket', "Offset")
         self.inputs.new ('FloatFactorSocket', "Influence")
         self.inputs.new ('xFormSocket', "Target")
         self.inputs.new ('EnableSocket', "Enable")
         #
         self.outputs.new('RelationshipSocket', "Output Relationship")
-
+        # color
+        self.use_custom_color = True
+        self.color = linkColor
+        self.initialized = True
 
         
-    
-    # def display_update(self, parsed_tree, context):
-        # node_tree = context.space_data.path[0].node_tree
-        # nc = parsed_tree.get(get_signature_from_edited_tree(self, context))
-        # if nc:
-            # bone_prev, bone_next = False, False
-            # if (inp := nc.inputs["Input Relationship"]).is_connected:
-                # if  from_node := inp.links[0].from_node:
-                    # if from_node.__class__.__name__ in ["xFormBone"]:
-                        # bone_prev=True
-            # bone_next=True
-            # try:
-                # xForm = nc.GetxForm()
-                # if xForm.__class__.__name__ not in "xFormBone":
-                    # bone_next=False
-            # except GraphError:
-                # bone_next=False
-            # if bone_next and bone_prev:
-                # # this is where we will set whether you can pick
-                # #  the other spaces or not.
-        
 class LinkCopyRotationNode(Node, LinkNode):
     '''A node representing Copy Rotation'''
     bl_idname = 'LinkCopyRotation'
     bl_label = "Copy Rotation"
     bl_icon = 'CON_ROTLIKE'
     useTarget : bpy.props.BoolProperty(default=True)
+    initialized : bpy.props.BoolProperty(default = False)
 
     def init(self, context):
         self.inputs.new ('RelationshipSocket', "Input Relationship")
@@ -217,6 +191,10 @@ class LinkCopyRotationNode(Node, LinkNode):
         self.inputs.new ('EnableSocket', "Enable")
         #
         self.outputs.new('RelationshipSocket', "Output Relationship")
+        # color
+        self.use_custom_color = True
+        self.color = linkColor
+        self.initialized = True
 
 
         
@@ -226,6 +204,7 @@ class LinkCopyScaleNode(Node, LinkNode):
     bl_label = "Copy Scale"
     bl_icon = 'CON_SIZELIKE'
     useTarget : bpy.props.BoolProperty(default=True)
+    initialized : bpy.props.BoolProperty(default = False)
 
     def init(self, context):
         self.inputs.new ('RelationshipSocket', "Input Relationship")
@@ -241,6 +220,10 @@ class LinkCopyScaleNode(Node, LinkNode):
         self.inputs.new ('EnableSocket', "Enable")
         #
         self.outputs.new('RelationshipSocket', "Output Relationship")
+        # color
+        self.use_custom_color = True
+        self.color = linkColor
+        self.initialized = True
 
 
         
@@ -252,6 +235,7 @@ class LinkInheritConstraintNode(Node, LinkNode):
     bl_label = "Inherit (constraint)"
     bl_icon = 'CON_CHILDOF'
     useTarget : bpy.props.BoolProperty(default=True)
+    initialized : bpy.props.BoolProperty(default = False)
 
     # === Optional Functions ===
     def init(self, context):
@@ -264,6 +248,10 @@ class LinkInheritConstraintNode(Node, LinkNode):
         self.inputs.new ('EnableSocket', "Enable")
         #
         self.outputs.new('RelationshipSocket', "Output Relationship")
+        # color
+        self.use_custom_color = True
+        self.color = inheritColor
+        self.initialized = True
 
 
         
@@ -275,6 +263,7 @@ class LinkCopyTransformNode(Node, LinkNode):
     bl_label = "Copy Transform"
     bl_icon = 'CON_TRANSLIKE'
     useTarget : bpy.props.BoolProperty(default=True)
+    initialized : bpy.props.BoolProperty(default = False)
 
 
     # === Optional Functions ===
@@ -290,6 +279,10 @@ class LinkCopyTransformNode(Node, LinkNode):
         self.inputs.new ('EnableSocket', "Enable")
         #
         self.outputs.new('RelationshipSocket', "Output Relationship")
+        # color
+        self.use_custom_color = True
+        self.color = linkColor
+        self.initialized = True
 
 
         
@@ -320,6 +313,9 @@ class LinkStretchToNode(Node, LinkNode):
         self.outputs.new('RelationshipSocket', "Output Relationship")
 
         self.initialized = True
+        # color
+        self.use_custom_color = True
+        self.color = trackingColor
 
 
         
@@ -342,6 +338,9 @@ class LinkDampedTrackNode(Node, LinkNode):
         self.outputs.new('RelationshipSocket', "Output Relationship")
 
         self.initialized = True
+        # color
+        self.use_custom_color = True
+        self.color = trackingColor
 
 
         
@@ -366,6 +365,9 @@ class LinkLockedTrackNode(Node, LinkNode):
         self.outputs.new('RelationshipSocket', "Output Relationship")
 
         self.initialized = True
+        # color
+        self.use_custom_color = True
+        self.color = trackingColor
 
 
         
@@ -393,6 +395,9 @@ class LinkTrackToNode(Node, LinkNode):
         self.outputs.new('RelationshipSocket', "Output Relationship")
 
         self.initialized = True
+        # color
+        self.use_custom_color = True
+        self.color = trackingColor
 
 
         
@@ -425,6 +430,9 @@ class LinkLimitLocationNode(Node, LinkNode):
         #
         self.outputs.new('RelationshipSocket', "Output Relationship")
         self.initialized = True
+        # color
+        self.use_custom_color = True
+        self.color = linkColor
 
 
             
@@ -457,6 +465,9 @@ class LinkLimitScaleNode(Node, LinkNode):
         #
         self.outputs.new('RelationshipSocket', "Output Relationship")
         self.initialized = True
+        # color
+        self.use_custom_color = True
+        self.color = linkColor
 
 
             
@@ -489,6 +500,9 @@ class LinkLimitRotationNode(Node, LinkNode):
         #
         self.outputs.new('RelationshipSocket', "Output Relationship")
         self.initialized = True
+        # color
+        self.use_custom_color = True
+        self.color = linkColor
 
 
         
@@ -498,6 +512,7 @@ class LinkLimitDistanceNode(Node, LinkNode):
     bl_label = "Limit Distance"
     bl_icon = 'CON_DISTLIMIT'
     useTarget : bpy.props.BoolProperty(default=True)
+    initialized : bpy.props.BoolProperty(default = False)
 
     def init(self, context):
         self.inputs.new ('RelationshipSocket', "Input Relationship")
@@ -513,6 +528,10 @@ class LinkLimitDistanceNode(Node, LinkNode):
         self.inputs.new ('EnableSocket', "Enable")
         #
         self.outputs.new('RelationshipSocket', "Output Relationship")
+        # color
+        self.use_custom_color = True
+        self.color = linkColor
+        self.initialized = True
 
         
         
@@ -522,6 +541,7 @@ class LinkTransformationNode(Node, LinkNode):
     bl_label = "Transformation"
     bl_icon = 'CON_TRANSFORM'
     useTarget : bpy.props.BoolProperty(default=True)
+    initialized : bpy.props.BoolProperty(default = False)
 
     def init(self, context):
         hide_me = []
@@ -559,6 +579,10 @@ class LinkTransformationNode(Node, LinkNode):
         
         for s in hide_me:
             s.hide = True
+        # color
+        self.use_custom_color = True
+        self.color = linkColor
+        self.initialized = True
 
     
     
@@ -593,30 +617,69 @@ class LinkArmatureNode(Node, LinkNode):
     bl_idname = "LinkArmature"
     bl_label = "Armature (Constraint)"
     bl_icon = "CON_ARMATURE"
+    initialized : bpy.props.BoolProperty(default = False)
     
     def init(self, context):
         self.inputs.new ("RelationshipSocket", "Input Relationship")
         self.inputs.new("BooleanSocket", "Preserve Volume")
         self.inputs.new("BooleanSocket", "Use Envelopes")
         self.inputs.new("BooleanSocket", "Use Current Location")
-        self.inputs.new("FloatSocket", "Influence")
+        self.inputs.new("FloatFactorSocket", "Influence")
         self.inputs.new ('EnableSocket', "Enable")
         self.outputs.new("RelationshipSocket", "Output Relationship")
+        # color
+        self.use_custom_color = True
+        self.color = inheritColor
+        self.initialized = True
+
+
     def traverse(self, socket):
         return default_traverse(self,socket)
     
     def draw_buttons(self, context, layout):
+        # return
         layout.operator( 'mantis.link_armature_node_add_target' )
         if (len(self.inputs) > 6):
             layout.operator( 'mantis.link_armature_node_remove_target' )
         else:
             layout.label(text="")
 
+# why did I define this twice?
+# class LinkSplineIK(Node, LinkNode):
+#     '''A node representing Spline IK'''
+#     bl_idname = 'LinkSplineIK'
+#     bl_label = "LinkSplineIK"
+#     bl_icon = 'CON_SPLINEIK'
+#     useTarget : bpy.props.BoolProperty(default=True)
+#     def init(self, context):
+#         self.inputs.new('xFormSocket', "Parent")
+#         self.inputs.new ('xFormSocket', "Target")
+#         self.inputs.new ('xFormSocket', "Pole Target")
+#         self.inputs.new ('BooleanSocket', "Use Tail")
+#         self.inputs.new ('BooleanSocket', "Stretch")
+#         self.inputs.new ('FloatFactorSocket', "Position")
+#         self.inputs.new ('FloatFactorSocket', "Rotation")
+#         self.inputs.new ('FloatFactorSocket', "Influence")
+#         self.inputs.new ('EnableSocket', "Enable")
+#         self.outputs.new('RelationshipSocket', "Inheritance")
+#         # color
+#         self.use_custom_color = True
+#         self.color = ikColor
+
+#     def traverse(self, socket):
+#         if (socket == self.outputs["Inheritance"]):
+#             return self.inputs["Parent"]
+#         if (socket == self.inputs["Parent"]):
+#             return self.outputs["Inheritance"]
+#         return None
+
+
 class LinkSplineIKNode(Node, LinkNode):
     """"A node representing Spline IK"""
     bl_idname = "LinkSplineIK"
     bl_label = "Spline IK"
     bl_icon = "CON_SPLINEIK"
+    initialized : bpy.props.BoolProperty(default = False)
     
     def init(self, context):
         self.inputs.new ("RelationshipSocket", "Input Relationship")
@@ -628,8 +691,13 @@ class LinkSplineIKNode(Node, LinkNode):
         self.inputs.new("EnumYScaleMode", "Y Scale Mode")
         self.inputs.new("EnumXZScaleMode", "XZ Scale Mode")
         self.inputs.new("BooleanSocket", "Use Original Scale")
-        self.inputs.new("FloatSocket", "Influence")
+        self.inputs.new("FloatFactorSocket", "Influence")
         self.outputs.new("RelationshipSocket", "Output Relationship")
+        # color
+        self.use_custom_color = True
+        self.color = ikColor
+        self.initialized = True
+
     def traverse(self, socket):
         return default_traverse(self,socket)
         
@@ -641,6 +709,7 @@ class LinkDrivenParameterNode(Node, LinkNode):
     bl_idname = "LinkDrivenParameter"
     bl_label = "Driven Parameter"
     bl_icon = "CONSTRAINT_BONE"
+    initialized : bpy.props.BoolProperty(default = False)
     
     def init(self, context):
         self.inputs.new ( "RelationshipSocket", "Input Relationship" )
@@ -650,6 +719,11 @@ class LinkDrivenParameterNode(Node, LinkNode):
         self.inputs.new ('EnableSocket', "Enable")
         #
         self.outputs.new( "RelationshipSocket", "Output Relationship" )
+        self.initialized = True
+        
     def traverse(self, socket):
         return default_traverse(self,socket)
+        # color
+        self.use_custom_color = True
+        self.color = driverColor
 

+ 214 - 0
math_containers.py

@@ -0,0 +1,214 @@
+from .node_container_common import *
+
+def TellClasses():
+    return [
+            MathStaticInt,
+            MathStaticFloat,
+            MathStaticVector,
+           ]
+
+#*#-------------------------------#++#-------------------------------#*#
+# M A T H  N O D E S
+#*#-------------------------------#++#-------------------------------#*#
+class MathStaticInt:
+    '''A node representing an armature object'''
+
+    def __init__(self, signature, base_tree):
+        self.base_tree=base_tree
+        self.executed = False
+        self.signature = signature
+        self.inputs = {
+          "Operation" : NodeSocket(is_input = True, name = "Operation", node = self),
+          "Int A"   : NodeSocket(is_input = True, name = "Int A", node = self),
+          "Int B"   : NodeSocket(is_input = True, name = "Int B", node = self),
+        }
+        self.outputs = {
+          "Result Int" : NodeSocket(name = "Result Int", node=self),
+        }
+        self.parameters = {
+          "Operation":None,
+          "Int A":None, 
+          "Int B":None, 
+          "Result Int":None, 
+        }
+        self.node_type = "UTILITY"
+        self.hierarchy_connections = []
+        self.connections = []
+        self.hierarchy_dependencies = []
+        self.dependencies = []
+        self.prepared = False
+        self.executed = False
+
+    def bPrepare(self, bContext = None,):
+        a = self.evaluate_input("Int A"); b = self.evaluate_input("Int B")
+        result = float("NaN")
+        if self.evaluate_input("Operation") == "ADD":
+            result = int(a+b)
+        if self.evaluate_input("Operation") == "SUBTRACT":
+            result = int(a-b)
+        if self.evaluate_input("Operation") == "MULTIPLY":
+            result = int(a*b)
+        if self.evaluate_input("Operation") == "FLOOR_DIVIDE":
+            result = a//b
+        if self.evaluate_input("Operation") == "MODULUS":
+            result = int(a%b)
+        if self.evaluate_input("Operation") == "POWER":
+            result = int(a**b)
+        if self.evaluate_input("Operation") == "ABSOLUTE":
+            result = int(abs(a))
+        if self.evaluate_input("Operation") == "MAXIMUM":
+            result = int(a if a <= b else b)
+        if self.evaluate_input("Operation") == "MINIMUM":
+            result = int(a if a >= b else b)
+        if self.evaluate_input("Operation") == "GREATER THAN":
+            result = int(a > b)
+        if self.evaluate_input("Operation") == "LESS THAN":
+            result = int(a < b)
+        self.parameters["Result Int"] = result
+
+        self.prepared = True
+        self.executed = True
+
+class MathStaticFloat:
+    '''A node representing an armature object'''
+
+    def __init__(self, signature, base_tree):
+        self.base_tree=base_tree
+        self.executed = False
+        self.signature = signature
+        self.inputs = {
+          "Operation" : NodeSocket(is_input = True, name = "Operation", node = self),
+          "Float A"   : NodeSocket(is_input = True, name = "Float A", node = self),
+          "Float B"   : NodeSocket(is_input = True, name = "Float B", node = self),
+        }
+        self.outputs = {
+          "Result Float" : NodeSocket(name = "Result Float", node=self),
+        }
+        self.parameters = {
+          "Operation":None,
+          "Float A":None, 
+          "Float B":None, 
+          "Result Float":None, 
+        }
+        self.node_type = "UTILITY"
+        self.hierarchy_connections = []
+        self.connections = []
+        self.hierarchy_dependencies = []
+        self.dependencies = []
+        self.prepared = False
+        self.executed = False
+
+    def bPrepare(self, bContext = None,):
+        a = self.evaluate_input("Float A"); b = self.evaluate_input("Float B")
+        result = float("NaN")
+        if self.evaluate_input("Operation") == "ADD":
+            result = a+b
+        if self.evaluate_input("Operation") == "SUBTRACT":
+            result = a-b
+        if self.evaluate_input("Operation") == "MULTIPLY":
+            result = a*b
+        if self.evaluate_input("Operation") == "DIVIDE":
+            result = a/b
+        if self.evaluate_input("Operation") == "FLOOR_DIVIDE":
+            result = a//b
+        if self.evaluate_input("Operation") == "MODULUS":
+            result = a%b
+        if self.evaluate_input("Operation") == "POWER":
+            result = a**b
+        if self.evaluate_input("Operation") == "ABSOLUTE":
+            result = abs(a)
+        if self.evaluate_input("Operation") == "MAXIMUM":
+            result = a if a <= b else b
+        if self.evaluate_input("Operation") == "MINIMUM":
+            result = a if a >= b else b
+        if self.evaluate_input("Operation") == "GREATER THAN":
+            result = float(a > b)
+        if self.evaluate_input("Operation") == "LESS THAN":
+            result = float(a < b)
+        self.parameters["Result Float"] = result
+        self.prepared = True
+        self.executed = True
+
+
+# enumVectorOperations = (('ADD', 'Add', 'Add (Component-wise)'),
+#                         ('SUBTRACT', "Subtract", "Subtract (Component-wise)"),
+#                         ('MULTIPLY', "Multiply", "Multiply (Component-wise)"),
+#                         ('SCALE', "Scale", "Scales vector by input float or average magnitude of input vector's components."),
+#                         ('DIVIDE', "Divide", "Divide (Component-wise)"),
+#                         ('POWER', "Power", "Power (Component-wise)"),
+#                         ('LENGTH', "Length", "Length"),
+#                         ('CROSS', "Cross Product", "Cross product of A X B"),
+#                         ('DOT', "Dot Product", "Dot product of A . B"),
+
+
+class MathStaticVector:
+    '''A node representing an armature object'''
+
+    def __init__(self, signature, base_tree):
+        self.base_tree=base_tree
+        self.executed = False
+        self.signature = signature
+        self.inputs = {
+          "Operation"  : NodeSocket(is_input = True, name = "Operation", node = self),
+          "Vector A"   : NodeSocket(is_input = True, name = "Vector A", node = self),
+          "Vector B"   : NodeSocket(is_input = True, name = "Vector B", node = self),
+          "Scalar A"   : NodeSocket(is_input = True, name = "Scalar A", node = self),
+        }
+        self.outputs = {
+          "Result Vector" : NodeSocket(name = "Result Vector", node=self),
+          "Result Float" : NodeSocket(name = "Result Float", node=self),
+        }
+        self.parameters = {
+          "Operation":None,
+          "Vector A":None, 
+          "Vector B":None, 
+          "Scalar A":None, 
+          "Result Vector":None, 
+          "Result Float":None, 
+        }
+        self.node_type = "UTILITY"
+        self.hierarchy_connections = []
+        self.connections = []
+        self.hierarchy_dependencies = []
+        self.dependencies = []
+        self.prepared = False
+        self.executed = False
+
+    def bPrepare(self, bContext = None,):
+        from mathutils import Vector
+        a = Vector(self.evaluate_input("Vector A")).copy()
+        b = Vector(self.evaluate_input("Vector B")).copy()
+        s = self.evaluate_input("Scalar A")
+        if hasattr(s, '__iter__'):
+            average = lambda iterable : sum(iterable)/len(iterable)
+            s = average(s)
+        f_result = float("NaN")
+        v_result = None
+        if self.evaluate_input("Operation") == "ADD":
+            v_result = a+b
+        if self.evaluate_input("Operation") == "SUBTRACT":
+            v_result = a-b
+        if self.evaluate_input("Operation") == "MULTIPLY":
+            v_result = a*b
+        if self.evaluate_input("Operation") == "DIVIDE":
+            v_result = a/b
+        if self.evaluate_input("Operation") == "POWER":
+            v_result = a**b
+        if self.evaluate_input("Operation") == "SCALE":
+            v_result = a.normalized() * s
+        if self.evaluate_input("Operation") == "LENGTH":
+            f_result =  a.magnitude
+        if self.evaluate_input("Operation") == "CROSS":
+            v_result =  a.cross(b)
+        if self.evaluate_input("Operation") == "DOT":
+            f_result =  a.dot(b)
+        if self.evaluate_input("Operation") == "NORMALIZE":
+            v_result =  a.normalized()
+        self.parameters["Result Float"] = f_result
+        self.parameters["Result Vector"] = v_result
+        self.prepared = True
+        self.executed = True
+
+
+for c in TellClasses():
+    setup_container(c)

+ 125 - 0
math_definitions.py

@@ -0,0 +1,125 @@
+import bpy
+from .base_definitions import MantisNode
+from bpy.types import Node
+from .utilities import (prRed, prGreen, prPurple, prWhite,
+                              prOrange,
+                              wrapRed, wrapGreen, wrapPurple, wrapWhite,
+                              wrapOrange,)
+from .base_definitions import get_signature_from_edited_tree
+
+
+def TellClasses():
+    return [
+     MathStaticInt,
+     MathStaticFloatNode,
+     MathStaticVectorNode,
+        ]
+
+
+        
+
+class MathStaticInt(Node, MantisNode):
+    """A node that performs mathematical operations on float numbers as a preprocess step before generating the rig."""
+    bl_idname = "MathStaticInt"
+    bl_label = "Static Int Math"
+    bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
+
+
+    def init(self, context):
+        self.inputs.new("MathFloatOperation", "Operation")
+        self.inputs.new("IntSocket", "Int A")
+        self.inputs.new("IntSocket", "Int B")
+        self.outputs.new("IntSocket", "Result Int")
+        self.initialized = True
+    
+
+    def display_update(self, parsed_tree, context):
+        if context.space_data:
+            node_tree = context.space_data.path[0].node_tree
+            nc = parsed_tree.get(get_signature_from_edited_tree(self, context))
+            op = nc.evaluate_input("Operation")
+            if op in ['ABSOLUTE']:
+                self.inputs["Int B"].hide = True
+            else:
+                self.inputs["Int B"].hide = False
+
+    def traverse(self, socket):
+        return default_traverse(self,socket)
+                   
+
+
+# do... make the operations now
+class MathStaticFloatNode(Node, MantisNode):
+    """A node that performs mathematical operations on float numbers as a preprocess step before generating the rig."""
+    bl_idname = "MathStaticFloat"
+    bl_label = "Static Float Math"
+    bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
+
+
+    def init(self, context):
+        self.inputs.new("MathFloatOperation", "Operation")
+        self.inputs.new("FloatSocket", "Float A")
+        self.inputs.new("FloatSocket", "Float B")
+        self.outputs.new("FloatSocket", "Result Float")
+        self.initialized = True
+    
+
+    def display_update(self, parsed_tree, context):
+        if context.space_data:
+            node_tree = context.space_data.path[0].node_tree
+            nc = parsed_tree.get(get_signature_from_edited_tree(self, context))
+            op = nc.evaluate_input("Operation")
+            if op in ['ABSOLUTE']:
+                self.inputs["Float B"].hide = True
+            else:
+                self.inputs["Float B"].hide = False
+
+    def traverse(self, socket):
+        return default_traverse(self,socket)
+
+
+class MathStaticVectorNode(Node, MantisNode):
+    """Performs a vector math operation as a preprocess before executing the tree."""
+    bl_idname = "MathStaticVector"
+    bl_label = "Static Vector Math"
+    bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        self.inputs.new("MathVectorOperation", "Operation")
+        self.inputs.new("VectorSocket", "Vector A")
+        self.inputs.new("VectorSocket", "Vector B")
+        h = self.inputs.new("FloatSocket", "Scalar A"); h.hide=True
+        self.outputs.new("VectorSocket", "Result Vector")
+        h = self.outputs.new("FloatSocket", "Result Float"); h.hide=True
+        self.initialized = True
+
+    def display_update(self, parsed_tree, context):
+        if context.space_data:
+            node_tree = context.space_data.path[0].node_tree
+            nc = parsed_tree.get(get_signature_from_edited_tree(self, context))
+            op = nc.evaluate_input("Operation")
+            # Scalar output
+            if op in ['LENGTH', 'DOT']:
+                self.outputs["Result Vector"].hide = True
+                self.outputs["Result Float"].hide = False
+            else: # Vector output
+                self.outputs["Result Vector"].hide = False
+                self.outputs["Result Float"].hide = True
+            
+            # Single Vector and Scalar input
+            if op in ['SCALE', ]:
+                self.inputs["Vector B"].hide = True
+                self.inputs["Scalar A"].hide = False
+            elif op in ['LENGTH', 'NORMALIZE']: # only a vector input
+                self.inputs["Vector B"].hide = True
+                self.inputs["Scalar A"].hide = True
+            else:
+                self.inputs["Vector B"].hide = False
+                self.inputs["Scalar A"].hide = True
+
+    
+    def traverse(self, socket):
+        return default_traverse(self,socket)

File diff suppressed because it is too large
+ 426 - 239
misc_containers.py


+ 0 - 2125
node_container_classes.py

@@ -1,2125 +0,0 @@
-# generic node classes
-
-#everything here sucks and is dumb and needs renaiming
-
-# this is stupid, lol
-def TellClasses():
-             # xForm
-    return [ xFormRoot,
-             xFormArmature,
-             xFormBone,
-             # special
-             LinkInherit,
-             # copy
-             LinkCopyLocation,
-             LinkCopyRotation,
-             LinkCopyScale,
-             LinkCopyTransforms,
-             # limit
-             LinkLimitLocation,
-             LinkLimitRotation,
-             LinkLimitScale,
-             LinkLimitDistance,
-             # tracking
-             LinkStretchTo,
-             LinkDampedTrack,
-             LinkLockedTrack,
-             LinkTrackTo,
-             #misc
-             LinkInheritConstraint,
-             # IK
-             LinkInverseKinematics,
-             # utility
-             InputFloat,
-             InputVector,
-             InputBoolean,
-             InputBooleanThreeTuple,
-             InputRotationOrder,
-             InputTransformSpace,
-             InputString,
-             InputQuaternion,
-             InputQuaternionAA,
-             InputMatrix,
-            ]
-    #kinda a dumb way to do this but it werks so whatever
-            
-            
-
-
-# have these so far
-  # simple nodes:
-             # InputFloatNode,
-             # InputVectorNode,
-             # InputBooleanNode,
-             # InputBooleanThreeTupleNode,
-             # InputRotationOrderNode,
-             # InputTransformSpaceNode,
-             # InputStringNode,
-             # InputQuaternionNode,
-             # InputQuaternionNodeAA,
-             # InputMatrixNode,
-
-  # xForm nodes:
-        # xFormNullNode,
-        # xFormBoneNode,
-        # xFormRootNode,
-        # xFormArmatureNode,
-        
-
-  # Link nodes:
-             # LinkInheritNode,
-             # LinkInverseKinematics,
-             # LinkCopyLocationNode,
-             # LinkCopyRotationNode,
-             # LinkCopyScaleNode,
-             # LinkInheritConstraintNode,
-             # LinkCopyTransformNode,
-             # LinkStretchToNode,
-             # LinkDampedTrackNode,
-             # LinkLockedTrackNode,
-             # LinkTrackToNode,
-             # LinkLimitLocationNode,
-             # LinkLimitScaleNode,
-             # LinkLimitRotationNode,
-             # LinkLimitDistanceNode,]
-             
-             
-#eventually add this:
-    # def print_to_mantis_script():
-        # #gonna eventually make this into a script
-        # # that can be written and read from a text file
-        # # and loaded into the interpretor
-        # pass
-# 
-
-
-
-# DO THIS:
-#  each node should have a name or UUID, since there must be a way to
-#   associate them with the lines that are read from the input-graph
-#  actually I cna and should just use the signature, since it can gimme
-#   any prototype node I need, and it encodes relationships, too.
-
-
-def fill_parameters(node_container, node_prototype):
-    from .utilities import to_mathutils_value
-    for key, value in node_container.parameters.items():
-        node_socket = node_prototype.inputs.get(key)
-        if not node_socket:
-            #maybe the node socket has no name
-            if ( ( len(node_prototype.inputs) == 0) and ( len(node_prototype.outputs) == 1) ):
-                # this is a simple input node.
-                node_socket = node_prototype.outputs[0]
-            elif key == 'Mute':
-                node_container.parameters[key] = node_prototype.mute
-                continue
-            else: # really don't know!
-                raise RuntimeError("No node socket found for " + key + " when filling out node parameters.")
-                continue
-        
-        if node_socket.bl_idname in  ['RelationshipSocket', 'xFormSocket']:
-            continue
-        
-        elif hasattr(node_socket, "default_value"):
-            default_value_type = type(node_socket.default_value)
-            #print (default_value_type)
-            math_val = to_mathutils_value(node_socket)
-            if math_val:
-                node_container.parameters[key] = math_val
-            # maybe we can use it directly.. ?
-            elif ( (default_value_type == str) or (default_value_type == bool) or
-                 (default_value_type == float) or (default_value_type == int) ):
-                node_container.parameters[key] = node_socket.default_value
-            # HACK: there should be no sets, I think, but...
-            elif default_value_type == set:
-                node_container.parameters[key] = node_socket.default_value
-                # TODO: make this make sense sometime in the future!
-                # There should not be any sets!
-            else:
-                raise RuntimeError("No value found for " + key + " when filling out node parameters for " + node_prototype.name)
-        else:
-                print (key, node_socket)
-                # do: remove these from parameters maybe
-                # since they are always None if not connected
-    # for key, value in node_container.parameters.items():
-        # if value:
-            # print (key, value)
-
-def evaluate_input(node_container, input_name):
-    # for simple cases
-    trace = trace_single_line(node_container, input_name)
-    prop = trace[0][-1].parameters.get(trace[1].to_socket)
-    # WHY doesn't this work for the Matrix inputs .. ?
-    
-    return prop
-
-
-def trace_node_lines(node_container):
-    """ Tells the depth of a node within the node tree. """
-    node_lines = []
-    if hasattr(node_container, "inputs"):
-        for key, socket in node_container.inputs.items():
-            # Recrusive search through the tree.
-            #  * checc each relevant input socket in the node
-            #  * for EACH input, find the node it's connected to
-            #    * repeat from here until you get all the lines
-            if ( ( key in ["Relationship", "Parent", "Input Relationship", "Target"])
-                          and (socket.is_connected) ):
-                # it is necesary to check the key because of Link nodes,
-                #   which don't really traverse like normal.
-                # TODO: see if I can refactor this to make it traverse
-                other = socket.from_node
-                if (other):
-                    other_lines = trace_node_lines(other)
-                    if not other_lines:
-                        node_lines.append([other])
-                    for line in other_lines:
-                        node_lines.append( [other] + line )
-    return node_lines
-    
-    
-
-def trace_single_line(node_container, input_name):
-    """ Tells the depth of a node within the node tree. """
-    nodes = [node_container]
-    if hasattr(node_container, "inputs"):
-        # Trace a single line
-        if (socket := node_container.inputs.get(input_name) ):
-            while (socket.is_connected):
-                other = socket.from_node.outputs.get(socket.from_socket)
-                if (other):
-                    socket = other
-                    if socket.can_traverse:
-                        socket = socket.traverse_target
-                        nodes.append(socket.to_node)
-                    else: # this is an output.
-                        nodes.append(socket.from_node)
-                        break
-                else:
-                    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):
-    """ Tells the depth of a node within the node tree. """
-    nodes = [node_container]
-    if hasattr(node_container, "outputs"):
-        # Trace a single line
-        if (socket := node_container.outputs.get(output_name) ):
-            while (socket.is_connected):
-                other = socket.to_node.inputs.get(socket.to_socket)
-                if (other):
-                    socket = other
-                    if socket.can_traverse:
-                        socket = socket.traverse_target
-                        nodes.append(socket.from_node)
-                    else: # this is an input.
-                        nodes.append(socket.to_node)
-                        break
-                else:
-                    break
-    return nodes, socket
-
-def node_depth(node_container):
-    maxlen = 0
-    for nodes in trace_node_lines(node_container):
-        if (len(nodes) > maxlen):
-            maxlen = len(nodes)
-    return maxlen
-        
-def get_parent(node_container):
-    node_line, socket = trace_single_line(node_container, "Relationship")
-    parent_nc = None
-    for i in range(len(node_line)):
-        print (node_line[i])
-        # check each of the possible parent types.
-        if ( isinstance(node_line[ i ], LinkInherit) ):
-            try: # it's the next one
-                return node_line[ i + 1 ]
-            except IndexError: # if there is no next one...
-                return None # then there's no parent!
-    return None
-    # TO DO!
-    #
-    # make this do shorthand parenting - if no parent, then use World
-    #  if the parent node is skipped, use the previous node (an xForm)
-    #  with default settings.
-    # it is OK to generate a new, "fake" node container for this!
-
-def get_target_and_subtarget(node_container, constraint, input_name = "Target"):
-    from bpy.types import PoseBone, Object
-    subtarget = ''; target = node_container.evaluate_input(input_name)
-    if target:
-        if (isinstance(target.bGetObject(), PoseBone)):
-            subtarget = target.bGetObject().name
-            target = target.bGetParentArmature()
-        elif (isinstance(target.bGetObject(), Object) ):
-            target = target.bGetObject()
-        else:
-            raise RuntimeError("Cannot interpret constraint target!")
-    if (input_name == 'Target'): # this is sloppy, but it werks
-        constraint.target, constraint.subtarget = target, subtarget
-    elif (input_name == 'Pole Target'):
-        constraint.pole_target, constraint.pole_subtarget = target, subtarget
-
-
-
-
-
-
-class NodeSocket:
-    # this is not meant to be a particularly robust class
-    # e.g., there will be no disconnect() method since it isn't needed
-    # I just wanna have something persistent (an object)
-    # I'd perfer to use pointers and structs, whatever
-    is_input = False
-    is_connected = False
-    from_node = None
-    to_node = None
-    from_socket = None
-    to_socket = None
-    can_traverse = False
-    traverse_target = None
-    
-    def __init__(self, is_input = False,
-                 from_socket = None, to_socket = None,
-                 from_node = None, to_node = None,
-                 traverse_target = None):
-        self.from_socket = from_socket
-        self.to_socket   = to_socket
-        self.from_node   = from_node
-        self.to_node     = to_node
-        self.is_input    = is_input
-        if (self.is_input and (self.from_node or self.from_socket)):
-            self.is_connected = True
-        elif ( not self.is_input and (self.to_node or self.to_socket)):
-            self.is_connected = True
-        self.set_traverse_target(traverse_target)
-        
-    def connect(self, node, socket):
-        if (self.is_input):
-            self.from_node   = node
-            self.from_socket = socket
-        else:
-            self.to_node   =  node
-            self.to_socket = socket
-        self.is_connected = True
-    
-    def set_traverse_target(self, traverse_target):
-        if (traverse_target):
-            self.traverse_target = traverse_target
-            self.can_traverse = True
-    
-    def __repr__(self):
-        if self.is_input:
-            return ( self.to_node.__repr__() + "::" + self.to_socket )
-        else:
-            return (self.from_node.__repr__() + "::" + self.from_socket)
-    
-    
-
-
-#*#-------------------------------#++#-------------------------------#*#
-# X - F O R M   N O D E S
-#*#-------------------------------#++#-------------------------------#*#
-
-# class xFormNull:
-    # '''A node representing an Empty object'''
-    # inputs =
-    # {
-     # "Name":None,
-     # "Rotation Order":None,
-     # "Matrix":None,
-     # "Relationship":None,
-    # }
-    # outputs =
-    # {
-     # "xFormOut":None,
-    # }
-    # parameters =
-    # {
-     # "Name":None,
-     # "Rotation Order":None,
-     # "Matrix":None,
-     # "Relationship":None,
-    # }
-    
-    # def evaluate_input(self, input):
-        # pass
-    
-    # def instantiate_blender_object(self):
-        # pass
-# for whatever reason, the above isn't implemented yet in the node-tree
-# so I'm not implementing it here, either
-
-
-class xFormRoot:
-    '''A node representing the root of the scene.'''
-    
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.inputs = {}
-        self.outputs = {"xForm Out":NodeSocket(from_socket="xForm Out", from_node = self),}
-        self.parameters = {}
-        self.links = {} # leave this empty for now!
-        self.node_type = 'XFORM'
-    
-    def init_to_node_line(line,):
-        pass
-    
-    def evaluate_input(self, input_name):
-        return "ROOT"
-    
-    def bExecute(self, bContext = None,):
-        pass
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-
-class xFormArmature:
-    '''A node representing an armature object'''
-    
-    bObject = None
-    
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.executed = False
-        self.signature = signature
-        self.inputs = {
-         "Name"           : NodeSocket(is_input = True, to_socket = "Name", to_node = self),
-         "Rotation Order" : NodeSocket(is_input = True, to_socket = "Rotation Order", to_node = self),
-         "Matrix"         : NodeSocket(is_input = True, to_socket = "Matrix", to_node = self),
-         "Relationship"   : NodeSocket(is_input = True, to_socket = "Relationship", to_node = self),
-        }
-        self.outputs = {
-         "xForm Out" : NodeSocket(from_socket="xForm Out", from_node = self),
-        }
-        self.parameters = {
-         "Name":None,
-         "Rotation Order":None,
-         "Matrix":None,
-         "Relationship":None,
-        }
-        self.links = {} # leave this empty for now!
-        # now set up the traverse target...
-        self.inputs["Relationship"].set_traverse_target(self.outputs["xForm Out"])
-        self.outputs["xForm Out"].set_traverse_target(self.inputs["Relationship"])
-        self.node_type = 'XFORM'
-    
-        
-    def evaluate_input(self, input_name):
-        return evaluate_input(self, input_name)
-    
-    def bExecute(self, bContext = None,):
-        from .utilities import get_node_prototype
-        
-        import bpy
-        if (not isinstance(bContext, bpy.types.Context)):
-            raise RuntimeError("Incorrect context")
-
-        name = self.evaluate_input("Name")
-        matrix = self.evaluate_input('Matrix')
-
-
-        #check if an object by the name exists
-        if (name) and (ob := bpy.data.objects.get(name)):
-            for pb in ob.pose.bones:
-                # clear it, even after deleting the edit bones, 
-                #  if we create them again the pose bones will be reused
-                while (pb.constraints):
-                    pb.constraints.remove(pb.constraints[-1])
-                pb.location = (0,0,0)
-                pb.rotation_euler = (0,0,0)
-                pb.rotation_quaternion = (1.0,0,0,0)
-                pb.rotation_axis_angle = (0,0,1.0,0)
-                pb.scale = (1.0,1.0,1.0)
-        else:
-            # Create the Object
-            ob = bpy.data.objects.new(name, bpy.data.armatures.new(name)) #create ob
-            if (ob.name != name):
-                raise RuntimeError("Could not create xForm object", name)
-            
-        self.bObject = ob.name
-        
-        ob.matrix_world = matrix
-        
-        
-        # first, get the parent object
-        parent_node = get_parent(self)
-        if hasattr(parent_node, "bObject"):
-            # this won't work of course, TODO
-            self.bObject.parent = parent_node.bObject
-        
-        # Link to Scene:
-        if (ob.name not in bContext.view_layer.active_layer_collection.collection.objects):
-            bContext.view_layer.active_layer_collection.collection.objects.link(ob)
-        #self.bParent(bContext)
-        
-        # Finalize the action
-        # prevAct = bContext.view_layer.objects.active
-        bContext.view_layer.objects.active = ob
-        bpy.ops.object.mode_set(mode='EDIT')
-        print ("Changing Armature Mode to EDIT")
-        # clear it
-        while (len(ob.data.edit_bones) > 0):
-            ob.data.edit_bones.remove(ob.data.edit_bones[0])
-        # bContext.view_layer.objects.active = prevAct
-
-        print ("Created Armature object: \""+ ob.name +"\"")
-        
-        
-        self.executed = True
-    
-    # # not used yet
-    # #
-    # def bFinalize(self, bContext = None):
-        # import bpy
-        # ob = self.bGetObject()
-        # prevAct = bContext.view_layer.objects.active
-        # bContext.view_layer.objects.active = ob
-        # bpy.ops.object.mode_set(mode='OBJECT')
-        # print ("Changing Armature Mode to OBJECT")
-        # bContext.view_layer.objects.active = prevAct
-
-    def bGetObject(self, mode = ''):
-        import bpy
-        return bpy.data.objects[self.bObject]
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-
-class xFormBone:
-    '''A node representing a bone in an armature'''
-    # DO: make a way to identify which armature this belongs to
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.executed = False
-        self.signature = signature
-        self.inputs = {
-         "Name"           : NodeSocket(is_input = True, to_socket = "Name", to_node = self,),
-         "Rotation Order" : NodeSocket(is_input = True, to_socket = "Rotation Order", to_node = self,),
-         "Matrix"         : NodeSocket(is_input = True, to_socket = "Matrix", to_node = self,),
-         "Relationship"   : NodeSocket(is_input = True, to_socket = "Relationship", to_node = self,),
-         # IK settings
-         "IK Stretch"     : NodeSocket(is_input = True, to_socket = "IK Stretch", to_node = self,),
-         "Lock IK"        : NodeSocket(is_input = True, to_socket = "Lock IK", to_node = self,),
-         "IK Stiffness"   : NodeSocket(is_input = True, to_socket = "IK Stiffness", to_node = self,),
-         "Limit IK"       : NodeSocket(is_input = True, to_socket = "Limit IK", to_node = self,),
-         "X Min"          : NodeSocket(is_input = True, to_socket = "X Min", to_node = self,),
-         "X Max"          : NodeSocket(is_input = True, to_socket = "X Max", to_node = self,),
-         "Y Min"          : NodeSocket(is_input = True, to_socket = "Y Min", to_node = self,),
-         "Y Max"          : NodeSocket(is_input = True, to_socket = "Y Max", to_node = self,),
-         "Z Min"          : NodeSocket(is_input = True, to_socket = "Z Min", to_node = self,),
-         "Z Max"          : NodeSocket(is_input = True, to_socket = "Z Max", to_node = self,),
-        }
-        self.outputs = {
-         "xForm Out"       : NodeSocket(from_socket = "xForm Out", from_node = self),
-        }
-        self.parameters = {
-         "Name":None,
-         "Rotation Order":None,
-         "Matrix":None,
-         "Relationship":None,
-         # IK settings
-         "IK Stretch":None,
-         "Lock IK":None,
-         "IK Stiffness":None,
-         "Limit IK":None,
-         "X Min":None,
-         "X Max":None,
-         "Y Min":None,
-         "Y Max":None,
-         "Z Min":None,
-         "Z Max":None,
-        }
-        self.links = {} # leave this empty for now!
-        # now set up the traverse target...
-        self.inputs["Relationship"].set_traverse_target(self.outputs["xForm Out"])
-        self.outputs["xForm Out"].set_traverse_target(self.inputs["Relationship"])
-        self.node_type = 'XFORM'
-        
-    def evaluate_input(self, input_name):
-        return evaluate_input(self, input_name)
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-    
-    def bGetParentArmature(self):
-        finished = False
-        if (trace := trace_single_line(self, "Relationship")[0] ) :
-            for i in range(len(trace)):
-                # have to look in reverse, actually
-                if ( isinstance(trace[ i ], xFormArmature ) ):
-                    return trace[ i ].bGetObject()
-        return None
-        #should do the trick...
-    
-    def bSetParent(self, eb):
-        from bpy.types import EditBone
-        parent_nc = get_parent(self)
-        parent = parent_nc.bGetObject(mode = 'EDIT')
-        if isinstance(parent, EditBone):
-            print (parent.name)
-            eb.parent = parent
-        else:
-            print(parent)
-        # otherwise, no need to do anything.
-        
-         
-    def bExecute(self, bContext = None,): #possibly will need to pass context?
-        import bpy
-        from mathutils import Vector
-        if (not isinstance(bContext, bpy.types.Context)):
-            raise RuntimeError("Incorrect context")
-        xF = self.bGetParentArmature()
-        
-        name = self.evaluate_input("Name")
-        matrix = self.evaluate_input("Matrix")
-        
-        length = matrix[3][3]
-        matrix[3][3] = 1.0 # set this bacc, could cause problems otherwise.
-        
-        if (xF):
-            if (xF.mode != "EDIT"):
-                raise RuntimeError("Armature Object Not in Edit Mode, exiting...")
-        else:
-            raise RuntimeError("No armature object to add bone to.")
-        #
-        # Create the Object
-        d = xF.data
-        eb = d.edit_bones.new(name)
-        
-        if (eb.name != name):
-            raise RuntimeError("Could not create bone ", name, "; Perhaps there is a duplicate bone name in the node tree?")
-        eb.matrix  = matrix.copy()
-        tailoffset = Vector((0,length,0)) #Vector((0,self.tailoffset, 0))
-        tailoffset = matrix.copy().to_3x3() @ tailoffset
-        eb.tail    = eb.head + tailoffset
-        
-        if (eb.name != name):
-            raise RuntimeError("Could not create edit bone: ", name)
-        self.bObject = eb.name
-        # The bone should have relationships going in at this point.
-        
-        
-            
-        self.bSetParent(eb)
-        
-        
-        return
-        self.bParent(bContext)
-
-        print ("Created Bone: \""+ eb.name+ "\" in \"" + self.bGetParentArmature().name +"\"")
-        self.executed = True
-
-    def bFinalize(self, bContext = None):
-        # prevAct = bContext.view_layer.objects.active
-        # bContext.view_layer.objects.active = ob
-        # bpy.ops.object.mode_set(mode='OBJECT')
-        # bContext.view_layer.objects.active = prevAct
-        #
-        #get relationship
-        # ensure we have a pose bone...
-        # set the ik parameters
-        pass
-
-
-    def bGetObject(self, mode = 'POSE'):
-        if (mode == 'EDIT'):
-            try:
-                return self.bGetParentArmature().data.edit_bones[self.bObject]
-            except KeyError:
-                return None
-        if (mode == 'OBJECT'):
-            try:
-                return self.bGetParentArmature().data.bones[self.bObject]
-            except KeyError:
-                return None
-        if (mode == 'POSE'):
-            try:
-                return self.bGetParentArmature().pose.bones[self.bObject]
-            except KeyError:
-                return None
-    
-        
-        
-
-#*#-------------------------------#++#-------------------------------#*#
-# L I N K   N O D E S
-#*#-------------------------------#++#-------------------------------#*#
-
-class LinkInherit:
-    '''A node representing inheritance'''
-    
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.inputs = {
-         "Parent"           : NodeSocket(is_input = True, to_socket = "Parent", to_node = self,),
-         # bone only:
-         "Inherit Rotation" : NodeSocket(is_input = True, to_socket = "Inherit Rotation", to_node = self,),
-         "Inherit Scale"    : NodeSocket(is_input = True, to_socket = "Inherit Scale", to_node = self,),
-         "Connected"        : NodeSocket(is_input = True, to_socket = "Connected", to_node = self,),
-        }
-        self.outputs = { "Inheritance" : NodeSocket(from_socket = "Inheritance", from_node = self) }
-        self.parameters = {
-         "Parent":None,
-         # bone only:
-         "Inherit Rotation":None,
-         "Inherit Scale":None,
-         "Connected":None,
-         "Mute":None,
-        }
-        self.links = {} # leave this empty for now!
-        # now set up the traverse target...
-        self.inputs["Parent"].set_traverse_target(self.outputs["Inheritance"])
-        self.outputs["Inheritance"].set_traverse_target(self.inputs["Parent"])
-        self.node_type = 'LINK'
-    
-        
-    def evaluate_input(self, input_name):
-        return evaluate_input(self, input_name)
-    
-    def bExecute(self, bContext = None,):
-        # this is handled by the xForm objects, since it isn't really
-        #  a constraint.
-        pass
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-
-
-class LinkCopyLocation:
-    '''A node representing Copy Location'''
-    
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.inputs = {
-            "Input Relationship" : NodeSocket(is_input = True, to_socket = "Input Relationship", to_node = self,),
-            "Head/Tail"          : NodeSocket(is_input = True, to_socket = "Head/Tail", to_node = self,),
-            "UseBBone"           : NodeSocket(is_input = True, to_socket = "UseBBone", to_node = self,),
-            "Axes"               : NodeSocket(is_input = True, to_socket = "Axes", to_node = self,),
-            "Invert"             : NodeSocket(is_input = True, to_socket = "Invert", to_node = self,),
-            "Target Space"       : NodeSocket(is_input = True, to_socket = "Target Space", to_node = self,),
-            "Owner Space"        : NodeSocket(is_input = True, to_socket = "Owner Space", to_node = self,),
-            "Influence"          : NodeSocket(is_input = True, to_socket = "Influence", to_node = self,),
-            "Target"             : NodeSocket(is_input = True, to_socket = "Target", to_node = self,), }
-        self.outputs = {
-            "Output Relationship" : NodeSocket(from_socket = "Output Relationship", from_node=self) }
-        self.parameters = {
-            "Input Relationship":None,
-            "Head/Tail":None,
-            "UseBBone":None,
-            "Axes":None,
-            "Invert":None,
-            "Target Space":None,
-            "Owner Space":None,
-            "Influence":None,
-            "Target":None,
-            "Mute":None, }
-        # now set up the traverse target...
-        self.inputs["Input Relationship"].set_traverse_target(self.outputs["Output Relationship"])
-        self.outputs["Output Relationship"].set_traverse_target(self.inputs["Input Relationship"])
-        self.node_type = 'LINK'
-        
-        
-        
-    def evaluate_input(self, input_name):
-        if (input_name == 'Target'):            
-            socket = self.inputs.get(input_name)
-            return socket.from_node
-            
-        else:
-            return evaluate_input(self, input_name)
-    
-    
-    
-    def GetxForm(self):
-        # I don't think I have a function for getting children yet!
-        trace = trace_single_line_up(self, "Output Relationship")
-        for node in trace[0]:
-            if (node.__class__ in [xFormRoot, xFormArmature, xFormBone]):
-                return node
-        return None
-
-
-
-    def bExecute(self, context):
-        
-        c = self.GetxForm().bGetObject().constraints.new('COPY_LOCATION')
-        get_target_and_subtarget(self, c)
-        c.head_tail = self.evaluate_input("Head/Tail")
-        c.use_bbone_shape = self.evaluate_input("UseBBone")
-
-        c.owner_space = self.evaluate_input("Owner Space")
-        c.target_space = self.evaluate_input("Target Space")
-        c.invert_x = self.evaluate_input("Invert")[0]
-        c.invert_y = self.evaluate_input("Invert")[1]
-        c.invert_z = self.evaluate_input("Invert")[2]
-        
-        c.use_x = self.evaluate_input("Axes")[0]
-        c.use_y = self.evaluate_input("Axes")[1]
-        c.use_z = self.evaluate_input("Axes")[2]
-        c.influence = self.evaluate_input("Influence")
-        print ("Creating Copy Location Constraint for bone: \""+ self.GetxForm().bGetObject().name + "\"")
-        
-        if self.parameters["Mute"]:
-            c.enabled = False
-
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-        
-
-class LinkCopyRotation:
-    '''A node representing Copy Rotation'''
-    
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.inputs = {
-            "Input Relationship" : NodeSocket(is_input = True, to_socket = "Input Relationship", to_node = self,),
-            "RotationOrder"      : NodeSocket(is_input = True, to_socket = "RotationOrder", to_node = self,),
-            "Rotation Mix"       : NodeSocket(is_input = True, to_socket = "Rotation Mix", to_node = self,),
-            "Axes"               : NodeSocket(is_input = True, to_socket = "Axes", to_node = self,),
-            "Invert"             : NodeSocket(is_input = True, to_socket = "Invert", to_node = self,),
-            "Target Space"       : NodeSocket(is_input = True, to_socket = "Target Space", to_node = self,),
-            "Owner Space"        : NodeSocket(is_input = True, to_socket = "Owner Space", to_node = self,),
-            "Influence"          : NodeSocket(is_input = True, to_socket = "Influence", to_node = self,),
-            "Target"             : NodeSocket(is_input = True, to_socket = "Target", to_node = self,), }
-        self.outputs = {
-            "Output Relationship" : NodeSocket(from_socket = "Output Relationship", from_node=self) }
-        self.parameters = {
-            "Input Relationship":None,
-            "RotationOrder":None,
-            "Rotation Mix":None,
-            "Axes":None,
-            "Invert":None,
-            "Target Space":None,
-            "Owner Space":None,
-            "Influence":None,
-            "Target":None,
-            "Mute":None, }
-        # now set up the traverse target...
-        self.inputs["Input Relationship"].set_traverse_target(self.outputs["Output Relationship"])
-        self.outputs["Output Relationship"].set_traverse_target(self.inputs["Input Relationship"])
-        self.node_type = 'LINK'
-        
-    def evaluate_input(self, input_name):
-        if (input_name == 'Target'):            
-            socket = self.inputs.get(input_name)
-            return socket.from_node
-            
-        else:
-            return evaluate_input(self, input_name)
-    
-    def GetxForm(self):
-        # I don't think I have a function for getting children yet!
-        trace = trace_single_line_up(self, "Output Relationship")
-        for node in trace[0]:
-            if (node.__class__ in [xFormRoot, xFormArmature, xFormBone]):
-                return node
-        return None
-
-    def bExecute(self, context):
-        
-        c = self.GetxForm().bGetObject().constraints.new('COPY_ROTATION')
-        
-        get_target_and_subtarget(self, c)
-        
-        rotation_order = self.evaluate_input("RotationOrder")
-        
-        
-        if ((rotation_order == 'QUATERNION') or (rotation_order == 'AXIS_ANGLE')):
-            c.euler_order = 'AUTO'
-        else:
-            c.euler_order = rotation_order
-        
-        #c.mix_mode = self.evaluate_input("Rotation Mix")
-        # kek, deal with this later
-        # TODO HACK
-        # dumb enums
-
-        c.owner_space = self.evaluate_input("Owner Space")
-        c.target_space = self.evaluate_input("Target Space")
-        c.invert_x = self.evaluate_input("Invert")[0]
-        c.invert_y = self.evaluate_input("Invert")[1]
-        c.invert_z = self.evaluate_input("Invert")[2]
-        
-        c.use_x = self.evaluate_input("Axes")[0]
-        c.use_y = self.evaluate_input("Axes")[1]
-        c.use_z = self.evaluate_input("Axes")[2]
-        c.influence = self.evaluate_input("Influence")
-        print ("Creating Copy Rotation Constraint for bone: \""+ self.GetxForm().bGetObject().name + "\"")
-        
-        if self.parameters["Mute"]:
-            c.enabled = False
-
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-        
-class LinkCopyScale:
-    '''A node representing Copy Scale'''
-    
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.inputs = {
-            "Input Relationship" : NodeSocket(is_input = True, to_socket = "Input Relationship", to_node = self,),
-            "Offset"             : NodeSocket(is_input = True, to_socket = "Offset", to_node = self,),
-            "Average"            : NodeSocket(is_input = True, to_socket = "Average", to_node = self,),
-            "Additive"           : NodeSocket(is_input = True, to_socket = "Additive", to_node = self,),
-            "Axes"               : NodeSocket(is_input = True, to_socket = "Axes", to_node = self,),
-            #"Invert"             : NodeSocket(is_input = True, to_socket = "Invert", to_node = self,),
-            "Target Space"       : NodeSocket(is_input = True, to_socket = "Target Space", to_node = self,),
-            "Owner Space"        : NodeSocket(is_input = True, to_socket = "Owner Space", to_node = self,),
-            "Influence"          : NodeSocket(is_input = True, to_socket = "Influence", to_node = self,),
-            "Target"             : NodeSocket(is_input = True, to_socket = "Target", to_node = self,), }
-        self.outputs = {
-            "Output Relationship" : NodeSocket(from_socket = "Output Relationship", from_node=self) }
-        self.parameters = {
-            "Input Relationship":None,
-            "Offset":None,
-            "Average":None,
-            "Axes":None,
-            #"Invert":None,
-            "Target Space":None,
-            "Owner Space":None,
-            "Influence":None,
-            "Target":None,
-            "Mute":None,}
-        # now set up the traverse target...
-        self.inputs["Input Relationship"].set_traverse_target(self.outputs["Output Relationship"])
-        self.outputs["Output Relationship"].set_traverse_target(self.inputs["Input Relationship"])
-        self.node_type = 'LINK'
-        
-    def evaluate_input(self, input_name):
-        if (input_name == 'Target'):            
-            socket = self.inputs.get(input_name)
-            return socket.from_node
-            
-        else:
-            return evaluate_input(self, input_name)
-    
-    def GetxForm(self):
-        trace = trace_single_line_up(self, "Output Relationship")
-        for node in trace[0]:
-            if (node.__class__ in [xFormRoot, xFormArmature, xFormBone]):
-                return node
-        return None
-
-    def bExecute(self, context):
-        
-        c = self.GetxForm().bGetObject().constraints.new('COPY_SCALE')
-        
-        get_target_and_subtarget(self, c)
-        
-        c.use_offset       = self.evaluate_input("Offset")
-        c.use_make_uniform = self.evaluate_input("Average")
-
-        c.owner_space = self.evaluate_input("Owner Space")
-        c.target_space = self.evaluate_input("Target Space")
-        c.use_x = self.evaluate_input("Axes")[0]
-        c.use_y = self.evaluate_input("Axes")[1]
-        c.use_z = self.evaluate_input("Axes")[2]
-        c.influence = self.evaluate_input("Influence")
-        print ("Creating Copy Location Constraint for bone: \""+ self.GetxForm().bGetObject().name + "\"")
-        
-        if self.parameters["Mute"]:
-            c.enabled = False
-
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-
-class LinkCopyTransforms:
-    '''A node representing Copy Transfoms'''
-    
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.inputs = {
-            "Input Relationship" : NodeSocket(is_input = True, to_socket = "Input Relationship", to_node = self,),
-            "Head/Tail"          : NodeSocket(is_input = True, to_socket = "Head/Tail", to_node = self,),
-            "UseBBone"           : NodeSocket(is_input = True, to_socket = "UseBBone", to_node = self,),
-            "Additive"           : NodeSocket(is_input = True, to_socket = "Additive", to_node = self,),
-            "Mix"                : NodeSocket(is_input = True, to_socket = "Mix", to_node = self,),
-            "Target Space"       : NodeSocket(is_input = True, to_socket = "Target Space", to_node = self,),
-            "Owner Space"        : NodeSocket(is_input = True, to_socket = "Owner Space", to_node = self,),
-            "Influence"          : NodeSocket(is_input = True, to_socket = "Influence", to_node = self,),
-            "Target"             : NodeSocket(is_input = True, to_socket = "Target", to_node = self,), }
-        self.outputs = {
-            "Output Relationship" : NodeSocket(from_socket = "Output Relationship", from_node=self) }
-        self.parameters = {
-            "Input Relationship":None,
-            "Head/Tail":None,
-            "UseBBone":None,
-            "Mix":None,
-            "Target Space":None,
-            "Owner Space":None,
-            "Influence":None,
-            "Target":None,
-            "Mute":None,}
-        # now set up the traverse target...
-        self.inputs["Input Relationship"].set_traverse_target(self.outputs["Output Relationship"])
-        self.outputs["Output Relationship"].set_traverse_target(self.inputs["Input Relationship"])
-        self.node_type = 'LINK'
-        
-    def evaluate_input(self, input_name):
-        if (input_name == 'Target'):            
-            socket = self.inputs.get(input_name)
-            return socket.from_node
-            
-        else:
-            return evaluate_input(self, input_name)
-    
-    def GetxForm(self):
-        trace = trace_single_line_up(self, "Output Relationship")
-        for node in trace[0]:
-            if (node.__class__ in [xFormRoot, xFormArmature, xFormBone]):
-                return node
-        return None
-
-    def bExecute(self, context):
-        
-        c = self.GetxForm().bGetObject().constraints.new('COPY_TRANSFORMS')
-        
-        get_target_and_subtarget(self, c)
-        
-        c.head_tail       = self.evaluate_input("Head/Tail")
-        c.use_bbone_shape = self.evaluate_input("UseBBone")
-        c.mix_mode = self.evaluate_input("Mix")
-
-        c.owner_space = self.evaluate_input("Owner Space")
-        c.target_space = self.evaluate_input("Target Space")
-        c.influence = self.evaluate_input("Influence")
-        print ("Creating Copy Transforms Constraint for bone: \""+ self.GetxForm().bGetObject().name + "\"")
-        
-        if self.parameters["Mute"]:
-            c.enabled = False
-
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-
-class LinkLimitLocation:
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.inputs = {
-            "Input Relationship" : NodeSocket(is_input = True, to_socket = "Input Relationship", to_node = self,),
-            "Use Max X"          : NodeSocket(is_input = True, to_socket = "Use Max X", to_node = self,),
-            "Max X"              : NodeSocket(is_input = True, to_socket = "Max X", to_node = self,),
-            "Use Max Y"          : NodeSocket(is_input = True, to_socket = "Use Max Y", to_node = self,),
-            "Max Y"              : NodeSocket(is_input = True, to_socket = "Max Y", to_node = self,),
-            "Use Max Z"          : NodeSocket(is_input = True, to_socket = "Use Max Z", to_node = self,),
-            "Max Z"              : NodeSocket(is_input = True, to_socket = "Max Z", to_node = self,),
-            "Use Min X"          : NodeSocket(is_input = True, to_socket = "Use Min X", to_node = self,),
-            "Min X"              : NodeSocket(is_input = True, to_socket = "Min X", to_node = self,),
-            "Use Min Y"          : NodeSocket(is_input = True, to_socket = "Use Min Y", to_node = self,),
-            "Min Y"              : NodeSocket(is_input = True, to_socket = "Min Y", to_node = self,),
-            "Use Min Z"          : NodeSocket(is_input = True, to_socket = "Use Min Z", to_node = self,),
-            "Min Z"              : NodeSocket(is_input = True, to_socket = "Min Z", to_node = self,),
-            "Affect Transform"   : NodeSocket(is_input = True, to_socket = "Affect Transform", to_node = self,),
-            "Owner Space"        : NodeSocket(is_input = True, to_socket = "Owner Space", to_node = self,),
-            "Influence"          : NodeSocket(is_input = True, to_socket = "Influence", to_node = self,), }
-        self.outputs = {
-            "Output Relationship" : NodeSocket(from_socket = "Output Relationship", from_node=self) }
-        self.parameters = {
-            "Input Relationship":None,
-            "Use Max X":None,
-            "Max X":None,
-            "Use Max Y":None,
-            "Max Y":None,
-            "Use Max Z":None,
-            "Max Z":None,
-            "Use Min X":None,
-            "Min X":None,
-            "Use Min Y":None,
-            "Min Y":None,
-            "Use Min Z":None,
-            "Min Z":None,
-            "Affect Transform":None,
-            "Owner Space":None,
-            "Influence":None,
-            "Mute":None,}
-        # now set up the traverse target...
-        self.inputs["Input Relationship"].set_traverse_target(self.outputs["Output Relationship"])
-        self.outputs["Output Relationship"].set_traverse_target(self.inputs["Input Relationship"])
-        self.node_type = 'LINK'
-        
-    def evaluate_input(self, input_name):
-        return evaluate_input(self, input_name)
-    
-    def GetxForm(self):
-        trace = trace_single_line_up(self, "Output Relationship")
-        for node in trace[0]:
-            if (node.__class__ in [xFormRoot, xFormArmature, xFormBone]):
-                return node
-        return None
-
-    def bExecute(self, context):
-        c = self.GetxForm().bGetObject().constraints.new('LIMIT_LOCATION')
-        #
-        c.max_x       = self.evaluate_input("Max X")
-        c.max_y       = self.evaluate_input("Max Y")
-        c.max_z       = self.evaluate_input("Max Z")
-        #
-        c.min_x       = self.evaluate_input("Min X")
-        c.min_y       = self.evaluate_input("Min Y")
-        c.min_z       = self.evaluate_input("Min Z")
-        
-        c.use_max_x       = self.evaluate_input("Use Max X")
-        c.use_max_y       = self.evaluate_input("Use Max Y")
-        c.use_max_z       = self.evaluate_input("Use Max Z")
-        #
-        c.use_min_x       = self.evaluate_input("Use Min X")
-        c.use_min_y       = self.evaluate_input("Use Min Y")
-        c.use_min_z       = self.evaluate_input("Use Min Z")
-        
-        c.use_transform_limit = self.evaluate_input("Affect Transform")
-
-        c.owner_space = self.evaluate_input("Owner Space")
-        c.influence = self.evaluate_input("Influence")
-        print ("Creating Limit Location Constraint for bone: \""+ self.GetxForm().bGetObject().name + "\"")
-        
-        if self.parameters["Mute"]:
-            c.enabled = False
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-        
-class LinkLimitRotation:
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.inputs = {
-            "Input Relationship" : NodeSocket(is_input = True, to_socket = "Input Relationship", to_node = self,),
-            "Use X"              : NodeSocket(is_input = True, to_socket = "Use X", to_node = self,),
-            "Use Y"              : NodeSocket(is_input = True, to_socket = "Use Y", to_node = self,),
-            "Use Z"              : NodeSocket(is_input = True, to_socket = "Use Z", to_node = self,),
-            "Max X"              : NodeSocket(is_input = True, to_socket = "Max X", to_node = self,),
-            "Max Y"              : NodeSocket(is_input = True, to_socket = "Max Y", to_node = self,),
-            "Max Z"              : NodeSocket(is_input = True, to_socket = "Max Z", to_node = self,),
-            "Min X"              : NodeSocket(is_input = True, to_socket = "Min X", to_node = self,),
-            "Min Y"              : NodeSocket(is_input = True, to_socket = "Min Y", to_node = self,),
-            "Min Z"              : NodeSocket(is_input = True, to_socket = "Min Z", to_node = self,),
-            "Affect Transform"   : NodeSocket(is_input = True, to_socket = "Affect Transform", to_node = self,),
-            "Owner Space"        : NodeSocket(is_input = True, to_socket = "Owner Space", to_node = self,),
-            "Influence"          : NodeSocket(is_input = True, to_socket = "Influence", to_node = self,), }
-        self.outputs = {
-            "Output Relationship" : NodeSocket(from_socket = "Output Relationship", from_node=self) }
-        self.parameters = {
-            "Input Relationship":None,
-            "Use X":None,
-            "Use Y":None,
-            "Use Z":None,
-            "Max X":None,
-            "Max Y":None,
-            "Max Z":None,
-            "Min X":None,
-            "Min Y":None,
-            "Min Z":None,
-            "Affect Transform":None,
-            "Owner Space":None,
-            "Influence":None,
-            "Mute":None,}
-        # now set up the traverse target...
-        self.inputs["Input Relationship"].set_traverse_target(self.outputs["Output Relationship"])
-        self.outputs["Output Relationship"].set_traverse_target(self.inputs["Input Relationship"])
-        self.node_type = 'LINK'
-        
-    def evaluate_input(self, input_name):
-        return evaluate_input(self, input_name)
-    
-    def GetxForm(self):
-        trace = trace_single_line_up(self, "Output Relationship")
-        for node in trace[0]:
-            if (node.__class__ in [xFormRoot, xFormArmature, xFormBone]):
-                return node
-        return None
-
-    def bExecute(self, context):
-        c = self.GetxForm().bGetObject().constraints.new('LIMIT_ROTATION')
-        #
-        c.max_x       = self.evaluate_input("Max X")
-        c.max_y       = self.evaluate_input("Max Y")
-        c.max_z       = self.evaluate_input("Max Z")
-        #
-        c.min_x       = self.evaluate_input("Min X")
-        c.min_y       = self.evaluate_input("Min Y")
-        c.min_z       = self.evaluate_input("Min Z")
-        #
-        c.use_limit_x       = self.evaluate_input("Use X")
-        c.use_limit_y       = self.evaluate_input("Use Y")
-        c.use_limit_z       = self.evaluate_input("Use Z")
-        #
-        c.use_transform_limit = self.evaluate_input("Affect Transform")
-        #
-        c.owner_space = self.evaluate_input("Owner Space")
-        c.influence = self.evaluate_input("Influence")
-        print ("Creating Limit Rotation Constraint for bone: \""+ self.GetxForm().bGetObject().name + "\"")
-        
-        if self.parameters["Mute"]:
-            c.enabled = False
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-        
-class LinkLimitScale:
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.inputs = {
-            "Input Relationship" : NodeSocket(is_input = True, to_socket = "Input Relationship", to_node = self,),
-            "Use Max X"          : NodeSocket(is_input = True, to_socket = "Use Max X", to_node = self,),
-            "Max X"              : NodeSocket(is_input = True, to_socket = "Max X", to_node = self,),
-            "Use Max Y"          : NodeSocket(is_input = True, to_socket = "Use Max Y", to_node = self,),
-            "Max Y"              : NodeSocket(is_input = True, to_socket = "Max Y", to_node = self,),
-            "Use Max Z"          : NodeSocket(is_input = True, to_socket = "Use Max Z", to_node = self,),
-            "Max Z"              : NodeSocket(is_input = True, to_socket = "Max Z", to_node = self,),
-            "Use Min X"          : NodeSocket(is_input = True, to_socket = "Use Min X", to_node = self,),
-            "Min X"              : NodeSocket(is_input = True, to_socket = "Min X", to_node = self,),
-            "Use Min Y"          : NodeSocket(is_input = True, to_socket = "Use Min Y", to_node = self,),
-            "Min Y"              : NodeSocket(is_input = True, to_socket = "Min Y", to_node = self,),
-            "Use Min Z"          : NodeSocket(is_input = True, to_socket = "Use Min Z", to_node = self,),
-            "Min Z"              : NodeSocket(is_input = True, to_socket = "Min Z", to_node = self,),
-            "Affect Transform"   : NodeSocket(is_input = True, to_socket = "Affect Transform", to_node = self,),
-            "Owner Space"        : NodeSocket(is_input = True, to_socket = "Owner Space", to_node = self,),
-            "Influence"          : NodeSocket(is_input = True, to_socket = "Influence", to_node = self,), }
-        self.outputs = {
-            "Output Relationship" : NodeSocket(from_socket = "Output Relationship", from_node=self) }
-        self.parameters = {
-            "Input Relationship":None,
-            "Use Max X":None,
-            "Max X":None,
-            "Use Max Y":None,
-            "Max Y":None,
-            "Use Max Z":None,
-            "Max Z":None,
-            "Use Min X":None,
-            "Min X":None,
-            "Use Min Y":None,
-            "Min Y":None,
-            "Use Min Z":None,
-            "Min Z":None,
-            "Affect Transform":None,
-            "Owner Space":None,
-            "Influence":None,
-            "Mute":None,}
-        # now set up the traverse target...
-        self.inputs["Input Relationship"].set_traverse_target(self.outputs["Output Relationship"])
-        self.outputs["Output Relationship"].set_traverse_target(self.inputs["Input Relationship"])
-        self.node_type = 'LINK'
-        
-    def evaluate_input(self, input_name):
-        return evaluate_input(self, input_name)
-    
-    def GetxForm(self):
-        trace = trace_single_line_up(self, "Output Relationship")
-        for node in trace[0]:
-            if (node.__class__ in [xFormRoot, xFormArmature, xFormBone]):
-                return node
-        return None
-
-    def bExecute(self, context):
-        c = self.GetxForm().bGetObject().constraints.new('LIMIT_SCALE')
-        #
-        c.max_x       = self.evaluate_input("Max X")
-        c.max_y       = self.evaluate_input("Max Y")
-        c.max_z       = self.evaluate_input("Max Z")
-        #
-        c.min_x       = self.evaluate_input("Min X")
-        c.min_y       = self.evaluate_input("Min Y")
-        c.min_z       = self.evaluate_input("Min Z")
-        
-        c.use_max_x       = self.evaluate_input("Use Max X")
-        c.use_max_y       = self.evaluate_input("Use Max Y")
-        c.use_max_z       = self.evaluate_input("Use Max Z")
-        #
-        c.use_min_x       = self.evaluate_input("Use Min X")
-        c.use_min_y       = self.evaluate_input("Use Min Y")
-        c.use_min_z       = self.evaluate_input("Use Min Z")
-        
-        c.use_transform_limit = self.evaluate_input("Affect Transform")
-
-        c.owner_space = self.evaluate_input("Owner Space")
-        c.influence = self.evaluate_input("Influence")
-        print ("Creating Limit Scale Constraint for bone: \""+ self.GetxForm().bGetObject().name + "\"")
-        
-        if self.parameters["Mute"]:
-            c.enabled = False
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-        
-class LinkLimitDistance:
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.inputs = {
-            "Input Relationship" : NodeSocket(is_input = True, to_socket = "Input Relationship", to_node = self,),
-            "Head/Tail"          : NodeSocket(is_input = True, to_socket = "Head/Tail", to_node = self,),
-            "UseBBone"           : NodeSocket(is_input = True, to_socket = "UseBBone", to_node = self,),
-            "Distance"           : NodeSocket(is_input = True, to_socket = "Distance", to_node = self,),
-            "Clamp Region"       : NodeSocket(is_input = True, to_socket = "Clamp Region", to_node = self,),
-            "Affect Transform"   : NodeSocket(is_input = True, to_socket = "Affect Transform", to_node = self,),
-            "Owner Space"        : NodeSocket(is_input = True, to_socket = "Owner Space", to_node = self,),
-            "Target Space"       : NodeSocket(is_input = True, to_socket = "Target Space", to_node = self,),
-            "Influence"          : NodeSocket(is_input = True, to_socket = "Influence", to_node = self,),
-            "Target"             : NodeSocket(is_input = True, to_socket = "Target", to_node = self,), }
-        self.outputs = {
-            "Output Relationship" : NodeSocket(from_socket = "Output Relationship", from_node=self) }
-        self.parameters = {
-            "Input Relationship":None,
-            "Head/Tail":None,
-            "UseBBone":None,
-            "Distance":None,
-            "Clamp Region":None,
-            "Affect Transform":None,
-            "Owner Space":None,
-            "Target Space":None,
-            "Influence":None,
-            "Target":None,
-            "Mute":None,}
-        # now set up the traverse target...
-        self.inputs["Input Relationship"].set_traverse_target(self.outputs["Output Relationship"])
-        self.outputs["Output Relationship"].set_traverse_target(self.inputs["Input Relationship"])
-        self.node_type = 'LINK'
-        
-    def evaluate_input(self, input_name):
-        if (input_name == 'Target'):            
-            socket = self.inputs.get(input_name)
-            return socket.from_node
-            
-        else:
-            return evaluate_input(self, input_name)
-
-    def GetxForm(self):
-        trace = trace_single_line_up(self, "Output Relationship")
-        for node in trace[0]:
-            if (node.__class__ in [xFormRoot, xFormArmature, xFormBone]):
-                return node
-        return None
-
-    def bExecute(self, context):
-        
-        c = self.GetxForm().bGetObject().constraints.new('LIMIT_DISTANCE')
-        
-        get_target_and_subtarget(self, c)
-        #
-        c.distance            = self.evaluate_input("Distance")
-        c.head_tail           = self.evaluate_input("Head/Tail")
-        c.limit_mode          = self.evaluate_input("Clamp Region")
-        c.use_bbone_shape     = self.evaluate_input("UseBBone")
-        c.use_transform_limit = self.evaluate_input("Affect Transform")
-        c.owner_space         = self.evaluate_input("Owner Space")
-        c.target_space        = self.evaluate_input("Target Space")
-        c.influence           = self.evaluate_input("Influence")
-        print ("Creating Limit Distance Constraint for bone: \""+ self.GetxForm().bGetObject().name + "\"")
-        
-        if self.parameters["Mute"]:
-            c.enabled = False
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-
-# Tracking
-
-class LinkStretchTo:
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.inputs = {
-            "Input Relationship" : NodeSocket(is_input = True, to_socket = "Input Relationship", to_node = self,),
-            "Head/Tail"          : NodeSocket(is_input = True, to_socket = "Head/Tail", to_node = self,),
-            "UseBBone"           : NodeSocket(is_input = True, to_socket = "UseBBone", to_node = self,),
-            "Original Length"    : NodeSocket(is_input = True, to_socket = "Original Length", to_node = self,),
-            "Volume Variation"   : NodeSocket(is_input = True, to_socket = "Volume Variation", to_node = self,),
-            "Use Volume Min"     : NodeSocket(is_input = True, to_socket = "Use Volume Min", to_node = self,),
-            "Volume Min"         : NodeSocket(is_input = True, to_socket = "Volume Min", to_node = self,),
-            "Use Volume Max"     : NodeSocket(is_input = True, to_socket = "Use Volume Max", to_node = self,),
-            "Volume Max"         : NodeSocket(is_input = True, to_socket = "Volume Max", to_node = self,),
-            "Smooth"             : NodeSocket(is_input = True, to_socket = "Smooth", to_node = self,),
-            "Maintain Volume"    : NodeSocket(is_input = True, to_socket = "Maintain Volume", to_node = self,),
-            "Rotation"           : NodeSocket(is_input = True, to_socket = "Rotation", to_node = self,),
-            "Influence"          : NodeSocket(is_input = True, to_socket = "Influence", to_node = self,),
-            "Target"             : NodeSocket(is_input = True, to_socket = "Target", to_node = self,), }
-        self.outputs = {
-            "Output Relationship" : NodeSocket(from_socket = "Output Relationship", from_node=self) }
-        self.parameters = {
-            "Input Relationship":None,
-            "Head/Tail":None,
-            "UseBBone":None,
-            "Original Length":None,
-            "Volume Variation":None,
-            "Use Volume Min":None,
-            "Volume Min":None,
-            "Use Volume Max":None,
-            "Volume Max":None,
-            "Smooth":None,
-            "Maintain Volume":None,
-            "Rotation":None,
-            "Influence":None,
-            "Target":None,
-            "Mute":None,}
-        # now set up the traverse target...
-        self.inputs["Input Relationship"].set_traverse_target(self.outputs["Output Relationship"])
-        self.outputs["Output Relationship"].set_traverse_target(self.inputs["Input Relationship"])
-        self.node_type = 'LINK'
-        
-    def evaluate_input(self, input_name):
-        if (input_name == 'Target'):            
-            socket = self.inputs.get(input_name)
-            return socket.from_node
-            
-        else:
-            return evaluate_input(self, input_name)
-
-    def GetxForm(self):
-        trace = trace_single_line_up(self, "Output Relationship")
-        for node in trace[0]:
-            if (node.__class__ in [xFormRoot, xFormArmature, xFormBone]):
-                return node
-        return None
-
-    def bExecute(self, context):
-        
-        c = self.GetxForm().bGetObject().constraints.new('STRETCH_TO')
-        
-        get_target_and_subtarget(self, c)
-        
-        c.head_tail       = self.evaluate_input("Head/Tail")
-        c.use_bbone_shape = self.evaluate_input("UseBBone")
-        c.bulge           = self.evaluate_input("Volume Variation")
-        c.use_bulge_min   = self.evaluate_input("Use Volume Min")
-        c.bulge_min       = self.evaluate_input("Volume Min")
-        c.use_bulge_max   = self.evaluate_input("Use Volume Max")
-        c.bulge_max       = self.evaluate_input("Volume Max")
-        c.bulge_smooth    = self.evaluate_input("Smooth")
-        c.keep_axis       = self.evaluate_input("Rotation")
-        c.volume          = self.evaluate_input("Maintain Volume")
-        c.rest_length     = self.evaluate_input("Original Length")
-        c.influence       = self.evaluate_input("Influence")
-        
-        if (c.rest_length == 0):
-            # this is meant to be set automatically.
-            c.rest_length = self.GetxForm().bGetObject().bone.length
-        
-
-        print ("Creating Stretch-To Constraint for bone: \""+ self.GetxForm().bGetObject().name + "\"")
-        
-        if self.parameters["Mute"]:
-            c.enabled = False
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-
-class LinkDampedTrack:
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.inputs = {
-            "Input Relationship" : NodeSocket(is_input = True, to_socket = "Input Relationship", to_node = self,),
-            "Head/Tail"          : NodeSocket(is_input = True, to_socket = "Head/Tail", to_node = self,),
-            "UseBBone"           : NodeSocket(is_input = True, to_socket = "UseBBone", to_node = self,),
-            "Track Axis"         : NodeSocket(is_input = True, to_socket = "Track Axis", to_node = self,),
-            "Influence"          : NodeSocket(is_input = True, to_socket = "Influence", to_node = self,),
-            "Target"             : NodeSocket(is_input = True, to_socket = "Target", to_node = self,), }
-        self.outputs = {
-            "Output Relationship" : NodeSocket(from_socket = "Output Relationship", from_node=self) }
-        self.parameters = {
-            "Input Relationship":None,
-            "Head/Tail":None,
-            "UseBBone":None,
-            "Track Axis":None,
-            "Influence":None,
-            "Target":None,
-            "Mute":None,}
-        # now set up the traverse target...
-        self.inputs["Input Relationship"].set_traverse_target(self.outputs["Output Relationship"])
-        self.outputs["Output Relationship"].set_traverse_target(self.inputs["Input Relationship"])
-        self.node_type = 'LINK'
-        
-    def evaluate_input(self, input_name):
-        if (input_name == 'Target'):            
-            socket = self.inputs.get(input_name)
-            return socket.from_node
-            
-        else:
-            return evaluate_input(self, input_name)
-
-    def GetxForm(self):
-        trace = trace_single_line_up(self, "Output Relationship")
-        for node in trace[0]:
-            if (node.__class__ in [xFormRoot, xFormArmature, xFormBone]):
-                return node
-        return None
-
-    def bExecute(self, context):
-        
-        c = self.GetxForm().bGetObject().constraints.new('DAMPED_TRACK')
-        
-        get_target_and_subtarget(self, c)
-        
-        c.head_tail       = self.evaluate_input("Head/Tail")
-        c.use_bbone_shape = self.evaluate_input("UseBBone")
-        c.track_axis      = self.evaluate_input("Track Axis")
-        c.influence       = self.evaluate_input("Influence")
-
-        print ("Creating Damped-Track Constraint for bone: \""+ self.GetxForm().bGetObject().name + "\"")
-        
-        if self.parameters["Mute"]:
-            c.enabled = False
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-
-class LinkLockedTrack:
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.inputs = {
-            "Input Relationship" : NodeSocket(is_input = True, to_socket = "Input Relationship", to_node = self,),
-            "Head/Tail"          : NodeSocket(is_input = True, to_socket = "Head/Tail", to_node = self,),
-            "UseBBone"           : NodeSocket(is_input = True, to_socket = "UseBBone", to_node = self,),
-            "Track Axis"         : NodeSocket(is_input = True, to_socket = "Track Axis", to_node = self,),
-            "Lock Axis"          : NodeSocket(is_input = True, to_socket = "Lock Axis", to_node = self,),
-            "Influence"          : NodeSocket(is_input = True, to_socket = "Influence", to_node = self,),
-            "Target"             : NodeSocket(is_input = True, to_socket = "Target", to_node = self,), }
-        self.outputs = {
-            "Output Relationship" : NodeSocket(from_socket = "Output Relationship", from_node=self) }
-        self.parameters = {
-            "Input Relationship":None,
-            "Head/Tail":None,
-            "UseBBone":None,
-            "Track Axis":None,
-            "Lock Axis":None,
-            "Influence":None,
-            "Target":None,
-            "Mute":None,}
-        # now set up the traverse target...
-        self.inputs["Input Relationship"].set_traverse_target(self.outputs["Output Relationship"])
-        self.outputs["Output Relationship"].set_traverse_target(self.inputs["Input Relationship"])
-        self.node_type = 'LINK'
-        
-    def evaluate_input(self, input_name):
-        if (input_name == 'Target'):            
-            socket = self.inputs.get(input_name)
-            return socket.from_node
-            
-        else:
-            return evaluate_input(self, input_name)
-
-    def GetxForm(self):
-        trace = trace_single_line_up(self, "Output Relationship")
-        for node in trace[0]:
-            if (node.__class__ in [xFormRoot, xFormArmature, xFormBone]):
-                return node
-        return None
-
-    def bExecute(self, context):
-        
-        c = self.GetxForm().bGetObject().constraints.new('LOCKED_TRACK')
-        
-        get_target_and_subtarget(self, c)
-        
-        c.head_tail       = self.evaluate_input("Head/Tail")
-        c.use_bbone_shape = self.evaluate_input("UseBBone")
-        c.track_axis      = self.evaluate_input("Track Axis")
-        c.lock_axis       = self.evaluate_input("Lock Axis")
-        c.influence       = self.evaluate_input("Influence")
-
-        print ("Creating Locked-Track Constraint for bone: \""+ self.GetxForm().bGetObject().name + "\"")
-        
-        if self.parameters["Mute"]:
-            c.enabled = False
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-
-class LinkTrackTo:
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.inputs = {
-            "Input Relationship" : NodeSocket(is_input = True, to_socket = "Input Relationship", to_node = self,),
-            "Head/Tail"          : NodeSocket(is_input = True, to_socket = "Head/Tail", to_node = self,),
-            "UseBBone"           : NodeSocket(is_input = True, to_socket = "UseBBone", to_node = self,),
-            "Track Axis"         : NodeSocket(is_input = True, to_socket = "Track Axis", to_node = self,),
-            "Up Axis"            : NodeSocket(is_input = True, to_socket = "Up Axis", to_node = self,),
-            "Use Target Z"       : NodeSocket(is_input = True, to_socket = "Use Target Z", to_node = self,),
-            "Influence"          : NodeSocket(is_input = True, to_socket = "Influence", to_node = self,),
-            "Target"             : NodeSocket(is_input = True, to_socket = "Target", to_node = self,), }
-        self.outputs = {
-            "Output Relationship" : NodeSocket(from_socket = "Output Relationship", from_node=self) }
-        self.parameters = {
-            "Input Relationship":None,
-            "Head/Tail":None,
-            "UseBBone":None,
-            "Track Axis":None,
-            "Up Axis":None,
-            "Use Target Z":None, 
-            "Influence":None,
-            "Target":None,
-            "Mute":None,}
-        # now set up the traverse target...
-        self.inputs["Input Relationship"].set_traverse_target(self.outputs["Output Relationship"])
-        self.outputs["Output Relationship"].set_traverse_target(self.inputs["Input Relationship"])
-        self.node_type = 'LINK'
-        
-    def evaluate_input(self, input_name):
-        if (input_name == 'Target'):            
-            socket = self.inputs.get(input_name)
-            return socket.from_node
-            
-        else:
-            return evaluate_input(self, input_name)
-
-    def GetxForm(self):
-        trace = trace_single_line_up(self, "Output Relationship")
-        for node in trace[0]:
-            if (node.__class__ in [xFormRoot, xFormArmature, xFormBone]):
-                return node
-        return None
-
-    def bExecute(self, context):
-        
-        c = self.GetxForm().bGetObject().constraints.new('TRACK_TO')
-        
-        get_target_and_subtarget(self, c)
-        
-        c.head_tail       = self.evaluate_input("Head/Tail")
-        c.use_bbone_shape = self.evaluate_input("UseBBone")
-        c.track_axis      = self.evaluate_input("Track Axis")
-        c.up_axis         = self.evaluate_input("Up Axis")
-        c.use_target_z    = self.evaluate_input("Use Target Z")
-        c.influence       = self.evaluate_input("Influence")
-
-        print ("Creating Track-To Constraint for bone: \""+ self.GetxForm().bGetObject().name + "\"")
-        
-        if self.parameters["Mute"]:
-            c.enabled = False
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-
-# relationships & misc.
-
-class LinkInheritConstraint:
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.inputs = {
-            "Input Relationship" : NodeSocket(is_input = True, to_socket = "Input Relationship", to_node = self,),
-            "Location"           : NodeSocket(is_input = True, to_socket = "Location", to_node = self,),
-            "Rotation"           : NodeSocket(is_input = True, to_socket = "Rotation", to_node = self,),
-            "Scale"              : NodeSocket(is_input = True, to_socket = "Scale", to_node = self,),
-            "Influence"          : NodeSocket(is_input = True, to_socket = "Influence", to_node = self,),
-            "Target"             : NodeSocket(is_input = True, to_socket = "Target", to_node = self,), }
-        self.outputs = {
-            "Output Relationship" : NodeSocket(from_socket = "Output Relationship", from_node=self) }
-        self.parameters = {
-            "Input Relationship":None,
-            "Location":None,
-            "Rotation":None,
-            "Scale":None,
-            "Influence":None,
-            "Target":None,
-            "Mute":None,}
-        # now set up the traverse target...
-        self.inputs["Input Relationship"].set_traverse_target(self.outputs["Output Relationship"])
-        self.outputs["Output Relationship"].set_traverse_target(self.inputs["Input Relationship"])
-        self.node_type = 'LINK'
-        
-    def evaluate_input(self, input_name):
-        if (input_name == 'Target'):            
-            socket = self.inputs.get(input_name)
-            return socket.from_node
-            
-        else:
-            return evaluate_input(self, input_name)
-
-    def GetxForm(self):
-        trace = trace_single_line_up(self, "Output Relationship")
-        for node in trace[0]:
-            if (node.__class__ in [xFormRoot, xFormArmature, xFormBone]):
-                return node
-        return None
-
-    def bExecute(self, context):
-        
-        c = self.GetxForm().bGetObject().constraints.new('CHILD_OF')
-        
-        get_target_and_subtarget(self, c)
-        
-        c.use_location_x = self.evaluate_input("Location")[0]
-        c.use_location_y = self.evaluate_input("Location")[1]
-        c.use_location_z = self.evaluate_input("Location")[2]
-        c.use_rotation_x = self.evaluate_input("Rotation")[0]
-        c.use_rotation_y = self.evaluate_input("Rotation")[1]
-        c.use_rotation_z = self.evaluate_input("Rotation")[2]
-        c.use_scale_x    = self.evaluate_input("Scale")[0]
-        c.use_scale_y    = self.evaluate_input("Scale")[1]
-        c.use_scale_z    = self.evaluate_input("Scale")[2]
-        c.influence      = self.evaluate_input("Influence")
-        c.set_inverse_pending
-
-
-        print ("Creating Child-of Constraint for bone: \""+ self.GetxForm().bGetObject().name + "\"")
-        
-        if self.parameters["Mute"]:
-            c.enabled = False
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-
-# Inverse Kinematics:
-
-#
-# r = self.inputs.new('BooleanSocket', "Inherit Rotation")
-# c = self.inputs.new('BooleanSocket', "Connected")
-# s = self.inputs.new('EnumInheritScale', "Inherit Scale")
-# self.inputs.new ('xFormSocket', "Target")
-# self.inputs.new ('xFormSocket', "Pole Target")
-# self.inputs.new ('IKChainLengthSocket', "Chain Length")
-# self.inputs.new ('BooleanSocket', "Use Tail")
-# self.inputs.new ('BooleanSocket', "Stretch")
-# self.inputs.new ('FloatFactorSocket', "Position")
-# self.inputs.new ('FloatFactorSocket', "Rotation")
-# self.inputs.new ('FloatFactorSocket', "Influence")
-# self.outputs.new('RelationshipSocket', "Inheritance")
-# self.inputs.new('xFormSocket', "Parent")
-#
-
-# Ugghh, this one is a little weird
-# I treat IK as a kind of inheritance
-#  that is mutually exclusive with other inheritance
-#  since Blender and other softwares treat it specially, anyway
-#  e.g. in Blender it's always treated as the last constraint
-# While it may be annoying to have it always appear first in each bones'
-#  stack, because this does not affect behaviour, I do not really care.
-class LinkInverseKinematics:
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.inputs = {
-            # Inheritance bits
-            "Input Relationship"  : NodeSocket(is_input = True, to_socket = "Input Relationship", to_node = self,),
-            "Inherit Rotation"    : NodeSocket(is_input = True, to_socket = "Inherit Rotation", to_node = self,),
-            "Inherit Scale"       : NodeSocket(is_input = True, to_socket = "Inherit Scale", to_node = self,),
-            "Connected"           : NodeSocket(is_input = True, to_socket = "Connected", to_node = self,),
-            # Constraint stuff
-            "Chain Length"        : NodeSocket(is_input = True, to_socket = "Chain Length", to_node = self,),
-            "Use Tail"            : NodeSocket(is_input = True, to_socket = "Use Tail", to_node = self,),
-            "Stretch"             : NodeSocket(is_input = True, to_socket = "Stretch", to_node = self,),
-            "Position"            : NodeSocket(is_input = True, to_socket = "Position", to_node = self,),
-            "Rotation"            : NodeSocket(is_input = True, to_socket = "Rotation", to_node = self,),
-            "Influence"           : NodeSocket(is_input = True, to_socket = "Influence", to_node = self,),
-            "Target"              : NodeSocket(is_input = True, to_socket = "Target", to_node = self,),
-            "Pole Target"         : NodeSocket(is_input = True, to_socket = "Pole Target", to_node = self,), }
-        self.outputs = {
-            "Output Relationship" : NodeSocket(from_socket = "Output Relationship", from_node=self) }
-        self.parameters = {
-            "Input Relationship":None,
-            "Inherit Rotation":None,
-            "Inherit Scale":None,
-            "Connected":None,
-            "Chain Length":None,
-            "Use Tail":None,
-            "Stretch":None,
-            "Position":None,
-            "Rotation":None,
-            "Influence":None,
-            "Target":None, 
-            "Pole Target":None,
-            "Mute":None,}
-        # now set up the traverse target...
-        self.inputs["Input Relationship"].set_traverse_target(self.outputs["Output Relationship"])
-        self.outputs["Output Relationship"].set_traverse_target(self.inputs["Input Relationship"])
-        self.node_type = 'LINK'
-        
-    def evaluate_input(self, input_name):
-        if (input_name == 'Target') or (input_name == 'Pole Target'):
-            socket = self.inputs.get(input_name)
-            return socket.from_node
-        else:
-            return evaluate_input(self, input_name)
-
-    def GetxForm(self):
-        trace = trace_single_line_up(self, "Output Relationship")
-        for node in trace[0]:
-            if (node.__class__ in [xFormRoot, xFormArmature, xFormBone]):
-                return node
-        return None
-
-    def bExecute(self, context):
-        # do not handle any inheritance stuff here, that is dealt with
-        #  by the xForm nodes instead!
-        myOb = self.GetxForm().bGetObject()
-        c = self.GetxForm().bGetObject().constraints.new('IK')
-        
-        get_target_and_subtarget(self, c)
-        get_target_and_subtarget(self, c, input_name = 'Pole Target')
-        
-        
-        if (c.pole_target): # Calculate the pole angle, the user shouldn't have to.
-            pole_object = c.pole_target
-            pole_location = pole_object.matrix_world.decompose()[0]
-            if (c.pole_subtarget):
-                pole_object = c.pole_target.pose.bones[c.subtarget]
-                pole_location = pole_object.matrix.decompose()[0]
-            #HACK HACK
-            handle_location = myOb.bone.tail_local if (self.evaluate_input("Use Tail")) else myOb.bone.head_local
-            counter = 0
-            parent = myOb
-            base_bone = myOb
-            while (parent is not None):
-                if ((self.evaluate_input("Chain Length") != 0) and (counter > self.evaluate_input("Chain Length"))):
-                    break
-                base_bone = parent
-                parent = parent.parent
-                counter+=1
-            head_location = base_bone.bone.head_local
-
-            pole_normal = (handle_location - head_location).cross(pole_location - head_location)
-            vector_u = myOb.bone.x_axis
-            vector_v = pole_normal.cross(base_bone.bone.y_axis)
-            angle = vector_u.angle(vector_v)
-            if (vector_u.cross(vector_v).angle(base_bone.bone.y_axis) < 1):
-                angle = -angle
-            
-            c.pole_angle = angle
-        
-        c.chain_count    = self.evaluate_input("Chain Length")
-        c.use_tail       = self.evaluate_input("Use Tail")
-        c.use_stretch    = self.evaluate_input("Stretch")
-        c.weight         = self.evaluate_input("Position")
-        c.orient_weight  = self.evaluate_input("Rotation")
-        c.influence      = self.evaluate_input("Influence")
-        # this should be sufficient, I think use_location and use_rotation
-        #  are meaningless if the weight is 0, anyway.
-        # Well, the minimum weight is 0.01, so we have to get the input again.
-        c.use_location   = self.evaluate_input("Position") > 0 
-        c.use_rotation   = self.evaluate_input("Rotation") > 0
-        # this is a little annoying because the constraint can have a
-        #  positive value for position/rotation without it being enabled.
-        print ("Creating IK Constraint for bone: \""+ self.GetxForm().bGetObject().name + "\"")
-        
-        if self.parameters["Mute"]:
-            c.enabled = False
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-
-
-
-
-#*#-------------------------------#++#-------------------------------#*#
-# G E N E R I C   N O D E S
-#*#-------------------------------#++#-------------------------------#*#
-
-
-# in reality, none of these inputs have names
-#  so I am using the socket name for now
-#  I suppose I could use any name :3
-class InputFloat:
-    '''A node representing float input'''
-    
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.outputs = {"Float Input" : NodeSocket(from_socket = "Float Input", from_node=self) }
-        self.parameters = {"Float Input":None, "Mute":None}
-        self.node_type = 'UTILITY'
-        
-    def evaluate_input(self, input_name):
-        return self.parameters["Float Input"]
-    
-    def bExecute(self, bContext = None,):
-        pass
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-    
-class InputVector:
-    '''A node representing vector input'''
-    
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.outputs = {"VectorSocket" : NodeSocket(from_socket = 'VectorSocket', from_node=self) }
-        self.parameters = {'VectorSocket':None, "Mute":None}
-        self.node_type = 'UTILITY'
-        
-    def evaluate_input(self, input_name):
-        return self.parameters["VectorSocket"]
-    
-    def bExecute(self, bContext = None,):
-        pass
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-
-class InputBoolean:
-    '''A node representing boolean input'''
-    
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.outputs = {"BooleanSocket" : NodeSocket(from_socket = 'BooleanSocket', from_node=self) }
-        self.parameters = {'BooleanSocket':None, "Mute":None}
-        self.node_type = 'UTILITY'
-        
-    def evaluate_input(self, input_name):
-        return self.parameters["BooleanSocket"]
-    
-    def bExecute(self, bContext = None,):
-        pass
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-
-class InputBooleanThreeTuple:
-    '''A node representing inheritance'''
-        
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.outputs = {"BooleanThreeTupleSocket" : NodeSocket(from_socket = 'BooleanThreeTupleSocket', from_node=self) }
-        self.parameters = {'BooleanThreeTupleSocket':None, "Mute":None}
-        self.node_type = 'UTILITY'
-        
-    def evaluate_input(self, input_name):
-        return self.parameters["BooleanThreeTupleSocket"]
-    
-    def bExecute(self, bContext = None,):
-        pass
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-
-class InputRotationOrder:
-    '''A node representing string input for rotation order'''
-        
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.outputs = {"RotationOrderSocket" : NodeSocket(from_socket = 'RotationOrderSocket', from_node=self) }
-        self.parameters = {'RotationOrderSocket':None, "Mute":None}
-        self.node_type = 'UTILITY'
-        
-    def evaluate_input(self, input_name):
-        return self.parameters["RotationOrderSocket"]
-    
-    def bExecute(self, bContext = None,):
-        pass
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-
-class InputTransformSpace:
-    '''A node representing string input for transform space'''
-        
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.outputs = {"TransformSpaceSocket" : NodeSocket(from_socket = 'TransformSpaceSocket', from_node=self) }
-        self.parameters = {'TransformSpaceSocket':None, "Mute":None}
-        self.node_type = 'UTILITY'
-        
-    def evaluate_input(self, input_name):
-        return self.parameters["TransformSpaceSocket"]
-    
-    def bExecute(self, bContext = None,):
-        pass
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-
-class InputString:
-    '''A node representing string input'''
-        
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.outputs = {"StringSocket" : NodeSocket(from_socket = 'StringSocket', from_node=self) }
-        self.parameters = {'StringSocket':None, "Mute":None}
-        self.node_type = 'UTILITY'
-        
-    def evaluate_input(self, input_name):
-        return self.parameters["StringSocket"]
-    
-    def bExecute(self, bContext = None,):
-        pass
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-
-class InputQuaternion:
-    '''A node representing quaternion input'''
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.outputs = {"QuaternionSocket" : NodeSocket(from_socket = 'QuaternionSocket', from_node=self) }
-        self.parameters = {'QuaternionSocket':None, "Mute":None}
-        self.node_type = 'UTILITY'
-        
-        
-    def evaluate_input(self, input_name):
-        return self.parameters["QuaternionSocket"]
-    
-    def bExecute(self, bContext = None,):
-        pass
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-
-class InputQuaternionAA:
-    '''A node representing axis-angle quaternion input'''
-        
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.outputs  = {"QuaternionSocketAA" : NodeSocket(from_socket = 'QuaternionSocketAA', from_node=self) }
-        self.parameters = {'QuaternionSocketAA':None, "Mute":None}
-        self.node_type = 'UTILITY'
-        
-    def evaluate_input(self, input_name):
-        return self.parameters["QuaternionSocketAA"]
-    
-    def bExecute(self, bContext = None,):
-        pass
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        fill_parameters(self, node_prototype)
-
-
-class InputMatrix:
-    '''A node representing axis-angle quaternion input'''
-        
-    def __init__(self, signature, base_tree):
-        self.base_tree=base_tree
-        self.signature = signature
-        self.outputs  = {"Matrix" : NodeSocket(from_socket = 'Matrix', from_node=self) }
-        self.parameters = {'Matrix':None, "Mute":None}
-        self.node_type = 'UTILITY'
-        
-    def evaluate_input(self, input_name):
-        return self.parameters["Matrix"]
-    
-    def bExecute(self, bContext = None,):
-        pass
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self, node_prototype):
-        # this node is peculiar for how its data is input
-        # It uses node properties that are not addressable as sockets.
-        from mathutils import Matrix
-        
-        matrix = ( node_prototype.first_row[ 0], node_prototype.first_row[ 1], node_prototype.first_row[ 2], node_prototype.first_row[ 3],
-                   node_prototype.second_row[0], node_prototype.second_row[1], node_prototype.second_row[2], node_prototype.second_row[3],
-                   node_prototype.third_row[ 0], node_prototype.third_row[ 1], node_prototype.third_row[ 2], node_prototype.third_row[ 3],
-                   node_prototype.fourth_row[0], node_prototype.fourth_row[1], node_prototype.fourth_row[2], node_prototype.fourth_row[3], )
-        self.parameters["Matrix"] = Matrix([matrix[0:4], matrix[4:8], matrix[8:12], matrix[12:16]])
-        print (self.parameters["Matrix"])
-        
-
-# # NOT YET IMPLEMENTED:
-# class InputMatrixNode(Node, MantisNode):
-    # '''A node representing matrix input'''
-    # inputs = 
-    # # the node is implemented as a set of sixteen float inputs
-    # # but I think I can boil it down to one matrix input
-
-
-# class ScaleBoneLengthNode(Node, MantisNode):
-    # '''Scale Bone Length'''
-    # pass
-

+ 438 - 116
node_container_common.py

@@ -9,29 +9,26 @@ from .base_definitions import GraphError, CircularDependencyError
 
 
 def get_socket_value(node_socket):
-    if node_socket.bl_idname in  ['RelationshipSocket', 'xFormSocket']:
-        return None
-    elif hasattr(node_socket, "default_value"):
-        from .utilities import to_mathutils_value
-        default_value_type = type(node_socket.default_value)
-        math_val = to_mathutils_value(node_socket)
-        if math_val:
-            return math_val
-        # maybe we can use it directly.. ?
-        elif ( (default_value_type == str) or (default_value_type == bool) or
-             (default_value_type == float) or (default_value_type == int) ):
-            return node_socket.default_value
-        return None
+    # if node_socket.bl_idname in  ['RelationshipSocket', 'xFormSocket']:
+    value = None
+    if hasattr(node_socket, "default_value"):
+        value = node_socket.default_value
+    if node_socket.bl_idname == 'MatrixSocket':
+        value =  node_socket.TellValue()
+    return value
+
 
 # TODO: unify the fill_paramaters for auto-gen nodes
-def fill_parameters(nc):
+def fill_parameters(nc, np = None):
     from .utilities import get_node_prototype
-    if (nc.signature[0] is "MANTIS_AUTOGENERATED"):
-        return None
-    else:
-        np = get_node_prototype(nc.signature, nc.base_tree)
-    if ( not np ):
-        raise RuntimeError(wrapRed("No node prototype found for... %s" % ( [nc.base_tree] + list(nc.signature[1:]) ) ) )
+    if not np:
+        if ( (nc.signature[0] in  ["MANTIS_AUTOGENERATED", "SCHEMA_AUTOGENERATED" ]) or 
+             (nc.signature[-1] in ["NodeGroupOutput", "NodeGroupInput"]) ): # I think this is harmless
+            return None
+        else:
+            np = get_node_prototype(nc.signature, nc.base_tree)
+        if not np:
+            raise RuntimeError(wrapRed("No node prototype found for... %s" % ( [nc.base_tree] + list(nc.signature[1:]) ) ) )
     for key in nc.parameters.keys():
         node_socket = np.inputs.get(key)
         # if (nc.signature[0] is "MANTIS_AUTOGENERATED"):
@@ -46,8 +43,8 @@ def fill_parameters(nc):
             if ( ( len(np.inputs) == 0) and ( len(np.outputs) == 1) ):
                 # this is a simple input node.
                 node_socket = np.outputs[0]
-            elif key == 'Name': # for Links we just use the Node Label
-                nc.parameters[key] = np.label
+            elif key == 'Name': # for Links we just use the Node Label, or if there is no label, the name.
+                nc.parameters[key] = np.label if np.label else np.name
                 continue
             else:
                 pass
@@ -61,24 +58,30 @@ def fill_parameters(nc):
                     raise RuntimeError(wrapRed("No value found for " + nc.__repr__() + " when filling out node parameters for " + np.name + "::"+node_socket.name))
             else:
                 pass
+        # if key in ['Use Target Z']:
+        #     prRed (nc, key, nc.parameters[key])
 
 
 
-def evaluate_input(node_container, input_name):
+def evaluate_input(node_container, input_name, index=0):
     if not (node_container.inputs.get(input_name)):
         # just return the parameter, there is no socket associated
+        # prOrange("No input: %s, %s" %(node_container, input_name))
         return node_container.parameters.get(input_name)
-    trace = trace_single_line(node_container, input_name)
+    trace = trace_single_line(node_container, input_name, index)
     # this should give a key error if there is a problem
     #  it is NOT handled here because it should NOT happen
-    prop = trace[0][-1].parameters[trace[1].name]
-    return prop
+    try:
+        prop = trace[0][-1].parameters[trace[1].name] #[0] = nodes [-1] = last node, read its parameters
+    except Exception as e:
+        prRed (trace[1].name, trace[0][-1], "prepared" if trace[0][-1].prepared else "NO","executed" if trace[0][-1].executed else "NO")
+        raise e
+    return prop # this should not be necessary...but dicts will be dicts
 
 def check_for_driver(node_container, input_name, index = None):
     prop = evaluate_input(node_container, input_name)
     if (index is not None):
         prop = prop[index]
-    # This should work, even for None.
     return (prop.__class__.__name__ == 'MantisDriver')
 
 def trace_node_lines(node_container):
@@ -106,32 +109,28 @@ def trace_node_lines(node_container):
 
 
 # TODO: modify this to work with multi-input nodes
-def trace_single_line(node_container, input_name):
+def trace_single_line(node_container, input_name, link_index=0):
     # DO: refactor this for new link class
     """Traces a line to its input."""
     nodes = [node_container]
-    if hasattr(node_container, "inputs"):
-        # Trace a single line
-        if (socket := node_container.inputs.get(input_name) ):
-            while (socket.is_linked):
-                link = socket.links[0] # inputs can only get one link.
-                other = link.from_node.outputs.get(link.from_socket)
-                if (other):
-                    socket = other
-                    if socket.can_traverse:
-                        socket = socket.traverse_target
-                        nodes.append(socket.node)
-                    else: # this is an output.
-                        nodes.append(socket.node)
-                        break
-                else:
+    # Trace a single line
+    if (socket := node_container.inputs.get(input_name) ):
+        while (socket.is_linked):
+            link = socket.links[link_index]; link_index = 0
+            if (socket := link.from_node.outputs.get(link.from_socket)):
+                nodes.append(socket.node)
+                if socket.can_traverse:
+                    socket = socket.traverse_target
+                else: # this is an output.
                     break
+            else:
+                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,):
-    """ Tells the depth of a node within the node tree. """
+    """I use this to get the xForm from a link node."""
     nodes = [node_container]
     if hasattr(node_container, "outputs"):
         # Trace a single line
@@ -139,7 +138,7 @@ def trace_single_line_up(node_container, output_name,):
             while (socket.is_linked):
                 # This is bad, but it's efficient for nodes that only expect
                 #  one path along the given line
-                link = socket.links[0]
+                link = socket.links[0] # TODO: find out if this is wise.
                 other = link.to_node.inputs.get(link.to_socket)
                 if (other):
                     socket = other
@@ -165,37 +164,145 @@ def trace_all_lines_up(nc, output_name):
     check_me = type('', (object,), copy_items)
     return get_depth_lines(check_me)[1]
 
-
-
+# KEEP THIS
+# don't modify this.... until the other one actually works.
+# def original_get_depth_lines(root):
+#     path, seek, nc_path = [0,], root, [root,]
+#     lines, nc_paths = {}, {}
+#     nc_len = len(root.hierarchy_connections)-1
+#     curheight=0
+#     while (path[0] <= nc_len):
+#         nc_path.append(nc_path[-1].hierarchy_connections[path[-1]])
+#         if (not (node_lines  := lines.get(nc_path[-1].signature, None))):
+#             node_lines = lines[nc_path[-1].signature] = set()
+#         if (not (node_paths  := nc_paths.get(nc_path[-1].signature, None))):
+#             node_paths = nc_paths[nc_path[-1].signature] = set()
+#         node_lines.add(tuple(path)); node_paths.add(tuple(nc_path))
+#         if not hasattr(nc_path[-1], "hierarchy_connections"):
+#             # TODO even though I am going to completely rewrite this function, I need to fix this
+#             prRed(f"{nc_path[-1]} has some sort of stupid hierarchy problem. Ignoring...")
+#             nc_path[-1].hierarchy_connections = []; nc_path[-1].connected_to = 0
+#         if nc_path[-1].hierarchy_connections:
+#             path.append(0); curheight+=1
+#         else:
+#             path[curheight] = path[curheight] + 1
+#             nc_path.pop()
+#             connected_nodes = nc_path[-1].hierarchy_connections
+#             if ( path[-1] <= len(connected_nodes)-1 ):
+#                 seek = connected_nodes[path[-1]]
+#             elif curheight > 0:
+#                 while(len(path) > 1):
+#                     path.pop(); curheight -= 1; path[curheight]+=1; nc_path.pop()
+#                     if ( (len(nc_path)>1) and path[-1] < len(nc_path[-1].hierarchy_connections) ):
+#                         break 
+#     return lines, nc_paths
+
+
+
+
+def num_hierarchy_connections(nc):
+    num=0
+    for out in nc.outputs:
+        for link in out.links:
+            if link.is_hierarchy: num+=1
+    return num
+
+def list_hierarchy_connections(nc):
+    return len(nc.hierarchy_connections)-1
+    hc=[]
+    for out in nc.outputs:
+        for link in out.links:
+            if link.is_hierarchy: hc.append(link.to_node)
+    return num
+
+# what this is doing is giving a list of Output-Index that is the path to the given node, from a given root.
+# HOW TO REWRITE...
+# we simply do the same thing, but we look at the outputs, not the old hierarchy-connections
+# we can do the same tree-search but we simply ignore an output if it is not hierarchy.
+# the existing code isn't complicated, it's just hard to read. So this new code should be easier to read, too.
 
 def get_depth_lines(root):
-    path, seek, nc_path = [0,], root, [root,]
+    from .base_definitions import GraphError
+    path, nc_path = [0,], [root,]
     lines, nc_paths = {}, {}
-    nc_len = len (root.hierarchy_connections)-1
+    nc_len = len(root.hierarchy_connections)-1
     curheight=0
     while (path[0] <= nc_len):
+        # this doesn't seem to make this any slower. It is good to check it.
+        if nc_path[-1] in nc_path[:-1]:
+            raise GraphError(wrapRed(f"Infinite loop detected while depth sorting for root {root}."))
+        #
         nc_path.append(nc_path[-1].hierarchy_connections[path[-1]])
         if (not (node_lines  := lines.get(nc_path[-1].signature, None))):
             node_lines = lines[nc_path[-1].signature] = set()
         if (not (node_paths  := nc_paths.get(nc_path[-1].signature, None))):
             node_paths = nc_paths[nc_path[-1].signature] = set()
         node_lines.add(tuple(path)); node_paths.add(tuple(nc_path))
-        if nc_path[-1].hierarchy_connections:
+        if nc_path[-1].hierarchy_connections: # if there is at least one element
             path.append(0); curheight+=1
-        else:
+        else: # at this point, nc_path is one longer than path because path is a segment between two nodes
+            # or more siimply, because nc_path has the root in it and path starts with the first node
             path[curheight] = path[curheight] + 1
-            nc_path.pop()
-            connected_nodes = nc_path[-1].hierarchy_connections
-            if ( path[-1] <= len(connected_nodes)-1 ):
-                seek = connected_nodes[path[-1]]
-            elif curheight > 0:
+            nc_path.pop() # so we go back and horizontal
+            if ( path[-1] <= len(nc_path[-1].hierarchy_connections)-1 ):
+                pass # and continue if we can
+            elif curheight > 0: # otherwise we keep going back
                 while(len(path) > 1):
                     path.pop(); curheight -= 1; path[curheight]+=1; nc_path.pop()
                     if ( (len(nc_path)>1) and path[-1] < len(nc_path[-1].hierarchy_connections) ):
                         break 
     return lines, nc_paths
-    
 
+# same but because the checks end up costing a fair amount of time, I don't want to use this one unless I need to.
+def get_prepared_depth_lines(root,):
+    # import pstats, io, cProfile
+    # from pstats import SortKey
+    # with cProfile.Profile() as pr:
+        path, nc_path = [0,], [root,]
+        lines, nc_paths = {}, {}
+        nc_len = len(prepared_connections(root, ))-1
+        curheight=0
+        while (path[0] <= nc_len):
+            if nc_path[-1] in nc_path[:-1]:
+                raise GraphError(wrapRed(f"Infinite loop detected while depth sorting for root {root}."))
+            nc_path.append(prepared_connections(nc_path[-1], )[path[-1]])
+            if (not (node_lines  := lines.get(nc_path[-1].signature, None))):
+                node_lines = lines[nc_path[-1].signature] = set()
+            if (not (node_paths  := nc_paths.get(nc_path[-1].signature, None))):
+                node_paths = nc_paths[nc_path[-1].signature] = set()
+            node_lines.add(tuple(path)); node_paths.add(tuple(nc_path))
+            if prepared_connections(nc_path[-1], ): # if there is at least one element
+                path.append(0); curheight+=1
+            else: # at this point, nc_path is one longer than path because path is a segment between two nodes
+                # or more siimply, because nc_path has the root in it and path starts with the first node
+                path[curheight] = path[curheight] + 1
+                nc_path.pop() # so we go back and horizontal
+                if path[-1] <= len(prepared_connections(nc_path[-1], ))-1:
+                    pass # and continue if we can
+                elif curheight > 0: # otherwise we keep going back
+                    while(len(path) > 1):
+                        path.pop(); curheight -= 1; path[curheight]+=1; nc_path.pop()
+                        if (len(nc_path)>1) and path[-1] < len(prepared_connections(nc_path[-1], ) ):
+                            break 
+        # from the Python docs at https://docs.python.org/3/library/profile.html#module-cProfile
+    # s = io.StringIO()
+    # sortby = SortKey.TIME
+    # ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
+    # ps.print_stats()
+    # print(s.getvalue())
+        return lines, nc_paths
+
+
+def prepared_connections(nc):
+    if nc.prepared:
+        return nc.hierarchy_connections
+    else:
+        ret = []
+        for hc in nc.hierarchy_connections:
+            if hc.prepared:
+                ret.append(hc)
+        return ret
+        # return [hc for hc in nc.hierarchy_connections if hc.prepared]
 
 
 def node_depth(lines):
@@ -288,20 +395,36 @@ def get_target_and_subtarget(node_container, linkOb, input_name = "Target"):
 
 
 def setup_custom_props(nc):
-    from .utilities import get_node_prototype    
-    if np := get_node_prototype(nc.signature, nc.base_tree):
-        for inp in np.inputs:
-            if not (inp.name in nc.inputs.keys()) :
-                nc.inputs[inp.name] = NodeSocket(is_input = True, name = inp.name, node = nc,)
-                nc.parameters[inp.name] = None
-        for out in np.outputs:
-            if not (out.name in nc.outputs.keys()) :
-                nc.outputs[out.name] = NodeSocket(is_input = False, name = out.name, node = nc,)
-                #nc.parameters[out.name] = None
-                # probably not something I want?
-                # I don't think this supports in and out by the same name oof
+    from .utilities import get_node_prototype
+    if nc.signature[0] == 'SCHEMA_AUTOGENERATED':
+        from .base_definitions import custom_props_types
+        if nc.__class__.__name__ not in custom_props_types:
+            # prRed(f"Reminder: figure out how to deal with custom property setting for Schema Node {nc}")
+            raise RuntimeError(wrapRed(f"Custom Properties not set up for node {nc}"))
+        return
     else:
-        prRed(nc)
+        np = get_node_prototype(nc.signature, nc.base_tree)
+    if np:
+        setup_custom_props_from_np(nc, np)
+    else:
+        prRed("Failed to setup custom properties for: nc")
+
+def setup_custom_props_from_np(nc, np):
+    for inp in np.inputs:
+        if inp.identifier == "__extend__": continue
+        if not (inp.name in nc.inputs.keys()) :
+            socket = NodeSocket(is_input = True, name = inp.name, node = nc,)
+            nc.inputs[inp.name] = socket
+            nc.parameters[inp.name] = None
+            for attr_name in ["min", "max", "soft_min", "soft_max", "description"]:
+                try:
+                    setattr(socket, attr_name, getattr(inp, attr_name))
+                except AttributeError:
+                    pass
+    for out in np.outputs:
+        if out.identifier == "__extend__": continue
+        if not (out.name in nc.outputs.keys()) :
+            nc.outputs[out.name] = NodeSocket(is_input = False, name = out.name, node = nc,)
             
 def prepare_parameters(nc):
     # some nodes add new parameters at runtime, e.g. Drivers
@@ -315,8 +438,28 @@ def prepare_parameters(nc):
     # should work, this is ugly.
 
 
+
+
 # TODO: this should handle sub-properties better
 def evaluate_sockets(nc, c, 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"]:
+            if not val: val=0
+            ob.key_blocks[att_name].value=val
+        elif "]." in att_name:
+            # it is of the form prop[int].prop2
+            prop=att_name.split('[')[0]
+            prop1=att_name.split('.')[1]
+            index = int(att_name.split('[')[1][0])
+            setattr(getattr(c, prop)[index], prop1, val)
+        else:
+            try:
+                setattr(ob, att_name, val)
+            except Exception as e:
+                prRed(ob, att_name, val); raise e
     # HACK I think I should do this in __init__
     if not hasattr(nc, "drivers"):
         nc.drivers = {}
@@ -331,7 +474,7 @@ def evaluate_sockets(nc, c, props_sockets):
             sock = (sock, index)
             original_prop = prop
             # TODO: deduplicate this terrible hack
-            if ("." in prop): # this is a property of a property...
+            if ("." in prop) and not c.__class__.__name__ in ["Key"]: # this is a property of a property...
                 sub_props = [c]
                 while ("." in prop):
                     split_prop = prop.split(".")
@@ -343,63 +486,135 @@ def evaluate_sockets(nc, c, props_sockets):
                         sub_props.append(getattr(sub_props[-1], sub_prop)[index])
                     else:
                         sub_props.append(getattr(sub_props[-1], sub_prop))
-                setattr(sub_props[-1], prop, default)
+                safe_setattr(sub_props[-1], prop, default)
             # this is really stupid
             else:
-                setattr(c, prop, default)
-            print("Adding driver %s to %s in %s" % (wrapPurple(original_prop), wrapWhite(nc.signature[-1]), wrapOrange(nc.GetxForm().bGetObject().name)))
-            nc.drivers[sock] = original_prop
+                safe_setattr(c, prop, default)
+            if nc.node_type in ['LINK',]:
+                printname  = wrapOrange(nc.GetxForm().bGetObject().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"]:
+                nc.drivers[sock] = "[\""+original_prop+"\"]" # lol. It is a dict element not a "true" property
+            elif c.__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):
-                setattr(c, prop, nc.evaluate_input(sock)[index])
+                safe_setattr(c, prop, nc.evaluate_input(sock)[index])
             else:                    # 'mute' is better than 'enabled'
                 # UGLY HACK          # because it is available in older
                 if (prop == 'mute'): # Blenders.
-                    setattr(c, prop, not nc.evaluate_input(sock))
+                    safe_setattr(c, prop, not nc.evaluate_input(sock))
                 else:
-                    setattr(c, prop, nc.evaluate_input(sock))
+                    try:
+                        # prRed(c.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__"):
+                            # 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]
+                                    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
+                        else:
+                            # prOrange("Skipping the Thing", getattr(c, prop))
+                            safe_setattr(c, prop, nc.evaluate_input(sock))
+                    except Exception as e:
+                        prRed(c, nc, prop, sock, nc.evaluate_input(sock))
+                        raise e
+
+
+def finish_driver(nc, driver_item, prop):
+    prWhite(nc, prop)
+    index = driver_item[1]; driver_sock = driver_item[0]
+    driver_trace = trace_single_line(nc, driver_sock)
+    driver_provider, driver_socket = driver_trace[0][-1], driver_trace[1]
+    if index is not None:
+        driver = driver_provider.parameters[driver_socket.name][index].copy()
+        # this is harmless and necessary for the weird ones where the property is a vector too
+        driver["ind"] = index
+    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]
+            while ("." in prop):
+                split_prop = prop.split(".")
+                prop = split_prop[1]
+                sub_prop = (split_prop[0])
+                if ("[" in sub_prop):
+                    sub_prop, index = sub_prop.split("[")
+                    index = int(index[0])
+                    sub_props.append(getattr(sub_props[-1], sub_prop)[index])
+                else:
+                    sub_props.append(getattr(sub_props[-1], sub_prop))
+            driver["owner"] = sub_props[-1]
+        elif nc.node_type in ['XFORM',] and nc.__class__.__name__ in ['xFormBone']:
+            # TODO: I really shouldn't have to hardcode this. Look into better solutions.
+            if prop in ['hide', 'show_wire']: # we need to get the bone, not the pose bone.
+                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
+        else:
+            driver["owner"] = nc.bObject
+        # prPurple("Successfully created driver for %s" % prop)
+        driver["prop"] = prop
+        return driver
+        # prWhite(driver)
+    else:
+        prOrange("Provider", driver_provider)
+        prGreen("socket", driver_socket)
+        print (index)
+        prPurple(driver_provider.parameters[driver_socket.name])
+        prRed("Failed to create driver for %s" % prop)
+        return None
 
-def finish_drivers(self):
+def finish_drivers(nc):
     # gonna make this into a common function...
     drivers = []
-    if not hasattr(self, "drivers"):
+    if not hasattr(nc, "drivers"):
+        # prGreen(f"No Drivers to construct for {nc}")
         return # HACK
-    for driver, prop in self.drivers.items():
-        #annoyingly, the driver may be a vector-driver...
-        index = driver[1]; driver = driver[0]
-        driver_trace = trace_single_line(self, driver)
-        driver_provider, driver_socket = driver_trace[0][-1], driver_trace[1]
-        if index is not None:
-            driver = driver_provider.parameters[driver_socket.name][index].copy()
+    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:
-            driver = driver_provider.parameters[driver_socket.name].copy()
-        if driver:
-            # todo: deduplicate this terrible hack
-            c = self.bObject # STUPID
-            if ("." in prop): # this is a property of a property...
-                sub_props = [c]
-                while ("." in prop):
-                    split_prop = prop.split(".")
-                    prop = split_prop[1]
-                    sub_prop = (split_prop[0])
-                    if ("[" in sub_prop):
-                        sub_prop, index = sub_prop.split("[")
-                        index = int(index[0])
-                        sub_props.append(getattr(sub_props[-1], sub_prop)[index])
-                    else:
-                        sub_props.append(getattr(sub_props[-1], sub_prop))
-                driver["owner"] = sub_props[-1]
-            else:
-                driver["owner"] = self.bObject
-            # prPurple("Successfully created driver for %s" % prop)
-            driver["prop"] = prop
-            drivers.append(driver)
-        else:
-            prRed("Failed to create driver for %s" % prop)
+            drivers.append(finish_driver(nc, driver_item, prop))
     from .drivers import CreateDrivers
     CreateDrivers(drivers)
 
 
+from .base_definitions import from_name_filter, to_name_filter
+def detect_hierarchy_link(from_node, from_socket, to_node, to_socket,):
+    if to_node.node_type in ['DUMMY_SCHEMA', 'SCHEMA']:
+        return False
+    if (from_socket in from_name_filter) or (to_socket in to_name_filter):
+        return False
+    # if from_node.__class__.__name__ in ["UtilityCombineVector", "UtilityCombineThreeBool"]:
+    #     return False
+    return True
+
 #Dummy classes for logic with node containers, they are not meant to do
 #  each and every little thing the "real" Blender classes do.
 class NodeLink:
@@ -409,35 +624,51 @@ class NodeLink:
     to_socket = None
     
     def __init__(self, from_node, from_socket, to_node, to_socket):
+        if from_node.signature == to_node.signature:
+            raise RuntimeError("Cannot connect a node to itself.")
         self.from_node = from_node
         self.from_socket = from_socket
         self.to_node = to_node
         self.to_socket = to_socket
         self.from_node.outputs[self.from_socket].links.append(self)
         self.to_node.inputs[self.to_socket].links.append(self)
+        self.is_hierarchy = detect_hierarchy_link(from_node, from_socket, to_node, to_socket,)
+        self.is_alive = True
     
     def __repr__(self):
         return self.from_node.outputs[self.from_socket].__repr__() + " --> " + self.to_node.inputs[self.to_socket].__repr__()
+        # link_string =   # if I need to colorize output for debugging.
+        # if self.is_hierarchy:
+        #     return wrapOrange(link_string)
+        # else:
+        #     return wrapWhite(link_string)
+    
+    def die(self):
+        self.is_alive = False
+        self.to_node.inputs[self.to_socket].flush_links()
+        self.from_node.outputs[self.from_socket].flush_links()
+
+
+
 
 
-class NodeSocket:
 
+class NodeSocket:
     @property # this is a read-only property.
     def is_linked(self):
-        return len(self.links) > 0
+        return bool(self.links)
         
     def __init__(self, is_input = False,
                  node = None, name = None,
                  traverse_target = None):
         self.can_traverse = False # to/from the other side of the parent node
-        self.traverse_target = None # a reference to another Socket
-        # will set in a sec
+        self.traverse_target = None
         self.node = node
         self.name = name
         self.is_input = is_input
         self.links = []
         if (traverse_target):
-            set_traverse_target(traverse_target)
+            self.can_traverse = True
         
     def connect(self, node, socket):
         if  (self.is_input):
@@ -446,6 +677,9 @@ class NodeSocket:
         else:
             from_node   = self.node; to_node   = node
             from_socket = self.name; to_socket = socket
+        for l in from_node.outputs[from_socket].links:
+            if l.to_node==to_node and l.to_socket==to_socket:
+                return None
         new_link = NodeLink(
                 from_node,
                 from_socket,
@@ -464,6 +698,9 @@ class NodeSocket:
     def set_traverse_target(self, traverse_target):
         self.traverse_target = traverse_target
         self.can_traverse = True
+    
+    def flush_links(self):
+        self.links = [l for l in self.links if l.is_alive]
         
     @property
     def is_connected(self):
@@ -472,3 +709,88 @@ class NodeSocket:
     
     def __repr__(self):
         return self.node.__repr__() + "::" + self.name
+
+
+# do I need this and the link class above?
+class DummyLink:
+    #gonna use this for faking links to keep the interface consistent
+    def __init__(self, from_socket, to_socket, nc_from=None, nc_to=None, original_from=None):
+        self.from_socket = from_socket
+        self.to_socket = to_socket
+        self.nc_from = nc_from
+        self.nc_to = nc_to
+        # self.from_node = from_socket.node
+        # self.to_node = to_socket.node
+        if (original_from):
+            self.original_from = original_from
+        else:
+            self.original_from = self.from_socket
+    def __repr__(self):
+        return(self.nc_from.__repr__()+":"+self.from_socket.name + " -> " + self.nc_to.__repr__()+":"+self.to_socket.name)
+
+
+
+
+
+
+
+# Here we setup some properties that we want every class to have
+# I am not using inheritance because I don't like it, I think it adds a bunch of weird complexity
+# This, on the otherhand, is basically just extending the class definitions I have with some boilerplate
+# and that means I can avoid writing the boilerplate, get the benefits of class inheritance, and none of the bad
+# 
+# I don't want these classes to share a superclass, because I want them to be nothing more than a way to
+#    package some methods and data together, so my node tree can be like a linked list using basic types like dict
+# essentially, I don't have any use for a superclass, but I do need these properties and attributes.
+# and now, instead of declaring it, inheriting it, and shadowing it elsewhere, I simply do not declare it unless I need it
+#
+# I've looked in to using an interface or abstract metaclass but all of those seem more complicated than doing this.
+def setup_container(some_class):
+    # NOTE: DO NOT use default properties except for None, we don't want to share data between classes.
+    # @property
+    # def dependencies(self):
+    #     c = []
+    #     for i in nc.inputs.values():
+    #         for l in i.links:
+    #             if l.is_hierarchy:
+    #             c.append(l.from_node)
+
+    # def default_prepare(self, bContext=None):
+    #     for dep in self.dependencies:
+
+    # def del_me(self):
+    #     from itertools import chain
+    #     links = []
+    #     for socket in chain(self.inputs, self.outputs):
+    #         for i in len(socket.links):
+    #             links.append(l)
+    #     for l in links:
+    #         del l
+    #     # just make sure to clean up links.
+
+    def flush_links(self):
+        for inp in self.inputs.values():
+            inp.flush_links()
+        for out in self.outputs.values():
+            out.flush_links()
+
+    functions = {
+        # "dependencies": dependencies,
+        # "num_dependencies": property(lambda self: len(self.dependencies)),
+        # "connections": some-property, # this one is potentially better as a property tho
+        # "hierarchy_connections": some-property, # I tried making this a getter property but it was a bit slower.
+        # importantly: these below are not properties.
+        "evaluate_input" :  lambda self, input_name, index=0 : evaluate_input(self, input_name, index),
+        "fill_parameters" : lambda self : fill_parameters(self),
+        'flush_links': flush_links,
+        # then I am not sure I want these because it is really easy to check
+        #   and it may be helpful to know if a node doesn't have one of these properties.
+        "bPrepare" : lambda self, bContext=None : None,
+        "bExecute" : lambda self, bContext=None : None,
+        "bFinalize" : lambda self, bContext=None : None,
+        # "__del__" : del_me,
+    }
+    for n, f in functions.items():
+        if not hasattr(some_class, n):
+            setattr(some_class, n, f)
+    some_class.__repr__ = lambda self : self.signature.__repr__()

+ 510 - 66
nodes_generic.py

@@ -2,8 +2,15 @@ import bpy
 from bpy.types import Node
 from .base_definitions import MantisNode
 
+
+from .utilities import (prRed, prGreen, prPurple, prWhite,
+                              prOrange,
+                              wrapRed, wrapGreen, wrapPurple, wrapWhite,
+                              wrapOrange,)
+
 def TellClasses():
     return [ InputFloatNode,
+             InputIntNode,
              InputVectorNode,
              InputBooleanNode,
              InputBooleanThreeTupleNode,
@@ -20,6 +27,8 @@ def TellClasses():
              
             #  ComposeMatrixNode,
              MetaRigMatrixNode,
+             UtilityMatrixFromCurve,
+             UtilityMatricesFromCurve,
             #  ScaleBoneLengthNode,
              UtilityMetaRigNode,
              UtilityBonePropertiesNode,
@@ -27,9 +36,28 @@ def TellClasses():
              UtilityFCurveNode,
              UtilityDriverNode,
              UtilitySwitchNode,
+             UtilityKeyframe,
              UtilityCombineThreeBoolNode,
              UtilityCombineVectorNode,
              UtilityCatStringsNode,
+             UtilityGetBoneLength,
+             UtilityPointFromBoneMatrix,
+             UtilitySetBoneLength,
+             UtilityMatrixSetLocation,
+             UtilityMatrixGetLocation,
+             UtilityMatrixFromXForm,
+             UtilityAxesFromMatrix,
+             UtilityBoneMatrixHeadTailFlip,
+             UtilityMatrixTransform,
+             UtilityTransformationMatrix,
+
+             UtilityIntToString,
+             UtilityArrayGet,
+             #
+             UtilityCompare,
+             UtilityChoose,
+             # for testing
+             UtilityPrint,
             ]
 
 
@@ -42,81 +70,110 @@ class InputFloatNode(Node, MantisNode):
     bl_idname = 'InputFloatNode'
     bl_label = "Float"
     bl_icon = 'NODE'
+    initialized : bpy.props.BoolProperty(default = False)
 
     def init(self, context):
         self.outputs.new('FloatSocket', "Float Input").input = True
+        self.initialized = True
+
+class InputIntNode(Node, MantisNode):
+    '''A node representing inheritance'''
+    bl_idname = 'InputIntNode'
+    bl_label = "Integer"
+    bl_icon = 'NODE'
+    initialized : bpy.props.BoolProperty(default = False)
+
+    def init(self, context):
+        self.outputs.new('IntSocket', "Integer").input = True
+        self.initialized = True
     
 class InputVectorNode(Node, MantisNode):
     '''A node representing inheritance'''
     bl_idname = 'InputVectorNode'
     bl_label = "Vector"
     bl_icon = 'NODE'
+    initialized : bpy.props.BoolProperty(default = False)
 
     def init(self, context):
         self.outputs.new('VectorSocket', "").input = True
+        self.initialized = True
 
 class InputBooleanNode(Node, MantisNode):
     '''A node representing inheritance'''
     bl_idname = 'InputBooleanNode'
     bl_label = "Boolean"
     bl_icon = 'NODE'
+    initialized : bpy.props.BoolProperty(default = False)
 
     def init(self, context):
         self.outputs.new('BooleanSocket', "").input = True
+        self.initialized = True
 
 class InputBooleanThreeTupleNode(Node, MantisNode):
     '''A node representing inheritance'''
     bl_idname = 'InputBooleanThreeTupleNode'
     bl_label = "Boolean Vector"
     bl_icon = 'NODE'
+    initialized : bpy.props.BoolProperty(default = False)
 
     def init(self, context):
         self.outputs.new('BooleanThreeTupleSocket', "")
+        self.initialized = True
 
 class InputRotationOrderNode(Node, MantisNode):
     '''A node representing inheritance'''
     bl_idname = 'InputRotationOrderNode'
     bl_label = "Rotation Order"
     bl_icon = 'NODE'
+    initialized : bpy.props.BoolProperty(default = False)
 
     def init(self, context):
         self.outputs.new('RotationOrderSocket', "").input = True
+        self.initialized = True
 
 class InputTransformSpaceNode(Node, MantisNode):
     '''A node representing inheritance'''
     bl_idname = 'InputTransformSpaceNode'
     bl_label = "Transform Space"
     bl_icon = 'NODE'
+    initialized : bpy.props.BoolProperty(default = False)
 
     def init(self, context):
         self.outputs.new('TransformSpaceSocket', "").input = True
+        self.initialized = True
 
 class InputStringNode(Node, MantisNode):
     '''A node representing inheritance'''
     bl_idname = 'InputStringNode'
     bl_label = "String"
     bl_icon = 'NODE'
+    initialized : bpy.props.BoolProperty(default = False)
 
     def init(self, context):
         self.outputs.new('StringSocket', "").input = True
+        self.initialized = True
 
 class InputQuaternionNode(Node, MantisNode):
     '''A node representing inheritance'''
     bl_idname = 'InputQuaternionNode'
     bl_label = "Quaternion"
     bl_icon = 'NODE'
+    initialized : bpy.props.BoolProperty(default = False)
 
     def init(self, context):
         self.outputs.new('QuaternionSocket', "").input = True
+        self.initialized = True
 
 class InputQuaternionNodeAA(Node, MantisNode):
     '''A node representing inheritance'''
     bl_idname = 'InputQuaternionNodeAA'
     bl_label = "Axis Angle"
     bl_icon = 'NODE'
+    initialized : bpy.props.BoolProperty(default = False)
 
     def init(self, context):
         self.outputs.new('QuaternionSocketAA', "").input = True
+        self.initialized = True
 
 
 class InputMatrixNode(Node, MantisNode):
@@ -128,6 +185,7 @@ class InputMatrixNode(Node, MantisNode):
     second_row : bpy.props.FloatVectorProperty(name="", size=4, default = (0.0, 1.0, 0.0, 0.0,))
     third_row  : bpy.props.FloatVectorProperty(name="", size=4, default = (0.0, 0.0, 1.0, 0.0,))
     fourth_row : bpy.props.FloatVectorProperty(name="", size=4, default = (0.0, 0.0, 0.0, 1.0,))
+    initialized : bpy.props.BoolProperty(default = False)
 
     def set_matrix(self):
         return (self.first_row[ 0], self.first_row[ 1], self.first_row[ 2], self.first_row[ 3],
@@ -137,11 +195,13 @@ class InputMatrixNode(Node, MantisNode):
 
     def init(self, context):
         self.outputs.new('MatrixSocket', "Matrix")
+        self.initialized = True
         
     def update_node(self, context):
         self.outputs["Matrix"].default_value = self.set_matrix()
 
     def draw_buttons(self, context, layout):
+        # return
         layout.prop(self, "first_row")
         layout.prop(self, "second_row")
         layout.prop(self, "third_row")
@@ -202,12 +262,14 @@ class ScaleBoneLengthNode(Node, MantisNode):
     bl_idname = 'ScaleBoneLength'
     bl_label = "Scale Bone Length"
     bl_icon = 'NODE'
+    initialized : bpy.props.BoolProperty(default = False)
 
     # === Optional Functions ===
     def init(self, context):
         self.inputs.new('MatrixSocket', "In Matrix")
         self.inputs.new('FloatSocket', "Factor")
         self.outputs.new('MatrixSocket', "Out Matrix")
+        self.initialized = True
 
 
 
@@ -221,6 +283,7 @@ class MetaRigMatrixNode(Node, MantisNode):
     second_row : bpy.props.FloatVectorProperty(name="", size=4, default = (0.0, 1.0, 0.0, 0.0,))
     third_row  : bpy.props.FloatVectorProperty(name="", size=4, default = (0.0, 0.0, 1.0, 0.0,))
     fourth_row : bpy.props.FloatVectorProperty(name="", size=4, default = (0.0, 0.0, 0.0, 1.0,))
+    initialized : bpy.props.BoolProperty(default = False)
 
     def set_matrix(self):
         return (self.first_row[ 0], self.first_row[ 1], self.first_row[ 2], self.first_row[ 3],
@@ -230,6 +293,7 @@ class MetaRigMatrixNode(Node, MantisNode):
 
     def init(self, context):
         self.outputs.new('MatrixSocket', "Matrix")
+        self.initialized = True
     
     def traverse(self, context):
         from mathutils import Matrix
@@ -248,6 +312,37 @@ class MetaRigMatrixNode(Node, MantisNode):
         mat_sock.default_value = self.set_matrix()
 
 
+class UtilityMatrixFromCurve(Node, MantisNode):
+    """Gets a matrix from a curve."""
+    bl_idname = "UtilityMatrixFromCurve"
+    bl_label = "Matrix from Curve"
+    bl_icon = "NODE"
+    
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        curv = self.inputs.new("EnumCurveSocket", "Curve")
+        curv.icon = "OUTLINER_OB_CURVE"
+        self.inputs.new('IntSocket', 'Total Divisions')
+        self.inputs.new('IntSocket', 'Matrix Index')
+        self.outputs.new("MatrixSocket", "Matrix")
+        self.initialized = True
+
+class UtilityMatricesFromCurve(Node, MantisNode):
+    """Gets a matrix from a curve."""
+    bl_idname = "UtilityMatricesFromCurve"
+    bl_label = "Matrices from Curve"
+    bl_icon = "NODE"
+    
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        curv = self.inputs.new("EnumCurveSocket", "Curve")
+        curv.icon = "OUTLINER_OB_CURVE"
+        self.inputs.new('IntSocket', 'Total Divisions')
+        o = self.outputs.new("MatrixSocket", "Matrices")
+        o.display_shape = 'SQUARE_DOT'
+        self.initialized = True
 
 class UtilityMetaRigNode(Node, MantisNode):
     """Gets a matrix from a meta-rig bone."""
@@ -257,6 +352,7 @@ class UtilityMetaRigNode(Node, MantisNode):
     
     armature:bpy.props.StringProperty()
     pose_bone:bpy.props.StringProperty()
+    initialized : bpy.props.BoolProperty(default = False)
     
     def init(self, context):
         armt = self.inputs.new("EnumMetaRigSocket", "Meta-Armature")
@@ -265,6 +361,7 @@ class UtilityMetaRigNode(Node, MantisNode):
         bone.icon = "BONE_DATA"
         bone.hide=True
         self.outputs.new("MatrixSocket", "Matrix")
+        self.initialized = True
     
     def display_update(self, parsed_tree, context):
         from .base_definitions import get_signature_from_edited_tree
@@ -283,6 +380,7 @@ class UtilityBonePropertiesNode(Node, MantisNode):
     bl_label = "Bone Properties"
     bl_icon = "NODE"
     #bl_width_default = 250
+    initialized : bpy.props.BoolProperty(default = False)
     
     def init(self, context):
         self.outputs.new("ParameterStringSocket", "matrix")
@@ -294,6 +392,7 @@ class UtilityBonePropertiesNode(Node, MantisNode):
         self.outputs.new("ParameterStringSocket", "rotation")
         self.outputs.new("ParameterStringSocket", "location")
         self.outputs.new("ParameterStringSocket", "scale")
+        self.initialized = True
         
         for o in self.outputs:
             o.text_only = True
@@ -303,52 +402,66 @@ class UtilityDriverVariableNode(Node, MantisNode):
     bl_idname = "UtilityDriverVariable"
     bl_label = "Driver Variable"
     bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
     
     
     def init(self, context):
-        self.inputs.new("EnumDriverVariableType", "Variable Type")
-        self.inputs.new("ParameterStringSocket", "Property")
-        self.inputs.new("IntSocket", "Property Index")
-        self.inputs.new("EnumDriverVariableEvaluationSpace", "Evaluation Space")
-        self.inputs.new("EnumDriverRotationMode", "Rotation Mode")
-        self.inputs.new("xFormSocket", "xForm 1")
-        self.inputs.new("xFormSocket", "xForm 2")
+        self.inputs.new("EnumDriverVariableType", "Variable Type")                 # 0
+        self.inputs.new("ParameterStringSocket", "Property")                       # 1
+        self.inputs.new("IntSocket", "Property Index")                             # 2
+        self.inputs.new("EnumDriverVariableTransformChannel", "Transform Channel") # 3
+        self.inputs.new("EnumDriverVariableEvaluationSpace", "Evaluation Space")   # 4
+        self.inputs.new("EnumDriverRotationMode", "Rotation Mode")                 # 5
+        self.inputs.new("xFormSocket", "xForm 1")                                  # 6
+        self.inputs.new("xFormSocket", "xForm 2")                                  # 7
         self.outputs.new("DriverVariableSocket", "Driver Variable")
+        self.initialized = True
         
-    def update_on_socket_change(self, context):
-        self.update()
+    # def update_on_socket_change(self, context):
+    #     self.update()
     
     def display_update(self, parsed_tree, context):
         from .base_definitions import get_signature_from_edited_tree
-        if context.space_data:
-            node_tree = context.space_data.path[0].node_tree
-            nc = parsed_tree.get(get_signature_from_edited_tree(self, context))
-            if nc:
-                driver_type = nc.evaluate_input("Variable Type")
-                if driver_type == 'SINGLE_PROP':
-                    self.inputs[0].hide = False
-                    self.inputs[1].hide = False
-                    self.inputs[2].hide = False
-                    self.inputs[3].hide = False
-                    self.inputs[4].hide = False
-                    self.inputs[5].hide = False
-                    self.inputs[6].hide = True
-                elif driver_type == 'LOC_DIFF':
-                    self.inputs[0].hide = False
-                    self.inputs[1].hide = True
-                    self.inputs[2].hide = True
-                    self.inputs[3].hide = True
-                    self.inputs[4].hide = True
-                    self.inputs[5].hide = False
-                    self.inputs[6].hide = False
-                elif driver_type == 'ROTATION_DIFF':
-                    self.inputs[0].hide = False
-                    self.inputs[1].hide = True
-                    self.inputs[2].hide = True
-                    self.inputs[3].hide = True
-                    self.inputs[4].hide = False
-                    self.inputs[5].hide = False
-                    self.inputs[6].hide = False
+        if self.inputs["Variable Type"].is_linked:
+            if context.space_data:
+                node_tree = context.space_data.path[0].node_tree
+                nc = parsed_tree.get(get_signature_from_edited_tree(self, context))
+                if nc:
+                    driver_type = nc.evaluate_input("Variable Type")
+        else:
+            driver_type = self.inputs[0].default_value
+        if driver_type == 'SINGLE_PROP':
+            self.inputs[1].hide = False
+            self.inputs[2].hide = False
+            self.inputs[3].hide = True
+            self.inputs[4].hide = False
+            self.inputs[5].hide = False
+            self.inputs[6].hide = False
+            self.inputs[7].hide = True
+        elif driver_type == 'LOC_DIFF':
+            self.inputs[1].hide = True
+            self.inputs[2].hide = True
+            self.inputs[3].hide = True
+            self.inputs[4].hide = True
+            self.inputs[5].hide = True
+            self.inputs[6].hide = False
+            self.inputs[7].hide = False
+        elif driver_type == 'ROTATION_DIFF':
+            self.inputs[1].hide = True
+            self.inputs[2].hide = True
+            self.inputs[3].hide = True
+            self.inputs[4].hide = True
+            self.inputs[5].hide = False
+            self.inputs[6].hide = False
+            self.inputs[7].hide = False
+        elif driver_type == 'TRANSFORMS':
+            self.inputs[1].hide = True
+            self.inputs[2].hide = True
+            self.inputs[3].hide = False
+            self.inputs[4].hide = False
+            self.inputs[5].hide = False
+            self.inputs[6].hide = False
+            self.inputs[7].hide = True
     
 
 class UtilityFCurveNode(Node, MantisNode):
@@ -357,38 +470,41 @@ class UtilityFCurveNode(Node, MantisNode):
     bl_label = "fCurve"
     bl_icon = "NODE"
     
-    use_kf_nodes   : bpy.props.BoolProperty(default=False)
-    fake_fcurve_ob : bpy.props.PointerProperty(type=bpy.types.Object)
+    use_kf_nodes   : bpy.props.BoolProperty(default=True)
+    # fake_fcurve_ob : bpy.props.PointerProperty(type=bpy.types.Object)
+    initialized : bpy.props.BoolProperty(default = False)
     
     def init(self, context):
         self.outputs.new("FCurveSocket", "fCurve")
-        if not self.fake_fcurve_ob:
-            ob = bpy.data.objects.new("fake_ob_"+self.name, None)
-            self.fake_fcurve_ob = ob
-            ob.animation_data_create()
-            ob.animation_data.action = bpy.data.actions.new('fake_action_'+self.name)
-            fc = ob.animation_data.action.fcurves.new('location', index=0, action_group='location')
-            fc.keyframe_points.add(2)
-            kf0 = fc.keyframe_points[0]; kf0.co_ui = (0, 0)
-            kf1 = fc.keyframe_points[1]; kf1.co_ui = (1, 1)
-            #
-            kf0.interpolation = 'BEZIER'
-            kf0.handle_left_type  = 'AUTO_CLAMPED'
-            kf0.handle_right_type = 'AUTO_CLAMPED'
-            kf1.interpolation = 'BEZIER'
-            kf1.handle_left_type  = 'AUTO_CLAMPED'
-            kf1.handle_right_type = 'AUTO_CLAMPED'
+        # if not self.fake_fcurve_ob:
+        #     ob = bpy.data.objects.new("fake_ob_"+self.name, None)
+        #     self.fake_fcurve_ob = ob
+        #     ob.animation_data_create()
+        #     ob.animation_data.action = bpy.data.actions.new('fake_action_'+self.name)
+        #     fc = ob.animation_data.action.fcurves.new('location', index=0, action_group='location')
+        #     fc.keyframe_points.add(2)
+        #     kf0 = fc.keyframe_points[0]; kf0.co_ui = (0, 0)
+        #     kf1 = fc.keyframe_points[1]; kf1.co_ui = (1, 1)
+        #     #
+        #     kf0.interpolation = 'BEZIER'
+        #     kf0.handle_left_type  = 'AUTO_CLAMPED'
+        #     kf0.handle_right_type = 'AUTO_CLAMPED'
+        #     kf1.interpolation = 'BEZIER'
+        #     kf1.handle_left_type  = 'AUTO_CLAMPED'
+        #     kf1.handle_right_type = 'AUTO_CLAMPED'
             #
+        self.initialized = True
             
     def draw_buttons(self, context, layout):
-        if self.use_kf_nodes:
-            layout.prop(self, "use_kf_nodes",  text="[ Use fCurve data ]", toggle=True, invert_checkbox=True)
-            layout.operator( 'mantis.fcurve_node_add_kf' )
-            if (len(self.inputs) > 0):
-                layout.operator( 'mantis.fcurve_node_remove_kf' )
-        else:
-            layout.prop(self, "use_kf_nodes",  text="[ Use Keyframe Nodes ]", toggle=True)
-            layout.operator('mantis.edit_fcurve_node')
+        # return
+        # if self.use_kf_nodes:
+            # layout.prop(self, "use_kf_nodes",  text="[ Use fCurve data ]", toggle=True, invert_checkbox=True)
+        layout.operator( 'mantis.fcurve_node_add_kf' )
+        if (len(self.inputs) > 0):
+            layout.operator( 'mantis.fcurve_node_remove_kf' )
+        # else:
+            # layout.prop(self, "use_kf_nodes",  text="[ Use Keyframe Nodes ]", toggle=True)
+            # layout.operator('mantis.edit_fcurve_node')
         
             
     
@@ -410,12 +526,14 @@ class UtilityDriverNode(Node, MantisNode):
     bl_idname = "UtilityDriver"
     bl_label = "Driver"
     bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
     
     def init(self, context):
         self.inputs.new("EnumDriverType", "Driver Type")
         self.inputs.new("FCurveSocket", "fCurve")
         self.inputs.new("StringSocket", "Expression")
         self.outputs.new("DriverSocket", "Driver")
+        self.initialized = True
         
     def update(self):
         return
@@ -438,8 +556,9 @@ class UtilityDriverNode(Node, MantisNode):
                 self.inputs["Expression"].hide = True
     
     def draw_buttons(self, context, layout):
+        # return
         layout.operator( 'mantis.driver_node_add_variable' )
-        if (len(self.inputs) > 2):
+        if (len(self.inputs) > 3):
             layout.operator( 'mantis.driver_node_remove_variable' )
 
 class UtilitySwitchNode(Node, MantisNode):
@@ -447,19 +566,22 @@ class UtilitySwitchNode(Node, MantisNode):
     bl_idname = "UtilitySwitch"
     bl_label = "Switch"
     bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
     
     def init(self, context):
-        self.inputs.new("xFormSocket", "xForm")
+        # self.inputs.new("xFormSocket", "xForm")
         self.inputs.new("ParameterStringSocket", "Parameter")
         self.inputs.new("IntSocket", "Parameter Index")
         self.inputs.new("BooleanSocket", "Invert Switch")
         self.outputs.new("DriverSocket", "Driver")
+        self.initialized = True
 
 class UtilityCombineThreeBoolNode(Node, MantisNode):
     """Combines three booleans into a three-bool."""
     bl_idname = "UtilityCombineThreeBool"
     bl_label = "CombineThreeBool"
     bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
     
     def init(self, context):
         self.inputs.new("BooleanSocket", "X")
@@ -468,11 +590,13 @@ class UtilityCombineThreeBoolNode(Node, MantisNode):
         self.outputs.new("BooleanThreeTupleSocket", "Three-Bool")
         # this node should eventually just be a Combine Boolean Three-Tuple node
         # and the "Driver" output will need to be figured out some other way
+        self.initialized = True
 class UtilityCombineVectorNode(Node, MantisNode):
     """Combines three floats into a vector."""
     bl_idname = "UtilityCombineVector"
     bl_label = "CombineVector"
     bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
     
     def init(self, context):
         self.inputs.new("FloatSocket", "X")
@@ -481,43 +605,363 @@ class UtilityCombineVectorNode(Node, MantisNode):
         self.outputs.new("VectorSocket", "Vector")
         # this node should eventually just be a Combine Boolean Three-Tuple node
         # and the "Driver" output will need to be figured out some other way
+        self.initialized = True
         
 class UtilityCatStringsNode(Node, MantisNode):
     """Adds a suffix to a string"""
     bl_idname = "UtilityCatStrings"
     bl_label = "Concatenate Strings"
     bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
     
     def init(self, context):
         self.inputs.new("StringSocket", "String_1")
         self.inputs.new("StringSocket", "String_2")
         self.outputs.new("StringSocket", "OutputString")
+        self.initialized = True
     
 class InputLayerMaskNode(Node, MantisNode):
     """Represents a layer mask for a bone."""
     bl_idname = "InputLayerMaskNode"
     bl_label = "Layer Mask"
     bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
     
     def init(self, context):
         self.outputs.new("LayerMaskInputSocket", "Layer Mask")
+        self.initialized = True
 
 class InputExistingGeometryObjectNode(Node, MantisNode):
     """Represents an existing geometry object from within the scene."""
     bl_idname = "InputExistingGeometryObject"
     bl_label = "Existing Object"
     bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
     
     def init(self, context):
         self.inputs.new("StringSocket", "Name")
         self.outputs.new("xFormSocket", "Object")
+        self.initialized = True
+    
 
 class InputExistingGeometryDataNode(Node, MantisNode):
     """Represents a mesh or curve datablock from the scene."""
     bl_idname = "InputExistingGeometryData"
     bl_label = "Existing Geometry"
     bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
     
     def init(self, context):
         self.inputs.new("StringSocket", "Name")
         self.outputs.new("GeometrySocket", "Geometry")
+        self.initialized = True
+
+class UtilityGetBoneLength(Node, MantisNode):
+    """Returns the length of the bone from its matrix."""
+    bl_idname = "UtilityGetBoneLength"
+    bl_label = "Get Bone Length"
+    bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        self.inputs.new("MatrixSocket", "Bone Matrix")
+        self.outputs.new("FloatSocket", "Bone Length")
+        self.initialized = True
+
+# TODO: make it work with BBones!
+class UtilityPointFromBoneMatrix(Node, MantisNode):
+    """Returns a point representing the location along a bone, given a matrix representing that bone's shape."""
+    bl_idname = "UtilityPointFromBoneMatrix"
+    bl_label = "Point from Bone Matrix"
+    bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        self.inputs.new("MatrixSocket", "Bone Matrix")
+        self.inputs.new("FloatFactorSocket", "Head/Tail")
+        self.outputs.new("VectorSocket", "Point")
+        self.initialized = True
+class UtilitySetBoneLength(Node, MantisNode):
+    """Sets the length of a bone matrix."""
+    bl_idname = "UtilitySetBoneLength"
+    bl_label = "Set Bone Matrix Length"
+    bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        self.inputs.new("MatrixSocket", "Bone Matrix")
+        self.inputs.new("FloatSocket", "Length")
+        self.outputs.new("MatrixSocket", "Bone Matrix")
+        self.initialized = True
+
+class UtilityKeyframe(Node, MantisNode):
+    """A keyframe for a FCurve"""
+    bl_idname = "UtilityKeyframe"
+    bl_label = "KeyFrame"
+    bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        # x and y
+        # output is keyframe
+        # self.inputs.new("EnumKeyframeInterpolationTypeSocket", "Interpolation")
+        # self.inputs.new("EnumKeyframeBezierHandleType", "Left Handle Type")
+        # self.inputs.new("EnumKeyframeBezierHandleType", "Right Handle Type")
+
+        # self.inputs.new("FloatSocket", "Left Handle Distance")
+        # self.inputs.new("FloatSocket", "Left Handle Value")
+        # self.inputs.new("FloatSocket", "Right Handle Frame")
+        # self.inputs.new("FloatSocket", "Right Handle Value")
+
+        self.inputs.new("FloatSocket", "Frame")
+        self.inputs.new("FloatSocket", "Value")
+        self.outputs.new("KeyframeSocket", "Keyframe")
+        # there will eventually be inputs for e.g. key type, key handles, etc.
+        # right now I am gonna hardcode LINEAR keyframes so I don't have to deal with anything else
+        # TODO TODO TODO
+    
+    
+    # def display_update(self, parsed_tree, context):
+    #     if context.space_data:
+    #         nc = parsed_tree.get(get_signature_from_edited_tree(self, context))
+    #         if nc.evaluate_input("Interpolation") in ["CONSTANT", "LINEAR"]:
+    #             for inp in self.inputs[1:6]:
+    #                 inp.hide = True
+    #         else:
+    #             if nc.evaluate_input("Left Handle Type") in ["FREE", "ALIGNED"]:
+
+    #             for inp in self.inputs[1:6]:
+    #                 inp.hide = False
+        self.initialized = True
+
+
+class UtilityBoneMatrixHeadTailFlip(Node, MantisNode):
+    """Flips a bone matrix so that the head is where the tail was and visa versa."""
+    bl_idname = "UtilityBoneMatrixHeadTailFlip"
+    bl_label = "Flip Head/Tail"
+    bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        self.inputs.new("MatrixSocket", "Bone Matrix")
+        self.outputs.new("MatrixSocket", "Bone Matrix")
+        self.initialized = True
+
+class UtilityMatrixTransform(Node, MantisNode):
+    """Transforms a matrix by another."""
+    bl_idname = "UtilityMatrixTransform"
+    bl_label = "Matrix Transform"
+    bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        self.inputs.new("MatrixSocket", "Matrix 1")
+        self.inputs.new("MatrixSocket", "Matrix 2")
+        self.outputs.new("MatrixSocket", "Out Matrix")
+        self.initialized = True
+
+class UtilityMatrixSetLocation(Node, MantisNode):
+    """Sets a matrix's location."""
+    bl_idname = "UtilityMatrixSetLocation"
+    bl_label = "Set Matrix Location"
+    bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        self.inputs.new("MatrixSocket", "Matrix")
+        self.inputs.new("VectorSocket", "Location")
+        self.outputs.new("MatrixSocket", "Matrix")
+        self.initialized = True
+
+class UtilityMatrixGetLocation(Node, MantisNode):
+    """Gets a matrix's location."""
+    bl_idname = "UtilityMatrixGetLocation"
+    bl_label = "Get Matrix Location"
+    bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        self.inputs.new("MatrixSocket", "Matrix")
+        self.outputs.new("VectorSocket", "Location")
+        self.initialized = True
+
+class UtilityTransformationMatrix(Node, MantisNode):
+    """Constructs a matrix representing a transformation"""
+    bl_idname = "UtilityTransformationMatrix"
+    bl_label = "Transformation Matrix"
+    bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        # first input is a transformation type - translation, rotation, or scale
+        #                         rotation is an especially annoying feature because it can take multiple types
+        #   so Euler, axis/angle, quaternion, matrix...
+        #   for now I am only going to implement axis-angle
+        # it should get an axis and a magnitude
+        # self.inputs.new("MatrixSocket", "Bone Matrix")
+        
+        self.inputs.new("MatrixTransformOperation", "Operation")
+        self.inputs.new("VectorSocket", "Vector")
+        self.inputs.new("FloatSocket", "W")
+        self.outputs.new("MatrixSocket", "Matrix")
+        self.initialized = True
+
+class UtilityMatrixFromXForm(Node, MantisNode):
+    """Returns the matrix of the given xForm node."""
+    bl_idname = "UtilityMatrixFromXForm"
+    bl_label = "Matrix of xForm"
+    bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        self.inputs.new("xFormSocket", "xForm")
+        self.outputs.new("MatrixSocket", "Matrix")
+        self.initialized = True
+
+class UtilityAxesFromMatrix(Node, MantisNode):
+    """Returns the axes of the matrix."""
+    bl_idname = "UtilityAxesFromMatrix"
+    bl_label = "Axes of Matrix"
+    bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        self.inputs.new("MatrixSocket", "Matrix")
+        self.outputs.new("VectorSocket", "X Axis")
+        self.outputs.new("VectorSocket", "Y Axis")
+        self.outputs.new("VectorSocket", "Z Axis")
+        self.initialized = True
+
+class UtilityIntToString(Node, MantisNode):
+    """Converts a number to a string"""
+    bl_idname = "UtilityIntToString"
+    bl_label = "Number String"
+    bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        
+        self.inputs.new("IntSocket", "Number")
+        self.inputs.new("IntSocket", "Zero Padding")
+        self.outputs.new("StringSocket", "String")
+        self.initialized = True
+
+
+class UtilityArrayGet(Node, MantisNode):
+    """Gets a value from an array at a specified index."""
+    bl_idname = "UtilityArrayGet"
+    bl_label  = "Array Get"
+    bl_icon   = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        self.inputs.new('EnumArrayGetOptions', 'OoB Behaviour')
+        self.inputs.new("IntSocket", "Index")
+        s = self.inputs.new("WildcardSocket", "Array", use_multi_input=True)
+        s.display_shape = 'SQUARE_DOT'
+        self.outputs.new("WildcardSocket", "Output")
+        self.initialized = True
+    
+    def update(self):
+        wildcard_color = (0.0,0.0,0.0,0.0)
+        if self.inputs['Array'].is_linked == False:
+            self.inputs['Array'].color = wildcard_color
+            self.outputs['Output'].color = wildcard_color
+
+    def insert_link(self, link):
+        prGreen(link.from_node.name, link.from_socket.identifier, link.to_node.name, link.to_socket.identifier)
+        if link.to_socket.identifier == self.inputs['Array'].identifier:
+            from_socket = link.from_socket
+            print (from_socket.color)
+            if hasattr(from_socket, "color"):
+                self.inputs['Array'].color = from_socket.color
+                self.outputs['Output'].color = from_socket.color
+
+
+class UtilityCompare(Node, MantisNode):
+    """Compares two inputs and produces a boolean output"""
+    bl_idname = "UtilityCompare"
+    bl_label = "Compare"
+    bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        self.inputs.new("WildcardSocket", "A")
+        self.inputs.new("WildcardSocket", "B")
+        self.outputs.new("BooleanSocket", "Result")
+        self.initialized = True
+    
+    def update(self):
+        wildcard_color = (0.0,0.0,0.0,0.0)
+        if self.inputs['A'].is_linked == False:
+            self.inputs['A'].color = wildcard_color
+        if self.inputs['B'].is_linked == False:
+            self.inputs['B'].color = wildcard_color
+
+    def insert_link(self, link):
+        if link.to_socket.identifier == self.inputs['A'].identifier:
+            self.inputs['A'].color = from_socket.color_simple
+            if hasattr(from_socket, "color"):
+                self.inputs['A'].color = from_socket.color
+        if link.to_socket.identifier == self.inputs['B'].identifier:
+            self.inputs['B'].color = from_socket.color_simple
+            if hasattr(from_socket, "color"):
+                self.inputs['B'].color = from_socket.color
+
+class UtilityChoose(Node, MantisNode):
+    """Chooses an output"""
+    bl_idname = "UtilityChoose"
+    bl_label = "Choose"
+    bl_icon = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        
+        self.inputs.new("BooleanSocket", "Condition")
+        self.inputs.new("WildcardSocket", "A")
+        self.inputs.new("WildcardSocket", "B")
+        self.outputs.new("WildcardSocket", "Result")
+        self.initialized = True
+    
+    def update(self):
+        wildcard_color = (0.0,0.0,0.0,0.0)
+        if self.inputs['A'].is_linked == False:
+            self.inputs['A'].color = wildcard_color
+            self.outputs['Result'].color = (1.0,0.0,0.0,0.0) # red for Error
+        if self.inputs['B'].is_linked == False:
+            self.inputs['B'].color = wildcard_color
+            self.outputs['Result'].color = (1.0,0.0,0.0,0.0)
+        # if both inputs are the same color, then use that color for the result
+        if self.inputs['A'].is_linked and self.inputs['A'].color == self.inputs['B'].color:
+            self.outputs['Result'].color = self.inputs['A'].color
+        #
+        if ((self.inputs['A'].is_linked and self.inputs['B'].is_linked) and
+            (self.inputs['A'].links[0].from_socket.bl_idname != self.inputs['B'].links[0].from_socket.bl_idname)):
+            self.inputs['A'].color = (1.0,0.0,0.0,0.0)
+            self.inputs['B'].color = (1.0,0.0,0.0,0.0)
+            self.outputs['Result'].color = (1.0,0.0,0.0,0.0)
+
+    def insert_link(self, link):
+        if link.to_socket.identifier == self.inputs['A'].identifier:
+            self.inputs['A'].color = from_socket.color_simple
+            if hasattr(from_socket, "color"):
+                self.inputs['A'].color = from_socket.color
+        if link.to_socket.identifier == self.inputs['B'].identifier:
+            self.inputs['B'].color = from_socket.color_simple
+            if hasattr(from_socket, "color"):
+                self.inputs['B'].color = from_socket.color
+
+
+
+
+class UtilityPrint(Node, MantisNode):
+    """A utility used to print arbitrary values."""
+    bl_idname = "UtilityPrint"
+    bl_label  = "Print"
+    bl_icon   = "NODE"
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        self.inputs.new("WildcardSocket", "Input")
+        self.initialized = True
+    

+ 129 - 44
ops_generate_tree.py

@@ -29,6 +29,22 @@ def create_inheritance_node(pb, parent_name, bone_inherit_node, node_tree):
     return parent_node
 
 def get_pretty_name(name):
+    if   name == "bbone_curveinx": return "BBone X Curve-In"
+    elif name == "bbone_curveinz": return "BBone Z Curve-In"
+    elif name == "bbone_curveoutx": return "BBone X Curve-Out"
+    elif name == "bbone_curveoutz": return "BBone Z Curve-Out"
+    elif name == "BBone HQ Deformation":
+        raise NotImplementedError(wrapRed("I wasn't expecting this property to be driven lol why would you even want to do that"))
+    
+    elif name == "bbone_handle_type_start": return "BBone Start Handle Type"
+    elif name == "bbone_handle_type_end": return "BBone End Handle Type"
+    elif name == "bbone_x": return "BBone X Size"
+    elif name == "bbone_z": return "BBone Z Size"
+    elif name == "bbone_rollin": return "BBone Roll-In"
+    elif name == "bbone_rollout": return "BBone Roll-Out"
+    elif name == "bbone_scalein": return "BBone Scale-In"
+    elif name == "bbone_scaleout": return "BBone Scale-Out"
+
     pretty = name.replace("_", " ")
     words = pretty.split(" "); pretty = ''
     for word in words:
@@ -64,7 +80,7 @@ def create_relationship_node_for_constraint(node_tree, c):
         return None
     
     
-def fill_parameters(node, c):
+def fill_parameters(node, c, context):
     # just try the basic parameters...
     
     node.mute = not c.enabled
@@ -75,7 +91,8 @@ def fill_parameters(node, c):
     try:
         owner_space = c.owner_space
         if c.owner_space == 'CUSTOM':
-            raise NotImplementedError("Custom Space is a TODO")
+            pass
+            #raise NotImplementedError("Custom Space is a TODO")
         if ( input := node.inputs.get("Owner Space") ):
             input.default_value = owner_space
     except AttributeError:
@@ -84,7 +101,8 @@ def fill_parameters(node, c):
     try:
         target_space = c.target_space
         if c.target_space == 'CUSTOM':
-            raise NotImplementedError("Custom Space is a TODO")
+            pass
+            #raise NotImplementedError("Custom Space is a TODO")
         if ( input := node.inputs.get("Target Space") ):
             input.default_value = target_space
     except AttributeError:
@@ -196,7 +214,8 @@ def fill_parameters(node, c):
         node.inputs["Use Envelopes"].default_value = c.use_bone_envelopes
         node.inputs["Use Current Location"].default_value = c.use_current_location
         for i in range(len(c.targets)):
-            bpy.ops.mantis.link_armature_node_add_target({'node':node})
+            with context.temp_override(node=node):
+                bpy.ops.mantis.link_armature_node_add_target()
     elif (c.type == 'SPLINE_IK'):
         node.inputs["Chain Length"].default_value = c.chain_count
         node.inputs["Even Divisions"].default_value = c.use_even_divisions
@@ -349,7 +368,15 @@ def setup_vp_settings(bone_node, pb, do_after, node_tree):
     bone_node.inputs["Custom Object Rotation"].default_value = pb.custom_shape_rotation_euler
     bone_node.inputs["Custom Object Scale to Bone Length"].default_value = pb.use_custom_shape_bone_size
     bone_node.inputs["Custom Object Wireframe"].default_value = pb.bone.show_wire
-    bone_node.inputs["Layer Mask"].default_value = pb.bone.layers
+    # bone_node.inputs["Layer Mask"].default_value = pb.bone.layers
+
+    collection_membership = ''
+    for col in pb.bone.collections:
+        # TODO: implement this!
+        pass
+    bone_node.inputs["Bone Collection"].default_value = collection_membership
+
+
     
     if (shape_ob := pb.custom_shape):
         shape_n = None
@@ -377,7 +404,7 @@ def setup_vp_settings(bone_node, pb, do_after, node_tree):
 
 
 def setup_df_settings(bone_node, pb):
-        bone_node.inputs["Deform"] = pb.bone.use_deform
+        bone_node.inputs["Deform"].default_value = pb.bone.use_deform
         # TODO: get the rest of these working
         # eb.envelope_distance     = self.evaluate_input("Envelope Distance")
         # eb.envelope_weight       = self.evaluate_input("Envelope Weight")
@@ -385,7 +412,7 @@ def setup_df_settings(bone_node, pb):
         # eb.head_radius           = self.evaluate_input("Envelope Head Radius")
         # eb.tail_radius           = self.evaluate_input("Envelope Tail Radius")
 
-def create_driver(in_node_name, out_node_name, armOb, finished_drivers, switches, driver_vars, fcurves, drivers, node_tree):
+def create_driver(in_node_name, out_node_name, armOb, finished_drivers, switches, driver_vars, fcurves, drivers, node_tree, context):
     # TODO: CLEAN this ABOMINATION
     print ("DRIVER: ", in_node_name, out_node_name)
     in_node  = node_tree.nodes[ in_node_name]
@@ -419,6 +446,11 @@ def create_driver(in_node_name, out_node_name, armOb, finished_drivers, switches
             # OK, let's prepare before making the node
             #  we want to reuse existing nodes if possible.
             target_string = fc.driver.variables[0].targets[0].data_path
+            if target_string == "":
+                for var in fc.driver.variables:
+                    print (var)
+                    print (var.name)
+                    print (var.targets)
             bone = target_string.split("pose.bones[\"")[1]
             bone = bone.split("\"]")[0]
             bone_node = node_tree.nodes.get(bone)
@@ -437,20 +469,20 @@ def create_driver(in_node_name, out_node_name, armOb, finished_drivers, switches
             fail = False
             switch_node = None
             for n in switches:
+                # if n.inputs[0].is_linked:
+                #     if n.inputs[0].links[0].from_node != bone_node:
+                #         fail = True
                 if n.inputs[0].is_linked:
                     if n.inputs[0].links[0].from_node != bone_node:
                         fail = True
-                if n.inputs[1].is_linked:
-                    if n.inputs[1].links[0].from_node != bone_node:
-                        fail = True
-                    if n.inputs[1].links[0].from_socket != bone_node.outputs.get(p_string):
+                    if n.inputs[0].links[0].from_socket != bone_node.outputs.get(p_string):
                         fail = True
                 else:
-                    if n.inputs[1].default_value != p_string:
+                    if n.inputs[0].default_value != p_string:
                         fail = True
-                if n.inputs[2].default_value != fc.array_index:
+                if n.inputs[1].default_value != fc.array_index:
                     fail = True
-                if n.inputs[3].default_value != inverted:
+                if n.inputs[2].default_value != inverted:
                     fail = True
                 if not fail:
                     switch_node = n
@@ -458,10 +490,13 @@ def create_driver(in_node_name, out_node_name, armOb, finished_drivers, switches
             else:
                 # make and connect the switch node
                 switch_node = node_tree.nodes.new("UtilitySwitch"); switches.append(switch_node)
-                node_tree.links.new(bone_node.outputs["xForm Out"], switch_node.inputs[0])
-                node_tree.links.new(bone_node.outputs[p_string], switch_node.inputs[1])
-                switch_node.inputs[2].default_value = fc.array_index
-                switch_node.inputs[3].default_value = inverted
+                # node_tree.links.new(bone_node.outputs["xForm Out"], switch_node.inputs[0])
+                try:
+                    node_tree.links.new(bone_node.outputs[p_string], switch_node.inputs[0])
+                except KeyError:
+                    prRed("this is such bad code lol fix this", p_string)
+                switch_node.inputs[1].default_value = fc.array_index
+                switch_node.inputs[2].default_value = inverted
                 #print ("   Inverted?  ", inverted, (fc.evaluate(0) == 1) and (fc.evaluate(1) == 0), switch_node.inputs[3].default_value)
                 if not inverted:
                     print ("    --> Check this node: %s" % switch_node.name)
@@ -490,6 +525,7 @@ def create_driver(in_node_name, out_node_name, armOb, finished_drivers, switches
                     target1, target2, bone_target, bone_target2 = [None]*4
                     var_data = {}
                     var_data["Variable Type"] = var.type
+                    var_data["Property"] = ""
                     if len(var.targets) >= 1:
                         target1 = var.targets[0]
                         if (var_data["Variable Type"] != 'SINGLE_PROP'):
@@ -514,8 +550,9 @@ def create_driver(in_node_name, out_node_name, armOb, finished_drivers, switches
                                 "SCALE_Y"   : ('scale', 1),
                                 "SCALE_Z"   : ('scale', 2),
                                 "SCALE_AVG" : ('scale', 3), }
-                            if (var.transform_type in transform_channel_map.keys()):
-                                var_data["Property"], var_data["Property Index"] = transform_channel_map[var.transform_type]
+                            # if (var.transform_type in transform_channel_map.keys()):
+                            #     var_data["Property"], var_data["Property Index"] = transform_channel_map[var.transform_type]
+                            prRed("I am pretty sure this thing does not friggin work with whatever it is I commented above...")
                             var_data["Evaluation Space"] = var.targets[0].transform_space
                             var_data["Rotation Mode"] = var.targets[0].rotation_mode
                     if len(var.targets) == 2:
@@ -564,7 +601,12 @@ def create_driver(in_node_name, out_node_name, armOb, finished_drivers, switches
                         var_node = node_tree.nodes.new("UtilityDriverVariable"); driver_vars.append(var_node)
                         prRed("Creating Node: %s" % var_node.name)
                         for key, value in var_data.items():
-                            var_node.inputs[key].default_value = value
+                            try:
+                                var_node.inputs[key].default_value = value
+                            except TypeError as e: # maybe it is a variable\
+                                if key == "Variable Type":
+                                    var_node.inputs[key].default_value = "SINGLE_PROP"
+                                else: raise e
                         if (target1 and bone_target):
                             node_tree.links.new(node_tree.nodes[bone_target].outputs['xForm Out'], var_node.inputs['xForm 1'])
                         elif (target1 and not bone_target):
@@ -617,16 +659,23 @@ def create_driver(in_node_name, out_node_name, armOb, finished_drivers, switches
                     break
                 else:
                     fCurve_node = node_tree.nodes.new("UtilityFCurve")
-                    fc_ob = fCurve_node.fake_fcurve_ob
-                    node_fc = fc_ob.animation_data.action.fcurves[0]
-                    fcurves.append(fCurve_node)
-                    while(node_fc.keyframe_points): # clear it, it has a default FC
-                        node_fc.keyframe_points.remove(node_fc.keyframe_points[0], fast=True)
-                    node_fc.update()
-                    node_fc.keyframe_points.add(len(keys))
-                    for k, v in keys.items():
-                        node_fc.keyframe_points[k].co_ui = v['co_ui']
-                        # todo eventually the other dict elements ofc
+                    # fc_ob = fCurve_node.fake_fcurve_ob
+                    # node_fc = fc_ob.animation_data.action.fcurves[0]
+                    # fcurves.append(fCurve_node)
+                    # while(node_fc.keyframe_points): # clear it, it has a default FC
+                    #     node_fc.keyframe_points.remove(node_fc.keyframe_points[0], fast=True)
+                    # node_fc.update()
+                    # node_fc.keyframe_points.add(len(keys))
+                    # for k, v in keys.items():
+                    #     node_fc.keyframe_points[k].co_ui = v['co_ui']
+                    #     # todo eventually the other dict elements ofc
+                    for num_keys, (k, v) in enumerate(keys.items()):
+                        fCurve_node.inputs.new("KeyframeSocket", "Keyframe."+str(num_keys).zfill(3))
+                        kf_node = node_tree.nodes.new("UtilityKeyframe")
+                        kf_node.inputs[0].default_value = v['co_ui'][0]
+                        kf_node.inputs[1].default_value = v['co_ui'][1]
+                        node_tree.links.new(kf_node.outputs[0], fCurve_node.inputs[num_keys])
+                        
                     
                 # NOW the driver itself
                 driver_node = None
@@ -638,22 +687,29 @@ def create_driver(in_node_name, out_node_name, armOb, finished_drivers, switches
                 
                 node_tree.links.new(fCurve_node.outputs[0], driver_node.inputs['fCurve'])
                 for i, var_node in zip(range(num_vars), var_nodes):
-                    # TODO TODO
-                    bpy.ops.mantis.driver_node_add_variable({'node':driver_node})
+                    # TODO TODO BUG HACK
+                    with context.temp_override(node=driver_node):
+                        bpy.ops.mantis.driver_node_add_variable()
                     # This causes an error when you run it from the console! DO NOT leave this
                     node_tree.links.new(var_node.outputs[0], driver_node.inputs[-1])
                 # HACK duplicated code from earlier...
                 parameter = fc.data_path
                 prWhite( "parameter: %s" % parameter)
+                property = ''
                 if len(parameter.split("[\"") ) == 3:
                     property = parameter.split(".")[-1]
                     if (property == 'mute'): # this is mapped to the 'Enable' socket...
                         prop_in = out_node.inputs.get('Enable')
                     else:
                         prop_in = out_node.inputs.get(get_pretty_name(property))
+                        if not prop_in: # this is a HACK because my solution is terrible and also bad
+                            if property == "head_tail":
+                                prop_in = out_node.inputs.get("Head/Tail")
+                                # the socket should probably know what Blender thing is being mapped to it as a custom prop
                     if not prop_in:
                         # try one last thing:
                         property = parameter.split("targets[")[-1]
+
                         target_index = int(property[0])
                         property = "targets[" + property # HACK lol
                         # get the property by index...
@@ -663,6 +719,9 @@ def create_driver(in_node_name, out_node_name, armOb, finished_drivers, switches
                         node_tree.links.new(driver_node.outputs["Driver"], prop_in)
                     else:
                         prRed ("   couldn't find: %s, %s, %s" % (property, out_node.label, out_node.name))
+                elif len(parameter.split("[\"") ) == 2:
+                    # print (parameter.split("[\"") ); raise NotImplementedError
+                    property = parameter.split(".")[-1]
                 else:
                     prWhite( "parameter: %s" % parameter)
                     prRed ("   couldn't find: ", property, out_node.label, out_node.name)
@@ -766,10 +825,34 @@ def do_generate_armature(context, node_tree):
                         
                         # bone_inherit_node[bone_node.name]=[root_child]
                     
+
+                    bone_node.inputs["Lock Location"].default_value = pb.lock_location
+                    bone_node.inputs["Lock Rotation"].default_value = pb.lock_rotation
+                    bone_node.inputs["Lock Scale"].default_value    = pb.lock_scale
+
                     setup_custom_properties(bone_node, pb)
                     setup_ik_settings(bone_node, pb)
                     setup_vp_settings(bone_node, pb, do_after, node_tree)
                     setup_df_settings(bone_node, pb)
+
+                    # BBONES
+                    bone_node.inputs["BBone X Size"].default_value = pb.bone.bbone_x
+                    bone_node.inputs["BBone Z Size"].default_value = pb.bone.bbone_z
+                    bone_node.inputs["BBone Segments"].default_value = pb.bone.bbone_segments
+                    if pb.bone.bbone_mapping_mode == "CURVED":
+                        bone_node.inputs["BBone HQ Deformation"].default_value = True
+                    bone_node.inputs["BBone Start Handle Type"].default_value = pb.bone.bbone_handle_type_start
+                    bone_node.inputs["BBone End Handle Type"].default_value = pb.bone.bbone_handle_type_end
+                    bone_node.inputs["BBone Custom Start Handle"].default_value = pb.bone.bbone_handle_type_start
+                    bone_node.inputs["BBone Custom End Handle"].default_value = pb.bone.bbone_handle_type_end
+                    
+                    bone_node.inputs["BBone X Curve-In"].default_value = pb.bone.bbone_curveinx
+                    bone_node.inputs["BBone Z Curve-In"].default_value = pb.bone.bbone_curveinz
+                    bone_node.inputs["BBone X Curve-Out"].default_value = pb.bone.bbone_curveoutx
+                    bone_node.inputs["BBone Z Curve-Out"].default_value = pb.bone.bbone_curveoutz
+
+                    prRed("BBone Implementation is not complete, expect errors and missing features for now")
+
                     
                     #
                     for c in pb.constraints:
@@ -797,7 +880,7 @@ def do_generate_armature(context, node_tree):
                             if (hasattr(c, "pole_subtarget")):
                                 if c.pole_target and c.pole_subtarget: # this node has a pole target, find the node associated with it... 
                                     do_after.append( ("Pole Target", c_node.name , c.pole_subtarget ) )
-                            fill_parameters(c_node, c)
+                            fill_parameters(c_node, c, context)
                             if (hasattr(c, "targets")): # Armature Modifier, annoying.
                                 for i in range(len(c.targets)):
                                     if (c.targets[i].subtarget):
@@ -808,10 +891,9 @@ def do_generate_armature(context, node_tree):
                                     pb_string = fc.data_path.split("[\"")[1]; pb_string = pb_string.split("\"]")[0]
                                     try:
                                         c_string = fc.data_path.split("[\"")[2]; c_string = c_string.split("\"]")[0]
-                                    except IndexError:
-                                        print ("Find out what causes this:   %s" % c_string)
-                                    if pb.name == pb_string and c.name == c_string:
                                         do_after.append ( ("driver", bone_node.name, c_node.name) )
+                                    except IndexError: # the above expects .pose.bones["some name"].constraints["some constraint"]
+                                        do_after.append ( ("driver", bone_node.name, bone_node.name) ) # it's a property I guess
                     try:
                         node_tree.links.new(parent_node.outputs["Inheritance"], bone_node.inputs['Relationship'])
                     except KeyError: # may have changed, see above
@@ -832,7 +914,10 @@ def do_generate_armature(context, node_tree):
                 node_tree.links.new(out_node.outputs["Object"], in_node.inputs["Target"])
             if task in ['Target', 'Pole Target']:
                 in_node  = node_tree.nodes[ in_node_name ]
-                out_node = node_tree.nodes[ out_node_name ]
+                try:
+                    out_node = node_tree.nodes[ out_node_name ]
+                except KeyError:
+                    prRed (f"Failed to find node: {out_node_name} as pole target for node: {in_node_name} and input {task}")
                 #
                 node_tree.links.new(out_node.outputs["xForm Out"], in_node.inputs[task])
             elif (task[:6] == 'Target'):
@@ -845,13 +930,13 @@ def do_generate_armature(context, node_tree):
                 for n in node_tree.nodes:
                     if n.name == out_node_name:
                         shape_xform_n = n
-                        node_tree.links.new(shape_xform_n.outputs["xForm"], node_tree.nodes[in_node_name].inputs['Custom Object xForm Override'])
+                        node_tree.links.new(shape_xform_n.outputs["xForm Out"], node_tree.nodes[in_node_name].inputs['Custom Object xForm Override'])
                         break
                 else: # make it a task
                     prRed("Cannot set custom object transform override for %s to %s" % (in_node_name, out_node_name))
                 
             elif task in ["driver"]:
-                create_driver(in_node_name, out_node_name, armOb, finished_drivers, switches, driver_vars, fcurves, drivers, node_tree)
+                create_driver(in_node_name, out_node_name, armOb, finished_drivers, switches, driver_vars, fcurves, drivers, node_tree, context)
                         
             # annoyingly, Rigify uses f-modifiers to setup its fcurves
             # I do not intend to support fcurve modifiers in Mantis at this time
@@ -869,10 +954,10 @@ def do_generate_armature(context, node_tree):
 
 
 
-class CreateMantisTree(Operator):
-    """Create Mantis Tree From Selected"""
-    bl_idname = "mantis.create_tree"
-    bl_label = "Create Mantis Tree"
+class GenerateMantisTree(Operator):
+    """Generate Mantis Tree From Selected"""
+    bl_idname = "mantis.generate_tree"
+    bl_label = "Generate Mantis Tree from Selected"
 
     @classmethod
     def poll(cls, context):

+ 235 - 224
ops_nodegroup.py

@@ -2,6 +2,11 @@ import bpy
 from bpy.types import Operator
 from mathutils import Vector
 
+from .utilities import (prRed, prGreen, prPurple, prWhite,
+                              prOrange,
+                              wrapRed, wrapGreen, wrapPurple, wrapWhite,
+                              wrapOrange,)
+
 def TellClasses():
     return [
         MantisGroupNodes,
@@ -11,12 +16,14 @@ def TellClasses():
         QueryNodeSockets,
         CleanUpNodeGraph,
         MantisMuteNode,
+        MantisVisualizeOutput,
         TestOperator,
         # xForm
         AddCustomProperty,
+        EditCustomProperty,
         RemoveCustomProperty,
         # Fcurve
-        EditFCurveNode,
+        # EditFCurveNode,
         FcurveAddKeyframeInput,
         FcurveRemoveKeyframeInput,
         # Driver
@@ -24,8 +31,8 @@ def TellClasses():
         DriverRemoveDriverVariableInput,
         # Armature Link Node
         LinkArmatureAddTargetInput,
-        LinkArmatureRemoveTargetInput,
-        ExportNodeTreeToJSON,]
+        LinkArmatureRemoveTargetInput,]
+        # ExportNodeTreeToJSON,]
 
 def mantis_tree_poll_op(context):
     # return True
@@ -78,95 +85,82 @@ class MantisGroupNodes(Operator):
     def poll(cls, context):
         return mantis_tree_poll_op(context)
 
-
-# IMPORTANT TODO: re-write this because it no longer work
-
-# source is https://github.com/aachman98/Sorcar/blob/master/operators/ScGroupNodes.py
-# checc here: https://github.com/nortikin/sverchok/blob/9002fd4af9ec8603e86f86ed7e567a4ed0d2e07c/core/node_group.py#L568
-
     def execute(self, context):
-        # Get space, path, current nodetree, selected nodes and a newly created group
-        space = context.space_data
-        path = space.path
-        node_tree = space.path[len(path)-1].node_tree
-        node_group = bpy.data.node_groups.new(ChooseNodeGroupNode(space.tree_type), space.tree_type)
-        selected_nodes = [i for i in node_tree.nodes if i.select]
-        nodes_len = len(selected_nodes)
-
-        # Store all links (internal/external) for the selected nodes to be created as group inputs/outputs
-        links_external_in = []
-        links_external_out = []
-        for n in selected_nodes:
-            for i in n.inputs:
-                if (i.is_linked):
-                    l = i.links[0]
-                    if (not l.from_node in selected_nodes):
-                        if (not l in links_external_in):
-                            links_external_in.append(l)
-            for o in n.outputs:
-                if (o.is_linked):
-                    for l in o.links:
-                        if (not l.to_node in selected_nodes):
-                            if (not l in links_external_out):
-                                links_external_out.append(l)
-
-        # Calculate the required locations for placement of grouped node and input/output nodes
-        loc_x_in = 0
-        loc_x_out = 0
-        loc_avg = Vector((0, 0))
-        for n in selected_nodes:
-            loc_avg += n.location/nodes_len
-            if (n.location[0] < loc_x_in):
-                loc_x_in = n.location[0]
-            if (n.location[0] > loc_x_out):
-                loc_x_out = n.location[0]
-        
-        # Create and relocate group input & output nodes in the newly created group
-        group_input = node_group.nodes.new("NodeGroupInput")
-        group_output = node_group.nodes.new("NodeGroupOutput")
-        group_input.location = Vector((loc_x_in-200, loc_avg[1]))
-        group_output.location = Vector((loc_x_out+200, loc_avg[1]))
-        
-        # Copy the selected nodes from current nodetree
-        if (nodes_len > 0):
-            bpy.ops.node.clipboard_copy(get_override(type='NODE_EDITOR'))
-        
-        # Create a grouped node with correct location and assign newly created group
-        group_node = node_tree.nodes.new(ChooseNodeGroupNode(space.tree_type))
-        node_tree.nodes.active = group_node
-        group_node.location = loc_avg
-        group_node.node_tree = node_group
-        
-        # Add overlay to node editor for the newly created group
-        path.append(node_group, node=group_node)
-        
-        # Paste the copied nodes to newly created group
-        if (nodes_len > 0):
-            bpy.ops.node.clipboard_paste(get_override(type='NODE_EDITOR'))
-
-        # Create group input/output links in the newly created group
-        o = group_input.outputs
-        for link in links_external_in:
-            # node_group.links.new(o.get(link.from_socket.name, o[len(o)-1]), node_group.nodes[link.to_node.name].inputs[link.to_socket.name])
-            node_group.links.new(group_input.outputs[''], node_group.nodes[link.to_node.name].inputs[link.to_socket.name])
-        i = group_output.inputs
-        for link in links_external_out:
-            # node_group.links.new(node_group.nodes[link.from_node.name].outputs[link.from_socket.name], i.get(link.to_socket.name, i[len(i)-1]))
-            node_group.links.new(node_group.nodes[link.from_node.name].outputs[link.from_socket.name], group_output.inputs[''])
-        
-        # Add new links to grouped node from original external links
-        for i in range(0, len(links_external_in)):
-            link = links_external_in[i]
-            node_tree.links.new(link.from_node.outputs[link.from_socket.name], group_node.inputs[i])
-        for i in range(0, len(links_external_out)):
-            link = links_external_out[i]
-            node_tree.links.new(group_node.outputs[i], link.to_node.inputs[link.to_socket.name])
-        
-        # Remove redundant selected nodes
-        for n in selected_nodes:
-            node_tree.nodes.remove(n)
-
-        return {"FINISHED"}
+        base_tree=context.space_data.path[-1].node_tree
+        base_tree.is_exporting = True
+
+        from .i_o import export_to_json, do_import
+        from random import random
+        grp_name = "".join([chr(int(random()*30)+35) for i in range(20)])
+        trees=[base_tree]
+        selected_nodes=export_to_json(trees, write_file=False, only_selected=True)
+        selected_nodes[base_tree.name][0]["name"]=grp_name
+        # this is for debugging the result of the export
+        # for k,v in selected_nodes[base_tree.name][2].items():
+        #     prPurple(k)
+        #     for k1, v1 in v["sockets"].items():
+        #         prRed("    ", k1, v1["name"])
+        do_import(selected_nodes, context)
+
+        affected_links_in = []
+        affected_links_out = []
+
+        for l in base_tree.links:
+            if l.from_node.select and not l.to_node.select: affected_links_out.append(l)
+            if not l.from_node.select and l.to_node.select: affected_links_in.append(l)
+        delete_me = []
+        all_nodes_bounding_box=[Vector((float("inf"),float("inf"))), Vector((-float("inf"),-float("inf")))]
+        for n in base_tree.nodes:
+            if n.select: 
+                if n.location.x < all_nodes_bounding_box[0].x:
+                    all_nodes_bounding_box[0].x = n.location.x
+                if n.location.y < all_nodes_bounding_box[0].y:
+                    all_nodes_bounding_box[0].y = n.location.y
+                #
+                if n.location.x > all_nodes_bounding_box[1].x:
+                    all_nodes_bounding_box[1].x = n.location.x
+                if n.location.y > all_nodes_bounding_box[1].y:
+                    all_nodes_bounding_box[1].y = n.location.y
+                delete_me.append(n)
+        grp_node = base_tree.nodes.new('MantisNodeGroup')
+        grp_node.node_tree = bpy.data.node_groups[grp_name]
+        bb_center = all_nodes_bounding_box[0].lerp(all_nodes_bounding_box[1],0.5)
+        for n in grp_node.node_tree.nodes:
+            n.location -= bb_center
+
+        grp_node.location = Vector((all_nodes_bounding_box[0].x+200, all_nodes_bounding_box[0].lerp(all_nodes_bounding_box[1], 0.5).y))
+
+        # for l in selected_nodes[base_tree.name][3]:
+        #     if source := l.get("source"):
+        #         n_from = base_tree.nodes.get(source[0])
+        #         # s_from = n_from.
+
+        for n in selected_nodes[base_tree.name][2].values():
+            for s in n["sockets"].values():
+                if source := s.get("source"):
+                    prGreen (s["name"], source[0], source[1])
+                    base_tree_node=base_tree.nodes.get(source[0])
+                    if s["is_output"]:
+                        for output in base_tree_node.outputs:
+                            if output.identifier == source[1]:
+                                break
+                        else:
+                            raise RuntimeError(wrapRed("Socket not found when grouping"))
+                        base_tree.links.new(input=output, output=grp_node.inputs[s["name"]])
+                    else:
+                        for s_input in base_tree_node.inputs:
+                            if s_input.identifier == source[1]:
+                                break
+                        else:
+                            raise RuntimeError(wrapRed("Socket not found when grouping"))
+                        base_tree.links.new(input=grp_node.outputs[s["name"]], output=s_input)
+
+        for n in delete_me: base_tree.nodes.remove(n)
+        base_tree.nodes.active = grp_node
+
+        base_tree.is_exporting = False
+        grp_node.node_tree.name = "Group_Node.000"
+        return {'FINISHED'}
 
 class MantisEditGroup(Operator):
     """Edit the group referenced by the active node (or exit the current node-group)"""
@@ -192,6 +186,8 @@ class MantisEditGroup(Operator):
         elif len(path) > 1:
             path.pop()
             path[0].node_tree.display_update(context)
+            # get the active node in the current path
+            path[len(path)-1].node_tree.nodes.active.update() # call update to force the node group to check if its tree has changed
         return {"CANCELLED"}
 
 class ExecuteNodeTree(Operator):
@@ -204,7 +200,6 @@ class ExecuteNodeTree(Operator):
         return (mantis_tree_poll_op(context))
 
     def execute(self, context):
-        from .utilities import parse_node_tree, print_lines
         from time import time
         from .utilities import wrapGreen
         
@@ -212,16 +207,36 @@ class ExecuteNodeTree(Operator):
         
         import cProfile
         from os import environ
+        start_time = time()
         do_profile=False
         print (environ.get("DOPROFILE"))
         if environ.get("DOPROFILE"):
             do_profile=True
         if do_profile:
-            cProfile.runctx("tree.update_tree(context)", None, locals())
-            cProfile.runctx("tree.execute_tree(context)", None, locals())
+            # cProfile.runctx("tree.update_tree(context)", None, locals())
+            # cProfile.runctx("tree.execute_tree(context)", None, locals())
+            # import hunter
+            # hunter.trace(stdlib=False, action=hunter.CallPrinter(force_colors=False))
+            # tree.update_tree(context)
+            # tree.execute_tree(context)
+            # return {"FINISHED"}
+            import pstats, io
+            from pstats import SortKey
+            with cProfile.Profile() as pr:
+                tree.update_tree(context)
+                tree.execute_tree(context)
+                # from the Python docs at https://docs.python.org/3/library/profile.html#module-cProfile
+                s = io.StringIO()
+                sortby = SortKey.TIME
+                # sortby = SortKey.CUMULATIVE
+                ps = pstats.Stats(pr, stream=s).strip_dirs().sort_stats(sortby)
+                ps.print_stats(20) # print the top 20
+                print(s.getvalue())
+
         else:
             tree.update_tree(context)
             tree.execute_tree(context)
+        prGreen("Finished executing tree in %f seconds" % (time() - start_time))
         return {"FINISHED"}
 
 # class CreateMetaGroup(Operator):
@@ -295,89 +310,19 @@ class CleanUpNodeGraph(bpy.types.Operator):
     """Clean Up Node Graph"""
     bl_idname = "mantis.nodes_cleanup"
     bl_label = "Clean Up Node Graph"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    # num_iterations=bpy.props.IntProperty(default=8)
+
 
     @classmethod
     def poll(cls, context):
         return hasattr(context, 'active_node')
 
     def execute(self, context):
-        
         base_tree=context.space_data.path[-1].node_tree
-        
-        from .grandalf.graphs import Vertex, Edge, Graph, graph_core
-        
-        class defaultview(object):
-            w,h = 1,1
-            xz = (0,0)
-        
-        verts = {}
-        for n in base_tree.nodes:
-            has_links=False
-            for inp in n.inputs:
-                if inp.is_linked:
-                    has_links=True
-                    break
-            for out in n.outputs:
-                if out.is_linked:
-                    has_links=True
-                    break
-            if not has_links:
-                continue
-                
-            v = Vertex(n.name)
-            v.view = defaultview()
-            v.view.xy = n.location
-            v.view.h = n.height*3
-            v.view.w = n.width*3
-            verts[n.name] = v
-            
-        edges = []
-        for link in base_tree.links:
-            weight = 1 # maybe this is useful
-            edges.append(Edge(verts[link.from_node.name], verts[link.to_node.name], weight) )
-        graph = Graph(verts.values(), edges)
-        
-
-        
-        from .grandalf.layouts import SugiyamaLayout
-        
-        
-        sug = SugiyamaLayout(graph.C[0]) # no idea what .C[0] is
-        
-        roots=[]
-        for node in base_tree.nodes:
-            
-            has_links=False
-            for inp in node.inputs:
-                if inp.is_linked:
-                    has_links=True
-                    break
-            for out in node.outputs:
-                if out.is_linked:
-                    has_links=True
-                    break
-            if not has_links:
-                continue
-                
-            if len(node.inputs)==0:
-                roots.append(verts[node.name])
-            else:
-                for inp in node.inputs:
-                    if inp.is_linked==True:
-                        break
-                else:
-                    roots.append(verts[node.name])
-        
-        sug.init_all(roots=roots,)
-        sug.draw(8)
-        for v in graph.C[0].sV:
-            for n in base_tree.nodes:
-                if n.name == v.data:
-                    n.location.x = v.view.xy[1]
-                    n.location.y = v.view.xy[0]
-        
-        
-        
+        from .utilities import SugiyamaGraph
+        SugiyamaGraph(base_tree, 12)
         return {'FINISHED'}
 
 
@@ -404,6 +349,29 @@ class MantisMuteNode(Operator):
         return {"FINISHED"}
 
 
+class MantisVisualizeOutput(Operator):
+    """Mantis Visualize Output Operator"""
+    bl_idname = "mantis.visualize_output"
+    bl_label = "Visualize Output"
+
+    @classmethod
+    def poll(cls, context):
+        return (mantis_tree_poll_op(context))
+
+    def execute(self, context):
+        from time import time
+        from .utilities import wrapGreen, prGreen
+        
+        tree=context.space_data.path[0].node_tree
+        tree.update_tree(context)
+        # tree.execute_tree(context)
+        prGreen(f"Visualize Tree: {tree.name}")
+        nodes = tree.parsed_tree
+        from .readtree import visualize_tree
+        visualize_tree(nodes, tree, context)
+        return {"FINISHED"}
+
+
 class TestOperator(Operator):
     """Mantis Test Operator"""
     bl_idname = "mantis.test_operator"
@@ -415,24 +383,23 @@ class TestOperator(Operator):
 
     def execute(self, context):
         path = context.space_data.path
-        node = path[len(path)-1].node_tree.nodes.active
-        print("Inputs:")
-        for sock in node.inputs:
-            print(sock.identifier)
-        print("Outputs:")
-        for sock in node.outputs:
-            print(sock.identifier)
-        print ("\n")
-        # if (not node):
-        #     return {"FINISHED"}
-        # for out in node.outputs:
-        #     utilities.lines_from_socket(out)
-        
-        # import bpy
-        # c = bpy.context
-        # print (c.space_data.path)
+        base_tree = path[0].node_tree
+        tree = path[len(path)-1].node_tree
+        node = tree.nodes.active
+        node.display_update(base_tree.parsed_tree, context)
+        # from .base_definitions import get_signature_from_edited_tree
+        # if nc := base_tree.parsed_tree.get(get_signature_from_edited_tree(node, context)):
+        #     from .utilities import get_all_dependencies
+        #     deps = get_all_dependencies(nc)
+        #     self.report({'INFO'}, f"Number of Node Dependencies: {len(deps)}")
+        #     # for n in deps:
+        #     #     prGreen(n)
+        # else:
+        #     # prRed("No NC found in parsed tree.")
+        #     self.report({'ERROR_INVALID_CONTEXT'}, "No data for node.")
         return {"FINISHED"}
 
+
 ePropertyType =(
         ('BOOL'  , "Boolean", "Boolean", 0),
         ('INT'   , "Integer", "Integer", 1),
@@ -508,7 +475,7 @@ class AddCustomProperty(bpy.types.Operator):
                 self.prop_name = self.prop_name[:-3] + str(number).zfill(3)
             except ValueError:
                 self.prop_name+='.001'
-                # WRONG
+                # WRONG # HACK # TODO # BUG #
         new_prop = n.inputs.new( socktype, self.prop_name)
         if self.prop_type in ['INT','FLOAT']:
             new_prop.min = self.min
@@ -519,22 +486,55 @@ class AddCustomProperty(bpy.types.Operator):
         # now do the output
         n.outputs.new( socktype, self.prop_name)
         
-        if (False):
-            print (new_prop.is_property_set("default_value"))
-            ui_data = new_prop.id_properties_ui("default_value")
-            ui_data.update(
-                description=new_prop.description,
-                default=0,) # for now
-            #if a number
-            for num_type in ['Float', 'Int', 'Bool']:
-                if num_type in new_prop.bl_idname:
-                    ui_data.update(
-                        min = new_prop.min,
-                        max = new_prop.max,
-                        soft_min = new_prop.soft_min,
-                        soft_max = new_prop.soft_max,)
         return {'FINISHED'}
 
+#DOESN'T WORK YET
+class EditCustomProperty(bpy.types.Operator):
+    """Edit Custom Property"""
+    bl_idname = "mantis.edit_custom_property"
+    bl_label = "Edit Custom Property"
+
+
+    prop_type : bpy.props.EnumProperty(
+        items=ePropertyType,
+        name="New Property Type",
+        description="Type of data for new Property",
+        default = 'BOOL',)
+    prop_name  : bpy.props.StringProperty(default='Prop')
+    
+    min:bpy.props.FloatProperty(default = 0)
+    max:bpy.props.FloatProperty(default = 1)
+    soft_min:bpy.props.FloatProperty(default = 0)
+    soft_max:bpy.props.FloatProperty(default = 1)
+    description:bpy.props.StringProperty(default = "") # TODO: use getters to fill these automatically
+    
+    node_invoked : bpy.props.PointerProperty(type=bpy.types.Node, 
+                options ={'HIDDEN'}) # note this seems to affect all
+                                     # subsequent properties
+
+    @classmethod
+    def poll(cls, context):
+        return True #( hasattr(context, 'node') ) 
+
+    def invoke(self, context, event):
+        self.node_invoked = context.node
+        wm = context.window_manager
+        return wm.invoke_props_dialog(self)
+        
+    def execute(self, context):
+        n = self.node_invoked
+        prop = n.inputs.get( self.prop_name )
+        if (s := n.inputs.get(self.prop_name)):
+            if self.prop_type in ['INT','FLOAT']:
+                new_prop.min = self.min
+                new_prop.max = self.max
+                new_prop.soft_min = self.soft_min
+                new_prop.soft_max = self.soft_max
+            new_prop.description = self.description
+        
+        return {'FINISHED'}
+
+
 
 class RemoveCustomProperty(bpy.types.Operator):
     """Remove a Custom Property from an xForm Node"""
@@ -764,10 +764,10 @@ class FcurveAddKeyframeInput(bpy.types.Operator):
         return (hasattr(context, 'active_node') )
 
     def execute(self, context):
-        context.node.inputs.new("KeyframeSocket", "Keyframe")
+        num_keys = len( context.node.inputs)
+        context.node.inputs.new("KeyframeSocket", "Keyframe."+str(num_keys).zfill(3))
         return {'FINISHED'}
 
-
 class FcurveRemoveKeyframeInput(bpy.types.Operator):
     """Remove a keyframe input from the fCurve node"""
     bl_idname = "mantis.fcurve_node_remove_kf"
@@ -829,7 +829,7 @@ class LinkArmatureAddTargetInput(bpy.types.Operator):
     def execute(self, context):           # unicode for 'a'
         num_targets = len( list(context.node.inputs)[6:])//2
         context.node.inputs.new("xFormSocket", "Target."+str(num_targets).zfill(3))
-        context.node.inputs.new("FloatSocket", "Weight."+str(num_targets).zfill(3))
+        context.node.inputs.new("FloatFactorSocket", "Weight."+str(num_targets).zfill(3))
         return {'FINISHED'}
 
 
@@ -850,28 +850,39 @@ class LinkArmatureRemoveTargetInput(bpy.types.Operator):
 
 
 
-class ExportNodeTreeToJSON(Operator):
-    """Export this node tree as a JSON file"""
-    bl_idname = "mantis.export_node_tree_json"
-    bl_label = "Export Mantis Tree to JSON"
+# class ExportNodeTreeToJSON(Operator):
+#     """Export this node tree as a JSON file"""
+#     bl_idname = "mantis.export_node_tree_json"
+#     bl_label = "Export Mantis Tree to JSON"
 
-    @classmethod
-    def poll(cls, context):
-        return (mantis_tree_poll_op(context))
+#     @classmethod
+#     def poll(cls, context):
+#         return (mantis_tree_poll_op(context))
 
-    def execute(self, context):
-        from .i_o import export_to_json
-        import bpy
+#     def execute(self, context):
+#         from .i_o import export_to_json
+#         import bpy
+
+#         tree = context.space_data.path[0].node_tree
+#         # tree.update_tree(context)
+#         trees = {tree}
+#         check_trees=[tree]
+#         while check_trees:
+#             check = check_trees.pop()
+#             for n in check.nodes:
+#                 if hasattr(n, "node_tree"):
+#                     if n.node_tree not in trees:
+#                         check_trees.append(n.node_tree)
+#                         trees.add(n.node_tree)
+        
 
-        tree = context.space_data.path[0].node_tree
-        tree.update_tree(context)
 
-        def remove_special_characters(stritree):
-            # https://stackoverflow.com/questions/295135/turn-a-stritree-into-a-valid-filename
-            # thank you user "Sophie Gage"
-            import re # regular expressions
-            return re.sub('[^\w_.)( -]', '', stritree)
+#         def remove_special_characters(stritree):
+#             # https://stackoverflow.com/questions/295135/turn-a-stritree-into-a-valid-filename
+#             # thank you user "Sophie Gage"
+#             import re # regular expressions
+#             return re.sub('[^\w_.)( -]', '', stritree)
 
-        path = bpy.path.abspath('//')+remove_special_characters(tree.name)+".json"
-        export_to_json(tree, path)
-        return {"FINISHED"}
+#         path = bpy.path.abspath('//')+remove_special_characters(tree.name)+".json"
+#         export_to_json(trees, path)
+#         return {"FINISHED"}

+ 11 - 7
primitives_containers.py

@@ -19,7 +19,6 @@ class CirclePrimitive:
 
     def __init__(self, signature, base_tree):
         self.base_tree=base_tree
-        self.executed = False
         self.signature = signature
         self.inputs = {
           "Name"               : NodeSocket(is_input = True, name = "Name", node = self),
@@ -36,9 +35,14 @@ class CirclePrimitive:
           "Circle":None, 
         }
         self.node_type = "UTILITY"
+        self.hierarchy_connections = []
+        self.connections = []
+        self.hierarchy_dependencies = []
+        self.dependencies = []
+        self.prepared = True
+        self.executed = False
+
 
-    def evaluate_input(self, input_name):
-        return evaluate_input(self, input_name)
 
     def bGetObject(self):
         from bpy import data
@@ -69,9 +73,9 @@ class CirclePrimitive:
             v.co = Matrix.Rotation(pi/2, 4, 'X') @ v.co
         # done with this, push it to the data and free the bmesh.
         bm.to_mesh(data); bm.free()
+        self.executed = True
+
 
-    def __repr__(self):
-        return self.signature.__repr__()
 
-    def fill_parameters(self):
-        fill_parameters(self)
+for c in TellClasses():
+    setup_container(c)

+ 2 - 0
primitives_definitions.py

@@ -16,9 +16,11 @@ class GeometryCirclePrimitive(Node, MantisNode):
     bl_idname = 'GeometryCirclePrimitive'
     bl_label = "Circle Primitive"
     bl_icon = 'NODE'
+    initialized : bpy.props.BoolProperty(default = False)
 
     def init(self, context):
         self.inputs.new('StringSocket', "Name")
         self.inputs.new('FloatPositiveSocket', "Radius")
         self.inputs.new('IntSocket', "Number of Points")
         self.outputs.new('GeometrySocket', "Circle")
+        self.initialized = True

File diff suppressed because it is too large
+ 638 - 434
readtree.py


+ 219 - 0
schema_containers.py

@@ -0,0 +1,219 @@
+from .node_container_common import *
+from math import pi, tau
+
+def TellClasses():
+    return [
+        SchemaIndex,
+        SchemaArrayInput,
+        SchemaArrayInputGet,
+        SchemaArrayOutput,
+        SchemaConstInput,
+        SchemaConstOutput,
+        SchemaOutgoingConnection,
+        SchemaIncomingConnection,
+    ]
+
+
+def init_parameters(nc, is_input = True, in_out='INPUT', category=''):
+    from .utilities import tree_from_nc
+    parent_tree = tree_from_nc(nc.signature, nc.base_tree)
+    if is_input:
+        sockets=nc.inputs
+    else:
+        sockets=nc.outputs
+    if category in ['Constant', 'Array', 'Connection']:
+        for item in parent_tree.interface.items_tree:
+            if item.item_type == 'PANEL': continue
+            if item.parent and item.parent.name == category:
+                if item.in_out == in_out:
+                    sockets[item.name] = NodeSocket(
+                        is_input=is_input,
+                        name=item.name,
+                        node=nc)
+                    nc.parameters[item.name] = None
+
+
+
+class SchemaIndex:
+    def __init__(self, signature, base_tree):
+        self.base_tree=base_tree
+        self.signature = signature
+        self.inputs = {}#{
+        #   "X"   : NodeSocket(is_input = True, name = "X", node = self),
+        #   "Y"   : NodeSocket(is_input = True, name = "Y", node = self),
+        #   "Z"   : NodeSocket(is_input = True, name = "Z", node = self),
+        # }
+        self.outputs = {
+          "Index" : NodeSocket(name = "Index", node=self),
+          "Schema Length" : NodeSocket(name = "Schema Length", node=self),
+        }
+        self.parameters = {
+          "Index":None,
+          "Schema Length":None,
+        }
+        self.node_type = 'SCHEMA'
+        self.hierarchy_connections = []
+        self.connections = []
+        self.hierarchy_dependencies = []
+        self.dependencies = []
+        self.prepared = True
+        self.executed = True
+
+
+
+class SchemaArrayInput:
+    def __init__(self, signature, base_tree):
+        self.base_tree=base_tree
+        self.signature = signature
+        self.inputs = {}
+        self.outputs = {}
+        self.parameters = {}
+        init_parameters(self, is_input=False, in_out='INPUT', category='Array')
+        self.node_type = 'SCHEMA'
+        self.hierarchy_connections = []
+        self.connections = []
+        self.hierarchy_dependencies = []
+        self.dependencies = []
+        self.prepared = True
+        self.executed = True
+
+
+class SchemaArrayInputGet:
+    def __init__(self, signature, base_tree):
+        self.base_tree=base_tree
+        self.signature = signature
+        self.inputs = {
+            "OoB Behaviour"  :  NodeSocket(is_input = True, name = "OoB Behaviour", node = self),
+            "Index"          :  NodeSocket(is_input = True, name = "Index", node = self),
+        }
+        self.outputs = {}
+        self.parameters = {
+            "OoB Behaviour"  :  None,
+            "Index"          :  None,
+
+        }
+        init_parameters(self, is_input=False, in_out='INPUT', category='Array')
+        self.node_type = 'SCHEMA'
+        self.hierarchy_connections = []
+        self.connections = []
+        self.hierarchy_dependencies = []
+        self.dependencies = []
+        self.prepared = True
+        self.executed = True
+
+
+class SchemaArrayOutput:
+    def __init__(self, signature, base_tree):
+        self.base_tree=base_tree
+        self.signature = signature
+        self.inputs = {}
+        self.outputs = {}
+        self.parameters = {}
+        init_parameters(self, is_input=True, in_out='OUTPUT', category='Array')
+        self.node_type = 'SCHEMA'
+        self.hierarchy_connections = []
+        self.connections = []
+        self.hierarchy_dependencies = []
+        self.dependencies = []
+        self.prepared = True
+        self.executed = True
+
+
+        
+
+class SchemaConstInput:
+    def __init__(self, signature, base_tree):
+        self.base_tree=base_tree
+        self.signature = signature
+        self.inputs = {}
+        self.outputs = {}
+        self.parameters = {}
+        init_parameters(self, is_input=False, in_out='INPUT', category='Constant')
+        self.node_type = 'SCHEMA'
+        self.hierarchy_connections = []
+        self.connections = []
+        self.hierarchy_dependencies = []
+        self.dependencies = []
+        self.prepared = True
+        self.executed = True
+
+
+
+
+
+class SchemaConstOutput:
+    def __init__(self, signature, base_tree):
+        self.base_tree=base_tree
+        self.signature = signature
+        self.inputs = {"Expose when N==":NodeSocket(is_input=True, name="Expose when N==", node=self)}
+        self.outputs = {}
+        self.parameters = {"Expose when N==":None}
+        init_parameters(self, is_input=True, in_out='OUTPUT', category='Constant')
+        self.node_type = 'SCHEMA'
+        self.hierarchy_connections = []
+        self.connections = []
+        self.hierarchy_dependencies = []
+        self.dependencies = []
+        self.prepared = True
+        self.executed = True
+
+
+        
+class SchemaOutgoingConnection:
+    def __init__(self, signature, base_tree):
+        self.base_tree=base_tree
+        self.signature = signature
+        self.inputs = {}
+        self.outputs = {}
+        self.parameters = {}
+        init_parameters(self, is_input=True, in_out='INPUT', category='Connection')
+        self.node_type = 'SCHEMA'
+        self.hierarchy_connections = []
+        self.connections = []
+        self.hierarchy_dependencies = []
+        self.dependencies = []
+        self.prepared = True
+        self.executed = True
+
+
+        
+class SchemaIncomingConnection:
+    def __init__(self, signature, base_tree):
+        self.base_tree=base_tree
+        self.signature = signature
+        self.inputs = {}
+        self.outputs = {}
+        self.parameters = {}
+        init_parameters(self, is_input=False, in_out='OUTPUT', category='Connection')
+        self.node_type = 'SCHEMA'
+        self.hierarchy_connections = []
+        self.connections = []
+        self.hierarchy_dependencies = []
+        self.dependencies = []
+        self.prepared = True
+        self.executed = True
+
+
+
+# class SchemaChoose:
+#     def __init__(self, signature, base_tree):
+#         self.base_tree=base_tree
+#         self.signature = signature
+#         init_parameters(self)
+#         self.node_type = 'UTILITY'
+        
+#     def evaluate_input(self, input_name):
+#         return evaluate_input(self, input_name)
+    
+#     def bExecute(self, bContext = None,):
+#         pass
+    
+#     def __repr__(self):
+#         return self.signature.__repr__()
+        
+#     def fill_parameters(self):
+#         fill_parameters(self)
+
+
+for c in TellClasses():
+    setup_container(c)

+ 324 - 0
schema_definitions.py

@@ -0,0 +1,324 @@
+import bpy
+from .base_definitions import SchemaNode
+from bpy.types import Node
+from .utilities import (prRed, prGreen, prPurple, prWhite,
+                              prOrange,
+                              wrapRed, wrapGreen, wrapPurple, wrapWhite,
+                              wrapOrange,)
+from bpy.props import BoolProperty
+
+from .utilities import get_socket_maps, relink_socket_map, do_relink
+
+
+def TellClasses():
+    return [
+        # tree i/o
+        SchemaIndex,
+        SchemaArrayInput,
+        SchemaArrayInputGet,
+        SchemaArrayOutput,
+        SchemaConstInput,
+        SchemaConstOutput,
+        SchemaOutgoingConnection,
+        SchemaIncomingConnection,
+        # # iterators
+        # SchemaIntMath,
+        # SchemaDeclarationValidWhen,
+        ]
+
+
+# IMPORTANT TODO:
+# - check what happens when these get plugged into each other
+# - probably disallow all or most of these connections in insert_link or update
+
+class SchemaIndex(Node, SchemaNode):
+    '''The current index of the schema execution'''
+    bl_idname = 'SchemaIndex'
+    bl_label = "Index"
+    bl_icon = 'GIZMO'
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        self.outputs.new("IntSocket", "Index")
+        self.outputs.new("IntSocket", "Schema Length")
+        self.initialized = True
+
+
+class SchemaArrayInput(Node, SchemaNode):
+    '''Array Inputs'''
+    bl_idname = 'SchemaArrayInput'
+    bl_label = "Array Input"
+    bl_icon = 'GIZMO'
+    initialized : bpy.props.BoolProperty(default = False)
+
+    def init(self, context):
+        self.update()
+
+    def update(self):
+        # self.initialized = False
+        output_map = get_socket_maps(self)[1]
+        self.outputs.clear()
+        for item in self.id_data.interface.items_tree:
+            if item.item_type == 'PANEL': continue
+            if item.parent and item.in_out == 'INPUT' and item.parent.name == 'Array':
+                relink_socket_map(self, self.outputs, output_map, item, in_out='OUTPUT')
+        if '__extend__' in output_map.keys() and output_map['__extend__']:
+            do_relink(self, None, output_map, in_out='OUTPUT', parent_name='Array' )
+        if len(self.inputs)<1 or self.inputs[-1].bl_idname not in ["WildcardSocket"]:
+            self.outputs.new('WildcardSocket', '', identifier='__extend__')
+        # self.initialized = True
+
+class SchemaArrayInputGet(Node, SchemaNode):
+    '''Array Inputs'''
+    bl_idname = 'SchemaArrayInputGet'
+    bl_label = "Array Input at Index"
+    bl_icon = 'GIZMO'
+    initialized : bpy.props.BoolProperty(default = False)
+
+    def init(self, context):
+        self.inputs.new('EnumArrayGetOptions', 'OoB Behaviour')
+        self.inputs.new("IntSocket", "Index")
+        self.update()
+
+    def update(self):
+        # self.initialized = False
+        output_map = get_socket_maps(self)[1]
+        self.outputs.clear()
+        for item in self.id_data.interface.items_tree:
+            if item.item_type == 'PANEL': continue
+            if item.parent and item.in_out == 'INPUT' and item.parent.name == 'Array':
+                relink_socket_map(self, self.outputs, output_map, item, in_out='OUTPUT')
+        if '__extend__' in output_map.keys() and output_map['__extend__']:
+            do_relink(self, None, output_map, in_out='OUTPUT', parent_name='Array' )
+        if len(self.inputs)<1 or self.inputs[-1].bl_idname not in ["WildcardSocket"]:
+            self.outputs.new('WildcardSocket', '', identifier='__extend__')
+        # self.initialized = True
+
+class SchemaArrayOutput(Node, SchemaNode):
+    '''Array Inputs'''
+    bl_idname = 'SchemaArrayOutput'
+    bl_label = "Array Output"
+    bl_icon = 'GIZMO'
+    initialized : bpy.props.BoolProperty(default = False)
+
+    def init(self, context):
+        self.update()
+
+    def update(self):
+        self.initialized = False
+        input_map = get_socket_maps(self)[0]
+        self.inputs.clear()
+        for item in self.id_data.interface.items_tree:
+            if item.item_type == 'PANEL': continue
+            if item.parent and item.in_out == 'OUTPUT' and item.parent.name == 'Array':
+                relink_socket_map(self, self.inputs, input_map, item, in_out='INPUT')
+        if '__extend__' in input_map.keys() and input_map['__extend__']:
+            do_relink(self, None, input_map, in_out='INPUT', parent_name='Array' )
+        if len(self.inputs)<1 or self.inputs[-1].bl_idname not in ["WildcardSocket"]:
+            self.inputs.new('WildcardSocket', '', identifier='__extend__')
+        for s in self.outputs:
+            s.input= True
+        self.initialized = True
+
+class SchemaConstInput(Node, SchemaNode):
+    '''Constant Inputs'''
+    bl_idname = 'SchemaConstInput'
+    bl_label = "Constant Input"
+    bl_icon = 'GIZMO'
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        self.update()
+
+    def update(self):
+        self.initialized = False
+        output_map = get_socket_maps(self)[1]
+        self.outputs.clear()
+        for item in self.id_data.interface.items_tree:
+            if item.item_type == 'PANEL': continue
+            if item.parent and item.in_out == 'INPUT' and item.parent.name == 'Constant':
+                relink_socket_map(self, self.outputs, output_map, item, in_out='OUTPUT')
+        if '__extend__' in output_map.keys() and output_map['__extend__']:
+            do_relink(self, None, output_map, in_out='OUTPUT', parent_name='Constant' )
+        if len(self.inputs)<1 or self.inputs[-1].bl_idname not in ["WildcardSocket"]:
+            self.outputs.new('WildcardSocket', '', identifier='__extend__')
+        self.initialized = True
+
+
+
+class SchemaConstOutput(Node, SchemaNode):
+    '''Constant Outputs'''
+    bl_idname = 'SchemaConstOutput'
+    bl_label = "Constant Output"
+    bl_icon = 'GIZMO'
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        self.update()
+
+    def update(self):
+        self.initialized = False
+        input_map = get_socket_maps(self)[0]
+        self.inputs.clear()
+        s = self.inputs.new('IntSocket', "Expose when N==")
+        for item in self.id_data.interface.items_tree:
+            if item.item_type == 'PANEL': continue
+            if item.parent and item.in_out == 'OUTPUT' and item.parent.name == 'Constant':
+                relink_socket_map(self, self.inputs, input_map, item, in_out='INPUT')
+        if '__extend__' in input_map.keys() and input_map['__extend__']:
+            do_relink(self, None, input_map, in_out='INPUT', parent_name='Constant' )
+        if len(self.inputs)<1 or self.inputs[-1].bl_idname not in ["WildcardSocket"]:
+            self.inputs.new('WildcardSocket', '', identifier='__extend__')
+        do_relink(self, s, input_map, in_out='INPUT')
+        for s in self.outputs:
+            s.input= True
+        
+        self.initialized = True
+
+
+
+class SchemaOutgoingConnection(Node, SchemaNode):
+    '''Outgoing Connections'''
+    bl_idname = 'SchemaOutgoingConnection'
+    bl_label = "Outgoing Connection"
+    bl_icon = 'GIZMO'
+    initialized : bpy.props.BoolProperty(default = False)
+    
+    def init(self, context):
+        # self.inputs.new('IntSocket', 'Valid From')
+        # self.inputs.new('IntSocket', 'Valid Until')
+        self.update()
+
+    def update(self):
+        self.initialized = False
+        input_map = get_socket_maps(self)[0]
+        self.inputs.clear()
+        for item in self.id_data.interface.items_tree:
+            if item.item_type == 'PANEL': continue
+            if item.parent and item.in_out == 'OUTPUT' and item.parent.name == 'Connection':
+                relink_socket_map(self, self.inputs, input_map, item, in_out='INPUT')
+        if '__extend__' in input_map.keys() and input_map['__extend__']:
+            do_relink(self, None, input_map, in_out='INPUT', parent_name='Connection' )
+        if len(self.inputs)<1 or self.inputs[-1].bl_idname not in ["WildcardSocket"]:
+            self.inputs.new('WildcardSocket', '', identifier='__extend__')
+        for s in self.outputs:
+            s.input= True
+        self.initialized = True
+
+
+
+class SchemaIncomingConnection(Node, SchemaNode):
+    '''Incoming Connections'''
+    bl_idname = 'SchemaIncomingConnection'
+    bl_label = "Incoming Connection"
+    bl_icon = 'GIZMO'
+    initialized : bpy.props.BoolProperty(default = False)
+
+    def init(self, context):
+        self.update()
+
+    def update(self):
+        self.initialized = False
+        output_map = get_socket_maps(self)[1]
+        self.outputs.clear()
+        for item in self.id_data.interface.items_tree:
+            if item.item_type == 'PANEL': continue
+            if item.parent and item.in_out == 'INPUT' and item.parent.name == 'Connection':
+                relink_socket_map(self, self.outputs, output_map, item, in_out='OUTPUT')
+        if '__extend__' in output_map.keys() and output_map['__extend__']:
+            do_relink(self, None, output_map, in_out='OUTPUT', parent_name='Connection' )
+        if len(self.inputs)<1 or self.inputs[-1].bl_idname not in ["WildcardSocket"]:
+            self.outputs.new('WildcardSocket', '', identifier='__extend__')
+        self.initialized = True
+
+
+
+# have a string for name
+# assign/get
+# and a fallback if none
+# get should take an integer: 0 = index, -1 = index -1, etc., no positive ints allowed
+# class SchemaLocalVariable(Node, SchemaNode):
+#     '''Constant Inputs'''
+#     bl_idname = 'SchemaIncomingConnection'
+#     bl_label = "Incoming Connection"
+#     bl_icon = 'GIZMO'
+    
+#     def init(self, context):
+#         # self.outputs.new("IntSocket", "Index")
+#         pass
+
+
+# class SchemaIntMath(Node, SchemaNode):
+#     '''Int Math'''
+#     bl_idname = 'SchemaIntMath'
+#     bl_label = "Int Math"
+#     bl_icon = 'GIZMO'
+    
+#     # def init(self, context):
+#     #     self.update()
+
+
+# class SchemaDeclarationValidWhen(Node, SchemaNode):
+#     '''Declaration Valid When'''
+#     bl_idname = 'SchemaDeclarationValidWhen'
+#     bl_label = "Declaration Valid When"
+#     bl_icon = 'GIZMO'
+    
+#     def init(self, context):
+#         self.inputs.new('IntSocket', 'Valid From')
+#         self.inputs.new('IntSocket', 'Valid Until')
+#         self.inputs.new('IntSocket', 'Add to N') # +
+#         self.inputs.new('IntSocket', 'Multiply N') # *
+#         self.inputs.new('IntSocket', 'Modulo of N') # %
+#         # self.inputs.new('IntSocket', 'n')
+
+
+# I need to figure out what to do with this right here...
+# There are a few options:
+#  - an actual control flow (if, then) -- but I don' like that because it's not declarative
+#  - "declaration valid when" statement that is basically a range with simple math rules
+#      - this is funcionally almost entirely the same
+#      - perhaps this sort of range plugs into the end of the schema?
+#      - but I want it to operate kind of like a frame or simulation zone
+#  - Begin / End declaration makes it more like a framed region
+#      - hypothetically I don't need to have any begin and I can just imply it
+#      - I don't wanna have to develop a bunch of code for dealing with new links that are only there for the sake of schema
+#  - then I ran into the problem that the in/out connections are relevant to a specific declaration
+#  - what I need is a way to modify the declaration in the loop, not a way to construct a bunch of different iterators....
+#  - so maybe I can get away with basic maths only
+
+# so I need a way to control a declaration by the index
+#   - a switch node, maybe one with an arbitrary socket type like wildcard that just adapts
+#   - it should be possible to do math with the index and len(schema)
+#       - example, for naming a bone 'Center' if index == len(schema)//2
+#            - the "if" is what annoys me about this
+#       - making numbers and chiral identifiers for names
+#       - array gets
+#   - 
+
+
+
+# class SchemaChoose(Node, SchemaNode):
+#     '''Choose'''
+#     bl_idname = 'SchemaChoose'
+#     bl_label = "Choose"
+#     bl_icon = 'GIZMO'
+#     initialized : bpy.props.BoolProperty(default = False)
+    
+#     def init(self, context):
+#         self.inputs.new('IntSocket', 'Number of Choices')
+#         self.inputs.new('IntSocket', 'Choose Index')
+#         self.outputs.new('WildcardSocket', 'Choice')
+#         self.update()
+
+#     def update(self):
+#         self.initialized = False
+#         input_map = get_socket_maps(self)[0]
+#         # print (input_map)
+#         self.inputs.clear()
+#         self.inputs.new('IntSocket', 'Number of Choices')
+#         self.inputs.new('IntSocket', 'Choose Index')
+#         #
+#         # update on this one requires being able to read the tree!
+#             # self.inputs.new("WildcardSocket", "")
+#         self.initialized = True

+ 653 - 0
schema_solve.py

@@ -0,0 +1,653 @@
+from .utilities import (prRed, prGreen, prPurple, prWhite,
+                              prOrange,
+                              wrapRed, wrapGreen, wrapPurple, wrapWhite,
+                              wrapOrange,)
+from .utilities import init_connections, init_dependencies
+from .utilities import class_for_mantis_prototype_node
+from .base_definitions import SchemaNode, replace_types, custom_props_types
+from .node_container_common import fill_parameters, setup_custom_props_from_np
+# a class that solves Schema nodes
+from uuid import uuid4
+
+# currently this is fairly ugly and a lot of cooupling but it is at least within the class
+
+# basically tho the idea is to make solving happen one iteration at time so that I can have nested structures
+# ultimately it will be much less messy, because I can simplify it
+# the initializer __init__ does all the necessary initialization
+# then solve_iteration should both solve the iteration and do all the cleanup
+# so the interface should be able to solve_iteration on this node until it is done
+
+# actually.. that wasn't necesary. Maybe the current solution is even... better?
+# I donno. Maybe doing one iteration at a time would be a better way to do things.
+# Now that it works, I can very easily write a second class and replace this one
+
+#example UUID and index
+#9e6df689-3d71-425e-be6c-fe768e7417ec.0000
+
+# def strip_uuid(signature):
+#     # prOrange("strip uuid",signature)
+#     ret_sig=[]
+#     for name in signature:
+#         if name is None:
+#             ret_sig.append(None)
+#             continue # this is normal, first element
+#         ret_sig.append(strip_uuid_string(name))
+#     return tuple(ret_sig)
+
+# def strip_uuid_string(string):
+#     import re
+#     split = re.split("\.[a-z,A-Z,0-9]{8}-[a-z,A-Z,0-9]{4}-[a-z,A-Z,0-9]{4}-[a-z,A-Z,0-9]{4}-[a-z,A-Z,0-9]{12}\.[0-9]{4}", string)
+#     prRed(string, split)
+#     return split[0]
+        
+# to get this working with groups.... ensure that group node sockets and such are all working (names, identifiers...)
+# current error seems to occur only when schema are nested
+# error is probably caused by the lack of auto-get nodes for Schema Group inputs that are not connected
+# but it may simply be caused by bad routing
+# both bugs should be fixed (if it is two bugs and not one)
+
+class SchemaSolver:
+    def __init__(self, schema_dummy, nodes, prototype, signature=None):
+        self.all_nodes = nodes # this is the parsed tree from Mantis
+        self.node = schema_dummy
+        # ugly.. but we are getting the Schema node's prototype, then its node tree
+        from .utilities import get_node_prototype
+        self.tree = prototype.node_tree# get_node_prototype(self.node.signature, self.node.base_tree).node_tree
+        self.uuid = self.node.uuid
+        self.schema_nodes={}
+        self.solved_nodes = {}
+        # self.out_nodes = {}
+
+        self.incoming_connections = {}
+        self.outgoing_connections = {}
+        self.constant_in = {}
+        self.constant_out = {}
+        self.array_input_connections = {}
+        self.array_output_connections = {}
+
+        # prGreen(self.node.signature[:-1])
+
+        # This singature/natural_signature thing is so, so bad and stupid and wrong... but it works so I won't change it
+        if signature:
+            self.natural_signature=signature
+            # print (signature)
+        else:
+            self.natural_signature=self.node.signature
+        
+        self.tree_path_names  = [*self.node.signature[:-1]] # same tree as the schema node
+        self.autogen_path_names = ['SCHEMA_AUTOGENERATED', *self.node.signature[1:-1]]
+        self.index_link = self.node.inputs['Schema Length'].links[0]
+
+        # TODO UNBREAK ME FIXME NOW
+        # identifiers are unfortunately not somethign I can set or predict in the items_tree
+        # but I can set them in the node.
+        # I either need to set them properly in the node so they always match
+        # or: I need to ignore the names and get the identifiers correctly
+        # self.node is a NC
+
+        # node_identifier_map_in = []
+        # node_identifier_map_out = []
+
+        for item in self.tree.interface.items_tree:
+            if item.item_type == 'PANEL': continue
+            if item.parent and item.parent.name == 'Connection':
+                if item.in_out == 'INPUT':
+                    if incoming_links := self.node.inputs[item.identifier].links:
+                        self.incoming_connections[item.name] = incoming_links[0]
+                    else:
+                        self.incoming_connections[item.name] = None # it isn't linked
+                        # print (self.node)
+                        # prRed("candidates...", self.node.inputs)
+                        # for k,v in self.node.inputs.items():
+                        #     print (k,v)
+                        # print(self.node.outputs)
+                        # print (self.node.parameters)
+                        # raise RuntimeError(f"Cannot find incoming connection \"{item.identifier}\" .")
+                else:
+                    if outgoing_links := self.node.outputs[item.identifier].links:
+                        self.outgoing_connections[item.name] = outgoing_links.copy()
+                    else:
+                        self.outgoing_connections[item.name] = []
+            if item.parent and item.parent.name == 'Constant':
+                if item.in_out == 'INPUT':
+                    if constant_in_links := self.node.inputs[item.identifier].links:
+                        self.constant_in[item.name] = constant_in_links[0]
+                    else:
+                        self.constant_in[item.name] = None
+                else:
+                    if constant_out_links := self.node.outputs[item.identifier].links:
+                        self.constant_out[item.name] = constant_out_links.copy()
+                    else:
+                        self.constant_out[item.name] = []
+            if item.parent and item.parent.name == 'Array':
+                if item.in_out == 'INPUT':
+                    if item.identifier not in self.array_input_connections.keys():
+                        self.array_input_connections[item.identifier]=[]
+                    if in_links := self.node.inputs[item.identifier].links:
+                        self.array_input_connections[item.identifier]=in_links.copy()
+                if item.in_out == 'OUTPUT':
+                    if item.identifier not in self.array_output_connections.keys():
+                        self.array_output_connections[item.identifier]=[]
+                    if out_links := self.node.outputs[item.identifier].links:
+                        self.array_output_connections[item.identifier] = out_links.copy()
+
+        self.held_links = []
+
+        # just define them for now... we redefine them properly later when they are needed. THis is messy.
+        self.index_str = lambda : '.'+str(self.uuid)+'.'+str(0).zfill(4)
+        self.prev_index_str = lambda : '.'+str(self.uuid)+'.'+str(0-1).zfill(4)
+
+        self.nested_schemas={}
+
+        self.autogenerated_nodes = {} # this is a bad ugly HACK, but I think I need to mark these and deal with them later
+
+        # Create the Schema Nodes
+        # prGreen(self.tree_path_names)
+
+        for n in self.tree.nodes:
+            if isinstance(n, SchemaNode):
+                # first we need to fill the parameters of the schema nodes.
+                # the node is actually in the Schema group so we include the schema_dummy name
+                # and we use the bl_idname because I think all schema nodes should be single-instance
+                signature = (*self.tree_path_names, self.node.signature[-1], n.bl_idname)
+                # get_sig = [*self.tree_path_names, strip_uuid_string(self.node.signature[-1]), n.bl_idname]
+                # get_sig[0] = None; get_sig = tuple(get_sig) # this is so dumb haha
+                get_sig = (*self.natural_signature, n.bl_idname)
+                
+                if not (nc := self.all_nodes.get(get_sig)): raise RuntimeError(wrapRed(f"Not found: {get_sig}"))
+                self.schema_nodes[signature] = nc
+                # nc.signature = signature # I don't really intend for this value to be mutable... but... HACK
+                # there is no need to mutate this. also I may need to reuse it later
+                fill_parameters(nc, n)
+                # print (nc)
+        
+    
+    def solve(self, schema_length):
+        import time
+        start_time = time.time()
+
+        # from .schema_containers import SchemaIndex
+        # for nc in self.schema_nodes.values():
+        #     if isinstance(nc, SchemaIndex):
+        #         #HACK? I thought for sure this was being done elsewhere...
+        #         nc.parameters["Schema Length"]=schema_length
+        #         prRed(nc.parameters)
+        frame_nc={}
+
+        for index in range(schema_length):
+            frame_nc = self.solve_iteration(index, schema_length)
+            for sig, nc in frame_nc.items():
+                if nc.node_type == 'DUMMY_SCHEMA':
+                    self.nested_schemas[sig] = nc
+        
+        # prRed (self.array_output_connections)
+        # for k,v in self.array_output_connections.items():
+        #     prRed(k,v)
+
+        self.finalize(frame_nc)
+
+        # prRed (self.array_output_connections)
+        # for k,v in self.array_output_connections.items():
+        #     prRed(k,v)
+
+
+        return self.solved_nodes
+
+    def solve_nested_schema(self, schema_nc):
+        if schema_nc.prepared == False:
+            all_nodes = self.all_nodes.copy()
+            # for k,v in self.solved_nodes.items():
+            #     all_nodes[k]=v
+
+            # from .utilities import get_node_prototype
+
+            np = schema_nc.prototype
+            # for n in self.node.base_tree.nodes:
+            #     print (n.name)
+            # print (schema_nc.signature[-1])
+            from .schema_solve import SchemaSolver
+            length = schema_nc.evaluate_input("Schema Length")
+
+            tree = np.node_tree
+            prOrange(f"Expanding schema {tree.name} in node {schema_nc} with length {length}.")
+
+            solver = SchemaSolver(schema_nc, all_nodes, np, schema_nc.natural_signature)
+            solved_nodes = solver.solve(length)
+            schema_nc.prepared = True
+
+            for k,v in solved_nodes.items():
+                self.solved_nodes[k]=v
+
+    def finalize(self, frame_nc):
+        from .schema_definitions import (SchemaOutgoingConnection,)
+        for i in range(len(self.held_links)):
+            link = self.held_links.pop()
+            to_np = link.to_socket.node; from_np = link.from_socket.node
+            if isinstance(to_np, SchemaOutgoingConnection):
+                if link.to_socket.name in self.outgoing_connections.keys():
+                    if (outgoing_links := self.outgoing_connections[link.to_socket.name]) is None: continue
+                    for outgoing in outgoing_links:
+                        if outgoing:
+                            to_node = outgoing.to_node
+                            from_node =frame_nc.get( (*self.autogen_path_names, from_np.name+self.index_str()) )
+                            connection = from_node.outputs[link.from_socket.name].connect(node=to_node, socket=outgoing.to_socket)
+                    # else: # the node just isn't connected out this socket.
+        
+
+        # # solve all unsolved nested schemas...
+        for schema_sig, schema_nc in self.nested_schemas.items():
+                self.solve_nested_schema(schema_nc)
+
+
+        for n in self.autogenerated_nodes.values():
+            init_connections(n)
+            for c in n.connections:
+                init_dependencies(c)
+
+        all_outgoing_links = []
+        for conn in self.outgoing_connections.values():
+            for outgoing in conn:
+                all_outgoing_links.append(outgoing)
+        for conn in self.constant_out.values():
+            for outgoing in conn:
+                all_outgoing_links.append(outgoing)
+        for conn in self.array_output_connections.values():
+            for outgoing in conn:
+                all_outgoing_links.append(outgoing)
+        
+        for outgoing in all_outgoing_links:
+            to_node = outgoing.to_node
+            for l in to_node.inputs[outgoing.to_socket].links:
+                other = l.to_node
+                other_input = l.to_socket
+                if self.node == l.from_node:
+                    l.die()
+        
+        for inp in self.node.inputs.values():
+            for l in inp.links:
+                init_connections(l.from_node) # to force it to have hierarchy connections with the new nodes.
+                    
+        
+
+    def solve_iteration(self, index, schema_length):
+
+        from .schema_definitions import (SchemaIndex,
+                                        SchemaArrayInput,
+                                        SchemaArrayInputGet,
+                                        SchemaArrayOutput,
+                                        SchemaConstInput,
+                                        SchemaConstOutput,
+                                        SchemaOutgoingConnection,
+                                        SchemaIncomingConnection,)
+        from bpy.types import (NodeFrame)
+        
+        from .utilities import clear_reroutes
+        from .utilities import get_link_in_out, link_node_containers
+        from .node_container_common import DummyLink
+
+        # if index_nc:
+        #     index_nc.parameters['Index']=index
+
+        self.index_str = lambda : '.'+str(self.uuid)+'.'+str(index).zfill(4)
+        # index_str = str(index).zfill(4)
+        self.prev_index_str = lambda : '.'+str(self.uuid)+'.'+str(index-1).zfill(4)
+        frame_nc = {}
+        # At this point, GENERATE all the nodes for the frame
+        for n in self.tree.nodes:
+            if isinstance(n, SchemaNode) or isinstance(n, NodeFrame):
+                continue
+            if n.bl_idname in ['NodeReroute']:
+                continue
+            # this is the N.C. which is a prototype of the NC we actually want to make...
+            signature = (*self.autogen_path_names, n.name+self.index_str())
+
+            # proto_nc = self.all_nodes.get((*self.tree_path_names, self.node.signature[-1], n.name))
+            proto_nc = self.all_nodes.get((*self.natural_signature, n.name))
+            # this proto_nc was generated inside the schema when we parsed the tree.
+            if not proto_nc:
+                raise RuntimeError(f"Node not found: {(*self.tree_path_names, self.node.signature[-1], n.name)}")
+            # for Schema sub-nodes ... they need a prototype to init.
+            if proto_nc.node_type in ['DUMMY', 'DUMMY_SCHEMA']:
+                from .utilities import get_node_prototype
+                np = get_node_prototype(proto_nc.signature, proto_nc.base_tree)
+                # assert np is not None
+                if proto_nc.node_type == 'DUMMY_SCHEMA':
+                    nat_sig = (*self.node.signature, np.name)
+                    nc = proto_nc.__class__(signature, proto_nc.base_tree, prototype=np, natural_signature=nat_sig)
+                else:
+                    nc = proto_nc.__class__(signature, proto_nc.base_tree, prototype=np)
+            else:
+            # try:
+                nc = proto_nc.__class__(signature, proto_nc.base_tree)
+            # except AttributeError:
+            #     from .utilities import get_node_prototype
+            #     np = get_node_prototype(proto_nc.signature, proto_nc.base_tree)
+            #     nc = proto_nc.__class__(signature, proto_nc.base_tree, prototype=np)
+
+            frame_nc[nc.signature] = nc
+            #
+            if nc.__class__.__name__ in custom_props_types:
+                setup_custom_props_from_np(nc, n)
+            fill_parameters(nc, n) # this is the best place to do this..
+
+
+        # This is where we handle node connections BETWEEN frames
+        for i in range(len(self.held_links)):
+            link = self.held_links.pop()
+            to_np = link.to_socket.node; from_np = link.from_socket.node
+            if isinstance(to_np, SchemaOutgoingConnection):
+                # incoming connection tells us where to take this.
+                incoming_node = self.schema_nodes[*self.tree_path_names, self.node.signature[-1], 'SchemaIncomingConnection']
+                for l in incoming_node.outputs[link.to_socket.name].links:
+                    to_node, to_socket = l.to_node, l.to_socket
+                    from_name = get_link_in_out(link)[0]
+                    from_node = self.solved_nodes.get( (*self.autogen_path_names, from_name+self.prev_index_str()) )
+                    #
+                    to_node = frame_nc.get( (*self.autogen_path_names, to_node.signature[-1]+self.index_str()) )
+                    # the to_node and to_socket don't really matter, the incoming connection will handle this part
+                    
+                    connection = from_node.outputs[link.from_socket.name].connect(node=to_node, socket=to_socket)
+                    if existing_link := self.incoming_connections[link.to_socket.name]:
+                        # if index == 0:
+                        if self.node.signature[-1] in existing_link.to_node.signature:
+                        # not sure this is helping
+                            existing_link.die()
+                        # could be better to make it a dummy link?
+                    self.incoming_connections[link.to_socket.name] = connection
+
+        links = clear_reroutes(list(self.tree.links))
+
+
+        # frame_nc_with_schema = frame_nc.copy()
+        # for k,v in self.schema_nodes.items(): frame_nc_with_schema[k]=v
+
+        # Now we handle links in the current frame, including those links between Schema nodes and "real" nodes
+        # this gets really complicated :\
+        awaiting_prep_stage = []
+        for link in links:
+        # at THIS POINT I should make a buncha dummy links to deal
+        #   with the schema node connections...
+            to_np = link.to_socket.node; from_np = link.from_socket.node
+            if isinstance(to_np, SchemaConstOutput) or isinstance(to_np, SchemaArrayOutput) or \
+                isinstance(from_np, SchemaArrayInputGet):# or isinstance(from_np, SchemaArrayInput):
+                awaiting_prep_stage.append(link)
+                continue
+            if isinstance(from_np, SchemaIndex):
+                if link.from_socket.name == "Index":
+                    _from_name, to_name = get_link_in_out(link)
+                    to_node = frame_nc.get( (*self.autogen_path_names, to_name+self.index_str()) )
+
+                    if to_node.node_type in ['DUMMY', 'DUMMY_SCHEMA']:
+                        # prRed("This is causing the problem, right?")
+                        from .utilities import gen_nc_input_for_data
+                        nc_cls = gen_nc_input_for_data(link.from_socket)
+                        if (nc_cls): #HACK
+                            sig = ("MANTIS_AUTOGENERATED", *self.tree_path_names[1:], self.index_str(), link.from_socket.name, link.from_socket.identifier)
+                            nc_from = nc_cls(sig, self.node.base_tree)
+                            # ugly! maybe even a HACK!
+                            nc_from.inputs = {}
+                            from .node_container_common import NodeSocket
+                            nc_from.outputs = {link.from_socket.name:NodeSocket(name = link.from_socket.name, node=nc_from)}
+                            from .node_container_common import get_socket_value
+                            nc_from.parameters = {link.from_socket.name:index}
+                            frame_nc[sig]=nc_from
+                            from_node = nc_from
+                            # self.autogenerated_nodes[sig]=from_node
+                            self.solved_nodes[sig]=from_node
+
+                            connection = from_node.outputs[link.from_socket.name].connect(node=to_node, socket=link.to_socket.identifier)
+                            # prGreen(connection)
+                        continue
+
+                    # this actually seems to work. I could use Autogen nodes
+                    # but maybe this is actually better since it results in fewer nodes.
+                    # if this never causes any problems, then I will do it in other places
+                    # HACK Here Be Danger HACK
+                    to_node.parameters[link.to_socket.name] = index
+                    del to_node.inputs[link.to_socket.name]
+                    # so I have tried to implement this as connections and actually this is the best way because otherwise I have
+                    #  to do something very similar on the input node.
+                    # HACK
+                elif link.from_socket.name == "Schema Length":
+                    # # see, here I can just use the schema node
+                    _from_name, to_name = get_link_in_out(link)
+                    to_node = frame_nc.get( (*self.autogen_path_names, to_name+self.index_str()) )
+                    # this self.index_link is only used here?
+                    if (self.index_link.from_node):
+                        connection = self.index_link.from_node.outputs[self.index_link.from_socket].connect(node=to_node, socket=link.to_socket.name)
+                    # otherwise we can autogen an int input I guess...?
+                    else:
+                        raise RuntimeError("I was expecting there to be an incoming connection here for Schema Length")
+                continue
+            if isinstance(from_np, SchemaIncomingConnection):
+                if link.from_socket.name in self.incoming_connections.keys():
+                    incoming = self.incoming_connections[link.from_socket.name]
+                    # if incoming is None:
+                    #     print (link.from_socket.name)
+                    from_node = incoming.from_node
+                    _from_name, to_name = get_link_in_out(link)
+                    to_node = frame_nc.get( (*self.autogen_path_names, to_name+self.index_str()) )
+                    # try:
+                    connection = from_node.outputs[incoming.from_socket].connect(node=to_node, socket=link.to_socket.name)
+                    # prGreen(connection)
+                    # except KeyError as e:
+                    #     prRed(f"busted: {from_node}:{incoming.from_socket} --> {to_node}:{link.to_socket.name},{link.to_socket.identifier}")
+                        # for output in from_node.outputs:
+                        #     prOrange(output)
+                        # for sinput in from_node.inputs:
+                        #     prPurple(sinput)
+                        # raise e
+                    init_connections(from_node)
+                continue
+            if isinstance(to_np, SchemaOutgoingConnection):
+                self.held_links.append(link)
+                continue 
+            if isinstance(from_np, SchemaConstInput):
+                if link.from_socket.name in self.constant_in.keys():
+                    incoming = self.constant_in[link.from_socket.name]
+                    from_node = incoming.from_node
+                    to_name = get_link_in_out(link)[1]
+                    to_node = frame_nc.get( (*self.autogen_path_names, to_name+self.index_str()) )
+
+                    # print(from_node, incoming.from_socket, "==>", to_node, link.to_socket.identifier)
+                    # print (to_node.inputs)
+                    # for k,v in from_node.outputs.items():
+                    #     print (k,v)
+                    # print (from_node.outputs[incoming.from_socket])
+                    to_socket=link.to_socket.name
+                    from .base_definitions import SchemaGroup
+                    if isinstance(to_np, SchemaGroup):
+                        to_socket=link.to_socket.identifier
+                    connection = from_node.outputs[incoming.from_socket].connect(node=to_node, socket=to_socket)
+                    init_connections(from_node)
+                continue 
+            if isinstance(to_np, SchemaArrayInputGet):
+                from_name, to_name = get_link_in_out(link)
+                from_nc = frame_nc.get( (*self.autogen_path_names, from_name+self.index_str()))
+                to_nc = self.schema_nodes.get((*self.tree_path_names, self.node.signature[-1], to_name))
+                # this only needs to be done once.
+                if index == 0:
+                    old_nc = self.all_nodes.get((*self.tree_path_names, self.node.signature[-1], from_name))
+                    # I am not sure about this!
+                    existing_link = old_nc.outputs[link.from_socket.name].links[0]
+                    existing_link.die()
+                #
+                connection = from_nc.outputs[link.from_socket.name].connect(node=to_nc, socket=link.to_socket.name)
+                continue
+            if isinstance(from_np, SchemaArrayInput):
+                get_index = index
+                try:
+                    incoming = self.array_input_connections[link.from_socket.identifier][get_index]
+                except IndexError:
+                    if len(self.array_input_connections[link.from_socket.identifier]) > 0:
+                        incoming = self.array_input_connections[link.from_socket.identifier][0]
+                        # prOrange(incoming.from_node.node_type)
+                        if incoming.from_node.node_type not in ['DUMMY_SCHEMA']:
+                            raise RuntimeError(wrapRed("You need to make it so Mantis checks if there are enough Array inputs."))
+                        else: # do nothing
+                            continue
+                    else:
+                        raise RuntimeError(wrapRed("make it so Mantis checks if there are enough Array inputs!"))
+                to_name = get_link_in_out(link)[1]
+                to_node = frame_nc.get((*self.autogen_path_names, to_name+self.index_str()))
+                connection = incoming.from_node.outputs[incoming.from_socket].connect(node=to_node, socket=link.to_socket.name)
+                init_connections(incoming.from_node)
+                continue
+            link_path_names = self.tree_path_names[1:]
+            # use this line to debug, usually I don't need it
+            connection = link_node_containers(self.autogen_path_names, link, frame_nc, from_suffix=self.index_str(), to_suffix=self.index_str())
+        for k,v in frame_nc.items():
+            self.solved_nodes[k]=v
+            init_dependencies(v) # it is hard to overstate how important this single line of code is
+            # for node in v.hierarchy_dependencies:
+            #     init_connections(node)
+
+        # done with the link handling.
+        
+
+        # Start Section: This is the place where we solve dependencies and continue
+        # we need to sort the nodes in solved_nc and prepare whatever can be preepared.
+        from collections import deque
+        unprepared= deque()
+        for nc in frame_nc.values():
+            if nc.prepared == False:
+                unprepared.append(nc)
+                # this is potentially a little inneficient but it isn't a big deal.
+                # since the only extra preparations I am doing is nodes that don't yet have outgoing connections
+                # but I may add them in the next step anyways, then I need to prepare them! so this is simpler.
+        # at this point it should be possible to sort unprepared to avoid the while loop
+        # but I don't really care. this will work since we have guarenteed solved all the
+        #  schema_dummy's dependencies.... I hope. lol.
+
+        while unprepared: # DANGER.
+            # raise NotImplementedError
+            # prRed("this part!")
+            nc = unprepared.pop()
+            # print(nc)
+            if sum([dep.prepared for dep in nc.hierarchy_dependencies]) == len(nc.hierarchy_dependencies):
+                nc.bPrepare()
+                if nc.node_type == 'DUMMY_SCHEMA':
+                    self.solve_nested_schema(nc)
+
+            else:
+                # print (sum([dep.prepared for dep in nc.hierarchy_dependencies]), len(nc.hierarchy_dependencies))
+                # for dep in nc.hierarchy_dependencies:
+                #     if not dep.prepared:
+                #         prOrange(dep)
+                unprepared.appendleft(nc)
+        
+        for i in range(len(awaiting_prep_stage)): #why is this a for loop and not a while loop?? # FIXME?
+            link = awaiting_prep_stage.pop()
+            to_np = link.to_socket.node; from_np = link.from_socket.node
+            if isinstance(to_np, SchemaConstOutput):
+                to_node = self.schema_nodes.get((*self.tree_path_names, self.node.signature[-1], to_np.bl_idname))
+                expose_when = to_node.evaluate_input('Expose when N==')
+                if index == expose_when:
+                    for outgoing in self.constant_out[link.to_socket.name]:
+                        to_node = outgoing.to_node
+                        from_name = get_link_in_out(link)[0]
+                        from_node = frame_nc.get( (*self.autogen_path_names, from_name+self.index_str()) )
+                        connection = from_node.outputs[link.from_socket.name].connect(node=to_node, socket=outgoing.to_socket)
+            if isinstance(to_np, SchemaArrayOutput): # if this duplicated code works, dedupe!
+                to_node = self.schema_nodes.get((*self.tree_path_names, self.node.signature[-1], to_np.bl_idname))
+                for outgoing in self.array_output_connections[link.to_socket.identifier]:
+                    # print (type(outgoing))
+                    from .schema_containers import SchemaIndex
+                    from_name = get_link_in_out(link)[0]
+                    from_node = frame_nc.get( (*self.autogen_path_names, from_name+self.index_str()) )
+                    if not from_node:
+                        from_node = self.schema_nodes.get( (*self.tree_path_names, self.node.signature[-1], from_np.bl_idname) )
+                    if not from_node:
+                        raise RuntimeError()
+                    to_node = outgoing.to_node
+
+                    if isinstance(from_node, SchemaIndex): # I think I need to dedup this stuff
+                        # print("INDEX")
+                        from .utilities import gen_nc_input_for_data
+                        nc_cls = gen_nc_input_for_data(link.from_socket)
+                        if (nc_cls): #HACK
+                            sig = ("MANTIS_AUTOGENERATED", *self.tree_path_names[1:], self.index_str(), link.from_socket.name, link.from_socket.identifier)
+                            nc_from = nc_cls(sig, self.node.base_tree)
+                            # ugly! maybe even a HACK!
+                            nc_from.inputs = {}
+                            from .node_container_common import NodeSocket
+                            nc_from.outputs = {link.from_socket.name:NodeSocket(name = link.from_socket.name, node=nc_from)}
+                            from .node_container_common import get_socket_value
+                            if link.from_socket.name in ['Index']:
+                                nc_from.parameters = {link.from_socket.name:index}
+                            else:
+                                nc_from.parameters = {link.from_socket.name:schema_length}
+                            frame_nc[sig]=nc_from
+                            from_node = nc_from
+                            self.solved_nodes[sig]=from_node
+
+                    # I have a feeling that something bad will happen if both of these conditions (above and below) are true
+                    if to_node.node_type == 'DUMMY_SCHEMA' and to_node.prepared:
+                        other_stem = ('SCHEMA_AUTOGENERATED', *to_node.signature[1:-1])
+                        from .utilities import get_node_prototype
+                        other_schema_np = get_node_prototype(to_node.signature, to_node.base_tree)
+                        other_schema_tree = other_schema_np.node_tree
+                        for n in other_schema_tree.nodes:
+                            if n.bl_idname not in ["SchemaArrayInput", "SchemaArrayInputGet"]:
+                                continue
+                            out = n.outputs[outgoing.to_socket]
+                            for l in out.links:
+                                other_index_str = lambda : '.'+str(to_node.uuid)+'.'+str(index).zfill(4)
+                                out_node = self.all_nodes.get((*other_stem, l.to_node.name+other_index_str()))
+                                connection = from_node.outputs[link.from_socket.name].connect(node=out_node, socket=l.to_socket.name)
+                    else:
+                        connection = from_node.outputs[link.from_socket.name].connect(node=to_node, socket=outgoing.to_socket)
+                    
+                    
+            if isinstance(from_np, SchemaArrayInputGet): # or isinstance(from_np, SchemaArrayInput) or 
+                get_index = index
+                if isinstance(from_np, SchemaArrayInputGet):
+                    from_node = self.schema_nodes.get((*self.tree_path_names, self.node.signature[-1], from_np.bl_idname))
+                    from .utilities import cap, wrap
+                    get_index = from_node.evaluate_input("Index", index)
+                    oob = from_node.evaluate_input("OoB Behaviour")
+                    # we must assume that the array has sent the correct number of links
+                    if oob == 'WRAP':
+                        get_index = wrap(get_index, len(self.array_input_connections[link.from_socket.identifier])-1, 0)
+                    if oob == 'HOLD':
+                        get_index = cap(get_index, len(self.array_input_connections[link.from_socket.identifier])-1)
+                try:
+                    incoming = self.array_input_connections[link.from_socket.identifier][get_index]
+                except IndexError:
+                    raise RuntimeError(wrapRed("Dummy! You need to make it so Mantis checks if there are enough Array inputs! It should probably have a Get Index!"))
+                to_name = get_link_in_out(link)[1]
+                to_node = frame_nc.get((*self.autogen_path_names, to_name+self.index_str()))
+                connection = incoming.from_node.outputs[incoming.from_socket].connect(node=to_node, socket=link.to_socket.name)
+                init_connections(incoming.from_node)
+            # end seciton
+        return frame_nc
+
+
+            
+
+# TODO: figure out why the tree is sorting wrong when using arrays!
+
+
+
+
+
+# despite a lot of ugly hacks, things are going well
+# current TODO:
+#  - get nested schema working when nodes in the parent tree depend on them
+#  - eventually I can clean all this up by re-designing the solver code to solve one iteration at a time instead of all at once
+
+#  - get schema-in-group working
+#  - groups need constants and arrays 
+# maybe the simplest solution is for groups to be schema always
+# but I don't want to do that
+
+# anyways the next milestone is for the spine to be a group or schema output
+
+# note that schemas will send out arrays and such. So I don't necessarily want groups to have array in/out
+# instead, groups are simple
+# they can contain schema, but schema can't have arrays from the group interface
+# groups are good for large, abstract components
+# fundamentally they are similar but groups are cleaner since they don't require dependency solving
+
+# anyways... schema with nesting and groups is good enough for now
+# I don't need it to be stable. I just need to rig my human model with Mantis and iterate from there. Getting it to rig a human character will find and fix most bugs.

File diff suppressed because it is too large
+ 333 - 156
socket_definitions.py


+ 44 - 9
update_grandalf.sh

@@ -1,12 +1,18 @@
 #!/bin/bash
 
-# delete stale grandalf
-rm -rf ./grandalf
-# oddly, github can convert to svn and then I can checkout the folder as an svn repo...?
-# get it
-svn checkout https://github.com/bdcht/grandalf/trunk/grandalf/
-# delete the .svn stuff and the __pycache__ if they exist
-rm -rf ./grandalf/.svn
+# # delete stale grandalf
+# rm -rf ./grandalf
+# # oddly, github can convert to svn and then I can checkout the folder as an svn repo...?
+# # get it
+# svn checkout https://github.com/bdcht/grandalf/trunk/grandalf/
+# # delete the .svn stuff and the __pycache__ if they exist
+# rm -rf ./grandalf/.svn
+# github no longer supports this, darn
+#
+# https://www.educative.io/answers/how-to-download-a-single-folder-or-directory-from-a-github-repo
+#
+# i have already set it up for sparse checkout but it doesn't do exactly what I want so I have top fudge it
+# pass
 if [[ -d "./grandalf/__pycache__" ]]
 then
     rm -r ./grandalf/__pycache__
@@ -15,7 +21,36 @@ if [[ -d "./grandalf/utils/__pycache__" ]]
 then
     rm -r ./grandalf/utils/__pycache__
 fi
+
+mkdir grandalf 2>/dev/null # we don't need the error message
+cd grandalf
+
+if [[ -d "./.git" ]]
+then
+    echo 'skipping initialize grandalf because it already exists'
+    rm -rf utils
+    rm -rf *.py # danger here
+else
+    git init
+    git remote add origin https://github.com/bdcht/grandalf.git
+    echo 'grandalf' > .git/info/sparse-checkout
+    git config core.sparseCheckout true
+fi
+
+git restore *
+git pull origin master
+mv ./grandalf/* .
+rmdir grandalf
+rm ./utils/__init__.py
+rm __init__.py
+# there is probably a right way to do this but the extensions thing makes it really irritating
+# annoying
+
 # fix the imports, they need to be made relative to Mantis.
-sed -i 's/grandalf\.utils/.utils/g' ./grandalf/*.py # annoyingly, it isn't consistent in the repo
-sed -i 's/\.utils/mantis.grandalf.utils/g' ./grandalf/*.py
+sed -i 's/grandalf\.utils/.utils/g' ./*.py # annoyingly, it isn't consistent in the repo
+# sed -i 's/\.utils/.grandalf.utils/g' ./*.py
+
+# no idea why anything this stupid and bad and dumb is necessary
+sed -i 's/from \.utils import Poset/from \.utils.poset import Poset/g' ./*.py
 # this works
+cd ..

+ 1026 - 250
utilities.py

@@ -22,15 +22,11 @@ def prOrange(*args): print (*[wrapOrange(arg) for arg in args])
 #                               wrapOrange,)
 
 
-# uncomment to turn them off.
-# def prRed(*args): return; print (*[wrapRed(arg) for arg in args])
-# def prGreen(*args): return; print (*[wrapGreen(arg) for arg in args])
-# def prPurple(*args): return; print (*[wrapPurple(arg) for arg in args])
-# def prWhite(*args): return; print (*[wrapWhite(arg) for arg in args])
-# def prOrange(*args): return; print (*[wrapOrange(arg) for arg in args])
 
 
 
+#  SOME PRINTS
+
 #DO! Figure out what the hell this does
 # then re-write it in a simpler, cleaner way
 # that ignores groups because it gets lines from a parsed tree
@@ -115,6 +111,7 @@ def print_parsed_node(parsed_node):
     return string
 
 
+## SIGNATURES ##
 def get_socket_signature(line_element):
     """
     This function creates a convenient, hashable signature for
@@ -131,285 +128,1064 @@ def get_socket_signature(line_element):
     signature.append(socket.node.name); signature.append(socket.identifier)
     return tuple(signature)
 
-    
-    
 def tuple_of_line(line):
     # For creating a set of lines
     return tuple(tuple_of_line_element(e) for e in line)
 def tuple_of_line_element(line_element):
     return (line_element[0], tuple(line_element[1]))
 
-
-# This has a lot of branches and is kinda slow.
-def socket_seek(socket, trees):
-    from bpy.types import NodeReroute, NodeGroupOutput
-    if (hasattr( socket.node, "traverse")):
-        if (socket.node.bl_idname == "MantisNodeGroup"):
-            trees.append(socket.node)
-            socket = socket.node.traverse(socket, "IN")
+# A fuction for getting to the end of a Reroute.
+def socket_seek(start_link, links):
+    link = start_link
+    while(link.from_socket):
+        for newlink in links:
+            if link.from_socket.node.inputs:
+                if newlink.to_socket == link.from_socket.node.inputs[0]:
+                    link=newlink; break
         else:
-            socket = socket.node.traverse(socket)
-    elif (isinstance(socket.node, NodeReroute)):
-        socket = socket.node.outputs[0]
-    elif (isinstance(socket.node, NodeGroupOutput)):
-        group_node = trees.pop()
-        if group_node:
-            socket = group_node.traverse(socket, "OUT")
+            break
+    return link.from_socket
+
+# this creates fake links that have the same interface as Blender's
+# so that I can bypass Reroutes
+def clear_reroutes(links):
+    from .node_container_common import DummyLink
+    kept_links, rerouted_starts = [], []
+    rerouted = []
+    all_links = links.copy()
+    while(all_links):
+        link = all_links.pop()
+        to_cls = link.to_socket.node.bl_idname
+        from_cls = link.from_socket.node.bl_idname
+        reroute_classes = ["NodeReroute"]
+        if (to_cls in reroute_classes and
+            from_cls in reroute_classes):
+                rerouted.append(link)
+        elif (to_cls in reroute_classes and not
+            from_cls in reroute_classes):
+                rerouted.append(link)
+        elif (from_cls in reroute_classes and not
+            to_cls in reroute_classes):
+                rerouted_starts.append(link)
         else:
-            raise RuntimeError("Error parsing Group Nodes")
-    else:
-        raise RuntimeError("Error: node tree cannot be navigated")
-    return socket, trees
-
-#This is a little slow.
-def lines_from_socket(sock, tree_path = [None]):
-    done = False
-    sPath =[0,]
-    lines = []
-    while (not done):
-        seek = sock
-        trees = tree_path.copy() # make sure to copy, lists are not immutable
-        for curheight, ind in enumerate(sPath):
-            if not seek: #this will cause the loop to complete normally
-                continue # which will return an error
-            if (ind <= (len(seek.links) -1)):
-                seek = seek.links[ind].to_socket
-                nextseek, trees = socket_seek(seek, trees.copy())
-                if (not nextseek): # The node has no no traverse function.
-                    # note, kind of duplicated code, TODO,
-                    lines.append(sPath[:curheight+1])
-                    if (curheight > 0):
-                        sPath[curheight] += 1
-                    else:
-                        done = True
-                    break
-                if (nextseek.is_linked): #otherwise this is a leaf
-                    seek = nextseek
-                    if (curheight == len(sPath)-1): #go up
-                        sPath.append(0)
-                elif not (nextseek.is_linked):
-                    lines.append(sPath[:curheight+1])
-                    sPath[curheight]+=1
-                    # this makes sure we're progressing through the tree.
-                    break
+            kept_links.append(link)
+    for start in rerouted_starts:
+        from_socket = socket_seek(start, rerouted)
+        new_link = DummyLink(from_socket=from_socket, to_socket=start.to_socket, nc_from=None, nc_to=None)
+        kept_links.append(new_link)
+    return kept_links
+
+
+def tree_from_nc(sig, base_tree):
+    if (sig[0] == 'MANTIS_AUTOGENERATED'):
+        sig = sig[:-2] # cut off the end part of the signature. (Why am I doing this??) # because it uses socket.name and socket.identifier
+        # this will lead to totally untraceble bugs in the event of a change in how signatures are assigned
+    tree = base_tree
+    for i, path_item in enumerate(sig):
+        if (i == 0) or (i == len(sig) - 1):
+            continue
+        tree = tree.nodes.get(path_item).node_tree
+    return tree
+    
+def get_node_prototype(sig, base_tree):
+    return tree_from_nc(sig, base_tree).nodes.get( sig[-1] )
+
+
+##################################################################################################
+# groups and changing sockets -- this is used extensively by Schema.
+##################################################################################################
+
+def get_socket_maps(node):
+    maps = [{}, {}]
+    node_collection = ["inputs", "outputs"]
+    links = ["from_socket", "to_socket"]
+    for collection, map, link in zip(node_collection, maps, links):
+        for sock in getattr(node, collection):
+            if sock.is_linked:
+                map[sock.identifier]=[ getattr(l, link) for l in sock.links ]
             else:
-                if (curheight > 0):
-                    sPath.pop() #go back...
-                    sPath[curheight-1] += 1
+                map[sock.identifier]=sock.get("default_value")
+    return maps
+
+def do_relink(node, s, map, in_out='INPUT', parent_name = ''):
+    tree = node.id_data; interface_in_out = 'OUTPUT' if in_out == 'INPUT' else 'INPUT'
+    if hasattr(node, "node_tree"):
+        tree = node.node_tree
+        interface_in_out=in_out
+    from bpy.types import NodeSocket
+    get_string = '__extend__'
+    if s: get_string = s.identifier
+    if val := map.get(get_string):
+        if isinstance(val, list):
+            for sub_val in val:
+                # this will only happen once because it assigns s, so it is safe to do in the for loop.
+                if s is None:
+                    # prGreen("zornpt")
+                    name = unique_socket_name(node, sub_val, tree)
+                    sock_type = sub_val.bl_idname
+                    if parent_name:
+                        interface_socket = update_interface(tree.interface, name, interface_in_out, sock_type, parent_name)
+                    if in_out =='INPUT':
+                        s = node.inputs.new(sock_type, name, identifier=interface_socket.identifier)
+                    else:
+                        s = node.outputs.new(sock_type, name, identifier=interface_socket.identifier)
+                    if parent_name == 'Array': s.display_shape='SQUARE_DOT'
+                    # then move it up and delete the other link.
+                    # this also needs to modify the interface of the node tree.
                     
-                else:
-                    done = True
-                break
+                    
+                #
+                if isinstance(sub_val, NodeSocket):
+                    if in_out =='INPUT':
+                        node.id_data.links.new(input=sub_val, output=s)
+                    else:
+                        node.id_data.links.new(input=s, output=sub_val)
         else:
-            raise RuntimeError("There has been an error parsing the tree")
-    return lines
+            try:
+                s.default_value = val
+            except (AttributeError, ValueError): # must be readonly or maybe it doesn't have a d.v.
+                pass
 
+def update_interface(interface, name, in_out, sock_type, parent_name):
+    if parent_name:
+        if not (interface_parent := interface.items_tree.get(parent_name)):
+            interface_parent = interface.new_panel(name=parent_name)
+        socket = interface.new_socket(name=name,in_out=in_out, socket_type=sock_type, parent=interface_parent)
+        if parent_name == 'Connection':
+            in_out = 'OUTPUT' if in_out == 'INPUT' else 'INPUT' # flip this make sure connections always do both
+            interface.new_socket(name=name,in_out=in_out, socket_type=sock_type, parent=interface_parent)
+        return socket
+    else:
+        raise RuntimeError(wrapRed("Cannot add interface item to tree without specifying type."))
 
-# only using this once, should this even be a function?
-def create_socket_lists(sock, tree_path, lines):
-    out_lines = []
-    for line in lines:
-        s = sock
-        trees = tree_path.copy()
-        out_line = [(s, trees)]
-        for i, ind in enumerate(line):
-            if i < len(line):
-                s_next = s.links[ind].to_socket
-                s_final, trees = socket_seek(s_next, trees.copy())
-                if s_final:
-                    s = s_final
-                else: # The node has no no traverse function.
-                    # this needs special check, if it's the first node,
-                    #  it's already in the tree.
-                    if (i > 0):
-                        out_line.append( (s, trees) )
-                    out_line.append( (s_next, trees) )
-                    break
-            # nodes to skip...
-            if (s.node.bl_idname in [
-                                     "NodeReroute",
-                                     "MantisNodeGroupOutput",
-                                     "NodeGroupOutput",
-                                     "MantisNodeGroupInput",
-                                     "NodeGroupInput"
-                                    ]):
-                continue
-            
-            out_line.append( (s, trees) )
-        out_lines.append(out_line)
-    return out_lines
-
-#NOTE: this may not work at all lol
-# TODO BUG HACK rename and remove this before publishing
-def find_root_nodes(tree, tree_path = [None]):
-    root_nodes = []
-    for node in tree.nodes:
-        addMe = True
-        for s in node.inputs:
-            if (s.is_linked == True):
-                addMe = False
-                # for now, don't try to sovle this, it will need 
-                #  a backwards search
-                for link in s.links:
-                    # we need to check if this is a "false" connection;
-                    #  that is, a Group In from an unconnected Group
-                    if (link.from_socket.node.bl_idname in ["NodeGroupInput", "MantisNodeGroupInput"]):
-                        identifier = link.from_socket.identifier
-                        for grp in tree_path[1:][::-1]:
-                            for inp in grp.inputs:
-                                if inp.identifier == identifier and not inp.is_linked:
-                                    addMe=True
-                                    break
-                            else:
-                                addMe=False
-        if (hasattr(node, "node_tree")):
-            # we use the node itself here for the node path, will use it for node signature later.
-            root_nodes.extend( find_root_nodes(node.node_tree, tree_path+[node]) )
-        
-        if (node.bl_idname in [
-                               "NodeReroute",
-                               "MantisNodeGroupOutput",
-                               "NodeGroupOutput",
-                               "MantisNodeGroupInput",
-                               "NodeGroupInput",
-                               "NodeFrame",
-                              ]):
-            addMe = False
-            continue
-        
-        if (addMe):
-            root_nodes.append( (tree_path, node) )
-    return root_nodes
+def relink_socket_map(node, node_collection, map, item, in_out=None):
+    from bpy.types import NodeSocket
+    if not in_out: in_out=item.in_out
+    if node.bl_idname in ['MantisSchemaGroup'] and item.parent and item.parent.name == 'Array':
+        multi = False
+        if in_out == 'INPUT':
+            multi=True
+        s = node_collection.new(type=item.socket_type, name=item.name, identifier=item.identifier,  use_multi_input=multi)
+        # s.link_limit = node.schema_length TODO
+    else:
+        s = node_collection.new(type=item.socket_type, name=item.name, identifier=item.identifier)
+    if item.parent.name == 'Array': s.display_shape = 'SQUARE_DOT'
+    do_relink(node, s, map)
 
+def unique_socket_name(node, other_socket, tree):
+    name_stem = other_socket.bl_label; num=0
+    # if hasattr(other_socket, "default_value"):
+    #     name_stem = type(other_socket.default_value).__name__
+    for item in tree.interface.items_tree:
+        if item.item_type == 'PANEL': continue
+        if other_socket.is_output and item.in_out == 'INPUT': continue
+        if not other_socket.is_output and item.in_out == 'OUTPUT': continue
+        if name_stem in item.name: num+=1
+    name = name_stem + '.' + str(num).zfill(3)
+    return name
 
 
-def parse_node_tree(tree):
-    root_nodes = find_root_nodes(tree)
-    # this seems to produce garbage results. Check this!
 
-    input_lines = []
-    
-    for path, node in root_nodes:
-        # print (path, node)
-        for s in node.outputs:
-            socket_lists = create_socket_lists(s, path, lines_from_socket(s, path))
-            for line in socket_lists:
-                in_line = line.copy()
-                input_lines.append(in_line)
-    # NOT SURE if any of this stuff below is necesary at all
-    return (input_lines) # let's find out if it is
-    #
-    
-    # I think the unreachable code here is bad. TODO: figure out if there's
-    #       any reason at all to revive this old code.
-    #
-    # I certainly *like* the idea of removing redundant data.
-    #
-    # note: it seems like the Execute Tree function is completing
-    #     in 80% of the previous time after removing the below
-    #     meanign that this was wasting 1/5 of the total time
-    no_dupes_sigs = set()
-    no_dupes_lines = set()
-    for in_line in input_lines:
-        sig = get_socket_signature(in_line[-1])
-        sig = list(sig)
-        sig.append(in_line[-1][0].name) # socket
-        sig = tuple(sig)
-
-        before = len(no_dupes_sigs)
-        no_dupes_sigs.add(sig)
-        after = len(no_dupes_sigs)
-        # make a tuple of the node path, too.
-        # in_line = tuple(in_line[0]), in_line[1]
-        
-        if (before < after): # new item
-            no_dupes_lines.add(tuple_of_line(in_line))
-    #MAYBE
-    # maybe i can get a list of all nodes
-    # including nodes in nodegroups and nested node groups
-    # then I can assign a uuid to each one
-    # and associate the uuids with the node lines
-    # perhaps I should do that before running this function
-    
-    # for line in no_dupes_lines:
-        # print (list(line))
-    
-    return (list(no_dupes_lines))
+##############################
+#  READ TREE and also Schema Solve!
+##############################
 
-    # don't deal with lines no mo. Do stuff with line elements
+def init_connections(nc):
+    c, hc = [], []
+    for i in nc.outputs.values():
+        for l in i.links:
+            # if l.from_node != nc:
+            #     continue
+            if l.is_hierarchy:
+                hc.append(l.to_node)
+            c.append(l.to_node)
+    nc.hierarchy_connections = hc
+    nc.connections = c
 
-# DO THIS!
-# make lines_from_socket attach a tree path to each node-socket, instead of just a tree
-# that way, if the tree-path is longer than the socket-path, the tree path won't be truncated.
+def init_dependencies(nc):
+    c, hc = [], []
+    for i in nc.inputs.values():
+        for l in i.links:
+            # if l.to_node != nc:
+            #     continue
+            if l.is_hierarchy:
+                hc.append(l.from_node)
+            c.append(l.from_node)
+    nc.hierarchy_dependencies = hc
+    nc.dependencies = c
 
+# schema_input_types = [
+#         'SchemaIndex',
+#         'SchemaArrayInput',
+#         'SchemaArrayInputGet',
+#         'SchemaConstInput',
+#         'SchemaIncomingConnection',
+# ]
+# schema_output_types = [
+#         'SchemaArrayOutput',
+#         'SchemaConstOutput',
+#         'SchemaOutgoingConnection',
+# ]
 
+from .base_definitions import from_name_filter, to_name_filter
 
-# for use with node signatures
+def init_schema_dependencies(schema, all_nc):
+    schema_name = schema.signature[-1]
+    all_input_nodes = []
+    all_output_nodes = []
+    # all_inernal_nodes = []
+    # for nc in all_nc.values():
+    #     for t in schema_input_types:
+    #         if nc.signature == (*schema.signature, t):
+    #             all_input_nodes.append(nc)
+    #     for t in schema_output_types:
+    #         if nc.signature == (*schema.signature, t):
+    #             all_output_nodes.append(nc)
+    # prOrange (schema.connections)
+    # print (schema.hierarchy_connections)
+    # prOrange (schema.dependencies)
+    # prOrange (schema.hierarchy_dependencies)
 
-def tree_from_nc(sig, base_tree):
-    if (sig[0] == 'MANTIS_AUTOGENERATED'):
-        sig = sig[:-2] # cut off the input part of the signature.
-    tree = base_tree
-    for i, path_item in enumerate(sig):
-        if (i == 0) or (i == len(sig) - 1):
+    # so the challenge is to map these and check both ends
+    from .base_definitions import from_name_filter, to_name_filter
+    # go through the interface items then of course
+    from .utilities import get_node_prototype
+    np = get_node_prototype(schema.signature, schema.base_tree)
+    tree = np.node_tree
+    schema.dependencies = []
+    schema.hierarchy_dependencies = []
+    for item in tree.interface.items_tree:
+        if item.item_type == 'PANEL':
             continue
-        tree = tree.nodes.get(path_item).node_tree
-    return tree
+        hierarchy = True
+        hierarchy_reason=""
+        if item.in_out == 'INPUT':
+            c = schema.dependencies
+            hc = schema.hierarchy_dependencies
+            if item.parent and item.parent.name == 'Array':
+                for t in ['SchemaArrayInput', 'SchemaArrayInputGet']:
+                    if (nc := all_nc.get( (*schema.signature, t) )):
+                        for to_link in nc.outputs[item.name].links:
+                            if to_link.to_socket in to_name_filter:
+                                # hierarchy_reason='a'
+                                hierarchy = False
+                        for from_link in schema.inputs[item.identifier].links:
+                            if from_link.from_socket in from_name_filter:
+                                hierarchy = False
+                                # hierarchy_reason='b'
+                        if from_link.from_node not in c:
+                            if hierarchy:
+                                hc.append(from_link.from_node)
+                            c.append(from_link.from_node)
+            if item.parent and item.parent.name == 'Constant':
+                if nc := all_nc.get((*schema.signature, 'SchemaConstInput')):
+                    for to_link in nc.outputs[item.name].links:
+                        if to_link.to_socket in to_name_filter:
+                            # hierarchy_reason='c'
+                            hierarchy = False
+                    for from_link in schema.inputs[item.identifier].links:
+                        if from_link.from_socket in from_name_filter:
+                            # hierarchy_reason='d'
+                            hierarchy = False
+                    if from_link.from_node not in c:
+                        if hierarchy:
+                            hc.append(from_link.from_node)
+                        c.append(from_link.from_node)
+            if item.parent and item.parent.name == 'Connection':
+                if nc := all_nc.get((*schema.signature, 'SchemaIncomingConnection')):
+                    for to_link in nc.outputs[item.name].links:
+                        if to_link.to_socket in to_name_filter:
+                            # hierarchy_reason='e'
+                            hierarchy = False
+                    for from_link in schema.inputs[item.identifier].links:
+                        if from_link.from_socket in from_name_filter:
+                            # hierarchy_reason='f'
+                            hierarchy = False
+                    if from_link.from_node not in c:
+                        if hierarchy:
+                            hc.append(from_link.from_node)
+                        c.append(from_link.from_node)
+        # prPurple(item.in_out)
+        # if hierarchy:
+        #     prOrange(item.name)
+        # else:
+        #     prWhite(item.name)
+        #     print(hierarchy_reason)
+
+        # else:
+        #     c = schema.connections
+        #     hc = schema.hierarchy_connections
+        #     if item.parent and item.parent.name == 'Array':
+        #         if nc := all_nc.get((*schema.signature, 'SchemaArrayOutput')):
+        #             for from_link in nc.inputs[item.name].links:
+        #                 if from_link.from_socket in from_name_filter:
+        #                     hierarchy = False
+        #             for to_link in schema.outputs[item.identifier].links:
+        #                 if to_link.to_socket in to_name_filter:
+        #                     hierarchy = False
+        #     if item.parent and item.parent.name == 'Constant':
+        #         if nc := all_nc.get((*schema.signature, 'SchemaConstOutput')):
+        #             for from_link in nc.inputs[item.name].links:
+        #                 if from_link.from_socket in from_name_filter:
+        #                     hierarchy = False
+        #             for to_link in schema.outputs[item.identifier].links:
+        #                 if to_link.to_socket in to_name_filter:
+        #                     hierarchy = False
+        #     if item.parent and item.parent.name == 'Connection':
+        #         if nc := all_nc.get((*schema.signature, 'SchemaOutgoingConnection')):
+        #             for from_link in nc.inputs[item.name].links:
+        #                 if from_link.from_socket in from_name_filter:
+        #                     hierarchy = False
+        #             for to_link in schema.outputs[item.identifier].links:
+        #                 if to_link.to_socket in to_name_filter:
+        #                     hierarchy = False
+    # for nc in all_input_nodes:
+    #     for output in nc.outputs.values():
+    #         for l in output.links:
+    #             if l.to_socket in to_name_filter:
+    #                 print("not hierarchy", l.to_socket)
+    #             else:
+    #                 print("hierarchy", l.to_socket)
+    # for inp in schema.inputs.values():
+    #     for l in inp.links:
+    #         if l.from_socket in from_name_filter:
+    #             print("not hierarchy", l.from_socket)
+    #         else:
+    #             print("hierarchy", l.from_socket)
     
-def get_node_prototype(sig, base_tree):
-    return tree_from_nc(sig, base_tree).nodes.get( sig[-1] )
+    # we need to get dependencies and connections
+    # but we can use the same method to do each
+
 
-      
+    # prPurple (schema.connections)
+    # # print (schema.hierarchy_connections)
+    # prPurple (schema.dependencies)
+    # prPurple (schema.hierarchy_dependencies)
+    # #
+
+
+def check_and_add_root(n, roots, include_non_hierarchy=False):
+    # if not (hasattr(n, 'inputs')) or ( len(n.inputs) == 0):
+    #     roots.append(n)
+    # elif (hasattr(n, 'inputs')):
+    #     for inp in n.inputs.values():
+    #         if inp.is_linked: return
+    if include_non_hierarchy == True and len(n.dependencies) > 0:
+        return 
+    elif len(n.hierarchy_dependencies) > 0:
+        return
+    roots.append(n)
+
+def get_link_in_out(link):
+    from .base_definitions import replace_types
+    from_name, to_name = link.from_socket.node.name, link.to_socket.node.name
+    # catch special bl_idnames and bunch the connections up
+    if link.from_socket.node.bl_idname in replace_types:
+        from_name = link.from_socket.node.bl_idname 
+    if link.to_socket.node.bl_idname in replace_types:
+        to_name = link.to_socket.node.bl_idname
+    return from_name, to_name
+
+def link_node_containers(tree_path_names, link, local_nc, from_suffix='', to_suffix=''):
+    dummy_types = ["DUMMY", "DUMMY_SCHEMA"]
+    from_name, to_name = get_link_in_out(link)
+    nc_from = local_nc.get( (*tree_path_names, from_name+from_suffix) )
+    nc_to = local_nc.get( (*tree_path_names, to_name+to_suffix))
+    if (nc_from and nc_to):
+        from_s, to_s = link.from_socket.name, link.to_socket.name
+        if nc_to.node_type in dummy_types: to_s = link.to_socket.identifier
+        if nc_from.node_type in dummy_types: from_s = link.from_socket.identifier
+        try:
+            connection = nc_from.outputs[from_s].connect(node=nc_to, socket=to_s)
+            if connection is None:
+                prWhite(f"Already connected: {from_name}:{from_s}->{to_name}:{to_s}")
+            return connection
+        except KeyError as e:
+            prRed(f"{nc_from}:{from_s} or {nc_to}:{to_s} missing; review the connections printed below:")
+            print (nc_from.outputs.keys())
+            print (nc_to.inputs.keys())
+            raise e
+    else:
+        prRed(nc_from, nc_to, (*tree_path_names, from_name+from_suffix), (*tree_path_names, to_name+to_suffix))
+        # for nc in local_nc.values():
+        #     prOrange(nc)
+        raise RuntimeError(wrapRed("Link not connected: %s -> %s in tree %s" % (from_name, to_name, tree_path_names[-1])))
+    
+def get_all_dependencies(nc):
+    """ Given a NC, find all dependencies for the NC as a dict of nc.signature:nc"""
+    nodes = []
+    can_descend = True
+    check_nodes = [nc]
+    while (len(check_nodes) > 0): # this seems innefficient, why 2 loops?
+        new_nodes = []
+        while (len(check_nodes) > 0):
+            node = check_nodes.pop()
+            connected_nodes = node.hierarchy_dependencies.copy()
+            for new_node in connected_nodes:
+                if new_node in nodes: continue 
+                new_nodes.append(new_node)
+                nodes.append(new_node)
+        check_nodes = new_nodes
+    return nodes
             
 ##################################################################################################
 # misc
 ##################################################################################################
 
-# This will not work with float properties. Use them directly.
-# this is an extremely idiotic way to do this
-# it's also slow!
-# TODO fix this
-#using isinstance is the most lizard-brained way to do this, utter idiocy.
+
+# this function is used a lot, so it is a good target for optimization.
 def to_mathutils_value(socket):
-    if (hasattr(socket, "default_value")):
-        val = socket.default_value
+    if hasattr(socket, "default_value"):
         from mathutils import Matrix, Euler, Quaternion, Vector
-        from bpy.types import (NodeSocketVector, NodeSocketVectorAcceleration,
-                              NodeSocketVectorDirection, NodeSocketVectorEuler,
-                              NodeSocketVectorTranslation, NodeSocketVectorVelocity,
-                              NodeSocketVectorXYZ,)
-        from . import socket_definitions
-        if ((isinstance(socket, NodeSocketVector)) or
-            (isinstance(socket, NodeSocketVectorAcceleration)) or
-            (isinstance(socket, NodeSocketVectorDirection)) or
-            (isinstance(socket, NodeSocketVectorTranslation)) or
-            (isinstance(socket, NodeSocketVectorXYZ)) or
-            (isinstance(socket, NodeSocketVectorVelocity)) or
-            (isinstance(socket, socket_definitions.VectorSocket)) or
-            (isinstance(socket, socket_definitions.VectorEulerSocket)) or
-            (isinstance(socket, socket_definitions.VectorTranslationSocket)) or
-            (isinstance(socket, socket_definitions.VectorScaleSocket)) or
-            (isinstance(socket, socket_definitions.ParameterVectorSocket))):
-            return (Vector(( val[0], val[1], val[2], )))
-        if (isinstance(socket, NodeSocketVectorEuler)):
-            return (Euler(( val[0], val[1], val[2])), 'XYZ',) #TODO make choice
-        if (isinstance(socket, socket_definitions.MatrixSocket)):
-            # return val #Blender makes it a Matrix for me <3
-            # nevermind... BLENDER HAS BETRAYED ME
+        val = socket.default_value
+        # if socket.bl_idname in [
+        #     'NodeSocketVector',
+        #     'NodeSocketVectorAcceleration',
+        #     'NodeSocketVectorDirection',
+        #     'NodeSocketVectorTranslation',
+        #     'NodeSocketVectorXYZ',
+        #     'NodeSocketVectorVelocity',
+        #     'VectorSocket',
+        #     'VectorEulerSocket',
+        #     'VectorTranslationSocket',
+        #     'VectorScaleSocket',
+        #     'ParameterVectorSocket',]:
+        # # if "Vector" in socket.bl_idname:
+        #     return (Vector(( val[0], val[1], val[2], )))
+        # if socket.bl_idname in ['NodeSocketVectorEuler']:
+        #     return (Euler(( val[0], val[1], val[2])), 'XYZ',) #TODO make choice
+        if socket.bl_idname in ['MatrixSocket']:
             return socket.TellValue()
-        if (isinstance(socket,socket_definitions.QuaternionSocket)):
-            return (Quaternion( (val[0], val[1], val[2], val[3],)) )
-        if (isinstance(socket,socket_definitions.QuaternionSocketAA)):
-            return (Quaternion( (val[1], val[2], val[3],), val[0], ) )
-        if ((isinstance(socket, socket_definitions.FloatSocket)) or
-            (isinstance(socket, socket_definitions.ParameterIntSocket)) or
-            (isinstance(socket, socket_definitions.ParameterFloatSocket))):
+        # elif socket.bl_idname in ['QuaternionSocket']:
+        #     return (Quaternion( (val[0], val[1], val[2], val[3],)) )
+        # elif socket.bl_idname in ['QuaternionSocketAA']:
+        #     return (Quaternion( (val[1], val[2], val[3],), val[0], ) )
+        # elif socket.bl_idname in ['BooleanThreeTupleSocket']:
+        #     return (val[0], val[1], val[2]) 
+        else:
             return val
-        if (isinstance(socket, socket_definitions.BooleanThreeTupleSocket)):
-            return (val[0], val[1], val[2]) # we'll send a tuple out
-        # if ((isinstance(socket, socket_definitions.LayerMaskSocket)) or
-        #     (isinstance(socket, socket_definitions.LayerMaskInputSocket))):
-        #     return tuple(val) # should werk
     else:
         return None
 
+
+def all_trees_in_tree(base_tree, selected=False):
+    """ Recursively finds all trees referenced in a given base-tree."""
+    # note that this is recursive but not by tail-end recursion
+    # a while-loop is a better way to do recursion in Python.
+    trees = [base_tree]
+    can_descend = True
+    check_trees = [base_tree]
+    while (len(check_trees) > 0): # this seems innefficient, why 2 loops?
+        new_trees = []
+        while (len(check_trees) > 0):
+            tree = check_trees.pop()
+            for node in tree.nodes:
+                if selected == True and node.select == False:
+                    continue
+                if new_tree := getattr(node, "node_tree", None):
+                    if new_tree in trees: continue 
+                    new_trees.append(new_tree)
+                    trees.append(new_tree)
+        check_trees = new_trees
+    return trees
+
+# this is a destructive operation, not a pure function or whatever. That isn't good but I don't care.
+def SugiyamaGraph(tree, iterations):
+        from grandalf.graphs import Vertex, Edge, Graph, graph_core
+        class defaultview(object):
+            w,h = 1,1
+            xz = (0,0)
+        
+        no_links = set()
+        verts = {}
+        for n in tree.nodes:
+            has_links=False
+            for inp in n.inputs:
+                if inp.is_linked:
+                    has_links=True
+                    break
+            else:
+                no_links.add(n.name)
+            for out in n.outputs:
+                if out.is_linked:
+                    has_links=True
+                    break
+            else:
+                try:
+                    no_links.remove(n.name)
+                except KeyError:
+                    pass
+            if not has_links:
+                continue
+                
+            v = Vertex(n.name)
+            v.view = defaultview()
+            v.view.xy = n.location
+            v.view.h = n.height*2.5
+            v.view.w = n.width*2.2
+            verts[n.name] = v
+            
+        edges = []
+        for link in tree.links:
+            weight = 1 # maybe this is useful
+            edges.append(Edge(verts[link.from_node.name], verts[link.to_node.name], weight) )
+        graph = Graph(verts.values(), edges)
+
+        from grandalf.layouts import SugiyamaLayout
+        sug = SugiyamaLayout(graph.C[0]) # no idea what .C[0] is
+        roots=[]
+        for node in tree.nodes:
+            
+            has_links=False
+            for inp in node.inputs:
+                if inp.is_linked:
+                    has_links=True
+                    break
+            for out in node.outputs:
+                if out.is_linked:
+                    has_links=True
+                    break
+            if not has_links:
+                continue
+                
+            if len(node.inputs)==0:
+                roots.append(verts[node.name])
+            else:
+                for inp in node.inputs:
+                    if inp.is_linked==True:
+                        break
+                else:
+                    roots.append(verts[node.name])
+        
+        sug.init_all(roots=roots,)
+        sug.draw(iterations)
+        for v in graph.C[0].sV:
+            for n in tree.nodes:
+                if n.name == v.data:
+                    n.location.x = v.view.xy[1]
+                    n.location.y = v.view.xy[0]
+        
+        # now we can take all the input nodes and try to put them in a sensible place
+
+        for n_name in no_links:
+            n = tree.nodes.get(n_name)
+            next_n = None
+            next_node = None
+            for output in n.outputs:
+                if output.is_linked == True:
+                    next_node = output.links[0].to_node
+                    break
+            # let's see if the next node
+            if next_node:
+                # need to find the other node in the same layer...
+                other_node = None
+                for s_input in next_node.inputs:
+                    if s_input.is_linked:
+                        other_node = s_input.links[0].from_node
+                        if other_node is n:
+                            continue
+                        else:
+                            break
+                if other_node:
+                    n.location = other_node.location
+                    n.location.y -= other_node.height*2
+                else: # we'll just position it next to the next node
+                    n.location = next_node.location
+                    n.location.x -= next_node.width*1.5
+        
+
+
+
+##################################################################################################
+# stuff I should probably refactor!!
+##################################################################################################
+
+# what in the cuss is this horrible abomination??
+def class_for_mantis_prototype_node(prototype_node):
+    """ This is a class which returns a class to instantiate for
+        the given prototype node."""
+    #from .node_container_classes import TellClasses
+    from . import xForm_containers, link_containers, misc_containers, primitives_containers, deformer_containers, math_containers, schema_containers
+    classes = {}
+    for module in [xForm_containers, link_containers, misc_containers, primitives_containers, deformer_containers, math_containers, schema_containers]:
+        for cls in module.TellClasses():
+            classes[cls.__name__] = cls
+    # I could probably do a string.replace() here
+    # But I actually think this is a bad idea since I might not
+    #  want to use this name convention in the future
+    #  this is easy enough for now, may refactor.
+    #
+    # kek, turns out it was completely friggin' inconsistent already
+    if prototype_node.bl_idname == 'xFormRootNode':
+        return classes["xFormRoot"]
+    elif prototype_node.bl_idname == 'xFormArmatureNode':
+        return classes["xFormArmature"]
+    elif prototype_node.bl_idname == 'xFormBoneNode':
+        return classes["xFormBone"]
+    elif prototype_node.bl_idname == 'xFormGeometryObject':
+        return classes["xFormGeometryObject"]
+    elif prototype_node.bl_idname == 'linkInherit':
+        return classes["LinkInherit"]
+    elif prototype_node.bl_idname == 'InputFloatNode':
+        return classes["InputFloat"]
+    elif prototype_node.bl_idname == 'InputVectorNode':
+        return classes["InputVector"]
+    elif prototype_node.bl_idname == 'InputBooleanNode':
+        return classes["InputBoolean"]
+    elif prototype_node.bl_idname == 'InputBooleanThreeTupleNode':
+        return classes["InputBooleanThreeTuple"]
+    elif prototype_node.bl_idname == 'InputRotationOrderNode':
+        return classes["InputRotationOrder"]
+    elif prototype_node.bl_idname == 'InputTransformSpaceNode':
+        return classes["InputTransformSpace"]
+    elif prototype_node.bl_idname == 'InputStringNode':
+        return classes["InputString"]
+    elif prototype_node.bl_idname == 'InputQuaternionNode':
+        return classes["InputQuaternion"]
+    elif prototype_node.bl_idname == 'InputQuaternionNodeAA':
+        return classes["InputQuaternionAA"]
+    elif prototype_node.bl_idname == 'InputMatrixNode':
+        return classes["InputMatrix"]
+    elif prototype_node.bl_idname == 'MetaRigMatrixNode':
+        return classes["InputMatrix"]
+    elif prototype_node.bl_idname == 'InputLayerMaskNode':
+        return classes["InputLayerMask"]
+    elif prototype_node.bl_idname == 'GeometryCirclePrimitive':
+        return classes["CirclePrimitive"]
+        
+    # every node before this point is not guarenteed to follow the pattern
+    # but every node not checked above does follow the pattern.
+    
+    try:
+        return classes[ prototype_node.bl_idname ]
+    except KeyError:
+        # prGreen(prototype_node.bl_idname)
+        # prWhite(classes.keys())
+        pass
+    
+    if prototype_node.bl_idname in [ 
+                                    "NodeReroute",
+                                    "NodeGroupInput",
+                                    "NodeGroupOutput",
+                                    "MantisNodeGroup",
+                                    "NodeFrame",
+                                    "MantisSchemaGroup",
+                                   ]:
+           return None
+    
+    prRed(prototype_node.bl_idname)
+    raise RuntimeError(wrapOrange("Failed to create node container for: ")+wrapRed("%s" % prototype_node.bl_idname))
+    return None
+
+
+# This is really, really stupid HACK
+def gen_nc_input_for_data(socket):
+    # Class List #TODO deduplicate
+    from . import xForm_containers, link_containers, misc_containers, primitives_containers, deformer_containers, math_containers, schema_containers
+    classes = {}
+    for module in [xForm_containers, link_containers, misc_containers, primitives_containers, deformer_containers, math_containers, schema_containers]:
+        for cls in module.TellClasses():
+            classes[cls.__name__] = cls
+    #
+    socket_class_map = {
+                        "MatrixSocket"                         : classes["InputMatrix"],
+                        "xFormSocket"                          : None,
+                        "RelationshipSocket"                   : classes["xFormRoot"], # world in
+                        "DeformerSocket"                       : classes["xFormRoot"], # world in
+                        "GeometrySocket"                       : classes["InputExistingGeometryData"],
+                        "EnableSocket"                         : classes["InputBoolean"],
+                        "HideSocket"                           : classes["InputBoolean"],
+                        #
+                        "DriverSocket"                         : None,
+                        "DriverVariableSocket"                 : None, 
+                        "FCurveSocket"                         : None, 
+                        "KeyframeSocket"                       : None,
+                        # "LayerMaskInputSocket"               : classes["InputLayerMask"],
+                        # "LayerMaskSocket"                    : classes["InputLayerMask"],
+                        "BoneCollectionSocket"                 : classes["InputString"],
+                        "BoneCollectionInputSocket"            : classes["InputString"],
+                        #
+                        "xFormParameterSocket"                 : None,
+                        "ParameterBoolSocket"                  : classes["InputBoolean"],
+                        "ParameterIntSocket"                   : classes["InputFloat"],  #TODO: make an Int node for this
+                        "ParameterFloatSocket"                 : classes["InputFloat"],
+                        "ParameterVectorSocket"                : classes["InputVector"],
+                        "ParameterStringSocket"                : classes["InputString"],
+                        #
+                        "TransformSpaceSocket"                 : classes["InputTransformSpace"],
+                        "BooleanSocket"                        : classes["InputBoolean"],
+                        "BooleanThreeTupleSocket"              : classes["InputBooleanThreeTuple"],
+                        "RotationOrderSocket"                  : classes["InputRotationOrder"],
+                        "QuaternionSocket"                     : classes["InputQuaternion"],
+                        "QuaternionSocketAA"                   : classes["InputQuaternionAA"],
+                        "IntSocket"                            : classes["InputFloat"],
+                        "StringSocket"                         : classes["InputString"],
+                        #
+                        "BoolUpdateParentNode"                 : classes["InputBoolean"],
+                        "IKChainLengthSocket"                  : classes["InputFloat"],
+                        "EnumInheritScale"                     : classes["InputString"],
+                        "EnumRotationMix"                      : classes["InputString"],
+                        "EnumRotationMixCopyTransforms"        : classes["InputString"],
+                        "EnumMaintainVolumeStretchTo"          : classes["InputString"],
+                        "EnumRotationStretchTo"                : classes["InputString"],
+                        "EnumTrackAxis"                        : classes["InputString"],
+                        "EnumUpAxis"                           : classes["InputString"],
+                        "EnumLockAxis"                         : classes["InputString"],
+                        "EnumLimitMode"                        : classes["InputString"],
+                        "EnumYScaleMode"                       : classes["InputString"],
+                        "EnumXZScaleMode"                      : classes["InputString"],
+                        "EnumCurveSocket"                      : classes["InputString"],
+                        # Deformers
+                        "EnumSkinning"                         : classes["InputString"],
+                        #
+                        "FloatSocket"                          : classes["InputFloat"],
+                        "FloatFactorSocket"                    : classes["InputFloat"],
+                        "FloatPositiveSocket"                  : classes["InputFloat"],
+                        "FloatAngleSocket"                     : classes["InputFloat"],
+                        "VectorSocket"                         : classes["InputVector"],
+                        "VectorEulerSocket"                    : classes["InputVector"],
+                        "VectorTranslationSocket"              : classes["InputVector"],
+                        "VectorScaleSocket"                    : classes["InputVector"],
+                        # Drivers             
+                        "EnumDriverVariableType"               : classes["InputString"],
+                        "EnumDriverVariableEvaluationSpace"    : classes["InputString"],
+                        "EnumDriverRotationMode"               : classes["InputString"],
+                        "EnumDriverType"                       : classes["InputString"],
+                        "EnumKeyframeInterpolationTypeSocket"  : classes["InputString"],
+                        "EnumKeyframeBezierHandleTypeSocket"   : classes["InputString"],
+                        # Math
+                        "MathFloatOperation"                   : classes["InputString"],
+                        "MathVectorOperation"                  : classes["InputString"],
+                        "MatrixTransformOperation"             : classes["InputString"],
+                        # Schema
+                        "WildcardSocket"                       : None,
+                       }
+    return socket_class_map.get(socket.bl_idname, None)
+
+####################################
+# CURVE STUFF
+####################################
+
+def rotate(l, n):
+    if ( not ( isinstance(n, int) ) ): #print an error if n is not an int:
+        raise TypeError("List slice must be an int, not float.")
+    return l[n:] + l[:n]
+#from stack exchange, thanks YXD
+
+# this stuff could be branchless but I don't use it much TODO
+def cap(val, maxValue):
+    if (val > maxValue):
+        return maxValue
+    return val
+
+def capMin(val, minValue):
+    if (val < minValue):
+        return minValue
+    return val
+
+# def wrap(val, min=0, max=1):
+#     raise NotImplementedError
+
+#wtf this doesn't do anything even remotely similar to wrap, or useful in
+# HACK BAD FIXME UNBREAK ME BAD
+# I don't understand what this function does but I am using it in multiple places?
+def wrap(val, maxValue, minValue = None):
+    if (val > maxValue):
+        return (-1 * ((maxValue - val) + 1))
+    if ((minValue) and (val < minValue)):
+        return (val + maxValue)
+    return val
+    #TODO clean this up
+
+
+def layerMaskCompare(mask_a, mask_b):
+    compare = 0
+    for a, b in zip(mask_a, mask_b):
+        if (a != b):
+            compare+=1
+    if (compare == 0):
+        return True
+    return False
+
+def lerpVal(a, b, fac = 0.5):
+    return a + ( (b-a) * fac)
+
+def RibbonMeshEdgeLengths(m, ribbon):
+    tE = ribbon[0]; bE = ribbon[1]; c = ribbon[2]
+    lengths = []
+    for i in range( len( tE ) ): #tE and bE are same length
+        if (c == True):
+            v1NextInd = tE[wrap((i+1), len(tE) - 1)]
+        else:
+            v1NextInd = tE[cap((i+1) , len(tE) - 1 )]
+        v1 = m.vertices[tE[i]]; v1Next = m.vertices[v1NextInd]
+        if (c == True):
+            v2NextInd = bE[wrap((i+1), len(bE) - 1)]
+        else:
+            v2NextInd = bE[cap((i+1) , len(bE) - 1 )]
+        v2 = m.vertices[bE[i]]; v2Next = m.vertices[v2NextInd]
+        
+        v = v1.co.lerp(v2.co, 0.5); vNext = v1Next.co.lerp(v2Next.co, 0.5)
+        # get the center, edges may not be straight so total length 
+        #  of one edge may be more than the ribbon center's length
+        lengths.append(( v - vNext ).length)
+    return lengths
+
+def EnsureCurveIsRibbon(crv, defaultRadius = 0.1):
+    crvRadius = 0
+    if (crv.data.bevel_depth == 0):
+        crvRadius = crv.data.extrude
+    else: #Set ribbon from bevel depth
+        crvRadius = crv.data.bevel_depth
+        crv.data.bevel_depth = 0
+        crv.data.extrude = crvRadius
+    if (crvRadius == 0):
+        crv.data.extrude = defaultRadius
+
+def SetRibbonData(m, ribbon):
+    #maybe this could be incorporated into the DetectWireEdges function?
+    #maybe I can check for closed poly curves here? under what other circumstance
+    # will I find the ends of the wire have identical coordinates?
+    ribbonData = []
+    tE = ribbon[0].copy(); bE = ribbon[1].copy()# circle = ribbon[2]
+    #
+    lengths = RibbonMeshEdgeLengths(m, ribbon)
+    lengths.append(0)
+    totalLength = sum(lengths)
+    # m.calc_normals() #calculate normals
+    # it appears this has been removed.
+    for i, (t, b) in enumerate(zip(tE, bE)):
+        ind = wrap( (i + 1), len(tE) - 1 )
+        tNext = tE[ind]; bNext = bE[ind]
+        ribbonData.append(  ( (t,b), (tNext, bNext), lengths[i] ) )
+        #if this is a circle, the last v in vertData has a length, otherwise 0
+    return ribbonData, totalLength
+
+
+def mesh_from_curve(crv, context,):
+    """Utility function for converting a mesh to a curve
+       which will return the correct mesh even with modifiers"""
+    import bpy
+    if (len(crv.modifiers) > 0):
+        do_unlink = False
+        if (not context.scene.collection.all_objects.get(crv.name)):
+            context.collection.objects.link(crv) # i guess this forces the dg to update it?
+            do_unlink = True
+        dg = context.view_layer.depsgraph
+        # just gonna modify it for now lol
+        EnsureCurveIsRibbon(crv)
+        # try:
+        dg.update()
+        mOb = crv.evaluated_get(dg)
+        m = bpy.data.meshes.new_from_object(mOb)
+        m.name=crv.data.name+'_mesh'
+        if (do_unlink):
+            context.collection.objects.unlink(crv)
+        return m
+        # except: #dg is None?? # FIX THIS BUG BUG BUG
+        #     print ("Warning: could not apply modifiers on curve")
+        #     return bpy.data.meshes.new_from_object(crv)
+    else: # (ಥ﹏ಥ) why can't I just use this !
+        # for now I will just do it like this
+        EnsureCurveIsRibbon(crv)
+        return bpy.data.meshes.new_from_object(crv)
+
+
+# def DataFromRibbon(obCrv, factorsList, context, fReport=None,):
+#     # BUG
+#     # no reasonable results if input is not  a ribbon
+#     import time
+#     start = time.time()
+#     """Returns a point from a u-value along a curve"""
+#     rM = MeshFromCurve(obCrv, context)
+#     ribbons = f_mesh.DetectRibbons(rM, fReport= fReport)
+#     for ribbon in ribbons:
+#         # could be improved, this will do a rotation for every ribbon
+#         # if even one is a circle
+#         if (ribbon[2]) == True:
+#             # could be a better implementation
+#             dupeCrv = obCrv.copy()
+#             dupeCrv.data = obCrv.data.copy()
+#             dupeCrv.data.extrude = 0
+#             dupeCrv.data.bevel_depth = 0 
+#             wM = MeshFromCurve(dupeCrv, context)
+#             wires = f_mesh.DetectWireEdges(wM)
+#             bpy.data.curves.remove(dupeCrv.data) #removes the object, too
+#             ribbonsNew = []
+#             for ribbon, wire in zip(ribbons, wires):
+#                 if (ribbon[2] == True): #if it's a circle
+#                     rNew = f_mesh.RotateRibbonToMatchWire(ribbon, rM, wire, wM)
+#                 else:
+#                     rNew = ribbon
+#                 ribbonsNew.append( rNew )
+#             ribbons = ribbonsNew
+#             break
+#     data = f_mesh.DataFromRibbon(rM, factorsList, obCrv.matrix_world, ribbons=ribbons, fReport=fReport)
+#     bpy.data.meshes.remove(rM)
+#     print ("time elapsed: ", time.time() - start)
+#     #expects data...
+#     # if ()
+
+
+#     return data
+
+def DetectRibbon(f, bm, skipMe):
+    fFirst = f.index
+    cont = True
+    circle = False
+    tEdge, bEdge = [],[]
+    while (cont == True):
+        skipMe.add(f.index)
+        tEdge.append (f.loops[0].vert.index) # top-left
+        bEdge.append (f.loops[3].vert.index) # bottom-left
+        nEdge = bm.edges.get([f.loops[1].vert, f.loops[2].vert])
+        nFaces = nEdge.link_faces
+        if (len(nFaces) == 1): 
+            cont = False
+        else:
+            for nFace in nFaces:
+                if (nFace != f):
+                    f = nFace
+                    break
+            if (f.index == fFirst):
+                cont = False
+                circle = True
+        if (cont == False): # we've reached the end, get the last two:
+            tEdge.append (f.loops[1].vert.index) # top-right
+            bEdge.append (f.loops[2].vert.index) # bottom-right
+            # this will create a loop for rings -- 
+            #  "the first shall be the last and the last shall be first"
+    return (tEdge,bEdge,circle)
+
+def DetectRibbons(m, fReport = None):
+    # Returns list of vertex indices belonging to ribbon mesh edges
+    # NOTE: this assumes a mesh object with only ribbon meshes
+    # ---DO NOT call this script with a mesh that isn't a ribbon!--- #
+    import bmesh
+    bm = bmesh.new()
+    bm.from_mesh(m)
+    mIslands, mIsland = [], []
+    skipMe = set()
+    bm.faces.ensure_lookup_table()
+    #first, get a list of mesh islands
+    for f in bm.faces:
+        if (f.index in skipMe):
+            continue #already done here
+        checkMe = [f]
+        while (len(checkMe) > 0):
+            facesFound = 0
+            for f in checkMe:
+                if (f.index in skipMe):
+                    continue #already done here
+                mIsland.append(f)
+                skipMe.add(f.index)
+                for e in f.edges:
+                    checkMe += e.link_faces
+            if (facesFound == 0):
+                #this is the last iteration
+                mIslands.append(mIsland)
+                checkMe, mIsland = [], []
+    ribbons = []
+    skipMe = set() # to store ends already checked
+    for mIsl in mIslands:
+        ribbon = None
+        first = float('inf')
+        for f in mIsl:
+            if (f.index in skipMe):
+                continue #already done here
+            if (f.index < first):
+                first = f.index
+            adjF = 0
+            for e in f.edges:
+                adjF+= (len(e.link_faces) - 1)
+                # every face other than this one is added to the list
+            if (adjF == 1):
+                ribbon = (DetectRibbon(f, bm, skipMe) )
+                break
+        if (ribbon == None):
+            ribbon = (DetectRibbon(bm.faces[first], bm, skipMe) )
+        ribbons.append(ribbon)
+    # print (ribbons)
+    return ribbons
+
+def data_from_ribbon_mesh(m, factorsList, mat, ribbons = None, fReport = None):
+    #Note, factors list should be equal in length the the number of wires
+    #Now working for multiple wires, ugly tho
+    if (ribbons == None):
+        ribbons = DetectRibbons(m, fReport=fReport)
+        if (ribbons is None):
+            if (fReport):
+                fReport(type = {'ERROR'}, message="No ribbon to get data from.")
+            else:  
+                print ("No ribbon to get data from.")
+            return None
+    ret = []
+    for factors, ribbon in zip(factorsList, ribbons):
+        points  = []
+        widths  = []
+        normals = []
+        ribbonData, totalLength = SetRibbonData(m, ribbon)
+
+        for fac in factors:
+            if (fac == 0):
+                data = ribbonData[0]
+                curFac = 0
+            elif (fac == 1):
+                data = ribbonData[-1]
+                curFac = 0
+            else:
+                targetLength = totalLength * fac
+                data = ribbonData[0]
+                curLength = 0
+                for ( (t, b), (tNext, bNext), length,) in ribbonData:
+                    if (curLength >= targetLength):
+                        break
+                    curLength += length
+                    data = ( (t, b), (tNext, bNext), length,)
+                targetLengthAtEdge = (curLength - targetLength)
+                if (targetLength == 0):
+                    curFac = 0
+                elif (targetLength == totalLength):
+                    curFac = 1
+                else:
+                    try:
+                        curFac = 1 - (targetLengthAtEdge/ data[2]) #length
+                    except ZeroDivisionError:
+                        curFac = 0
+                        if (fReport):
+                            fReport(type = {'WARNING'}, message="Division by Zero.")
+                        else:  
+                            prRed ("Division by Zero Error in evaluating data from curve.")
+            t1 = m.vertices[data[0][0]]; b1 = m.vertices[data[0][1]]
+            t2 = m.vertices[data[1][0]]; b2 = m.vertices[data[1][1]]
+            #location
+            loc1 = (t1.co).lerp(b1.co, 0.5)
+            loc2 = (t2.co).lerp(b2.co, 0.5)
+            #width
+            w1 = (t1.co - b1.co).length/2
+            w2 = (t2.co - b2.co).length/2 #radius, not diameter
+            #normal
+            n1 = (t1.normal).slerp(b1.normal, 0.5)
+            n2 = (t1.normal).slerp(b2.normal, 0.5)
+            if ((data[0][0] > data[1][0]) and (ribbon[2] == False)):
+                curFac = 0
+                #don't interpolate if at the end of a ribbon that isn't circular
+            if ( 0 < curFac < 1):
+                outPoint = loc1.lerp(loc2, curFac)
+                outNorm  = n1.lerp(n2, curFac)
+                outWidth = w1 + ( (w2-w1) * curFac)
+            elif (curFac <= 0):
+                outPoint = loc1.copy()
+                outNorm = n1
+                outWidth = w1
+            elif (curFac >= 1):
+                outPoint = loc2.copy()
+                outNorm = n2
+                outWidth = w2
+            outPoint = mat @ outPoint
+            outNorm.normalize()
+            points.append ( outPoint.copy() ) #copy because this is an actual vertex location
+            widths.append ( outWidth )
+            normals.append( outNorm )
+        ret.append( (points, widths, normals) )
+    return ret # this is a list of tuples containing three lists

BIN
wheels/grandalf-0.8-py3-none-any.whl


+ 396 - 178
xForm_containers.py

@@ -42,8 +42,6 @@ def TellClasses():
     
     # def instantiate_blender_object(self):
         # pass
-# for whatever reason, the above isn't implemented yet in the node-tree
-# so I'm not implementing it here, either
 
 class xFormRoot:
     '''A node representing the root of the scene.'''
@@ -54,23 +52,14 @@ class xFormRoot:
         self.inputs = {}
         self.outputs = {"World Out":NodeSocket(name="World Out", node = self),}
         self.parameters = {}
-        self.links = {} # leave this empty for now!
         self.node_type = 'XFORM'
-    
-    def evaluate_input(self, input_name):
-        return "ROOT"
-    
-    def bExecute(self, bContext = None,):
-        pass
-        
-    def bFinalize(self, bContext = None,):
-        pass
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self,):
-        pass
+        self.hierarchy_connections = []
+        self.connections = []
+        self.hierarchy_dependencies = []
+        self.dependencies = []
+        self.prepared = True
+        self.executed = True
+
 
 class xFormArmature:
     '''A node representing an armature object'''
@@ -79,7 +68,6 @@ class xFormArmature:
     
     def __init__(self, signature, base_tree):
         self.base_tree=base_tree
-        self.executed = False
         self.signature = signature
         self.inputs = {
          "Name"           : NodeSocket(is_input = True, name = "Name", node = self),
@@ -101,21 +89,27 @@ class xFormArmature:
         self.inputs["Relationship"].set_traverse_target(self.outputs["xForm Out"])
         self.outputs["xForm Out"].set_traverse_target(self.inputs["Relationship"])
         self.node_type = 'XFORM'
-    
-        
-    def evaluate_input(self, input_name):
-        return evaluate_input(self, input_name)
+        self.hierarchy_connections = []
+        self.connections = []
+        self.hierarchy_dependencies = []
+        self.dependencies = []
+        self.prepared = True
+        self.executed = False
+
     
     def bExecute(self, bContext = None,):
-        from .utilities import get_node_prototype
+        # from .utilities import get_node_prototype
         
         import bpy
         if (not isinstance(bContext, bpy.types.Context)):
             raise RuntimeError("Incorrect context")
 
         name = self.evaluate_input("Name")
-        matrix = self.evaluate_input('Matrix')
+        if not ( matrix := self.evaluate_input('Matrix')):
+            raise RuntimeError(wrapRed(f"No matrix found for Armature {self}"))
+        self.parameters['Matrix'] = matrix
 
+        reset_transforms = False
 
         #check if an object by the name exists
         if (name) and (ob := bpy.data.objects.get(name)):
@@ -127,11 +121,12 @@ class xFormArmature:
                 #  if we create them again the pose bones will be reused 
                 while (pb.constraints):
                     pb.constraints.remove(pb.constraints[-1])
-                pb.location = (0,0,0)
-                pb.rotation_euler = (0,0,0)
-                pb.rotation_quaternion = (1.0,0,0,0)
-                pb.rotation_axis_angle = (0,0,1.0,0)
-                pb.scale = (1.0,1.0,1.0)
+                if reset_transforms:
+                    pb.location = (0,0,0)
+                    pb.rotation_euler = (0,0,0)
+                    pb.rotation_quaternion = (1.0,0,0,0)
+                    pb.rotation_axis_angle = (0,0,1.0,0)
+                    pb.scale = (1.0,1.0,1.0)
             # feels ugly and bad, whatever
             collections = []
             for bc in ob.data.collections:
@@ -149,7 +144,7 @@ class xFormArmature:
                 raise RuntimeError("Could not create xForm object", name)
             
         self.bObject = ob.name
-        ob.matrix_world = matrix
+        ob.matrix_world = matrix.copy()
         
         # # first, get the parent object
         # parent_node = get_parent(self)
@@ -213,18 +208,74 @@ class xFormArmature:
     def bGetObject(self, mode = ''):
         import bpy; return bpy.data.objects[self.bObject]
 
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self,):
-        fill_parameters(self)
-
+bone_inputs= [
+         "Name",
+         "Rotation Order",
+         "Matrix",
+         "Relationship",
+         # IK settings
+         "IK Stretch",
+         "Lock IK",
+         "IK Stiffness",
+         "Limit IK",
+         "X Min",
+         "X Max",
+         "Y Min",
+         "Y Max",
+         "Z Min",
+         "Z Max",
+         # Visual stuff
+         "Bone Collection",
+         "Hide",
+         "Custom Object",
+         "Custom Object xForm Override",
+         "Custom Object Scale to Bone Length",
+         "Custom Object Wireframe",
+         "Custom Object Scale",
+         "Custom Object Translation",
+         "Custom Object Rotation",
+         # Deform Stuff
+         "Deform",
+         "Envelope Distance",
+         "Envelope Weight",
+         "Envelope Multiply",
+         "Envelope Head Radius",
+         "Envelope Tail Radius",
+         # BBone stuff:
+         "BBone Segments",
+         "BBone X Size",
+         "BBone Z Size",
+         "BBone HQ Deformation",
+         "BBone X Curve-In",
+         "BBone Z Curve-In",
+         "BBone X Curve-Out",
+         "BBone Z Curve-Out",
+         "BBone Roll-In",
+         "BBone Roll-Out",
+         "BBone Inherit End Roll",
+         "BBone Scale-In",
+         "BBone Scale-Out",
+         "BBone Ease-In",
+         "BBone Ease-Out",
+         "BBone Easing",
+         "BBone Start Handle Type",
+         "BBone Custom Start Handle",
+         "BBone Start Handle Scale",
+         "BBone Start Handle Ease",
+         "BBone End Handle Type",
+         "BBone Custom End Handle",
+         "BBone End Handle Scale",
+         "BBone End Handle Ease",
+         # locks
+         "Lock Location",
+         "Lock Rotation",
+         "Lock Scale",
+]
 class xFormBone:
     '''A node representing a bone in an armature'''
     # DO: make a way to identify which armature this belongs to
     def __init__(self, signature, base_tree):
         self.base_tree=base_tree
-        self.executed = False
         self.signature = signature
         self.inputs = {
          "Name"           : NodeSocket(is_input = True, name = "Name", node = self,),
@@ -259,8 +310,37 @@ class xFormBone:
          "Envelope Multiply"    : NodeSocket(is_input = True, name = "Envelope Multiply", node = self,),
          "Envelope Head Radius" : NodeSocket(is_input = True, name = "Envelope Head Radius", node = self,),
          "Envelope Tail Radius" : NodeSocket(is_input = True, name = "Envelope Tail Radius", node = self,),
+         # BBone stuff:
+         "BBone Segments" : NodeSocket(is_input = True, name = "BBone Segments", node=self,),
+         "BBone X Size" : NodeSocket(is_input = True, name = "BBone X Size", node=self,),
+         "BBone Z Size" : NodeSocket(is_input = True, name = "BBone Z Size", node=self,),
+         "BBone HQ Deformation" : NodeSocket(is_input = True, name = "BBone HQ Deformation", node=self,),
+         "BBone X Curve-In" : NodeSocket(is_input = True, name = "BBone X Curve-In", node=self,),
+         "BBone Z Curve-In" : NodeSocket(is_input = True, name = "BBone Z Curve-In", node=self,),
+         "BBone X Curve-Out" : NodeSocket(is_input = True, name = "BBone X Curve-Out", node=self,),
+         "BBone Z Curve-Out" : NodeSocket(is_input = True, name = "BBone Z Curve-Out", node=self,),
+         "BBone Roll-In" : NodeSocket(is_input = True, name = "BBone Roll-In", node=self,),
+         "BBone Roll-Out" : NodeSocket(is_input = True, name = "BBone Roll-Out", node=self,),
+         "BBone Inherit End Roll" : NodeSocket(is_input = True, name = "BBone Inherit End Roll", node=self,),
+         "BBone Scale-In" : NodeSocket(is_input = True, name = "BBone Scale-In", node=self,),
+         "BBone Scale-Out" : NodeSocket(is_input = True, name = "BBone Scale-Out", node=self,),
+         "BBone Ease-In" : NodeSocket(is_input = True, name = "BBone Ease-In", node=self,),
+         "BBone Ease-Out" : NodeSocket(is_input = True, name = "BBone Ease-Out", node=self,),
+         "BBone Easing" : NodeSocket(is_input = True, name = "BBone Easing", node=self,),
+         "BBone Start Handle Type" : NodeSocket(is_input = True, name = "BBone Start Handle Type", node=self,),
+         "BBone Custom Start Handle" : NodeSocket(is_input = True, name = "BBone Custom Start Handle", node=self,),
+         "BBone Start Handle Scale" : NodeSocket(is_input = True, name = "BBone Start Handle Scale", node=self,),
+         "BBone Start Handle Ease" : NodeSocket(is_input = True, name = "BBone Start Handle Ease", node=self,),
+         "BBone End Handle Type" : NodeSocket(is_input = True, name = "BBone End Handle Type", node=self,),
+         "BBone Custom End Handle" : NodeSocket(is_input = True, name = "BBone Custom End Handle", node=self,),
+         "BBone End Handle Scale" : NodeSocket(is_input = True, name = "BBone End Handle Scale", node=self,),
+         "BBone End Handle Ease" : NodeSocket(is_input = True, name = "BBone End Handle Ease", node=self,),
+
+         # locks
+         "Lock Location"    : NodeSocket(is_input = True, name = "Lock Location", node = self,),
+         "Lock Rotation" : NodeSocket(is_input = True, name = "Lock Rotation", node = self,),
+         "Lock Scale" : NodeSocket(is_input = True, name = "Lock Scale", node = self,),
         }
-        
         self.outputs = {
          "xForm Out"       : NodeSocket(name = "xForm Out", node = self),
         }
@@ -296,22 +376,48 @@ class xFormBone:
          "Envelope Multiply"    : None,
          "Envelope Head Radius" : None,
          "Envelope Tail Radius" : None,
+         #
+         "BBone Segments"  : None,
+         "BBone X Size"  : None,
+         "BBone Z Size"  : None,
+         "BBone HQ Deformation"  : None,
+         "BBone X Curve-In"  : None,
+         "BBone Z Curve-In"  : None,
+         "BBone X Curve-Out"  : None,
+         "BBone Z Curve-Out"  : None,
+         "BBone Roll-In"  : None,
+         "BBone Roll-Out"  : None,
+         "BBone Inherit End Roll"  : None,
+         "BBone Scale-In"  : None,
+         "BBone Scale-Out"  : None,
+         "BBone Ease-In"  : None,
+         "BBone Ease-Out"  : None,
+         "BBone Easing"  : None,
+         "BBone Start Handle Type"  : None,
+         "BBone Custom Start Handle"  : None,
+         "BBone Start Handle Scale"  : None,
+         "BBone Start Handle Ease"  : None,
+         "BBone End Handle Type"  : None,
+         "BBone Custom End Handle"  : None,
+         "BBone End Handle Scale"  : None,
+         "BBone End Handle Ease"  : None,
+        #
+         "Lock Location"    : None,
+         "Lock Rotation" : None,
+         "Lock Scale" : None,
         }
         self.links = {} # leave this empty for now!
         # now set up the traverse target...
         self.inputs["Relationship"].set_traverse_target(self.outputs["xForm Out"])
         self.outputs["xForm Out"].set_traverse_target(self.inputs["Relationship"])
         self.node_type = 'XFORM'
-        setup_custom_props(self)
-             
-    def evaluate_input(self, input_name):
-        return evaluate_input(self, input_name)
-    
-    def __repr__(self):
-        return self.signature.__repr__()
-        
-    def fill_parameters(self):
-        fill_parameters(self)
+        self.hierarchy_connections = []
+        self.connections = []
+        self.hierarchy_dependencies = []
+        self.dependencies = []
+        self.prepared = True
+        self.executed = False
+        self.input_length = len(self.inputs) # HACK HACK HACK 
     
     def bGetParentArmature(self):
         finished = False
@@ -328,7 +434,12 @@ class xFormBone:
         from bpy.types import EditBone
         parent_nc = get_parent(self, type='LINK')
         # print (self, parent_nc.inputs['Parent'].from_node)
-        parent = parent_nc.inputs['Parent'].links[0].from_node.bGetObject(mode = 'EDIT')
+        parent=None
+        if parent_nc.inputs['Parent'].links[0].from_node.node_type == 'XFORM':
+            parent = parent_nc.inputs['Parent'].links[0].from_node.bGetObject(mode = 'EDIT')
+        else:
+            raise RuntimeError(wrapRed(f"Cannot set parent for node {self}"))
+
         if isinstance(parent, EditBone):
             eb.parent = parent
         
@@ -345,20 +456,24 @@ class xFormBone:
     def bExecute(self, bContext = None,): #possibly will need to pass context?
         import bpy
         from mathutils import Vector
+        if not (name := self.evaluate_input("Name")):
+            raise RuntimeError(wrapRed(f"Could not set name for bone in {self}"))
         if (not isinstance(bContext, bpy.types.Context)):
             raise RuntimeError("Incorrect context")
-        xF = self.bGetParentArmature()
+        if not (xF := self.bGetParentArmature()):
+            raise RuntimeError("Could not create edit bone: ", name, " from node:", self.signature, " Reason: No armature object to add bone to.")
+
         
-        name = self.evaluate_input("Name")
-        matrix = self.evaluate_input("Matrix")
+        if not ( matrix := self.evaluate_input('Matrix')):
+            # print(self.inputs['Matrix'].links[0].from_node.parameters)
+            raise RuntimeError(wrapRed(f"No matrix found for Bone {self}"))
         
+        self.parameters['Matrix'] = matrix
         length = matrix[3][3]
         
         if (xF):
             if (xF.mode != "EDIT"):
                 raise RuntimeError("Armature Object Not in Edit Mode, exiting...")
-        else:
-            raise RuntimeError("Could not create edit bone: ", name, " from node:", self.signature, " Reason: No armature object to add bone to.")
         #
         # Create the Object
         d = xF.data
@@ -374,7 +489,7 @@ class xFormBone:
         for collection_list in bone_collections:
             hierarchy = collection_list.split(">")
             col_parent = None
-            for i, sCol in enumerate(hierarchy):
+            for sCol in hierarchy:
                 if ( col := d.collections.get(sCol) ) is None:
                     col = d.collections.new(sCol)
                 col.parent = col_parent
@@ -382,11 +497,16 @@ class xFormBone:
             col.assign(eb)
         
         if (eb.name != name):
+            prRed(f"Expected bone of name: {name}, got {eb.name} instead.")
             raise RuntimeError("Could not create bone ", name, "; Perhaps there is a duplicate bone name in the node tree?")
         eb.matrix  = matrix.copy()
         tailoffset = Vector((0,length,0)) #Vector((0,self.tailoffset, 0))
         tailoffset = matrix.copy().to_3x3() @ tailoffset
         eb.tail    = eb.head + tailoffset
+
+        if eb.head == eb.tail:
+            raise RuntimeError(wrapRed(f"Could not create edit bone: {name} because bone head was located in the same place as bone tail."))
+
         
         if (eb.name != name):
             raise RuntimeError("Could not create edit bone: ", name)
@@ -411,6 +531,46 @@ class xFormBone:
         self.executed = True
 
     def bFinalize(self, bContext = None):
+        do_bb=False
+        b = self.bGetParentArmature().data.bones[self.bObject]
+        b.bbone_x = self.evaluate_input("BBone X Size"); b.bbone_x = max(b.bbone_x, 0.0002)
+        b.bbone_z = self.evaluate_input("BBone Z Size"); b.bbone_z = max(b.bbone_z, 0.0002)
+        if (segs := self.evaluate_input("BBone Segments")) > 1:
+            do_bb=True
+            b.bbone_segments = segs
+            b.bbone_x = self.evaluate_input("BBone X Size")
+            b.bbone_z = self.evaluate_input("BBone Z Size")
+            if self.evaluate_input("BBone HQ Deformation"):
+                b.bbone_mapping_mode = "CURVED"
+            # 'bbone_handle_type_start'    : ("BBone Start Handle Type", "AUTO"),
+            # 'bbone_handle_type_end'      : ("BBone End Handle Type", "AUTO"),
+            # 'bbone_custom_handle_start'  : ("BBone Custom Start Handle", "AUTO"),
+            # 'bbone_custom_handle_end'    : ("BBone Custom End Handle", "AUTO"),
+            if handle_type := self.evaluate_input("BBone Start Handle Type"):
+                b.bbone_handle_type_start = handle_type
+            if handle_type := self.evaluate_input("BBone End Handle Type"):
+                b.bbone_handle_type_end = handle_type
+            
+            try:
+                if (custom_handle := self.evaluate_input("BBone Custom Start Handle")):
+                    b.bbone_custom_handle_start = self.bGetParentArmature().data.bones[custom_handle]
+                # hypothetically we should support xForm inputs.... but we won't do that for now
+                # elif custom_handle is None:
+                #     b.bbone_custom_handle_start = self.inputs["BBone Custom Start Handle"].links[0].from_node.bGetObject().name
+                if (custom_handle := self.evaluate_input("BBone Custom End Handle")):
+                    b.bbone_custom_handle_end = self.bGetParentArmature().data.bones[custom_handle]
+            except KeyError:
+                prRed("Warning: BBone start or end handle not set because of missing bone in armature.")
+            
+            b.bbone_curveinx = self.evaluate_input("BBone X Curve-In")
+            b.bbone_curveinz = self.evaluate_input("BBone Z Curve-In")
+            b.bbone_curveoutx = self.evaluate_input("BBone X Curve-Out")
+            b.bbone_curveoutz = self.evaluate_input("BBone Z Curve-Out")
+            # 'bbone_curveinx'             : ("BBone X Curve-In", pb.bone.bbone_curveinx),
+            # 'bbone_curveinz'             : ("BBone Z Curve-In", pb.bone.bbone_curveinz),
+            # 'bbone_curveoutx'            : ("BBone X Curve-Out", pb.bone.bbone_curveoutx),
+            # 'bbone_curveoutz'            : ("BBone Z Curve-Out", pb.bone.bbone_curveoutz),
+            
         import bpy
         from .drivers import MantisDriver
         # prevAct = bContext.view_layer.objects.active
@@ -426,117 +586,172 @@ class xFormBone:
         # Don't need to bother about whatever that was
         
         pb = self.bGetParentArmature().pose.bones[self.bObject]
+        rotation_mode = self.evaluate_input("Rotation Order")
+        if rotation_mode == "AUTO": rotation_mode = "XYZ"
+        pb.rotation_mode = rotation_mode
         pb.id_properties_clear()
         # these are kept around unless explicitly deleted.
-        from .utilities import get_node_prototype
-        np = get_node_prototype(self.signature, self.base_tree)
+        # from .utilities import get_node_prototype
+        # np = get_node_prototype(self.signature, self.base_tree)
         driver = None
         do_prints=False
-        for i, inp in enumerate(np.inputs):
-            if ('Parameter' in inp.bl_idname):
-                name = inp.name
-                # print(inp.name)
-                try:
-                    value = self.evaluate_input(inp.name)
-                except KeyError as e:
-                    trace = trace_single_line(self, inp.name)
-                    if do_prints: print(trace[0][-1], trace[1])
-                    if do_prints: print (trace[0][-1].parameters)
-                    #prop = trace[0][-1].parameters[trace[1].name]
-                    raise e
-                # This may be driven, so let's do this:
-                if do_prints: print (value)
-                if (isinstance(value, tuple)):
-                    # it's either a CombineThreeBool or a CombineVector.
-                    prRed("COMITTING SUICIDE NOW!!")
-                    bpy.ops.wm.quit_blender()
-                if (isinstance(value, MantisDriver)):
-                    # the value should be the default for its socket...
-                    if do_prints: print (type(inp.default_value))
-                    type_val_map = {
-                        str:"",
-                        bool:False,
-                        int:0,
-                        float:0.0,
-                        bpy.types.bpy_prop_array:(0,0,0),
-                        }
-                    driver = value
-                    value = type_val_map[type(inp.default_value)]
-                if (value is None):
-                    prRed("This is probably not supposed to happen")
-                    value = 0
-                    raise RuntimeError("Could not set value of custom parameter")
-                    # it creates a more confusing error later sometimes, better to catch it here.
-                
-                # IMPORTANT: Is it possible for more than one driver to
-                #   come through here, and for the variable to be
-                #   overwritten?
-                    
-                #TODO important
-                #from rna_prop_ui import rna_idprop_ui_create
-                # use this ^
+
+        # print (self.input_length)
+        # even worse hack coming
+        for i, inp in enumerate(self.inputs.values()):
+            if inp.name in bone_inputs:
+                continue
+            
+            
+            name = inp.name
+            try:
+                value = self.evaluate_input(inp.name)
+            except KeyError as e:
+                trace = trace_single_line(self, inp.name)
+                if do_prints: print(trace[0][-1], trace[1])
+                if do_prints: print (trace[0][-1].parameters)
+                raise e
+            # This may be driven, so let's do this:
+            if do_prints: print (value)
+            if (isinstance(value, tuple)):
+                # it's either a CombineThreeBool or a CombineVector.
+                prRed("COMITTING SUICIDE NOW!!")
+                bpy.ops.wm.quit_blender()
+            if (isinstance(value, MantisDriver)):
+                # the value should be the default for its socket...
+                if do_prints: print (type(self.parameters[inp.name]))
+                type_val_map = {
+                    str:"",
+                    bool:False,
+                    int:0,
+                    float:0.0,
+                    bpy.types.bpy_prop_array:(0,0,0),
+                    }
+                driver = value
+                value = type_val_map[type(self.parameters[inp.name])]
+            if (value is None):
+                prRed("This is probably not supposed to happen")
+                value = 0
+                raise RuntimeError("Could not set value of custom parameter")
+                # it creates a more confusing error later sometimes, better to catch it here.
+            
+            # IMPORTANT: Is it possible for more than one driver to
+            #   come through here, and for the variable to be
+            #   overwritten?
                 
-                # add the custom properties to the **Pose Bone**
-                pb[name] = value
-                # This is much simpler now.
-                ui_data = pb.id_properties_ui(name)
+            #TODO important
+            #from rna_prop_ui import rna_idprop_ui_create
+            # use this ^
+            
+            # add the custom properties to the **Pose Bone**
+            pb[name] = value
+            # This is much simpler now.
+            ui_data = pb.id_properties_ui(name)
+            description=''
+            ui_data.update(
+                description=description,#inp.description,
+                default=value,)
+            #if a number
+
+            
+            if type(value) == float:
+                ui_data.update(
+                    min = inp.min,
+                    max = inp.max,
+                    soft_min = inp.soft_min,
+                    soft_max = inp.soft_max,)
+                        
+            elif type(value) == int:
                 ui_data.update(
-                    description=inp.description,
-                    default=value,)
-                #if a number
-                for num_type in ['Float']:
-                    if num_type in inp.bl_idname:
-                        ui_data.update(
-                            min = inp.min,
-                            max = inp.max,
-                            soft_min = inp.soft_min,
-                            soft_max = inp.soft_max,)
-                            
-                for num_type in ['Int']:
-                    # for some reason the types don't cast implicitly
-                    if num_type in inp.bl_idname:
-                        ui_data.update(
-                            min = int(inp.min),
-                            max = int(inp.max),
-                            soft_min = int(inp.soft_min),
-                            soft_max = int(inp.soft_max),)
-                for bool_type in ['Bool']:
-                    # for some reason the types don't cast implicitly
-                    if bool_type in inp.bl_idname:
-                        # prPurple(ui_data.update.__text_signature__)
-                        # prPurple(ui_data.update.__doc__)
-                        ui_data.update() # TODO I can't figure out what the update function expects because it isn't documented
-                # Doesn't seem to work?
-                #pb.property_overridable_library_set("["+name+"]", True)
-        # Set up IK settings, these belong to the pose bone.
-        if (pb.is_in_ik_chain): # cool!
-            pb. ik_stretch = self.evaluate_input("IK Stretch")
-            lock           = self.evaluate_input("Lock IK")
-            stiffness      = self.evaluate_input("IK Stiffness")
-            limit          = self.evaluate_input("Limit IK")
-            pb.ik_min_x    = self.evaluate_input("X Min")
-            pb.ik_max_x    = self.evaluate_input("X Max")
-            pb.ik_min_y    = self.evaluate_input("Y Min")
-            pb.ik_max_y    = self.evaluate_input("Y Max")
-            pb.ik_min_z    = self.evaluate_input("Z Min")
-            pb.ik_max_z    = self.evaluate_input("Z Max")
-            pb.ik_stiffness_x  = stiffness[0]
-            pb.ik_stiffness_y  = stiffness[1]
-            pb.ik_stiffness_z  = stiffness[2]
-            pb.lock_ik_x       = lock[0]
-            pb.lock_ik_y       = lock[1]
-            pb.lock_ik_z       = lock[2]
-            pb. use_ik_limit_x = limit[0]
-            pb.use_ik_limit_y  = limit[1]
-            pb.use_ik_limit_z  = limit[2]
+                    min = int(inp.min),
+                    max = int(inp.max),
+                    soft_min = int(inp.soft_min),
+                    soft_max = int(inp.soft_max),)
+            elif type(value) == bool:
+                ui_data.update() # TODO I can't figure out what the update function expects because it isn't documented
+        
+        if (pb.is_in_ik_chain):
+            # this  props_socket thing wasn't really meant to work here but it does, neat
+            props_sockets = {
+            'ik_stretch'          : ("IK Stretch", 0),
+            'lock_ik_x'           : (("Lock IK", 0), False),
+            'lock_ik_y'           : (("Lock IK", 1), False),
+            'lock_ik_z'           : (("Lock IK", 2), False),
+            'ik_stiffness_x'      : (("IK Stiffness", 0), 0.0),
+            'ik_stiffness_y'      : (("IK Stiffness", 1), 0.0),
+            'ik_stiffness_z'      : (("IK Stiffness", 2), 0.0),
+            'use_ik_limit_x'      : (("Limit IK", 0), False),
+            'use_ik_limit_y'      : (("Limit IK", 1), False),
+            'use_ik_limit_z'      : (("Limit IK", 2), False),
+            'ik_min_x'            : ("X Min", 0),
+            'ik_max_x'            : ("X Max", 0),
+            'ik_min_y'            : ("Y Min", 0),
+            'ik_max_y'            : ("Y Max", 0),
+            'ik_min_z'            : ("Z Min", 0),
+            'ik_max_z'            : ("Z Max", 0),
+            }
+            evaluate_sockets(self, pb, props_sockets)
+        if do_bb:
+            props_sockets = {
+            'bbone_curveinx'             : ("BBone X Curve-In", pb.bone.bbone_curveinx),
+            'bbone_curveinz'             : ("BBone Z Curve-In", pb.bone.bbone_curveinz),
+            'bbone_curveoutx'            : ("BBone X Curve-Out", pb.bone.bbone_curveoutx),
+            'bbone_curveoutz'            : ("BBone Z Curve-Out", pb.bone.bbone_curveoutz),
+            'bbone_easein'               : ("BBone Ease-In", 0),
+            'bbone_easeout'              : ("BBone Ease-Out", 0),
+            'bbone_rollin'               : ("BBone Roll-In", 0),
+            'bbone_rollout'              : ("BBone Roll-Out", 0),
+            'bbone_scalein'              : ("BBone Scale-In", (1,1,1)),
+            'bbone_scaleout'             : ("BBone Scale-Out", (1,1,1)),
+            }
+            prRed("BBone Implementation is not complete, expect errors and missing features for now")
+            evaluate_sockets(self, pb, props_sockets)
+            # we need to clear this stuff since our only real goal was to get some drivers from the above
+            for attr_name in props_sockets.keys():
+                try:
+                    setattr(pb, attr_name, 0) # just clear it
+                except ValueError:
+                    setattr(pb, attr_name, (1.0,1.0,1.0)) # scale needs to be set to 1
+
+
+            # important TODO... all of the drivers and stuff should be handled this way, right?
         # time to set up drivers!
+
+
+        # just gonna add this to the end and build off it I guess
+        props_sockets = {
+            "lock_location"               : ("Lock Location", [False, False, False]),
+            "lock_rotation"               : ("Lock Rotation", [False, False, False]),
+            "lock_scale"                  : ("Lock Scale", [False, False, False]),
+            'custom_shape_scale_xyz'      : ("Custom Object Scale",  (0.0,0.0,0.0) ),
+            'custom_shape_translation'    : ("Custom Object Translation",  (0.0,0.0,0.0) ),
+            'custom_shape_rotation_euler' : ("Custom Object Rotation",  (0.0,0.0,0.0) ),
+            'use_custom_shape_bone_size'  : ("Custom Object Scale to Bone Length",  True,)
+        }
+
+        evaluate_sockets(self, pb, props_sockets)
+
+        # this could probably be moved to bExecute
+        props_sockets = {
+            'hide'      : ("Hide", False),
+            'show_wire' : ("Custom Object Wireframe", False),
+        }
+        evaluate_sockets(self, pb.bone, props_sockets)
+
+        
         if (driver):
             pass
+        # whatever I was doing there.... was stupid. CLEAN UP TODO
+        # this is the right thing to do.
+        finish_drivers(self)
         #
         # OK, visual settings
         #
         # Get the override xform's bone:
-        
+
+
+
+    
         
         if len(self.inputs["Custom Object xForm Override"].links) > 0:
             trace = trace_single_line(self, "Custom Object xForm Override")
@@ -554,12 +769,14 @@ class xFormBone:
             if type(ob) in [bpy.types.Object]:
                 pb.custom_shape = ob
         #
-        pb.bone.hide = self.evaluate_input("Hide")
-        pb.custom_shape_scale_xyz = self.evaluate_input("Custom Object Scale")
-        pb.custom_shape_translation = self.evaluate_input("Custom Object Translation")
-        pb.custom_shape_rotation_euler = self.evaluate_input("Custom Object Rotation")
-        pb.use_custom_shape_bone_size = self.evaluate_input("Custom Object Scale to Bone Length")
-        pb.bone.show_wire = self.evaluate_input("Custom Object Wireframe")
+        # pb.bone.hide = self.evaluate_input("Hide")
+        # pb.custom_shape_scale_xyz = self.evaluate_input("Custom Object Scale")
+        # pb.custom_shape_translation = self.evaluate_input("Custom Object Translation")
+        # pb.custom_shape_rotation_euler = self.evaluate_input("Custom Object Rotation")
+        # pb.use_custom_shape_bone_size = self.evaluate_input("Custom Object Scale to Bone Length")
+        # pb.bone.show_wire = self.evaluate_input("Custom Object Wireframe")
+
+
         # #
         # # D E P R E C A T E D
         # #
@@ -621,6 +838,11 @@ class xFormBone:
             prRed ("Cannot get bone for %s" % self)
             raise e
 
+    def fill_parameters(self):
+        # this is the fill_parameters that is run if it isn't a schema
+        setup_custom_props(self)
+        fill_parameters(self)
+        # otherwise we will do this from the schema 
 
 
 class xFormGeometryObject:
@@ -630,13 +852,13 @@ class xFormGeometryObject:
 
     def __init__(self, signature, base_tree):
         self.base_tree=base_tree
-        self.executed = False
         self.signature = signature
         self.inputs = {
           "Name"           : NodeSocket(is_input = True, name = "Name", node = self),
           "Geometry" : NodeSocket(is_input = True, name = "Geometry", node = self),
           "Matrix"         : NodeSocket(is_input = True, name = "Matrix", node = self),
           "Relationship"   : NodeSocket(is_input = True, name = "Relationship", node = self),
+          "Deformer"   : NodeSocket(is_input = True, name = "Relationship", node = self),
         }
         self.outputs = {
           "xForm Out" : NodeSocket(is_input = False, name="xForm Out", node = self), }
@@ -645,6 +867,7 @@ class xFormGeometryObject:
           "Geometry":None, 
           "Matrix":None, 
           "Relationship":None, 
+          "Deformer":None, 
         }
         self.links = {} # leave this empty for now!
         # now set up the traverse target...
@@ -652,6 +875,8 @@ class xFormGeometryObject:
         self.outputs["xForm Out"].set_traverse_target(self.inputs["Relationship"])
         self.node_type = "XFORM"
         self.bObject = None
+        self.prepared = True
+        self.executed = False
 
     def bSetParent(self, ob):
         from bpy.types import Object, Bone
@@ -670,40 +895,33 @@ class xFormGeometryObject:
             #   matrix after setting the parent.
             #
             # deal with parenting settings here, if necesary
-        
-    def evaluate_input(self, input_name):
-        return evaluate_input(self, input_name)
-
-    def bExecute(self, bContext = None,):
+    def bPrepare(self, bContext = None,):
         import bpy
         self.bObject = bpy.data.objects.get(self.evaluate_input("Name"))
-        if not self.bObject:
+        if (not self.bObject) or (self.inputs["Geometry"].is_linked and self.bObject.type == "EMPTY"):
             trace = trace_single_line(self, "Geometry")
             if trace[-1]:
                 self.bObject = bpy.data.objects.new(self.evaluate_input("Name"), trace[-1].node.bGetObject())
         else: # clear it
             self.bObject.constraints.clear()
-            # Don't clear this, we want to be able to reuse this
-            # self.bObject.vertex_groups.clear()
-            #
-            # This is probably also not what we want - but for now it is OK
+            self.bObject.animation_data_clear() # this is a little dangerous.
             self.bObject.modifiers.clear()
+                    
         try:
             bpy.context.collection.objects.link(self.bObject)
         except RuntimeError: #already in; but a dangerous thing to pass.
             pass
         
         self.bSetParent(self.bObject)
-        self.bObject.matrix_world = self.evaluate_input("Matrix")
+        matrix = self.evaluate_input("Matrix")
+        self.parameters['Matrix'] = matrix
+        self.bObject.matrix_world = matrix
+        self.executed = True
             
-    def bFinalize(self, bContext = None):
-        pass
         
     def bGetObject(self, mode = 'POSE'):
         return self.bObject
 
-    def __repr__(self):
-        return self.signature.__repr__()
 
-    def fill_parameters(self):
-        fill_parameters(self)
+for c in TellClasses():
+    setup_container(c)

+ 190 - 94
xForm_definitions.py

@@ -61,10 +61,99 @@ class xFormRootNode(Node, xFormNode):
     bl_idname = 'xFormRootNode'
     bl_label = "World Root"
     bl_icon = 'WORLD'
+    initialized : bpy.props.BoolProperty(default = False)
 
     def init(self, context):
         self.outputs.new('RelationshipSocket', "World Out")
-        
+        self.initialized=True
+
+
+
+# I had chat gpt flip these so they may be a little innacurate
+# always visible
+main_names = {
+"Name":'StringSocket',
+"Rotation Order":'RotationOrderSocket',
+"Relationship":'RelationshipSocket',
+"Matrix":'MatrixSocket',}
+
+# IK SETTINGS
+ik_names = {
+"IK Stretch":'FloatFactorSocket',
+"Lock IK":'BooleanThreeTupleSocket',
+"IK Stiffness":'NodeSocketVector',
+"Limit IK":'BooleanThreeTupleSocket',
+"X Min":'NodeSocketFloatAngle',
+"X Max":'NodeSocketFloatAngle',
+"Y Min":'NodeSocketFloatAngle',
+"Y Max":'NodeSocketFloatAngle',
+"Z Min":'NodeSocketFloatAngle',
+"Z Max":'NodeSocketFloatAngle',
+}
+
+#display settings
+display_names = {
+"Bone Collection":'BoneCollectionSocket',
+"Custom Object":'xFormSocket',
+"Custom Object xForm Override":'xFormSocket',
+"Custom Object Scale to Bone Length":'BooleanSocket',
+"Custom Object Wireframe":'BooleanSocket',
+"Custom Object Scale":'VectorScaleSocket',
+"Custom Object Translation":'VectorSocket',
+"Custom Object Rotation":'VectorEulerSocket',
+}
+
+# deform_names
+deform_names = {
+"Deform":'BooleanSocket',
+"Envelope Distance":'FloatPositiveSocket',
+"Envelope Weight":'FloatFactorSocket',
+"Envelope Multiply":'BooleanSocket',
+"Envelope Head Radius":'FloatPositiveSocket',
+"Envelope Tail Radius":'FloatPositiveSocket',
+}
+
+bbone_names = {
+    "BBone Segments":"IntSocket", # BONE
+    "BBone X Size":"FloatSocket", # BONE
+    "BBone Z Size":"FloatSocket", # BONE
+    # "bbone_mapping_mode":"StringSocket", <== BONE
+    "BBone HQ Deformation":"BooleanSocket", # BONE bbone_mapping_mode
+    "BBone X Curve-In":"FloatSocket", # BONE AND POSE
+    "BBone Z Curve-In":"FloatSocket", # BONE AND POSE
+    "BBone X Curve-Out":"FloatSocket", # BONE AND POSE
+    "BBone Z Curve-Out":"FloatSocket", # BONE AND POSE
+    "BBone Roll-In":"FloatSocket", # BONE AND POSE
+    "BBone Roll-Out":"FloatSocket", # BONE AND POSE
+    "BBone Inherit End Roll":"BooleanSocket", # BONE
+    "BBone Scale-In":"VectorSocket", # BONE AND POSE
+    "BBone Scale-Out":"VectorSocket", # BONE AND POSE
+    "BBone Ease-In":"FloatSocket", # BONE AND POSE
+    "BBone Ease-Out":"FloatSocket", # BONE AND POSE
+    "BBone Easing":"BooleanSocket", # BONE
+    "BBone Start Handle Type":"EnumBBoneHandleType", # BONE
+    "BBone Custom Start Handle":"StringSocket", # BONE
+    "BBone Start Handle Scale":"BooleanThreeTupleSocket", # BONE
+    "BBone Start Handle Ease":"BooleanSocket", # BONE
+    "BBone End Handle Type":"EnumBBoneHandleType", # BONE
+    "BBone Custom End Handle":"StringSocket", # BONE
+    "BBone End Handle Scale":"BooleanThreeTupleSocket", # BONE
+    "BBone End Handle Ease":"BooleanSocket", # BONE
+
+}
+
+other_names = {
+    "Lock Location":'BooleanThreeTupleSocket',
+    "Lock Rotation":'BooleanThreeTupleSocket',
+    "Lock Scale":'BooleanThreeTupleSocket',
+    "Hide":'HideSocket',
+}
+
+from mathutils import Color
+xFormColor = Color((0.093172, 0.047735, 0.028036)).from_scene_linear_to_srgb()
+
+
+
 class xFormBoneNode(Node, xFormNode):
     '''A node representing a Bone'''
     bl_idname = 'xFormBoneNode'
@@ -74,76 +163,66 @@ class xFormBoneNode(Node, xFormNode):
     display_ik_settings : bpy.props.BoolProperty(default=False)
     display_vp_settings : bpy.props.BoolProperty(default=False)
     display_def_settings : bpy.props.BoolProperty(default=False)
+    display_bb_settings : bpy.props.BoolProperty(default=False)
     socket_count : bpy.props.IntProperty()
+    initialized : bpy.props.BoolProperty(default = False)
     
     def init(self, context):
-        self.inputs.new('StringSocket', "Name")
-        self.inputs.new('RotationOrderSocket', "Rotation Order")
-        self.inputs.new('RelationshipSocket', "Relationship")
-        self.inputs.new('MatrixSocket', "Matrix")
 
-        # IK SETTINGS
-        a = []
-        # a.append(self.inputs.new ('LabelSocket', "IK Settings"))
-        a.append(self.inputs.new ('FloatFactorSocket', "IK Stretch"))
-        a.append(self.inputs.new ('BooleanThreeTupleSocket', "Lock IK"))
-        a.append(self.inputs.new ('NodeSocketVector', "IK Stiffness"))
-        a.append(self.inputs.new ('BooleanThreeTupleSocket', "Limit IK"))
-        a.append(self.inputs.new ('NodeSocketFloatAngle', "X Min"))
-        a.append(self.inputs.new ('NodeSocketFloatAngle', "X Max"))
-        a.append(self.inputs.new ('NodeSocketFloatAngle', "Y Min"))
-        a.append(self.inputs.new ('NodeSocketFloatAngle', "Y Max"))
-        a.append(self.inputs.new ('NodeSocketFloatAngle', "Z Min"))
-        a.append(self.inputs.new ('NodeSocketFloatAngle', "Z Max"))
-        #4-14
-        
-        # visual settings:
-        b = []
-        b.append(self.inputs.new ('BoneCollectionInputSocket', "Bone Collection"))
-        b.append(self.inputs.new ('xFormSocket', "Custom Object"))
-        b.append(self.inputs.new ('xFormSocket', "Custom Object xForm Override"))
-        b.append(self.inputs.new ('BooleanSocket', "Custom Object Scale to Bone Length"))
-        b.append(self.inputs.new ('BooleanSocket', "Custom Object Wireframe"))
-        b.append(self.inputs.new ('VectorScaleSocket', "Custom Object Scale"))
-        b.append(self.inputs.new ('VectorSocket', "Custom Object Translation"))
-        b.append(self.inputs.new ('VectorEulerSocket', "Custom Object Rotation"))
-        # 16-21
-        # Deform Settings:
-        c = []
-        c.append(self.inputs.new ('BooleanSocket', "Deform"))
-        c.append(self.inputs.new ('FloatPositiveSocket', "Envelope Distance"))
-        c.append(self.inputs.new ('FloatFactorSocket',   "Envelope Weight"))
-        c.append(self.inputs.new ('BooleanSocket', "Envelope Multiply"))
-        c.append(self.inputs.new ('FloatPositiveSocket', "Envelope Head Radius"))
-        c.append(self.inputs.new ('FloatPositiveSocket', "Envelope Tail Radius"))
-        #22-27
-        
-        # c[0].default_value=False
-        
-        # Hide should be last
-        b.append(self.inputs.new ('HideSocket',   "Hide"))
-    
+        for name, sock_type in main_names.items():
+            self.inputs.new(sock_type, name)
+
+        for name, sock_type in ik_names.items():
+            s = self.inputs.new(sock_type, name)
+            s.hide = True
+
+        for name, sock_type in display_names.items():
+            s = self.inputs.new(sock_type, name)
+            if s.name in ['Custom Object', 'Bone Collection']:
+                continue
+            s.hide = True
         
-        for sock in a:
-            sock.hide = True
-        for sock in b:
-            if sock.name in ['Custom Object', 'Bone Collection']:
+        for name, sock_type in deform_names.items():
+            s = self.inputs.new(sock_type, name)
+            if s.name == 'Deform':
                 continue
-            sock.hide = True
-        for sock in c:
-            if sock.name == 'Deform':
+            s.hide = True
+        
+        for name, sock_type in bbone_names.items():
+            s = self.inputs.new(sock_type, name)
+            if s.name == "BBone Segments":
                 continue
-            sock.hide = True
-            
-        # Thinking about using colors for nodes, why not?
-        # cxForm          = (0.443137, 0.242157, 0.188235,) #could even fetch the theme colors...
-        # self.color=cxForm
-        # self.use_custom_color=True
+            s.hide = True
+
+        for name, sock_type in other_names.items():
+            self.inputs.new(sock_type, name)
+        # could probably simplify this further with iter_tools.chain() but meh
+
         self.socket_count = len(self.inputs)
         #
         self.outputs.new('xFormSocket', "xForm Out")
+
+
+        # set up some defaults...
+        self.inputs['Rotation Order'].default_value = "XYZ"
+        self.inputs['Lock Location'].default_value[0] = True
+        self.inputs['Lock Location'].default_value[1] = True
+        self.inputs['Lock Location'].default_value[2] = True
+        self.inputs['Lock Rotation'].default_value[0] = True
+        self.inputs['Lock Rotation'].default_value[1] = True
+        self.inputs['Lock Rotation'].default_value[2] = True
+        self.inputs['Lock Scale'].default_value[0] = True
+        self.inputs['Lock Scale'].default_value[1] = True
+        self.inputs['Lock Scale'].default_value[2] = True
+
+        # color
+        self.use_custom_color = True
+        self.color = xFormColor
+        #
+        self.initialized=True
     
     def draw_buttons(self, context, layout):
+        # return
         layout.operator("mantis.add_custom_property", text='+Add Custom Parameter')
         # layout.label(text="Edit Parameter ... not implemented")
         if (len(self.inputs) >= self.socket_count):
@@ -153,7 +232,6 @@ class xFormBoneNode(Node, xFormNode):
         
     def display_update(self, parsed_tree, context):
         if context.space_data:
-            node_tree = context.space_data.path[0].node_tree
             nc = parsed_tree.get(get_signature_from_edited_tree(self, context))
             other_nc = None
             if len(self.inputs.get("Relationship").links)>0:
@@ -164,21 +242,24 @@ class xFormBoneNode(Node, xFormNode):
             if nc and other_nc:
                 self.display_vp_settings = nc.inputs["Custom Object"].is_connected
                 self.display_def_settings = nc.evaluate_input("Deform")
+                self.display_bb_settings = nc.evaluate_input("BBone Segments") > 1
                 self.display_ik_settings = False
                 #
                 from .node_container_common import ( trace_all_lines_up,
                                                      trace_single_line)
-                trace = trace_all_lines_up(nc, "xForm Out")
-                
-                for key in trace.keys():
-                    if (ik_nc:= parsed_tree.get(key)):
-                        if ik_nc.__class__.__name__ in ["LinkInverseKinematics"]:
-                            # if the tree is invalid? This shouldn't be necessary.
-                            if ik_nc.inputs["Input Relationship"].is_connected:
-                                chain_count = ik_nc.evaluate_input("Chain Length")
-                                if chain_count == 0:
-                                    self.display_ik_settings = True
-                                else:
+                #TODO find a much faster way to do this
+                if False and self.id_data.do_live_update: #TODO this is extremely freaking slow
+                    trace = trace_all_lines_up(nc, "xForm Out")
+
+                    for key in trace.keys():
+                        if (ik_nc:= parsed_tree.get(key)):
+                            if ik_nc.__class__.__name__ in ["LinkInverseKinematics"]:
+                                # if the tree is invalid? This shouldn't be necessary.
+                                if ik_nc.inputs["Input Relationship"].is_connected:
+                                    chain_count = ik_nc.evaluate_input("Chain Length")
+                                    # if chain_count == 0: # this is wrong
+                                    #     self.display_ik_settings = True
+                                    # else:
                                     if ik_nc.evaluate_input("Use Tail") == False:
                                         chain_count+=1
                                     for line in trace[key]:
@@ -191,9 +272,8 @@ class xFormBoneNode(Node, xFormNode):
                                             if path_nc.node_type == 'XFORM':
                                                 xForm_line.append(path_nc)
                                             prev_path_nc = path_nc
-                                            
-                                        else:
-                                            if len(xForm_line) < chain_count:
+                                        else: # if it can finish cleaning the line of non-xForms and still has a connection
+                                            if (len(xForm_line) < chain_count) or (chain_count == 0):
                                                 self.display_ik_settings = True
                 
                 inp = nc.inputs["Relationship"]
@@ -212,26 +292,27 @@ class xFormBoneNode(Node, xFormNode):
                         link = inp.links[0]
                     else:
                         link = None
-            #
-            if self.display_ik_settings == True:
-                for inp in self.inputs[4:14]:
-                    inp.hide = False
-            else:
-                for inp in self.inputs[4:14]:
-                    inp.hide = True
-            if self.display_vp_settings == True:
-                for inp in self.inputs[16:21]:
-                    inp.hide = False
-            else:
-                for inp in self.inputs[16:21]:
-                    inp.hide = True
-            #
-            if self.display_def_settings == True:
-                for inp in self.inputs[23:28]:
-                    inp.hide = False
-            else:
-                for inp in self.inputs[23:28]:
-                    inp.hide = True
+        
+            
+            for name in ik_names.keys():
+                self.inputs[name].hide = not self.display_ik_settings
+            
+            for name in display_names.keys():
+                if name in ['Custom Object', 'Bone Collection']: continue
+                self.inputs[name].hide = not self.display_vp_settings
+
+            for name in deform_names.keys():
+                if name in ['Deform']: continue
+                self.inputs[name].hide = not self.display_def_settings
+
+            for name in bbone_names.keys():
+                if name in ['BBone Segments']: continue
+                self.inputs[name].hide = not self.display_bb_settings
+            
+
+    # def update(self):
+    #     self.display_update()
+
         
     
     # def copy(ectype, archtype):
@@ -244,6 +325,7 @@ class xFormArmatureNode(Node, xFormNode):
     bl_idname = 'xFormArmatureNode'
     bl_label = "Armature"
     bl_icon = 'OUTLINER_OB_ARMATURE'
+    initialized : bpy.props.BoolProperty(default = False)
 
     def init(self, context):
         self.inputs.new('StringSocket', "Name")
@@ -252,16 +334,30 @@ class xFormArmatureNode(Node, xFormNode):
         self.inputs.new('MatrixSocket', "Matrix")
         self.outputs.new('xFormSocket', "xForm Out")
 
+        # color
+        self.use_custom_color = True
+        self.color = xFormColor
+
+        self.initialized=True
+
 
 class xFormGeometryObjectNode(Node, xFormNode):
     """Represents a curve or mesh object."""
     bl_idname = "xFormGeometryObject"
     bl_label = "Geometry Object"
     bl_icon = "EMPTY_AXIS"
+    initialized : bpy.props.BoolProperty(default = False)
     
     def init(self, context):
         self.inputs.new('StringSocket', "Name")
         self.inputs.new('GeometrySocket', "Geometry")
         self.inputs.new('MatrixSocket', "Matrix")
         self.inputs.new('RelationshipSocket', "Relationship")
+        self.inputs.new('DeformerSocket', "Deformer")
         self.outputs.new('xFormSocket', "xForm Out")
+
+        # color
+        self.use_custom_color = True
+        self.color = xFormColor
+
+        self.initialized=True

Some files were not shown because too many files changed in this diff