Browse Source

Add Custom Interface Socket Types

this commit (re)introduces custom node socket interface types
I did this a long time ago but didn't really understand it so I let Blender
do it automatically
But I am bringing it back because it lets me set some custom variables
for different socket types, such as default values or whether the socket
should be an array or not.

Interface Classes set the multi and default value now

note that this is VERY untested and probably doesn't work for Schema

update interface draw for correct UI and clarity

removes default value from xForm for outputs
removes array if interface item is a connection
and visa versa

fix: unbound local error when updating group interface

initial versioning for new interface classes

Implement Custom Interface Classes

this commit implements versioning and basic functionality for
 - default values on group sockets
 - arrays and connections editable in the Node Group Interface
 - array support for regular Node Groups

tested and working with Human, Elephant, and Stego files.

Disable "Connected To" feature

this one is a little too ambitious and it will be too hard to implement

Fix: default value disabled for Matrix

(it is not yet implemented)

Fix: correct default value type for vectors

New Feature:  Default Values for base tree

Fix: interface panel doesn't have an identifier

Fix: make default values work with more socket types

this commit also improves the error message
and it unfortunately(?) adds an assert from something I think is a safe assumption.

Initialize Tree with correct version number

WIP: Route Group I/O through interface nodes

This commit does not work! But it is a good start.

The purpose of this commit is to move towards bundling connections
at the base of each group in/out so that I can cache things, manage
variables, and most importantly for present purposes, send arrays
in and out in a more consistent way. Probably I revert this commit
and do something very similar without the intervening node until I
get that slightly simpler strategy working.

GroupInterfaceNodes at group in/out

This commit fixes and finishes the last commit.
All I forgot was to add the nodes to the parsed tree,
and deal with a little bit of hardcoded foolishness
that assumed there would be no intervening nodes
using socket traversal.

There will be more fixing for this behaviour in the near
future, no doubt. But this works, and it opens up
a whole lot of doors.

clean up useless prints

Fix: remove many instances of hardcoded node get

search ".link[0]" to find the rest of them and fix before release
for now, the matter of array sort id is much more pressing

Fix: Correctly Sort Links to Group Arrays

Look at the comment in readtree to understand this commit
Special note: this really needs more robust testing
but since I have tested stuff that I understand the only
test cases I am unsure about are those which are too hard
for me to imagine ahead of time. So probably I fix it if/when
someone actually encounters it.

cleanup xForm get_parent_node

Fix: lazy parents broken

Fix: Spline IK broken
Joseph Brandenburg 3 tuần trước cách đây
mục cha
commit
3b5e420b37
15 tập tin đã thay đổi với 618 bổ sung318 xóa
  1. 7 4
      __init__.py
  2. 68 62
      base_definitions.py
  3. 23 5
      internal_containers.py
  4. 46 35
      link_nodes.py
  5. 2 1
      misc_nodes.py
  6. 2 3
      node_container_common.py
  7. 166 97
      readtree.py
  8. 2 2
      schema_nodes.py
  9. 18 10
      schema_nodes_ui.py
  10. 7 6
      schema_solve.py
  11. 127 0
      socket_definitions.py
  12. 61 24
      utilities.py
  13. 38 1
      versioning.py
  14. 35 50
      visualize.py
  15. 16 18
      xForm_nodes.py

+ 7 - 4
__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=28
+from .base_definitions import (MANTIS_VERSION_MAJOR,
+                               MANTIS_VERSION_MINOR,
+                               MANTIS_VERSION_SUB)
 
 classLists = [module.TellClasses() for module in [
  link_nodes_ui,
@@ -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
 

+ 68 - 62
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,12 +80,16 @@ def hash_tree(tree):
     links.sort(); hash_data+=''.join(links)
     return hash(hash_data)
 
+MANTIS_VERSION_MAJOR=0
+MANTIS_VERSION_MINOR=12
+MANTIS_VERSION_SUB=27
+
 class MantisTree(NodeTree):
     '''A custom node tree type that will show up in the editor type list'''
     bl_idname = 'MantisTree'
     bl_label = "Rigging Nodes"
     bl_icon = 'OUTLINER_OB_ARMATURE'
-    
+
     tree_valid:BoolProperty(default=False)
     hash:StringProperty(default='')
     do_live_update:BoolProperty(default=True) # use this to disable updates for e.g. scripts
@@ -97,21 +102,22 @@ 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)
 
     #added to work around a bug in 4.5.0 LTS
     interface_helper : StringProperty(default='')
-    
+
     parsed_tree={}
 
     if (bpy.app.version < (4, 4, 0) or bpy.app.version >= (4,5,0)):  # in 4.4 this leads to a crash
         @classmethod
         def valid_socket_type(cls : NodeTree, socket_idname: str):
             return valid_interface_types(cls, socket_idname)
-    
+
     def update(self): # set the reroute colors
         if (bpy.app.version >= (4,4,0)):
             fix_reroute_colors(self)
@@ -155,7 +161,7 @@ class MantisTree(NodeTree):
                 except Exception as e:
                     print("Node \"%s\" failed to update display with error: %s" %(wrapGreen(node.name), wrapRed(e)))
         self.is_executing = False
-        
+
         # TODO: deal with invalid links properly.
         #    - Non-hierarchy links should be ignored in the circle-check and so the links should be marked valid in such a circle
         #    - hierarchy-links should be marked invalid and prevent the tree from executing.
@@ -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='')
 
