56 次代碼提交 fcdc86d053 ... bb161c0f79

作者 SHA1 備註 提交日期
  Joseph Brandenburg bb161c0f79 v 0.13.0 Beta 2 周之前
  Joseph Brandenburg 2f545bea82 Cleanup misguided changes in DummyNode 2 周之前
  Joseph Brandenburg 909a4fe43e Get String Variables at Execution Time 2 周之前
  Joseph Brandenburg 9c3089b090 Fix syntax error 2 周之前
  Brandenburg 49b4285bb4 Fix Schema fail when Group linked to In/Outgoing 3 周之前
  Brandenburg 4343696338 some unsaved stuff I forgot to add. trash? 1 月之前
  Brandenburg 980a110e2e WIP string variables 1 月之前
  Joseph Brandenburg d987aeeef1 Basic implementation of Interface nodes in Schema 2 周之前
  Joseph Brandenburg 618292c1fb Fix: Spline IK broken 2 周之前
  Joseph Brandenburg 8afbe08f36 Fix: lazy parents broken 2 周之前
  Joseph Brandenburg 0ae5f8987f cleanup xForm get_parent_node 2 周之前
  Joseph Brandenburg 079bb2c4f3 Fix: Correctly Sort Links to Group Arrays 2 周之前
  Joseph Brandenburg 78e29ad9a5 Fix: remove many instances of hardcoded node get 2 周之前
  Joseph Brandenburg 29192e3deb clean up useless prints 2 周之前
  Joseph Brandenburg 826cb475e3 GroupInterfaceNodes at group in/out 2 周之前
  Brandenburg 669737d63b WIP: Route Group I/O through interface nodes 2 周之前
  Joseph Brandenburg fc2190cdaf Initialize Tree with correct version number 3 周之前
  Joseph Brandenburg 65f0a9b19d Fix: make default values work with more socket types 3 周之前
  Joseph Brandenburg 6a1569004d Fix: interface panel doesn't have an identifier 3 周之前
  Joseph Brandenburg c3db52cfbe New Feature: Default Values for base tree 3 周之前
  Joseph Brandenburg 01ee680b48 Fix: correct default value type for vectors 3 周之前
  Joseph Brandenburg 0a2a483050 Fix: default value disabled for Matrix 3 周之前
  Joseph Brandenburg e14e03e97e Disable "Connected To" feature 3 周之前
  Joseph Brandenburg c0b20ed5c9 Implement Custom Interface Classes 3 周之前
  Joseph Brandenburg 371c0cc05a initial versioning for new interface classes 3 周之前
  Joseph Brandenburg 501c025165 fix: unbound local error when updating group interface 3 周之前
  Joseph Brandenburg fb77125b3d update interface draw for correct UI and clarity 3 周之前
  Joseph Brandenburg 05bf3a0188 Interface Classes set the multi and default value now 3 周之前
  Joseph Brandenburg 85783ff580 Add Custom Interface Socket Types 3 周之前
  Joseph Brandenburg 965c6c48d6 v0.12.28 revert unhelpful broken patch 2 周之前
  Joseph Brandenburg d0d622a238 Revert "Fix: Nested Choose fails when linked to group output" 2 周之前
  Joseph Brandenburg 0eb22ed488 v 0.12.27 bug fix release 2 周之前
  Joseph Brandenburg 003e6b573a Fix: Nested Choose fails when linked to group output 2 周之前
  Joseph Brandenburg cac82c21f8 Cleanup Drivers 2 周之前
  Brandenburg aa15119835 Fix: Custom properties fail in Drivers 3 周之前
  Brandenburg 1a642ea8bb Fix: DriverVariable fails when linked to non-xForm 3 周之前
  Brandenburg 595c799eb8 cleanup: remove prints 3 周之前
  Brandenburg 022e7e8952 IO: set Mantis Version on new tree immediately 3 周之前
  Joseph Brandenburg 82d0d38a04 Cleanup: remove green color from visualize code 3 周之前
  Joseph Brandenburg 68beca0a49 Cleanup: unboundlocal error in node_group_update 3 周之前
  Joseph Brandenburg 21d2200458 Cleanup: Remove prints from array_choose_relink 3 周之前
  Joseph Brandenburg e8c9d1a784 Clean up print in file load 3 周之前
  Brandenburg fcdc86d053 Fix: Custom Properties don't trigger tree update 3 周之前
  Brandenburg b68cbab0b4 Fix: custom properties borked due to syntax error 3 周之前
  Brandenburg 1fbf709b10 Fix Schema fail when Group linked to In/Outgoing 3 周之前
  Brandenburg 2c7bfbe798 Fix: Generate Tree skill issue always led to error 3 周之前
  Brandenburg 31b7265818 Fix: rare buffer overflow in custom properties 3 周之前
  Joseph Brandenburg 8d9c4deb79 Fix: Custom Properties not Library Overridable 1 月之前
  Brandenburg 66fd20f7ea Fix: Custom Properties don't trigger tree update 3 周之前
  Brandenburg 190f01686b Fix: custom properties borked due to syntax error 3 周之前
  Brandenburg a11e4be7da Fix Schema fail when Group linked to In/Outgoing 3 周之前
  Brandenburg d3fdd0f109 Fix: Generate Tree skill issue always led to error 3 周之前
  Brandenburg 1781ab8df0 Fix: rare buffer overflow in custom properties 3 周之前
  Brandenburg baf1847975 some unsaved stuff I forgot to add. trash? 1 月之前
  Brandenburg 82613019b2 WIP string variables 1 月之前
  Joseph Brandenburg 60a0c8b177 Fix: Custom Properties not Library Overridable 1 月之前
共有 21 個文件被更改,包括 783 次插入441 次删除
  1. 3 1
      .gitignore
  2. 11 8
      __init__.py
  3. 55 54
      base_definitions.py
  4. 3 3
      blender_manifest.toml
  5. 14 14
      drivers.py
  6. 43 42
      i_o.py
  7. 21 12
      internal_containers.py
  8. 46 35
      link_nodes.py
  9. 72 75
      misc_nodes.py
  10. 2 3
      node_container_common.py
  11. 1 1
      ops_nodegroup.py
  12. 3 3
      preferences.py
  13. 153 83
      readtree.py
  14. 2 2
      schema_nodes.py
  15. 18 10
      schema_nodes_ui.py
  16. 47 6
      schema_solve.py
  17. 127 0
      socket_definitions.py
  18. 82 43
      utilities.py
  19. 37 1
      versioning.py
  20. 27 27
      visualize.py
  21. 16 18
      xForm_nodes.py

+ 3 - 1
.gitignore

