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 10 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