@@ -219,7 +226,7 @@ class MantisSocketTemplate():
     hide             : bool = field(default=False)
     use_multi_input  : bool = field(default=False)
     default_value    : Any = field(default=None)
-    
+
 
 
 #TODO: do a better job explaining how MantisNode and MantisUINode relate.
@@ -236,7 +243,7 @@ class MantisUINode:
     @classmethod
     def poll(cls, ntree):
         return (ntree.bl_idname in ['MantisTree', 'SchemaTree'])
-                
+
     @classmethod
     def set_mantis_class(self):
         from importlib import import_module
@@ -260,7 +267,7 @@ class MantisUINode:
                     node_tree.num_links+=1
                 elif (link.to_socket.is_multi_input):
                     node_tree.num_links+=1
-    
+
     def init_sockets(self, socket_templates : tuple[MantisSocketTemplate]):
         for template in socket_templates:
             collection = self.outputs
@@ -286,7 +293,7 @@ class MantisUINode:
                 #   responsibility to send the right type.
             if template.use_multi_input: # this is an array
                 socket.display_shape = 'SQUARE_DOT'
-            
+
 class SchemaUINode(MantisUINode):
     mantis_node_library='.schema_nodes'
     is_updating:BoolProperty(default=False)
@@ -299,7 +306,7 @@ class LinkNode(MantisUINode):
     @classmethod
     def poll(cls, ntree):
         return (ntree.bl_idname in ['MantisTree', 'SchemaTree'])
-    
+
 class xFormNode(MantisUINode):
     mantis_node_library='.xForm_nodes'
     @classmethod
@@ -388,11 +395,11 @@ def node_group_update(node, force = False):
                 if update_input: continue # done here
                 if s.bl_idname != item.bl_socket_idname: update_input = True; continue
             else: update_input = True; continue
-    
+
     # Schema has an extra input for Length and for Extend.
     if node.bl_idname == 'MantisSchemaGroup':
         found_in.extend(['Schema Length', ''])
-    
+
     # get the socket maps before modifying stuff
     if update_input or update_output:
         socket_maps = get_socket_maps(node,)
@@ -405,7 +412,7 @@ def node_group_update(node, force = False):
             # We have to initialize the node because it only has its base inputs.
         elif socket_maps is None:
             node.id_data.do_live_update = toggle_update
-    
+
     # if we have too many elements, just get rid of the ones we don't need
     if len(node.inputs) > len(found_in):#
         for inp in node.inputs:
@@ -441,7 +448,7 @@ def node_group_update(node, force = False):
                     remove_me.append(socket)
             while remove_me:
                 node.inputs.remove(remove_me.pop())
-            
+
         if update_output:
             remove_me=[]
             for socket in node.outputs:
@@ -465,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
@@ -502,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
@@ -565,10 +572,10 @@ class MantisNodeGroup(Node, MantisUINode):
             return "Node Group"
         else:
             return self.node_tree.name
-    
+
     def draw_buttons(self, context, layout):
         group_draw_buttons(self, context, layout)
-        
+
     def update(self):
         if self.node_tree is None:
             return
@@ -603,7 +610,7 @@ def poll_node_tree_schema(self, object):
 class SchemaGroup(Node, MantisUINode):
     bl_idname = "MantisSchemaGroup"
     bl_label = "Node Schema"