@@ -6,4 +6,6 @@ index.json
 mantis.zip
 mantis.*.zip
 widgets/*
-components/*
+components/*
+mantis-beta.zip
+mantis_beta.zip

+ 11 - 8
__init__.py

@@ -16,9 +16,9 @@ from .ops_generate_tree import GenerateMantisTree
 
 from .utilities import prRed
 
-MANTIS_VERSION_MAJOR=0
-MANTIS_VERSION_MINOR=12
-MANTIS_VERSION_SUB=26
+from .base_definitions import (MANTIS_VERSION_MAJOR,
+                               MANTIS_VERSION_MINOR,
+                               MANTIS_VERSION_SUB)
 
 classLists = [module.TellClasses() for module in [
  link_nodes_ui,
@@ -267,8 +267,7 @@ def execute_handler(scene):
                 node_tree.tree_valid=False
 
 from .versioning import versioning_tasks
-def node_version_update(node):
-    do_once = True
+def node_version_update(node, do_once):
     for bl_idname, task, required_kwargs in versioning_tasks:
         arg_map = {}
         if 'node' in required_kwargs:
@@ -279,11 +278,11 @@ def node_version_update(node):
             if do_once:
                 print (f"Updating tree {node.id_data.name} to "
                        f"{MANTIS_VERSION_MAJOR}.{MANTIS_VERSION_MINOR}.{MANTIS_VERSION_SUB}")
-                do_once=False
             task(**arg_map)
 
 def do_version_update(node_tree):
     # set updating status for dynamic nodes to prevent bugs in socket remapping
+    do_once = True
     for node in node_tree.nodes:
         if hasattr(node, 'is_updating'):
             node.is_updating = True
@@ -296,7 +295,8 @@ def do_version_update(node_tree):
         task(**arguments)
     # run the updates that have no prerequisites
     for node in node_tree.nodes:
-        node_version_update(node)
+        node_version_update(node, do_once)
+        do_once = False
     # NOTE: if future versoning tasks have prerequisites, resolve them here and update again
     # reset the updating status for dynamic nodes
     for node in node_tree.nodes:
@@ -312,7 +312,6 @@ def version_update_handler(filename):
     for node_tree in bpy.data.node_groups: # ensure it can update again after file load.
         if node_tree.bl_idname in ["MantisTree", "SchemaTree"]:
                 node_tree.is_exporting=False; node_tree.is_executing=False
-
     for node_tree in bpy.data.node_groups:
         if node_tree.bl_idname in ["MantisTree", "SchemaTree"]:
             if (node_tree.mantis_version[0] < MANTIS_VERSION_MAJOR) or \
@@ -386,6 +385,10 @@ def on_undo_post_handler(scene): # the undo will trigger a depsgraph update
 from .menu_classes import (node_context_menu_draw, node_add_menu_draw,
                            armature_add_menu_draw, import_menu_draw)
 
+from .socket_definitions import generate_custom_interface_types
+generated_classes = generate_custom_interface_types()
+classes.extend(generated_classes)
+
 def register():
     from bpy.utils import register_class
 

+ 55 - 54
base_definitions.py

@@ -13,6 +13,7 @@ from .utilities import (prRed, prGreen, prPurple, prWhite,
 from .utilities import get_socket_maps, relink_socket_map, do_relink
 
 FLOAT_EPSILON=0.0001 # used to check against floating point inaccuracy
+links_sort_key= lambda a : (-a.multi_input_sort_id, -a.sub_sort_id)
 
 def TellClasses():
     #Why use a function to do this? Because I don't need every class to register.
@@ -79,6 +80,10 @@ def hash_tree(tree):
     links.sort(); hash_data+=''.join(links)
     return hash(hash_data)
 
+MANTIS_VERSION_MAJOR=0
+MANTIS_VERSION_MINOR=13
+MANTIS_VERSION_SUB=0
+
 class MantisTree(NodeTree):
     '''A custom node tree type that will show up in the editor type list'''
     bl_idname = 'MantisTree'
@@ -97,7 +102,8 @@ class MantisTree(NodeTree):
     is_exporting:BoolProperty(default=False)
     execution_id:StringProperty(default='')
     # prev_execution_id:StringProperty(default='')
-    mantis_version:IntVectorProperty(default=[0,9,2])
+    mantis_version:IntVectorProperty(default=[
+        MANTIS_VERSION_MAJOR, MANTIS_VERSION_MINOR, MANTIS_VERSION_SUB])
     # this prevents the node group from executing on the next depsgraph update
     # because I don't always have control over when the dg upadte happens.
     prevent_next_exec:BoolProperty(default=False)
@@ -189,7 +195,8 @@ class SchemaTree(NodeTree):
     is_executing:BoolProperty(default=False)
     is_exporting:BoolProperty(default=False)
 
-    mantis_version:IntVectorProperty(default=[0,9,2])
+    mantis_version:IntVectorProperty(default=[
+        MANTIS_VERSION_MAJOR, MANTIS_VERSION_MINOR, MANTIS_VERSION_SUB])
     # see the note in MantisTree
     interface_helper : StringProperty(default='')
 
@@ -344,12 +351,9 @@ def node_group_update(node, force = False):
            (node.id_data.is_exporting == True):
             return
     # note: if (node.id_data.is_exporting == True) I need to be able to update so I can make links.
-
     if node.node_tree is None:
         node.inputs.clear(); node.outputs.clear()
-        node.id_data.do_live_update = toggle_update
         return
-
     toggle_update = node.id_data.do_live_update
     node.id_data.do_live_update = False
 
@@ -468,18 +472,18 @@ def node_group_update(node, force = False):
             reorder_collection = reorder_me_input if is_input else reorder_me_output
             if socket_map:
                 if item.identifier in socket_map.keys():
-                    socket = relink_socket_map_add_socket(node, socket_collection, item)
+                    socket = relink_socket_map_add_socket(node, socket_collection, item, item.in_out)
                     do_relink(node, socket, socket_map, item.in_out)
                 else:
                     for has_socket in socket_collection:
                         if has_socket.bl_idname == item.bl_socket_idname and \
-                            has_socket.name == item.name:
+                                has_socket.name == item.name:
                             reorder_collection.append((has_socket, counter))
                             break
                     else:
-                        socket = relink_socket_map_add_socket(node, socket_collection, item)
+                        socket = relink_socket_map_add_socket(node, socket_collection, item, item.in_out)
             else:
-                socket = relink_socket_map_add_socket(node, socket_collection, item)
+                socket = relink_socket_map_add_socket(node, socket_collection, item, item.in_out)
             counter += 1
 
         # TODO: de-duplicate this hideous stuff
@@ -505,7 +509,7 @@ def node_group_update(node, force = False):
                         if exists.identifier == item.identifier:
                             break
                     else:
-                        update_group_sockets(item, True)
+                        update_group_sockets(item, False)
                 else:
                     update_group_sockets(item, False)
                 output_index += 1
@@ -848,40 +852,45 @@ class MantisNode:
         for conn in self.hierarchy_connections:
             conn.reset_execution_recursive()
 
+    def get_interface_signature(self):
+        interface_signature = (*self.signature[:-1], 'InputInterface')
+        ui_name = self.ui_signature[-1]
+        name = self.signature[-1]
+        if ui_name != name:
+            schema_suffix = ''
+            for i in range(len(name)):
+                if name[i] == ui_name[i]: continue
+                break
+            schema_suffix = name[i+1:]
+            interface_signature = (*interface_signature[:-1],
+                                'InputInterface'+schema_suffix)
+        return interface_signature
 
     # TODO: make this MUCH more efficient!
     # alternatively: call this ONCE when initializing the tree, precache results?
     def apply_string_variables(self, string):
         # We get the mContext, iterate through the signature, and string-replace variables
         # this function should be called by evaluate_input if the result is a string.
-        all_vars = {} # maybe the individual nodes should store this as a class member, too...
-        name=""
-        do_once = False
+        result=string; name=""
         for i in range(len(self.signature[:-1])):
-            if i == 0:
-                continue
+            if i == 0: continue # it is None or AUTOGENERATED or something
             name+=self.signature[i]
-            prWhite(name)
-            vars = self.mContext.string_variables.get(name, None)
-            if vars is None:
-                prRed("Can't get string variables for node")
-                print (vars, type(vars))
-                prRed (name)
-                prWhite(self.mContext.string_variables.keys())
-                for k in self.mContext.string_variables.keys():
-                    print (name == k)
-                    print (len(name), name,)
-                    print (len(k), k)
-                raise RuntimeError
-                continue
-            elif not vars:
-                prRed(self)
-            for var_name, var_value in vars.items():
-                do_once=True
-                string = string.replace("$"+var_name, var_value)
-        if do_once == False:
-            raise NotImplementedError
-        return string
+        vars = self.mContext.string_variables.get(name, None)
+        if vars is None:
+            raise RuntimeError("Can't get string variables for node")
+        elif not vars:
+            prWhite(f"INFO: No vars available for {self}")
+        var_name_keys = list(vars.keys()); var_name_keys.sort(key=lambda a : -len(a))
+        for var_name in var_name_keys:
+            var_value = vars[var_name]
+            if var_value is None:
+                interface_signature = self.get_interface_signature()
+                interface_node = self.base_tree.parsed_tree.get(interface_signature)
+                from .utilities import set_string_variables_at_execution
+                set_string_variables_at_execution(interface_node, var_name)
+                var_value = vars[var_name]                
+            result = result.replace("$"+var_name, var_value)
+        return result
 
     def evaluate_input(self, input_name, index=0)  -> Any:
         from .node_container_common import trace_single_line
@@ -890,17 +899,9 @@ class MantisNode:
         # this trace() should give a key error if there is a problem
         #  it is NOT handled here because it should NOT happen - so I want the error message.
         trace = trace_single_line(self, input_name, index)
-        try:
-            prop = trace[0][-1].parameters[trace[1].name] #trace[0] = the list of traced nodes; read its parameters
-        except Exception as e:
-            print (trace[0][-1])
-            print (trace[1].name)
-            print (trace[0][-1].parameters.keys())
-            print (trace[0][-1].parameters.values())
-            raise e
-        if isinstance(prop, str) and "$" in prop:
-            print (self, prop)
-            prop = self.apply_string_variables(prop)
+        prop = trace[0][-1].parameters[trace[1].name] #trace[0] = the list of traced nodes; read its parameters
+        # apply the string variables if possible
+        if isinstance(prop, str) and "$" in prop: prop = self.apply_string_variables(prop)
         return prop
 
     def fill_parameters(self, ui_node=None)  -> None:
@@ -943,7 +944,6 @@ class MantisNode:
            nodes are discovered, the method is called by each node in dependency order.
            The first argument MUST be the name of the method as a string.
         """
-        prGreen(self)
         if args[0] == 'call_on_all_ancestors': raise RuntimeError("Very funny!")
         from .utilities import get_all_dependencies
         from collections import deque
@@ -954,7 +954,6 @@ class MantisNode:
         solved = set()
         while can_solve:
             node = can_solve.pop()
-            print(node)
             method = getattr(node, args[0])
             method(*args[0:], **kwargs)
             solved.add(node)
@@ -1051,7 +1050,7 @@ class NodeLink:
     to_node = None
     to_socket = None
 
-    def __init__(self, from_node, from_socket, to_node, to_socket, multi_input_sort_id=0):
+    def __init__(self, from_node, from_socket, to_node, to_socket, multi_input_sort_id=0, sub_sort_id=0):
         if from_node.signature == to_node.signature:
             raise RuntimeError("Cannot connect a node to itself.")
         self.from_node = from_node
@@ -1060,7 +1059,8 @@ class NodeLink:
         self.to_socket = to_socket
         self.from_node.outputs[self.from_socket].links.append(self)
         # it is the responsibility of the node that uses these links to sort them correctly based on the sort_id
-        self.multi_input_sort_id = multi_input_sort_id
+        self.multi_input_sort_id = multi_input_sort_id # this is the sort_id of the link in the UI
+        self.sub_sort_id = sub_sort_id # this is for sorting within a  bundled link (one link in the UI)
         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
@@ -1109,7 +1109,7 @@ class NodeSocket:
         if (traverse_target):
             self.can_traverse = True
 
-    def connect(self, node, socket, sort_id=0):
+    def connect(self, node, socket, sort_id=0, sub_sort_id=0):
         if  (self.is_input):
             to_node   = self.node; from_node = node
             to_socket = self.name; from_socket = socket
@@ -1126,7 +1126,8 @@ class NodeSocket:
                 from_socket,
                 to_node,
                 to_socket,
-                sort_id)
+                sort_id,
+                sub_sort_id)
         return new_link
 
     def set_traverse_target(self, traverse_target):
@@ -1136,7 +1137,7 @@ class NodeSocket:
     def flush_links(self):
         """ Removes dead links from this socket."""
         self.links = [l for l in self.links if l.is_alive]
-        self.links.sort(key=lambda a : -a.multi_input_sort_id)
+        self.links.sort(key=links_sort_key)
         self.is_linked = bool(self.links)
 
     @property

+ 3 - 3
blender_manifest.toml

@@ -2,9 +2,9 @@ schema_version = "1.0.0"
 
 # Example of manifest file for a Blender extension
 # Change the values according to your extension
-id = "mantis"
-version = "0.12.26"
-name = "Mantis"
+id = "mantis_beta"
+version = "0.13.0"
+name = "Mantis (Beta)"
 tagline = "Mantis is a rigging nodes toolkit"
 maintainer = "Nodespaghetti <josephbburg@protonmail.com>"
 # Supported types: "add-on", "theme"

+ 14 - 14
drivers.py

@@ -68,7 +68,7 @@ def CreateDrivers(drivers):
         drv.type = driver["type"]
         if (expr := driver.get("expression")) and isinstance(expr, str):
             drv.expression = expr
-        
+
         fc.extrapolation = "CONSTANT"
         if (extrapolation_mode := driver.get("extrapolation")) in ("CONSTANT", "LINEAR"):
             fc.extrapolation = extrapolation_mode
@@ -76,12 +76,12 @@ def CreateDrivers(drivers):
             prRed(f"Extrapolation Mode in driver has incorrect data: {extrapolation_mode}")
 
         # logic for handling type can go here
-        
+
         # start by clearing
         while (len(drv.variables) > 0):
             v = drv.variables[0]
             dVar = drv.variables.remove(v)
-            
+
         for v in driver["vars"]:
             pose_bone = False
             bone = ''; target2bone = ''
@@ -93,40 +93,42 @@ def CreateDrivers(drivers):
                 vob = v["owner"].id_data
                 bone = v["owner"].name
             #
-            
+
             if "xForm 2" in v.keys() and v["xForm 2"]:
                 if (isinstance(v["xForm 2"], Object)):
                     target2ob = v["xForm 2"]
                 else:
                     target2ob = v["xForm 2"].id_data
                     target2bone = v["xForm 2"].name
-            
+
             dVar = drv.variables.new()
-            
-            
+
+
             dVar.name = v["name"]
             dVar.type = v["type"]
             #for now, assume this is always true:
             #dVar.targets[0].id_type = "OBJECT"
             #it's possible to use other datablocks, but this is not commonly done
             #actually, it looks like Blender figures this out for me.
-            
+
             dVar.targets[0].id = vob
             dVar.targets[0].bone_target = bone
             if len(dVar.targets) > 1:
                 dVar.targets[1].id = target2ob
                 dVar.targets[1].bone_target = target2bone
-            
+
             if (dVar.type == "TRANSFORMS"):
                 dVar.targets[0].transform_space = v["space"]
                 dVar.targets[0].transform_type = v["channel"]
             if (dVar.type == 'SINGLE_PROP'):
                 if pose_bone:
                     stub = "pose.bones[\""+v["owner"].name+"\"]"
+                    dVar.targets[0].data_path = stub + brackets(v["prop"])
                     if (hasattr( v["owner"], v["prop"] )):
                         dVar.targets[0].data_path = stub + "."+ (v["prop"])
-                    else:
-                        dVar.targets[0].data_path = stub + brackets(v["prop"])
+                    # else: # the property may be added later.
+                    # TODO BUG I want a guarantee that this property is already there.
+
                 else:
                     if (hasattr( v["owner"], v["prop"] )):
                         dVar.targets[0].data_path = (v["prop"])
@@ -136,7 +138,7 @@ def CreateDrivers(drivers):
         kp = fc.keyframe_points
         for key in driver["keys"]:
             k = kp.insert(frame=key["co"][0], value = key["co"][1],)
-            
+
             k.interpolation     = key["interpolation"]
             if (key["interpolation"] == 'BEZIER'):
                 k.handle_left_type  = key["handle_left_type" ]
@@ -146,5 +148,3 @@ def CreateDrivers(drivers):
                 if (k.handle_right_type in ("ALIGNED", "VECTOR", "FREE")):
                     k.handle_right      = (k.co[0] + key["handle_right"][0], k.co[1] + key["handle_right"][1])
             k.type = key["type"]
-      
-      

+ 43 - 42
i_o.py

@@ -49,7 +49,7 @@ prop_ignore = [ "__dict__", "__doc__", "__module__", "__weakref__",# "name",
                 # these are in Bone
                 "socket_count", "display_bb_settings", "display_def_settings",
                 "display_ik_settings", "display_vp_settings",
-                ] 
+                ]
 # 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
@@ -127,7 +127,7 @@ def fix_custom_parameter(n, property_definition, ):
     if n.bl_idname in ['xFormNullNode', 'xFormBoneNode', '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)
@@ -135,7 +135,7 @@ def fix_custom_parameter(n, property_definition, ):
             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"]
@@ -173,7 +173,7 @@ def fix_custom_parameter(n, property_definition, ):
 # to see if a dependency is created by node connections.
 # TODO it remains to be seen if that is even a desirable behaviour.
 def scan_tree_for_objects(base_tree, current_tree):
-    # this should work 
+    # this should work
     armatures, curves    = set(), set()
     if current_tree == base_tree:
         scan_tree_dependencies(base_tree, curves, armatures,)
@@ -310,7 +310,7 @@ def get_curve_for_pack(object):
 
 def matrix_as_tuple(matrix):
     return ( matrix[0][0], matrix[0][1], matrix[0][2], matrix[0][3],
-             matrix[1][0], matrix[1][1], matrix[1][2], matrix[1][3], 
+             matrix[1][0], matrix[1][1], matrix[1][2], matrix[1][3],
              matrix[2][0], matrix[2][1], matrix[2][2], matrix[2][3],
              matrix[3][0], matrix[3][1], matrix[3][2], matrix[3][3], )
 
@@ -322,7 +322,7 @@ class metabone_data:
     matrix                : tuple[float] = field(default=()),
     parent                : str = field(default=''),
     length                : float = field(default=-1.0),
-    children              : list[str] = field(default_factory=[]), 
+    children              : list[str] = field(default_factory=[]),
 # keep it really simple for now. I'll add BBone and envelope later on
 # when I make them accessible from the meta-rig
 
@@ -361,7 +361,7 @@ def get_socket_data(socket, ignore_if_default=False):
     socket_data["bl_idname"] = socket.bl_idname
     socket_data["is_output"] = socket.is_output
     socket_data["is_multi_input"] = socket.is_multi_input
-    
+
     # here is where we'll handle a socket_data'socket special data
     if socket.bl_idname == "EnumMetaBoneSocket":
         socket_data["bone"] = socket.bone
@@ -454,7 +454,7 @@ def get_tree_data(tree):
     tree_info = {}
     for propname  in dir(tree):
         # if getattr(tree, propname):
-        #     pass  
+        #     pass
         if (propname in prop_ignore_tree) or ( callable(getattr(tree, propname)) ):
             continue
         v = getattr(tree, propname)
@@ -489,7 +489,7 @@ def get_interface_data(tree, tree_in_out):
                 bl_socket_idname = 'VectorSocket'
             elif bl_socket_idname == 'NodeSocketInt':
                 bl_socket_idname = 'IntSocket'
-            
+
             try:
                 socket_class = getattr(socket_definitions, bl_socket_idname)
             except AttributeError: # sometimes the class doesn't work.
@@ -524,7 +524,7 @@ def get_interface_data(tree, tree_in_out):
             else:
                 sock_data["socket_type"] = sock.bl_socket_idname
             tree_in_out[sock.identifier] = sock_data
-            
+
 
 def export_to_json(trees, base_tree=None, path="", write_file=True, only_selected=False, ):
     export_data = {}
@@ -532,7 +532,7 @@ def export_to_json(trees, base_tree=None, path="", write_file=True, only_selecte
         current_tree_is_base_tree = False
         if tree is trees[-1]:
             current_tree_is_base_tree = True
-        
+
         tree_info, tree_in_out = {}, {}
         tree_info = get_tree_data(tree)
         curves, metarig_data = {}, {}
@@ -558,7 +558,7 @@ def export_to_json(trees, base_tree=None, path="", write_file=True, only_selecte
             if only_selected and node.select == False:
                 continue
             nodes[node.name] = get_node_data(node)
-            
+
         links = []
         in_sockets, out_sockets = {}, {}
         unique_sockets_from, unique_sockets_to = {}, {}
@@ -587,7 +587,7 @@ def export_to_json(trees, base_tree=None, path="", write_file=True, only_selecte
             else:
                 problem = link.to_node.name + "::" + link.to_socket.name
                 raise RuntimeError(wrapRed(f"Error saving index of socket: {problem}"))
-            
+
             if current_tree_is_base_tree:
                 if (only_selected and link.from_node.select) and (not link.to_node.select):
                     # handle an output in the tree
@@ -670,7 +670,7 @@ def export_to_json(trees, base_tree=None, path="", write_file=True, only_selecte
                         sock_data["in_out"]="INPUT"
                         sock_data["index"]=in_sock["index"]
 
-                        
+
                         tree_in_out[sock_name] = sock_data
 
                     from_node_name=in_node.get("name")
@@ -690,8 +690,8 @@ def export_to_json(trees, base_tree=None, path="", write_file=True, only_selecte
                            to_input_index,
                            from_socket_name,
                            to_socket_name) ) # 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():
@@ -714,7 +714,7 @@ def export_to_json(trees, base_tree=None, path="", write_file=True, only_selecte
             nodes["MANTIS_AUTOGEN_GROUP_OUTPUT"]=out_node
 
         export_data[tree.name] = (tree_info, tree_in_out, nodes, links, curves, metarig_data,) # f_curves)
-    
+
     return export_data
 
 def write_json_data(data, path):
@@ -722,7 +722,7 @@ def write_json_data(data, path):
     with open(path, "w") as file:
         print(wrapWhite("Writing mantis tree data to: "), wrapGreen(file.name))
         file.write( json.dumps(data, indent = 4) )
-        
+
 def get_link_sockets(link, tree, tree_socket_id_map):
     from_node_name = link[0]
     from_socket_id = link[1]
@@ -759,7 +759,7 @@ def get_link_sockets(link, tree, tree_socket_id_map):
         id1 = tree_socket_id_map.get(from_socket_id)
     for from_sock in from_node.outputs:
         if from_sock.identifier == id1: break
-    else: 
+    else:
         from_sock = None
 
     id2 = to_socket_id
@@ -908,7 +908,7 @@ def do_import_from_file(filepath, context):
     for tree in all_trees:
         tree.is_exporting = True
         tree.do_live_update = False
-    
+
     def do_cleanup(tree):
         tree.is_exporting = False
         tree.do_live_update = True
@@ -954,6 +954,8 @@ def do_import(data, context, search_multi_files=False, filepath='', skip_existin
         tree_info = tree_data[0]
         tree_in_out = tree_data[1]
 
+        print (tree_info)
+
 
         # TODO: IMPORT THIS DATA HERE!!!
         try:
@@ -962,14 +964,12 @@ def do_import(data, context, search_multi_files=False, filepath='', skip_existin
         except IndexError: # shouldn't happen but maybe someone has an old file
             curves = {}
             armatures = {}
-        
+
         for curve_name, curve_data in curves.items():
             from .utilities import import_curve_data_to_object, import_metarig_data
             import_curve_data_to_object(curve_name, curve_data)
             for armature_name, armature_data in armatures.items():
                 import_metarig_data(armature_data)
-        
-
 
         # need to make a new tree; first, try to get it:
         tree = bpy.data.node_groups.get(tree_info["name"])
@@ -978,21 +978,22 @@ def do_import(data, context, search_multi_files=False, filepath='', skip_existin
             continue # already done here because the tree already exists.
         if tree is None:
             tree = bpy.data.node_groups.new(tree_info["name"], tree_info["bl_idname"])
+        tree.mantis_version = tree_info['mantis_version']
         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 = {}
         tree_sock_id_maps[tree.name] = tree_sock_id_map
-        
+
         interface_parent_me = {}
 
         # I need to guarantee that the interface items are in the right order.
         interface_sockets = [] # I'll just sort them afterwards so I hold them here.
         default_position=0 # We'll use this if the position attribute is not set when e.g. making groups.
-        
+
 
         for s_name, s_props in tree_in_out.items():
             if s_props["item_type"] == 'SOCKET':
@@ -1016,14 +1017,14 @@ def do_import(data, context, search_multi_files=False, filepath='', skip_existin
                     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,
                                     )
-        
+
         # BUG this was screwing up the order of things
         # so I want to fix it and re-enable it
         if True:
@@ -1031,7 +1032,7 @@ def do_import(data, context, search_multi_files=False, filepath='', skip_existin
             interface_sockets.sort(key=lambda a : a[1])
             for (socket, position) in interface_sockets:
                 tree.interface.move(socket, position)
-        
+
     # Now go and do nodes and links
     for tree_name, tree_data in data.items():
         if tree_name in skip_trees:
@@ -1041,7 +1042,7 @@ def do_import(data, context, search_multi_files=False, filepath='', skip_existin
         tree_info = tree_data[0]
         nodes = tree_data[2]
         links = tree_data[3]
-        
+
         parent_me = []
 
         tree = bpy.data.node_groups.get(tree_info["name"])
@@ -1051,9 +1052,9 @@ def do_import(data, context, search_multi_files=False, filepath='', skip_existin
         trees.append(tree)
 
         tree_sock_id_map=tree_sock_id_maps[tree.name]
-        
+
         interface_parent_me = {}
-        
+
 #        from mantis.utilities import prRed, prWhite, prOrange, prGreen
         for name, propslist in nodes.items():
             bl_idname = propslist["bl_idname"]
@@ -1089,7 +1090,7 @@ def do_import(data, context, search_multi_files=False, filepath='', skip_existin
                                 "SchemaIncomingConnection",]:
                 n.update()
 
-            
+
             if sub_tree := propslist.get("node_tree"):
                 # now that I am doing multi-file exports, this is tricky
                 # we need to see if the tree exists and if not, recurse
@@ -1127,13 +1128,13 @@ def do_import(data, context, search_multi_files=False, filepath='', skip_existin
                          "sockets",
                          "inputs",
                          "outputs",
-                         "warning_propagation", 
+                         "warning_propagation",
                          "socket_idname"]:
                     continue
                 # will throw AttributeError if read-only
                 # will throw TypeError if wrong type...
                 if n.bl_idname == "NodeFrame" and p in ["width, height, location"]:
-                    continue 
+                    continue
                 if version  < (4,4,0) and p == 'location_absolute':
                     continue
                 if p == "parent" and v is not None:
@@ -1144,7 +1145,7 @@ def do_import(data, context, search_multi_files=False, filepath='', skip_existin
                 except Exception as e:
                     prRed (p)
                     raise e
-                
+
 
         for l in links:
             from_socket_name = l[6]
@@ -1168,16 +1169,16 @@ def do_import(data, context, search_multi_files=False, filepath='', skip_existin
                     raise RuntimeError
                 else:
                     prRed(f"Failed to add link in {tree.name}: {name1}:{from_socket_name}, {name2}:{to_socket_name}")
-            
+
             # 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
-        
+
 
 def export_multi_file(trees : list,  base_tree, filepath : str, base_name :str) -> None:
     for t in trees:
@@ -1191,7 +1192,7 @@ def export_multi_file(trees : list,  base_tree, filepath : str, base_name :str)
                         clean_name(t.name)+'.rig'))
         write_json_data(export_data, os_path.join(directory,
                         clean_name(t.name)+'.rig'))
-        
+
 
 import bpy
 
@@ -1228,7 +1229,7 @@ class MantisExportNodeTreeSaveAs(Operator, ExportHelper):
         # 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]
@@ -1396,4 +1397,4 @@ class MantisReloadNodeTree(Operator):
 # 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
+#  - it would be nice to have a library of these that can be imported alongside the mantis graph

+ 21 - 12
internal_containers.py

@@ -1,6 +1,6 @@
 from .node_container_common import *
 from bpy.types import Node
-from .base_definitions import MantisNode
+from .base_definitions import MantisNode, MantisSocketTemplate
 from uuid import uuid4
 
 class DummyNode(MantisNode):
@@ -11,7 +11,6 @@ class DummyNode(MantisNode):
         self.prepared = True
         self.uuid = uuid4()
         self.solver = None
-        self.did_set_variables = False
         if prototype:
             if prototype.bl_idname in ["MantisSchemaGroup"]:
                 self.node_type = 'DUMMY_SCHEMA'
@@ -33,14 +32,6 @@ class DummyNode(MantisNode):
         # this is ugly and I hate it.
         self.execution_prepared=True # in case it gets left behind in the tree as a dependency
 
-    def bPrepare(self, bContext=None):
-        from .utilities import set_string_variables_during_exec
-        set_string_variables_during_exec(self, self.mContext)
-        self.did_set_variables = True # I just need to know if this is getting them all
-
-    def __del__(self):
-        print (self, self.did_set_variables)
-
 class NoOpNode(MantisNode):
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree)
@@ -49,8 +40,8 @@ class NoOpNode(MantisNode):
         self.init_parameters()
         self.set_traverse([("Input", "Output")])
         self.node_type = 'UTILITY'
-        self.prepared = True
-        self.executed = True
+        self.prepared, self.executed = True, True
+        self.execution_prepared=True
     # this node is useful for me to insert in the tree and use for debugging especially connections.
 
 class AutoGenNode(MantisNode):
@@ -58,7 +49,25 @@ class AutoGenNode(MantisNode):
         super().__init__(signature, base_tree)
         self.node_type = 'UTILITY'
         self.prepared, self.executed = True, True
+        self.execution_prepared=True
 
     def reset_execution(self):
         super().reset_execution()
         self.prepared, self.executed = True, True
+
+
+# The Group Interface node is responsible for gathering node connections
+#   going in or out of the group and connecting back out the other side
+# this is also where caching and overlays live
+class GroupInterface(MantisNode):
+    def __init__(self, signature, base_tree, prototype, in_out):
+        super().__init__(signature, base_tree)
+        self.node_type = 'UTILITY'
+        self.prepared, self.executed = True, True; sockets = []
+        self.in_out = in_out
+        # init the sockets based on in/out, then set up traversal
+        collection = prototype.inputs if in_out == 'INPUT' else prototype.outputs
+        for socket in collection: sockets.append(socket.identifier)
+        self.inputs.init_sockets(sockets); self.outputs.init_sockets(sockets)
+        for socket in self.inputs.keys(): self.set_traverse( [(socket, socket)] )
+        self.execution_prepared=True

+ 46 - 35
link_nodes.py

@@ -53,7 +53,10 @@ class MantisLinkNode(MantisNode):
         if ('Target' in input_name) and input_name not in  ["Target Space", "Use Target Z"]:
             socket = self.inputs.get(input_name)
             if socket.is_linked:
-                return socket.links[0].from_node
+                node_line, _last_socket = trace_single_line(self, input_name, index)
+                for other_node in node_line:
+                    if other_node.node_type == 'XFORM':
+                        return other_node
             return None
 
         else:
@@ -61,28 +64,46 @@ class MantisLinkNode(MantisNode):
 
     def gen_property_socket_map(self) -> dict:
         props_sockets = super().gen_property_socket_map()
-        if (os := self.inputs.get("Owner Space")) and os.is_connected and os.links[0].from_node.node_type == 'XFORM':
-            del props_sockets['owner_space']
-        if (ts := self.inputs.get("Target Space")) and ts.is_connected and ts.links[0].from_node.node_type == 'XFORM':
-            del props_sockets['target_space']
+        if (os := self.inputs.get("Owner Space")) and os.is_connected:
+            node_line, _last_socket = trace_single_line(self, "Owner Space")
+            for other_node in node_line:
+                if other_node.node_type == 'XFORM':
+                    del props_sockets['owner_space']; break
+        if (ts := self.inputs.get("Target Space")) and ts.is_connected:
+            node_line, _last_socket = trace_single_line(self, "Target Space")
+            for other_node in node_line:
+                if other_node.node_type == 'XFORM':
+                    del props_sockets['target_space']; break
         return props_sockets
 
     def set_custom_space(self):
         for c in self.bObject:
-            if (os := self.inputs.get("Owner Space")) and os.is_connected and os.links[0].from_node.node_type == 'XFORM':
-                c.owner_space='CUSTOM'
-                xf = self.inputs["Owner Space"].links[0].from_node.bGetObject(mode="OBJECT")
-                if isinstance(xf, Bone):
-                    c.space_object=self.inputs["Owner Space"].links[0].from_node.bGetParentArmature(); c.space_subtarget=xf.name
-                else:
-                    c.space_object=xf
-            if ts := self.inputs.get("Target_Space") and ts.is_connected and ts.links[0].from_node.node_type == 'XFORM':
-                c.target_space='CUSTOM'
-                xf = self.inputs["Target_Space Space"].links[0].from_node.bGetObject(mode="OBJECT")
-                if isinstance(xf, Bone):
-                    c.space_object=self.inputs["Target_Space Space"].links[0].from_node.bGetParentArmature(); c.space_subtarget=xf.name
-                else:
-                    c.space_object=xf
+            if (os := self.inputs.get("Owner Space")) and os.is_connected:
+                node_line, _last_socket = trace_single_line(self, "Owner Space")
+                owner_space_target = None
+                for other_node in node_line:
+                    if other_node.node_type == 'XFORM':
+                        owner_space_target = other_node; break
+                if owner_space_target:
+                    c.owner_space='CUSTOM'
+                    xf = owner_space_target.bGetObject(mode="OBJECT")
+                    if isinstance(xf, Bone):
+                        c.space_object=owner_space_target.bGetParentArmature(); c.space_subtarget=xf.name
+                    else:
+                        c.space_object=xf
+            if (ts := self.inputs.get("Target Space")) and ts.is_connected:
+                node_line, _last_socket = trace_single_line(self, "Target Space")
+                target_space_target = None
+                for other_node in node_line:
+                    if other_node.node_type == 'XFORM':
+                        target_space_target = other_node; break
+                if target_space_target:
+                    c.target_space='CUSTOM'
+                    xf = target_space_target.bGetObject(mode="OBJECT")
+                    if isinstance(xf, Bone):
+                        c.space_object=target_space_target.bGetParentArmature(); c.space_subtarget=xf.name
+                    else:
+                        c.space_object=xf
 
     def GetxForm(nc, output_name="Output Relationship"):
         break_condition= lambda node : node.node_type=='XFORM'
@@ -207,20 +228,7 @@ class LinkCopyScale(MantisLinkNode):
             if constraint_name := self.evaluate_input("Name"):
                 c.name = constraint_name
             self.bObject.append(c)
-            if self.inputs["Owner Space"].is_connected and self.inputs["Owner Space"].links[0].from_node.node_type == 'XFORM':
-                c.owner_space='CUSTOM'
-                xf = self.inputs["Owner Space"].links[0].from_node.bGetObject(mode="OBJECT")
-                if isinstance(xf, Bone):
-                    c.space_object=self.inputs["Owner Space"].links[0].from_node.bGetParentArmature(); c.space_subtarget=xf.name
-                else:
-                    c.space_object=xf
-            if self.inputs["Target Space"].is_connected and self.inputs["Target Space"].links[0].from_node.node_type == 'XFORM':
-                c.target_space='CUSTOM'
-                xf = self.inputs["Target Space"].links[0].from_node.bGetObject(mode="OBJECT")
-                if isinstance(xf, Bone):
-                    c.space_object=self.inputs["Owner Space"].links[0].from_node.bGetParentArmature(); c.space_subtarget=xf.name
-                else:
-                    c.space_object=xf
+            self.set_custom_space()
             props_sockets = self.gen_property_socket_map()
             evaluate_sockets(self, c, props_sockets)
         self.executed = True
@@ -806,7 +814,6 @@ class LinkArmature(MantisLinkNode):
                     weight_value=0
                 targets_weights[i]=weight_value
                 props_sockets["targets[%d].weight" % i] = (weight_input_name, 0)
-                # targets_weights.append({"weight":(weight_input_name, 0)})
             evaluate_sockets(self, c, props_sockets)
             for target, value in targets_weights.items():
                 c.targets[target].weight=value
@@ -831,8 +838,12 @@ class LinkSplineIK(MantisLinkNode):
             c = xf.bGetObject().constraints.new('SPLINE_IK')
             # set the spline - we need to get the right one
             spline_index = self.evaluate_input("Spline Index")
+            proto_curve = None
+            node_line, _last_socket = trace_single_line(self, "Target")
+            for other_node in node_line: # trace and get the input
+                if other_node.node_type == 'XFORM':
+                    proto_curve = other_node.bGetObject(); break
             from .utilities import get_extracted_spline_object
-            proto_curve = self.inputs['Target'].links[0].from_node.bGetObject()
             curve = get_extracted_spline_object(proto_curve, spline_index, self.mContext)
             # link it to the view layer
             if (curve.name not in bContext.view_layer.active_layer_collection.collection.objects):

+ 72 - 75
misc_nodes.py

@@ -145,22 +145,16 @@ def array_choose_relink(node, indices, array_input, output, ):
     """
         Used to choose the correct link to send out of an array-choose node.
     """
-    prGreen(node)
-    for l in node.inputs[array_input].links:
-        print(l)
     keep_links = []
     for index in indices:
-        prOrange(index)
         l = node.inputs[array_input].links[index]
         keep_links.append(l)
     for link in node.outputs[output].links:
-        prOrange(link)
         to_node = link.to_node
         for l in keep_links:
             new_link = l.from_node.outputs[l.from_socket].connect(to_node, link.to_socket)
             array_link_init_hierarchy(new_link)
             node.rerouted.append(new_link) # so I can access this in Schema Solve
-            prPurple(new_link)
         link.die()
 
 
@@ -217,7 +211,7 @@ class SimpleInputNode(MantisNode):
 
 class InputFloat(SimpleInputNode):
     '''A node representing float input'''
-    
+
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree)
         outputs = ["Float Input"]
@@ -226,16 +220,16 @@ class InputFloat(SimpleInputNode):
 
 class InputIntNode(SimpleInputNode):
     '''A node representing integer input'''
-    
+
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree)
         outputs = ["Integer"]
         self.outputs.init_sockets(outputs)
         self.init_parameters()
-    
+
 class InputVector(SimpleInputNode):
     '''A node representing vector input'''
-    
+
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree)
         outputs = [""]
@@ -244,7 +238,7 @@ class InputVector(SimpleInputNode):
 
 class InputBoolean(SimpleInputNode):
     '''A node representing boolean input'''
-    
+
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree)
         outputs = [""]
@@ -258,31 +252,31 @@ class InputBooleanThreeTuple(SimpleInputNode):
         outputs = [""]
         self.outputs.init_sockets(outputs)
         self.init_parameters()
-    
+
 class InputRotationOrder(SimpleInputNode):
     '''A node representing string input for rotation order'''
-        
+
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree)
         outputs = [""]
         self.outputs.init_sockets(outputs)
         self.init_parameters()
-    
+
 class InputTransformSpace(SimpleInputNode):
     '''A node representing string input for transform space'''
-        
+
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree)
         outputs = [""]
         self.outputs.init_sockets(outputs)
         self.init_parameters()
-        
+
     def evaluate_input(self, input_name):
         return self.parameters[""]
-    
+
 class InputString(SimpleInputNode):
     '''A node representing string input'''
-        
+
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree)
         outputs = [""]
@@ -291,7 +285,7 @@ class InputString(SimpleInputNode):
 
 class InputMatrix(SimpleInputNode):
     '''A node representing axis-angle quaternion input'''
-        
+
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree)
         outputs  = ["Matrix",]
@@ -300,7 +294,7 @@ class InputMatrix(SimpleInputNode):
 
 class InputThemeBoneColorSets(SimpleInputNode):
     '''A node representing the theme's colors'''
-        
+
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree)
         from .base_definitions import MantisSocketTemplate
@@ -325,7 +319,7 @@ class InputColorSetPallete(SimpleInputNode):
     '''A node representing the theme's colors'''
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree)
-    
+
     def fill_parameters(self, ui_node=None):
         if not ui_node:
             from .utilities import get_node_prototype
@@ -403,7 +397,7 @@ class UtilityMatrixFromCurve(MantisNode):
         self.parameters["Matrix"] = mat
         self.prepared = True
         self.executed = True
-    
+
     def bFinalize(self, bContext=None):
         cleanup_curve(self.evaluate_input("Curve"), self.base_tree.execution_id)
 
@@ -437,7 +431,7 @@ class UtilityPointFromCurve(MantisNode):
             p = data[spline_index][0][0] - curve.location
         self.parameters["Point"] = p
         self.prepared, self.executed = True, True
-    
+
     def bFinalize(self, bContext=None):
         cleanup_curve(self.evaluate_input("Curve"), self.base_tree.execution_id)
 
@@ -500,7 +494,7 @@ class UtilityMatricesFromCurve(MantisNode):
         self.prepared = True
         self.executed = True
         # prGreen(f"Matrices from curves took {time.time() - start_time} seconds.")
-    
+
     def bFinalize(self, bContext=None):
         import bpy
         curve_name = self.evaluate_input("Curve")
@@ -524,7 +518,7 @@ class UtilityNumberOfCurveSegments(MantisNode):
         self.outputs.init_sockets(outputs)
         self.init_parameters()
         self.node_type = "UTILITY"
-    
+
     def bPrepare(self, bContext = None,):
         curve_name = self.evaluate_input("Curve")
         curve = bpy_object_get_guarded( curve_name, self)
@@ -541,7 +535,7 @@ class UtilityNumberOfSplines(MantisNode):
         super().__init__(signature, base_tree, NumberOfSplinesSockets)
         self.init_parameters()
         self.node_type = "UTILITY"
-    
+
     def bPrepare(self, bContext = None,):
         curve_name = self.evaluate_input("Curve")
         curve = bpy_object_get_guarded( curve_name, self)
@@ -602,7 +596,7 @@ class UtilityMatrixFromCurveSegment(MantisNode):
             m.translation = head - curve.location
             self.parameters["Matrix"] = m
         self.prepared, self.executed = True, True
-    
+
     def bFinalize(self, bContext=None):
         cleanup_curve(self.evaluate_input("Curve"), self.base_tree.execution_id)
 
@@ -611,7 +605,7 @@ class UtilityGetCurvePoint(MantisNode):
         super().__init__(signature, base_tree, GetCurvePointSockets)
         self.init_parameters()
         self.node_type = "UTILITY"
-    
+
     def bPrepare(self, bContext=None):
         import bpy
         curve_name = self.evaluate_input("Curve")
@@ -636,7 +630,7 @@ class UtilityGetNearestFactorOnCurve(MantisNode):
         super().__init__(signature, base_tree, GetNearestFactorOnCurveSockets)
         self.init_parameters()
         self.node_type = "UTILITY"
-    
+
     def bPrepare(self, bContext = None,):
         import bpy
         curve_name = self.evaluate_input("Curve")
@@ -770,14 +764,14 @@ class UtilityMetaRig(MantisNode):
         import bpy
         from mathutils import Matrix
         m = Matrix.Identity(4)
-        
+
         meta_rig  = self.evaluate_input("Meta-Armature")
         if meta_rig is None:
             raise RuntimeError("Invalid input for Meta-Armature.")
         meta_bone = self.evaluate_input("Meta-Bone")
         if meta_rig is None or meta_bone is None:
             raise RuntimeError("Invalid input for Meta-Bone.")
-        
+
         if meta_rig:
             if ( armOb := bpy.data.objects.get(meta_rig) ):
                 m = armOb.matrix_world
@@ -794,7 +788,7 @@ class UtilityMetaRig(MantisNode):
                 #     prRed("no bone for MetaRig node ", self)
         else:
             raise RuntimeError(wrapRed(f"No meta-rig input for MetaRig node {self}"))
-        
+
         self.parameters["Matrix"] = m
         self.prepared = True
         self.executed = True
@@ -820,7 +814,7 @@ class UtilityBoneProperties(SimpleInputNode):
 
     def fill_parameters(self, prototype=None):
         return
-        
+
 # TODO this should probably be moved to Links
 class UtilityDriverVariable(MantisNode):
     '''A node representing an armature object'''
@@ -844,23 +838,24 @@ class UtilityDriverVariable(MantisNode):
         self.init_parameters()
         self.node_type = "DRIVER" # MUST be run in Pose mode
         self.prepared = True
-    
+
     def reset_execution(self):
         super().reset_execution()
         # clear this to ensure there are no stale reference pointers
         self.parameters["Driver Variable"] = None
         self.prepared=True
-        
+
     def evaluate_input(self, input_name):
         if input_name == 'Property':
             if self.inputs.get('Property'):
                 if self.inputs['Property'].is_linked:
-                # get the name instead...
                     trace = trace_single_line(self, input_name)
-                    return trace[1].name # the name of the socket
+                    # CANNOT UNDERSTATE HOW CRITICAL THIS CHECK IS
+                    if trace[0][-1].node_type == 'XFORM':
+                        return trace[1].name # the name of the socket
             return self.parameters["Property"]
         return super().evaluate_input(input_name)
-        
+
     def GetxForm(self, index=1):
         trace = trace_single_line(self, "xForm 1" if index == 1 else "xForm 2")
         for node in trace[0]:
@@ -903,7 +898,7 @@ class UtilityDriverVariable(MantisNode):
             if self.evaluate_input("Property") == 'scale_average':
                 dVarChannel = "SCALE_AVG"
         if dVarChannel: v_type = "TRANSFORMS"
-        
+
         my_var = {
             "owner"         : xForm1, # will be filled in by Driver
             "prop"          : self.evaluate_input("Property"), # will be filled in by Driver
@@ -913,10 +908,11 @@ class UtilityDriverVariable(MantisNode):
             "xForm 1"       : xForm1,#self.GetxForm(index = 1),
             "xForm 2"       : xForm2,#self.GetxForm(index = 2),
             "channel"       : dVarChannel,}
-        
+
         self.parameters["Driver Variable"] = my_var
+        print (my_var['prop'])
         self.executed = True
-            
+
 class UtilityKeyframe(MantisNode):
     '''A node representing a keyframe for a F-Curve'''
 
@@ -964,7 +960,7 @@ class UtilityFCurve(MantisNode):
         self.node_type = "UTILITY"
         setup_custom_props(self)
         self.prepared = True
-    
+
     def reset_execution(self):
         super().reset_execution()
         self.prepared=True
@@ -990,7 +986,7 @@ class UtilityFCurve(MantisNode):
         keys.append(extrap_mode)
         self.parameters["fCurve"] = keys
         self.executed = True
-#TODO make the fCurve data a data class instead of a dict 
+#TODO make the fCurve data a data class instead of a dict
 
 class UtilityDriver(MantisNode):
     '''A node representing an armature object'''
@@ -1007,7 +1003,7 @@ class UtilityDriver(MantisNode):
         ]
         from .drivers import MantisDriver
         additional_parameters = {
-          "Driver":MantisDriver(), 
+          "Driver":MantisDriver(),
         }
         self.inputs.init_sockets(inputs)
         self.outputs.init_sockets(outputs)
@@ -1015,13 +1011,13 @@ class UtilityDriver(MantisNode):
         self.node_type = "DRIVER" # MUST be run in Pose mode
         setup_custom_props(self)
         self.prepared = True
-    
+
     def reset_execution(self):
         super().reset_execution()
         from .drivers import MantisDriver
         self.parameters["Driver"]=MantisDriver()
         self.prepared=True
-    
+
     def bRelationshipPass(self, bContext = None,):
         prepare_parameters(self)
         from .drivers import MantisDriver
@@ -1049,9 +1045,9 @@ class UtilityDriver(MantisNode):
                      "vars"          :  my_vars,
                      "keys"          :  keys[:-1],
                      "extrapolation" :  keys[-1] }
-        
+
         my_driver = MantisDriver(my_driver)
-        
+
         self.parameters["Driver"].update(my_driver)
         print("Initializing driver %s " % (wrapPurple(self.__repr__())) )
         self.executed = True
@@ -1071,7 +1067,7 @@ class UtilitySwitch(MantisNode):
         ]
         from .drivers import MantisDriver
         additional_parameters = {
-          "Driver":MantisDriver(), 
+          "Driver":MantisDriver(),
         }
         self.inputs.init_sockets(inputs)
         self.outputs.init_sockets(outputs)
@@ -1093,7 +1089,7 @@ class UtilitySwitch(MantisNode):
             if (node.__class__ in [xFormArmature, xFormBone]):
                 return node #this will fetch the first one, that's good!
         return None
-    
+
     def reset_execution(self):
         super().reset_execution()
         from .drivers import MantisDriver
@@ -1104,12 +1100,12 @@ class UtilitySwitch(MantisNode):
         #prepare_parameters(self)
         #prPurple ("Executing Switch Node")
         xForm = self.GetxForm()
-        if xForm : xForm = xForm.bGetObject() 
+        if xForm : xForm = xForm.bGetObject()
         if not xForm:
             raise RuntimeError("Could not evaluate xForm for %s" % self)
         from .drivers import MantisDriver
         my_driver ={ "owner" : None,
-                     "prop"  : None, # will be filled out in the node that uses the driver 
+                     "prop"  : None, # will be filled out in the node that uses the driver
                      "ind"   : -1, # same here
                      "type"  : "SCRIPTED",
                      "vars"  : [ { "owner" : xForm,
@@ -1124,13 +1120,13 @@ class UtilitySwitch(MantisNode):
                                    "type":"KEYFRAME",},],
                       "extrapolation": 'CONSTANT', }
         my_driver   ["expression"] = "a"
-        
+
         my_driver = MantisDriver(my_driver)
     # this makes it so I can check for type later!
-        
+
         if self.evaluate_input("Invert Switch") == True:
             my_driver   ["expression"] = "1 - a"
-        
+
         # this way, regardless of what order things are handled, the
         #  driver is sent to the next node.
         # In the case of some drivers, the parameter may be sent out
@@ -1159,7 +1155,7 @@ class UtilityCombineThreeBool(MantisNode):
         self.outputs.init_sockets(outputs)
         self.init_parameters()
         self.node_type = "UTILITY"
-    
+
     def reset_execution(self): # need to make sure any references are deleted
         super().reset_execution() # so we prepare the node again to reset them
         if self.parameters["Three-Bool"] is not None:
@@ -1201,7 +1197,7 @@ class UtilityCombineVector(MantisNode):
             for param in self.parameters["Vector"]:
                 if isinstance(param, dict):
                     self.prepared=False; break
-    
+
     def bPrepare(self, bContext = None,):
         #prPurple("Executing CombineVector Node")
         prepare_parameters(self)
@@ -1210,7 +1206,7 @@ class UtilityCombineVector(MantisNode):
           self.evaluate_input("Y"),
           self.evaluate_input("Z"), )
         self.prepared, self.executed = True, True
-  
+
 class UtilitySeparateVector(MantisNode):
     '''A node for separating a vector into three floats'''
 
@@ -1251,7 +1247,7 @@ class UtilityCatStrings(MantisNode):
         self.outputs.init_sockets(outputs)
         self.init_parameters()
         self.node_type = "UTILITY"
-    
+
     def bPrepare(self, bContext = None,):
         self.parameters["OutputString"] = self.evaluate_input("String_1")+self.evaluate_input("String_2")
         self.prepared, self.executed = True, True
@@ -1263,7 +1259,7 @@ class InputWidget(MantisNode):
         super().__init__(signature, base_tree, InputWidgetSockets)
         self.init_parameters()
         self.node_type = "XFORM"
-    
+
     def reset_execution(self):
         super().reset_execution()
         self.prepared=False
@@ -1338,10 +1334,10 @@ class InputWidget(MantisNode):
             flip_modifier["Socket_3"]=axes_flipped[1]
             flip_modifier["Socket_4"]=axes_flipped[2]
         self.prepared, self.executed = True, True
-    
+
     def bGetObject(self, mode=''):
         return self.bObject
-    
+
 # TODO move this to the Xform file
 class InputExistingGeometryObject(MantisNode):
     '''A node representing an existing object'''
@@ -1358,7 +1354,7 @@ class InputExistingGeometryObject(MantisNode):
         self.outputs.init_sockets(outputs)
         self.init_parameters()
         self.node_type = "XFORM"
-    
+
     def reset_execution(self):
         super().reset_execution()
         self.prepared=False
@@ -1372,7 +1368,7 @@ class InputExistingGeometryObject(MantisNode):
             prRed(f"No object found with name {name} in {self}")
         self.bObject=ob
         self.prepared, self.executed = True, True
-    
+
     def bGetObject(self, mode=''):
         return self.bObject
 
@@ -1418,7 +1414,7 @@ class UtilityDeclareCollections(MantisNode):
     def reset_execution(self):
         super().reset_execution()
         self.prepared, self.executed = True, True
-    
+
     def fill_parameters(self, ui_node=None):
         if ui_node is None:
             from .utilities import get_node_prototype
@@ -1428,7 +1424,7 @@ class UtilityDeclareCollections(MantisNode):
         for out in ui_node.outputs:
             if not (out.name in self.outputs.keys()) :
                 templates.append(SockTemplate(name=out.name,
-                        identifier=out.identifier, is_input=False,))    
+                        identifier=out.identifier, is_input=False,))
         self.outputs.init_sockets(templates)
         # now we have our parameters, fill them. This is a little inefficient I guess.
         for out in ui_node.outputs:
@@ -1445,7 +1441,7 @@ class UtilityCollectionJoin(MantisNode):
     def reset_execution(self):
         super().reset_execution()
         self.prepared, self.executed = False, False
-    
+
     def bPrepare(self, bContext = None,):
         if self.inputs['Collections'].links:
             bCol_groups = []
@@ -1613,7 +1609,7 @@ class UtilitySetBoneLength(MantisNode):
         self.outputs.init_sockets(outputs)
         self.init_parameters()
         self.node_type = "UTILITY"
-    
+
     def bPrepare(self, bContext = None,):
         from mathutils import Vector
         if matrix := self.evaluate_input("Bone Matrix"):
@@ -1642,7 +1638,7 @@ class UtilityMatrixSetLocation(MantisNode):
         self.outputs.init_sockets(outputs)
         self.init_parameters()
         self.node_type = "UTILITY"
-    
+
     def bPrepare(self, bContext = None,):
         from mathutils import Vector
         if matrix := self.evaluate_input("Matrix"):
@@ -1667,7 +1663,7 @@ class UtilityMatrixGetLocation(MantisNode):
         self.outputs.init_sockets(outputs)
         self.init_parameters()
         self.node_type = "UTILITY"
-    
+
     def bPrepare(self, bContext = None,):
         from mathutils import Vector
         if matrix := self.evaluate_input("Matrix"):
@@ -1688,14 +1684,14 @@ class UtilityMatrixFromXForm(MantisNode):
         self.inputs.init_sockets(inputs)
         self.outputs.init_sockets(outputs)
         self.init_parameters()
-    
+
     def GetxForm(self):
         trace = trace_single_line(self, "xForm")
         for node in trace[0]:
             if (node.node_type == 'XFORM'):
                 return node
         raise GraphError("%s is not connected to an xForm" % self)
-        
+
     def bPrepare(self, bContext = None,):
         from mathutils import Vector, Matrix
         self.parameters["Matrix"] = Matrix.Identity(4)
@@ -1725,7 +1721,7 @@ class UtilityAxesFromMatrix(MantisNode):
         self.outputs.init_sockets(outputs)
         self.init_parameters()
         self.node_type = "UTILITY"
-        
+
     def bPrepare(self, bContext = None,):
         from mathutils import Vector
         if matrix := self.evaluate_input("Matrix"):
@@ -1865,7 +1861,7 @@ class UtilityMatrixAlignRoll(MantisNode):
         #  it directly from the Y axis, the normalized projection of the align
         #  axis, and their cross-product. That only nearly worked.
         # this calculation should not work better, but it does. Why?
-        
+
 class UtilityTransformationMatrix(MantisNode):
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree)
@@ -1939,13 +1935,14 @@ class UtilityArrayGet(MantisNode):
         self.rerouted=[]
 
     def bPrepare(self, bContext = None,):
+        from .base_definitions import links_sort_key
         if len(self.rerouted)>0:
             self.prepared, self.executed = True, True
             return #Either it is already done or it doesn't matter.
         elif self.prepared == False:
             # sort the array entries
             for inp in self.inputs.values():
-                inp.links.sort(key=lambda a : -a.multi_input_sort_id)
+                inp.links.sort(key=links_sort_key)
             oob   = self.evaluate_input("OoB Behaviour")
             index = self.evaluate_input("Index")
 

+ 2 - 3
node_container_common.py

@@ -19,7 +19,6 @@ def get_socket_value(node_socket):
 
 # TODO: modify this to work with multi-input nodes
 def trace_single_line(node_container, input_name, link_index=0):
-    # DO: refactor this for new link class
     """Traces a line to its input."""
     nodes = [node_container]
     # Trace a single line
@@ -231,8 +230,8 @@ def evaluate_sockets(nc, b_object, props_sockets,):
 def finish_driver(nc, b_object, driver_item, prop):
     # prWhite(nc, prop)
     index = driver_item[1]; driver_sock = driver_item[0]
-    driver_trace = trace_single_line(nc, driver_sock)
-    driver_provider, driver_socket = driver_trace[0][-1], driver_trace[1]
+    node_line, last_socket = trace_single_line(nc, driver_sock)
+    driver_provider, driver_socket = node_line[-1], last_socket
     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

+ 1 - 1
ops_nodegroup.py

@@ -1,4 +1,4 @@
-qimport bpy
+import bpy
 from bpy.types import Operator
 from mathutils import Vector
 

+ 3 - 3
preferences.py

@@ -6,8 +6,8 @@ dir_path = os.path.dirname(os.path.realpath(__file__))
 
 def get_bl_addon_object(raise_error = False):
     from bpy import context
-    try_these_first = ['bl_ext.nodes_tools.mantis', 
-        'bl_ext.repos.mantis', 'bl_ext.blender_modules_enabled.mantis',]
+    try_these_first = ['bl_ext.nodes_tools.mantis_beta', 
+        'bl_ext.repos.mantis_beta', 'bl_ext.blender_modules_enabled.mantis_beta',]
     for mantis_key in try_these_first:
         bl_mantis_addon = context.preferences.addons.get(mantis_key)
         if bl_mantis_addon is not None: # chekc the addon AND the prefs
@@ -16,7 +16,7 @@ def get_bl_addon_object(raise_error = False):
             # the prefs will be None if the addon is disabled.
     else:
         for k in context.preferences.addons.keys():
-            if k.endswith("mantis"):
+            if k.endswith("mantis_beta"):
                 bl_mantis_addon = context.preferences.addons[k]
                 if bl_mantis_addon is not None:
                     if bl_mantis_addon.preferences is not None:

+ 153 - 83
readtree.py

@@ -2,91 +2,103 @@ from .utilities import prRed, prGreen, prPurple, prWhite, prOrange, \
                         wrapRed, wrapGreen, wrapPurple, wrapWhite, wrapOrange
 
 
-
-
-def grp_node_reroute_common(nc, nc_to, all_nc):
-    # we need to do this: go  to the to-node
-    # then reroute the link in the to_node all the way to the beginning
-    # so that the number of links in "real" nodes is unchanged
-    # then the links in the dummy nodes need to be deleted
-    for inp_name, inp in nc.inputs.items():
-        # assume each input socket only has one input for now
-        if inp.is_connected:
-            while (inp.links):
-                in_link = inp.links.pop()
-                from_nc = in_link.from_node
-                from_socket = in_link.from_socket
-                links = []
-                from_links = from_nc.outputs[from_socket].links.copy()
-                while(from_links):
-                    from_link = from_links.pop()
-                    if from_link == in_link:
-                        from_link.die()
-                        continue # DELETE the dummy node link
-                    links.append(from_link)
-                from_nc.outputs[from_socket].links = links
-                down = nc_to.outputs[inp_name]
-                for downlink in down.links:
-                    downlink.from_node = from_nc
-                    downlink.from_socket = from_socket
-                    from_nc.outputs[from_socket].links.append(downlink)
-                    if hasattr(downlink.to_node, "reroute_links"):
-                        downlink.to_node.reroute_links(downlink.to_node, all_nc)
-                in_link.die()
-
-def reroute_links_grp(nc, all_nc):
-    if nc.inputs:
-        if (nc_to := all_nc.get( ( *nc.signature, "NodeGroupInput") )):
-            grp_node_reroute_common(nc, nc_to, all_nc)
+# this function is kind of confusing and is very important,
+# so it bears a full explanation: its purpose is to connect
+# the links going into a group to the nodes in that group.
+# FIRST we connect all the incoming links into the Group Node to
+# a Group Interface node that does nothing but mark the entrance.
+# Then, we connect all the outgoing links back to the nodes
+# that had incoming links, so the nodes OUTSIDE the Node Group
+# are connected directly to BOTH the GroupInterface and the
+# nodes INSIDE the node group.
+# we give the GroupInterface nodes an obscenely high
+# multi_input_sort_id so that they are always last.
+# but since these links are going IN, they shouldn't cause any problems.
+# the sub_sort_id is set here in case there are UI links which represent
+# multiple Mantis links - the mantis links are sorted within the UI links
+# and the UI links are sorted as normal, so all the links are in the right
+# order.... probably. BUG here?
+# I want the Group Interface nodes to be part of the hierarchy...
+# but I want to cut the links. hmmm what to do? Fix it if it causes problems.
+# solution to hypothetical BUG could be to do traversal on the links
+# instead of the sockets.
+def grp_node_reroute_common(in_node, out_node, interface):
+    from .base_definitions import links_sort_key
+    for in_node_input in in_node.inputs:
+        i = 0
+        if len(in_node_input.links)>1: # sort here to ensure correct sub_sort_id
+            in_node_input.links.sort(key=links_sort_key)
+        while (in_node_input.links):
+            in_link = in_node_input.links.pop()
+            from_node = in_link.from_node; from_socket = in_link.from_socket
+            link = from_node.outputs[from_socket].connect(
+                interface,in_node_input.name, sort_id = 2**16, sub_sort_id=i)
+            i += 1; in_link.die()
+    for out_node_output in out_node.outputs:
+        while (out_node_output.links):
+            out_link = out_node_output.links.pop()
+            to_node = out_link.to_node; to_socket = out_link.to_socket
+            for j, l in enumerate(interface.inputs[out_node_output.name].links):
+                # we are connecting the link from the ORIGINAL output to the FINAL input.
+                link = l.from_node.outputs[l.from_socket].connect(
+                    to_node, to_socket, sort_id = out_link.multi_input_sort_id)
+                link.sub_sort_id = j
+            out_link.die()
+
+def reroute_links_grp(group, all_nodes):
+    from .internal_containers import GroupInterface
+    interface = GroupInterface(
+        ( *group.signature, "InputInterface"),
+        group.base_tree, group.prototype, 'INPUT',)
+    all_nodes[interface.signature] = interface
+    if group.inputs:
+        if group_input := all_nodes.get(( *group.signature, "NodeGroupInput")):
+            grp_node_reroute_common(group, group_input, interface)
         else:
             raise RuntimeError("internal error: failed to enter a node group ")
 
-def reroute_links_grpout(nc, all_nc):
-    if (nc_to := all_nc.get( ( *nc.signature[:-1],) )):
-        grp_node_reroute_common(nc, nc_to, all_nc)
+def reroute_links_grpout(group_output, all_nodes):
+    if (group := all_nodes.get( ( *group_output.signature[:-1],) )):
+        from .internal_containers import GroupInterface
+        interface = GroupInterface(
+            ( *group.signature, "OutputInterface"),
+            group.base_tree, group.prototype, 'OUTPUT',)
+        all_nodes[interface.signature] = interface
+        grp_node_reroute_common(group_output, group, interface)
     else:
-        raise RuntimeError("error leaving a node group (maybe you are running the tree from inside a node group?)")
+        prOrange(f"WARN: unconnected outputs from a node group "
+                 "(maybe you are running the tree from inside a node group?)")
 
 # FIXME I don't think these signatures are unique.
+# TODO this is a really silly and bad and also really dumb way to do this
 def insert_lazy_parents(nc):
     from .link_nodes import LinkInherit
-    from .base_definitions import NodeLink
     inherit_nc = None
     if nc.inputs["Relationship"].is_connected:
-        link = nc.inputs["Relationship"].links[0]
-        # print(nc)
-        from_nc = link.from_node
-        if from_nc.node_type in ["XFORM"] and link.from_socket in ["xForm Out"]:
+        from .node_container_common import trace_single_line
+        node_line, last_socket = trace_single_line(nc, 'Relationship')
+        # if last_socket is from a valid XFORM, it is the relationship in
+        # because it was traversed from the xForm Out... so get the traverse target.
+        if last_socket.traverse_target is None:
+            return # this is not a valid lazy parent.
+        for other_node in node_line[1:]: # skip the first one, it is the same node
+            if other_node.node_type == 'LINK':
+                return # this one has a realtionship connection.
+            elif other_node.node_type == 'XFORM':
+                break
+        if other_node.node_type in ["XFORM"] and last_socket.traverse_target.name in ["xForm Out"]:
+            for link in other_node.outputs['xForm Out'].links:
+                if link.to_node == nc: link.die()
             inherit_nc = LinkInherit(("MANTIS_AUTOGENERATED", *nc.signature[1:], "LAZY_INHERIT"), nc.base_tree)
-            for from_link in from_nc.outputs["xForm Out"].links:
-                if from_link.to_node == nc and from_link.to_socket == "Relationship":
-                    break # this is it
-            from_link.to_node = inherit_nc; from_link.to_socket="Parent"
-            from_link.to_node.inputs[from_link.to_socket].is_linked=True
-
-            links=[]
-            while (nc.inputs["Relationship"].links):
-                to_link = nc.inputs["Relationship"].links.pop()
-                if to_link.from_node == from_nc and to_link.from_socket == "xForm Out":
-                    continue # don't keep this one
-                links.append(to_link)
-                to_link.from_node.outputs[from_link.from_socket].is_linked=True
-
-            nc.inputs["Relationship"].links=links
-            link=NodeLink(from_node=inherit_nc, from_socket="Inheritance", to_node=nc, to_socket="Relationship")
-            inherit_nc.inputs["Parent"].links.append(from_link)
-
-            inherit_nc.parameters = {
-                                     "Parent":None,
-                                     "Inherit Rotation":True,
-                                     "Inherit Scale":'FULL',
-                                     "Connected":False,
-                                    }
+            l = other_node.outputs['xForm Out'].connect(inherit_nc, 'Parent')
+            l1 = inherit_nc.outputs['Inheritance'].connect(nc, 'Relationship')
+            inherit_nc.parameters = { "Parent":None,
+                                      "Inherit Rotation":True,
+                                      "Inherit Scale":'FULL',
+                                      "Connected":False, }
             # because the from node may have already been done.
-            init_connections(from_nc)
-            init_dependencies(from_nc)
-            init_connections(inherit_nc)
-            init_dependencies(inherit_nc)
+            init_connections(other_node); init_dependencies(other_node)
+            init_connections(inherit_nc); init_dependencies(inherit_nc)
     return inherit_nc
 
 # *** # *** # *** # *** # *** # *** # *** # *** # *** # *** # *** # *** # *** # *** #
@@ -97,9 +109,6 @@ from .base_definitions import replace_types, NodeSocket
 
 def autogen_node(base_tree, ui_socket, signature, mContext):
     mantis_node=None
-    from .utilities import  gen_nc_input_for_data
-    # nc_cls = gen_nc_input_for_data(ui_socket)
-    # if (nc_cls):
     from .internal_containers import AutoGenNode
     mantis_node = AutoGenNode(signature, base_tree)
     mantis_node.mContext = mContext
@@ -199,6 +208,9 @@ def data_from_tree(base_tree, tree_path, mContext, dummy_nodes, all_nc, all_sche
         from .utilities import link_node_containers
         for link in links:
             link_node_containers((None, *tree_path_names), link, local_nc)
+        if current_tree == base_tree:
+            # in the base tree, we need to auto-gen the default values in a slightly different way to node groups.
+            insert_default_values_base_tree(base_tree, all_nc)
         # Now, descend into the Node Groups and recurse
         for nc in group_nodes:
             data_from_tree(base_tree, tree_path+[nc.prototype], mContext, dummy_nodes, all_nc, all_schema)
@@ -298,6 +310,68 @@ def get_schema_length_dependencies(node, all_nodes={}):
                     trees.append((sub_node.prototype.node_tree, sub_node.signature))
     return list(filter(deps_filter, deps))
 
+def insert_default_values_base_tree(base_tree, all_mantis_nodes):
+    # we can get this by name because group inputs are gathered to the bl_idname
+    InputNode = all_mantis_nodes.get((None, 'NodeGroupInput'))
+    if InputNode is None: return # nothing to do here.
+    ui_node = InputNode.prototype
+
+    for i, output in enumerate(InputNode.outputs):
+        ui_output = ui_node.outputs[i] # I need this for the error messages to make sense
+        assert ui_output.identifier == output.name, "Cannot find UI Socket for Default Value"
+        for interface_item in base_tree.interface.items_tree:
+            if interface_item.item_type == 'PANEL': continue
+            if interface_item.identifier == output.name: break
+        else:
+            raise RuntimeError(f"Default value {ui_output.name} does not exist in {base_tree.name} ")
+        if interface_item.item_type == "PANEL":
+            raise RuntimeError(f"Cannot get default value for {ui_output.name} in {base_tree.name} ")
+        default_value = None
+        from bpy.types import bpy_prop_array
+        from mathutils import Vector
+        val_type = None
+        if hasattr(ui_output, 'default_value'):
+            val_type = type(ui_output.default_value) # why tf can't I match/case here?
+        if val_type is bool: default_value = interface_item.default_bool
+        elif val_type is int: default_value = interface_item.default_int
+        elif val_type is float: default_value = interface_item.default_float
+        elif val_type is Vector: default_value = interface_item.default_vector
+        elif val_type is str: default_value = interface_item.default_string
+        elif val_type is bpy_prop_array: default_value = interface_item.default_bool_vector
+        elif interface_item.bl_socket_idname == "xFormSocket":
+            if interface_item.default_xForm == 'ARMATURE':
+                default_value = 'MANTIS_DEFAULT_ARMATURE'
+            else:
+                raise RuntimeError(f"No xForm connected for {ui_output.name} in {base_tree.name}.")
+
+        else:
+            raise RuntimeError(f"Cannot get default value for {ui_output.name} in {base_tree.name} ")
+        output_name = output.name
+        if interface_item.bl_socket_idname not in ['xFormSocket']:
+            signature = ("MANTIS_AUTOGENERATED", f"Default Value {output.name}",)
+            autogen_mantis_node = all_mantis_nodes.get(signature)
+            if autogen_mantis_node is None:
+                autogen_mantis_node = autogen_node(base_tree, output, signature, InputNode.mContext)
+                autogen_mantis_node.parameters[output_name]=default_value
+        elif interface_item.bl_socket_idname == 'xFormSocket' \
+                                        and default_value == 'MANTIS_DEFAULT_ARMATURE':
+            signature = ("MANTIS_AUTOGENERATED", "MANTIS_DEFAULT_ARMATURE",)
+            autogen_mantis_node = all_mantis_nodes.get(signature)
+            if autogen_mantis_node is None:
+                from .xForm_nodes import xFormArmature
+                autogen_mantis_node = xFormArmature(signature, base_tree)
+                autogen_mantis_node.parameters['Name']=base_tree.name+'_MANTIS_AUTOGEN'
+                autogen_mantis_node.mContext =  InputNode.mContext
+                from mathutils import Matrix
+                autogen_mantis_node.parameters['Matrix'] = Matrix.Identity(4)
+            output_name = 'xForm Out'
+        while output.links:
+            l = output.links.pop()
+            to_node = l.to_node; to_socket = l.to_socket
+            l.die()
+            autogen_mantis_node.outputs[output_name].connect(to_node, to_socket)
+            init_connections(l.from_node); init_dependencies(l.from_node)
+        all_mantis_nodes[autogen_mantis_node.signature]=autogen_mantis_node
 
 def parse_tree(base_tree, error_popups=False):
     from uuid import uuid4
@@ -404,7 +478,6 @@ def parse_tree(base_tree, error_popups=False):
                     e = execution_error_cleanup(n, e, show_error=error_popups)
                     if error_popups == False:
                         raise e
-
                 schema_solve_done.add(n)
                 for conn in n.hierarchy_connections:
                     if conn not in schema_solve_done and conn not in solve_layer:
@@ -436,7 +509,8 @@ def parse_tree(base_tree, error_popups=False):
         if nc.signature[0] == "MANTIS_AUTOGENERATED" and len(nc.inputs) == 0 and len(nc.outputs) == 1:
             from .base_definitions import can_remove_socket_for_autogen
             output=list(nc.outputs.values())[0]
-            value=list(nc.parameters.values())[0]   # IDEA modify the dependecy get function to exclude these nodes completely
+            value=list(nc.parameters.values())[0]
+            # We can remove this node if it is safe to push it into the other node's socket.
             keep_me = False
             for l in output.links:
                 to_node = l.to_node; to_socket = l.to_socket
@@ -572,9 +646,8 @@ def execute_tree(nodes, base_tree, context, error_popups = False):
         check_and_add_root(nc, xForm_pass)
     mContext.execution_failed = False
 
-    switch_me = [] # switch the mode on these objects
+    select_me, switch_me = [], [] # switch the mode on these objects
     active = None # only need it for switching modes
-    select_me = []
     try:
         sorted_nodes, execution_failed = sort_execution(nodes, xForm_pass)
         for n in sorted_nodes:
@@ -611,12 +684,10 @@ def execute_tree(nodes, base_tree, context, error_popups = False):
                     raise e
                 execution_failed = True; break
 
-
         switch_mode(mode='OBJECT', objects=switch_me)
         # switch to pose mode here so that the nodes can use the final pose data
         # this will require them to update the depsgraph.
 
-
         for ob in switch_me:
             ob.data.pose_position = 'POSE'
 
@@ -629,7 +700,6 @@ def execute_tree(nodes, base_tree, context, error_popups = False):
                     raise e
                 execution_failed = True; break
 
-
         # REST pose for deformer bind, so everything is in the rest position
         for ob in switch_me:
             ob.data.pose_position = 'REST'

+ 2 - 2
schema_nodes.py

@@ -16,7 +16,7 @@ def TellClasses():
     ]
 
 def schema_init_sockets(nc, is_input = True, in_out='INPUT', category=''):
-    from .utilities import tree_from_nc
+    from .utilities import tree_from_nc, read_schema_type
     parent_tree = tree_from_nc(nc.signature, nc.base_tree)
     if is_input:
         sockets=nc.inputs
@@ -25,7 +25,7 @@ def schema_init_sockets(nc, is_input = True, in_out='INPUT', category=''):
     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.parent and read_schema_type(item) == category:
                 if item.in_out == in_out:
                     sockets.init_sockets([item.name])
     nc.init_parameters()

+ 18 - 10
schema_nodes_ui.py

@@ -7,7 +7,8 @@ from .utilities import (prRed, prGreen, prPurple, prWhite,
                               wrapOrange,)
 from bpy.props import BoolProperty
 
-from .utilities import get_socket_maps, relink_socket_map, do_relink
+from .utilities import (get_socket_maps, relink_socket_map,
+                        do_relink, read_schema_type)
 
 
 def TellClasses():
@@ -29,7 +30,6 @@ def TellClasses():
 # - 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, SchemaUINode):
     '''The current index of the schema execution'''
     bl_idname = 'SchemaIndex'
@@ -66,7 +66,8 @@ class SchemaArrayInput(Node, SchemaUINode):
         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':
+            parent_name = read_schema_type(item)
+            if item.in_out == 'INPUT' and 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' )
@@ -98,7 +99,8 @@ class SchemaArrayInputAll(Node, SchemaUINode):
         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':
+            parent_name = read_schema_type(item)
+            if item.in_out == 'INPUT' and 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' )
@@ -132,7 +134,8 @@ class SchemaArrayInputGet(Node, SchemaUINode):
         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':
+            parent_name = read_schema_type(item)
+            if item.in_out == 'INPUT' and 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' )
@@ -164,7 +167,8 @@ class SchemaArrayOutput(Node, SchemaUINode):
         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':
+            parent_name = read_schema_type(item)
+            if item.in_out == 'OUTPUT' and 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' )
@@ -198,7 +202,8 @@ class SchemaConstInput(Node, SchemaUINode):
         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':
+            parent_name = read_schema_type(item)
+            if item.in_out == 'INPUT' and 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' )
@@ -233,7 +238,8 @@ class SchemaConstOutput(Node, SchemaUINode):
         s = self.inputs.new('UnsignedIntSocket', "Expose at Index")
         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':
+            parent_name = read_schema_type(item)
+            if item.in_out == 'OUTPUT' and 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' )
@@ -271,7 +277,8 @@ class SchemaOutgoingConnection(Node, SchemaUINode):
         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':
+            parent_name = read_schema_type(item)
+            if item.in_out == 'OUTPUT' and 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' )
@@ -307,7 +314,8 @@ class SchemaIncomingConnection(Node, SchemaUINode):
         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':
+            parent_name = read_schema_type(item)
+            if item.in_out == 'INPUT' and 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' )

+ 47 - 6
schema_solve.py

@@ -4,7 +4,7 @@ from .utilities import (prRed, prGreen, prPurple, prWhite,
                               wrapOrange,)
 from .utilities import init_connections, init_dependencies, get_link_in_out
 from .base_definitions import (SchemaUINode, custom_props_types, \
-    MantisNodeGroup, SchemaGroup, replace_types, GraphError)
+    MantisNodeGroup, SchemaGroup, replace_types, GraphError, links_sort_key)
 from .node_container_common import setup_custom_props_from_np
 # a class that solves Schema nodes
 from bpy.types import NodeGroupInput, NodeGroupOutput
@@ -61,7 +61,7 @@ class SchemaSolver:
         # Sort the multi-input nodes in reverse order of ID, this ensures that they are
         #   read in the order they were created
         for inp in self.node.inputs.values():
-            inp.links.sort(key=lambda a : -a.multi_input_sort_id)
+            inp.links.sort(key=links_sort_key)
 
         from bpy.types import NodeGroupInput, NodeGroupOutput
         for ui_node in self.tree.nodes:
@@ -89,6 +89,35 @@ class SchemaSolver:
                 mantis_node = SchemaConstOutput(signature=signature, base_tree=self.node.base_tree, parent_schema_node=self.node)
                 self.schema_nodes[signature] = mantis_node
                 mantis_node.fill_parameters(ui_node)
+        interface = self.setup_interface_node(signature=( *self.signature, "InputInterface"))
+    
+    def setup_interface_node(self, signature):
+        from .internal_containers import GroupInterface
+        interface = GroupInterface(
+            signature,
+            self.node.base_tree, self.node.prototype, 'INPUT',)
+        # now we need to connect it up
+        # adapted from readtree.grp_node_reroute_common
+        from .base_definitions import links_sort_key
+        for in_node_input in self.node.inputs:
+            if in_node_input.name != 'Schema Length':
+                for interface_socket in self.tree.interface.items_tree:
+                    if interface_socket.item_type == 'PANEL': continue
+                    if interface_socket.identifier == in_node_input.name: break
+                if hasattr(interface_socket, "is_array") and interface_socket.is_array: continue
+                if hasattr(interface_socket, "is_connection") and interface_socket.is_connection: continue
+                # for now, do not try and connect to the array/connection inputs.
+            i = 0
+            if len(in_node_input.links)>1: # sort here to ensure correct sub_sort_id
+                in_node_input.links.sort(key=links_sort_key)
+            for in_link in in_node_input.links:
+                from_node = in_link.from_node; from_socket = in_link.from_socket
+                link = from_node.outputs[from_socket].connect(
+                    interface,in_node_input.name, sort_id = 2**16, sub_sort_id=i)
+                i += 1
+            in_node_input.links.sort(key=links_sort_key)
+        return interface # I want to be able to set this up elsewhere
+
 
     def set_index_strings(self):
         self.index_str = lambda : '.'+str(self.uuid)+'.'+str(self.index).zfill(4)
@@ -101,9 +130,9 @@ class SchemaSolver:
         """ Sort and store the links to/from the Schema group node."""
         for item in self.tree.interface.items_tree:
             if item.item_type == 'PANEL': continue
-            parent_name='Constant'
-            if item.parent.name != '': # in an "prphan" item this is left blank , it is not None or an AttributeError.
-                parent_name = item.parent.name
+            from .utilities import read_schema_type
+            parent_name = read_schema_type(item)
+            # just gonna try and make the 
             match parent_name:
                 case 'Connection':
                     if item.in_out == 'INPUT':
@@ -202,6 +231,11 @@ class SchemaSolver:
             mantis_node.fill_parameters(prototype_ui_node)
             # be sure to pass on the Mantis Context to them
             mantis_node.mContext=mContext
+        # set up an interface node at this point.
+        interface = self.setup_interface_node(signature=( *self.signature, "InputInterface"+self.index_str() ))
+        interface.mContext = mContext
+        frame_mantis_nodes[interface.signature] = interface
+        self.all_nodes[interface.signature] = interface
 
     def handle_link_from_index_input(self, index, frame_mantis_nodes, ui_link):
         from .base_definitions import can_remove_socket_for_autogen
@@ -246,6 +280,12 @@ class SchemaSolver:
         else:
             raise RuntimeError("247 I This code should be unreachable. Please report this as a bug!")
 
+    # TODO: link the incoming connection to the current Interface node
+    #       It is complicated because the Interface needs the identifier
+    #       but most of this stuff uses the name and I donno how to get the ID
+    #       this isn't very important. I'll add this feature when someone tries
+    #       to use it and complains.
+
     def handle_link_from_incoming_connection_input(self, frame_mantis_nodes, ui_link):
         incoming = self.incoming_connections[ui_link.from_socket.name]
         if incoming is not None:
@@ -389,7 +429,8 @@ class SchemaSolver:
             to_socket_name=ui_link.to_socket.name
             if to_node.node_type in ['DUMMY_SCHEMA']:
                 to_socket_name=ui_link.to_socket.identifier
-            connection = NodeLink(l.from_node, l.from_socket, to_node, to_socket_name, l.multi_input_sort_id)
+            connection = NodeLink(l.from_node, l.from_socket, to_node, to_socket_name,
+                                  l.multi_input_sort_id, l.sub_sort_id)
             to_node.flush_links()
 
     def handle_link_to_constant_output(self, frame_mantis_nodes, index, ui_link,  to_ui_node):

+ 127 - 0
socket_definitions.py

@@ -292,6 +292,133 @@ def tell_valid_bl_idnames():
     valid_classes = filter(lambda cls : cls.is_valid_interface_type, [cls for cls in TellClasses()])
     return (cls.bl_idname for cls in valid_classes)
 
+
+enum_default_xForm_values =(
+        ('NONE', "None", "None - fail if unconnected (RECOMMENDED)."),
+        ('ARMATURE', "Generated Armature", "Generate an armature automatically. "
+                     "PLEASE use this only for development and testing. "
+                     "If you use this as a feature in your rigs you will be sorry."),)
+    
+# Custom Interface Types give the user the ability to set properties for the interface
+# we'll define a base class, and generate the individual classes from the base class
+# but we'll leave the option to define a few of them directly.
+from bpy.types import NodeTreeInterfaceSocket
+
+def interface_socket_update(self, context):
+    # we're just gonna do this the dumb way for now and invalidate the tree
+    # BUG HACK TODO actually I am gonna do this stuff later
+    # later, I can do this based on the connections in the tree
+    # and the socket updater can use the same code for group interface modifications
+    # TODO do this stuff because the tree will be a lot snappier
+    pass
+
+
+interface_default_value_description="The default value of the socket when it is not connected."
+class MantisInterfaceSocketBaseClass():
+    is_array : bpy.props.BoolProperty(default =False, update=interface_socket_update,
+            description="Whether the socket is an array, otherwise it is constant." ) 
+    is_connection : bpy.props.BoolProperty(default =False, update=interface_socket_update,
+            description="If the socket is a connection or not. Ensure this is always paired"
+                        " with an input and an output." ) 
+    connected_to : bpy.props.StringProperty(default="", update=interface_socket_update,
+            description="The name of the socket this one is connected to." ) 
+    # we are just gonna use ONE base class (it's easier)
+    # so generate ALL properties and show only what is needed.
+    default_string : bpy.props.StringProperty(default="", update=interface_socket_update,
+            description=interface_default_value_description, ) 
+    default_float : bpy.props.FloatProperty(default=0.0, update=interface_socket_update,
+            description=interface_default_value_description, ) 
+    default_vector : bpy.props.FloatVectorProperty( size = 3, default = (0.0, 0.0, 0.0, ),
+            description=interface_default_value_description, update=interface_socket_update,) 
+    default_int : bpy.props.IntProperty(default=0, update=interface_socket_update,
+            description=interface_default_value_description, ) 
+    default_bool : bpy.props.BoolProperty(default=False, update=interface_socket_update,
+            description=interface_default_value_description, ) 
+    default_bool_vector : bpy.props.BoolVectorProperty(subtype = "XYZ", update=interface_socket_update,
+            description=interface_default_value_description, ) 
+    default_xForm : bpy.props.EnumProperty( default = 'NONE', update = interface_socket_update,
+        items=enum_default_xForm_values, description=interface_default_value_description,)
+
+def interface_draw(self, context, layout):
+    if not self.is_connection:
+        layout.prop(self, "is_array", text="Is Array", toggle=True,)
+    if not self.is_array and self.id_data.bl_idname == 'SchemaTree':
+        layout.prop(self, "is_connection", text="Is Connection", toggle=True,)
+        if False: # DISABLED for now because it will take a big change to Schema to make this work.
+            if self.is_connection: # only show this if in a Schema AND set to is_connection
+                layout.prop(self, "connected_to", text="Connected To", toggle=True,)
+
+# Different classes to handle different data types. In the future, these should also
+#  have settable min/max and such where appropriate
+def interface_string_draw(self, context, layout):
+    layout.prop(self, "default_string", text="Default Value", toggle=True,)
+    interface_draw(self, context, layout)
+
+def interface_float_draw(self, context, layout):
+    layout.prop(self, "default_float", text="Default Value", toggle=True,)
+    interface_draw(self, context, layout)
+
+def interface_vector_draw(self, context, layout):
+    layout.prop(self, "default_vector", text="Default Value", toggle=True,)
+    interface_draw(self, context, layout)
+
+def interface_int_draw(self, context, layout):
+    layout.prop(self, "default_int", text="Default Value", toggle=True,)
+    interface_draw(self, context, layout)
+
+def interface_bool_draw(self, context, layout):
+    layout.prop(self, "default_bool", text="Default Value", toggle=True,)
+    interface_draw(self, context, layout)
+
+def interface_bool_vector_draw(self, context, layout):
+    layout.prop(self, "default_bool_vector", text="Default Value", toggle=True,)
+    interface_draw(self, context, layout)
+
+def interface_xform_draw(self, context, layout):
+    if self.in_out == 'INPUT':
+        layout.prop(self, "default_xForm", text="Default Value", toggle=True,)
+    interface_draw(self, context, layout)
+
+
+def generate_custom_interface_types():
+    generated_classes = []
+    # copied from above
+    valid_classes = filter(lambda cls : cls.is_valid_interface_type, [cls for cls in TellClasses()])
+    for cls in valid_classes:
+        name = cls.__name__ + "Interface"
+            
+        my_interface_draw = interface_draw
+        # set the right draw function by the value's type
+        match map_color_to_socket_type(cls.color_simple): #there has to be a better way to do this
+            case "BooleanSocket":
+                my_interface_draw = interface_bool_draw
+            case "IntSocket":
+                my_interface_draw = interface_int_draw
+            case "FloatSocket":
+                my_interface_draw = interface_float_draw
+            case "BooleanThreeTupleSocket":
+                my_interface_draw = interface_bool_vector_draw
+            case "VectorSocket":
+                my_interface_draw = interface_vector_draw
+            case "StringSocket":
+                my_interface_draw = interface_string_draw
+            case "xFormSocket":
+                my_interface_draw = interface_xform_draw
+
+        interface = type(
+                      name,
+                      (MantisInterfaceSocketBaseClass, NodeTreeInterfaceSocket,),
+                      {
+                          "draw"             : my_interface_draw,
+                          "bl_idname"        : name,
+                          "bl_socket_idname" : cls.bl_idname,
+                          "socket_type"      : cls.bl_idname,
+                      },
+                  )
+        generated_classes.append(interface)
+    return generated_classes
+
+
 # Was setting color like this:
 # color : bpy.props.FloatVectorProperty(size = 4, default = cFCurve,)
 # but this didn't work when Blender automatically generated interface classes?

+ 82 - 43
utilities.py

@@ -111,31 +111,33 @@ def get_node_prototype(sig, base_tree):
 # This one is the simplest case so it is easiest to use its own function.
 def set_string_variables_at_creation_time(n, prototype, mContext):
     # we're gonna store the variables using the node's signature
-    prev_group_key = ''
-    prev_group_vars = {}
-    for i in range(len(n.signature[:-1])):
-        if i == 0: continue # this will cut any AUTOGEN or None in the base
-        prev_group_key+=n.signature[i]
-        prev_group_vars=mContext.string_variables.get(prev_group_key, {})
-    # IS THIS WISE???
-    from copy import deepcopy # so nothing spooky will happen
-    group_vars = mContext.string_variables["".join(n.signature[1:])] = deepcopy(prev_group_vars)
+    group_key = ''.join(n.signature[1:])
+    # Get the variables if they exist, otherwise just get a dict
+    group_vars=mContext.string_variables.get(group_key, {})
+    mContext.string_variables[group_key]=group_vars
     for input in prototype.inputs:
         if hasattr(input, "default_value") and not input.is_linked:
             if isinstance (input.default_value, str):
                 group_vars[input.name]=input.default_value
             elif hasattr(input.default_value, "name"):
                 group_vars[input.name]=input.default_value.name
-
-
-def set_string_variables_during_exec(n, mContext):
-    print(n)
-    pass
-    # so for this we need to get the UI node to get the string
-    # when a node is executed, we check for string variables that were set at runtime
-    # we need the dummy node for this
-
-
+        elif hasattr(input, "default_value") and input.is_linked:
+            group_vars[input.name]=None
+
+def set_string_variables_at_execution(interface_node, var_name):
+    # get the ID of the input
+    prototype = get_node_prototype(interface_node.signature[1:-1], 
+                                   interface_node.base_tree)
+    tree = prototype.node_tree
+    for interface_item in tree.interface.items_tree:
+        if interface_item.item_type == 'PANEL': continue
+        if interface_item.name == var_name: break
+    else:
+        prRed(f"Failed to get value for variable ${var_name}."); return
+    var_value = interface_node.evaluate_input(interface_item.identifier)
+    group_key = ''.join(interface_node.signature[1:-1])
+    group_vars=interface_node.mContext.string_variables.get(group_key, {})
+    group_vars[var_name]=var_value
 
 ##################################################################################################
 # groups and changing sockets -- this is used extensively by Schema.
@@ -275,6 +277,28 @@ def do_relink(node, socket, map, in_out='INPUT', parent_name = ''):
             except (AttributeError, ValueError): # must be readonly or maybe it doesn't have a d.v.
                 pass
 
+def read_schema_type(interface_item):
+    # VERSIONING CODE
+    tree=interface_item.id_data
+    version = tree.mantis_version
+    old_version = False
+    if  version[0] == 0: 
+        if version[1] < 12: old_version = True
+        elif version[1] == 12 and version[2] < 27: old_version = True
+    # unfortunately we need to check this stuff for the versioning code to run correctly the first time.
+    # UNLESS I can find a way to prevent this code from running before versioning
+
+    if old_version or (not hasattr(interface_item, 'is_array')):
+        # it is not a custom interface class and/or the file is old.
+        if interface_item.parent:
+            return interface_item.parent.name
+    else:
+        if interface_item.is_array:
+            return 'Array'
+        if interface_item.is_connection:
+            return 'Connection'
+    return 'Constant'
+
 def update_interface(interface, name, in_out, sock_type, parent_name):
     from bpy.app import version as bpy_version
     if parent_name:
@@ -296,26 +320,40 @@ def update_interface(interface, name, in_out, sock_type, parent_name):
 
 # D.node_groups['Rigging Nodes'].interface.new_socket('beans', description='the b word', socket_type='NodeSocketGeometry')
 #UGLY BAD REFACTOR
-def relink_socket_map_add_socket(node, socket_collection, item, in_out=None,):
+def relink_socket_map_add_socket(node, socket_collection, item,  in_out=None,):
     from bpy.app import version as bpy_version
-    if not in_out: in_out=item.in_out
-    if node.bl_idname in ['MantisSchemaGroup'] and item.parent and item.parent.name == 'Array':
-        multi = True if in_out == 'INPUT' else False
-        # have to work around a bug in 4.5.0 that prevents me from declaring custom socket types
-        # I have arbitrarily chosen to use the NodeSocketGeometry type to signal that this one is affected.
-        if bpy_version == (4, 5, 0) and item.bl_socket_idname == 'NodeSocketGeometry':
-            from .versioning import socket_add_workaround_for_4_5_0_LTS
-            s = socket_add_workaround_for_4_5_0_LTS(item, socket_collection, multi)
-        else:
-            s = socket_collection.new(type=item.bl_socket_idname, name=item.name, identifier=item.identifier,  use_multi_input=multi)
+    # if not in_out: in_out=item.in_out
+    multi=False
+    if in_out == 'INPUT' and read_schema_type(item) == 'Array':
+        multi = True
+    # have to work around a bug in 4.5.0 that prevents me from declaring custom socket types
+    # I have arbitrarily chosen to use the NodeSocketGeometry type to signal that this one is affected.
+    if bpy_version == (4, 5, 0) and item.bl_socket_idname == 'NodeSocketGeometry':
+        from .versioning import socket_add_workaround_for_4_5_0_LTS
+        s = socket_add_workaround_for_4_5_0_LTS(item, socket_collection, multi)
     else:
-        if bpy_version == (4, 5, 0) and item.bl_socket_idname == 'NodeSocketGeometry':
-            from .versioning import socket_add_workaround_for_4_5_0_LTS
-            s = socket_add_workaround_for_4_5_0_LTS(item, socket_collection, multi=False,)
-        else:
-            s = socket_collection.new(type=item.bl_socket_idname, name=item.name, identifier=item.identifier)
-    if item.parent.name == 'Array': s.display_shape = 'SQUARE_DOT'
-    elif item.parent.name == 'Constant': s.display_shape='CIRCLE_DOT'
+        s = socket_collection.new(type=item.bl_socket_idname, name=item.name, identifier=item.identifier,  use_multi_input=multi)
+    if hasattr(s, 'default_value') and hasattr(s, 'is_valid_interface_type') and \
+          s.is_valid_interface_type == True:
+        if s.bl_idname not in ['MatrixSocket']: # no default value implemented
+            from bpy.types import bpy_prop_array
+            from mathutils import Vector
+            default_value = 'REPORT BUG ON GITLAB' # default to bug string
+            val_type = type(s.default_value) # why tf can't I match/case here?
+            if val_type is bool: default_value = item.default_bool
+            if val_type is int: default_value = item.default_int
+            if val_type is float: default_value = item.default_float
+            if val_type is Vector: default_value = item.default_vector
+            if val_type is str: default_value = item.default_string
+            if val_type is bpy_prop_array: default_value = item.default_bool_vector
+            s.default_value = default_value
+
+    if read_schema_type(item) == 'Array': s.display_shape = 'SQUARE_DOT'
+    elif node.bl_idname in ['MantisSchemaGroup'] and read_schema_type(item) == 'Constant':
+        s.display_shape='CIRCLE_DOT'
+
+    # if item.parent.name == 'Array': s.display_shape = 'SQUARE_DOT'
+    # elif item.parent.name == 'Constant': s.display_shape='CIRCLE_DOT'
     return s
 
 # TODO REFACTOR THIS
@@ -323,9 +361,9 @@ def relink_socket_map_add_socket(node, socket_collection, item, in_out=None,):
 # but I have provided this interface to Mantis
 # I did not follow the Single Responsibility Principle
 # I am now suffering for it, as I rightly deserve.
-def relink_socket_map(node, socket_collection, map, item, in_out=None,):
-    s = relink_socket_map_add_socket(node, socket_collection, item, in_out=None,)
-    do_relink(node, s, map)
+def relink_socket_map(node, socket_collection, map, item, in_out):
+    new_socket = relink_socket_map_add_socket(node, socket_collection, item, in_out,)
+    do_relink(node, new_socket, map, in_out, parent_name=read_schema_type(item))
 
 def unique_socket_name(node, other_socket, tree):
     name_stem = other_socket.bl_label; num=0
@@ -617,7 +655,8 @@ def schema_dependency_handle_item(schema, all_nc, item,):
     if item.in_out == 'INPUT':
         dependencies = schema.dependencies
         hierarchy_dependencies = schema.hierarchy_dependencies
-        if item.parent and item.parent.name == 'Array':
+        parent_name = read_schema_type(item)
+        if parent_name == 'Array':
             for schema_idname in ['SchemaArrayInput', 'SchemaArrayInputGet', 'SchemaArrayInputAll']:
                 if (nc := all_nc.get( (*schema.signature, schema_idname) )):
                     for to_link in nc.outputs[item.name].links:
@@ -632,7 +671,7 @@ def schema_dependency_handle_item(schema, all_nc, item,):
                             if hierarchy:
                                 hierarchy_dependencies.append(from_link.from_node)
                             dependencies.append(from_link.from_node)
-        if item.parent and item.parent.name == 'Constant':
+        if 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:
@@ -646,7 +685,7 @@ def schema_dependency_handle_item(schema, all_nc, item,):
                         if hierarchy:
                             hierarchy_dependencies.append(from_link.from_node)
                         dependencies.append(from_link.from_node)
-        if item.parent and item.parent.name == 'Connection':
+        if 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:

+ 37 - 1
versioning.py

@@ -220,7 +220,6 @@ def cleanup_4_5_0_LTS_interface_workaround(*args, **kwargs):
             interface_item.description = ''
     # that should be enough!
 
-
 def up_0_12_25_replace_floor_offset_type(*args, **kwargs):
     # add an inherit color input.
     node = kwargs['node']
@@ -244,6 +243,42 @@ def up_0_12_25_replace_floor_offset_type(*args, **kwargs):
         print(e)
 
 
+def schema_enable_custom_interface_types(*args, **kwargs):
+    # return
+    tree = kwargs['tree']
+    current_major_version = tree.mantis_version[0]
+    current_minor_version = tree.mantis_version[1]
+    current_sub_version = tree.mantis_version[2]
+    if  current_major_version > 0: return# major version must be 0
+    if current_minor_version >= 13: return# minor version must be 12 or less
+    # we need to set the new interface values on the schema interface stuff
+    prGreen(f"Updating Schema tree {tree.name} to support new, improved UI!")
+    try:
+        for item in tree.interface.items_tree:
+            if item.item_type == 'PANEL':
+                continue
+            parent_name = 'Constant'
+            if item.parent:
+                parent_name=item.parent.name
+            if hasattr(item, "is_array"):
+                # if is_array exists we're in the custom interface class
+                # so we'll assume the other attributes exist
+                if parent_name == 'Array':
+                    item.is_array = True
+                if parent_name == 'Connection':
+                    item.is_array = False
+                    item.is_connection=True
+                    item.connected_to=item.name
+                    # since heretofore it has been a requirement that the names match
+    except Exception as e:
+        prRed(f"Error updating version in tree: {tree.name}; see error:")
+        print(e)
+            
+
+
+
+
+
 
 versioning_tasks = [
     # node bl_idname    task                required keyword arguments
@@ -253,6 +288,7 @@ versioning_tasks = [
     (['MantisTree', 'SchemaTree'], cleanup_4_5_0_LTS_interface_workaround, ['tree']),
     (['InputWidget'], up_0_12_13_add_widget_scale, ['node']),
     (['LinkFloor'], up_0_12_25_replace_floor_offset_type, ['node']),
+    (['SchemaTree'], schema_enable_custom_interface_types, ['tree']),
 ]
 
 

+ 27 - 27
visualize.py

@@ -50,11 +50,6 @@ class MantisVisualizeNode(Node):
             case 'DUMMY_SCHEMA': self.color = (0.85 ,0.95, 0.9)
             case 'DUMMY':        self.color = (0.05 ,0.05, 0.15)
 
-        # if mantis_node.execution_prepared:
-        #     self.color = (0.02, 0.98, 0.02) # GREEN!
-
-        # if mantis_node.execution_debug_tag:
-        #     self.color = (0.02 ,0.02, 0.02)
 
         self.name = '.'.join(mantis_node.signature[1:]) # this gets trunc'd
         self.signature = '|'.join(mantis_node.signature[1:])
@@ -70,7 +65,10 @@ class MantisVisualizeNode(Node):
                     case "DEBUG_CONNECTIONS":
                         if not inp.is_connected:
                             continue
-                s = self.inputs.new('WildcardSocket', inp.name)
+                multi = False
+                if len(inp.links) > 1: multi = True
+                s = self.inputs.new('WildcardSocket', inp.name, use_multi_input=multi)
+                s.link_limit = 4000
                 try:
                     if sock := np.inputs.get(inp.name):
                         s.color = sock.color_simple
@@ -100,7 +98,10 @@ class MantisVisualizeNode(Node):
                     case "DEBUG_CONNECTIONS":
                         if not inp.is_connected:
                             continue
-                self.inputs.new('WildcardSocket', inp.name)
+                multi = False
+                if len(inp.links) > 1: multi = True
+                s = self.inputs.new('WildcardSocket', inp.name)
+                s.link_limit = 4000
             for out in mantis_node.outputs:
                 match mode:
                     case "DEBUG_CONNECTIONS":
@@ -111,12 +112,12 @@ class MantisVisualizeNode(Node):
 def gen_vis_node( mantis_node,
                   vis_tree,
                   links,
-                  omit_simple=True,
+                  omit_simple=False,
                  ):
     from .base_definitions import array_output_types
-    if mantis_node.node_type == 'UTILITY' and \
-         mantis_node.execution_prepared == True:
-            return
+    # if mantis_node.node_type == 'UTILITY' and \
+    #      mantis_node.execution_prepared == True:
+    #         return
     base_tree= mantis_node.base_tree
     vis = vis_tree.nodes.new('MantisVisualizeNode')
     vis.gen_data(mantis_node)
@@ -140,12 +141,11 @@ def visualize_tree(m_nodes, base_tree, context):
     import cProfile
     import pstats, io
     from pstats import SortKey
+    cull_no_links = False
     with cProfile.Profile() as pr:
         try:
             trace_from_roots = True
-            all_links = set()
-            mantis_nodes=set()
-            nodes={}
+            all_links = set(); mantis_nodes=set(); nodes={}
             if trace_from_roots:
                 roots=[]
                 for n in m_nodes.values():
@@ -156,6 +156,7 @@ def visualize_tree(m_nodes, base_tree, context):
                     print ("No nodes to visualize")
                     return
             else:
+                mantis_keys  = list(base_tree.parsed_tree.keys())
                 mantis_nodes = list(base_tree.parsed_tree.values())
 
             vis_tree = bpy.data.node_groups.new(base_tree.name+'_visualized', type='MantisVisualizeTree')
@@ -166,11 +167,11 @@ def visualize_tree(m_nodes, base_tree, context):
                 # not in the parsed tree or available from trace_all_nodes_from_root.
 
             for l in all_links:
-                if l.to_node.node_type in ['DUMMY_SCHEMA', 'DUMMY'] or \
-                l.from_node.node_type in ['DUMMY_SCHEMA', 'DUMMY']:
-                    pass
-                from_node=nodes.get(l.from_node.signature)
-                to_node=nodes.get(l.to_node.signature)
+                # if l.to_node.node_type in ['DUMMY_SCHEMA', 'DUMMY'] or \
+                # l.from_node.node_type in ['DUMMY_SCHEMA', 'DUMMY']:
+                #     pass
+                from_node = nodes.get(l.from_node.signature)
+                to_node   = nodes.get(l.to_node.signature)
                 from_socket, to_socket = None, None
                 if from_node and to_node:
                     from_socket = from_node.outputs.get(l.from_socket)
@@ -196,14 +197,13 @@ def visualize_tree(m_nodes, base_tree, context):
                 return False
 
             no_links=[]
-
-            for n in vis_tree.nodes:
-                if not has_links(n):
-                    no_links.append(n)
-
-            while (no_links):
-                n = no_links.pop()
-                vis_tree.nodes.remove(n)
+            if cull_no_links == True:
+                for n in vis_tree.nodes:
+                    if not has_links(n):
+                        no_links.append(n)
+                while (no_links):
+                    n = no_links.pop()
+                    vis_tree.nodes.remove(n)
 
         finally:
             s = io.StringIO()

+ 16 - 18
xForm_nodes.py

@@ -26,8 +26,7 @@ def reset_object_data(ob):
 def get_parent_node(node_container, type = 'XFORM'):
     # type variable for selecting whether to get either
     #   the parent xForm  or the inheritance node
-    node_line, socket = trace_single_line(node_container, "Relationship")
-    parent_nc = None
+    node_line, _last_socket = trace_single_line(node_container, "Relationship")
     for i in range(len(node_line)):
         # check each of the possible parent types.
         if ( (node_line[ i ].__class__.__name__ == 'LinkInherit') ):
@@ -267,23 +266,22 @@ class xFormBone(xFormNode):
         #should do the trick...
 
     def bSetParent(self, eb):
-        # print (self.bObject)
         from bpy.types import EditBone
         parent_nc = get_parent_node(self, type='LINK')
-        # print (self, parent_nc.inputs['Parent'].from_node)
-        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')
+        if parent_nc is None:
+            raise RuntimeError(wrapRed(f"Cannot set parent for node {self}"))
+        node_lines, _last_socket = trace_single_line(parent_nc, 'Parent')
+        for other_node in node_lines:
+            if isinstance(other_node, (xFormArmature, xFormBone)):
+                parent = other_node.bGetObject(mode = 'EDIT'); break
         else:
             raise RuntimeError(wrapRed(f"Cannot set parent for node {self}"))
-
-        if isinstance(parent, EditBone):
+        if isinstance(parent, EditBone): # otherwise, no need to do anything.
             eb.parent = parent
+            eb.use_connect = parent_nc.evaluate_input("Connected")
+            eb.use_inherit_rotation = parent_nc.evaluate_input("Inherit Rotation")
+            eb.inherit_scale = parent_nc.evaluate_input("Inherit Scale")
 
-        eb.use_connect = parent_nc.evaluate_input("Connected")
-        eb.use_inherit_rotation = parent_nc.evaluate_input("Inherit Rotation")
-        eb.inherit_scale = parent_nc.evaluate_input("Inherit Scale")
-        # otherwise, no need to do anything.
 
     def bPrepare(self, bContext=None):
         self.parameters['Matrix'] = get_matrix(self)
@@ -435,9 +433,6 @@ class xFormBone(xFormNode):
             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:
@@ -847,8 +842,11 @@ class xFormCurvePin(xFormNode):
         for socket_name in ["Curve Pin Factor", "Forward Axis","Up Axis",]:
             if self.inputs.get(socket_name) is None: continue # in case it has been bypassed
             if self.inputs[socket_name].is_linked:
-                link = self.inputs[socket_name].links[0]
-                driver = link.from_node
+                node_line, _last_socket = trace_single_line(self, socket_name)
+                driver = None
+                for other_node in node_line:
+                    if other_node.node_type == 'DRIVER':
+                        driver = other_node; break
                 if isinstance(driver, UtilityDriver):
                     prop_amount = driver.evaluate_input("Property")
                 elif isinstance(driver, UtilitySwitch):