1
0

23 Incheckningar d87c7eff33 ... 5ce1e6fb59

Upphovsman SHA1 Meddelande Datum
  Joseph Brandenburg 5ce1e6fb59 Fix (?) mContext interface key no tag 1 vecka sedan
  Joseph Brandenburg fcd405ab0e Fix: root tree overlays not working 1 vecka sedan
  Joseph Brandenburg f5fb53ff83 fix: overlay button for Simple Input 1 vecka sedan
  Joseph Brandenburg 7c0e7e9aa9 fix: autogen nodes to overlay 1 vecka sedan
  Joseph Brandenburg fd9cbe326b fix: handle_overlay_detect types 1 vecka sedan
  Joseph Brandenburg 2b04d930b6 Use fill_parameters for autogen node 1 vecka sedan
  Joseph Brandenburg 361aeb7755 generate interface nodes during tree parse 1 vecka sedan
  Joseph Brandenburg fb1d3984c4 overlay always check first schema interface 1 vecka sedan
  Joseph Brandenburg 8749ca77b5 fix error message in get_from_curve_lut 1 vecka sedan
  Joseph Brandenburg 1b79536b5c initial implementation for number overlay 1 vecka sedan
  Joseph Brandenburg 6d31fdf63d Overlay Variables for Schema 1 vecka sedan
  Joseph Brandenburg f3f14b9658 Interfaces for Schema 1 vecka sedan
  Joseph Brandenburg 8820d2fbcc missing socket interface types 1 vecka sedan
  Joseph Brandenburg 6d58a1dc31 visualize: color sockets black 1 vecka sedan
  Joseph Brandenburg b2c4be425e Cache curves to avoid re-calculation 1 vecka sedan
  Joseph Brandenburg 32e4e465a8 Fix: Weights don't match targets in Arm. Constrt. 1 vecka sedan
  Joseph Brandenburg c5b213186b Fix: Schema failed to prepare MatricesFromCurve 1 vecka sedan
  josephbburg 2cce97db5c Merge branch 'patch-1' into 'master' 1 vecka sedan
  whalemenace 7d2b6df677 Fix: don't abort group-socket rebuild on color-set sockets 3 månader sedan
  josephbburg 874083c23a Merge branch 'kahn-sort-fifo' into 'master' 1 vecka sedan
  whalemenace 449cd55c07 kahn sort: pop FIFO (popleft) so execution order matches the previous tree walk 1 månad sedan
  josephbburg 232494be09 Merge branch 'fix/empty-mantis-tree-execution' into 'master' 1 vecka sedan
  Iken-O dd57051688 Fix empty Mantis tree execution loop 5 månader sedan
12 ändrade filer med 839 tillägg och 487 borttagningar
  1. 1 1
      __init__.py
  2. 23 7
      base_definitions.py
  3. 15 7
      dev_helpers/visualize.py
  4. 148 58
      execution/readtree.py
  5. 91 43
      execution/schema_solve.py
  6. 37 19
      internal_nodes.py
  7. 13 9
      link_nodes.py
  8. 233 217
      misc_nodes.py
  9. 22 2
      socket_definitions.py
  10. 161 47
      utilities/utilities.py
  11. 1 0
      versioning.py
  12. 94 77
      xForm_nodes.py

+ 1 - 1
__init__.py

@@ -273,8 +273,8 @@ def execute_handler(scene):
             # check here instead of in execute_tree because these  values can be
             #  modified at weird times and checking from the handler is more consistent
             if ( node_tree.tree_valid) and ( node_tree.do_live_update ):
-                node_tree.execute_tree(context)
                 node_tree.tree_valid=False
+                node_tree.execute_tree(context)
 
 from .versioning import versioning_tasks
 def node_version_update(node, do_once):

+ 23 - 7
base_definitions.py

@@ -315,7 +315,7 @@ class MantisTree(NodeTree):
                 self.parsed_tree = readtree.parse_tree(self, error_popups)
                 if context:
                     self.display_update(context)
-                self.tree_valid = True
+                self.tree_valid = bool(self.parsed_tree)
             except Exception as e:
                 prRed("Failed to update node tree due to error.")
                 self.tree_valid = False
@@ -352,6 +352,8 @@ class MantisTree(NodeTree):
         self.prevent_next_exec = False
         if not self.hash_recursive:
             return
+        if not self.parsed_tree:
+            return
         if self.is_exporting or self.is_executing:
             return
         prGreen("Executing Tree: %s" % self.name)
@@ -929,8 +931,8 @@ class MantisNode:
         "hierarchy_dependencies", "dependencies",
         "unmet_hierarchy_dependency_count",
         "prepared", "executed", "execution_prepared",
-        "mContext",
-        "is_schema_prototype"
+        "mContext", "is_schema_prototype",
+        "generated_at_schema_index",
         )
     node_type = 'UNINITIALIZED'
     socket_templates = None
@@ -955,6 +957,10 @@ class MantisNode:
         #  again without changing the readtree code much.
         self.mContext = None # for now I am gonna set this at runtime
         self.is_schema_prototype = False
+        # For nodes generated in a schema, it's a list so that we can store the whole path
+        # this is not strictly necessary for interface resolution but it is much easier
+        # initialize it as all -1, we'll modify this if we need to, e.g. in the schema
+        self.generated_at_schema_index = [-1] * len(self.signature)
         # I know it isn't "beautiful OOP" or whatever, but it is just easier
         # code should be simple and do things in the simplest way.
         # in the future I can refactor it, but it will require changes to 100+
@@ -994,10 +1000,10 @@ class MantisNode:
             if socket.ui_only == True: continue # used in UI but not in generation
             if socket.is_input:
                 self.inputs[socket.name] = NodeSocket(is_input=True, name=socket.name,
-                                    node=self,)
+                                    node=self, socket_type=socket.bl_idname)
             else:
                 self.outputs[socket.name] = NodeSocket(is_input=False, name=socket.name,
-                                    node=self,)
+                                    node=self, socket_type=socket.bl_idname)
             self.parameters[socket.name] = None
         for traverse_owner, traverse_target in traverse_in.items():
             self.inputs[traverse_owner].traverse_target = self.outputs[traverse_target]
@@ -1202,8 +1208,14 @@ class MantisNode:
                                                     tuple(v[8:12]),
                                                     tuple(v[12:16]) ))
                 elif (value := node_socket.default_value) is not None:
+                    if hasattr(node_socket, 'use_overlay') and node_socket.use_overlay:
+                        value = "$"+node_socket.overlay_text
+                        # TODO: the overlay system should use a math parser for numerical types
+                        #       that treats strings as variable names
+                        # but for now I am just gonna hamfist it like this lol
                     if isinstance(value, str) and "$" in value:
-                        value = handle_overlay_detect(self, node_socket.identifier, value)
+                        value = handle_overlay_detect(self,
+                            node_socket.identifier, node_socket.bl_idname, value)
                     self.parameters[key] = value
                 else:
                     raise RuntimeError(wrapRed("No value found for " + self.__repr__() + " when filling out node parameters for " + ui_node.name + "::"+node_socket.name))
@@ -1406,10 +1418,12 @@ class NodeSocket:
         "is_input",
         "links",
         "is_linked",
+        "socket_type",
     )
     def __init__(self, is_input = False,
                  node = None, name = None,
-                 traverse_target = None):
+                 traverse_target = None,
+                 socket_type=""):
         self.can_traverse = False # to/from the other side of the parent node
         self.traverse_target = None
         self.node = node
@@ -1417,6 +1431,7 @@ class NodeSocket:
         self.is_input = is_input
         self.links = []
         self.is_linked = False
+        self.socket_type = socket_type
         if (traverse_target):
             self.can_traverse = True
 
@@ -1470,6 +1485,7 @@ class MantisNodeSocketCollection(dict):
             if socket.ui_only == True: continue # used in UI but not in generation
             if socket.is_input != self.is_input: continue
             self[socket.name] = NodeSocket(is_input=self.is_input, name=socket.name, node=self.node)
+    
     def __delitem__(self, key):
         """Deletes a node socket by name, and all its links."""
         socket = self[key]

+ 15 - 7
dev_helpers/visualize.py

@@ -35,7 +35,7 @@ class MantisVisualizeNode(Node):
 
     def gen_data(self, mantis_node, mode='DEBUG_CONNECTIONS'):
         from ..utilities.utilities import get_ui_node
-        if mantis_node.node_type in ['SCHEMA', 'DUMMY']:
+        if mantis_node.node_type in ['SCHEMA', 'DUMMY', 'INTERFACE']:
             np=None
         elif mantis_node.ui_signature is None:
             np=None
@@ -68,6 +68,7 @@ class MantisVisualizeNode(Node):
                 multi = False
                 if len(inp.links) > 1: multi = True
                 s = self.inputs.new('WildcardSocket', inp.name, use_multi_input=multi)
+                s.color = (0.0, 0.0, 0.0, 1.0)
                 s.link_limit = 4000
                 try:
                     if sock := np.inputs.get(inp.name):
@@ -83,6 +84,7 @@ class MantisVisualizeNode(Node):
                             continue
 
                 s = self.outputs.new('WildcardSocket', out.name)
+                s.color = (0.0, 0.0, 0.0, 1.0)
                 try:
                     if sock := np.outputs.get(out.name):
                         s.color = sock.color_simple
@@ -101,18 +103,21 @@ class MantisVisualizeNode(Node):
                 multi = False
                 if len(inp.links) > 1: multi = True
                 s = self.inputs.new('WildcardSocket', inp.name)
+                s.color = (0.0, 0.0, 0.0, 1.0)
                 s.link_limit = 4000
             for out in mantis_node.outputs:
                 match mode:
                     case "DEBUG_CONNECTIONS":
                         if not out.is_connected:
                             continue
-                self.outputs.new('WildcardSocket', out.name)
+                s = self.outputs.new('WildcardSocket', out.name)
+                s.color = (0.0, 0.0, 0.0, 1.0)
 
 def gen_vis_node( mantis_node,
                   vis_tree,
                   links,
                   omit_simple=False,
+                  only_hierarchy_links=False
                  ):
     from ..base_definitions import array_output_types
     # if mantis_node.node_type == 'UTILITY' and \