-    
+
     node_tree:PointerProperty(type=NodeTree, poll=poll_node_tree_schema, update=node_tree_prop_update,)
     is_updating:BoolProperty(default=False)
 
@@ -615,7 +622,7 @@ class SchemaGroup(Node, MantisUINode):
             return "Schema Group"
         else:
             return self.node_tree.name
-        
+
     def update(self):
         if self.is_updating: # update() can be called from update() and that leads to an infinite loop.
             return           # so we check if an update is currently running.
@@ -734,12 +741,12 @@ class MantisNode:
     @property
     def name(self):
         return self.ui_signature[-1]
-    
+
     @property
     def bl_idname(self): # this and the above exist solely to maintain interface w/bpy.types.Node
         from .utilities import get_node_prototype
         return get_node_prototype(self.ui_signature, self.base_tree).bl_idname
-    
+
     def reset_execution(self) -> None:
         """ Reset the node for additional execution without re-building the tree."""
         self.drivers={}; self.bObject=None
@@ -757,7 +764,7 @@ class MantisNode:
             self.parameters[socket.name] = None
         for key, value in additional_parameters.items():
             self.parameters[key]=value
-    
+
     def gen_property_socket_map(self) -> dict:
         props_sockets = {}
         for s_template in self.socket_templates:
@@ -769,7 +776,7 @@ class MantisNode:
                 for index, sub_prop in enumerate(s_template.blender_property):
                     props_sockets[sub_prop]=( (s_template.name, index),s_template.default_value[index] )
         return props_sockets
-    
+
     def set_traverse(self, traversal_pairs = [(str, str)]) -> None:
         for (a, b) in traversal_pairs:
             self.inputs[a].set_traverse_target(self.outputs[b])
@@ -780,12 +787,12 @@ class MantisNode:
             inp.flush_links()
         for out in self.outputs.values():
             out.flush_links()
-    
+
     def update_socket_value(self, blender_property, value) -> bool:
         change_handled=False
         if self.node_type == 'LINK':
             if len(self.bObject) == 0: # - there are no downstream xForms
-                return True # so there is nothing to do here 
+                return True # so there is nothing to do here
             for b_ob in self.bObject:
                 try:
                     setattr(b_ob, blender_property, value)
@@ -835,7 +842,7 @@ class MantisNode:
                             change_handled=False
                 break # we don't have to look through any more socket templates
         return change_handled
-    
+
     # the goal here is to tag the node as unprepared
     # but some nodes are always prepared, so we have to kick it forward.
     def reset_execution_recursive(self):
@@ -843,7 +850,7 @@ class MantisNode:
         if self.prepared==False: return # all good from here
         for conn in self.hierarchy_connections:
             conn.reset_execution_recursive()
-    
+
     def evaluate_input(self, input_name, index=0)  -> Any:
         from .node_container_common import trace_single_line
         if not (self.inputs.get(input_name)): # get the named parameter if there is no input
@@ -853,12 +860,12 @@ class MantisNode:
         trace = trace_single_line(self, input_name, index)
         prop = trace[0][-1].parameters[trace[1].name] #trace[0] = the list of traced nodes; read its parameters
         return prop
-    
+
     def fill_parameters(self, ui_node=None)  -> None:
         from .utilities import get_node_prototype
         from .node_container_common import get_socket_value
         if not ui_node:
-            if ( (self.signature[0] in  ["MANTIS_AUTOGENERATED", "SCHEMA_AUTOGENERATED" ]) or 
+            if ( (self.signature[0] in  ["MANTIS_AUTOGENERATED", "SCHEMA_AUTOGENERATED" ]) or
                 (self.signature[-1] in ["NodeGroupOutput", "NodeGroupInput"]) ): # I think this is harmless
                 return None
             else: # BUG shouldn't this use ui_signature??
@@ -894,7 +901,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
@@ -905,7 +911,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)
@@ -913,7 +918,7 @@ class MantisNode:
             if self in solved:
                 break
         return
-    
+
     # gets targets for constraints and deformers and should handle all cases
     def get_target_and_subtarget(self, constraint_or_deformer, input_name = "Target"):
         from bpy.types import PoseBone, Object, SplineIKConstraint
@@ -925,7 +930,7 @@ class MantisNode:
                 else:
                     name = 'NAME NOT FOUND'
                 prRed(f"No {input_name} target found for {name} in {self} because there is no connected node, or node is wrong type")
-                return 
+                return
             if (isinstance(target.bGetObject(), PoseBone)):
                 subtarget = target.bGetObject().name
                 target = target.bGetParentArmature()
@@ -933,7 +938,7 @@ class MantisNode:
                 target = target.bGetObject()
             else:
                 raise RuntimeError("Cannot interpret constraint or deformer target!")
-        
+
         if   (isinstance(constraint_or_deformer, SplineIKConstraint)):
                 if target and target.type not in ["CURVE"]:
                     raise GraphError(wrapRed("Error: %s requires a Curve input, not %s" %
@@ -960,12 +965,12 @@ class MantisNode:
         return
     def bModifierApply(self, bContext=None):
         return
-    
+
     if environ.get("DOERROR"):
-        def __repr__(self): 
+        def __repr__(self):
             return self.signature.__repr__()
     else:
-        def __repr__(self): 
+        def __repr__(self):
             return self.ui_signature.__repr__()
 
 # do I need this and the link class above?
@@ -1001,8 +1006,8 @@ class NodeLink:
     from_socket = None
     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
@@ -1011,11 +1016,12 @@ 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
-    
+
     def __repr__(self):
         return self.from_node.outputs[self.from_socket].__repr__() + " --> " + self.to_node.inputs[self.to_socket].__repr__()
         # link_string =   # if I need to colorize output for debugging.
@@ -1023,12 +1029,12 @@ class NodeLink:
         #     return wrapOrange(link_string)
         # else:
         #     return wrapWhite(link_string)
-    
+
     def die(self):
         self.is_alive = False
         self.to_node.inputs[self.to_socket].flush_links()
         self.from_node.outputs[self.from_socket].flush_links()
-    
+
     def insert_node(self, middle_node, middle_node_in, middle_node_out, re_init_hierarchy = True):
         to_node = self.to_node
         to_socket = self.to_socket
@@ -1046,7 +1052,7 @@ class NodeSocket:
     # @property # this is a read-only property.
     # def is_linked(self):
     #     return bool(self.links)
-        
+
     def __init__(self, is_input = False,
                  node = None, name = None,
                  traverse_target = None):
@@ -1059,8 +1065,8 @@ class NodeSocket:
         self.is_linked = False
         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
@@ -1077,24 +1083,25 @@ 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):
         self.traverse_target = traverse_target
         self.can_traverse = True
-    
+
     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
     def is_connected(self):
         return len(self.links)>0
-    
-    
+
+
     def __repr__(self):
         return self.node.__repr__() + "::" + self.name
 
@@ -1102,7 +1109,7 @@ class MantisNodeSocketCollection(dict):
     def __init__(self, node, is_input=False):
         self.is_input = is_input
         self.node = node
-    
+
     def init_sockets(self, sockets):
         for socket in sockets:
             if isinstance(socket, str):
@@ -1112,15 +1119,14 @@ class MantisNodeSocketCollection(dict):
                 self[socket.name] = NodeSocket(is_input=self.is_input, name=socket.name, node=self.node)
             else:
                 raise RuntimeError(f"NodeSocketCollection keys must be str or MantisSocketTemplate, not {type(socket)}")
-            
+
     def __delitem__(self, key):
         """Deletes a node socket by name, and all its links."""
         socket = self[key]
         for l in socket.links:
             l.die()
         super().__delitem__(key)
-    
+
     def __iter__(self):
         """Makes the class iterable"""
         return iter(self.values())
-

+ 23 - 5
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):
@@ -41,8 +41,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):
@@ -50,7 +50,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
+        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'
@@ -205,20 +226,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
@@ -804,7 +812,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
@@ -829,8 +836,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):

+ 2 - 1
misc_nodes.py

@@ -1935,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

+ 166 - 97
readtree.py

@@ -1,92 +1,104 @@
 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
@@ -122,7 +131,7 @@ def make_connections_to_ng_dummy(base_tree, tree_path_names, local_nc, all_nc, n
             signature = ("MANTIS_AUTOGENERATED", *tree_path_names, nc_to.ui_signature[-1], inp.name, inp.identifier, str(uuid4()))
             nc_from = all_nc.get(signature) # creating this without checking and
             #  using UUID signature leads to TERRIBLE CONFUSING BUGS.
-            if nc_from is None: 
+            if nc_from is None:
                 nc_from = autogen_node(base_tree, inp, signature, nc_to.mContext)
             from .node_container_common import get_socket_value
             if nc_from: # autogen can fail and we should catch it.
@@ -189,10 +198,13 @@ def data_from_tree(base_tree, tree_path, dummy_nodes, all_nc, all_schema):#
         from .utilities import clear_reroutes
         links = clear_reroutes(list(current_tree.links))
         gen_node_containers(base_tree, current_tree, tree_path_names, all_nc, local_nc, dummy_nodes, group_nodes, all_schema)
-        
+
         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], dummy_nodes, all_nc, all_schema)