@@ -123,11 +128,15 @@ def gen_vis_node( mantis_node,
     vis.gen_data(mantis_node)
     for inp in mantis_node.inputs.values():
         for l in inp.links:
-            if l.from_node in mantis_node.hierarchy_dependencies:
+            if only_hierarchy_links and l.from_node in mantis_node.hierarchy_dependencies:
+                links.add(l)
+            else:
                 links.add(l)
     for out in mantis_node.outputs.values():
         for l in out.links:
-            if l.to_node in mantis_node.hierarchy_connections:
+            if only_hierarchy_links and l.to_node in mantis_node.hierarchy_connections:
+                links.add(l)
+            else:
                 links.add(l)
     return vis
 
@@ -141,7 +150,7 @@ def visualize_tree(m_nodes, base_tree, context):
     import cProfile
     import pstats, io
     from pstats import SortKey
-    cull_no_links = False
+    cull_no_links = True
     with cProfile.Profile() as pr:
         try:
             trace_from_roots = True
@@ -228,8 +237,7 @@ class MantisVisualizeOutput(Operator):
 
     def execute(self, context):
         from time import time
-        from ..utilities.utilities import wrapGreen, prGreen
-
+        from ..utilities.utilities import prGreen
         tree=context.space_data.path[0].node_tree
         tree.update_tree(context)
         prGreen(f"Visualize Tree: {tree.name}")

+ 148 - 58
execution/readtree.py

@@ -45,12 +45,12 @@ def autogen_node(base_tree, ui_socket, signature, mContext):
     from ..internal_nodes import AutoGenNode
     mantis_node = AutoGenNode(signature, base_tree)
     mantis_node.mContext = mContext
-    mantis_node.outputs[ui_socket.name] = NodeSocket(is_input=False, name=ui_socket.name, node=mantis_node)
+    mantis_node.outputs[ui_socket.identifier] = NodeSocket(is_input=False, name=ui_socket.identifier, node=mantis_node)
+    mantis_node.parameters[ui_socket.identifier] = None
     mantis_node.ui_signature = None # does not exist in the UI
     return mantis_node
 
-# TODO: investigate whether I can set the properties in the downstream nodes directly.
-#       I am doing this in Schema Solver and it seems to work quite efficiently.
+
 def make_connections_to_ng_dummy(base_tree, tree_path_names, local_mantis_nodes, all_mantis_nodes, to_mantis_node):
     from ..socket_definitions import no_default_value
     for inp in to_mantis_node.ui_node.inputs:
@@ -61,26 +61,21 @@ def make_connections_to_ng_dummy(base_tree, tree_path_names, local_mantis_nodes,
         if not inp.is_linked: # make an autogenerated NC for the inputs of the group node
             # This can be run inside schema. Make it unique with uuid() to be safe.
             from uuid import uuid4
-            signature = ("MANTIS_AUTOGENERATED", *tree_path_names, to_mantis_node.ui_signature[-1], inp.name, inp.identifier, str(uuid4()))
+            signature = (None, *tree_path_names, to_mantis_node.ui_signature[-1]+"_"+inp.name+"_"+inp.identifier+"_"+str(uuid4()) )
             from_mantis_node = all_mantis_nodes.get(signature) # creating this without checking and
-            #  using UUID signature leads to TERRIBLE CONFUSING BUGS.
+                                                               #  using UUID signature leads to TERRIBLE CONFUSING BUGS.
             if from_mantis_node is None: 
                 from_mantis_node = autogen_node(base_tree, inp, signature, to_mantis_node.mContext)
+                local_mantis_nodes[signature]=from_mantis_node
+                all_mantis_nodes[signature]=from_mantis_node
             if from_mantis_node: # autogen can fail and we should catch it.
-                from mathutils import Matrix
-                from_mantis_node.parameters = {inp.name: 
-                                               Matrix(( tuple(inp.default_value[0:4]), \
-                                                        tuple(inp.default_value[4:8]), \
-                                                        tuple(inp.default_value[8:12]), \
-                                                        tuple(inp.default_value[12:16]) )) if inp.bl_idname == 'MatrixSocket' \
-                                                            else inp.default_value}
-                local_mantis_nodes[signature] = from_mantis_node; all_mantis_nodes[signature] = from_mantis_node
-                from_mantis_node.outputs[inp.name].connect(node=to_mantis_node, socket=to_s, sort_id=0)
+                from_mantis_node.fill_parameters(ui_node=to_mantis_node.ui_node)
+                from_mantis_node.outputs[inp.identifier].connect(node=to_mantis_node, socket=to_s, sort_id=0)
             else:
-                prRed("No available auto-generated class for input %s in %s" % (inp.name, np.name))
+                prRed("No available auto-generated class for input %s in %s" % (inp.name, to_mantis_node.ui_node.name))
 
 def gen_mantis_nodes(base_tree, current_tree, tree_path_names, mContext, all_mantis_nodes, local_mantis_nodes, dummy_nodes, group_nodes, schema_nodes, in_schema ):
-    from ..internal_nodes import DummyNode
+    from ..internal_nodes import DummyNode, GroupInterface
     for ui_node in current_tree.nodes:
         mantis_node = None
         if ui_node.bl_idname in ["NodeFrame", "NodeReroute"]:
@@ -92,18 +87,39 @@ def gen_mantis_nodes(base_tree, current_tree, tree_path_names, mContext, all_man
             if not local_mantis_nodes.get(sig):
                 mantis_node = DummyNode( signature=sig , base_tree=base_tree, ui_node=ui_node, ui_signature=ui_sig )
                 local_mantis_nodes[sig] = mantis_node; all_mantis_nodes[sig] = mantis_node; dummy_nodes[sig] = mantis_node
-                if ui_node.bl_idname in ["NodeGroupOutput"]:
+                if ui_node.bl_idname in {"NodeGroupOutput"}:
+                    # We need an interface node, let's make it now.
+                    items_tree = ui_node.id_data.interface.items_tree
+                    interface = GroupInterface(
+                        # we'll clip the 'NodeGroupOutput' from the signature
+                        ( *mantis_node.signature[:-1], "OutputInterface"),
+                        mantis_node.base_tree, 'OUTPUT', ui_node=mantis_node.ui_node,
+                        items_tree=items_tree,)
+                    interface.mContext = mContext
+                    all_mantis_nodes[interface.signature] = interface; local_mantis_nodes[interface.signature] = interface
                     mantis_node.reroute_links_type = 'out'
         elif ui_node.bl_idname in  ["MantisNodeGroup", "MantisSchemaGroup"]:
             mantis_node = DummyNode( signature= (sig := (None, *tree_path_names, ui_node.name) ), base_tree=base_tree, ui_node=ui_node )
+            mantis_node.mContext = mContext
             local_mantis_nodes[sig] = mantis_node; all_mantis_nodes[sig] = mantis_node; dummy_nodes[sig] = mantis_node
-            make_connections_to_ng_dummy(base_tree, tree_path_names, local_mantis_nodes, all_mantis_nodes, mantis_node)
-            group_nodes.append(mantis_node)
+            items_tree = ui_node.node_tree.interface.items_tree
             if ui_node.bl_idname == "MantisNodeGroup":
                 mantis_node.reroute_links_type = 'group'
+                name = 'InputInterface'
+                interface_in_out = 'INPUT'
             else:
-                mantis_node.reroute_links_type = 'schema'
                 schema_nodes[sig] = mantis_node
+                name = 'InputInterface.0000'
+                interface_in_out = 'SCHEMA_INPUT'
+            # let's make the interface node for this now
+            interface = GroupInterface(
+                ( *mantis_node.signature, name), mantis_node.base_tree,
+                   interface_in_out, mantis_node.ui_node, items_tree=items_tree )
+            interface.mContext = mContext
+            all_mantis_nodes[interface.signature] = interface; local_mantis_nodes[interface.signature] = interface
+            group_nodes.append(mantis_node)
+            # This function depends on generating the interface
+            make_connections_to_ng_dummy(base_tree, tree_path_names, local_mantis_nodes, all_mantis_nodes, mantis_node)
         # if it wasn't the types we ignore or the types we make a Dummy for, use this to catch all non-special cases.
         elif (mantis_class := ui_node.mantis_class):
             sig = (None, *tree_path_names, ui_node.name)
@@ -161,31 +177,65 @@ overlay_valid_socket_types = [
 forbidden_key_names = ["",]
 forbidden_in_key_name = ['$',]
 def node_scan_overlays(node, socket_id,):
+    from ..misc_nodes import SimpleInputNode
+    from ..internal_nodes import AutoGenNode
+    from ..utilities.utilities import get_ui_node
     from ..utilities.node_common import GraphError
     interface_nodes = node.mContext.interface_nodes
-    from ..utilities.utilities import get_ui_node
-    ui_node = get_ui_node(node.ui_signature, node.base_tree)
-    # we'll assume that the overlay tag system checked if this is linked.
-    # First let's check if it is a simple input node, by checking outputs that are marked "input"
-    for socket in ui_node.outputs:
-        # weird edge case for simple input nodes with unnamed sockets
-        # the socket.identifier is not used by Mantis but it defaults to 'socket'\
-        if socket.identifier == socket_id and socket.input == True: break
-    else:
-        for socket in ui_node.inputs:
-            if socket.identifier == socket_id: break
+    if not isinstance(node, AutoGenNode):
+        ui_node = get_ui_node(node.ui_signature, node.base_tree)
+        # we'll assume that the overlay tag system checked if this is linked.
+        # TODO this is probably redundant with the check above this
+        # First let's check if it is a simple input node, by checking outputs that are marked "input"
+        for socket in ui_node.outputs:
+            # weird edge case for simple input nodes with unnamed sockets
+            # the socket.identifier is not used by Mantis but it defaults to 'socket'\
+            if socket.identifier == socket_id and socket.input == True: break
         else:
-            raise KeyError(f"Socket not found by ID {socket_id} in node {node.ui_signature}\n"
-                        f"Candidates: {[socket.identifier for socket in ui_node.inputs]}")
-    value = socket.default_value # get the ACTUAL UI value
+            for socket in ui_node.inputs:
+                if socket.identifier == socket_id: break
+            else:
+                raise KeyError(f"Socket not found by ID {socket_id} in node {node.ui_signature}\n"
+                            f"Candidates: {[socket.identifier for socket in ui_node.inputs]}")
+        value = socket.default_value # get the ACTUAL UI value
+        if hasattr(socket, 'overlay_text'): # TODO this is all wrong but for now
+            value = "$"+socket.overlay_text # I am not implementing the Parser feature
+    else: # since it doesn't have a UI node
+        value = node.parameters[socket_id]
+    
+    overlay_data_type = str
+    if isinstance(node, (AutoGenNode) ):
+        overlay_data_type = type(node.parameters[list(node.outputs.values())[0].name])
+    else: # we have the UI socket
+        match socket.bl_idname:
+            case "IntSocket" | "UnsignedIntSocket":
+                overlay_data_type = int
+            case "FloatSocket" | "FloatPositiveSocket" | "FloatFactorSocket" | "FloatAngleSocket":
+                overlay_data_type = float
+    
     if not isinstance(value, str): return
     # First step: get the interface nodes and the overlay keys
     # this should only check interfaces that are ancestors of this node
     my_interfaces = []
     for index in range(len(node.signature)-1):
         if index == 0: continue # root
-        interface_signature = (*node.signature[:index+1], "InputInterface")
-        my_interfaces.append( interface_nodes[interface_signature] )
+        # we add one here because the node inside the schema references the schema's interface...
+        current_schema_interface_index = node.generated_at_schema_index[index+1]
+        if current_schema_interface_index < 0: # this indicates that it is not in a schema
+            interface = interface_nodes.get(
+                interface_signature := (None, *node.signature[1:index+1], "InputInterface"))
+        else: # First we get the interface that enters the schema (index 0)
+            interface = interface_nodes.get(
+                interface_signature := (None, *node.signature[1:index+1],
+                "InputInterface."+str( 0 ).zfill(4)))
+            if interface and interface not in my_interfaces: my_interfaces.append(interface)
+            if current_schema_interface_index == 0: continue # just did it
+            # we need to get the schema of the current index
+            interface = interface_nodes.get(
+                interface_signature := (None, *node.signature[1:index+1],
+                "InputInterface."+str(current_schema_interface_index).zfill(4)))
+        if interface:
+            my_interfaces.append( interface )
     # the my_interfaces list is sorted in REVERSE order of importance
     # Next, get all the overlay providers.
     name_to_id_map = {}
@@ -196,17 +246,21 @@ def node_scan_overlays(node, socket_id,):
     for interface_item in node.mContext.base_tree.interface.items_tree:
         if interface_item.item_type != "SOCKET": continue
         if interface_item.in_out != "INPUT": continue
-        # NOTE this will need to change if overlays extend to other data types
-        if hasattr(interface_item, "default_string"):
-            # NOTE that there may be duplicate names. Too bad!
-            root_overlay_values[interface_item.name] = interface_item.default_string
-        elif hasattr(interface_item, "default_int"):
-            root_overlay_values[interface_item.name] = str(interface_item.default_int).zfill(STRING_PADDING)
-        elif hasattr(interface_item, "default_float"):
-            root_overlay_values[interface_item.name] = str(interface_item.default_float).zfill(STRING_PADDING)
+        base_value=None
+        match interface_item.bl_socket_idname:
+            case "IntSocket" | "BooleanSocket":
+                # TODO parser for number types
+                base_value = interface_item.default_int
+            case "FloatSocket":
+                base_value = interface_item.default_float
+            case "StringSocket":
+                base_value = interface_item.default_string
+        if base_value:
+            root_overlay_values[interface_item.name] = base_value
         overlay_keys.add(interface_item.name)
         name_to_id_map[interface_item.name] = None # don't worry about it
         name_to_type_map[interface_item.name] = interface_item.bl_socket_idname
+    # BUG? should this be my_interfaces ? why bother with interfaces this may not be a part of?
     for interface_node in interface_nodes.values():
         # problem: the interface node stores socket identifiers as keys
         # so in reality we need to get the group UI node and ask for its input names
@@ -223,6 +277,14 @@ def node_scan_overlays(node, socket_id,):
                 name_to_id_map[interface_item.name]=interface_item.identifier
                 name_to_type_map[interface_item.name] = interface_item.bl_socket_idname
                 overlay_keys.add(interface_item.name)
+        # For schema interfaces, we also need to get the length and index.
+        if 'Index' in interface.outputs:
+            overlay_keys.add('Index'); overlay_keys.add('Length')
+    if 'Index' in overlay_keys:
+        name_to_id_map['Index']='Index'
+        name_to_type_map['Index']= "IntSocket"
+        name_to_id_map['Length']='Length'
+        name_to_type_map['Length']= "IntSocket"
     # sort by reverse length so we find "CatsAndFrogs" before "Cats"
     overlay_keys = sorted(overlay_keys, key= lambda a :-1*len(a))
     # First I need to break it into substrings based on quotation marks
@@ -231,7 +293,10 @@ def node_scan_overlays(node, socket_id,):
     overlay_tries = []
     for quoted_substring in  quoted_substrings:
         overlay_tries.extend(quoted_substring.split("$"))
-    result = overlay_tries[0] # the 0th element is always before the split character
+    if overlay_data_type == str:
+        result = overlay_tries[0] # the 0th element is always before the split character
+    else: # TODO HACK later use the parser feature
+        result = 0
     # the reason for the difference between overlay_try and key is because
     #  we don't actually know if the $text part has a key in it
     for overlay_try in overlay_tries[1:]:
@@ -241,7 +306,11 @@ def node_scan_overlays(node, socket_id,):
             if key in overlay_try:
                 overlay_resolved_value = "$" + key
                 for interface in reversed(my_interfaces): # since this is reversed when we build it
-                    if interface.inputs.get(actual_key := name_to_id_map.get(key)):
+                    actual_key = name_to_id_map.get(key)
+                    socket = interface.inputs.get(actual_key)
+                    if key in ['Index', 'Length']:
+                        socket = interface.outputs.get(actual_key)
+                    if socket:
                         overlay_value = None
                         try:
                             overlay_value = interface.evaluate_input(actual_key)
@@ -251,17 +320,20 @@ def node_scan_overlays(node, socket_id,):
                                                  f"an invalid input in the node group: {interface.signature[:-1]}.")
                             raise GraphError( f"Error: Variable {key} is not supported because it is "
                                               f"type {socket_type}" )
-                            continue
                             # NOTE that the above can also be caused by any BUG occuring in node group update
                             # because it causes the interface to be invalid for the UI node that spawns the interface node.
-                        
-                        if isinstance(overlay_value, (float, int)):
-                            overlay_value = str(overlay_value).zfill(STRING_PADDING)
-                        assert (isinstance(overlay_value, str)), f"Error: Variable {key} is not supported because it is type {type(overlay_value)}"
-                        overlay_resolved_value = overlay_try.replace(key, overlay_value)
+                        if overlay_data_type == str:
+                            if isinstance(overlay_value, (float, int)):
+                                overlay_value = str(overlay_value).zfill(STRING_PADDING)
+                            assert (isinstance(overlay_value, str)), f"Error: Variable {key} is not supported because it is type {type(overlay_value)}"
+                            overlay_resolved_value = overlay_try.replace(key, overlay_value)
+                        else:
+                            overlay_resolved_value = overlay_value
                         break
                 else: # we'll try the base tree
-                    if base_tree_result := root_overlay_values.get(overlay_try):
+                    if base_tree_result := root_overlay_values.get(key):
+                        if overlay_data_type == str and not isinstance(base_tree_result, str):
+                            base_tree_result = str(base_tree_result).zfill(STRING_PADDING)
                         overlay_resolved_value = base_tree_result
                     else:
                         prOrange(f"WARN: no overlay value applied for {node.ui_signature} for key {key}")
@@ -309,7 +381,7 @@ schema_bl_idnames = [   "SchemaIndex",
                         "SchemaIncomingConnection",
                     ]
 
-from ..utilities.utilities import get_all_dependencies
+from ..utilities.utilities import get_all_dependencies, read_schema_type
 def get_schema_length_dependencies(node, all_nodes={}):
     """ Get a list of all dependencies for the given node's length or array properties.
         This function will also recursively search for dependencies in its sub-trees.
@@ -326,6 +398,11 @@ def get_schema_length_dependencies(node, all_nodes={}):
                 if inp.name in prepare_links_to:
                     deps.append(l.from_node)
                     deps.extend(get_all_dependencies(l.from_node))
+                # we also always need to prepare these inputs because they create links.
+                # HACK below until I can be less stupid and solve this the right way
+                if l.from_node.__class__.__name__ in 'UtilityMatricesFromCurve':
+                    deps.append(l.from_node)
+                    deps.extend(get_all_dependencies(l.from_node))
     def deps_filter(dep): # remove any nodes inside the schema
         if len(dep.signature) > len(node.signature):
             for i in range(len(node.signature)):
@@ -474,8 +551,8 @@ def parse_tree(base_tree, error_popups=False):
         if mantis_node.__class__.__name__ in array_output_types:
             solve_only_these.append(mantis_node)
             array_nodes.append(mantis_node)
-        if mantis_node.node_type == 'INTERFACE':
-            mContext.interface_nodes[mantis_node.signature] = mantis_node
+        if mantis_node.node_type == 'INTERFACE': # strip the SCHEMA/AUTOGEN tag if it is there
+            mContext.interface_nodes[( None, *mantis_node.signature[1:]) ] = mantis_node
 
     from itertools import chain
     for schema in chain(all_schema.values(), array_nodes):
@@ -498,6 +575,18 @@ def parse_tree(base_tree, error_popups=False):
 
     solve_only_these = set(solve_only_these)
 
+
+    overlay_tagged_nodes = {}
+    for node_signature, socket_id in mContext.tagged_for_overlay:
+        if overlay_tagged_nodes.get(node_signature):
+            overlay_tagged_nodes[node_signature].append(socket_id)
+        else:
+            overlay_tagged_nodes[node_signature] = [socket_id]
+    def check_and_apply_overlay(n):
+        if overlays_for_this_node := overlay_tagged_nodes.get(n.signature):
+            for socket_id in overlays_for_this_node:
+                node_scan_overlays(n, socket_id,)
+
     solve_layer = unsolved_schema.copy(); solve_layer.extend(roots)
     while(solve_layer):
         n = solve_layer.pop()
@@ -535,6 +624,7 @@ def parse_tree(base_tree, error_popups=False):
                 if dep not in schema_solve_done:
                     break
             else:
+                check_and_apply_overlay(n)
                 try:
                     n.bPrepare()
                 except Exception as e:
@@ -661,8 +751,8 @@ def sort_execution(nodes, error_popups):
     
     processed_count = 0
     while(xForm_pass):
-        # for some reason using popleft here breaks everything... better not push my luck/
-        n = xForm_pass.pop()
+        n = xForm_pass.popleft()
+        # if n.execution_prepared: continue
         n.execution_prepared = True
         sorted_nodes.append(n)
         processed_count+=1

+ 91 - 43
execution/schema_solve.py

@@ -7,11 +7,25 @@ from ..base_definitions import (SchemaUINode, custom_props_types, \
     MantisNodeGroup, SchemaGroup, replace_types, GraphError, links_sort_key)
 from ..utilities.node_common import setup_custom_props_from_np
 # a class that solves Schema nodes
-from bpy.types import NodeGroupInput, NodeGroupOutput
+from bpy.types import NodeGroupInput, NodeGroupOutput, NodeSocket
 
 from .readtree import execution_error_cleanup
 
 class SchemaSolver:
+    __slots__ = (
+        "all_nodes", "node", "solved", "tree",
+        "uuid", "error_popups", "signature",
+        "schema_nodes", "solved_nodes",
+        "incoming_connections", "outgoing_connections",
+        "constant_in", "constant_out",
+        "array_input_connections", "array_output_connections",
+        "nested_schemas", "autogenerated_nodes",
+        "held_links", "tree_path_names",
+        "autogen_path_names", "is_node_group",
+        "index_link", "solve_length", "index",
+        "interfaces", "index_str", "prev_index_str",
+        "current_interface", "next_interface"
+    )
     def __init__(self, schema_dummy, nodes, ui_node, signature=None, error_popups=False):
         self.all_nodes = nodes # this is the parsed tree from Mantis
         self.node = schema_dummy
@@ -24,16 +38,11 @@ class SchemaSolver:
             self.signature = signature
         else:
             self.signature = self.node.signature
-        self.schema_nodes={}
-        self.solved_nodes = {}
-        self.incoming_connections = {}
-        self.outgoing_connections = {}
-        self.constant_in = {}
-        self.constant_out = {}
-        self.array_input_connections = {}
-        self.array_output_connections = {}
-        self.nested_schemas = {}
-        self.autogenerated_nodes = {}
+        self.schema_nodes={}; self.solved_nodes = {}
+        self.incoming_connections = {}; self.outgoing_connections = {}
+        self.constant_in = {}; self.constant_out = {}
+        self.array_input_connections = {}; self.array_output_connections = {}
+        self.nested_schemas = {}; self.autogenerated_nodes = {}
         self.held_links = []
         self.tree_path_names  = [*self.node.signature] # same tree as the schema node
         self.autogen_path_names = ['SCHEMA_AUTOGENERATED', *self.node.signature[1:]]
@@ -51,19 +60,20 @@ class SchemaSolver:
         self.solve_length = self.node.evaluate_input("Schema Length")
         # I'm making this a property of the solver because the solver's data is modified as it solves each iteration
         self.index = 0
+        self.interfaces = []
 
         prWhite(f"\nExpanding schema {self.tree.name} in node {self.node.signature}"
                  f" with length {self.solve_length}.")
 
         self.init_schema_links()
-        self.set_index_strings()
+        self.generate_interface_nodes()
+        self.set_schema_per_interation_constants()
 
         # 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=links_sort_key)
 
-        from bpy.types import NodeGroupInput, NodeGroupOutput
         for ui_node in self.tree.nodes:
             # Make the mantis nodes for the special interface types
             # we use the bl_idname because all schema nodes should be single-instance
@@ -87,15 +97,6 @@ class SchemaSolver:
                 mantis_node = SchemaConstOutput(signature=signature, base_tree=self.node.base_tree, parent_schema_node=self.node)
                 self.schema_nodes[signature] = mantis_node
 
-    # Using lambdas here causes a lot of function calls, should be optimized out
-    # since after all this only needs to change when the index updates
-    def set_index_strings(self):
-        self.index_str = '.'+str(self.uuid)+'.'+str(self.index).zfill(4)
-        self.prev_index_str = '.'+str(self.uuid)+'.'+str(self.index-1).zfill(4)
-        if self.is_node_group:
-            self.index_str = ''
-            self.prev_index_str = ''
-
     def init_schema_links(self,):
         """ Sort and store the links to/from the Schema group node."""
         for item in self.tree.interface.items_tree:
@@ -143,7 +144,66 @@ class SchemaSolver:
                         if out_links := self.node.outputs[item.identifier].links:
                             self.array_output_connections[item.identifier] = out_links.copy()
 
-
+    # Using lambdas here causes a lot of function calls, should be optimized out
+    # since after all this only needs to change when the index updates
+    def set_schema_per_interation_constants(self):
+        # NOTE: I removed the UUID that has always been here
+        #  I can't figure out why I needed it and as far as I can tell there
+        #  really is no way for this to have name clashes. Tests work OK, too.
+        self.index_str = '.'+str(self.index).zfill(4)
+        self.prev_index_str = '.'+str(self.index-1).zfill(4)
+        self.current_interface = self.interfaces[self.index]
+        self.next_interface = self.interfaces[self.index + 1] # this is guaranteed
+        if self.is_node_group:
+            self.index_str = ''; self.prev_index_str = ''
+
+    def generate_interface_nodes(self):
+        from ..internal_nodes import GroupInterface
+        # One input interface per iteration, and one output interface
+        items_tree = self.tree.interface.items_tree
+        mContext=self.node.mContext
+        for i in range(self.solve_length): # one input per
+            index_str = '.'+str(i).zfill(4)
+            name = 'InputInterface'+index_str
+            if i == 0: in_out = 'SCHEMA_INPUT'
+            else:      in_out = 'SCHEMA_BETWEEN'
+            signature = (*self.autogen_path_names, name)
+            if not (interface := self.all_nodes.get( signature)):
+                interface = GroupInterface(signature=signature, base_tree=self.tree, in_out=in_out, 
+                            ui_node=None, items_tree=items_tree)
+                interface.mContext = mContext
+            # strip the AUTOGEN/SCHEMA tag if it is there
+            mContext.interface_nodes[( None, *interface.signature[1:]) ] = interface
+            interface.generated_at_schema_index = self.node.generated_at_schema_index + [i]
+            # add the index and length socket constants
+            interface.parameters['Index'] = i
+            if i == 0: # TODO I am duplicating Length for the interface feature, this is not necessary
+                interface.parameters['Length'] = self.solve_length
+            # the GroupInterface will initialize with the right sockets, but we still have to link them
+
+            if in_out == 'SCHEMA_INPUT': # Take the links into the schema and plug them here
+                for item in items_tree:
+                    if item.item_type == 'PANEL': continue
+                    if (mantis_link := self.constant_in.get(item.name)):
+                        mantis_link.from_node.outputs[mantis_link.from_socket].connect(interface, item.identifier)
+                    # for mantis_link in self.array_input_connections.get(item.name, []):
+                    #     mantis_link.from_node.outputs[mantis_link.from_socket].connect(interface, item.identifier)
+                    if (mantis_link := self.incoming_connections.get(item.name)):
+                        mantis_link.from_node.outputs[mantis_link.from_socket].connect(interface, item.identifier)
+            self.interfaces.append(interface)
+            # solved nodes will get added to the outer all_nodes later
+            self.solved_nodes[signature]=interface
+
+        # now do the output
+        signature = (*self.autogen_path_names, "OutputInterface" + index_str)
+        interface = GroupInterface(signature=signature, base_tree=self.tree,
+                    in_out='OUTPUT', ui_node=None, items_tree=items_tree)
+        self.interfaces.append(interface)
+        interface.generated_at_schema_index = i
+        self.solved_nodes[signature]=interface
+        interface.mContext = mContext
+        # strip the AUTOGEN/SCHEMA tag if it is there
+        mContext.interface_nodes[( None, *interface.signature[1:]) ] = interface
 
     def is_node_deeper_nested(self, queried_node, compare_node):
         return len(compare_node.signature) < len(queried_node.signature)
@@ -196,9 +256,11 @@ class SchemaSolver:
                 unprepared.append(mantis_node)
             if mantis_node.__class__.__name__ in custom_props_types:
                 setup_custom_props_from_np(mantis_node, ui_node)
-            mantis_node.fill_parameters(ui_node)
-            # be sure to pass on the Mantis Context to them
+            # be sure to pass on the Mantis Context to them.
             mantis_node.mContext=mContext
+            mantis_node.generated_at_schema_index = self.node.generated_at_schema_index + [self.index]
+            mantis_node.fill_parameters(ui_node)
+            assert mantis_node.mContext is not None
 
     def handle_link_from_index_input(self, index, frame_mantis_nodes, ui_link):
         from ..base_definitions import can_remove_socket_for_autogen
@@ -207,20 +269,9 @@ class SchemaSolver:
 
         if (not can_remove_socket_for_autogen(to_node, ui_link.to_socket.name)) or \
                                     to_node.node_type in ['DUMMY', 'DUMMY_SCHEMA']:
-            from .readtree import autogen_node
-            unique_name = "".join([
-                ui_link.to_socket.node.name+self.index_str,
-                ui_link.from_socket.name, ui_link.from_socket.identifier,
-                "==>", ui_link.to_socket.name, ui_link.to_socket.identifier,],)
-            signature = ("MANTIS_AUTOGENERATED", *self.tree_path_names[1:-1], unique_name)
-            from_node = self.all_nodes.get(signature)
-            if not from_node:
-                from_node = autogen_node(self.node.base_tree, ui_link.from_socket,
-                                signature=signature, mContext=self.node.mContext)
-                from_node.parameters = {ui_link.from_socket.name:index}
-                frame_mantis_nodes[signature]=from_node; self.solved_nodes[signature]=from_node
-                self.all_nodes[signature]=from_node
-            _connection = from_node.outputs[ui_link.from_socket.name].connect(node=to_node, socket=ui_link.to_socket.identifier)
+            from_node = self.current_interface
+            connection = from_node.outputs[ui_link.from_socket.name].connect(node=to_node, socket=ui_link.to_socket.identifier)
+            connection.is_hierarchy = False # For now this is the easiest way to do this
             return
         # Since the index is already determined, it is safe to remove the socket and just keep the value.
         to_node.parameters[ui_link.to_socket.name] = index
@@ -400,7 +451,6 @@ class SchemaSolver:
                              f" but it terminates at index {self.solve_length-1}. "
                              f"The Schema must have a length of at least {expose_when+1}.")
         from_name = get_link_in_out(ui_link)[0]
-        from bpy.types import NodeSocket
         #use it directly if it is a mantis node; this happens when the previous node was a Schema
         if isinstance( ui_link.from_socket, NodeSocket): # normal
             from_node = frame_mantis_nodes[(*self.autogen_path_names, from_name+self.index_str) ]
@@ -439,7 +489,6 @@ class SchemaSolver:
     def handle_link_to_array_output(self, frame_mantis_nodes, index, ui_link, to_ui_node, from_ui_node):# if this duplicated code works, dedupe!
         to_node = self.schema_nodes[(*self.tree_path_names, to_ui_node.bl_idname)] # get it by [], we want a KeyError if this fails
         from_name = get_link_in_out(ui_link)[0]
-        from bpy.types import NodeSocket
         #use it directly if it is a mantis node; this happens when the previous node was a Schema
         if isinstance( ui_link.from_socket, NodeSocket): # normal
             from_node = frame_mantis_nodes[(*self.autogen_path_names, from_name+self.index_str) ]
@@ -573,7 +622,7 @@ class SchemaSolver:
                                         SchemaIncomingConnection,)
         from ..utilities.utilities import clear_reroutes, link_mantis_nodes
         from ..base_definitions import array_output_types
-        self.set_index_strings()
+        self.set_schema_per_interation_constants()
         frame_mantis_nodes = {}
 
         # Later we have to run bPrepare() on these guys, so we make the deque and fill it now.
@@ -807,7 +856,6 @@ class SchemaSolver:
         for index in range(self.solve_length):
             self.index = index
             frame_mantis_nodes = self.solve_iteration()
-            self.set_index_strings()
             for sig, mantis_node in frame_mantis_nodes.items():
                 if mantis_node.node_type == 'DUMMY_SCHEMA':
                     self.nested_schemas[sig] = mantis_node

+ 37 - 19
internal_nodes.py

@@ -49,10 +49,7 @@ def grp_node_reroute_common(in_node, out_node, interface):
             out_link.die()
 
 def reroute_links_grp(group, all_nodes):
-    interface = GroupInterface(
-        ( *group.signature, "InputInterface"),
-        group.base_tree, group.ui_node, 'INPUT',)
-    all_nodes[interface.signature] = interface
+    interface = all_nodes.get( ( *group.signature, "InputInterface") )
     if group.inputs:
         if group_input := all_nodes.get(( *group.signature, "NodeGroupInput")):
             grp_node_reroute_common(group, group_input, interface)
@@ -60,12 +57,9 @@ def reroute_links_grp(group, all_nodes):
             raise RuntimeError("internal error: failed to enter a node group ")
 
 def reroute_links_grpout(group_output, all_nodes):
-    if (group := all_nodes.get( ( *group_output.signature[:-1],) )):
-        interface = GroupInterface(
-            ( *group.signature, "OutputInterface"),
-            group.base_tree, group.ui_node, 'OUTPUT',)
-        all_nodes[interface.signature] = interface
-        grp_node_reroute_common(group_output, group, interface)
+    if (group_output_node := all_nodes.get( ( *group_output.signature[:-1],) )):
+        interface = all_nodes.get( ( *group_output_node.signature, "OutputInterface") )
+        grp_node_reroute_common(group_output, group_output_node, interface)
     else:
         prOrange(f"WARN: unconnected outputs from a node group "
                  "(maybe you are running the tree from inside a node group?)")
@@ -83,7 +77,7 @@ class DummyNode(MantisNode):
         self.uuid = uuid4()
         self.solver = None
         self.node_type = 'DUMMY'
-        self.reroute_links_type='group' # group, out, schema
+        self.reroute_links_type=""
         if ui_node:
             if ui_node.bl_idname in ["MantisSchemaGroup"]:
                 self.node_type = 'DUMMY_SCHEMA'
@@ -109,8 +103,6 @@ class DummyNode(MantisNode):
         match self.reroute_links_type:
             case "group":
                 reroute_links_grp(self, all_nodes)
-            case "schema":
-                pass # gonna implement this later
             case "out":
                 reroute_links_grpout(self, all_nodes)
 
@@ -146,14 +138,40 @@ class AutoGenNode(MantisNode):
 class GroupInterface(MantisNode):
     __slots__ = ("in_out")
     node_type = 'INTERFACE'
-    def __init__(self, signature, base_tree, ui_node, in_out):
+    def __init__(self, signature, base_tree, in_out, ui_node = None, items_tree = None):
+        # DO: refactor me to use the tree interface
         super().__init__(signature, base_tree)
         self.prepared, self.executed = True, True
         self.in_out = in_out
         # init the sockets based on in/out, then set up traversal
-        collection = ui_node.inputs if in_out == 'INPUT' else ui_node.outputs
-        for socket in collection:
-            self.inputs[socket.identifier] = NodeSocket(is_input=True, node=self, name=socket.identifier)
-            self.outputs[socket.identifier] = NodeSocket(is_input=False, node=self, name=socket.identifier)
-        for socket in self.inputs.keys(): self.set_traverse( [(socket, socket)] )
+        
+        # the index and length are like constant inputs.
+        if self.in_out == 'SCHEMA_INPUT':
+            # this one is here because the tree expects it when linking things together
+            self.outputs["Schema Length"] = NodeSocket(is_input=False, node=self, name="Schema Length")
+            # HACK this one here is just there for overlays, really, doesn't hurt
+            self.outputs["Length"] = self.outputs["Schema Length"]
+        if 'SCHEMA' in self.in_out: 
+            self.outputs["Index"]  = NodeSocket(is_input=False, node=self, name="Index")
+               
+        for i, item in enumerate(items_tree):
+            if item.item_type == 'PANEL': continue
+            is_connection = False; is_array = False
+            if  hasattr(item, 'is_connection'): # in older files and for some types this happens
+                is_connection = item.is_connection # BUG really every socket should be a Mantis interface type
+                is_array = item.is_array
+            if (self.in_out in {'INPUT', 'OUTPUT'} and item.in_out == self.in_out):
+                source = self.inputs[item.identifier] = NodeSocket(is_input=True, node=self, name=item.identifier)
+                target = self.outputs[item.identifier] = NodeSocket(is_input=False, node=self, name=item.identifier)
+                self.set_traverse( [(source.name, target.name)] )
+            elif self.in_out == 'SCHEMA_INPUT':
+                if is_array: continue  # I'll skip these for now
+                source = self.inputs[item.identifier] = NodeSocket(is_input=True, node=self, name=item.identifier)
+                target = self.outputs[item.identifier] = NodeSocket(is_input=False, node=self, name=item.identifier)
+                self.set_traverse( [(source.name, target.name)] )
+            elif self.in_out == 'SCHEMA_BETWEEN':
+                if is_connection: # we need these at every in-between because they are modified between
+                    source = self.inputs[item.identifier] = NodeSocket(is_input=True, node=self, name=item.identifier)
+                    target = self.outputs[item.identifier] = NodeSocket(is_input=False, node=self, name=item.identifier)
+                    self.set_traverse( [(source.name, target.name)] )
         self.execution_prepared=True

+ 13 - 9
link_nodes.py

@@ -781,29 +781,33 @@ class LinkArmature(MantisLinkNode):
             #       are all in the same armature
             # Mantis should intelligently provide a working armature constraint
             # even if the user supplies targets that would not work in vanilla Blender
-            # TODO find out this is reading the arrays in reverse order? is that bad?
             targets_weights = {}
+            # NOTE: Mantis does not generally sort links because it is slow
+            #       so it is the responsibility of array types to do this.
+            from .base_definitions import links_sort_key
+            for inp in {'Targets', 'Weights'}:
+                self.inputs[inp].links.sort(key=links_sort_key)
             for i in range(num_targets):
                 target = c.targets.new()
                 self.get_target_and_subtarget(target, 'Targets', i)
                 weight_value=self.evaluate_input("Weights", i)
-                if not isinstance(weight_value, float):
-                    weight_value=0
-                targets_weights[i]=weight_value
-                from .mantis_dataclasses import MantisSocketFiller
                 default_value = 1.0
                 if self.inputs['Weights'].links:
                     default_value = 1/len(self.inputs['Weights'].links)
+                if not isinstance(weight_value, float):
+                    weight_value=default_value
+                targets_weights[i]=weight_value
+                from .mantis_dataclasses import MantisSocketFiller
+                property_group=f"target.{str(i).zfill(4)}"
                 prop_name = 'weight'
-                # prop_name = f"targets[{i}].weight"
                 filler = MantisSocketFiller(
-                    property_group = f"target.{str(i).zfill(4)}",
+                    property_group = property_group,
                     property_name = prop_name,
                     socket_name = "Weights",
                     array_index=i,
-                    default_value=default_value,
+                    default_value=weight_value,
                 )
-                b_objects[f"target.{str(i).zfill(4)}"] = target
+                b_objects[property_group] = target
                 props_sockets[prop_name+str(i)] = filler
             evaluate_sockets(self, b_objects, props_sockets)
             for target, value in targets_weights.items():

+ 233 - 217
misc_nodes.py

@@ -72,6 +72,167 @@ def TellClasses():
              UtilityPrint,
             ]
 
+# This cache is used for storing evaluated curve matrices
+# the idea is to get the data once and hold onto it
+# this prevents numerous wasteful calls to dg.update()
+CURVE_LUT_CACHE = {}
+CURVE_HASHES = {}
+SPLINE_HASHES ={}
+
+# todo: make a "matrix from 2 u" function, a "matrix for segments" function
+# basically I want the SplineLUT to handle all curve matrix getters
+class SplineLUT:
+    __slots__ = ("u_sample_points", "positions", "tangents", "normals", "hash", "curve_name")
+
+    def __init__(self, u_sample_points, positions, tangents, normals, hash, curve_name):
+        # points: ordered iterable of (u, position, tangent, normal)
+        # Each vector is an iterable of 3 floats.
+        self.u_sample_points = u_sample_points; self.hash = hash; self.curve_name = curve_name
+        self.positions = positions; self.tangents = tangents; self.normals = normals
+
+    def data_at_u(self, u):
+        if u <= 0 + FLOAT_EPSILON: return self.positions[0], self.tangents[0], self.normals[0]
+        elif u >= 1 - FLOAT_EPSILON: return self.positions[-1], self.tangents[-1], self.normals[-1]
+        else:
+            u_sample_points = self.u_sample_points
+            # get the index in the list just above the desired u value
+            from bisect import bisect_right # this finds an "insertion point" quickly
+            i = bisect_right(u_sample_points, u) 
+            u0 = u_sample_points[i-1]; u1 = u_sample_points[i]
+            if u - FLOAT_EPSILON >= u0 and u0 <= u + FLOAT_EPSILON :
+                factor = (u - u0) / (u1 - u0)
+                position = self.positions[i-1].lerp( self.positions[i], factor)
+                tangent = self.tangents[i-1].lerp( self.tangents[i], factor).normalized()
+                normal = self.normals[i-1].lerp( self.normals[i], factor).normalized()
+                return position, tangent, normal
+            else: # we're exactly on the vertex.
+                return self.positions[i-1], self.tangents[i-1], self.normals[i-1]
+    
+    def matrix_at_u(self, u):
+        from mathutils import Matrix
+        position, tangent, normal = self.data_at_u(u)
+        normal = (normal - tangent * normal.dot(tangent)).normalized()
+        bitangent = normal.cross(tangent).normalized()
+        matrix = Matrix.Identity(3)
+        matrix[0] = normal
+        matrix[1] = tangent
+        matrix[2] = bitangent
+        matrix.transpose(); matrix = matrix.to_4x4()
+        matrix.translation = position
+        return matrix
+
+    def matrix_from_start_end_u(self, start_u, end_u):
+        from mathutils import Matrix
+        position_start, _tangent_start, normal_start = self.data_at_u(start_u)
+        position_end, _tangent_end, _normal_end = self.data_at_u(end_u)
+        tangent = position_end - position_start
+        length = tangent.length
+        if tangent.length_squared < FLOAT_EPSILON:
+            raise RuntimeError(f"Failed to read the curve {self.curve_name}.")
+        tangent.normalize()
+        normal = (normal_start - tangent * normal_start.dot(tangent)).normalized()
+        bitangent = normal.cross(tangent).normalized()
+        matrix = Matrix.Identity(3)
+        matrix[0] = normal
+        matrix[1] = tangent
+        matrix[2] = bitangent
+        matrix.transpose(); matrix = matrix.to_4x4()
+        matrix.translation = position_start
+        matrix[3][3] = length # set the lenght with our HACK metadata thing
+        return matrix
+
+    def matrices_from_u_segments(self, u_values=[]):
+        return tuple( self.matrix_from_start_end_u(u_values[i], u_values[i+1]) for i in range(len(u_values)-1) )
+
+    def point_and_u_at_nearest_point_on_curve(self, point):
+        from mathutils.geometry import intersect_point_line
+        # because kd_find can return any number of points it always returns a list
+        nearest_point, index, distance = kd_find(self, self.positions, ref_pt=point, )[0]
+        # ONE of the segments this point belongs to will have the right value
+        max_index = len(self.u_sample_points)-1
+        if index >= 0 and index < max_index: # check the line between the found point and the next point
+            closest_point, parametric_distance_to = intersect_point_line(
+                            point, self.positions[index], self.positions[index + 1],)
+            if parametric_distance_to > 0 - FLOAT_EPSILON and parametric_distance_to < 1 + FLOAT_EPSILON:
+                # we have found our point
+                a = self.u_sample_points[index]; b = self.u_sample_points[index + 1]
+                return closest_point, (a + (b - a) * parametric_distance_to)
+        if index > 0 and index <= max_index: # check the previous segment
+            closest_point, parametric_distance_to = intersect_point_line(
+                            point, self.positions[index], self.positions[index - 1],)
+            if parametric_distance_to > 0 - FLOAT_EPSILON and parametric_distance_to < 1 + FLOAT_EPSILON:
+                # we have found our point
+                a = self.u_sample_points[index]; b = self.u_sample_points[index - 1]
+                return closest_point, (a + (b - a) * parametric_distance_to)
+
+    def __repr__(self):
+        return f"Cached curve {self.curve_name}"
+
+
+def hash_spline(spline) -> int:
+    # explicit tuple because Python will make a freakin' generator object otherwise
+    spline_metadata = ( spline.type, spline.resolution_u, spline.order_u, 
+                        spline.use_cyclic_u, spline.use_endpoint_u,
+                        spline.tilt_interpolation, spline.radius_interpolation, )
+    if spline.type == 'BEZIER':
+        points_data = tuple( ( *bp.co, *bp.handle_left, *bp.handle_right,
+                                bp.handle_right_type, bp.handle_left_type,
+                                bp.tilt, bp.radius) for bp in spline.bezier_points )
+    else:
+        points_data = tuple( (*p.co, p.tilt, p.radius ) for p in spline.points )
+    return hash( (spline_metadata, points_data) )
+
+def hash_curve(curve, spline_hashes) -> int:
+    curve_metadata = ( curve.dimensions, curve.twist_mode, curve.resolution_u, )
+    return hash( (curve_metadata, spline_hashes) )
+
+def fill_curve_lut(curve_name, bContext):
+    curve = bpy_object_get_guarded( curve_name )
+
+    if not curve: raise RuntimeError(f"WARN: No curve found for {curve_name}.")
+    elif curve.type != "CURVE": raise RuntimeError(f"WARN: Object {curve_name} is not a curve.")
+    else:
+        spline_hashes = tuple( hash_spline(spline) for spline in curve.data.splines)
+        curve_hash = hash_curve(curve.data, spline_hashes)
+        SPLINE_HASHES[curve.name] = spline_hashes; CURVE_HASHES[curve_name] = curve_hash
+        if existing_cache := CURVE_LUT_CACHE.get(curve_name):
+            if existing_cache['hash'] == curve_hash: return
+        mesh = get_mesh_from_curve(curve_name, "_MANTIS_CACHE_CURVE", bContext)
+        curve_lut_cache = []
+        from .utilities.utilities import all_data_from_ribbon, DetectRibbons
+        for i, ribbon in enumerate(DetectRibbons(mesh)):
+            spline_hash = spline_hashes[i]
+            if existing_cache := CURVE_LUT_CACHE.get(curve_name):
+                if len(existing_cache['spline_LUT']) >= i:
+                    if existing_cache['spline_LUT'][i].hash == spline_hash: continue
+            # othewise we need to get the data
+            # if I ever decide to use the radius in Mantis I'll add it to the LUT
+            u_samples, positions, normals, tangents, radii = all_data_from_ribbon(mesh, curve.matrix_world, ribbon)
+            for radius in radii:
+                if radius < FLOAT_EPSILON: # radius is 0:
+                    raise RuntimeError(zero_radius_error_message(None, curve))
+            spline_lut = SplineLUT(
+                u_samples, positions, tangents, normals, hash=spline_hash, curve_name=curve_name, )
+            curve_lut_cache.append(spline_lut)
+    
+    CURVE_LUT_CACHE[curve_name] = {}
+    CURVE_LUT_CACHE[curve_name]['hash'] = curve_hash
+    CURVE_LUT_CACHE[curve_name]['spline_LUT'] = curve_lut_cache
+    # remove the mesh, it is no longer needed - we have built a cache
+    # import bpy
+    # bpy.data.meshes.remove(mesh)
+
+def get_from_curve_lut(curve_name, spline_index, bContext, node_for_error=None) -> SplineLUT:
+    spline_lookup = None
+    try:
+        fill_curve_lut(curve_name=curve_name, bContext=bContext)
+        spline_lookup = CURVE_LUT_CACHE[curve_name]['spline_LUT'][spline_index]
+    except IndexError: # replace with a more helpful error message
+        raise IndexError(f"{node_for_error} is attempting to read from a spline in {curve_name} "
+                "that does not exist. Try and reduce the value of Spline Index.")
+    return spline_lookup
+
+
 def matrix_from_head_tail(head, tail, normal=None):
     from mathutils import Vector, Matrix
     if normal is None:
@@ -101,14 +262,7 @@ def get_mesh_from_curve(curve_name : str, execution_id : str, bContext, ribbon=T
             m.name = m_name
         return m
 
-def cleanup_curve(curve_name : str, execution_id : str) -> None:
-        import bpy
-        curve = bpy_object_get_guarded(curve_name)
-        m_name = curve_name+'.'+str(hash(curve.name+'.'+ execution_id))
-        if (mesh := bpy.data.meshes.get(m_name)):
-            bpy.data.meshes.remove(mesh)
-
-def kd_find(node, points, ref_pt, num_points):
+def kd_find(node, points, ref_pt, num_points = 1):
         if num_points == 0:
             raise RuntimeError(f"Cannot find 0 points for {node}")
         from mathutils import kdtree
@@ -220,8 +374,10 @@ class SimpleInputNode(MantisMiscNode):
         ui_outputs = ui_node.outputs
         node_socket = ui_outputs[0] # this is a simple input node.
         if (value := node_socket.default_value) is not None:
+            if hasattr(node_socket, 'use_overlay') and node_socket.use_overlay:
+                value = '$' + node_socket.overlay_text
             if isinstance(value, str) and "$" in value:
-                value = handle_overlay_detect(self, node_socket.identifier, value)
+                value = handle_overlay_detect(self, node_socket.identifier, node_socket.bl_idname, value)
             self.parameters[node_socket.name] = value
         
 #*#-------------------------------#++#-------------------------------#*#
@@ -326,7 +482,6 @@ class InputColorSetPallete(SimpleInputNode):
             self.parameters[o.name] = o.default_value
         return super().fill_parameters(ui_node)
 
-
 class UtilityCustomProperty(MantisMiscNode):
     __slots__ = ()
     socket_templates = UtilityCustomPropertySockets
@@ -380,80 +535,38 @@ class UtilityMatrixFromCurve(MantisMiscNode):
     def bPrepare(self, bContext = None,):
         from mathutils import Matrix
         import bpy
-        mat = Matrix.Identity(4)
-        curve_name = self.evaluate_input("Curve")
-        curve = bpy_object_get_guarded( curve_name, self)
-        mode = self.evaluate_input("Mode")
+        curve_name = self.evaluate_input("Curve"); mode = self.evaluate_input("Mode")
         # DIVISIONS, FACTOR
-        if not curve:
-            prRed(f"WARN: No curve found for {self}. Using an identity matrix instead.")
-            mat[3][3] = 1.0
-        elif curve.type != "CURVE":
-            prRed(f"WARN: Object {curve.name} is not a curve. Using an identity matrix instead.")
-            mat[3][3] = 1.0
-        else:
-            if bContext is None: bContext = bpy.context # is this wise?
-            m = get_mesh_from_curve(curve.name, self.base_tree.execution_id, bContext)
-            from .utilities.utilities import data_from_ribbon_mesh
-            #
-            num_divisions = self.evaluate_input("Total Divisions")
-            if mode == 'DIVISIONS' and num_divisions <= 0:
-                raise GraphError("The number of divisions in the curve must be 1 or greater.")
-            m_index = self.evaluate_input("Matrix Index")
-            if mode == 'DIVISIONS' and m_index >= num_divisions:
-                prRed(m_index, num_divisions)
-                raise GraphError(f"{self} tried to get a matrix-index greater the total number of divisions."
-                                  "The matrix index starts at 0. You're probably off by +1.")
-            spline_index = self.evaluate_input("Spline Index")
-            if spline_index > len(curve.data.splines)-1:
-                raise GraphError(f"{self} is attempting to read from a spline in {curve.name} that does not exist."
-                                  " Try and reduce the value of Spline Index.")
-            splines_factors = [ [] for i in range (spline_index)]
-            
-            if mode == 'FACTOR':
-                factor = self.evaluate_input('Factor')
-                factor = min(factor, 1 - 0.0001) # to make our fake tangent work
-                factors = [ factor, factor + 0.0001 ]
-                # TODO this is technically not accurate, we want the real tangent
-                # ... but this should be close enough.
-            elif mode == 'FACTOR_WITH_TAIL':
-                factor = self.evaluate_input('Factor')
-                factor = min(factor, 1 - 0.0001) # to make our fake tangent work
-                factors = [ factor,
-                            self.evaluate_input('Factor (Tail)')]
-            else:  # mode == 'DIVISIONS':
-                factors = [1/num_divisions*m_index, 1/num_divisions*(m_index+1)]
-            splines_factors.append(factors)
-            data = data_from_ribbon_mesh(m, splines_factors, curve.matrix_world)
-            if data[spline_index][1][0] < FLOAT_EPSILON: # radius is None:
-                raise RuntimeError(zero_radius_error_message(self, curve))
-            
-            head=data[spline_index][0][0]
-            tail= data[spline_index][0][1]
-            axis = (tail-head).normalized()
-            if axis.length_squared < FLOAT_EPSILON:
-                raise RuntimeError(f"Failed to read the curve {curve.name}.")
-            normal=data[spline_index][2][0]
-            # make sure the normal is perpendicular to the tail
-            from .utilities.utilities import make_perpendicular
-            normal = make_perpendicular(axis, normal)
-            mat = matrix_from_head_tail(head, tail, normal)
-            # this is in world space... let's just convert it back
-            mat.translation = head - curve.location
-            if mode == 'FACTOR':
-                mat[3][3]=1.0 # Set the length
-
-            # TODO HACK TODO
-            # all the nodes should work in world-space, and it should be the responsibility
-            # of the xForm node to convert!
-
-        self.parameters["Matrix"] = mat
+        if bContext is None: bContext = bpy.context # is this wise?
+        spline_index = self.evaluate_input("Spline Index")
+        spline_lookup : SplineLUT = get_from_curve_lut(curve_name, spline_index, bContext, self)
+        assert(spline_lookup)
+        #
+        num_divisions = self.evaluate_input("Total Divisions")
+        if mode == 'DIVISIONS' and num_divisions <= 0:
+            raise GraphError("The number of divisions in the curve must be 1 or greater.")
+        m_index = self.evaluate_input("Matrix Index")
+        if mode == 'DIVISIONS' and m_index >= num_divisions:
+            prRed(m_index, num_divisions)
+            raise GraphError(f"{self} tried to get a matrix-index greater the total number of divisions."
+                                "The matrix index starts at 0. You're probably off by +1.")
+        if mode == 'FACTOR':
+            factor = self.evaluate_input('Factor')
+            matrix = spline_lookup.matrix_at_u(factor)
+        elif mode == 'FACTOR_WITH_TAIL':
+            factor = self.evaluate_input('Factor')
+            factor_tail = self.evaluate_input('Factor (Tail)')
+            matrix = spline_lookup.matrix_from_start_end_u(factor, factor_tail)
+        else:  # mode == 'DIVISIONS':
+            factor = 1/num_divisions*m_index
+            factor_tail = 1/num_divisions*(m_index+1)
+            matrix = spline_lookup.matrix_from_start_end_u(factor, factor_tail)
+        
+        self.parameters["Matrix"] = matrix
         self.prepared = True
         self.executed = True
 
-    def bFinalize(self, bContext=None):
-        cleanup_curve(self.evaluate_input("Curve"), self.base_tree.execution_id)
-
+#TODO there is no harm in grabbing normal and tangent here, too
 class UtilityPointFromCurve(MantisMiscNode):
     '''Get a point from a curve'''
     __slots__ = ()
@@ -463,29 +576,14 @@ class UtilityPointFromCurve(MantisMiscNode):
     def bPrepare(self, bContext = None,):
         import bpy
         curve_name = self.evaluate_input("Curve")
-        curve = bpy_object_get_guarded( curve_name, self)
-        if not curve:
-            raise RuntimeError(f"No curve found for {self}.")
-        elif curve.type != "CURVE":
-            raise GraphError(f"ERROR: Object {curve.name} is not a curve.")
-        else:
-            if bContext is None: bContext = bpy.context # is this wise?
-            m = get_mesh_from_curve(curve.name, self.base_tree.execution_id, bContext)
-            from .utilities.utilities import data_from_ribbon_mesh
-            #
-            num_divisions = 1
-            spline_index = self.evaluate_input("Spline Index")
-            splines_factors = [ [] for i in range (spline_index)]
-            factors = [self.evaluate_input("Factor")]
-            splines_factors.append(factors)
-            data = data_from_ribbon_mesh(m, splines_factors, curve.matrix_world)
-            p = data[spline_index][0][0] - curve.location
-        self.parameters["Point"] = p
+        if bContext is None: bContext = bpy.context # is this wise?
+        spline_index = self.evaluate_input("Spline Index")
+        spline_lookup : SplineLUT = get_from_curve_lut(curve_name, spline_index, bContext, self)
+        factor = self.evaluate_input("Factor")
+        position, _tan, _nrm = spline_lookup.data_at_u(factor)
+        self.parameters["Point"] = position
         self.prepared, self.executed = True, True
 
-    def bFinalize(self, bContext=None):
-        cleanup_curve(self.evaluate_input("Curve"), self.base_tree.execution_id)
-
 class UtilityMatricesFromCurve(MantisMiscNode):
     '''Get matrices from a curve'''
     __slots__ = ()
@@ -493,41 +591,15 @@ class UtilityMatricesFromCurve(MantisMiscNode):
     passes=frozenset({'bPrepare', 'bFinalize'})
 
     def bPrepare(self, bContext = None,):
-        import time
-        # start_time = time.time()
-        #
-        from mathutils import Matrix
-        import bpy
-        m = Matrix.Identity(4)
         curve_name = self.evaluate_input("Curve")
-        curve = bpy_object_get_guarded( curve_name, self)
-        if not curve:
-            prRed(f"WARN: No curve found for {self}. Using an identity matrix instead.")
-            m[3][3] = 1.0
-        elif curve.type != "CURVE":
-            prRed(f"WARN: Object {curve.name} is not a curve. Using an identity matrix instead.")
-            m[3][3] = 1.0
-        else:
-            if bContext is None: bContext = bpy.context # is this wise?
-            mesh = get_mesh_from_curve(curve.name, self.base_tree.execution_id, bContext)
-            from .utilities.utilities import data_from_ribbon_mesh
-            num_divisions = self.evaluate_input("Total Divisions")
-            spline_index = self.evaluate_input("Spline Index")
-            splines_factors = [ [] for i in range (spline_index)]
-            factors = [0.0] + [(1/num_divisions*(i+1)) for i in range(num_divisions)]
-            splines_factors.append(factors)
-            data = data_from_ribbon_mesh(mesh, splines_factors, curve.matrix_world)
-            # [spline_index][points,tangents,normals][datapoint_index]
-            from .utilities.utilities import make_perpendicular
-            matrices=[]
-            for i in range(num_divisions):
-                if data[spline_index][1][i] < FLOAT_EPSILON: # radius is None:
-                    raise RuntimeError(zero_radius_error_message(self, curve))
-                m = matrix_from_head_tail (
-                data[spline_index][0][i], data[spline_index][0][i+1],
-                make_perpendicular((data[spline_index][0][i+1]-data[spline_index][0][i]).normalized(), data[spline_index][2][i]),)
-                m.translation = data[spline_index][0][i] - curve.location
-                matrices.append(m)
+        spline_index = self.evaluate_input("Spline Index")
+        import bpy
+        if bContext is None: bContext = bpy.context # is this wise?
+        spline_lookup : SplineLUT = get_from_curve_lut(curve_name, spline_index, bContext, self)
+    
+        num_divisions = self.evaluate_input("Total Divisions")
+        factors = [0.0] + [(1/num_divisions*(i+1)) for i in range(num_divisions)]
+        matrices = spline_lookup.matrices_from_u_segments(factors)
 
         for link in self.outputs["Matrices"].links:
             for i, m in enumerate(matrices):
@@ -535,23 +607,10 @@ class UtilityMatricesFromCurve(MantisMiscNode):
                 if not (out := self.outputs.get(name)): # reuse them if there are multiple links.
                     out = self.outputs[name] = NodeSocket(name = name, node=self)
                 c = out.connect(link.to_node, link.to_socket)
-                # prOrange(c)
                 self.parameters[name] = m
-                # print (mesh)
             link.die()
+        self.prepared = True; self.executed = True
 
-        self.prepared = True
-        self.executed = True
-        # prGreen(f"Matrices from curves took {time.time() - start_time} seconds.")
-
-    def bFinalize(self, bContext=None):
-        import bpy
-        curve_name = self.evaluate_input("Curve")
-        curve = bpy_object_get_guarded( curve_name, self)
-        m_name = curve.name+'.'+self.base_tree.execution_id
-        if (mesh := bpy.data.meshes.get(m_name)):
-            prGreen(f"Freeing mesh data {m_name}...")
-            bpy.data.meshes.remove(mesh)
 
 class UtilityNumberOfCurveSegments(MantisMiscNode):
     __slots__ = ()
@@ -565,8 +624,7 @@ class UtilityNumberOfCurveSegments(MantisMiscNode):
             self.parameters["Number of Segments"] = len(spline.bezier_points)-1
         else:
             self.parameters["Number of Segments"] = len(spline.points)-1
-        self.prepared = True
-        self.executed = True
+        self.prepared = True; self.executed = True
 
 class UtilityNumberOfSplines(MantisMiscNode):
     __slots__ = ()
@@ -586,61 +644,27 @@ class UtilityMatrixFromCurveSegment(MantisMiscNode):
     def bPrepare(self, bContext = None,):
         import bpy
         curve_name = self.evaluate_input("Curve")
-        curve = bpy_object_get_guarded( curve_name, self)
-        if not curve:
-            raise RuntimeError(f"No curve found for {self}.")
-        elif curve.type != "CURVE":
-            raise GraphError(f"ERROR: Object {curve.name} is not a curve.")
-        else:
-            if bContext is None: bContext = bpy.context # is this wise?
-            m = get_mesh_from_curve(curve.name, self.base_tree.execution_id, bContext)
-            from .utilities.utilities import data_from_ribbon_mesh
-            # this section is dumb, but it is because the data_from_ribbon_mesh
-            #  function is designed to pull data from many splines at once (for optimization)
-            #  so we have to give it empty splines for each one we skip.
-            # TODO: Refactor this to make it so I can select spline index
-            spline_index = self.evaluate_input("Spline Index")
-            spline = curve.data.splines[spline_index]
-            splines_factors = [ [] for i in range (spline_index)]
-            factors = [0.0]
-            points = spline.bezier_points if spline.type == 'BEZIER' else spline.points
-            total_length=0.0
-            for i in range(len(points)-1):
-                total_length+= (seg_length := (points[i+1].co - points[i].co).length)
-                factors.append(seg_length)
-            prev_length = 0.0
-            for i in range(len(factors)):
-                factors[i] = prev_length+factors[i]/total_length
-                prev_length=factors[i]
-                # Why does this happen? Floating point error?
-                if factors[i]>1.0: factors[i] = 1.0
-            splines_factors.append(factors)
-            #
-            data = data_from_ribbon_mesh(m, splines_factors, curve.matrix_world)
-            segment_index = self.evaluate_input("Segment Index")
-            if data[spline_index][1][segment_index] < FLOAT_EPSILON: # radius is None:
-                raise RuntimeError(zero_radius_error_message(self, curve))
-            head=data[spline_index][0][segment_index]
-            tail= data[spline_index][0][segment_index+1]
-            axis = (tail-head).normalized()
-            normal=data[spline_index][2][segment_index]
-            # make sure the normal is perpendicular to the tail
-            from .utilities.utilities import make_perpendicular
-            normal = make_perpendicular(axis, normal)
-            m = matrix_from_head_tail(head, tail, normal)
-            m.translation = head - curve.location
-            self.parameters["Matrix"] = m
+        curve = bpy_object_get_guarded(curve_name, self)
+        spline_index = self.evaluate_input("Spline Index")
+        if bContext is None: bContext = bpy.context # is this wise?
+        spline_lookup = get_from_curve_lut(curve_name, spline_index, bContext, self)
+        spline = curve.data.splines[spline_index]
+        factors = []
+        points = spline.bezier_points if spline.type == 'BEZIER' else spline.points
+        total_length=0.0
+        for i in range(len(points)): # points[i].co[:3] because nurbs have W element :/
+            _pt, u = spline_lookup.point_and_u_at_nearest_point_on_curve(points[i].co[:3])
+            factors.append(u)
+        segment_index = self.evaluate_input("Segment Index")
+        m = spline_lookup.matrix_from_start_end_u(factors[segment_index], factors[segment_index+1])
+        self.parameters["Matrix"] = m
         self.prepared, self.executed = True, True
 
-    def bFinalize(self, bContext=None):
-        cleanup_curve(self.evaluate_input("Curve"), self.base_tree.execution_id)
-
 class UtilityGetCurvePoint(MantisMiscNode):
     __slots__ = ()
     socket_templates = GetCurvePointSockets
 
     def bPrepare(self, bContext=None):
-        import bpy
         curve_name = self.evaluate_input("Curve")
         curve = bpy_object_get_guarded( curve_name, self)
         if not curve:
@@ -665,24 +689,12 @@ class UtilityGetNearestFactorOnCurve(MantisMiscNode):
     def bPrepare(self, bContext = None,):
         import bpy
         curve_name = self.evaluate_input("Curve")
-        curve = bpy_object_get_guarded( curve_name, self)
-        if not curve:
-            raise RuntimeError(f"No curve found for {self}.")
-        elif curve.type != "CURVE":
-            raise GraphError(f"ERROR: Object {curve.name} is not a curve.")
-        else:
-            if bContext is None: bContext = bpy.context # is this wise?
-            m = get_mesh_from_curve(curve.name,
-                                    self.base_tree.execution_id,
-                                    bContext, ribbon=False)
-            # this is confusing but I am not re-writing these old functions
-            from .utilities.utilities import FindNearestPointOnWireMesh as nearest_point
-            spline_index = self.evaluate_input("Spline Index")
-            ref_pt = self.evaluate_input("Reference Point")
-            splines_points = [ [] for i in range (spline_index)]
-            splines_points.append([ref_pt])
-            pt =  nearest_point(m, splines_points)[spline_index][0]
-            self.parameters["Factor"] = pt
+        spline_index = self.evaluate_input("Spline Index")
+        if bContext is None: bContext = bpy.context # is this wise?
+        spline_lookup = get_from_curve_lut(curve_name, spline_index, bContext, self)
+        ref_pt = self.evaluate_input("Reference Point")
+        _closest_point, u = spline_lookup.point_and_u_at_nearest_point_on_curve(ref_pt)
+        self.parameters["Factor"] = u
         self.prepared, self.executed = True, True
 
 class UtilityKDChoosePoint(MantisMiscNode):
@@ -922,6 +934,10 @@ class UtilityFCurve(MantisMiscNode):
             keys.append( self.evaluate_input('Keyframes', i) )
         if len(keys) <1:
             prOrange(f"WARN: no keys in fCurve {self}.")
+        # IT IS VERY IMPORTANT to sort the links
+        from .base_definitions import links_sort_key
+        for inp in {'Keyframes'}:
+            self.inputs[inp].links.sort(key=links_sort_key)
         keys.append(extrap_mode)
         self.parameters["fCurve"] = keys
         self.prepared = True

+ 22 - 2
socket_definitions.py

@@ -46,6 +46,24 @@ class MantisSocket(parent_classes):
     def draw_color_simple(self):
         return self.color_simple    
 
+class MantisSocketOverlayEnabled(MantisSocket):
+    # this class just needs a couple properties
+    use_overlay: bpy.props.BoolProperty( default=False )
+    overlay_text: bpy.props.StringProperty( default="", )
+    def draw(self, context, layout, node, text):
+        is_simple_out = ((hasattr(self, "default_value")) and hasattr(self, "input") and getattr(self, "input"))
+        if self.is_linked and not is_simple_out:
+            layout.label(text=getattr(self, "display_text", "") )
+        elif hasattr(self, "default_value"):
+            has_overlay = hasattr(self, "overlay_text") and hasattr(self, "use_overlay")
+            row = layout.row(align=True) if has_overlay else layout
+            # We'll show the property or the overlay field here
+            prop_field = "overlay_text" if (has_overlay and self.use_overlay) else "default_value"
+            row.prop(self, prop_field,text=self.name)
+            # and the button to choose whether or not to use the overlay field
+            if has_overlay: row.prop(self, "use_overlay", text="", icon='CONSOLE', toggle=True)
+        else:
+            layout.label(text=self.name)
 
 
 
@@ -438,6 +456,8 @@ def generate_custom_interface_types():
                 my_interface_draw = interface_string_draw
             case "xFormSocket":
                 my_interface_draw = interface_xform_draw
+            case _:
+                my_interface_draw = interface_draw
 
         interface = type(
                       name,
@@ -930,7 +950,7 @@ class QuaternionSocketAA(MantisSocket):
         default = (1.0, 0.0, 0.0, 0.0,),
         update = update_socket,)
 
-class IntSocket(MantisSocket):
+class IntSocket(MantisSocketOverlayEnabled):
     '''Custom node socket type'''
     bl_idname = 'IntSocket'
     bl_label = "Integer"
@@ -940,7 +960,7 @@ class IntSocket(MantisSocket):
     input : bpy.props.BoolProperty(default =False,)
     is_valid_interface_type=True
 
-class UnsignedIntSocket(MantisSocket):
+class UnsignedIntSocket(MantisSocketOverlayEnabled):
     '''Unsigned Integer Socket'''
     bl_idname = 'UnsignedIntSocket'
     bl_label = "Unsigned Integer"

+ 161 - 47
utilities/utilities.py

@@ -377,7 +377,7 @@ def relink_socket_map_add_socket(node, socket_collection, item,  in_out=None,):
             if not s.is_property_readonly("default_value"):
                 s.default_value = default_value
             else: # TODO: make this work with color sets.
-                raise Warning("WARN: Not yet implemented: default values for color sets.")
+                print("WARN: Not yet implemented: default values for color sets.")
     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'
@@ -964,19 +964,38 @@ def project_point_to_plane(point, origin, normal):
 # If a $ character is in a value, it is an overlay request
 # in the root tree, supply it immediately, otherwise tag this node
 # once we finish pulling data from the tree we handle the overlay.
-def handle_overlay_detect(node, socket_id, value) -> str:
+def handle_overlay_detect(node, socket_id, socket_idname, value) -> str:
     mContext = node.mContext
     root_tree = mContext.base_tree
     root_items_tree = root_tree.interface.items_tree
     # detect if it is in the root tree, if so, handle immediately
     out_value = value
-    if node.signature[0] is None and len(node.signature) ==2:
+    # what this recieves is determined by its idname
+    socket_type = str
+    match socket_idname:
+        case "IntSocket" | "UnsignedIntSocket":
+            socket_type = int
+        case "FloatSocket" | "FloatPositiveSocket" | "FloatFactorSocket" | "FloatAngleSocket":
+            socket_type = float
+    # ONLY for root node trees, we can just directly fill this.
+    if node.signature[0] is None and len(node.signature) == 2:
         for key, interface_item in root_items_tree.items():
             if interface_item.item_type != "SOCKET": continue
             if interface_item.in_out != "INPUT": continue
-            # NOTE this will need to change if overlays extend to other data types
-            if not hasattr(interface_item, "default_string"): continue
-            out_value = out_value.replace("$"+key, interface_item.default_string)
+            # then what the interface provides is determined by its idname in turn
+            # NOTE I'm not 100% sure checking the socket_type is the right thing to do here
+            match interface_item.bl_socket_idname:
+                case "IntSocket" | "BooleanSocket":
+                    if socket_type in {int, float} and '$'+key == out_value:
+                        # TODO this is the wrong way to do this, replace this with parser later
+                        out_value = socket_type(interface_item.default_int)
+                case "FloatSocket":
+                    if socket_type in {int, float} and '$'+key == out_value:
+                        out_value = socket_type(interface_item.default_float)
+                case "StringSocket":
+                    if socket_type == str:
+                        out_value = socket_type(out_value.replace("$"+key, interface_item.default_string))
+
     else: # since it is not root, we have to tag it.
         mContext.tagged_for_overlay.append((node.signature, socket_id))
     return out_value
@@ -1137,13 +1156,10 @@ def SetRibbonData(m, ribbon):
     #maybe I can check for closed poly curves here? under what other circumstance
     # will I find the ends of the wire have identical coordinates?
     ribbonData = []
-    tE = ribbon[0].copy(); bE = ribbon[1].copy()# circle = ribbon[2]
-    #
+    tE = ribbon[0].copy(); bE = ribbon[1].copy()
     lengths = RibbonMeshEdgeLengths(m, ribbon)
     lengths.append(0)
     totalLength = sum(lengths)
-    # m.calc_normals() #calculate normals
-    # it appears this has been removed.
     for i, (t, b) in enumerate(zip(tE, bE)):
         ind = old_bad_wrap_that_should_be_refactored( (i + 1), len(tE) - 1 )
         tNext = tE[ind]; bNext = bE[ind]
@@ -1262,43 +1278,39 @@ def mesh_from_curve(crv, context, ribbon=True):
        which will return the correct mesh even with modifiers"""
     import bpy
     m = None
-    bevel = crv.data.bevel_depth
-    extrude = crv.data.extrude
-    offset = crv.data.offset
-    try:
-        if (len(crv.modifiers) > 0):
-            do_unlink = False
-            if (not context.scene.collection.all_objects.get(crv.name)):
-                context.collection.objects.link(crv) # i guess this forces the dg to update it?
-                do_unlink = True
-            dg = context.view_layer.depsgraph
-            # just gonna modify it for now lol
-            if ribbon:
-                EnsureCurveIsRibbon(crv)
-            else:
-                crv.data.bevel_depth=0
-                crv.data.extrude=0
-                crv.data.offset=0
-            # try:
-            dg.update()
-            mOb = crv.evaluated_get(dg)
-            m = bpy.data.meshes.new_from_object(mOb)
-            m.name=crv.data.name+'_mesh'
-            if (do_unlink):
-                context.collection.objects.unlink(crv)
-        else: # (ಥ﹏ಥ) why can't I just use this !
-            # for now I will just do it like this
-            if ribbon:
-                EnsureCurveIsRibbon(crv)
-            else:
-                crv.data.bevel_depth=0
-                crv.data.extrude=0
-                crv.data.offset=0
-            m = bpy.data.meshes.new_from_object(crv)
-    finally:
-        crv.data.bevel_depth = bevel
-        crv.data.extrude = extrude
-        crv.data.offset = offset
+    crv_ob = crv.copy()
+    crv_data_copy = crv.data.copy()
+    crv_ob.data = crv_data_copy
+    if (len(crv_ob.modifiers) > 0):
+        do_unlink = False
+        if (not context.scene.collection.all_objects.get(crv_ob.name)):
+            context.collection.objects.link(crv_ob) # i guess this forces the dg to update it?
+            do_unlink = True
+        dg = context.view_layer.depsgraph
+        # just gonna modify it for now lol
+        if ribbon:
+            EnsureCurveIsRibbon(crv_ob)
+        else:
+            crv_ob.data.bevel_depth=0
+            crv_ob.data.extrude=0
+            crv_ob.data.offset=0
+        # try:
+        dg.update()
+        mOb = crv_ob.evaluated_get(dg)
+        m = bpy.data.meshes.new_from_object(mOb)
+        m.name=crv_ob.data.name+'_mesh'
+        if (do_unlink):
+            context.collection.objects.unlink(crv_ob)
+    else: # (ಥ﹏ಥ) why can't I just use this !
+        # for now I will just do it like this
+        if ribbon:
+            EnsureCurveIsRibbon(crv_ob)
+        else:
+            crv_ob.data.bevel_depth=0
+            crv_ob.data.extrude=0
+            crv_ob.data.offset=0
+        m = bpy.data.meshes.new_from_object(crv_ob)
+    bpy.data.objects.remove(crv_ob)
     return m
 
 def DetectRibbon(f, bm, skipMe):
@@ -1459,7 +1471,109 @@ def data_from_ribbon_mesh(m, factorsList, mat, ribbons = None, fReport = None):
         ret.append( (points, widths, normals) )
     return ret # this is a list of tuples containing three lists
 
+from mathutils import Vector
+
+# Used for filling the Curve LUTimport numpy as np
+import numpy as np
+
+def all_data_from_ribbon(mesh, mat, ribbon):
+    top_indices, bottom_indices, is_closed = ribbon
+    top_indices = np.asarray(top_indices, dtype=np.intp)
+    bottom_indices = np.asarray(bottom_indices, dtype=np.intp)
+    count = len(top_indices)
+    assert count >= 2, "this should never happen, report this as a bug"
+    # we'll use numpy to calculate the positions and normals because it is fast.
+    vertex_count = len(mesh.vertices)
+    coordinates = np.empty(vertex_count * 3, dtype=np.float64)
+    mesh.vertices.foreach_get("co", coordinates)
+    coordinates = coordinates.reshape((-1, 3))
+    vertex_normals = np.empty(vertex_count * 3, dtype=np.float64)
+    mesh.vertices.foreach_get("normal", vertex_normals)
+    vertex_normals = vertex_normals.reshape((-1, 3))
+    
+    top_local = coordinates[top_indices]; bottom_local = coordinates[bottom_indices]
+    # similarly we have to make the world matrix a numpy array
+    transform = np.asarray(mat, dtype=np.float64)
+    basis = transform[:3, :3]; translation = transform[:3, 3]
+    #.T is transpose because this numpy array expects row-major
+    top_positions = top_local @ basis.T + translation
+    bottom_positions = bottom_local @ basis.T + translation
+    positions = (top_positions + bottom_positions) * 0.5
+    radii = np.linalg.norm(top_positions - bottom_positions, axis=1) * 0.5
+
+    # transform the potisions by the object's matrix, mat
+    # and the normals need a slightly different transformation
+    normal_transform = np.linalg.inv(basis).T
+    local_normals = ( vertex_normals[top_indices] + vertex_normals[bottom_indices] )
+    normals = local_normals @ normal_transform.T
+    normal_lengths = np.linalg.norm(normals, axis=1)
+    # Normalize the normals by dividing them by the lengths. avoid dividing by 0
+    normals = np.divide(
+        normals,
+        normal_lengths[:, None],
+        out=np.zeros_like(normals),
+        where=normal_lengths[:, None] != 0,
+    )
+
+    # all of this incoming/outgoing subtraction stuff is doing the following:
+    # take the current vertex positon and get the tangent vector of each segment
+    # it is a part of, then average them
+    if is_closed: # have to adjust it back, I don't really know if this works
+        incoming = positions - np.roll(positions, 1, axis=0)
+        outgoing = np.roll(positions, -1, axis=0) - positions
+    else:
+        incoming = np.zeros_like(positions); outgoing = np.zeros_like(positions)
+        incoming[1:] = positions[1:] - positions[:-1]
+        outgoing[:-1] = positions[1:] - positions[:-1]
+        incoming[0] = outgoing[0]
+        outgoing[-1] = incoming[-1]
+
+    incoming_lengths = np.linalg.norm(incoming, axis=1)
+    outgoing_lengths = np.linalg.norm(outgoing, axis=1)
+    # the segments may be different lengths so we have to normalize them
+    incoming = np.divide(
+        incoming,
+        incoming_lengths[:, None],
+        out=np.zeros_like(incoming),
+        where=incoming_lengths[:, None] != 0,
+    )
+    outgoing = np.divide(
+        outgoing,
+        outgoing_lengths[:, None],
+        out=np.zeros_like(outgoing),
+        where=outgoing_lengths[:, None] != 0,
+    )
+    # then we add them together and normalize again to average them
+    tangents = incoming + outgoing
+    tangent_lengths = np.linalg.norm(tangents, axis=1)
+    tangents = np.divide(
+        tangents,
+        tangent_lengths[:, None],
+        out=np.zeros_like(tangents),
+        where=tangent_lengths[:, None] != 0,
+    )
+    # calcualte the U value of the samples based on the length of the spline
+    segment_lengths = np.linalg.norm(positions[1:] - positions[:-1], axis=1)
+    if is_closed:
+        closing_length = np.linalg.norm(positions[0] - positions[-1])
+        total_length = segment_lengths.sum() + closing_length
+    else:
+        total_length = segment_lengths.sum()
 
+    if total_length:
+        u_samples = np.concatenate(([0.0], np.cumsum(segment_lengths))) / total_length
+    else:
+        u_samples = np.zeros(count, dtype=np.float64)
+    
+    # now we have to convert the values back to values Blender uses
+    # TODO eventually I want to port everything to numpy even tho it is ugly
+    u_samples = tuple(u_samples)
+    radii = tuple(radii)
+    from mathutils import Vector
+    positions = [Vector(elem) for elem in positions]
+    normals = [Vector(elem) for elem in normals]
+    tangents = [Vector(elem) for elem in tangents]
+    return u_samples, positions, normals, tangents, radii
 
 
 #This bisection search is generic, and it searches based on the

+ 1 - 0
versioning.py

@@ -22,6 +22,7 @@ from .utilities.utilities import (prRed, prGreen, prPurple, prWhite,
 # (add more whenever a weird or difficult to debug problem occurs)
 # Did you check the update script on a tree that doesn't need it?
 # Did you use a >= when you should have used a > (or similar)?
+# Did you make sure that nodes with array inputs are sorting them properly?
 
 def version_upgrade_very_old(*args, **kwargs):
     node = kwargs['node']

+ 94 - 77
xForm_nodes.py

@@ -702,9 +702,6 @@ class xFormObjectInstance(xFormNode):
         modifier["Socket_1"] = self.evaluate_input("As Instance")
         # custom properties
         self.setup_custom_props(self.bGetObject(), "Custom Properties")
-        for i, (driver_key, driver_item) in enumerate(self.drivers.items()):
-            print (wrapGreen(i), wrapWhite(self), wrapPurple(driver_key))
-            prOrange(driver_item)
         finish_drivers(self)
 
     def bGetObject(self, mode = 'POSE'):
@@ -720,34 +717,62 @@ class xFormCurvePin(xFormNode):
             additional_parameters={"Matrix":None})
         setup_custom_property_inputs_outputs(self)
 
-    def prep_driver_values(self, constraint):
-        enums =  ( None,
-                   ("FORWARD_X", "FORWARD_Y", "FORWARD_Z", "TRACK_NEGATIVE_X", "TRACK_NEGATIVE_Y", "TRACK_NEGATIVE_Z"),
-                   ("UP_X", "UP_Y", "UP_Z"),
-                 )
-        for i, socket_name in enumerate(("Curve Pin Factor", "Forward Axis","Up Axis",)):
-            if self.inputs.get(socket_name) is None: continue 
-            enum = enums[i]
-            for template in self.socket_templates:
-                if template.name == socket_name: break
-            prop_amount = find_driven_default_value(self, socket_name)
-            # after all it may not be driven so it is probably a string if it is one of the axis properties.
-            if isinstance(prop_amount, (int, float)) and enum: # it is actually driving an enum value instead...
-                for i, enum_val in enumerate(enum):
-                    # check if the prop amount is in the current int as the enum val
-                    if i+1 > prop_amount: break
-                prop_amount = enum_val            
-            try:
-                setattr(constraint, template.blender_property, prop_amount )
-            except TypeError:
-                setattr(constraint, template.blender_property, template.default_value )
+    def get_matrix_from_spline(self, spline_lookup):
+        matrix = spline_lookup.matrix_at_u(self.parameters["Curve Pin Factor"])
+        track_axis = self.parameters["Forward Axis"]; up_axis = self.parameters["Up Axis"]
+        # Copy the original basis before changing anything.
+        from mathutils import Vector
+        tangent = Vector(matrix.col[1][:3]); normal = Vector(matrix.col[2][:3])
+        # I have to take the Z axis and invert it because of the way Mantis calculates the normal
+        # using the ribbon mesh rather which extrudes along the binormal
+        # this is bad but it is too late to change this now, oh well
+        normal *= -1 # I have checked this with every combination and it works
+        track_sign = -1 if track_axis.startswith("TRACK_NEGATIVE_") else 1
+        track_axis = track_axis[-1]; up_axis = up_axis[-1]
+        axis_index = {"X": 0, "Y": 1, "Z": 2} #  there is probably a less stupid way to do this
+        track_index = axis_index[track_axis]; up_index = axis_index[up_axis]
+        if track_index == up_index: raise ValueError("Track and up_axis axes must be different")
+        # Assign the requested axes.
+        cols = [None, None, None]
+        cols[track_index] = tangent * track_sign; cols[up_index] = normal
+        # Fill the remaining axis to make a right-handed basis.
+        # the bitanget calculation is order dependent if you want to stay right-handed
+        if track_index != 0 and up_index != 0:   cols[0] = cols[1].cross(cols[2])
+        elif track_index != 1 and up_index != 1: cols[1] = cols[2].cross(cols[0])
+        else:                                    cols[2] = cols[0].cross(cols[1])
+        matrix.col[0].xyz = cols[0]; matrix.col[1].xyz = cols[1]
+        matrix.col[2].xyz = cols[2]
+        return matrix
+
+    def generate_constraints(self, ob, curve):
+        # we'll limit all the transforms so we can parent it without
+        # having to deal with the weird curve parenting (path-animation)
+        c1 = ob.constraints.new("LIMIT_LOCATION")
+        for max_min in ['max','min']:
+            for axis in "xyz":
+                setattr(c1, "use_"+max_min+"_"+axis, True)
+                setattr(c1, max_min+"_"+axis, 0.0)
+        c2 = ob.constraints.new("LIMIT_ROTATION")
+        for axis in "xyz":
+            setattr(c2, "use_limit_"+axis, True)
+            setattr(c2, max_min+"_"+axis, 0.0)
+        c3 = ob.constraints.new("LIMIT_SCALE")
+        for max_min in ['max','min']:
+            for axis in "xyz":
+                setattr(c3, "use_"+max_min+"_"+axis, True)
+                setattr(c3, max_min+"_"+axis, 1.0)
+        c4 = ob.constraints.new("FOLLOW_PATH")
+        c4.target = curve
+        c4.use_fixed_location = True
+        c4.use_curve_radius = True
+        c4.use_curve_follow = True
+        c4.name = "Curve Pin"
+        return (c1, c2, c3, c4,)
 
     def bPrepare(self, bContext = None,):
-        from bpy import data
-
-        if not bContext: # lol
-            import bpy
-            bContext = bpy.context
+        from bpy import data, context
+        if not bContext: bContext = context # HACK is this wise?
+        # Generate the empty
         ob = data.objects.get(self.evaluate_input("Name"))
         if not ob:
             ob = data.objects.new(self.evaluate_input("Name"), None)
@@ -757,72 +782,64 @@ class xFormCurvePin(xFormNode):
             ob.lock_rotation_w = True
             ob.empty_display_type = 'CONE'
             ob.empty_display_size = 0.10
-
-        self.bObject = ob
-        self.bRegisterObject('OBJECT', self.bObject)
-        reset_object_data(ob)
-
+        self.bObject = ob; self.bRegisterObject('OBJECT', ob); reset_object_data(ob)
+        # Get the curve and check it
         node_line = trace_single_line(self, "Parent Curve")[0][1:] # slice excludes self
         for other_node in node_line:
-            if other_node.node_type == 'XFORM':
-                break
+            if other_node.node_type == 'XFORM': break
         else:
             raise GraphError(f"ERROR: {self} is not connected to a parent curve")
         if isinstance(other_node, (xFormArmature, xFormBone, xFormObjectInstance,)):
             raise GraphError(f"ERROR: {self} must be connected to curve,"
                               " not {other_node.__class__.__name__}")
-        curve=other_node.bGetObject()
+        curve=other_node.bGetObject(); curve_name = curve.name
         if curve.type != 'CURVE':
             raise GraphError(f"ERROR: {self} must be connected to curve,"
                               " not {curve.type}")
-        # we'll limit all the transforms so we can parent it
-        #  because it is annoying to have a cluttered outliner.
-        #
-        # always do this so that everything stays consistent.
-        spline_index = self.evaluate_input("Spline Index")
         from .utilities.utilities import get_extracted_spline_object
+        spline_index = self.evaluate_input("Spline Index")
         curve = get_extracted_spline_object(curve, spline_index, self.mContext)
-        # Link to Scene:
-        for link_me in [ob, curve]:
+        for link_me in [ob, curve]: # Link everything to the scene:
             if (link_me.name not in bContext.view_layer.active_layer_collection.collection.objects):
                 bContext.view_layer.active_layer_collection.collection.objects.link(link_me)
 
-        c = ob.constraints.new("LIMIT_LOCATION")
-        for max_min in ['max','min']:
-            for axis in "xyz":
-                setattr(c, "use_"+max_min+"_"+axis, True)
-                setattr(c, max_min+"_"+axis, 0.0)
-        c = ob.constraints.new("LIMIT_ROTATION")
-        for axis in "xyz":
-            setattr(c, "use_limit_"+axis, True)
-            setattr(c, max_min+"_"+axis, 0.0)
-        c = ob.constraints.new("LIMIT_SCALE")
-        for max_min in ['max','min']:
-            for axis in "xyz":
-                setattr(c, "use_"+max_min+"_"+axis, True)
-                setattr(c, max_min+"_"+axis, 1.0)
-        c = ob.constraints.new("FOLLOW_PATH")
-        c.target = curve
-        c.use_fixed_location = True
-        c.use_curve_radius = True
-        c.use_curve_follow = True
-        c.name = "Curve Pin"
-
-        constraint_props_sockets = self.gen_property_socket_map( prop_owner_group='constraints')
-        props_sockets = self.gen_property_socket_map( prop_owner_group='object')
+        constraints = self.generate_constraints(ob, curve); c = constraints[-1]
+        props_sockets = self.gen_property_socket_map()
         b_objects = {"constraints":c, "object":self.bObject}
         evaluate_sockets(self, b_objects, props_sockets)
-        # this isn't usually run on xForm nodes so for now I need to set the
-        #   driver's default values manually if I want a matrix now.
-        # because the drivers may not have initialized yet.
-        self.prep_driver_values(c)
-        # now if all goes well... the matrix will be correct.
-        # NOTE: this dg.update() makes this a very expensive node
-        dg = bContext.view_layer.depsgraph
-        dg.update()
-        # and the matrix should be correct now - copy because it may be modified
-        self.parameters['Matrix'] = ob.matrix_world.copy()
+
+        # Find the default values to set even if the properties are drive:
+        enums =  ( None,
+                ("FORWARD_X", "FORWARD_Y", "FORWARD_Z", "TRACK_NEGATIVE_X", "TRACK_NEGATIVE_Y", "TRACK_NEGATIVE_Z"),
+                ("UP_X", "UP_Y", "UP_Z"),
+                )
+        for i, socket_name in enumerate(("Curve Pin Factor", "Forward Axis","Up Axis",)):
+            if self.inputs.get(socket_name) is None: continue 
+            enum = enums[i]
+            for template in self.socket_templates:
+                if template.name == socket_name: break
+            prop_amount = find_driven_default_value(self, socket_name)
+            # after all it may not be driven so it is probably a string if it is one of the axis properties.
+            if isinstance(prop_amount, (int, float)) and enum: # it is actually driving an enum value instead...
+                for i, enum_val in enumerate(enum):
+                    # check if the prop amount is in the current int as the enum val
+                    if i+1 > prop_amount: break
+                prop_amount = enum_val
+            self.parameters[socket_name] = prop_amount
+            # Explicitly set the constraint attributes
+            setattr(c, template.blender_property, prop_amount)
+
+        # Now set the matrix - since this may provide dependencies to other nodes
+        from .misc_nodes import get_from_curve_lut
+        spline_lookup = get_from_curve_lut(curve_name, spline_index, bContext, self)
+        matrix = self.get_matrix_from_spline(spline_lookup)
+        # HACK: this is giving a local space matrix, and that's dumb
+        # TODO: Mantis should handle coordinate space conversion automatically
+        self.parameters['Matrix'] = matrix
         ob.parent=curve
+        # NOTE: Blender does weird things with curve parenting when there is Path Animation
+        #  and it sets the inverse parent matrix based on the evaluated matrix from the point
+        #  on the curve set by path animation. (That's why we need all the WS limit constraints)
         print( wrapGreen("Created Curve Pin: ") + wrapOrange(self.bObject.name) )
 
         parent_xForm_info = get_parent_xForm_info(self)