@@ -246,7 +258,7 @@ schema_bl_idnames = [   "SchemaIndex",
                         "SchemaConstInput",
                         "SchemaConstOutput",
                         "SchemaOutgoingConnection",
-                        "SchemaIncomingConnection", 
+                        "SchemaIncomingConnection",
                     ]
 
 from .utilities import get_all_dependencies
@@ -261,7 +273,7 @@ def get_schema_length_dependencies(node, all_nodes={}):
             for l in inp.links:
                 if not l.from_node in node.hierarchy_dependencies:
                     continue
-                if "MANTIS_AUTOGENERATED" in l.from_node.signature: 
+                if "MANTIS_AUTOGENERATED" in l.from_node.signature:
                     deps.extend([l.from_node]) # why we need this lol
                 if inp.name in prepare_links_to:
                     deps.append(l.from_node)
@@ -292,11 +304,73 @@ 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
     base_tree.execution_id = uuid4().__str__() # set the unique id of this execution
-    
+
     from .base_definitions import MantisExecutionContext
     mContext = MantisExecutionContext(base_tree=base_tree)
 
@@ -305,13 +379,13 @@ def parse_tree(base_tree, error_popups=False):
     # annoyingly I have to pass in values for all of the dicts because if I initialize them in the function call
     #  then they stick around because the function definition inits them once and keeps a reference
     # so instead I have to supply them to avoid ugly code or bugs elsewhere
-    # it's REALLY confusing when you run into this sort of problem. So it warrants four entire lines of comments!      
+    # it's REALLY confusing when you run into this sort of problem. So it warrants four entire lines of comments!
     dummy_nodes, all_mantis_nodes, all_schema =  data_from_tree(base_tree, tree_path = [None], dummy_nodes = {}, all_nc = {}, all_schema={})
     for dummy in dummy_nodes.values():    # reroute the links in the group nodes
         if (hasattr(dummy, "reroute_links")):
             dummy.reroute_links(dummy, all_mantis_nodes)
     prGreen(f"Pulling data from tree took {time.time() - data_start_time} seconds")
-    
+
     start_time = time.time()
     solve_only_these = []; solve_only_these.extend(list(all_schema.values()))
     roots, array_nodes = [], []
@@ -365,7 +439,7 @@ def parse_tree(base_tree, error_popups=False):
             for dep in n.hierarchy_dependencies:
                 if dep not in schema_solve_done and (dep in solve_only_these):
                     if dep.prepared:
-                        continue 
+                        continue
                     solve_layer.appendleft(n)
                     break
             else:
@@ -398,7 +472,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:
@@ -420,7 +493,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
@@ -499,7 +573,7 @@ def sort_execution(nodes, xForm_pass):
             execution_failed = True
             raise GraphError("There is probably a cycle somewhere in the graph. "
                                 "Or a connection missing in a Group/Schema Input")
-        i+=1    
+        i+=1
         n = xForm_pass.pop()
         if visited.get(n.signature) is not None:
             visited[n.signature]+=1
@@ -556,9 +630,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:
@@ -595,11 +668,9 @@ 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'
@@ -612,8 +683,7 @@ def execute_tree(nodes, base_tree, context, error_popups = False):
                 if 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'
@@ -627,10 +697,10 @@ def execute_tree(nodes, base_tree, context, error_popups = False):
                 if error_popups == False:
                     raise e
                 execution_failed = True; break
-                
+
         for ob in switch_me:
             ob.data.pose_position = 'POSE'
-        
+
         tot_time = (time() - start_execution_time)
         if not execution_failed:
             prGreen(f"Executed tree of {len(sorted_nodes)} nodes in {tot_time} seconds")
@@ -656,4 +726,3 @@ def execute_tree(nodes, base_tree, context, error_popups = False):
                 ob.select_set(True)
             except RuntimeError: # it isn't in the view layer
                 pass
-

+ 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' )

+ 7 - 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:
@@ -101,9 +101,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':
@@ -386,7 +386,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?

+ 61 - 24
utilities.py

@@ -247,6 +247,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:
@@ -268,26 +290,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
@@ -295,9 +331,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
@@ -589,7 +625,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:
@@ -604,7 +641,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:
@@ -618,7 +655,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:

+ 38 - 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,43 @@ 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 > 12: return# minor version must be 12 or less
+    if current_minor_version == 12 and current_sub_version > 27: return 
+    # 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 +289,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']),
 ]
 
 

+ 35 - 50
visualize.py

@@ -21,7 +21,7 @@ class MantisVisualizeNode(Node):
     @classmethod
     def poll(cls, ntree):
         return (ntree.bl_idname in ['MantisVisualizeTree'])
-    
+
     def init(self, context):
         pass
 
@@ -32,7 +32,7 @@ class MantisVisualizeNode(Node):
             label+=elem+', '
         label = label[:-2] # cut the last comma
         return label
-    
+
     def gen_data(self, mantis_node, mode='DEBUG_CONNECTIONS'):
         from .utilities import get_node_prototype
         if mantis_node.node_type in ['SCHEMA', 'DUMMY']:
@@ -49,7 +49,7 @@ class MantisVisualizeNode(Node):
             case 'DRIVER':       self.color = (0.7, 0.05, 0.8)
             case 'DUMMY_SCHEMA': self.color = (0.85 ,0.95, 0.9)
             case 'DUMMY':        self.color = (0.05 ,0.05, 0.15)
-        
+
 
         self.name = '.'.join(mantis_node.signature[1:]) # this gets trunc'd
         self.signature = '|'.join(mantis_node.signature[1:])
@@ -65,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
@@ -95,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":
@@ -103,15 +109,15 @@ class MantisVisualizeNode(Node):
                             continue
                 self.outputs.new('WildcardSocket', out.name)
 
-def gen_vis_node( mantis_node, 
-                  vis_tree, 
+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)
@@ -124,7 +130,7 @@ def gen_vis_node( mantis_node,
             if l.to_node in mantis_node.hierarchy_connections:
                 links.add(l)
     return vis
-                
+
 def visualize_tree(m_nodes, base_tree, context):
     # first create a MantisVisualizeTree
     from .readtree import check_and_add_root
@@ -135,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():
@@ -151,12 +156,13 @@ 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')
 
-            for m in mantis_nodes:
-                nodes[m.signature]=gen_vis_node(m, vis_tree,all_links)
+            for i, m in enumerate(mantis_nodes):
+                nodes[m.signature]=gen_vis_node(m, vis_tree, all_links)
                 # useful for debugging: check the connections for nodes that are
                 # not in the parsed tree or available from trace_all_nodes_from_root.
 
@@ -164,8 +170,8 @@ def visualize_tree(m_nodes, base_tree, context):
                 # 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_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)
@@ -189,37 +195,16 @@ def visualize_tree(m_nodes, base_tree, context):
                     if output.is_linked:
                         return True
                 return False
-            
+
             no_links=[]
+            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)
 
-            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)
-            
-            # def side_len(n):
-            #     from math import floor
-            #     side = floor(n**(1/2)) + 1
-            #     return side
-            # side=side_len(len(no_links))
-            # break_me = True
-            # for i in range(side):
-            #     for j in range(side):
-            #         index = side*i+j
-            #         try:
-            #             n = no_links[index]
-            #             n.location.x = i*200
-            #             n.location.y = j*200
-            #         except IndexError:
-            #             break_me = True # it's too big, that's OK the square is definitely bigger
-            #             break
-            #     if break_me:
-            #         break
-            # from .utilities import SugiyamaGraph
-            # SugiyamaGraph(vis_tree, 1) # this can take a really long time
         finally:
             s = io.StringIO()
             sortby = SortKey.TIME
@@ -244,10 +229,10 @@ class MantisVisualizeOutput(Operator):
     def execute(self, context):
         from time import time
         from .utilities import wrapGreen, prGreen
-        
+
         tree=context.space_data.path[0].node_tree
         tree.update_tree(context)
         prGreen(f"Visualize Tree: {tree.name}")
         nodes = tree.parsed_tree
         visualize_tree(nodes, tree, context)
-        return {"FINISHED"}
+        return {"FINISHED"}

+ 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):