1
0

38 İşlemeler aba14a26c3 ... 2fb4c48324

Yazar SHA1 Mesaj Tarih
  Joseph Brandenburg 2fb4c48324 remove excess function call in schema 2 hafta önce
  Joseph Brandenburg 634d736dba BIG REFACTOR of evaluate_sockets 2 hafta önce
  Joseph Brandenburg 61403562b3 Fix prep_driver_values in CurvePin 2 hafta önce
  Joseph Brandenburg c8d0bc6f19 revert previous commit 2 hafta önce
  Joseph Brandenburg c364d30b09 ensure sort_execution is depth-ordered 2 hafta önce
  Joseph Brandenburg 95c167528b rearange driver variable, fcurve setup 2 hafta önce
  Joseph Brandenburg ddac30fb2f Fix: Curve Pin didn't display custom props 2 hafta önce
  Joseph Brandenburg 28f51d64c4 Existing Geometry can now add Lattice 2 hafta önce
  Joseph Brandenburg 3fec3a5368 consistent error print for Selection Sync 2 hafta önce
  Joseph Brandenburg 2fb23f2efb Fix: Drivers required bone, armature 2 hafta önce
  Joseph Brandenburg 511da6a872 fix: mantis node reuse in gen_mantis_nodes 2 hafta önce
  Joseph Brandenburg 722a64156e fix Transformation Constraint props_socket 2 hafta önce
  Joseph Brandenburg c07a4a4815 Widget Node axis flip for Blender 5.2 2 hafta önce
  Joseph Brandenburg 64007c248e Cache IK Pole Angle results 2 hafta önce
  Joseph Brandenburg d0a137e691 Optimize fill_parameters 2 hafta önce
  Joseph Brandenburg cc43b0f239 no_default_value now a frozen set 2 hafta önce
  Joseph Brandenburg 9df315a5a7 optimize clear_reroutes() 2 hafta önce
  Joseph Brandenburg 6b1f58402e profiling sort by function calls 2 hafta önce
  Joseph Brandenburg 736d3d4d96 cleanup remove accidental print 2 hafta önce
  Joseph Brandenburg 836b087941 cache property socket map at startup 2 hafta önce
  Joseph Brandenburg e6ed10fa3f refactor socket templates in mantis classes 2 hafta önce
  Joseph Brandenburg 16cc366d11 fix: Automatic Weights for Armature Deformer 2 hafta önce
  Joseph Brandenburg d5dd06f335 Reroute Cache Feature: 2 hafta önce
  Joseph Brandenburg 7e805378de Cleanup: Limit Rotation placement in menu 2 hafta önce
  Joseph Brandenburg 522abd34ad profile individual functions easily 2 hafta önce
  Joseph Brandenburg 8ac3db51ea fix for init sockets optimization 2 hafta önce
  Joseph Brandenburg 151d858148 fix: missing import 2 hafta önce
  Joseph Brandenburg 1b9ad4ec8b optimize init_sockets a bit 2 hafta önce
  Joseph Brandenburg 84baf81de8 remove expensive check from schema solve 2 hafta önce
  Joseph Brandenburg 0473f7bcaa remove unneeded check in execute 2 hafta önce
  Joseph Brandenburg cc95126b34 dev helper: add cumulative option to profiling 2 hafta önce
  Joseph Brandenburg 600d59cff3 make everything use __slots__ 2 hafta önce
  Joseph Brandenburg f84ab2ba4c separate profiling for update and execute 3 hafta önce
  Joseph Brandenburg 6def44027d Fix: overlooked versioning compare 3 hafta önce
  Joseph Brandenburg 1681570a1c Cleanup: Make node_type a class attribute 3 hafta önce
  Joseph Brandenburg 9ba27cbd69 Better Cycle Detection and feedback 4 hafta önce
  Joseph Brandenburg b1e7d687e1 new Curve Matrix, Compose Matrix modes 4 hafta önce
  Joseph Brandenburg 9950219dce fix selection sync error again lol 4 hafta önce

+ 30 - 2
__init__.py

@@ -99,8 +99,8 @@ link_transform_category = [
         NodeItem("LinkCopyScale"),
         NodeItem("LinkCopyTransforms"),
         NodeItem("LinkLimitLocation"),
-        NodeItem("LinkLimitScale"),
         NodeItem("LinkLimitRotation"),
+        NodeItem("LinkLimitScale"),
         NodeItem("LinkLimitDistance"),
         NodeItem("LinkTransformation"),
         NodeItem("LinkGeometryAttribute"),
@@ -389,6 +389,17 @@ def begin_selection_sync_timer(filename):
         SELECTION_POLL_RATE = bl_mantis_addon.preferences.SelectionSyncPollRate
         bpy.app.timers.register(timer_update_selection_sync)
 
+# Build the reroute cache on file load
+@persistent
+def on_load_build_reroute_cache(filename):
+    for tree in bpy.data.node_groups:
+        if tree.bl_idname in ['MantisTree', 'SchemaTree']:
+            # and I have to do this somewhere, this is the best place...
+            tree.is_quitting = False # reset the quitting flag
+            from .base_definitions import hash_tree, build_reroute_cache
+            tree.hash = str(hash_tree(tree, recurse=False))
+            build_reroute_cache(tree, tree.links)
+
 # I'll need to do some fiddling here when it comes time to try
 #   and make rig definitions animatable.
 @persistent
@@ -407,10 +418,18 @@ def on_undo_post_handler(scene): # the undo will trigger a depsgraph update
     for t in bpy.data.node_groups: # so we enable prevent_next_exec.
         if t.bl_idname in ['MantisTree', 'SchemaTree']:
             t.prevent_next_exec = True
-            t.hash=""
+            t.hash_recursive=""
             # set the tree to invalid to trigger a tree update
             # since the context data is wiped by an undo.
 
+@persistent
+def on_quit_handler(is_ui):
+    # we'll let the node trees know they're quitting
+    # this prevents them from spamming updates on file close.
+    for tree in bpy.data.node_groups:
+        if tree.bl_idname in ['MantisTree', 'SchemaTree']:
+            tree.is_quitting=True
+
 from .menu_classes import (node_context_menu_draw, node_add_menu_draw,
                            armature_add_menu_draw, import_menu_draw)
 
@@ -446,12 +465,21 @@ def register():
     bpy.app.handlers.load_post.insert(0, begin_selection_sync_timer)
     bpy.app.handlers.load_post.insert(0, autoload_components)
     bpy.app.handlers.load_post.insert(0, version_update_handler)
+    bpy.app.handlers.load_post.insert(0, on_load_build_reroute_cache)
     bpy.app.handlers.animation_playback_pre.insert(0, on_animation_playback_pre_handler)
     bpy.app.handlers.animation_playback_post.insert(0, on_animation_playback_post_handler)
     bpy.app.handlers.undo_post.insert(0, on_undo_post_handler)
+    bpy.app.handlers.exit_pre.insert(0, on_quit_handler)
     # I'm adding mine in first to ensure other addons don't mess up mine
     # but I am a good citizen! so my addon won't mess up yours! probably...
 
+    # Now it is time to pre-cache stuff for Mantis
+    from .base_definitions import MantisNode
+    mantis_classes = [cls for cls in classes if isinstance(cls, MantisNode)]
+    for cls in mantis_classes:
+        # Set up the property socket map cache
+        cls.gen_property_socket_map()
+
 
 def unregister():
     bpy.types.NODE_MT_context_menu.remove(node_context_menu_draw)

+ 364 - 90
base_definitions.py

@@ -13,12 +13,21 @@ from .utilities.utilities import (prRed, prGreen, prPurple, prWhite,
 from .utilities.utilities import get_socket_maps, relink_socket_map, do_relink
 from .mantis_dataclasses import MantisSocketTemplate
 
+MANTIS_VERSION_MAJOR=0
+MANTIS_VERSION_MINOR=13
+MANTIS_VERSION_SUB=5
+
 FLOAT_EPSILON=0.0001 # used to check against floating point inaccuracy
 links_sort_key= lambda a : (-a.multi_input_sort_id, -a.sub_sort_id)
 
 selection_sync_node_to_object = {} # what objects a node spawns
 selection_sync_object_to_node = {} # what node an object comes from
 
+# The reroute cache is used to accelerate reading reroute nodes
+# it uses the tree name and hash to ensure reroutes are only updated when needed
+# reroutes recursively find their source and remember it
+reroute_cache = {}
+
 def TellClasses():
     #Why use a function to do this? Because I don't need every class to register.
     return [ MantisTree,
@@ -44,15 +53,13 @@ def valid_interface_types(cls : NodeTree, socket_idname : str):
         return ['NodeSocketGeometry']
     else: # once versioning is finished this will be unnecesary.
         return socket_idname in tell_valid_bl_idnames()
-
 def fix_reroute_colors(tree):
     context = bpy.context
     if any((tree.is_executing, tree.is_exporting, tree.do_live_update==False, context.space_data is None) ):
         return
-    from collections import deque
     from .utilities.utilities import find_reroute_start_socket
     from .socket_definitions import MantisSocket
-    reroutes_without_color = deque()
+    reroutes_without_color = []
     for n in tree.nodes:
         if n.bl_idname=='NodeReroute' and n.inputs[0].bl_idname == "NodeSocketColor":
             reroutes_without_color.append(n)
@@ -69,11 +76,11 @@ def fix_reroute_colors(tree):
 
 #functions to identify the state of the system using hashes
 # this function runs a lot so it should be optimized as well as possible.
-def hash_tree(tree):
+def hash_tree(tree, recurse=True):
     trees=set(); links=[]; hash_data=""
     for node in tree.nodes:
         hash_data+=str(node.name)
-        if hasattr(node, 'node_tree') and node.node_tree:
+        if recurse and hasattr(node, 'node_tree') and node.node_tree:
             trees.add(node.node_tree)
     for other_tree in trees:
         hash_data+=str(hash_tree(other_tree))
@@ -84,9 +91,157 @@ def hash_tree(tree):
     links.sort(); hash_data+=''.join(links)
     return hash(hash_data)
 
-MANTIS_VERSION_MAJOR=0
-MANTIS_VERSION_MINOR=13
-MANTIS_VERSION_SUB=5
+# TODO: I should see if I can make these work without every using NodeSocket.links
+#  as that getter is very slow at scale
+# however with the cache in place it is probably negligible
+def update_reroute_cache_item_source_recursively(reroute, tree_cache, source):
+    # find all the inputs to this reroute and update their destination sets
+    # every reroute ancestor pushes to this destination... but not every recursive-child
+    #   of the root reroute necessarily does, so pushing back like this is right
+    # since each reroute node only has one ancestor, this is easy
+    from .mantis_dataclasses import RerouteCacheItem
+    tree= reroute.id_data
+    reroutes = {reroute}
+    while(reroutes):
+        reroute = reroutes.pop()
+        # for making a new cache item
+        if (cache_object := tree_cache.get(reroute.name)) is None:
+            cache_object = RerouteCacheItem(
+                    reroute_name=reroute.name,
+                    reroute_tree_name=tree.name,
+                )
+            tree_cache[reroute.name] = cache_object
+        cache_object.source = source
+        if reroute.outputs[0].is_linked: # reroutes are always single-output
+            for l in reroute.outputs[0].links: # with potentially many links
+                if l.to_node.bl_idname == 'NodeReroute':
+                    reroutes.add(l.to_node)
+
+def update_reroute_cache_item_destination_recursively(reroute, tree_cache, destination):
+    # find all the inputs to this reroute and update their destination sets
+    # every reroute ancestor pushes to this destination... but not every recursive-child
+    #   of the root reroute necessarily does, so pushing back like this is right
+    # since each reroute node only has one ancestor, this is easy
+    from .mantis_dataclasses import RerouteCacheItem
+    tree= reroute.id_data
+    while(reroute):
+        # for making a new cache item
+        if (cache_object := tree_cache.get(reroute.name)) is None:
+            cache_object = RerouteCacheItem(
+                    reroute_name=reroute.name,
+                    reroute_tree_name=tree.name,
+                    destinations= {destination},
+                )
+            tree_cache[reroute.name] = cache_object
+        else:
+            cache_object.destinations.add(destination)
+        # Odd: if I check reroute.inputs[0].is_linked with live updates, CRASH.
+        if rr_links := reroute.inputs[0].links:
+            # reroutes are always single-input with at most 1 link
+            if (l := rr_links[0]).from_node == 'NodeReroute':
+                reroute = l.from_node
+                continue
+        reroute = None
+
+# Update the reroute cache from the link
+# NOTE this is updated as the user adds links by the node.insert_link() callback
+def reroute_cache_handle_link(tree, tree_cache, link):
+    from .mantis_dataclasses import RerouteCacheItem
+    # if it is a link to or from a reroute, add or update the cache item
+    # first check if the cache item is stale
+    # then try and fill the source and destination - make sure to recursively fill them
+    # if it isn't connected to a source or destination, it's part of a network
+    # We only need to check the ends, and we can do both the destination and source
+    #  passes simultaneously since they don't depend on each other.
+    # NOTE that we use socket index to make things explicit and fast
+    if link.from_node.bl_idname == 'NodeReroute' and link.to_node.bl_idname != 'NodeReroute':
+        other_node = link.to_node
+        other_socket = link.to_socket
+        for socket_index, check_socket in enumerate(other_node.inputs):
+            if check_socket.identifier == other_socket.identifier: break
+        else:
+            raise RuntimeError("Error, cannot make reroute cache for link: "
+                               f"{link.from_node.name}:{link.from_socket.name} ==>"
+                               f"{link.to_node.name}:{link.to_socket.name}")
+        reroute = link.from_node
+        destination = (other_node.name, socket_index)
+        if (cache_object := tree_cache.get(reroute.name)) is None:
+            cache_object = RerouteCacheItem(
+                reroute_name=reroute.name,
+                reroute_tree_name=tree.name,
+                destinations= {destination},
+            )
+            tree_cache[reroute.name]=cache_object
+        update_reroute_cache_item_destination_recursively(reroute, tree_cache,
+            destination=destination)
+    elif link.from_node.bl_idname != 'NodeReroute' and link.to_node.bl_idname == 'NodeReroute':
+        other_node = link.from_node
+        other_socket = link.from_socket
+        for socket_index, check_socket in enumerate(other_node.outputs):
+            if check_socket.identifier == other_socket.identifier: break
+        else:
+            raise RuntimeError("Error, cannot make reroute cache for link: "
+                               f"{link.from_node.name}:{link.from_socket.name} ==>"
+                               f"{link.to_node.name}:{link.to_socket.name}")
+        reroute = link.to_node
+        source = (link.from_node.name, socket_index)
+        if (cache_object := tree_cache.get(reroute.name)) is None:
+            cache_object = RerouteCacheItem(
+                reroute_name=reroute.name,
+                reroute_tree_name=tree.name,
+                source = source,
+            )
+            tree_cache[reroute.name]=cache_object
+        update_reroute_cache_item_source_recursively(reroute, tree_cache,
+            source=source)
+
+def build_reroute_cache(tree, links):
+    # we'll track this to know if a tree update should invalidate the hash on incremental update
+    tree.number_of_reroute_links = 0
+    # make sure to clear the previous cache or confusing things will happen
+    tree_cache = {}; reroute_cache[tree.name] = tree_cache
+    tree_cache['_MANTIS_RESERVED_TREE_HASH'] = tree.hash
+    for link in links:
+        if link.from_node.bl_idname == 'NodeReroute' or link.to_node.bl_idname == 'NodeReroute':
+            tree.number_of_reroute_links += 1
+        reroute_cache_handle_link(tree, tree_cache, link)
+
+def tree_update_common(tree): # set the reroute colors
+    if tree.is_quitting: return
+    if (bpy.app.version == (4,4,0)): # CTD bug in that version
+        return
+    fix_reroute_colors(tree)
+    # intialize it if e.g the tree is new.
+    if (tree_cache := reroute_cache.get(tree.name)) is None:
+        tree_cache = {}; reroute_cache[tree.name] = tree_cache
+    # events fire in this order:
+    #   - node.insert_link (one for each node)
+    #   - node.update (one for each node)
+    #   - tree.update
+    # so if the node's insert_link handled things, we don't need to check
+    tree.hash = str(hash_tree(tree, recurse=False))
+    if tree.reroute_link_handled: # we can update the hash, it's still good.
+        if tree_cache := reroute_cache.get(tree.name):
+            tree_cache["_MANTIS_RESERVED_TREE_HASH"]=tree.hash
+        tree.reroute_link_handled = False # always make sure to reset the flag
+    else:
+        reroute_links = []
+        # set the UI tree hash for e.g. reroute cache
+        for link in tree.links:
+            if link.from_node.bl_idname == 'NodeReroute' or link.to_node.bl_idname == 'NodeReroute':
+                reroute_links.append(link)
+        if len(reroute_links) == tree.number_of_reroute_links:
+            if tree_cache := reroute_cache.get(tree.name): # we can update the hash, it's still good.
+                tree_cache["_MANTIS_RESERVED_TREE_HASH"] = tree.hash
+            else: # there is no cache after all. How did that happen?
+                build_reroute_cache(tree, reroute_links)
+            # NOTE I put a lot of effort into catching deleted links but it just isn't practical
+            #      to catch this and it is almost free to rebuild the cache.
+        else:
+            build_reroute_cache(tree, reroute_links)
+            # reset the count but do not update the hash.
+        tree.number_of_reroute_links = len(reroute_links)
+
 
 class MantisTree(NodeTree):
     '''A custom node tree type that will show up in the editor type list'''
@@ -95,7 +250,8 @@ class MantisTree(NodeTree):
     bl_icon = 'OUTLINER_OB_ARMATURE'
 
     tree_valid:BoolProperty(default=False)
-    hash:StringProperty(default='')
+    hash:StringProperty(default='') # use for UI updates, reroute cache
+    hash_recursive:StringProperty(default='') # use for checking Mantis state
     do_live_update:BoolProperty(default=True) # use this to disable updates for e.g. scripts
     # operator settings for re-exporting the tree.
     filepath:StringProperty(default="", subtype='FILE_PATH')
@@ -103,6 +259,7 @@ class MantisTree(NodeTree):
     #
     is_executing:BoolProperty(default=False)
     is_exporting:BoolProperty(default=False)
+    is_quitting:BoolProperty(default=False)
     execution_id:StringProperty(default='')
     # prev_execution_id:StringProperty(default='')
     mantis_version:IntVectorProperty(default=[
@@ -114,6 +271,11 @@ class MantisTree(NodeTree):
     #added to work around a bug in 4.5.0 LTS
     interface_helper : StringProperty(default='')
 
+    # for reroute cache, to check if it is still good
+    number_of_reroute_links : IntProperty(default=0)
+    # flag to indicate a link was already handled this update
+    reroute_link_handled : BoolProperty(default=False)
+
     parsed_tree={}
 
     # NOTE that these are class properties that should be serialized
@@ -129,16 +291,19 @@ class MantisTree(NodeTree):
         def valid_socket_type(cls : NodeTree, socket_idname: str):
             return valid_interface_types(cls, socket_idname)
 
-    def update(self): # set the reroute colors
-        if (bpy.app.version >= (4,4,0)):
-            fix_reroute_colors(self)
+    def update(self):
+        tree_update_common(self)
 
     def update_tree(self, context = None, force=False, error_popups=False):
         if self.is_exporting:
             return
         my_hash = str( hash_tree(self) )
-        if (my_hash != self.hash) or force:
-            self.hash = my_hash
+        my_ui_hash = str( hash_tree(self, recurse=False))
+        tree_cache = reroute_cache.get(self.name)
+        if tree_cache['_MANTIS_RESERVED_TREE_HASH'] != my_ui_hash:
+            build_reroute_cache(self, self.links) # somehow it became invalid, rebuild it.
+        if (my_hash != self.hash_recursive) or force:
+            self.hash_recursive = my_hash
             self.is_executing = True
             from .execution import readtree
             prGreen("Validating Tree: %s" % self.name)
@@ -154,11 +319,15 @@ class MantisTree(NodeTree):
             except Exception as e:
                 prRed("Failed to update node tree due to error.")
                 self.tree_valid = False
-                self.hash='' # unset the hash to mark the tree as un-parsed.
+                self.hash_recursive='' # unset the hash to mark the tree as un-parsed.
                 raise e
             finally:
                 scene.render.use_lock_interface = False
                 self.is_executing = False
+        if self.parsed_tree is None:
+            self.parsed_tree = {}
+            return 1 # failure
+        return 0 # success
 
     def display_update(self, context):
         if self.is_exporting:
@@ -181,7 +350,7 @@ class MantisTree(NodeTree):
 
     def execute_tree(self,context, error_popups = False, profile=False):
         self.prevent_next_exec = False
-        if not self.hash:
+        if not self.hash_recursive:
             return
         if self.is_exporting or self.is_executing:
             return
@@ -207,20 +376,25 @@ class SchemaTree(NodeTree):
     do_live_update:BoolProperty(default=True) # default to true so that updates work
     is_executing:BoolProperty(default=False)
     is_exporting:BoolProperty(default=False)
+    is_quitting:BoolProperty(default=False)
 
     mantis_version:IntVectorProperty(default=[
         MANTIS_VERSION_MAJOR, MANTIS_VERSION_MINOR, MANTIS_VERSION_SUB])
     # see the note in MantisTree
     interface_helper : StringProperty(default='')
 
+    # for reroute cache, to check if it is still good
+    number_of_reroute_links : IntProperty(default=0)
+    # flag to indicate a link was already handled this update
+    reroute_link_handled : BoolProperty(default=False)
+
     if (bpy.app.version < (4, 4, 0) or bpy.app.version >= (4,5,0)):  # in 4.4 this leads to a crash
         @classmethod
         def valid_socket_type(cls : NodeTree, socket_idname: str):
             return valid_interface_types(cls, socket_idname)
 
-    def update(self): # set the reroute colors
-        if (bpy.app.version >= (4,4,0)):
-            fix_reroute_colors(self)
+    def update(self):
+        tree_update_common(self)
 
 
 #TODO: do a better job explaining how MantisNode and MantisUINode relate.
@@ -286,6 +460,18 @@ class MantisUINode:
             if template.use_multi_input: # this is an array
                 socket.display_shape = 'SQUARE_DOT'
 
+    def insert_link(self, link):
+        if (bpy.app.version == (4, 4, 0)):
+            return # this causes a crash due to a bug.
+        tree = self.id_data
+        if tree.reroute_link_handled: return
+        if ( link.from_node.bl_idname == 'NodeReroute' or
+             link.to_node.bl_idname == 'NodeReroute' ):
+                tree.number_of_reroute_links+=1
+                reroute_cache_handle_link(tree=tree,
+                    tree_cache=reroute_cache.get(tree.name), link=link)
+                tree.reroute_link_handled = True
+        
 # NOTE
 # MantisUINode has no proper mantis libraryclass on its own, so:
 # register()'s are in SUBclasses; they have to run AFTER library is defined
@@ -676,12 +862,12 @@ custom_props_types = ["LinkArmature", "UtilityKeyframe", "UtilityFCurve", "Utili
                       "xFormArmature", "xFormGeometryObject", "xFormObjectInstance", "xFormCurvePin",]
 
 # filters for determining if a link is a hierarchy link or a non-hierarchy (cyclic) link.
-from_name_filter = ["Driver",]
-to_name_filter = [
+from_name_filter = frozenset({"Driver",})
+to_name_filter = frozenset({
                    "Custom Object xForm Override",
                    "Custom Object",
                    "Deform Bones",
-                 ]
+                 })
 
 # nodes that must be solved as if they were Schema because they send arrays out.
 array_output_types = [
@@ -723,22 +909,42 @@ class MantisExecutionContext():
 
 from typing import Any
 
+# using these frozen sets accelerate lookup when checking for this
+FILL_PARAMETERS_SKIP_ROOTS = frozenset({ 'MANTIS_AUTOGENERATED', 'SCHEMA_AUTOGENERATED', })
+FILL_PARAMETERS_SKIP_NAMES = frozenset({ "NodeGroupOutput", "NodeGroupInput", })
+FILL_PARAMETERS_SKIP_SOCKETS = frozenset({ 'RelationshipSocket', 'xFormSocket', })
+from .socket_definitions import no_default_value
+
 class MantisNode:
     """
         This class contains the basic interface for a Mantis Node.
         A MantisNode is used internally by Mantis to represent the final evaluated node graph.
         It gets generated with data from a MantisUINode when the graph is read.
     """
+    __slots__ = (
+        "base_tree", "signature", "ui_signature",
+        "inputs", "outputs",
+        "parameters", "drivers", "bObject",
+        "hierarchy_connections", "connections",
+        "hierarchy_dependencies", "dependencies",
+        "unmet_hierarchy_dependency_count",
+        "prepared", "executed", "execution_prepared",
+        "mContext",
+        "is_schema_prototype"
+        )
+    node_type = 'UNINITIALIZED'
+    socket_templates = None
+    property_socket_map = {}
     def __init__(self, signature : tuple,
                  base_tree : bpy.types.NodeTree,
-                 socket_templates : list[MantisSocketTemplate]=[],):
+                 socket_templates : list[MantisSocketTemplate]=[],
+                 additional_parameters :dict = {}, ):
         self.base_tree=base_tree
         self.signature = signature
         self.ui_signature = signature
         self.inputs = MantisNodeSocketCollection(node=self, is_input=True)
         self.outputs = MantisNodeSocketCollection(node=self, is_input=False)
-        self.parameters, self.drivers = {}, {}; self.bObject=None
-        self.node_type='UNINITIALIZED'
+        self.parameters, self.drivers = {}, []; self.bObject=None
         self.hierarchy_connections, self.connections = [], []
         self.hierarchy_dependencies, self.dependencies = [], []
         self.unmet_hierarchy_dependency_count = float("inf") # we will set this when filling the dependencies lists
@@ -746,14 +952,14 @@ class MantisNode:
         self.execution_prepared = False
         # execution_prepared is for tracking prep state in execution, so that I can avoid preparing nodes
         #  again without changing the readtree code much.
-        self.socket_templates = socket_templates
         self.mContext = None # for now I am gonna set this at runtime
+        self.is_schema_prototype = False
         # 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+
         #  classes, instead of adding about 5 lines of code elsewhere.
         if self.socket_templates:
-            self.init_sockets()
+            self.init_sockets( additional_parameters )
 
     @property
     def name(self):
@@ -766,13 +972,40 @@ class MantisNode:
     
     def reset_execution(self) -> None:
         """ Reset the node for additional execution without re-building the tree."""
-        self.drivers={}; self.bObject=None
+        self.drivers.clear(); self.bObject=None
         self.executed = False
         self.execution_prepared = False
 
-    def init_sockets(self) -> None:
-        self.inputs.init_sockets(self.socket_templates)
-        self.outputs.init_sockets(self.socket_templates)
+    def init_sockets(self, additional_parameters : dict = {}) -> None:
+        # Formerly I was doing this:
+        # self.inputs.init_sockets(self.socket_templates)
+        # self.outputs.init_sockets(self.socket_templates)
+        # Since doing each one causes some redundancy, we'll just reimplement it
+        # This is a highly used and therefore expensive function so we have to make small
+        # optimizations like this, even tho it is ugly.
+        traverse_in, traverse_out = {}, {}
+        for socket in self.socket_templates:
+            if socket.traverse_target:
+                if socket.is_input:
+                    traverse_in[socket.name] = socket.traverse_target
+                else:
+                    traverse_out[socket.name] = socket.traverse_target
+            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,)
+            else:
+                self.outputs[socket.name] = NodeSocket(is_input=False, name=socket.name,
+                                    node=self,)
+            self.parameters[socket.name] = None
+        for traverse_owner, traverse_target in traverse_in.items():
+            self.inputs[traverse_owner].traverse_target = self.outputs[traverse_target]
+            self.inputs[traverse_owner].can_traverse = True
+        for traverse_owner, traverse_target in traverse_out.items():
+            self.outputs[traverse_owner].traverse_target = self.inputs[traverse_target]
+            self.outputs[traverse_owner].can_traverse = True
+        for key, value in additional_parameters.items():
+            self.parameters[key]=value
 
     def init_parameters(self, additional_parameters = {}) -> None:
         for socket in self.inputs:
@@ -782,36 +1015,67 @@ class MantisNode:
         for key, value in additional_parameters.items():
             self.parameters[key]=value
 
-    def gen_property_socket_map(self, prop_owner_group=None) -> dict:
+    # We really only need to do this once at class registration
+    # Therefore the goal is not to have any mutable data here
+    # ======================= READ BELOW =======================
+    # We have the following classes of Mantis Socket ==> Blender Proeprty relationships:
+    # Scalar: 1 Mantis scalar ==> 1 Blender property. 1 Filler.
+    # Vector: 1 Mantis Vector socket ==> Blender vector property. 1 Filler.
+    # Scalar Combo: 1 Mantis scalar ==> many Blender scalars. 1 Filler per property.
+    # Vector Combo: 1 Mantis vector ==> vector of Blender vector property. 1 Filler per property.
+    # In every case, 1 filler per Blender property.
+    @classmethod
+    def gen_property_socket_map(cls, prop_owner_group=None) -> dict:
         from .mantis_dataclasses import MantisSocketFiller
         from dataclasses import replace
         from bpy.app import version as bpy_version
+        if cls.property_socket_map: # retrieve the cache if possible
+            return cls.property_socket_map
         props_sockets = {}
-        for s_template in self.socket_templates:
+        for s_template in cls.socket_templates:
             if s_template.min_bpy_version:
                 if bpy_version < s_template.min_bpy_version:
                     prPurple(f"Skipping socket {s_template.name} because "
                              f"it requires Blender {s_template.min_bpy_version} or higher")
                     continue # We can't set or drive this obviously.
-            if prop_owner_group is not None and \
-                    s_template.prop_owner_group != prop_owner_group:
+            if prop_owner_group is not None and s_template.prop_owner_group != prop_owner_group:
                 continue # for composite objects like bones
-            if not s_template.blender_property:
-                continue
-            filler = MantisSocketFiller(socket_name=s_template.name)
-            if isinstance(s_template.blender_property, str):
-                filler = replace(filler)
-                filler.property_name = s_template.blender_property
-                filler.default_value=s_template.default_value
-                props_sockets[s_template.blender_property]=filler
-            elif isinstance(s_template.blender_property, (tuple, list)):
-                for index, sub_prop in enumerate(s_template.blender_property):
-                    filler = replace(filler)
-                    filler.property_name = sub_prop
-                    filler.value_index=index
-                    filler.default_value = s_template.default_value[index]
-                    filler.combo_property=True
-                    props_sockets[sub_prop]= filler
+            if not s_template.blender_property: # this one has to be handled by the node itself.
+                continue # Or maybe it is UI-only, in any case we don't need to do anything here.
+            # Default to (Scalar <==> Scalar), as tuples for compatibility with other code paths
+            vector_property, combo = False, False # TODO: make socket templates pre-tuple-ize this
+            defaults = ( s_template.default_value, ); properties = ( s_template.blender_property, )
+            if isinstance( s_template.default_value, (list, tuple) ):
+                vector_property = True # this tells us if MANTIS provides a vector
+                # if Blender consumes a vector, then we map the vector to the vector. Easy.
+            if isinstance(s_template.blender_property, (tuple, list)):
+                # But maybe we provide data from one Mantis Socket to many Blender properties...
+                combo=True; properties = s_template.blender_property
+                if vector_property: # we have to break the default value up so we can map
+                                    # mantis data element-wise to a vector of Blender properties 
+                    defaults = s_template.default_value 
+            # Now iteratre through the properties and make 1 filler per.
+            for i, property in enumerate(properties):
+                default_index = i if vector_property and combo else 0 # only needed for Combo Vector
+                default = defaults[default_index] # usually 1:1 except for Combo Scalar
+                key = s_template.name + f"_{default_index}" + (f"_{property}" if combo else "")
+                value_index = default_index if combo and vector_property else None
+                from .utilities import socket_filler_operators as operators
+                property_value_operator = None
+                if s_template.property_value_operator:
+                    property_value_operator = getattr(operators, s_template.property_value_operator)
+                props_sockets[key] = MantisSocketFiller(
+                    socket_name = s_template.name,     # This is a dict because node
+                    property_name = property,          # instances should be able 
+                    value_index = value_index,         # to edit Fillers.
+                    default_value = default,           # The keys are not inherently
+                    combo_property = combo,            # meaningful and are
+                    vector_property = vector_property, # NOT USED DOWNSTREAM!
+                    is_animatable = s_template.is_animatable,
+                    property_group = s_template.prop_owner_group,
+                    property_value_operator = property_value_operator)
+        if prop_owner_group is None:
+            cls.property_socket_map = props_sockets  # We should cache this one!
         return props_sockets
 
     def set_traverse(self, traversal_pairs = [(str, str)]) -> None:
@@ -912,40 +1176,36 @@ class MantisNode:
         return prop
 
     def fill_parameters(self, ui_node=None)  -> None:
-        from .utilities.utilities import get_ui_node
-        from .utilities.node_common import get_socket_value
+        from mathutils import Matrix
+        from .utilities.utilities import handle_overlay_detect
         if not ui_node:
-            if ( (self.signature[0] in  ["MANTIS_AUTOGENERATED", "SCHEMA_AUTOGENERATED" ]) or
-                (self.signature[-1] in ["NodeGroupOutput", "NodeGroupInput"]) ): # I think this is harmless
+            if ( (self.signature[0] in FILL_PARAMETERS_SKIP_ROOTS) or
+                (self.signature[-1] in FILL_PARAMETERS_SKIP_NAMES) ): # I think this is harmless
                 return None
-            else: # BUG shouldn't this use ui_signature??
-                ui_node = get_ui_node(self.signature, self.base_tree)
-            if not ui_node:
-                raise RuntimeError(wrapRed("No UI Node found for... %s" % ( [self.base_tree] + list(self.signature[1:]) ) ) )
+            raise RuntimeError(wrapRed("No UI Node found for... %s" % ( [self.base_tree] + list(self.signature[1:]) ) ) )
+        ui_inputs = ui_node.inputs; ui_outputs = ui_node.outputs
         for key in self.parameters.keys():
-            node_socket = ui_node.inputs.get(key)
             if self.parameters[key] is not None: # the parameters are usually initialized as None.
                 continue # will be filled by the node itself
-            if not node_socket: #maybe the node socket has no name
-                if ( ( len(ui_node.inputs) == 0) and ( len(ui_node.outputs) == 1) ):
-                    node_socket = ui_node.outputs[0] # this is a simple input node.
-                elif key == 'Name': # for Links we just use the Node Label, or if there is no label, the name.
-                    self.parameters[key] = ui_node.label if ui_node.label else ui_node.name
-                    continue
-            if node_socket:
-                if node_socket.bl_idname in  ['RelationshipSocket', 'xFormSocket']: continue
+            if node_socket := ui_inputs.get(key):
+                if node_socket.bl_idname in FILL_PARAMETERS_SKIP_SOCKETS: continue
                 elif node_socket.is_linked and (not node_socket.is_output): continue
                 # we will get the value from the link, because this is a linked input port.
                 # very importantly, we do not pass linked outputs
                 # fill these because they are probably Input nodes.
-                elif hasattr(node_socket, "default_value"):
-                    if (value := get_socket_value(node_socket)) is not None:
-                        if isinstance(value, str) and "$" in value:
-                            from .utilities.utilities import handle_overlay_detect
-                            value = handle_overlay_detect(self, node_socket.identifier, 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))
+                if node_socket.bl_idname in no_default_value: continue
+                if node_socket.bl_idname == 'MatrixSocket':
+                    v = node_socket.default_value
+                    self.parameters[key] = Matrix(( tuple(v[0:4]),
+                                                    tuple(v[4:8]),
+                                                    tuple(v[8:12]),
+                                                    tuple(v[12:16]) ))
+                elif (value := node_socket.default_value) is not None:
+                    if isinstance(value, str) and "$" in value:
+                        value = handle_overlay_detect(self, node_socket.identifier, 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))
 
     # I don't think this works! but I like the idea
     def call_on_all_ancestors(self, *args, **kwargs):
@@ -1045,6 +1305,10 @@ class MantisNode:
 
 # do I need this and the link class above?
 class DummyLink:
+    __slots__ = ("from_socket", "to_socket",
+                 "from_mantis_node", "to_mantis_node",
+                 "multi_input_sort_id",
+                 "original_from")
     #gonna use this for faking links to keep the interface consistent
     def __init__(self, from_socket, to_socket, from_mantis_node=None, to_mantis_node=None, original_from=None, multi_input_sort_id=0):
         self.from_socket = from_socket
@@ -1065,7 +1329,7 @@ class DummyLink:
 def detect_hierarchy_link(from_node, from_socket, to_node, to_socket,):
     if to_node.node_type in ['DUMMY_SCHEMA', 'SCHEMA']:
         return False #TODO: find out if filtering SCHEMA types is wise
-    if from_socket in from_name_filter and to_socket in ["Array"]:
+    if from_socket in from_name_filter and to_socket in {"Array"}:
         # SPECIAL CASE: arrays essentially extend the non-hierarchy edge
         #       so if the connection to the array isn't hierarchy, the array node
         #       in the middle gets disconnected and never executes.
@@ -1080,10 +1344,16 @@ def detect_hierarchy_link(from_node, from_socket, to_node, to_socket,):
     return True
 
 class NodeLink:
-    from_node = None
-    from_socket = None
-    to_node = None
-    to_socket = None
+    __slots__ = (
+        "from_node",
+        "from_socket",
+        "to_node",
+        "to_socket",
+        "multi_input_sort_id",
+        "sub_sort_id",
+        "is_hierarchy",
+        "is_alive",
+    )
 
     def __init__(self, from_node, from_socket, to_node, to_socket, multi_input_sort_id=0, sub_sort_id=0):
         if from_node.signature == to_node.signature:
@@ -1127,6 +1397,15 @@ class NodeLink:
             init_dependencies(to_node)
 
 class NodeSocket:
+    __slots__ = (
+        "can_traverse",
+        "traverse_target",
+        "node",
+        "name",
+        "is_input",
+        "links",
+        "is_linked",
+    )
     def __init__(self, is_input = False,
                  node = None, name = None,
                  traverse_target = None):
@@ -1180,21 +1459,16 @@ class NodeSocket:
         return self.node.__repr__() + "::" + self.name
 
 class MantisNodeSocketCollection(dict):
+    __slots__ = ("is_input", "node",)
     def __init__(self, node, is_input=False):
         self.is_input = is_input
         self.node = node
 
     def init_sockets(self, sockets):
         for socket in sockets:
-            if isinstance(socket, str):
-                self[socket] = NodeSocket(is_input=self.is_input, name=socket, node=self.node)
-            elif isinstance(socket, MantisSocketTemplate):
-                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)
-            else:
-                raise RuntimeError(f"NodeSocketCollection keys must be str or MantisSocketTemplate, not {type(socket)}")
-
+            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]

+ 52 - 103
deformer_nodes.py

@@ -35,11 +35,13 @@ def trace_xForm_back(mantis_node, socket):
         raise GraphError(wrapRed(f"No other object found for {mantis_node}."))
 
 class MantisDeformerNode(MantisNode):
+    node_type = 'LINK'
+    __slots__ = ()
     def __init__(self, signature : tuple,
                  base_tree : NodeTree,
                  socket_templates : list[MantisSocketTemplate]=[]):
-        super().__init__(signature, base_tree, socket_templates)
-        self.node_type = 'LINK'
+        super().__init__(signature, base_tree, socket_templates,
+                         additional_parameters= {"Name":None})
         self.prepared = True
         self.bObject=[]
     # we need evaluate_input to have the same behaviour as links.
@@ -69,6 +71,10 @@ class MantisDeformerNode(MantisNode):
         super().reset_execution()
         self.bObject=[]; self.prepared=True
 
+    def fill_parameters(self, ui_node=None):
+        super().fill_parameters(ui_node)
+        self.parameters["Name"] = ui_node.label if ui_node.label else ui_node.name
+
 def standard_modifier_bind(self, bContext=None, operator=None):
     for d in self.bObject:
         # we'll only bind it if it is un-muted.
@@ -79,13 +85,8 @@ def standard_modifier_bind(self, bContext=None, operator=None):
 
 class DeformerArmature(MantisDeformerNode):
     '''A node representing an armature deformer'''
-
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, DeformArmatureSockets)
-        self.init_parameters(additional_parameters={"Name":None})
-        self.set_traverse([("Deformer", "Deformer")])
-        self.node_type = "LINK"
-        self.prepared = True
+    __slots__ = ()
+    socket_templates = DeformArmatureSockets
 
     def GetxForm(self, socket="Deformer"):
         if socket == "Deformer":
@@ -185,14 +186,16 @@ class DeformerArmature(MantisDeformerNode):
                             'selected_objects':[ob, armOb],
                             'active_pose_bone':deform_bones[0],
                             'selected_pose_bones':deform_bones,}
-        for b in armOb.data.bones:
-            b.select = True
+        
+        from .utilities.utilities import set_bone_selection
+        for pb in armOb.pose.bones:
+            set_bone_selection(pose_bone=pb, selection_state=True)
         with bContext.temp_override(**context_override):
             bpy.ops.paint.weight_paint_toggle()
             bpy.ops.paint.weight_from_bones(type='AUTOMATIC')
             bpy.ops.paint.weight_paint_toggle()
-        for b in armOb.data.bones:
-            b.select = False
+        for pb in armOb.pose.bones:
+            set_bone_selection(pose_bone=pb, selection_state=False)
         armOb.data.pose_position = 'POSE'
         # TODO: modify Blender to make this available as a Python API function.
 
@@ -254,7 +257,6 @@ class DeformerArmature(MantisDeformerNode):
                 vg.add([p_index], weights[b_name]/max_pre_normalized_weight, 'REPLACE')
         armOb.data.pose_position = 'POSE'
     
-
     def bFinalize(self, bContext=None):
         prGreen("Executing Armature Deform Node")
         mod_name = self.evaluate_input("Name")
@@ -265,8 +267,8 @@ class DeformerArmature(MantisDeformerNode):
                 raise RuntimeError(f"Modifier was not created in node {self} -- the object is invalid.")
             self.bObject.append(d)
             d.object = self.bGetParentArmature()
-            props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, d, props_sockets)
+            props_sockets = self.property_socket_map
+            evaluate_sockets(self, {"DEFAULT":d}, props_sockets)
             #
             if (skin_method := self.evaluate_input("Skinning Method")) == "AUTOMATIC_HEAT":
                 match ob.type:
@@ -280,16 +282,10 @@ class DeformerArmature(MantisDeformerNode):
             # elif skin_method == "EXISTING_GROUPS":
             #     pass
 
-
 class DeformerHook(MantisDeformerNode):
     '''A node representing a hook deformer'''
-
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, HookSockets)
-        # now set up the traverse target...
-        self.init_parameters(additional_parameters={"Name":None})
-        self.set_traverse([("Deformer", "Deformer")])
-        self.prepared = True
+    __slots__ = ()
+    socket_templates = HookSockets
 
     def driver_for_radius(self, object, hook, index, influence, bezier=True):
         """ Creates a driver to control the radius of a curve point with the hook."""
@@ -352,7 +348,7 @@ class DeformerHook(MantisDeformerNode):
         auto_bezier = self.evaluate_input("Auto-Bezier")
         target_node = self.evaluate_input('Hook Target')
         target = target_node.bGetObject(); subtarget = ""
-        props_sockets = self.gen_property_socket_map()
+        props_sockets = self.property_socket_map
         if isinstance(target, Bone) or isinstance(target, PoseBone):
             subtarget = target.name; target = target.id_data
         for xf in self.GetxForm():
@@ -410,19 +406,15 @@ class DeformerHook(MantisDeformerNode):
                 self.driver_for_radius(ob, target_node.bGetObject(), vertex, d.strength, bezier=False)
                 
             d.vertex_indices_set(vertices_used)
-            evaluate_sockets(self, d, props_sockets)
+            evaluate_sockets(self, {"DEFAULT":d}, props_sockets)
         finish_drivers(self)
         # todo: this node should be able to take many indices in the future.
         # Also: I have a Geometry Nodes implementation of this I can use... maybe...
 
-
 class DeformerMorphTarget(MantisDeformerNode):
     '''A node representing an armature deformer'''
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, MorphTargetSockets)
-        self.init_parameters(additional_parameters={"Name":None})
-        self.node_type = "LINK"
-        self.prepared = True
+    __slots__ = ()
+    socket_templates = MorphTargetSockets
     
     def GetxForm(self, trace_input="Object"):
         trace = trace_single_line(self, trace_input)
@@ -431,7 +423,6 @@ class DeformerMorphTarget(MantisDeformerNode):
                 return node
         raise GraphError("%s is not connected to an upstream xForm" % self)
 
-
     def bRelationshipPass(self, bContext = None,):
         prGreen("Executing Morph Target Node")
 
@@ -456,33 +447,16 @@ class DeformerMorphTarget(MantisDeformerNode):
         self.parameters["Name"] = ob # this is redundant but it's OK since accessing the mt is tedious
         self.executed = True
 
-
-
 class DeformerMorphTargetDeform(MantisDeformerNode):
     '''A node representing an armature deformer'''
+    __slots__ = ()
+    socket_templates = MorphTargetDeformSockets
 
     def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree)
-        inputs = [
-            "Deformer",
-            "Use Shape Key",
-            "Use Offset",
-        ]
-        outputs = [
-          "Deformer",
-        ]
-        self.outputs.init_sockets(outputs)
-        self.inputs.init_sockets(inputs)
-        self.init_parameters(additional_parameters={"Name":None})
-        self.set_traverse([("Deformer", "Deformer")])
-        self.node_type = "LINK"
-        self.prepared = True
+        super().__init__(signature, base_tree, self.socket_templates)
         self.executed = True
         setup_custom_property_inputs_outputs(self)
     
-    # bpy.data.node_groups["Morph Deform.045"].nodes["Named Attribute.020"].data_type = 'FLOAT_VECTOR'
-    # bpy.context.object.add_rest_position_attribute = True
-
     def reset_execution(self):
         return super().reset_execution()
         self.executed=True
@@ -498,14 +472,14 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
         
         # at this point we make the node tree
         from .utilities.geometry_node_graphgen import gen_morph_target_nodes
-        m, props_sockets = gen_morph_target_nodes(
+        d, props_sockets = gen_morph_target_nodes(
                             self.evaluate_input("Name"),
                             xf.bGetObject(),
                             targets,
                             context,
                             use_offset=self.evaluate_input("Use Offset"))
-        self.bObject.append(m)
-        evaluate_sockets(self, m, props_sockets)
+        self.bObject.append(d)
+        evaluate_sockets(self, {"DEFAULT":d}, props_sockets)
         finish_drivers(self)
 
     def gen_shape_key_lattice(self, xf, context):
@@ -534,7 +508,7 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
         xf.has_shape_keys = True
         
         # first make a basis shape key
-        keys, props_sockets, ={}, {}
+        keys = {}
         for i, t in enumerate(targets):
             mt_node = t.links[0].from_node; sk_ob = mt_node.GetxForm().bGetObject()
             if sk_ob is None:
@@ -562,12 +536,12 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
             sk.slider_max = 10
             keys[mt_name]=sk
             from .mantis_dataclasses import MantisSocketFiller
-            socket_value = MantisSocketFiller(
+            filler = MantisSocketFiller(
                 property_name=mt_name,
                 socket_name = "Value."+str(i).zfill(3),
                 default_value= 1.0,
             )
-            props_sockets[mt_name]= socket_value
+            evaluate_sockets( self, {"DEFAULT":sk}, {mt_name:filler})
         for i, t in enumerate(targets):
             mt_node = t.links[0].from_node; sk_ob = mt_node.GetxForm().bGetObject()
             if sk_ob is None: continue
@@ -576,7 +550,6 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
                 sk.relative_key = keys.get(rel)
         
         self.bObject.append(sk.id_data)
-        evaluate_sockets(self, sk.id_data, props_sockets)
         finish_drivers(self)
         prWhite(f"Initializing morph target took {time() -start_time} seconds")
 
@@ -623,7 +596,6 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
 
         # first make a basis shape key
         keys={}
-        props_sockets={}
         for i, t in enumerate(targets):
             mt_node = t.links[0].from_node; sk_ob = mt_node.GetxForm().bGetObject()
             if sk_ob is None:
@@ -656,12 +628,13 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
             sk.slider_max = 10
             keys[mt_name]=sk
             from .mantis_dataclasses import MantisSocketFiller
-            socket_value = MantisSocketFiller(
+            filler = MantisSocketFiller(
+                property_group=mt_name,
                 property_name=mt_name,
                 socket_name = "Value."+str(i).zfill(3),
                 default_value= 1.0,
             )
-            props_sockets[mt_name]= socket_value
+            evaluate_sockets( self, {"DEFAULT":sk}, {mt_name:filler})
         for i, t in enumerate(targets):
             mt_node = t.links[0].from_node; sk_ob = mt_node.GetxForm().bGetObject()
             if sk_ob is None: continue
@@ -670,7 +643,6 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
                 sk.relative_key = keys.get(rel)
         
         self.bObject.append(sk.id_data)
-        evaluate_sockets(self, sk.id_data, props_sockets)
         finish_drivers(self)
         prWhite(f"Initializing morph target took {time() -start_time} seconds")
 
@@ -700,17 +672,10 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
             else:
                 self.gen_morph_target_modifier(xf, bContext)
 
-
-
 class DeformerSurfaceDeform(MantisDeformerNode):
     '''A node representing an surface deform modifier'''
-
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, SurfaceDeformSockets)
-        # now set up the traverse target...
-        self.init_parameters(additional_parameters={"Name":None})
-        self.set_traverse([("Deformer", "Deformer")])
-        self.prepared = True
+    __slots__ = ()
+    socket_templates = SurfaceDeformSockets
 
     def GetxForm(self, socket="Deformer"):
         if socket == "Deformer":
@@ -731,23 +696,17 @@ class DeformerSurfaceDeform(MantisDeformerNode):
                 raise RuntimeError(f"Modifier was not created in node {self} -- the object is invalid.")
             self.bObject.append(d)
             self.get_target_and_subtarget(d, input_name="Target")
-            props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, d, props_sockets)
+            props_sockets = self.property_socket_map
+            evaluate_sockets(self, {"DEFAULT":d}, props_sockets)
 
     def bModifierApply(self, bContext=None):
         from bpy import ops
         standard_modifier_bind(self, bContext, ops.object.surfacedeform_bind)
 
-
 class DeformerMeshDeform(MantisDeformerNode):
     '''A node representing a mesh deform modifier'''
-
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, MeshDeformSockets)
-        # now set up the traverse target...
-        self.init_parameters(additional_parameters={"Name":None})
-        self.set_traverse([("Deformer", "Deformer")])
-        self.prepared = True
+    __slots__ = ()
+    socket_templates = MeshDeformSockets
 
     def GetxForm(self, socket="Deformer"):
         if socket == "Deformer":
@@ -768,8 +727,8 @@ class DeformerMeshDeform(MantisDeformerNode):
                 raise RuntimeError(f"Modifier was not created in node {self} -- the object is invalid.")
             self.bObject.append(d)
             self.get_target_and_subtarget(d, input_name="Object")
-            props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, d, props_sockets)
+            props_sockets = self.property_socket_map
+            evaluate_sockets(self, {"DEFAULT":d}, props_sockets)
 
     def bModifierApply(self, bContext=None):
         from bpy import ops
@@ -777,13 +736,8 @@ class DeformerMeshDeform(MantisDeformerNode):
 
 class DeformerLatticeDeform(MantisDeformerNode):
     '''A node representing a lattice deform modifier'''
-
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, LatticeDeformSockets)
-        # now set up the traverse target...
-        self.init_parameters(additional_parameters={"Name":None})
-        self.set_traverse([("Deformer", "Deformer")])
-        self.prepared = True
+    __slots__ = ()
+    socket_templates = LatticeDeformSockets
 
     def GetxForm(self, socket="Deformer"):
         if socket == "Deformer":
@@ -804,18 +758,13 @@ class DeformerLatticeDeform(MantisDeformerNode):
                 raise RuntimeError(f"Modifier was not created in node {self} -- the object is invalid.")
             self.bObject.append(d)
             self.get_target_and_subtarget(d, input_name="Object")
-            props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, d, props_sockets)
+            props_sockets = self.property_socket_map
+            evaluate_sockets(self, {"DEFAULT":d}, props_sockets)
 
 class DeformerSmoothCorrectiveDeform(MantisDeformerNode):
     '''A node representing a corrective smooth deform modifier'''
-
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, SmoothDeformSockets)
-        # now set up the traverse target...
-        self.init_parameters(additional_parameters={"Name":None})
-        self.set_traverse([("Deformer", "Deformer")])
-        self.prepared = True
+    __slots__ = ()
+    socket_templates = SmoothDeformSockets
 
     def GetxForm(self, socket="Deformer"):
         if socket == "Deformer":
@@ -836,5 +785,5 @@ class DeformerSmoothCorrectiveDeform(MantisDeformerNode):
                 raise RuntimeError(f"Modifier was not created in node {self} -- the object is invalid.")
             self.bObject.append(d)
             # self.get_target_and_subtarget(d, input_name="Object")
-            props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, d, props_sockets)
+            props_sockets = self.property_socket_map
+            evaluate_sockets(self, {"DEFAULT":d}, props_sockets)

+ 2 - 3
deformer_socket_templates.py

@@ -26,11 +26,10 @@ DeformArmatureSockets = [
                            blender_property='use_bone_envelopes'),
     UseVertexGroups := ST(name="Use Vertex Groups", bl_idname="BooleanSocket", is_input=True, default_value=True,
                            blender_property='use_vertex_groups'),
-    DeformerInput := ST(name="Deformer", bl_idname="DeformerSocket", is_input=True),
+    DeformerInput := ST(name="Deformer", bl_idname="DeformerSocket", is_input=True, traverse_target='Deformer'),
     CopySkinWeightsFrom := ST(name="Copy Skin Weights From", bl_idname="xFormSocket", is_input=True, hide=True),
     SkinningMethod := ST(name="Skinning Method", bl_idname="EnumSkinning", is_input=True),
-    DeformerOutput := ST(name="Deformer", bl_idname="DeformerSocket", is_input=False),
-
+    DeformerOutput := ST(name="Deformer", bl_idname="DeformerSocket", is_input=False, traverse_target='Deformer'),
 ]
 
 HookSockets= [

+ 21 - 0
execution/parse_expression.py

@@ -0,0 +1,21 @@
+# This file is a minimal parser for simple expressions
+# It's used for e.g. Overlay variables
+# Also used to figure out the initial value of a driver for e.g. Curve Pin Node
+
+# For now I will only support basic operations.
+# Once I get this working it will be easy to expand
+tokens = {
+    "+" : "ADD",
+    "-" : 'SUBTRACT',
+    "/" : 'DIVIDE',
+    "*" : 'MULTIPLY',
+}
+
+#I'm too tired to work on this but I need to do it later so... I'll save the file.
+def parse_expression(expression):
+    numbers = []
+    current_number = ""
+    # for i range len(expression):
+    #     if 
+    pass
+        

+ 101 - 121
execution/readtree.py

@@ -1,82 +1,15 @@
 from ..utilities.utilities import prRed, prGreen, prPurple, prWhite, prOrange, \
                         wrapRed, wrapGreen, wrapPurple, wrapWhite, wrapOrange
 
-
-# this function is kind of confusing and is very important,
-# so it bears a full explanation: its purpose is to connect
-# the links going into a group to the nodes in that group.
-# FIRST we connect all the incoming links into the Group Node to
-# a Group Interface node that does nothing but mark the entrance.
-# Then, we connect all the outgoing links back to the nodes
-# that had incoming links, so the nodes OUTSIDE the Node Group
-# are connected directly to BOTH the GroupInterface and the
-# nodes INSIDE the node group.
-# we give the GroupInterface nodes an obscenely high
-# multi_input_sort_id so that they are always last.
-# but since these links are going IN, they shouldn't cause any problems.
-# the sub_sort_id is set here in case there are UI links which represent
-# multiple Mantis links - the mantis links are sorted within the UI links
-# and the UI links are sorted as normal, so all the links are in the right
-# order.... probably. BUG here?
-# I want the Group Interface nodes to be part of the hierarchy...
-# but I want to cut the links. hmmm what to do? Fix it if it causes problems.
-# solution to hypothetical BUG could be to do traversal on the links
-# instead of the sockets.
-def grp_node_reroute_common(in_node, out_node, interface):
-    from ..base_definitions import links_sort_key
-    for in_node_input in in_node.inputs:
-        i = 0
-        if len(in_node_input.links)>1: # sort here to ensure correct sub_sort_id
-            in_node_input.links.sort(key=links_sort_key)
-        while (in_node_input.links):
-            in_link = in_node_input.links.pop()
-            from_node = in_link.from_node; from_socket = in_link.from_socket
-            link = from_node.outputs[from_socket].connect(
-                interface,in_node_input.name, sort_id = 2**16, sub_sort_id=i)
-            i += 1; in_link.die()
-    for out_node_output in out_node.outputs:
-        while (out_node_output.links):
-            out_link = out_node_output.links.pop()
-            to_node = out_link.to_node; to_socket = out_link.to_socket
-            for j, l in enumerate(interface.inputs[out_node_output.name].links):
-                # we are connecting the link from the ORIGINAL output to the FINAL input.
-                link = l.from_node.outputs[l.from_socket].connect(
-                    to_node, to_socket, sort_id = out_link.multi_input_sort_id)
-                link.sub_sort_id = j
-            out_link.die()
-
-def reroute_links_grp(group, all_nodes):
-    from ..internal_nodes import GroupInterface
-    interface = GroupInterface(
-        ( *group.signature, "InputInterface"),
-        group.base_tree, group.ui_node, 'INPUT',)
-    all_nodes[interface.signature] = interface
-    if group.inputs:
-        if group_input := all_nodes.get(( *group.signature, "NodeGroupInput")):
-            grp_node_reroute_common(group, group_input, interface)
-        else:
-            raise RuntimeError("internal error: failed to enter a node group ")
-
-def reroute_links_grpout(group_output, all_nodes):
-    if (group := all_nodes.get( ( *group_output.signature[:-1],) )):
-        from ..internal_nodes import GroupInterface
-        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)
-    else:
-        prOrange(f"WARN: unconnected outputs from a node group "
-                 "(maybe you are running the tree from inside a node group?)")
-
 # FIXME I don't think these signatures are unique.
 # TODO this is a really silly and bad and also really dumb way to do this
 def insert_lazy_parents(mantis_node):
     from ..link_nodes import LinkInherit
     inherit_node = None
     if mantis_node.inputs["Relationship"].is_connected:
-        from ..utilities.node_common import trace_single_line
-        node_line, last_socket = trace_single_line(mantis_node, 'Relationship')
+        from ..utilities.node_common import trace_single_line_safe
+        # BUG this line can create infinite loops
+        node_line, last_socket = trace_single_line_safe(mantis_node, 'Relationship')
         # if last_socket is from a valid XFORM, it is the relationship in
         # because it was traversed from the xForm Out... so get the traverse target.
         if last_socket.traverse_target is None:
@@ -112,7 +45,7 @@ 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.init_sockets([ui_socket.name])
+    mantis_node.outputs[ui_socket.name] = NodeSocket(is_input=False, name=ui_socket.name, node=mantis_node)
     mantis_node.ui_signature = None # does not exist in the UI
     return mantis_node
 
@@ -133,20 +66,23 @@ def make_connections_to_ng_dummy(base_tree, tree_path_names, local_mantis_nodes,
             #  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)
-            from ..utilities.node_common import get_socket_value
             if from_mantis_node: # autogen can fail and we should catch it.
-                from_mantis_node.parameters = {inp.name:get_socket_value(inp)}
+                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)
             else:
                 prRed("No available auto-generated class for input %s in %s" % (inp.name, np.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 ):
+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
     for ui_node in current_tree.nodes:
-        # HACK I found that this isn't being set sometimes. I wonder why? It makes the most sense to do this here.
-        if hasattr(ui_node, 'initialized'): ui_node.initialized=True
-        # end HACK. TODO: find out why this is not set sometimes. This is only needed for UI socket change updates.
+        mantis_node = None
         if ui_node.bl_idname in ["NodeFrame", "NodeReroute"]:
             continue # not a Mantis Node
         if ui_node.bl_idname in ["NodeGroupInput", "NodeGroupOutput"]:
@@ -157,16 +93,16 @@ def gen_mantis_nodes(base_tree, current_tree, tree_path_names, mContext, all_man
                 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"]:
-                    mantis_node.reroute_links = reroute_links_grpout
+                    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 )
             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)
             if ui_node.bl_idname == "MantisNodeGroup":
-                group_nodes.append(mantis_node)
-                mantis_node.reroute_links = reroute_links_grp
+                mantis_node.reroute_links_type = 'group'
             else:
-                group_nodes.append(mantis_node)
+                mantis_node.reroute_links_type = 'schema'
                 schema_nodes[sig] = 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):
@@ -181,12 +117,14 @@ def gen_mantis_nodes(base_tree, current_tree, tree_path_names, mContext, all_man
         else:
             mantis_node = None
             prRed(f"Can't make mantis_node for.. {ui_node.bl_idname}")
-        # this should be done at init
-        mantis_node.mContext = mContext
-        if mantis_node.signature[0] not in ['MANTIS_AUTOGENERATED'] and mantis_node.node_type not in ['SCHEMA', 'DUMMY', 'DUMMY_SCHEMA']:
-            mantis_node.fill_parameters()
-
-def data_from_tree(base_tree, tree_path, mContext, dummy_nodes, all_mantis_nodes, all_schema):#
+        if mantis_node is not None:
+            # this should be done at init
+            mantis_node.mContext = mContext
+            if mantis_node.signature[0] != 'MANTIS_AUTOGENERATED' and mantis_node.node_type not in {'SCHEMA', 'DUMMY', 'DUMMY_SCHEMA'}:
+                mantis_node.fill_parameters(ui_node)
+            mantis_node.is_schema_prototype = in_schema
+
+def data_from_tree(base_tree, tree_path, mContext, dummy_nodes, all_mantis_nodes, all_schema, in_schema):#
     # TODO: it should be relatively easy to make this use a while loop instead of recursion.
     local_mantis_nodes, group_nodes = {}, []
     tree_path_names = [tree.name for tree in tree_path if hasattr(tree, "name")]
@@ -196,9 +134,11 @@ def data_from_tree(base_tree, tree_path, mContext, dummy_nodes, all_mantis_nodes
         current_tree = base_tree
     #
     if current_tree: # the node-group may not have a tree set - if so, ignore it.
+        if not in_schema:
+            in_schema = current_tree.bl_idname in ['SchemaTree']
         from ..utilities.utilities import clear_reroutes
         links = clear_reroutes(list(current_tree.links))
-        gen_mantis_nodes(base_tree, current_tree, tree_path_names, mContext, all_mantis_nodes, local_mantis_nodes, dummy_nodes, group_nodes, all_schema)
+        gen_mantis_nodes(base_tree, current_tree, tree_path_names, mContext, all_mantis_nodes, local_mantis_nodes, dummy_nodes, group_nodes, all_schema, in_schema)
 
         from ..utilities.utilities import link_mantis_nodes
         for link in links:
@@ -208,7 +148,7 @@ def data_from_tree(base_tree, tree_path, mContext, dummy_nodes, all_mantis_nodes
             insert_default_values_base_tree(base_tree, all_mantis_nodes)
         # Now, descend into the Node Groups and recurse
         for mantis_node in group_nodes:
-            data_from_tree(base_tree, tree_path+[mantis_node.ui_node], mContext, dummy_nodes, all_mantis_nodes, all_schema)
+            data_from_tree(base_tree, tree_path+[mantis_node.ui_node], mContext, dummy_nodes, all_mantis_nodes, all_schema, in_schema)
     return dummy_nodes, all_mantis_nodes, all_schema
 
 STRING_PADDING = 4
@@ -334,13 +274,6 @@ def node_scan_overlays(node, socket_id,):
     return result # because sometimes I need to get the result, too
 
 from ..utilities.utilities import check_and_add_root, init_connections, init_dependencies, init_schema_dependencies
-
-def is_signature_in_other_signature(parent_signature, child_signature):
-    # If the other signature is shorter, it isn't a child node
-    if len(parent_signature) > len(child_signature):
-        return False
-    return parent_signature[0:] == child_signature[:len(parent_signature)]
-
 def solve_schema_to_tree(mantis_node, all_mantis_nodes, roots=[], error_popups=False):
     from ..utilities.utilities import get_ui_node
     np = get_ui_node(mantis_node.signature, mantis_node.base_tree)
@@ -351,17 +284,8 @@ def solve_schema_to_tree(mantis_node, all_mantis_nodes, roots=[], error_popups=F
     except Exception as e:
     #     # the schema will run the error cleanup code, we just need to raise or not
         solved_nodes = {}
-        mantis_node.base_tree.hash=''
+        mantis_node.base_tree.hash_recursive=''
         raise execution_error_cleanup(mantis_node, e, show_error=error_popups)
-
-    # maybe this should be done in schema solver. TODO invesitigate a more efficient way
-    del_me = []
-    for k, v in all_mantis_nodes.items():
-        # delete all the schema's ui_node and interface nodes. The links have already been deleted by the solver.
-        if v.signature[0] not in ['MANTIS_AUTOGENERATED'] and is_signature_in_other_signature(mantis_node.signature, k):
-            del_me.append(k)
-    for k in del_me:
-        del all_mantis_nodes[k]
     for k,v in solved_nodes.items():
         all_mantis_nodes[k]=v
         init_connections(v)
@@ -495,6 +419,7 @@ def parse_tree(base_tree, error_popups=False):
     base_tree.execution_id = uuid4().__str__() # set the unique id of this execution
 
     from ..base_definitions import MantisExecutionContext
+    from ..utilities.node_common import GraphError
     mContext = MantisExecutionContext(base_tree=base_tree)
 
     import time
@@ -503,10 +428,27 @@ def parse_tree(base_tree, error_popups=False):
     #  then they stick around because the function definition inits them once and keeps a reference
     # so instead I have to supply them to avoid ugly code or bugs elsewhere
     # it's REALLY confusing when you run into this sort of problem. So it warrants four entire lines of comments!      
-    dummy_nodes, all_mantis_nodes, all_schema =  data_from_tree(base_tree, tree_path = [None], mContext=mContext, dummy_nodes = {}, all_mantis_nodes = {}, all_schema={})
+    try:
+        dummy_nodes, all_mantis_nodes, all_schema =  data_from_tree(
+            base_tree,
+            tree_path = [None], 
+            mContext=mContext,
+            dummy_nodes = {},
+            all_mantis_nodes = {},
+            all_schema={},
+            in_schema=False
+            )
+    except GraphError as e:
+        base_tree.tree_valid = False; base_tree.hash_recursive=""
+        if error_popups:
+            from bpy import context
+            def error_popup_draw(self, context):
+                error_popup_draw_inner(self.layout, e, ())
+            context.window_manager.popup_menu(error_popup_draw, title="Error", icon='ERROR')
+        else: raise e
+        return # it is not possible to parse the tree until GraphErrors are resolved.
     for dummy in dummy_nodes.values():    # reroute the links in the group nodes
-        if (hasattr(dummy, "reroute_links")):
-            dummy.reroute_links(dummy, all_mantis_nodes)
+        dummy.reroute_links(all_mantis_nodes)
     prGreen(f"Pulling data from tree took {time.time() - data_start_time} seconds")
 
     start_time = time.time()
@@ -576,7 +518,7 @@ def parse_tree(base_tree, error_popups=False):
                     solved_nodes = {}
                     if error_popups == False:
                         raise e
-                    return # break out of this function regardless.
+                    return # it is not possible to parse the tree.
                 unsolved_schema.remove(n)
                 schema_solve_done.add(n)
                 for node in solved_nodes.values():
@@ -634,16 +576,29 @@ def parse_tree(base_tree, error_popups=False):
             init_connections(mantis_node) # because we have removed many connections.
 
         if (mantis_node.node_type in ['XFORM']) and ("Relationship" in mantis_node.inputs.keys()):
-            if (new_mantis_node := insert_lazy_parents(mantis_node)):
-                kept_mantis_node[new_mantis_node.signature]=new_mantis_node
-                # be sure to add the Mantis context.
-                new_mantis_node.mContext =mContext
+            from ..utilities.node_common import GraphError
+            try:
+                if (new_mantis_node := insert_lazy_parents(mantis_node)):
+                    kept_mantis_node[new_mantis_node.signature]=new_mantis_node
+                    # be sure to add the Mantis context.
+                    new_mantis_node.mContext =mContext
+            except GraphError as e:
+                e = execution_error_cleanup(mantis_node, e, show_error=error_popups)
+                if error_popups == False:
+                    raise e
+                return  # it is not possible to parse the tree.
         kept_mantis_node[mantis_node.signature]=mantis_node
     prWhite(f"Parsing tree took {time.time()-start_time} seconds.")
     prWhite("Number of Nodes: %s" % (len(kept_mantis_node)))
     return kept_mantis_node
 
 from ..utilities.utilities import switch_mode
+def error_popup_draw_inner(layout, exception, ui_sig = ()):
+    node_string = f"See node: {ui_sig[1:]}." if ui_sig else ""
+    message = f"Error: {exception}\n" + node_string  
+    lines = message.splitlines()
+    for line in lines:
+        layout.label(text=line)
 
 def execution_error_cleanup(node, exception, switch_objects = [], show_error=False ):
     from bpy import  context
@@ -671,8 +626,7 @@ def execution_error_cleanup(node, exception, switch_objects = [], show_error=Fal
                 pass
             finally:
                 def error_popup_draw(self, context):
-                    self.layout.label(text=f"Error: {exception}")
-                    self.layout.label(text=f"see node: {ui_sig[1:]}.")
+                    error_popup_draw_inner(self.layout, exception, ui_sig)
                 context.window_manager.popup_menu(error_popup_draw, title="Error", icon='ERROR')
     switch_mode(mode='OBJECT', objects=switch_objects)
     for ob in switch_objects:
@@ -680,7 +634,13 @@ def execution_error_cleanup(node, exception, switch_objects = [], show_error=Fal
     prRed(f"Error: {exception} in node {ui_sig}")
     return exception
 
-def sort_execution(nodes):
+def print_cycle(nodes):
+    ret_str = ""
+    for node in nodes:
+        ret_str+= node.__repr__() + '\n'
+    return ret_str
+
+def sort_execution(nodes, error_popups):
     from ..utilities.node_common import GraphError
     from collections import deque
     xForm_pass = deque()
@@ -688,15 +648,20 @@ def sort_execution(nodes):
     sorted_nodes = []
     # initialize the hierarchy dependency count. We'll use this to ensure there
     # are no cycles (using Kahn's algorithm).
+    expected_sort_quantity = len(nodes)
     for mantis_node in nodes.values():
+        if mantis_node.node_type in ['DUMMY', 'DUMMY_SCHEMA', 'INTERFACE']:
+            expected_sort_quantity -=1 ; continue
+        if mantis_node.is_schema_prototype:
+            expected_sort_quantity -= 1; continue
         mantis_node.unmet_hierarchy_dependency_count = len(mantis_node.hierarchy_dependencies)
         if mantis_node.unmet_hierarchy_dependency_count == 0 :
             xForm_pass.append(mantis_node)
     
     processed_count = 0
     while(xForm_pass):
+        # for some reason using popleft here breaks everything... better not push my luck/
         n = xForm_pass.pop()
-        # if n.execution_prepared: continue
         n.execution_prepared = True
         sorted_nodes.append(n)
         processed_count+=1
@@ -704,9 +669,17 @@ def sort_execution(nodes):
             child.unmet_hierarchy_dependency_count-= 1
             if child.unmet_hierarchy_dependency_count == 0:
                 xForm_pass.append(child)
-    if processed_count < len(nodes):
+    if processed_count < expected_sort_quantity:
+        cycle = []
+        for n in nodes.values():
+            if n.execution_prepared == False:
+                cycle.append(n)
         execution_failed = True
-        raise GraphError("detected a cycle in the tree")
+        e = GraphError("Detected a cycle in the tree, containing at "
+                         "least one of the following nodes: " + print_cycle(cycle))
+        e = execution_error_cleanup(cycle[-1], e, show_error=error_popups)
+        prRed(processed_count, expected_sort_quantity)
+        raise e
     return sorted_nodes, execution_failed
 
 def execute_tree(nodes, base_tree, context, error_popups = False, profile=False):
@@ -751,8 +724,15 @@ def execute_tree(nodes, base_tree, context, error_popups = False, profile=False)
         profiler.new_session(mContext.execution_id)
         sort_key = 'total'
     try:
-        sorted_nodes, execution_failed = sort_execution(nodes)
+        sorted_nodes, execution_failed = sort_execution(nodes, error_popups=error_popups)
+    except Exception as e:
+        # We'll fire the popup in sort_execution because it has the node in scope
+        if error_popups == False:
+            raise e
+        execution_failed = True
+    try:
         for n in sorted_nodes:
+            if execution_failed: break
             try:
                 check_and_apply_overlay(n)
                 if not n.prepared:

+ 31 - 30
execution/schema_solve.py

@@ -65,7 +65,7 @@ class SchemaSolver:
 
         from bpy.types import NodeGroupInput, NodeGroupOutput
         for ui_node in self.tree.nodes:
-            # first we need to fill the parameters of the schema nodes.
+            # Make the mantis nodes for the special interface types
             # we use the bl_idname because all schema nodes should be single-instance
             signature = (*self.tree_path_names, ui_node.bl_idname)
             if isinstance(ui_node, (SchemaUINode, NodeGroupInput, NodeGroupOutput)):
@@ -77,25 +77,24 @@ class SchemaSolver:
                 if not (mantis_node := self.all_nodes.get(get_sig)):
                     raise RuntimeError(wrapRed(f"Not found: {get_sig}"))
                 self.schema_nodes[signature] = mantis_node
-                mantis_node.fill_parameters(ui_node)
             # HACK to make Group Nodes work
             if ui_node.bl_idname == "NodeGroupInput":
                 from ..schema_nodes import SchemaConstInput
                 mantis_node = SchemaConstInput(signature=signature, base_tree=self.node.base_tree, parent_schema_node=self.node)
                 self.schema_nodes[signature] = mantis_node
-                mantis_node.fill_parameters(ui_node)
             if ui_node.bl_idname == "NodeGroupOutput":
                 from ..schema_nodes import SchemaConstOutput
                 mantis_node = SchemaConstOutput(signature=signature, base_tree=self.node.base_tree, parent_schema_node=self.node)
                 self.schema_nodes[signature] = mantis_node
-                mantis_node.fill_parameters(ui_node)
 
+    # 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 = lambda : '.'+str(self.uuid)+'.'+str(self.index).zfill(4)
-        self.prev_index_str = lambda : '.'+str(self.uuid)+'.'+str(self.index-1).zfill(4)
+        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=lambda : ''
-            self.prev_index_str=lambda : ''
+            self.index_str = ''
+            self.prev_index_str = ''
 
     def init_schema_links(self,):
         """ Sort and store the links to/from the Schema group node."""
@@ -152,7 +151,7 @@ class SchemaSolver:
     def gen_solve_iteration_mantis_nodes(self, frame_mantis_nodes, unprepared):
         for ui_node in self.tree.nodes:
             mantis_node_name = ui_node.name
-            index_str = self.index_str()
+            index_str = self.index_str
             mContext=self.node.mContext
             if isinstance(ui_node, SchemaUINode):
                 continue  # IGNORE the schema interface nodes, we already made them in __init__()
@@ -179,7 +178,8 @@ class SchemaSolver:
                     if ui_node.bl_idname in ["MantisNodeGroup"]:
                         mantis_node.prepared = False
                         mantis_node.node_type = 'DUMMY_SCHEMA' # we promote it to a schema for now
-                        mantis_node.inputs.init_sockets(['Schema Length']) # add a Schema Length socket
+                        from ..base_definitions import NodeSocket
+                        mantis_node.inputs['Schema Length'] = NodeSocket(is_input=True, node=mantis_node, name='Schema Length')
                         mantis_node.parameters['Schema Length'] = 1 # set the length to 1 since it is a single group instance
                         # we'll make the autogenerated nodes for constant inputs. It doesn't matter that there is technically
                         #  a ui_node available for each one  -- these are cheap and I want this to be easy.
@@ -203,13 +203,13 @@ class SchemaSolver:
     def handle_link_from_index_input(self, index, frame_mantis_nodes, ui_link):
         from ..base_definitions import can_remove_socket_for_autogen
         _from_name, to_name = get_link_in_out(ui_link)
-        to_node = frame_mantis_nodes[ (*self.autogen_path_names, to_name+self.index_str()) ]
+        to_node = frame_mantis_nodes[ (*self.autogen_path_names, to_name+self.index_str) ]
 
         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.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)
@@ -232,7 +232,7 @@ class SchemaSolver:
         if to_name in replace_types:
             to_node = self.schema_nodes[(*self.autogen_path_names, to_name)]
         else:
-            to_node = frame_mantis_nodes[(*self.autogen_path_names, to_name+self.index_str())]
+            to_node = frame_mantis_nodes[(*self.autogen_path_names, to_name+self.index_str)]
         # this self.index_link is only used here?
         if self.index_link is None:
             # this should be impossible because the Schema gets an auto-generated Int input.
@@ -248,7 +248,7 @@ class SchemaSolver:
         if incoming is not None:
             from_node = incoming.from_node
             _from_name, to_name = get_link_in_out(ui_link)
-            to_node = frame_mantis_nodes[ (*self.autogen_path_names, to_name+self.index_str()) ]
+            to_node = frame_mantis_nodes[ (*self.autogen_path_names, to_name+self.index_str) ]
             socket_name=ui_link.to_socket.name
             if to_node.node_type in [ 'DUMMY_SCHEMA' ]:
                 socket_name=ui_link.to_socket.identifier
@@ -260,10 +260,10 @@ class SchemaSolver:
         for mantis_link in mantis_incoming_node.outputs[ui_link.to_socket.name].links:
             to_mantis_node, to_socket_name = mantis_link.to_node, mantis_link.to_socket
             from_name = get_link_in_out(ui_link)[0]
-            from_mantis_node = self.solved_nodes[ (*self.autogen_path_names, from_name+self.prev_index_str()) ]
+            from_mantis_node = self.solved_nodes[ (*self.autogen_path_names, from_name+self.prev_index_str) ]
 
             to_mantis_node_signature = ( *self.autogen_path_names,
-                            to_mantis_node.signature[-1] + self.index_str() )
+                            to_mantis_node.signature[-1] + self.index_str )
 
             # we need to detect if the next node is in a group.
             # REMEMBER: at this point, nested groups haven't been solved yet.
@@ -271,7 +271,7 @@ class SchemaSolver:
                     self.is_node_deeper_nested(to_mantis_node, from_mantis_node):
                 to_mantis_node = frame_mantis_nodes[ (
                     *self.autogen_path_names, # the SCHEMA_AUTOGENERATED string
-                    to_mantis_node.signature[-2]+ self.index_str() ) ]
+                    to_mantis_node.signature[-2]+ self.index_str ) ]
                 # this connection was forbidden before, right? so this should be a safe assumption.
                 assert to_mantis_node.node_type == 'DUMMY_SCHEMA', "I expected this to be a group/schema"
                 # we need to get the identifier of the named socket now.
@@ -300,7 +300,7 @@ class SchemaSolver:
             raise GraphError (f"{self.node} is missing a required input: {ui_link.from_socket.name}")
         from_node = incoming.from_node
         to_name = get_link_in_out(ui_link)[1]
-        to_node = frame_mantis_nodes[(*self.autogen_path_names, to_name+self.index_str())]
+        to_node = frame_mantis_nodes[(*self.autogen_path_names, to_name+self.index_str)]
         to_socket=ui_link.to_socket.name
         from ..base_definitions import MantisNodeGroup, SchemaGroup
         if isinstance(to_ui_node, (SchemaGroup, MantisNodeGroup)):
@@ -331,7 +331,7 @@ class SchemaSolver:
             raise RuntimeError(f"The array index {get_index} is out of bounds. Size: "
                                f"{len(self.array_input_connections[ui_link.from_socket.identifier])}")
         to_name = get_link_in_out(ui_link)[1]
-        to_node = frame_mantis_nodes[(*self.autogen_path_names, to_name+self.index_str())]
+        to_node = frame_mantis_nodes[(*self.autogen_path_names, to_name+self.index_str)]
         from_socket = incoming.from_node.outputs[incoming.from_socket]
         connection = from_socket.connect(to_node, ui_link.to_socket.name)
         # TODO: kill links  to the array get that are no longer needed
@@ -341,7 +341,7 @@ class SchemaSolver:
 
     def handle_link_to_array_input_get(self, frame_mantis_nodes, ui_link):
         from_name, to_name = get_link_in_out(ui_link)
-        from_mantis_node = frame_mantis_nodes[(*self.autogen_path_names, from_name+self.index_str())]
+        from_mantis_node = frame_mantis_nodes[(*self.autogen_path_names, from_name+self.index_str)]
         to_mantis_node = self.schema_nodes[(*self.tree_path_names, to_name)]
         # this only needs to be done once:
         if self.index == 0: # BUG? HACK? TODO find out what is going on here.
@@ -371,14 +371,14 @@ class SchemaSolver:
             else:
                 raise RuntimeError(wrapRed("make it so Mantis checks if there are enough Array inputs!"))
         to_name = get_link_in_out(ui_link)[1]
-        to_node = frame_mantis_nodes[(*self.autogen_path_names, to_name+self.index_str())]
+        to_node = frame_mantis_nodes[(*self.autogen_path_names, to_name+self.index_str)]
         connection = incoming.from_node.outputs[incoming.from_socket].connect(node=to_node, socket=ui_link.to_socket.name)
         init_connections(incoming.from_node)
 
     def handle_link_from_array_input_all(self, frame_mantis_nodes, ui_link):
         all_links = self.array_input_connections[ui_link.from_socket.identifier]
         to_name = get_link_in_out(ui_link)[1]
-        to_node = frame_mantis_nodes[(*self.autogen_path_names, to_name+self.index_str())]
+        to_node = frame_mantis_nodes[(*self.autogen_path_names, to_name+self.index_str)]
         # connection = incoming.from_node.outputs[incoming.from_socket].connect(node=to_node, socket=ui_link.to_socket.name)
         for l in all_links:
             # we need to copy the link with the new from-node info
@@ -403,7 +403,7 @@ class SchemaSolver:
         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()) ]
+            from_node = frame_mantis_nodes[(*self.autogen_path_names, from_name+self.index_str) ]
         else: # it's a mantis socket, set manually while looping though prepped links
             from_node = ui_link.from_socket.node
         from_socket_name = ui_link.from_socket.name
@@ -425,7 +425,7 @@ class SchemaSolver:
         # wire the schema node itself to the output so it will push the connections when solving
         to_node = self.schema_nodes[(*self.tree_path_names, to_ui_node.bl_idname)]
         from_name = get_link_in_out(ui_link)[0]
-        from_node = frame_mantis_nodes[ (*self.autogen_path_names, from_name+self.index_str()) ]
+        from_node = frame_mantis_nodes[ (*self.autogen_path_names, from_name+self.index_str) ]
         connection = from_node.outputs[ui_link.from_socket.identifier].connect(node=to_node, socket=ui_link.to_socket.name)
 
     def handle_link_from_array_type_to_array_out(self, original_ui_link, dummy_link):
@@ -442,7 +442,7 @@ class SchemaSolver:
         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()) ]
+            from_node = frame_mantis_nodes[(*self.autogen_path_names, from_name+self.index_str) ]
         else: # it's a mantis socket, set manually while looping though prepped links
             from_node = ui_link.from_socket.node
         from_socket_name = ui_link.from_socket.name
@@ -452,7 +452,7 @@ class SchemaSolver:
             to_node = outgoing.to_node
             from ..schema_nodes import SchemaIndex
             if isinstance(from_node, SchemaIndex):
-                signature = ("MANTIS_AUTOGENERATED", *self.tree_path_names[1:-1], self.index_str(),
+                signature = ("MANTIS_AUTOGENERATED", *self.tree_path_names[1:-1], self.index_str,
                              ui_link.from_socket.name, ui_link.from_socket.identifier)
                 from_node = self.all_nodes.get(signature)
                 if not from_node:
@@ -646,8 +646,8 @@ class SchemaSolver:
 
             # for any of the special cases, we hit a 'continue' block. So this connection is not special, and is made here.
             connection = link_mantis_nodes(self.autogen_path_names, ui_link,
-                                frame_mantis_nodes, from_suffix=self.index_str(),
-                                to_suffix=self.index_str())
+                                frame_mantis_nodes, from_suffix=self.index_str,
+                                to_suffix=self.index_str)
 
         for signature, node in frame_mantis_nodes.items():
             self.solved_nodes[signature]=node
@@ -691,7 +691,7 @@ class SchemaSolver:
                 ui_link=self.spoof_link_for_subschema_to_output_edge_case(ui_link)
                 links_to_output[i] = ui_link
             from_name = get_link_in_out(ui_link)[0]
-            signature = (*self.autogen_path_names, from_name+self.index_str())
+            signature = (*self.autogen_path_names, from_name+self.index_str)
             #use it directly if it is a mantis node; this happens when the previous node was a Schema
             if hasattr(ui_link, "from_node") and (from_node := self.schema_nodes.get( (*self.node.ui_signature, ui_link.from_node.bl_idname))):
                 unprepared.append(from_node)
@@ -753,7 +753,7 @@ class SchemaSolver:
                     for outgoing in outgoing_links:
                         if outgoing:
                             to_node = outgoing.to_node
-                            from_node =mantis_nodes_in_frame[(*self.autogen_path_names, from_np.name+self.index_str()) ]
+                            from_node =mantis_nodes_in_frame[(*self.autogen_path_names, from_np.name+self.index_str) ]
                             from_socket_name = link.from_socket.name
                             if from_node.node_type in ['DUMMY_SCHEMA']:
                                 from_socket_name = link.from_socket.identifier
@@ -807,6 +807,7 @@ 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

+ 94 - 9
internal_nodes.py

@@ -3,14 +3,87 @@ from bpy.types import Node
 from .base_definitions import MantisNode, MantisSocketTemplate
 from uuid import uuid4
 
+
+# this function is kind of confusing and is very important,
+# so it bears a full explanation: its purpose is to connect
+# the links going into a group to the nodes in that group.
+# FIRST we connect all the incoming links into the Group Node to
+# a Group Interface node that does nothing but mark the entrance.
+# Then, we connect all the outgoing links back to the nodes
+# that had incoming links, so the nodes OUTSIDE the Node Group
+# are connected directly to BOTH the GroupInterface and the
+# nodes INSIDE the node group.
+# we give the GroupInterface nodes an obscenely high
+# multi_input_sort_id so that they are always last.
+# but since these links are going IN, they shouldn't cause any problems.
+# the sub_sort_id is set here in case there are UI links which represent
+# multiple Mantis links - the mantis links are sorted within the UI links
+# and the UI links are sorted as normal, so all the links are in the right
+# order.... probably. BUG here?
+# I want the Group Interface nodes to be part of the hierarchy...
+# but I want to cut the links. hmmm what to do? Fix it if it causes problems.
+# solution to hypothetical BUG could be to do traversal on the links
+# instead of the sockets.
+# NOTE: using NodeSocket.links is slow, it should be cached
+def grp_node_reroute_common(in_node, out_node, interface):
+    from .base_definitions import links_sort_key
+    for in_node_input in in_node.inputs:
+        i = 0
+        if len(in_node_input.links)>1: # sort here to ensure correct sub_sort_id
+            in_node_input.links.sort(key=links_sort_key)
+        while (in_node_input.links):
+            in_link = in_node_input.links.pop()
+            from_node = in_link.from_node; from_socket = in_link.from_socket
+            link = from_node.outputs[from_socket].connect(
+                interface,in_node_input.name, sort_id = 2**16, sub_sort_id=i)
+            i += 1; in_link.die()
+    for out_node_output in out_node.outputs:
+        while (out_node_output.links):
+            out_link = out_node_output.links.pop()
+            to_node = out_link.to_node; to_socket = out_link.to_socket
+            for j, l in enumerate(interface.inputs[out_node_output.name].links):
+                # we are connecting the link from the ORIGINAL output to the FINAL input.
+                link = l.from_node.outputs[l.from_socket].connect(
+                    to_node, to_socket, sort_id = out_link.multi_input_sort_id)
+                link.sub_sort_id = j
+            out_link.die()
+
+def reroute_links_grp(group, all_nodes):
+    interface = GroupInterface(
+        ( *group.signature, "InputInterface"),
+        group.base_tree, group.ui_node, 'INPUT',)
+    all_nodes[interface.signature] = interface
+    if group.inputs:
+        if group_input := all_nodes.get(( *group.signature, "NodeGroupInput")):
+            grp_node_reroute_common(group, group_input, interface)
+        else:
+            raise RuntimeError("internal error: failed to enter a node group ")
+
+def reroute_links_grpout(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)
+    else:
+        prOrange(f"WARN: unconnected outputs from a node group "
+                 "(maybe you are running the tree from inside a node group?)")
+
+
+# TODO: make node_type a class property like it is for everything else
+# I can't do this without making some changes to schema and anything that
+# checks for DUMMY SCHEMA
 class DummyNode(MantisNode):
+    __slots__ = ("node_type", "ui_node", "solver", "uuid", "reroute_links_type")
     def __init__(self, signature, base_tree, ui_node = None, ui_signature=None):
         super().__init__(signature, base_tree)
         self.ui_node = ui_node
-        self.node_type = 'DUMMY'
         self.prepared = True
         self.uuid = uuid4()
         self.solver = None
+        self.node_type = 'DUMMY'
+        self.reroute_links_type='group' # group, out, schema
         if ui_node:
             if ui_node.bl_idname in ["MantisSchemaGroup"]:
                 self.node_type = 'DUMMY_SCHEMA'
@@ -32,23 +105,33 @@ class DummyNode(MantisNode):
         # this is ugly and I hate it.
         self.execution_prepared=True # in case it gets left behind in the tree as a dependency
 
+    def reroute_links(self, all_nodes):
+        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)
 
 class NoOpNode(MantisNode):
+    __slots__ = ()
+    node_type = 'UTILITY'
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree)
-        self.inputs.init_sockets(["Input"])
-        self.outputs.init_sockets(["Output"])
+        self.inputs['Input'] = NodeSocket(is_input=True, node=self, name='Input')
+        self.outputs['Output'] = NodeSocket(is_input=False, node=self, name='Output')
         self.init_parameters()
         self.set_traverse([("Input", "Output")])
-        self.node_type = 'UTILITY'
         self.prepared, self.executed = True, True
         self.execution_prepared=True
     # this node is useful for me to insert in the tree and use for debugging especially connections.
 
 class AutoGenNode(MantisNode):
+    __slots__ = ()
+    node_type = 'UTILITY'
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree)
-        self.node_type = 'UTILITY'
         self.prepared, self.executed = True, True
         self.execution_prepared=True
 
@@ -61,14 +144,16 @@ class AutoGenNode(MantisNode):
 #   going in or out of the group and connecting back out the other side
 # this is also where caching and overlays live
 class GroupInterface(MantisNode):
+    __slots__ = ("in_out")
+    node_type = 'INTERFACE'
     def __init__(self, signature, base_tree, ui_node, in_out):
         super().__init__(signature, base_tree)
-        self.node_type = 'INTERFACE'
-        self.prepared, self.executed = True, True; sockets = []
+        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: sockets.append(socket.identifier)
-        self.inputs.init_sockets(sockets); self.outputs.init_sockets(sockets)
+        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)] )
         self.execution_prepared=True

+ 127 - 174
link_nodes.py

@@ -37,16 +37,27 @@ def TellClasses():
              LinkDrivenParameter,
             ]
 
+# In order to save time and give users a little more control, we'll cache
+# the IK Pole Vector solution for a given set of bone matrices
+IK_POLE_VECTOR_CACHE = {}
+
+def make_ik_pole_vector_cache_key(start_effector, end_effector, pole_location, knee_location):
+    return frozenset({ tuple( start_effector[:]),
+                       tuple( end_effector[:]),
+                       tuple( pole_location[:]),
+                       tuple( knee_location[:]), })
+
 # set the name if it is available, otherwise just use the constraint's nice name
 set_constraint_name = lambda mantis_node : mantis_node.evaluate_input("Name") if mantis_node.evaluate_input("Name") else mantis_node.__class__.__name__
 
 
 class MantisLinkNode(MantisNode):
+    __slots__ = ()
+    node_type = 'LINK'
     def __init__(self, signature : tuple,
-                 base_tree : NodeTree,
-                 socket_templates : list[SockTemplate]=[]):
-        super().__init__(signature, base_tree, socket_templates)
-        self.node_type = 'LINK'
+                 base_tree : NodeTree,):
+        super().__init__(signature, base_tree, self.socket_templates,
+                         additional_parameters= {'Name':None})
         self.prepared = True; self.bObject=[]
 
     def bTransformPass(self, bContext=None):
@@ -68,7 +79,10 @@ class MantisLinkNode(MantisNode):
             return super().evaluate_input(input_name, index)
 
     def gen_property_socket_map(self) -> dict:
-        props_sockets = super().gen_property_socket_map()
+        if self.property_socket_map:
+            props_sockets = self.property_socket_map.copy()
+        else:
+            props_sockets = super().gen_property_socket_map()
         if (os := self.inputs.get("Owner Space")) and os.is_connected:
             node_line, _last_socket = trace_single_line(self, "Owner Space")
             for other_node in node_line:
@@ -129,6 +143,10 @@ class MantisLinkNode(MantisNode):
     def bFinalize(self, bContext=None):
         finish_drivers(self)
 
+    def fill_parameters(self, ui_node=None):
+        super().fill_parameters(ui_node)
+        self.parameters["Name"] = ui_node.label if ui_node.label else ui_node.name
+
 #*#-------------------------------#++#-------------------------------#*#
 # L I N K   N O D E S
 #*#-------------------------------#++#-------------------------------#*#
@@ -136,11 +154,9 @@ class MantisLinkNode(MantisNode):
 
 class LinkInherit(MantisLinkNode):
     '''A node representing inheritance'''
+    __slots__ = ()
+    socket_templates = LinkInheritSockets
 
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, LinkInheritSockets)
-        self.init_parameters()
-        self.set_traverse([('Parent', 'Inheritance')])
 
     def bTransformPass(self, bContext=None):
         parent_xForm_info = get_parent_xForm_info(self, 'Parent')
@@ -157,13 +173,8 @@ class LinkInherit(MantisLinkNode):
 
 class LinkCopyLocation(MantisLinkNode):
     '''A node representing Copy Location'''
-
-    def __init__(self, signature : tuple,
-                 base_tree : NodeTree,):
-        super().__init__(signature, base_tree, LinkCopyLocationSockets)
-        additional_parameters = { "Name":None }
-        self.init_parameters(additional_parameters=additional_parameters)
-        self.set_traverse([("Input Relationship", "Output Relationship")])
+    __slots__ = ()
+    socket_templates = LinkCopyLocationSockets
 
     def bRelationshipPass(self, context):
         prepare_parameters(self)
@@ -178,17 +189,13 @@ class LinkCopyLocation(MantisLinkNode):
             self.bObject.append(c)
             self.set_custom_space()
             props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, c, props_sockets)
+            evaluate_sockets(self, {"DEFAULT":c}, props_sockets)
         self.executed = True
 
 class LinkCopyRotation(MantisLinkNode):
     '''A node representing Copy Rotation'''
-
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, LinkCopyRotationSockets)
-        additional_parameters = { "Name":None }
-        self.init_parameters(additional_parameters=additional_parameters)
-        self.set_traverse([("Input Relationship", "Output Relationship")])
+    __slots__ = ()
+    socket_templates = LinkCopyRotationSockets
 
     def bRelationshipPass(self, context):
         prepare_parameters(self)
@@ -212,17 +219,13 @@ class LinkCopyRotation(MantisLinkNode):
             self.bObject.append(c)
             self.set_custom_space()
             props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, c, props_sockets)
+            evaluate_sockets(self, {"DEFAULT":c}, props_sockets)
         self.executed = True
 
 class LinkCopyScale(MantisLinkNode):
     '''A node representing Copy Scale'''
-
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, LinkCopyScaleSockets)
-        additional_parameters = { "Name":None }
-        self.init_parameters(additional_parameters=additional_parameters)
-        self.set_traverse([("Input Relationship", "Output Relationship")])
+    __slots__ = ()
+    socket_templates = LinkCopyScaleSockets
 
     def bRelationshipPass(self, context):
         prepare_parameters(self)
@@ -237,18 +240,14 @@ class LinkCopyScale(MantisLinkNode):
             self.bObject.append(c)
             self.set_custom_space()
             props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, c, props_sockets)
+            evaluate_sockets(self, {"DEFAULT":c}, props_sockets)
         self.executed = True
 
 
 class LinkCopyTransforms(MantisLinkNode):
     '''A node representing Copy Transfoms'''
-
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, LinkCopyTransformsSockets)
-        additional_parameters = { "Name":None }
-        self.init_parameters(additional_parameters=additional_parameters)
-        self.set_traverse([("Input Relationship", "Output Relationship")])
+    __slots__ = ()
+    socket_templates = LinkCopyTransformsSockets
 
     def bRelationshipPass(self, context):
         prepare_parameters(self)
@@ -263,16 +262,13 @@ class LinkCopyTransforms(MantisLinkNode):
             self.bObject.append(c)
             self.set_custom_space()
             props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, c, props_sockets)
+            evaluate_sockets(self, {"DEFAULT":c}, props_sockets)
         self.executed = True
 
 class LinkTransformation(MantisLinkNode):
     '''A node representing Copy Transfoms'''
-
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, LinkTransformationSockets)
-        self.init_parameters(additional_parameters={"Name":None })
-        self.set_traverse([("Input Relationship", "Output Relationship")])
+    __slots__ = ()
+    socket_templates = LinkTransformationSockets
 
     def ui_modify_socket(self, ui_socket, socket_name=None):
         from_suffix, to_suffix = '', ''
@@ -303,46 +299,12 @@ class LinkTransformation(MantisLinkNode):
             self.bObject.append(c)
             self.set_custom_space()
             props_sockets = self.gen_property_socket_map()
-            # we have to fix the blender-property for scale/rotation
-            # because Blender stores these separately.
-            # I do not care that this code is ugly.
-            from_suffix, to_replace = '', ''
-            if self.evaluate_input("Map From") == 'ROTATION':
-                from_suffix='_rot'
-            elif self.evaluate_input("Map From") == 'SCALE':
-                from_suffix='_scale'
-            if self.evaluate_input("Map To") == 'ROTATION':
-                to_replace='_rot'
-            elif self.evaluate_input("Map To") == 'SCALE':
-                to_replace='_scale'
-            if from_suffix:
-                for axis in ['x', 'y', 'z']:
-                    stub='from_min_'+axis
-                    props_sockets[stub+from_suffix]=props_sockets[stub]
-                    props_sockets[stub+from_suffix].property_name = stub+from_suffix
-                    del props_sockets[stub]
-                    stub='from_max_'+axis
-                    props_sockets[stub+from_suffix]=props_sockets[stub]
-                    props_sockets[stub+from_suffix].property_name = stub+from_suffix
-                    del props_sockets[stub]
-            if to_replace:
-                for axis in ['x', 'y', 'z']:
-                    stub='to_min_'+axis
-                    props_sockets[stub+to_replace]=props_sockets[stub]
-                    props_sockets[stub+from_suffix].property_name = stub+to_replace
-                    del props_sockets[stub]
-                    stub='to_max_'+axis
-                    props_sockets[stub+to_replace]=props_sockets[stub]
-                    props_sockets[stub+from_suffix].property_name = stub+to_replace
-                    del props_sockets[stub]
-            evaluate_sockets(self, c, props_sockets)
+            evaluate_sockets(self, {"DEFAULT":c}, props_sockets)
         self.executed = True
 
 class LinkLimitLocation(MantisLinkNode):
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, LinkLimitLocationScaleSockets)
-        self.init_parameters(additional_parameters={ "Name":None })
-        self.set_traverse([("Input Relationship", "Output Relationship")])
+    __slots__ = ()
+    socket_templates = LinkLimitLocationSockets
 
     def bRelationshipPass(self, context):
         prepare_parameters(self)
@@ -356,14 +318,12 @@ class LinkLimitLocation(MantisLinkNode):
             self.bObject.append(c)
             self.set_custom_space()
             props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, c, props_sockets)
+            evaluate_sockets(self, {"DEFAULT":c}, props_sockets)
         self.executed = True
 
 class LinkLimitRotation(MantisLinkNode):
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, LinkLimitRotationSockets)
-        self.init_parameters(additional_parameters={ "Name":None })
-        self.set_traverse([("Input Relationship", "Output Relationship")])
+    __slots__ = ()
+    socket_templates = LinkLimitRotationSockets
 
     def bRelationshipPass(self, context):
         prepare_parameters(self)
@@ -372,20 +332,17 @@ class LinkLimitRotation(MantisLinkNode):
             print(wrapGreen("Creating ")+wrapWhite("Limit Rotation")+
                 wrapGreen(" Constraint for bone: ") +
                 wrapOrange(xf.bGetObject().name))
-
             if constraint_name := self.evaluate_input("Name"):
                 c.name = constraint_name
             self.bObject.append(c)
             self.set_custom_space()
             props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, c, props_sockets)
+            evaluate_sockets(self, {"DEFAULT":c}, props_sockets)
         self.executed = True
 
 class LinkLimitScale(MantisLinkNode):
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, LinkLimitLocationScaleSockets)
-        self.init_parameters(additional_parameters={ "Name":None })
-        self.set_traverse([("Input Relationship", "Output Relationship")])
+    __slots__ = ()
+    socket_templates = LinkLimitScaleSockets
 
     def bRelationshipPass(self, context):
         prepare_parameters(self)
@@ -400,14 +357,12 @@ class LinkLimitScale(MantisLinkNode):
             self.bObject.append(c)
             self.set_custom_space()
             props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, c, props_sockets)
+            evaluate_sockets(self, {"DEFAULT":c}, props_sockets)
         self.executed = True
 
 class LinkLimitDistance(MantisLinkNode):
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, LinkLimitDistanceSockets)
-        self.init_parameters(additional_parameters={ "Name":None })
-        self.set_traverse([("Input Relationship", "Output Relationship")])
+    __slots__ = ()
+    socket_templates = LinkLimitDistanceSockets
 
     def bRelationshipPass(self, context):
         prepare_parameters(self)
@@ -422,16 +377,14 @@ class LinkLimitDistance(MantisLinkNode):
             self.bObject.append(c)
             self.set_custom_space()
             props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, c, props_sockets)
+            evaluate_sockets(self, {"DEFAULT":c}, props_sockets)
         self.executed = True
 
 # Tracking
 
 class LinkStretchTo(MantisLinkNode):
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, LinkStretchToSockets)
-        self.init_parameters(additional_parameters={ "Name":None })
-        self.set_traverse([("Input Relationship", "Output Relationship")])
+    __slots__ = ()
+    socket_templates = LinkStretchToSockets
 
     def bRelationshipPass(self, context):
         prepare_parameters(self)
@@ -445,7 +398,7 @@ class LinkStretchTo(MantisLinkNode):
                 c.name = constraint_name
             self.bObject.append(c)
             props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, c, props_sockets)
+            evaluate_sockets(self, {"DEFAULT":c}, props_sockets)
 
             if (self.evaluate_input("Original Length") == 0):
                 # this is meant to be set automatically.
@@ -453,11 +406,9 @@ class LinkStretchTo(MantisLinkNode):
         self.executed = True
 
 class LinkDampedTrack(MantisLinkNode):
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, LinkDampedTrackSockets)
-        self.init_parameters(additional_parameters={ "Name":None })
-        self.set_traverse([("Input Relationship", "Output Relationship")])
-
+    __slots__ = ()
+    socket_templates = LinkDampedTrackSockets
+    
     def bRelationshipPass(self, context):
         prepare_parameters(self)
         for xf in self.GetxForm():
@@ -470,14 +421,12 @@ class LinkDampedTrack(MantisLinkNode):
                 c.name = constraint_name
             self.bObject.append(c)
             props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, c, props_sockets)
+            evaluate_sockets(self, {"DEFAULT":c}, props_sockets)
         self.executed = True
 
 class LinkLockedTrack(MantisLinkNode):
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree,LinkLockedTrackSockets)
-        self.init_parameters(additional_parameters={"Name":None })
-        self.set_traverse([("Input Relationship", "Output Relationship")])
+    __slots__ = ()
+    socket_templates = LinkLockedTrackSockets
 
     def bRelationshipPass(self, context):
         prepare_parameters(self)
@@ -491,14 +440,12 @@ class LinkLockedTrack(MantisLinkNode):
                 c.name = constraint_name
             self.bObject.append(c)
             props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, c, props_sockets)
+            evaluate_sockets(self, {"DEFAULT":c}, props_sockets)
         self.executed = True
 
 class LinkTrackTo(MantisLinkNode):
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, LinkTrackToSockets)
-        self.init_parameters(additional_parameters={"Name":None })
-        self.set_traverse([("Input Relationship", "Output Relationship")])
+    __slots__ = ()
+    socket_templates = LinkTrackToSockets
 
     def bRelationshipPass(self, context):
         prepare_parameters(self)
@@ -512,14 +459,12 @@ class LinkTrackTo(MantisLinkNode):
                 c.name = constraint_name
             self.bObject.append(c)
             props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, c, props_sockets)
+            evaluate_sockets(self, {"DEFAULT":c}, props_sockets)
         self.executed = True
 
 class LinkInheritConstraint(MantisLinkNode):
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, LinkInheritConstraintSockets)
-        self.init_parameters(additional_parameters={"Name":None })
-        self.set_traverse([("Input Relationship", "Output Relationship")])
+    __slots__ = ()
+    socket_templates = LinkInheritConstraintSockets
 
     def bRelationshipPass(self, context):
         prepare_parameters(self)
@@ -534,15 +479,13 @@ class LinkInheritConstraint(MantisLinkNode):
             self.bObject.append(c)
 
             props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, c, props_sockets)
+            evaluate_sockets(self, {"DEFAULT":c}, props_sockets)
             c.set_inverse_pending
             self.executed = True
 
 class LinkInverseKinematics(MantisLinkNode):
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, LinkInverseKinematicsSockets)
-        self.init_parameters(additional_parameters={"Name":None })
-        self.set_traverse([("Input Relationship", "Output Relationship")])
+    __slots__ = ("cache_available", "cache_key")
+    socket_templates = LinkInverseKinematicsSockets
 
     def get_base_ik_bone(self, ik_bone):
         chain_length : int = (self.evaluate_input("Chain Length"))
@@ -595,6 +538,18 @@ class LinkInverseKinematics(MantisLinkNode):
 
         # this is the X-Axis of the bone's rest-pose, added to its bone
         knee_location = base_ik_bone.bone.matrix_local.col[0].xyz+start_effector
+
+        # At this point we can check the cache for the correct IK pole angle
+        cache_key = make_ik_pole_vector_cache_key(start_effector,
+                                                  end_effector,
+                                                  pole_location,
+                                                  knee_location)
+        self.cache_key.append(cache_key)
+        if pole_angle := IK_POLE_VECTOR_CACHE.get(cache_key):
+            self.cache_available.append(True)
+            return pole_angle
+        self.cache_available.append(False)
+
         ik_axis = (end_effector-start_effector).normalized()
         from .utilities.utilities import project_point_to_plane
         pole_planar_projection = project_point_to_plane(pole_location, start_effector, ik_axis)
@@ -681,6 +636,8 @@ class LinkInverseKinematics(MantisLinkNode):
             i+=1
 
     def bRelationshipPass(self, context):
+        self.cache_available = []
+        self.cache_key = []
         prepare_parameters(self)
         for xf in self.GetxForm():
             print(wrapGreen("Creating ")+wrapOrange("Inverse Kinematics")+
@@ -698,28 +655,35 @@ class LinkInverseKinematics(MantisLinkNode):
             #  a whole bunch of circular dependency crap from having infinite chain length
             if (c.pole_target):
                 self.set_pole_angle(c, self.calc_pole_angle_pre(c, ik_bone))
+            else: # no need for cache
+                self.cache_available.append(False)
+                self.cache_key.append(None)
 
             props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, c, props_sockets)
+            evaluate_sockets(self, {"DEFAULT":c}, props_sockets)
             c.use_location   = self.evaluate_input("Position") > 0
             c.use_rotation   = self.evaluate_input("Rotation") > 0
         self.executed = True
 
     def bFinalize(self, bContext = None):
-        # adding a test here
         if bContext:
             for i, constraint in enumerate(self.bObject):
+                # retrieve the cached result if it is available
                 ik_bone = self.GetxForm()[i].bGetObject(mode='POSE')
                 if constraint.pole_target:
-                    prWhite(f"Fine-tuning IK Pole Angle for {self}")
-                    # make sure to enable it first
-                    enabled_before = constraint.mute
-                    constraint.mute = False
-                    self.calc_pole_angle_post(constraint, ik_bone, bContext)
-                    constraint.mute = enabled_before
+                    if self.cache_available[i]:
+                        prWhite(f"Retrieving fine-tuned IK Pole Angle for {self} from cache.")
+                        self.set_pole_angle(constraint, IK_POLE_VECTOR_CACHE[self.cache_key[i]])
+                    else:
+                        prWhite(f"Fine-tuning IK Pole Angle for {self}")
+                        # make sure to enable it first
+                        enabled_before = constraint.mute
+                        constraint.mute = False
+                        self.calc_pole_angle_post(constraint, ik_bone, bContext)
+                        constraint.mute = enabled_before
+                        IK_POLE_VECTOR_CACHE[self.cache_key[i]] = constraint.pole_angle
         super().bFinalize(bContext)
 
-
 def ik_report_error(pb, context, do_print=False):
     dg = context.view_layer.depsgraph
     dg.update()
@@ -740,10 +704,8 @@ def ik_report_error(pb, context, do_print=False):
 # This is kinda a weird design decision?
 class LinkDrivenParameter(MantisLinkNode):
     '''A node representing an armature object'''
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, LinkDrivenParameterSockets)
-        self.init_parameters(additional_parameters={ "Name":None })
-        self.set_traverse([("Input Relationship", "Output Relationship")])
+    __slots__ = ()
+    socket_templates = LinkDrivenParameterSockets
 
     def bRelationshipPass(self, bContext = None,):
         prepare_parameters(self)
@@ -776,7 +738,7 @@ class LinkDrivenParameter(MantisLinkNode):
                                           socket_name="Value",
                                           default_value=def_value)
             }
-            evaluate_sockets(self, ob, props_sockets)
+            evaluate_sockets(self, {"DEFAULT":ob}, props_sockets)
 
         self.executed = True
 
@@ -796,12 +758,8 @@ class LinkDrivenParameter(MantisLinkNode):
 
 class LinkArmature(MantisLinkNode):
     '''A node representing an armature object'''
-
-    def __init__(self, signature, base_tree,):
-        super().__init__(signature, base_tree, LinkArmatureSockets)
-        self.init_parameters(additional_parameters={"Name":None })
-        self.set_traverse([("Input Relationship", "Output Relationship")])
-        setup_custom_property_inputs_outputs(self) # <-- this takes care of the runtime-added sockets
+    __slots__ = ()
+    socket_templates = LinkArmatureSockets
 
     def bRelationshipPass(self, bContext = None,):
         prepare_parameters(self)
@@ -810,6 +768,7 @@ class LinkArmature(MantisLinkNode):
                 wrapGreen(" Constraint for bone: ") +
                 wrapOrange(xf.bGetObject().name))
             c = xf.bGetObject().constraints.new('ARMATURE')
+            b_objects = {"DEFAULT":c}
             if constraint_name := self.evaluate_input("Name"):
                 c.name = constraint_name
             self.bObject.append(c)
@@ -834,25 +793,26 @@ class LinkArmature(MantisLinkNode):
                 default_value = 1.0
                 if self.inputs['Weights'].links:
                     default_value = 1/len(self.inputs['Weights'].links)
+                prop_name = 'weight'
+                # prop_name = f"targets[{i}].weight"
                 filler = MantisSocketFiller(
-                    property_name= (prop_name := "targets[%d].weight" % i),
-                    socket_name="Weights",
-                    default_value=default_value,
+                    property_group = f"target.{str(i).zfill(4)}",
+                    property_name = prop_name,
+                    socket_name = "Weights",
                     array_index=i,
+                    default_value=default_value,
                 )
-                props_sockets[prop_name] = filler
-            evaluate_sockets(self, c, props_sockets)
+                b_objects[f"target.{str(i).zfill(4)}"] = target
+                props_sockets[prop_name+str(i)] = filler
+            evaluate_sockets(self, b_objects, props_sockets)
             for target, value in targets_weights.items():
                 c.targets[target].weight=value
         self.executed = True
 
 class LinkSplineIK(MantisLinkNode):
     '''A node representing an armature object'''
-
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, LinkSplineIKSockets)
-        self.init_parameters(additional_parameters={"Name":None })
-        self.set_traverse([("Input Relationship", "Output Relationship")])
+    __slots__ = ()
+    socket_templates = LinkSplineIKSockets
 
     def bRelationshipPass(self, bContext = None,):
         prepare_parameters(self)
@@ -880,17 +840,14 @@ class LinkSplineIK(MantisLinkNode):
                 c.name = constraint_name
             self.bObject.append(c)
             props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, c, props_sockets)
+            evaluate_sockets(self, {"DEFAULT":c}, props_sockets)
         self.executed = True
 
 
 class LinkFloor(MantisLinkNode):
     '''A node representing an armature object'''
-
-    def __init__(self, signature, base_tree,):
-        super().__init__(signature, base_tree, LinkFloorSockets)
-        self.init_parameters(additional_parameters={"Name":None })
-        self.set_traverse([("Input Relationship", "Output Relationship")])
+    __slots__ = ()
+    socket_templates = LinkFloorSockets
 
     def bRelationshipPass(self, bContext = None,):
         prepare_parameters(self)
@@ -905,15 +862,13 @@ class LinkFloor(MantisLinkNode):
             self.bObject.append(c)
             self.set_custom_space()
             props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, c, props_sockets)
+            evaluate_sockets(self, {"DEFAULT":c}, props_sockets)
         self.executed = True
 
 class LinkShrinkWrap(MantisLinkNode):
     '''A node representing a shrinkwrap relationship.'''
-    def __init__(self, signature, base_tree,):
-        super().__init__(signature, base_tree, LinkShrinkWrapSockets)
-        self.init_parameters(additional_parameters={"Name":None })
-        self.set_traverse([("Input Relationship", "Output Relationship")])
+    __slots__ = ()
+    socket_templates = LinkShrinkWrapSockets
 
     def bRelationshipPass(self, bContext = None,):
         prepare_parameters(self)
@@ -927,17 +882,15 @@ class LinkShrinkWrap(MantisLinkNode):
             if constraint_name := self.evaluate_input("Name"):
                 c.name = constraint_name
             props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, c, props_sockets)
+            evaluate_sockets(self, {"DEFAULT":c}, props_sockets)
             # self.set_custom_space() # this needs to be overridden for me TODO
         self.executed = True
 
 
 class LinkGeometryAttribute(MantisLinkNode):
     '''A node representing a geometry attribute relationship.'''
-    def __init__(self, signature, base_tree,):
-        super().__init__(signature, base_tree, LinkGeometryAttributeSockets)
-        self.init_parameters(additional_parameters={"Name":None })
-        self.set_traverse([("Input Relationship", "Output Relationship")])
+    __slots__ = ()
+    socket_templates = LinkGeometryAttributeSockets
 
     def bRelationshipPass(self, bContext = None,):
         from bpy.app import version
@@ -953,5 +906,5 @@ class LinkGeometryAttribute(MantisLinkNode):
             if constraint_name := self.evaluate_input("Name"):
                 c.name = constraint_name
             props_sockets = self.gen_property_socket_map()
-            evaluate_sockets(self, c, props_sockets)
+            evaluate_sockets(self, {"DEFAULT":c}, props_sockets)
         self.executed = True

+ 2 - 2
link_nodes_ui.py

@@ -183,7 +183,7 @@ class LinkLimitLocationNode(Node, LinkNode):
     bl_icon = 'CON_LOCLIMIT'
     mantis_node_class_name=bl_idname
     category_color=linkTransformColor
-    socket_templates = LinkLimitLocationScaleSockets
+    socket_templates = LinkLimitLocationSockets
 
 class LinkLimitScaleNode(Node, LinkNode):
     '''A node representing Limit Scale'''
@@ -192,7 +192,7 @@ class LinkLimitScaleNode(Node, LinkNode):
     bl_icon = 'CON_SIZELIMIT'
     mantis_node_class_name=bl_idname
     category_color=linkTransformColor
-    socket_templates = LinkLimitLocationScaleSockets
+    socket_templates = LinkLimitScaleSockets
 
 class LinkLimitRotationNode(Node, LinkNode):
     '''A node representing Limit Rotation'''

+ 18 - 10
link_socket_templates.py

@@ -6,7 +6,8 @@ from .misc_nodes_socket_templates import SplineIndexTemplate
 # Socket Templates we will reuse:
 # inputs:
 InputRelationshipTemplate : SockTemplate = SockTemplate(
-    name="Input Relationship", is_input=True,  bl_idname='RelationshipSocket', )
+    name="Input Relationship", is_input=True,  bl_idname='RelationshipSocket',
+    traverse_target='Output Relationship')
 TargetTemplate : SockTemplate = SockTemplate(
     name="Target", is_input=True,  bl_idname='xFormSocket', )
 Head_Tail_Template : SockTemplate = SockTemplate(
@@ -32,7 +33,8 @@ InfluenceTemplate : SockTemplate = SockTemplate(
     default_value=1.0, blender_property='influence')
 EnableTemplate : SockTemplate = SockTemplate(
     name="Enable", is_input=True,  bl_idname='EnableSocket',
-    default_value=True, blender_property='mute')
+    default_value=True, blender_property='mute',
+    property_value_operator='invert_cast_to_bool')
 OffsetTemplate : SockTemplate = SockTemplate(
     name="Offset", bl_idname='BooleanSocket', is_input=True,
     default_value=False, blender_property='use_offset')
@@ -78,7 +80,8 @@ TrackAxisTemplate= SockTemplate(name="Track Axis", bl_idname="EnumTrackAxis",
 
 # outputs:
 OutputRelationshipTemplate : SockTemplate = SockTemplate(
-    name="Output Relationship", is_input=False,  bl_idname='RelationshipSocket', )
+    name="Output Relationship", is_input=False,  bl_idname='RelationshipSocket',
+    traverse_target='Input Relationship')
 
 
 LinkInheritSockets = [
@@ -88,8 +91,10 @@ LinkInheritSockets = [
                  bl_idname='EnumInheritScale',    default_value="FULL",),
     SockTemplate(name="Connected",        is_input=True,
                  bl_idname='BooleanSocket',       default_value=False,),
-    SockTemplate(name="Parent",           is_input=True,   bl_idname='xFormSocket',),
-    SockTemplate(name="Inheritance",      is_input=False,  bl_idname='RelationshipSocket',),
+    SockTemplate(name="Parent",           is_input=True,   bl_idname='xFormSocket',
+                 traverse_target='Inheritance'),
+    SockTemplate(name="Inheritance",      is_input=False,  bl_idname='RelationshipSocket',
+                 traverse_target='Parent'),
 ]
 
 
@@ -160,7 +165,8 @@ TransformationMinMaxTemplateGenerator = lambda name, bprop  : SockTemplate(
     name=name, is_input=True,
     bl_idname='EnumTransformationAxes' if "Source" in name else "FloatSocket",
     default_value='X' if "Source" in name else 1.0,
-    blender_property=bprop)
+    blender_property= bprop if "Source" in name else ( bprop, bprop+"_rot", bprop+"_scale") ,
+    )
 
 LinkTransformationSockets = [
     InputRelationshipTemplate,
@@ -206,7 +212,7 @@ LinkTransformationSockets = [
     OutputRelationshipTemplate,
 ]
 
-LinkLimitLocationScaleSockets = [
+LinkLimitLocationSockets = [
     InputRelationshipTemplate,
     *LimitAxesSocketTemplates, # we generated these ahead of time in a list
     AffectTransformTemplate,
@@ -226,7 +232,7 @@ LinkLimitRotationSockets = [
     OutputRelationshipTemplate,
 ]
 
-LinkLimitLocationScaleSockets = [
+LinkLimitScaleSockets = [
     InputRelationshipTemplate,
     *LimitAxesSocketTemplates, # we generated these ahead of time in a list
     AffectTransformTemplate,
@@ -380,8 +386,10 @@ LinkArmatureSockets=[
                  default_value=False, blender_property='use_bone_envelopes'),
     SockTemplate(name="Use Current Location", bl_idname='BooleanSocket', is_input=True,
                  default_value=False, blender_property='use_current_location'),
-    TargetsArray := replace(TargetTemplate, name='Targets', use_multi_input=True),
-    WeightsArray := replace(InfluenceTemplate, name='Weights', use_multi_input=True),
+    TargetsArray := replace(TargetTemplate, name='Targets', use_multi_input=True,
+                            blender_property=None), # the node sets this up
+    WeightsArray := replace(InfluenceTemplate, name='Weights', use_multi_input=True,
+                            blender_property=None), # the node sets this up
     InfluenceTemplate,
     EnableTemplate,
     OutputRelationshipTemplate,

+ 24 - 15
mantis_dataclasses.py

@@ -1,11 +1,11 @@
 from dataclasses import dataclass, field
-from typing import List, Any, Tuple, Optional
+from typing import List, Any, Tuple, Optional, Callable
 
-@dataclass
+@dataclass(slots=True)
 class MantisSocketTemplate():
     name             : str = field(default="")
     bl_idname        : str = field(default="")
-    traverse_target  : str = field(default="")
+    traverse_target  : Optional[str] = field(default=None)
     identifier       : str = field(default="")
     display_shape    : str = field(default="") # for arrays
     category         : str = field(default="") # for use in display update
@@ -15,6 +15,9 @@ class MantisSocketTemplate():
     use_multi_input  : bool = field(default=False)
     ui_only          : bool = field(default=False)
     default_value    : Any = field(default=None)
+    # The data operator is used to transform the Mantis data to a format Blender can use.
+    # For example, type-casting when Blender's C/C++ is more type-sensitve than Python
+    property_value_operator : Optional[Callable] = field (default=None)
     # this is a hint for the props_sockets system for complex situations
     #  where multiple blender objects own the properties, like e.g. bone/posebone
     prop_owner_group : str = field(default="")
@@ -23,7 +26,7 @@ class MantisSocketTemplate():
     min_bpy_version  : Optional[tuple[int]] = field(default=None)
     max_bpy_version  : Optional[tuple[int]] = field(default=None)
 
-@dataclass
+@dataclass(slots=True)
 class xForm_info():
     object_type          :  str = field(default="")
     root_armature        :  str = field(default="")
@@ -33,7 +36,7 @@ class xForm_info():
     self_edit_name      :  str = field(default="")
 # should I add node signatures to this?
 
-@dataclass
+@dataclass(slots=True)
 class custom_prop_template():
     name      : str = field(default="")
     prop_type : str = field(default="")
@@ -48,7 +51,7 @@ class custom_prop_template():
     soft_max : float|int = field(default=1)
     description : str = field(default="")
 
-@dataclass
+@dataclass(slots=True)
 class MantisBoneTransform():
     matrix    : Any = field(default=[1,0,0,0,
                                      0,1,0,0,
@@ -59,7 +62,7 @@ class MantisBoneTransform():
     matrix_space  : str = field(default="OBJECT")
     length    : float = field(default=1.0),
 
-@dataclass
+@dataclass(slots=True)
 class MantisBBoneTransform():
     num_bbone_segmetns: int = field(default=1)
     # Offsets & Curve Handles
@@ -84,19 +87,25 @@ class MantisBBoneTransform():
     bbone_custom_handle_end: str = field(default=None)
 
 # used to apply values or drivers to blender properties in a generic way
-@dataclass
+@dataclass(slots=True)
 class MantisSocketFiller():
     socket_name: str = field(default="")
+    property_group : str = field(default="")
     property_name : str = field(default="")
     default_value: Any = field(default=None)
     array_index: int = field(default=0) # for multi-input array sockets
     value_index: Optional[int] = field(default=None) # for vector properties
     # A Combo Property is a property that jams a bunch of blender properties into one socket
-    combo_property: bool = field(default=False)
+    combo_property: bool = field(default=False) # i.e. it provides a vector of Blender Properties
+    vector_property : bool = field(default=False)
+    is_animatable : bool = field(default=True) # for catching bad drivers
+    # we pack a function for transforming the data if needed.
+    property_value_operator: Optional[Callable] = field(default=None)
 
-    def __repr__(self):
-        return f"socket_name: {self.socket_name}, " \
-               f"property_name: {self.property_name}, " \
-               f"default_value: {self.default_value}, " \
-               f"array_index: {self.array_index or 0}, " \
-               f"value_index: {self.value_index or None}" 
+
+@dataclass(slots=True)
+class RerouteCacheItem:
+    reroute_name      : str = field(default="") # the reroute node name in the tree 
+    reroute_tree_name : str = field(default="") # the tree to find the reroute in
+    source            : Tuple[str, str] = field(default_factory=tuple) # the beginning of the reroute network
+    destinations      : set[Tuple[str, str]] = field(default_factory=set) # (node_name, socket_name)

+ 12 - 18
math_nodes.py

@@ -51,12 +51,10 @@ def math_operation(operation, a, b):
 # M A T H  N O D E S
 #*#-------------------------------#++#-------------------------------#*#
 class MathStaticInt(MantisNode):
-    '''A node representing an armature object'''
-
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, StaticIntSockets)
-        self.init_parameters()
-        self.node_type = "UTILITY"
+    '''A node representing an integer math operation'''
+    __slots__ = ()
+    socket_templates = StaticIntSockets
+    node_type = "UTILITY"
 
     def bPrepare(self, bContext = None,):
         a = self.evaluate_input("Int A"); b = self.evaluate_input("Int B")
@@ -65,12 +63,10 @@ class MathStaticInt(MantisNode):
         self.prepared, self.executed = True, True
 
 class MathStaticFloat(MantisNode):
-    '''A node representing an armature object'''
-
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, StaticFloatSockets)
-        self.init_parameters()
-        self.node_type = "UTILITY"
+    '''A node representing a float math operation'''
+    __slots__ = ()
+    socket_templates = StaticFloatSockets
+    node_type = "UTILITY"
 
     def bPrepare(self, bContext = None,):
         a = self.evaluate_input("Float A"); b = self.evaluate_input("Float B")
@@ -79,12 +75,10 @@ class MathStaticFloat(MantisNode):
         self.prepared, self.executed = True, True
 
 class MathStaticVector(MantisNode):
-    '''A node representing an armature object'''
-
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, StaticVectorSockets)
-        self.init_parameters()
-        self.node_type = "UTILITY"
+    '''A node representing a vector math operation'''
+    __slots__ = ()
+    socket_templates = StaticVectorSockets
+    node_type = "UTILITY"
 
     def bPrepare(self, bContext = None,):
         from mathutils import Vector

+ 1 - 1
menu_classes.py

@@ -119,7 +119,7 @@ class MantisActiveTreePanel(Panel):
         spc = area.spaces[0]
         nt = spc.node_tree
         layout = self.layout
-        layout.label(text=f"Tree Hash: {nt.hash}")
+        layout.label(text=f"Tree Hash: {nt.hash_recursive}")
         layout.prop(nt, "do_live_update", text='Live Updates',)
         layout.operator("mantis.invalidate_node_tree")
         layout.operator("mantis.execute_node_tree")

Dosya farkı çok büyük olduğundan ihmal edildi
+ 257 - 293
misc_nodes.py


+ 14 - 5
misc_nodes_socket_templates.py

@@ -58,7 +58,8 @@ MatrixFromCurveSockets=[
         bl_idname='UnsignedIntSocket', is_input=True, default_value=0,),
     MatrixIndexTemplate := ST(name="Matrix Index", category = "divisions",
         bl_idname='UnsignedIntSocket', is_input=True, default_value=0,),
-    replace(FactorTemplate, category="factor"),
+    MatrixFromCurveFactor := replace(FactorTemplate, category="factor"),
+    MatrixFromCurveTailFactor := replace(FactorTemplate, name='Factor (Tail)', category="factor_tail"),
     MatrixOutput,
 ]
 
@@ -372,14 +373,22 @@ MatrixInvertSockets=[
 ]
 
 MatrixComposeSockets=[
+    MatrixComposeMode := ST(name='Mode', bl_idname='EnumComposeMatrixModes',
+        category='always_show', is_input=True,),
+    ComposeMatrixRotation := ST( name="Euler Rotation", is_input=True,  
+        bl_idname='VectorEulerSocket', hide = True,
+        default_value = (0.0, 0.0, 0.0), category="transform" ),
     XBasisVector := ST(
     name="X Basis Vector", is_input=True,  bl_idname='VectorSocket', 
-        default_value = (1.0, 0.0, 0.0) ),
+        default_value = (1.0, 0.0, 0.0), category='axes', ),
     YBasisVector := replace(XBasisVector, name="Y Basis Vector",
-        default_value = (0.0, 1.0, 0.0) ),
+        default_value = (0.0, 1.0, 0.0), category='axes',),
     ZBasisVector := replace(XBasisVector, name="Z Basis Vector",
-        default_value = (0.0, 0.0, 1.0) ),
-    Translation := replace(XBasisVector, name="Translation"),
+        default_value = (0.0, 0.0, 1.0), category='axes', ),
+    Translation := replace(XBasisVector, name="Translation", category='always_show'),
+    ComposeMatrixBoneLength := ST( name="Bone Length", is_input=True,
+        bl_idname='FloatPositiveSocket', 
+        default_value = 1.0, category="always_show" ),
     MatrixOutput,
 ]
 

+ 45 - 9
misc_nodes_ui.py

@@ -234,14 +234,27 @@ class UtilityMatrixFromCurve(Node, MantisMiscNode):
             mode = self.inputs['Mode'].default_value
         # We just hide/show based on the selected mode.
         for template in self.__class__.socket_templates:
-            if mode == 'DIVISIONS' and template.category == 'factor':
-                self.inputs[template.name].hide=True
-            if mode == 'DIVISIONS' and template.category == 'divisions':
-                self.inputs[template.name].hide=False
-            elif mode == 'FACTOR' and template.category == 'divisions':
-                self.inputs[template.name].hide=True
-            elif mode == 'FACTOR' and template.category == 'factor':
-                self.inputs[template.name].hide=False
+            if mode == 'DIVISIONS':
+                if template.category == 'factor':
+                    self.inputs[template.name].hide=True
+                elif template.category == 'divisions':
+                    self.inputs[template.name].hide=False
+                elif template.category == 'factor_tail':
+                    self.inputs[template.name].hide=True
+            elif mode == 'FACTOR':
+                if template.category == 'factor':
+                    self.inputs[template.name].hide=False
+                elif template.category == 'divisions':
+                    self.inputs[template.name].hide=True
+                elif template.category == 'factor_tail':
+                    self.inputs[template.name].hide=True
+            elif mode == 'FACTOR_WITH_TAIL':
+                if template.category == 'factor':
+                    self.inputs[template.name].hide=False
+                elif template.category == 'divisions':
+                    self.inputs[template.name].hide=True
+                elif template.category == 'factor_tail':
+                    self.inputs[template.name].hide=False
 
 class UtilityPointFromCurve(Node, MantisMiscNode):
     """Gets a point from a curve."""
@@ -731,10 +744,33 @@ class UtilityMatrixInvert(Node, MantisMiscNode):
 class UtilityMatrixCompose(Node, MantisMiscNode):
     """Inverts an invertable matrix, otherwise throws an error."""
     bl_idname = "UtilityMatrixCompose"
-    bl_label = "Compose Matrix"
+    bl_label = "Compose Bone Matrix"
     mantis_node_class_name=bl_idname
     socket_templates=MatrixComposeSockets
 
+    def display_update(self, parsed_tree, context):
+        # first we'll checc if the mode is connected to a noodle, should be rare
+        mode = 'BASIS'
+        if self.inputs['Mode'].is_linked:
+            if mantis_node := parsed_tree.get(get_signature_from_edited_tree(self, context)):
+                mode = mantis_node.evaluate_input("Mode")
+        else: # We can use it directly, this is a little faster.
+            mode = self.inputs['Mode'].default_value
+        # We just hide/show based on the selected mode.
+        for template in self.__class__.socket_templates:
+            if mode == 'BASIS':
+                if template.category == 'transform':
+                    self.inputs[template.name].hide=True
+                elif template.category == 'axes':
+                    self.inputs[template.name].hide=False
+            elif mode == 'CHANNELS':
+                if template.category == 'transform':
+                    self.inputs[template.name].hide=False
+                elif template.category == 'axes':
+                    self.inputs[template.name].hide=True
+            if template.category == 'always_show':
+                self.inputs[template.name].hide=False
+
 class UtilityMatrixAlignRoll(Node, MantisMiscNode):
     """Aligns the Z axis of a matrix to point in a direction."""
     bl_idname = "UtilityMatrixAlignRoll"

+ 29 - 6
ops_nodegroup.py

@@ -27,7 +27,7 @@ def tag_mantis_node_for_update(context, node):
         mantis_node = node_tree.parsed_tree.get(
                     get_signature_from_edited_tree(node, context))
         if mantis_node is None:
-            node_tree.hash = ''
+            node_tree.hash_recursive = ''
         else:
             mantis_node.reset_execution_recursive() # tag this as a change.
 #########################################################################3
@@ -256,7 +256,7 @@ class InvalidateNodeTree(Operator):
     def execute(self, context):
         print("Clearing Active Node Tree cache")
         tree=context.space_data.path[0].node_tree
-        tree.execution_id=''; tree.hash=''
+        tree.execution_id=''; tree.hash_recursive=''
         return {"FINISHED"}
 
 class ExecuteNodeTree(Operator):
@@ -287,19 +287,42 @@ class ExecuteNodeTree(Operator):
         if do_profile:
             import pstats, io
             from pstats import SortKey
+            sortby = SortKey.TIME
+            if 'cum' in environ.get("DOPROFILE").lower():
+                sortby = SortKey.CUMULATIVE
+            if "calls" in environ.get("DOPROFILE").lower():
+                sortby = SortKey.CALLS
+            profile_function = environ.get("PROFILE_FUNCTION")
+            printme1 = ''
             with cProfile.Profile() as pr:
                 tree.update_tree(context, error_popups = pass_error)
+                # from the Python docs at https://docs.python.org/3/library/profile.html#module-cProfile
+                s1 = io.StringIO()
+                ps1 = pstats.Stats(pr, stream=s1).strip_dirs().sort_stats(sortby)
+                ps1.print_stats(30) # print the top 20
+                if profile_function:
+                    ps1.print_callers(profile_function)
+                    ps1.print_callees(profile_function)
+                printme1 += s1.getvalue()
+                printme1 += f"Total profiled time: {ps1.total_tt:.6f} seconds"
+            with cProfile.Profile() as pr:
                 tree.execute_tree(context, error_popups = pass_error, profile=True)
                 # from the Python docs at https://docs.python.org/3/library/profile.html#module-cProfile
                 s = io.StringIO()
-                sortby = SortKey.TIME
-                # sortby = SortKey.CUMULATIVE
                 ps = pstats.Stats(pr, stream=s).strip_dirs().sort_stats(sortby)
-                ps.print_stats(20) # print the top 20
+                ps.print_stats(30) # print the top 20
+                if profile_function:
+                    ps.print_callers(profile_function)
+                    ps.print_callees(profile_function)
+                print (printme1)
                 print(s.getvalue())
+                print(f"Total profiled time: {ps.total_tt:.6f} seconds")
+
 
         else:
-            tree.update_tree(context, error_popups = pass_error)
+            update_failed = tree.update_tree(context, error_popups = pass_error)
+            if update_failed: # failed. Cannot execute.
+                return {"FINISHED"}
             tree.execute_tree(context, error_popups = pass_error)
         prGreen("Finished executing tree in %f seconds" % (time() - start_time))
         return {"FINISHED"}

+ 15 - 23
primitives_nodes.py

@@ -1,5 +1,6 @@
 from .utilities.node_common import *
 from .base_definitions import MantisNode, NodeSocket
+from .primitives_sockets import *
 
 def TellClasses():
     return [
@@ -13,10 +14,13 @@ def TellClasses():
 #*#-------------------------------#++#-------------------------------#*#
 
 class PrimitiveNode(MantisNode):
-
-    def __init__(self, signature, base_tree, socket_templates=[]):
-        super().__init__(signature, base_tree, socket_templates)
-        self.node_type = "UTILITY"
+    __slots__ = ()
+    node_type = "UTILITY"
+    
+    def __init__(self, signature, base_tree, socket_templates=[],
+                 additional_parameters = {}):
+        super().__init__(signature, base_tree, socket_templates,
+                         additional_parameters)
         self.prepared = True
     
     def reset_execution(self):
@@ -25,21 +29,8 @@ class PrimitiveNode(MantisNode):
 
 class CirclePrimitive(PrimitiveNode):
     '''A node representing a Circle Primitive mesh'''
-
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree)
-        inputs = [
-          "Name",
-          "Radius",
-          "Number of Points",
-        ]
-        outputs = [
-          "Circle",
-        ]
-        additional_parameters = {}
-        self.inputs.init_sockets(inputs)
-        self.outputs.init_sockets(outputs)
-        self.init_parameters(additional_parameters=additional_parameters)
+    __slots__ = ()
+    socket_templates = CirclePrimitiveSockets
 
     # mode is unused but needed for interface consistency
     def bGetObject(self, mode=''):
@@ -76,10 +67,11 @@ class CirclePrimitive(PrimitiveNode):
 from .primitives_sockets import LatticeSockets
 class GeometryLattice(PrimitiveNode):
     '''A node representing a Circle Primitive mesh'''
+    __slots__ = ()
+    socket_templates = LatticeSockets
 
     def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, LatticeSockets)
-        self.init_parameters(additional_parameters= {})
+        super().__init__(signature, base_tree, self.socket_templates)
         self.prepared = False
 
     def reset_execution(self):
@@ -99,6 +91,6 @@ class GeometryLattice(PrimitiveNode):
         import bpy
         if not data:
             data = bpy.data.lattices.new( self.evaluate_input("Name") )
-        props_sockets = self.gen_property_socket_map()
-        evaluate_sockets(self, data, props_sockets)
+        props_sockets = self.property_socket_map
+        evaluate_sockets(self, {"DEFAULT":data}, props_sockets)
         self.prepared = True; self.executed = True

+ 24 - 16
schema_nodes.py

@@ -29,14 +29,14 @@ def schema_init_sockets(mantis_node, is_input = True, in_out='INPUT', category='
             if item.item_type == 'PANEL': continue
             if item.parent and read_schema_type(item) == category:
                 if item.in_out == in_out:
-                    sockets.init_sockets([item.name])
-    mantis_node.init_parameters()
+                    sockets[item.name] = NodeSocket(is_input=is_input, name=item.name, node=mantis_node)
 
 
 class SchemaNode(MantisNode):
+    __slots__ = ()
+    node_type = 'SCHEMA'
     def __init__(self, signature, base_tree, socket_templates=[]):
         super().__init__(signature, base_tree, socket_templates)
-        self.node_type = 'SCHEMA'
         self.prepared = True; self.executed = True
         self.execution_prepared = True # should not affect execution!
 
@@ -48,32 +48,41 @@ class SchemaNode(MantisNode):
 
 
 class SchemaIndex(SchemaNode):
-    def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, SchemaIndexSockets)
-        self.init_parameters()
+    __slots__ = ()
+    socket_templates = SchemaIndexSockets
 
 class SchemaArrayInput(SchemaNode):
+    __slots__ = ()
+    socket_templates = None
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree)
         schema_init_sockets(self, is_input=False, in_out='INPUT', category='Array')
 
 
 class SchemaArrayInputGet(SchemaNode):
+    __slots__ = ()
+    socket_templates = None
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree, SchemaArrayInputGetSockets)
         schema_init_sockets(self, is_input=False, in_out='INPUT', category='Array')
 
 class SchemaArrayInputAll(SchemaNode):
+    __slots__ = ()
+    socket_templates = None
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree)
         schema_init_sockets(self, is_input=False, in_out='INPUT', category='Array')
 
 class SchemaArrayOutput(SchemaNode):
+    __slots__ = ()
+    socket_templates = None
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree)
         schema_init_sockets(self, is_input=True, in_out='OUTPUT', category='Array')
 
 class SchemaConstInput(SchemaNode):
+    __slots__ = ()
+    socket_templates = None
     def __init__(self, signature, base_tree, parent_schema_node=None):
         super().__init__(signature, base_tree)
         if parent_schema_node:
@@ -84,30 +93,26 @@ class SchemaConstInput(SchemaNode):
             for item in parent_tree.interface.items_tree:
                 if item.item_type == 'PANEL': continue
                 if item.in_out == 'INPUT':
-                    sockets.init_sockets([item.name])
-            self.init_parameters()
-            
+                    sockets[item.name] = NodeSocket(is_input=False, node=self, name=item.name)
         else:
             schema_init_sockets(self, is_input=False, in_out='INPUT', category='Constant')
 
 
 class SchemaConstOutput(SchemaNode):
+    __slots__ = ()
+    socket_templates = None
     def __init__(self, signature, base_tree, parent_schema_node=None):
         super().__init__(signature, base_tree)
-        inputs = [
-            "Expose at Index",
-        ]
-        self.inputs.init_sockets(inputs)
+        sockets=self.inputs
+        sockets["Expose at Index"] = NodeSocket(is_input=False, node=self, name="Expose at Index")
         if parent_schema_node:
             # this allows us to generate the Constant Input from a normal Node Group
             # and treat the node group as a schema
             parent_tree = parent_schema_node.ui_node.node_tree
-            sockets=self.inputs
             for item in parent_tree.interface.items_tree:
                 if item.item_type == 'PANEL': continue
                 if item.in_out == 'OUTPUT':
-                    sockets.init_sockets([item.name, "Expose at Index",])
-            self.init_parameters()
+                    sockets[item.name] = NodeSocket(is_input=False, node=self, name=item.name)
             self.parameters['Expose at Index"']=1
 
         else:
@@ -116,12 +121,15 @@ class SchemaConstOutput(SchemaNode):
 
         
 class SchemaOutgoingConnection(SchemaNode):
+    __slots__ = ()
+    socket_templates = None
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree)
         schema_init_sockets(self, is_input=True, in_out='INPUT', category='Connection')
 
         
 class SchemaIncomingConnection(SchemaNode):
+    __slots__ = ()
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree)
         schema_init_sockets(self, is_input=False, in_out='OUTPUT', category='Connection')

+ 8 - 23
socket_definitions.py

@@ -3,14 +3,13 @@ from bpy.types import NodeSocket, NodeSocketStandard
 
 # Classes which do not have default_value
 # needed to detect when there is an error updating dynamic nodes
-no_default_value= [
+no_default_value= frozenset({
             'MantisSocket',
             'RelationshipSocket',
             'DeformerSocket',
             'xFormSocket',
             'GeometrySocket',
             'GenericRotationSocket',
-            'EnumCustomPropTypeSocket', 
             'CustomPropSocket',
             'FCurveSocket',
             'DriverSocket',
@@ -19,7 +18,7 @@ no_default_value= [
             'MorphTargetSocket',
             'KeyframeSocket',
             'WildcardSocket',
-]
+})
 # the sockets that do not have this field do not transfer data.
 # instead, it is the link itself which is meaningful.
 from bpy.app import version as bpy_version
@@ -90,7 +89,10 @@ enumRotationOrder =(('AUTO', 'Auto', 'Auto'),
 eMatrixFromCurveModes = (('DIVISIONS', "Divisions", "Get a matrix from the curve automattically "
                                                     "based on dividing it into pieces."),
                          ('FACTOR', "Spline Factor", "Get a matrix from a specific factor"
-                                                     " along the spline."))
+                                                     " along the spline."),
+                         ('FACTOR_WITH_TAIL', "Spline Factor (With Tail)",
+                          "Get a matrix from a specific factor along the spline, "
+                          "and place the tail on the curve at a specific factor. "))
 
 eMatrixComposeModes = (('BASIS', "Basis Vectors", "Compose a matrix from basis vectors. "
                                                   "Mantis will not automatically orthonormalize them."),
@@ -732,19 +734,6 @@ class MatrixSocket(MantisSocket):
                                 mat[1][0], mat[1][1], mat[1][2], mat[1][3],
                                 mat[2][0], mat[2][1], mat[2][2], mat[2][3],
                                 mat[3][0], mat[3][1], mat[3][2], mat[3][3], )
-    def TellValue(self):
-        from mathutils import Matrix
-        v = self.default_value
-        return Matrix( ( ( v[ 0], v[ 1], v[ 2], v[ 3],),
-                         ( v[ 4], v[ 5], v[ 6], v[ 7],),
-                         ( v[ 8], v[ 9], v[10], v[11],),
-                         ( v[12], v[13], v[14], v[15]), ) )
-                         #NOTE, we're not using the last row
-                         # so we're gonna use it to store data
-                         # unused, unused, unused, bone_length
-                         # but we're not going to make it
-                         # available except by accessor functions
-    # would like to make this stuff easier to deal with tho
     def TellBoneLength(self):
         return self.default_value[15]
 
@@ -900,8 +889,6 @@ class BooleanThreeTupleSocket(MantisSocket):
     color : bpy.props.FloatVectorProperty(default=cBool3, size=4)
     input : bpy.props.BoolProperty(default =False,)
     is_valid_interface_type=True
-    def TellValue(self):
-        return (self.default_value[0], self.default_value[1], self.default_value[2])
 
 class RotationOrderSocket(MantisSocket):
     '''Custom node socket type'''
@@ -1177,8 +1164,6 @@ class EnumCustomPropTypeSocket(MantisSocket):
     color : bpy.props.FloatVectorProperty(default=cString, size=4)
     input : bpy.props.BoolProperty(default =False,)
 
-# what is this one again?
-
 class CustomPropSocket(MantisSocket):
     '''Custom Property'''
     bl_idname = 'CustomPropSocket'
@@ -2894,7 +2879,7 @@ class EnumMatrixFromCurveMode(MantisSocket):
     """Matrix From Curve Mode"""
     bl_idname = 'EnumMatrixFromCurveMode'
     bl_label = "Mode"
-
+    enum_display_type='DROPDOWN'
     default_value :bpy.props.EnumProperty(
         name="",
         description="Mode",
@@ -2911,7 +2896,7 @@ class EnumComposeMatrixModes(MantisSocket):
     """Compose Matrix Mode"""
     bl_idname = 'EnumComposeMatrixModes'
     bl_label = "Mode"
-
+    enum_display_type='DROPDOWN'
     default_value :bpy.props.EnumProperty(
         name="",
         description="Mode",

+ 11 - 10
timers.py

@@ -97,7 +97,10 @@ def timer_update_selection_sync():
                 if len(tree_path[-1].nodes) == 0:
                     return SELECTION_POLL_RATE # empty graph
                 parsed_tree = root_tree.parsed_tree
-                active_ui_signature = (get_signature_from_space(tree_path[-1].nodes.active, space))
+                if tree_path[-1].nodes.active is not None:
+                    active_ui_signature = (get_signature_from_space(tree_path[-1].nodes.active, space))
+                else:
+                    active_ui_signature = None
                 for n in tree_path[-1].nodes:
                     if n.bl_idname not in WATCH_FOR_SELECTION: continue
                     selection_state[ui_signature := get_signature_from_space(n, space)] = n.select
@@ -116,20 +119,18 @@ def timer_update_selection_sync():
                     active_ui_signature, view_layer,
                     selection_state, deselect)
             except Exception as e:
+                prRed ("Error in Selection Sync")
                 prRed (e)
                 return SELECTION_POLL_RATE
     except Exception as e:
-        print ("Error in Selection Sync")
-        print (e)
-        raise e
+        from .__init__ import DOERROR
+        if DOERROR:
+            prRed ("Error in Selection Sync")
+            prRed (e)
+            raise e
     return SELECTION_POLL_RATE
 
-def set_bone_selection(pose_bone, selection_state):
-    # in some older blender versions, select was a bone property
-    if hasattr(pose_bone, 'select'):
-        pose_bone.select = selection_state
-    elif hasattr(pose_bone.bone, 'select'):
-        pose_bone.bone.select = selection_state
+from .utilities.utilities import set_bone_selection
 
 def update_scene_selection_from_node_graph_selection_sync(
         active_ui_signature, view_layer,

+ 7 - 1
utilities/drivers.py

@@ -58,10 +58,16 @@ def CreateDrivers(drivers):
             ob = driver["owner"]
         else: # Pose Bone:
             ob = driver["owner"].id_data
+        path_from_id = driver.get('path_from_id')
+        # TODO: I want to refactor path_from_id check&set out. it should always be provided
+        # I think this is still here because of the 2 or three places where nodes make their
+        # own drivers e.g. Hook deformer for curves
+        if path_from_id is None:
+            path_from_id = driver["owner"].path_from_id(driver["prop"])
         if isinstance(driver["owner"], Key):
             fcurves = ob.driver_add(driver["prop"])
         else:
-            fcurves = ob.driver_add(driver["owner"].path_from_id(driver["prop"]), driver["ind"])
+            fcurves = ob.driver_add(path_from_id, driver["ind"])
         if not isinstance(fcurves, list):
             fcurves = [fcurves] # ususally it is a single fCurve, sometimes it is a list
             # for e.g. array properties if driver_add is given -1 as index

+ 341 - 204
utilities/node_common.py

@@ -11,21 +11,55 @@ from collections.abc import Callable
 from bpy.app import version as bpy_version
 
 
-#TODO: refactor the socket definitions so this becomes unnecessary.
-def get_socket_value(node_socket):
-    value = None
-    if hasattr(node_socket, "default_value"): 
-        value = node_socket.default_value
-    if node_socket.bl_idname == 'MatrixSocket':
-        value =  node_socket.TellValue()
-    return value
+# Utility to print links nicely for end user.
+def print_link_list(links):
+    ret_str = ""
+    for link in links:
+        ret_str+= link.__repr__() + '\n'
+    return ret_str
 
-# TODO: modify this to work with multi-input nodes
-def trace_single_line(mantis_node, input_name, link_index=0):
+# I've moved this to a separate function because the trace functions
+# are kind of hard to understand and the cycle check breaks the flow
+# of the code. 
+def detect_cycle_in_trace(link, link_circle):
+    if link.is_hierarchy:
+        if link in link_circle:
+            raise GraphError( "There is a cycle in the following links:\n" +
+                                print_link_list(link_circle))
+        link_circle.append(link)
+
+# NOTE this is a dupe of trace_single_line, and it must be used
+#      at any time before the tree is parsed (to check for cycles)
+#      I don't want or need to pay the performance cost to check this everywhere
+def trace_single_line_safe(mantis_node, input_name, link_index=0):
     # DO: refactor this for new link class
     """Traces a line to its input."""
     nodes = [mantis_node]
+    # a test for cyclic connections
+    link_circle = []
     # Trace a single line
+    if (socket := mantis_node.inputs.get(input_name) ):
+        while (socket.is_linked):
+            link = socket.links[link_index]; link_index = 0
+            # should throw an error if it is in the set
+            if detect_cycle_in_trace(link, link_circle):
+                return
+            if (socket := link.from_node.outputs.get(link.from_socket)):
+                nodes.append(socket.node)
+                if socket.can_traverse:
+                    socket = socket.traverse_target
+                else: # this is an output.
+                    break
+            else:
+                break
+    return nodes, socket
+
+
+def trace_single_line(mantis_node, input_name, link_index=0):
+    # DO: refactor this for new link class
+    """Traces a line to its input."""
+    nodes = [mantis_node]
+    # a test for cyclic connections
     if (socket := mantis_node.inputs.get(input_name) ):
         while (socket.is_linked):
             link = socket.links[link_index]; link_index = 0
@@ -39,7 +73,7 @@ def trace_single_line(mantis_node, input_name, link_index=0):
                 break
     return nodes, socket
 
-# this is same as the other, just flip from/to and in/out
+
 def trace_single_line_up(mantis_node, output_name,):
     """I use this to get the xForm from a link node."""
     nodes = [mantis_node]
@@ -87,6 +121,180 @@ def trace_line_up_branching(mantis_node : MantisNode, output_name : str,
                             leaf_nodes.append(socket.node)
     return leaf_nodes
 
+
+# For this, we basically just evaluate the driver once and use the result
+def find_driven_default_value(mantis_node, socket_name):
+    from ..misc_nodes import UtilityDriver, UtilitySwitch
+    from .utilities import get_ui_node
+    xf_inputs_values_var_names = []
+
+    node_line, _last_socket = trace_single_line(mantis_node, socket_name)
+    driver_node = None
+    driver_type = 'SUM'
+    keys = [ (0, 0), (1, 1) ]
+    for driver_node in node_line:
+        if driver_node.node_type == 'DRIVER': break
+    else: # it's not being driven
+        return mantis_node.evaluate_input(socket_name)
+    
+    if isinstance(driver_node, UtilityDriver):
+        driver_type = driver_node.evaluate_input("Driver Type")
+        # then get the variables
+        for i, input in enumerate(driver_node.inputs):
+            # unicode for 'a'
+            var_name = chr(i - 2 + 96)
+            if i == 0 : continue # the Driver Type doesn't interest us
+            elif i ==1 : continue # for now. we do need the fCurve
+            else:
+                if not input.is_linked: continue # nothing to do here
+            var_node    = input.links[0].from_node
+            # NOTE: this var_node will not be prepared on time unless we sort the graph by depth
+            #       and it doesn't seem to be possible to do that without breaking drivers... so
+            # HACK it's safe to do this, but VERY ugly and bad.
+            if not var_node.prepared:
+                var_node.bPrepare()
+            # end HACK
+            var_data    = var_node.parameters["Driver Variable"]
+            var_type    = var_data['type']
+            var_channel = var_data['channel']
+            prop_name = None; value = None 
+            xf1 = var_node.GetxForm(index=1)
+            match var_type:
+                case "SINGLE_PROP":
+                    prop_name = var_node.evaluate_input("Property")
+                    # we'll get the property value in the next step
+                case "TRANSFORMS":
+                    matrix1 = xf1.evaluate_input['Matrix']
+                    # Mantis ensures that these are identity at creation time
+                    if "LOC" in var_channel or "ROT" in var_channel:
+                            value = 0
+                    else: # Scale, defaults to 1
+                            value = 1
+                case "ROTATION_DIFF":
+                    xf2 = var_node.GetxForm(index=2)
+                    matrix1 = xf1.evaluate_input['Matrix']
+                    matrix2 = xf2.evaluate_input['Matrix']
+                    value = matrix1.to_quaternion().rotation_difference(matrix2.to_quaternion()).angle
+                case "LOC_DIFF":
+                    xf2 = var_node.GetxForm(index=2)
+                    matrix1 = xf1.evaluate_input['Matrix']
+                    matrix2 = xf2.evaluate_input['Matrix']
+                    value = (matrix1.to_translation() - matrix2.to_translation()).length
+                case "CONTEXT_PROP":
+                    raise NotImplementedError("Mantis Does not yet support Context Property")
+                    pass
+            xf_inputs_values_var_names.append((xf1, prop_name, value, var_name))
+
+        # Now we'll get the keyframe points and sort them, too!
+        if driver_node.inputs['fCurve'].is_linked:
+            fCurve_node = driver_node.inputs['fCurve'].links[0].from_node
+        extrapolation_mode = 'CONSTANT'
+        keys = []
+        if fCurve_node:
+            extrapolation_mode = fCurve_node.evaluate_input("Extrapolation Mode")
+            for l in fCurve_node.inputs['Keyframes'].links:
+                key_node = l.from_node
+                if key_node:
+                    keys.append( (key_node.evaluate_input("Frame"),  key_node.evaluate_input("Value")) )
+        keys = sorted( keys, key = lambda a : a[0]) # sort by the frame
+
+    elif isinstance(driver_node, UtilitySwitch):
+        # Reimplementing the GetxForm from the switch here
+        # because it is faster to inline this and store the trace result
+        trace = trace_single_line(driver_node, "Parameter" )
+        for node in trace[0]:
+            if node.node_type == 'XFORM':
+                xf = node                    # reuse trace HERE
+                prop_name = trace[1].name    # saves us from tracing again.
+                xf_inputs_values_var_names.append(( xf, prop_name, None, "a")); break
+        else:
+            raise RuntimeError(f"ERROR: Cannot Set the default of the driven value {socket_name} \n"
+                                f"in node {mantis_node}")
+    # We're gonna assume the xForm is sorting these right
+    # but we'll check the name if it is cheap
+    default_values = {}
+    for xf, prop_name, value, var_name in xf_inputs_values_var_names:
+        i=0
+        for output in xf.outputs:
+            if 'xForm Out' == output.name: continue
+            if output.name == prop_name: break
+            i+=1
+        node_line, _last_socket = trace_single_line(xf, "Custom Properties", i)
+        custom_prop_node = node_line[-1]
+        if value:
+            default_values[var_name] = value; continue
+        if prop_name:
+            if custom_prop_node.evaluate_input("Name") == prop_name:
+                custom_prop_type = custom_prop_node.evaluate_input("Type")
+                default_prop_name = ""
+                match custom_prop_type:
+                    case "BOOL":
+                        default_prop_name = 'Default (Bool)'
+                    case "FLOAT":
+                        default_prop_name = 'Default (Float)'
+                    case "INT":
+                        default_prop_name = 'Default (Int)'
+                    case _:
+                        raise RuntimeError(f"ERROR: Cannot Set the default of the driven value {socket_name} \n"
+                                           f"in node {mantis_node}"
+                                           f"Reason: Custom Property {prop_name} is not a valid type." )
+                default_values[var_name] = float(custom_prop_node.evaluate_input(default_prop_name))
+            else:
+                raise RuntimeError(f"ERROR: failed to get proeprty {prop_name} for node {xf}, \n"
+                                   f"cannot Set the default of the driven value {socket_name} \n"
+                                   f"in node {mantis_node}"
+                                    "Reason: Cannot Find Custom Property Node")
+    frame = 0.0
+    if default_values:
+        match driver_type:
+            case "AVERAGE":
+                frame = sum(default_values.values())/len(default_values)
+            case "SUM":
+                frame = sum(default_values.values())
+            case "MIN":
+                frame = min(default_values.values())
+            case "MAX":
+                frame = max(default_values.values())
+            case "SCRIPTED":
+                raise NotImplementedError (f"ERROR: Cannot Set the default of the driven value {socket_name} \n"
+                                        f"in node {mantis_node} \n"
+                                            "Reason: Driver Expressions are not yet implemented.")
+    # Now we apply the fCurve
+    # For now, We'll assume LINEAR keys TODO: come back to this during Pose update!
+    if len(keys) >= 2:
+        previous_frame = None
+        next_frame = None
+        previous_value = None
+        next_value = None
+        for key in keys:
+            if key[0] <= frame: previous_frame, previous_value = key
+            if key[0] >= frame: next_frame, next_value = key
+            if previous_frame is not None and next_frame is not None: break
+        else: # if our frame is out-of-bounds we need a slope to extrapolate on.
+            if previous_frame and not next_frame:
+                next_frame, next_value = previous_frame, previous_value
+                if extrapolation_mode == 'LINEAR':
+                    previous_frame, previous_value = keys[-2] # we need the second last key to extrapolate
+            elif next_frame and not previous_frame:
+                previous_frame, previous_value = next_frame, next_value
+                if extrapolation_mode == 'LINEAR':
+                    next_frame, next_value = keys[1] # we need the second key to extrapolate
+            else:   
+                raise RuntimeError (f"ERROR: Cannot Set the default of the driven value {socket_name} \n"
+                                    f"in node {mantis_node} \n"
+                                    "Reason: Cannot place value on driver fCurve.")
+        if previous_frame == next_frame: # the frame is exactly on a key
+            return previous_value
+        # this is just a simple lerp because we are assuming the keys are linear for now
+        result = previous_value + ((frame - previous_frame) / (next_frame - previous_frame)) * (next_value - previous_value)
+    elif len(keys) ==1:
+        result = key[1] # flat curve, take the value of the only key
+    else:
+        raise RuntimeError (f"ERROR: Cannot Set the default of the driven value {socket_name} \n"
+                            f"in node {mantis_node} \n"
+                             "Reason: Invalid fCurve has 0 keyframes")
+    return result
+        
 # this function exists to walk back to the first ancestor node of XFORM or LINK without
 #  breaking the traverse behaviour which I still need for various reasons.
 def get_parent_xForm_info(mantis_node, socket_name = 'Relationship'):
@@ -161,7 +369,7 @@ def prepare_parameters(mantis_node):
             mantis_node.parameters[s_name] = sock.links[0].from_node.parameters[sock.name]
     # should work, this is ugly.
 
-def check_for_driver(mantis_node, input_name, value_index = None, array_index = 0, combo_property=False):
+def check_for_driver(mantis_node, input_name, value_index = None, array_index = 0):
     prop = mantis_node.evaluate_input(input_name, array_index)
     if (value_index is not None):
         try:
@@ -171,204 +379,133 @@ def check_for_driver(mantis_node, input_name, value_index = None, array_index =
         # TODO an Index or Key error here should have a reasonable error message
     return (prop.__class__.__name__ == 'MantisDriver')
 
-def print_driver_item(mantis_node, b_object, original_prop):
-    if mantis_node.node_type in ['LINK',]:
-        printname  = wrapOrange(b_object.id_data.name)
-    elif mantis_node.node_type in ['XFORM',]:
-        printname  = wrapOrange(mantis_node.bGetObject().name)
-    else:
-        printname = wrapOrange(mantis_node)
-    print("Adding driver %s to %s in %s" % (wrapPurple(original_prop), wrapWhite(mantis_node.signature[-1]), printname))
-
-# HACK This is the most hideous voodoo black magic fucntion in the entire program
-#      but everything depends on it and it works 
-def evaluate_sockets(mantis_node, b_object, props_sockets,):
-    # this is neccesary because some things use dict properties for dynamic properties and setattr doesn't work
-    def safe_setattr(ob, att_name, val):
-        if ob.__class__.__name__ in ["NodesModifier"]:
+# this is neccesary because some things use dict properties for dynamic properties and setattr doesn't work
+def safe_setattr(ob, att_name, val):
+    ob_class_name = ob.__class__.__name__
+    match ob_class_name:
+        case "NodesModifier":
             ob[att_name]=val
-        elif b_object.__class__.__name__ in ["Key"]:
-            if not val: val=0
+        case "Key":
             ob.key_blocks[att_name].value=val
-        elif "]." in att_name:
-            # it is of the form prop[int].prop2
-            prop=att_name.split('[')[0]
-            prop1=att_name.split('.')[1]
-            get_index = int(att_name.split('[')[1][0])
-            setattr(getattr(b_object, prop)[get_index], prop1, val)
-        else:
-            try:
-                setattr(ob, att_name, val)
-            except Exception as e:
-                print ("ob, att_name, val")
-                prRed(ob, att_name, val)
-                raise e
-    for prop, filler_item in props_sockets.items():
-        if isinstance(filler_item, tuple):
-            raise NotImplementedError(mantis_node)
-        sock = filler_item.socket_name
-        default = filler_item.default_value
-        value_index = filler_item.value_index
-        array_index = filler_item.array_index
-        combo_property = filler_item.combo_property
-        driver_key = (sock, value_index, array_index)
-        if (check_for_driver(mantis_node, sock, value_index, array_index, combo_property)):
-            original_prop = prop
-            # TODO: deduplicate this terrible hack
-            if ("." in prop) and not b_object.__class__.__name__ in ["Key"]: # this is a property of a property...
-                sub_props = [b_object]
-                while ("." in prop):
-                    split_prop = prop.split(".")
-                    prop = split_prop[1]
-                    sub_prop = (split_prop[0])
-                    if ("[" in sub_prop):
-                        sub_prop, sub_index = sub_prop.split("[")
-                        sub_prop_index = int(sub_index[0])
-                        sub_props.append(getattr(sub_props[-1], sub_prop)[sub_prop_index])
-                    else:
-                        sub_props.append(getattr(sub_props[-1], sub_prop))
-                safe_setattr(sub_props[-1], prop, default)
-            # this is really stupid
-            else:
-                safe_setattr(b_object, prop, default)
-            print_driver_item( mantis_node, b_object, original_prop)
-            if b_object.__class__.__name__ in ["NodesModifier"]:
-                mantis_node.drivers[driver_key] = "[\""+original_prop+"\"]" # lol. It is a dict element not a "true" property
-            elif b_object.__class__.__name__ in ["Key"]:
-                mantis_node.drivers[driver_key] = "key_blocks[\""+original_prop+"\"].value"
-            else:
-                mantis_node.drivers[driver_key] = original_prop
-        else: # here we can do error checking for the socket if needed
-            if (value_index is not None):
-                safe_setattr(b_object, prop, mantis_node.evaluate_input(sock)[value_index])
-            else:                    # 'mute' is better than 'enabled'
-                # UGLY HACK          # because it is available in older
-                if (prop == 'mute'): # Blenders.
-                    safe_setattr(b_object, prop, not bool(mantis_node.evaluate_input(sock)))
-                elif (prop == 'hide'): # this will not cast it for me, annoying.
-                    safe_setattr(b_object, prop, bool(mantis_node.evaluate_input(sock)))
-                else:
-                    try:
-                    # just wanna see if we are dealing with some collection
-                    # check hasattr in case it is one of those ["such-and-such"] props, and ignore those
-                        if hasattr(b_object, prop) and (not isinstance(getattr(b_object, prop), str)) and hasattr(getattr(b_object, prop), "__getitem__"):
-                            # prGreen("Doing the thing")
-                            for val_index, value in enumerate(mantis_node.evaluate_input(sock)):
-                                # assume this will work, both because val should have the right number of elements, and because this should be the right data type.
-                                from .drivers import MantisDriver
-                                if isinstance(value, MantisDriver):
-                                    getattr(b_object,prop)[val_index] = default[val_index]
-                                    print("Adding driver %s to %s in %s" % (wrapPurple(prop), wrapWhite(mantis_node.signature[-1]), mantis_node))
-                                    try:
-                                        mantis_node.drivers[sock].append((prop, val_index))
-                                    except:
-                                        mantis_node.drivers[sock] = [(prop, val_index)]
-                                else:
-                                    getattr(b_object,prop)[val_index] =  value
-                        else:
-                            safe_setattr(b_object, prop, mantis_node.evaluate_input(sock))
-                    except Exception as e:
-                        prRed(b_object, mantis_node, prop, sock, mantis_node.evaluate_input(sock))
-                        raise e
+        case _:
+            setattr(ob, att_name, val)
 
-def finish_driver(mantis_node, b_object, driver_item, prop):
-    try:
-        driver_sock = driver_item[0]; index = driver_item[1]; array_index =driver_item[2]
-    except Exception as e:
-        print (wrapGreen(mantis_node), wrapOrange(b_object), wrapRed(driver_item), wrapWhite(prop),)
-        raise e
-    driver_trace = trace_single_line(mantis_node, driver_sock, array_index)
-    driver_provider, driver_socket = driver_trace[0][-1], driver_trace[1]
-    
-    combo_property = False
-    # try to detect combo properties
-    if mantis_node.socket_templates:
-        for st in mantis_node.socket_templates:
-            if st.name == driver_sock:
-                if isinstance(st.blender_property, tuple):
-                    combo_property = True
-                break
+# do: make this function end in a single setattr
+def evaluate_sockets(mantis_node, b_objects, props_sockets,):
+    # TODO: I think it is wasteful to run this once per b_object for e.g. Link nodes.
+    #       Should be able to get the results once and pass 'em along. Probably only saves
+    #       a handful of fn calls in a big graph. Not worth doing a big refactor right now
+    # Because Mantis Nodes are sometimes a composition of different Blender objects,
+    # We have to set properties on different Blender Objects and the easiest way
+    # is to send the object in directly instead of trying to find e.g. nested properties
+    for property_group, b_object in b_objects.items():
+        ob_class_name = b_object.__class__.__name__
+        for filler_item in props_sockets.values():
+            filler_property_group = filler_item.property_group or "DEFAULT"
+            prop = filler_item.property_name
+            if filler_property_group != property_group: continue
+            if b_object is None: continue
 
-    if index is not None:
-        try:
-            driver = driver_provider.parameters[driver_socket.name][index].copy()
-        except KeyError as e: # try single driver, it should still work fine.
-            if combo_property:
-                driver = driver_provider.parameters[driver_socket.name].copy()
-            else:
-                raise e
-        # this is harmless and necessary for the weird ones where the property is a vector too
-        if not combo_property:
-            driver["ind"] = index
-    else:
-        driver = driver_provider.parameters[driver_socket.name].copy()
-    if driver:
-        socket_template = None
-        if mantis_node.socket_templates:
-            for socket_template in mantis_node.socket_templates:
-                if socket_template.name == driver_sock: break
-        if socket_template:
-            if socket_template.is_animatable == False:
-                raise RuntimeError(f"Driver is not allowed for {driver_sock} in node {mantis_node}")
-        if ("." in prop) and mantis_node.__class__.__name__ != "DeformerMorphTargetDeform": # this is a property of a property...
-            sub_props = [b_object]
-            while ("." in prop):
-                split_prop = prop.split(".")
-                prop = split_prop[1]
-                sub_prop = (split_prop[0])
-                if ("[" in sub_prop):
-                    sub_prop, index = sub_prop.split("[")
-                    index = int(index[0])
-                    sub_props.append(getattr(sub_props[-1], sub_prop)[index])
+            input_name = filler_item.socket_name
+            default = ( filler_item.default_value, )
+            value_index = filler_item.value_index
+            array_index = filler_item.array_index
+            combo = filler_item.combo_property
+            vector = filler_item.vector_property
+
+            # Check for a driver -- this is inlined from check_for_driver()
+            evaluated_value = mantis_node.evaluate_input(input_name, array_index)
+            driver = evaluated_value
+            drivers_exist = (evaluated_value.__class__.__name__ == 'MantisDriver', )
+            single_driver_input = drivers_exist[0]
+
+            if combo and vector: # we map an element of the socket's vector to a single scalar property
+                evaluated_value = evaluated_value[value_index]
+                drivers_exist = (evaluated_value.__class__.__name__ == 'MantisDriver', )
+            if vector and not combo: # then we map a vector to a vector and have to check for drivers per-element
+                default = filler_item.default_value
+                # the evaluated value could either be a vector of scalar values (or scalar drivers) OR one driver
+                if single_driver_input: # we have alredy checked for single-driver, so this is a vector otherwise
+                    drivers_exist = tuple( True for _unused in range(len(default)) )
                 else:
-                    sub_props.append(getattr(sub_props[-1], sub_prop))
-            driver["owner"] = sub_props[-1]
-        elif mantis_node.node_type in ['XFORM',] and mantis_node.__class__.__name__ in ['xFormBone']:
-            # TODO: I really shouldn't have to hardcode this. Look into better solutions.
-            # NOTE: VERSIONING in Blender 5.0 we have to use pose bone for this, too.
-            # default to pose bone, it's usually what we want
-            bone_col = mantis_node.bGetParentArmature().pose.bones
-            from ..xForm_socket_templates import attributes_shared_by_bone_and_pose_bone
-            if socket_template.prop_owner_group == 'bone':
-                bone_col = mantis_node.bGetParentArmature().data.bones
-            # this is a little bit clumsy. TODO clean up here
-            if prop in attributes_shared_by_bone_and_pose_bone:
-                # we want to use the pose bone for this...only if it has a driver
-                bone_col = mantis_node.bGetParentArmature().pose.bones
-            driver["owner"] = bone_col[b_object] # we use "unsafe" brackets instead of get() because we want to see any errors that occur
-        # HACK having special cases here is indicitave of a deeper problem that should be refactored
-        elif mantis_node.__class__.__name__ in ['xFormCurvePin'] and \
-                      prop in ['offset_factor', 'forward_axis', 'up_axis']:
-                driver["owner"] = b_object.constraints['Curve Pin']
-        else:
-            driver["owner"] = b_object
-        driver["prop"] = prop
-        return driver
-    else:
-        prOrange("Provider", driver_provider)
-        prGreen("socket", driver_socket)
-        print (index)
-        prPurple(driver_provider.parameters[driver_socket.name])
-        prRed("Failed to create driver for %s" % prop)
-        return None
+                    drivers_exist = tuple( val.__class__.__name__ == 'MantisDriver' for val in evaluated_value )
+            assign_this_value = evaluated_value # Usually we just assign the value right away
+            if single_driver_input and vector: # in this pernicious case, we need to lift
+                assign_this_value = list(default) # the evaluated value from scalar to vector
+            # We had to pack everything into this weird iterable format so we can handle
+            # all 4 cases, (scalar, vector, scalar combo, vector combo) in one place
+            # this isn't perfectly optimal but I expect it to cost a few milliseconds at most
+            # and while it is kind of confusing it is nice to keep all this in one code path
+            if any(drivers_exist):
+                if vector and not combo and isinstance(assign_this_value, tuple):
+                    assign_this_value = list(assign_this_value) # because we have to mutate it
+                for i, (default_value, driver_exists) in enumerate(zip(default, drivers_exist)):
+                    if driver_exists: # we need to pack data for the driver
+                        if filler_item.is_animatable == False:
+                            raise RuntimeError(f"Driver is not allowed for input \"{input_name}\" in node \"{mantis_node}\".")
+                        # NOTE at this point we can inject the driver's default resulting value
+                        #      and we have to do so in Curve Pin where it has a special case
+                        #      In the future I may implement this HERE in general
+                        printname = wrapOrange(mantis_node)
+
+                        if vector and not combo: assign_this_value[i] = default_value
+                        else: assign_this_value = default_value
+
+                        if mantis_node.node_type == 'LINK': printname  = wrapOrange(b_object.id_data.name)
+                        elif mantis_node.node_type == 'XFORM': printname  = wrapOrange(mantis_node.bGetObject().name)
+                        print(f"Adding driver to property: {wrapPurple(prop)} in {printname} in node: {wrapWhite(mantis_node.ui_signature[1:])}")
+                        # NOW we can set up the driver.
+                        # TODO: Maybe can prefill this in the socket template or cache the result per-socket
+                        driver_properties = {} # use this dict to pack this data without mutating the driver itself
+                        driver_properties["path_from_id"] = b_object.path_from_id(prop,)
+                        driver_properties['owner'] = b_object # it is the CALLER's responsibility to send the right object!!
+                        # we need a special case here for Bones :/
+                        # TODO: refactor so I don't need this
+                        if mantis_node.__class__.__name__ == 'xFormBone':
+                            from ..xForm_socket_templates import attributes_shared_by_bone_and_pose_bone
+                            if prop in attributes_shared_by_bone_and_pose_bone:
+                                driver_properties['owner'] = b_objects['pose_bone']
+                                driver_properties["path_from_id"] = b_objects['pose_bone'].path_from_id(prop,)
+                        if (vector and not combo):
+                            driver_properties['ind'] = i
+                        if vector and single_driver_input:
+                            driver_properties['ind'] = -1 # the driver will be added to all 3 channels
+                        # TODO: I would like the socket filler to handle the special cases here.
+                        #       even if it means I have to pack another operator in the filler.
+                        resolved_prop_name = prop
+                        match ob_class_name:
+                            case "NodesModifier": # It is a dict element.
+                                resolved_prop_name = "[\""+prop+"\"]"
+                            case "Key": # that is, the outer data block that owns shape keys, not a key-frame.
+                                resolved_prop_name = "key_blocks[\""+prop+"\"].value"
+                        driver_properties["prop"] = resolved_prop_name
+                        driver_index = value_index if vector and not single_driver_input else None
+                        if driver_index: driver = driver[driver_index]
+                        mantis_node.drivers.append( (driver, driver_properties) )
+
+            # We wait until now to skip this because it is important to do the
+            #   is_animatable check above and give the user a meaningful error
+            if prop is None: continue
+            # Some socket values need to be transformed, so we can use the operator to do that.
+            if filler_item.property_value_operator:
+                assign_this_value = filler_item.property_value_operator(mantis_node, evaluated_value)
+            # at this point there is ONE filler per property and we just have to assign the value
+            if ob_class_name not in {'NodesModifier', 'Key'}:
+                setattr(b_object, prop, assign_this_value)
+            elif ob_class_name == "NodesModifier":
+                b_object[prop] = assign_this_value
+            else: # ob_class_name == "Key":
+                b_object.key_blocks[prop].value = assign_this_value
 
 def finish_drivers(mantis_node):
     drivers = []
-    if not hasattr(mantis_node, "drivers"):
-        return # HACK
-    for driver_item, prop in mantis_node.drivers.items():
-        b_objects = [mantis_node.bObject]
-        if mantis_node.node_type == 'LINK':
-            b_objects = mantis_node.bObject # this is already a list
-        for b_object in b_objects:
-            # Mutating the driver_item here is the WORST THING I HAVE EVER SEEN
-            # HACK UGLY BUG TERRIBLE BAD CODE
-            if isinstance(prop, list):
-                for sub_item in prop:
-                        drivers.append(finish_driver(mantis_node, b_object, (driver_item, sub_item[1], 0), sub_item[0]))
-                else:
-                    drivers.append(finish_driver(mantis_node, b_object, (driver_item, sub_item[1], 0), sub_item[0]))
-            else:
-                drivers.append(finish_driver(mantis_node, b_object, driver_item, prop))
+    for driver, driver_properties in mantis_node.drivers:
+        drivers_from_socket = (driver,)
+        if isinstance(driver, (tuple, list)):
+            drivers_from_socket = driver # this is a collection e.g. from a Vector Combine
+        for driver in drivers_from_socket:
+            driver_copy = driver.copy() # it is safe to copy it here because it is guarenteed to be filled out.
+            driver_copy.update(driver_properties)
+            drivers.append(driver_copy)
     from .drivers import CreateDrivers
-    CreateDrivers(drivers)
+    CreateDrivers(drivers)

+ 16 - 0
utilities/socket_filler_operators.py

@@ -0,0 +1,16 @@
+# This file is for defining socket-filler operators by name
+# These operators are defined by name in Socket Templates and used by
+#    the Socket Filler system in the Property-Socket Map to transform
+#    Mantis data to Blender-ready data
+# Primarily, this will involve type-casting and enum handling
+# Probably I will need to expand this system to take arbitrary arguments
+#    but for now we'll take the Mantis Node and the data as arguments
+
+def cast_to_bool(*args, **kwargs):
+    return bool(args[1]) # args[1] is the data
+
+def invert_cast_to_bool(*args, **kwargs):
+    return bool( not args[1]) # args[1] is the data
+
+def resolve_rotation_mode_auto(*args, **kwargs):
+    return 'XYZ' # for now this is hardcoded lmao

+ 75 - 69
utilities/utilities.py

@@ -26,64 +26,74 @@ def prOrange(*args): print (*[wrapOrange(arg) for arg in args])
 def float_lerp(a : float, b : float, factor : float)  -> float:
     return (a * (1.0 - factor)) + (b * factor)
 
-# TODO: BUG may live here
-#I need to stress-test this to make sure it doesn't hang on:
-# - empty reroute nodes with no links
-# - links starting with reroute nodes with no inputs
-# - links ending with reroute nodes with no outputs
-def find_reroute_start_socket(reroute, track='BACK'):
-    # "BACK" traces back through the tree
-    # "FORWARD" traces forward through the tree
-    socket = None
-    while (reroute and track == 'BACK'):
-        if len(reroute.inputs[0].links) == 1:
-            link = reroute.inputs[0].links[0]
-            socket = link.from_socket
-            if link.from_node.bl_idname == 'NodeReroute':
-                reroute =  link.from_node
-            else:
-                link, reroute = None, None
-    while (reroute and track == 'FORWARD'):
-        if len(reroute.outputs[0].links) == 1:
-            link = reroute.outputs[0].links[0]
-            socket = link.to_socket
-            if link.to_node.bl_idname == 'NodeReroute':
-                reroute =  link.to_node
-            else:
-                link, reroute = None, None
-    return socket
+def find_reroute_start_socket(reroute,):
+    # "BACK" traces back through the tree and returns one socket
+    # "FORWARD" traces forward through the tree and returns a list of sockets
+    from ..base_definitions import reroute_cache
+    from .node_common import GraphError
+    # We assume the cache is good, the tree is responsible for this
+    tree = reroute.id_data
+    tree_cache = reroute_cache.get(tree.name)
+    cache_item = tree_cache.get(reroute.name)
+    if not cache_item or not cache_item.source:
+        raise GraphError(f"There is a missing link in the tree at reroute node: {reroute.name}")
+    return tree.nodes[cache_item.source[0]].outputs[cache_item.source[1]]
+
+def find_reroute_end_socket(reroute,):
+    # "BACK" traces back through the tree and returns one socket
+    # "FORWARD" traces forward through the tree and returns a list of sockets
+    from ..base_definitions import reroute_cache
+    from .node_common import GraphError
+    # We assume the cache is good, the tree is responsible for this
+    tree = reroute.id_data
+    tree_cache = reroute_cache.get(tree.name)
+    cache_item = tree_cache.get(reroute.name); sockets = []
+    if not cache_item: # I think this is OK, they don't always go somewhere
+        return # And this FORWARD trace is always a background task
+    sockets = [ tree.nodes.get(node_name).inputs[socket_index] for \
+                node_name, socket_index in cache_item.destinations.items()]
+    return sockets
 
 # this creates fake links that have the same interface as Blender's
 # so that I can bypass Reroutes
 def clear_reroutes(links):
     from ..base_definitions import DummyLink
-    kept_links, rerouted_starts = [], []
-    rerouted = []
-    all_links = links.copy()
-    while(all_links):
-        link = all_links.pop()
+    kept_links = []
+    for i in range (len(links)):
+        link = links[i]
         to_cls = link.to_socket.node.bl_idname
         from_cls = link.from_socket.node.bl_idname
         reroute_classes = ["NodeReroute"]
         if (to_cls in reroute_classes and
             from_cls in reroute_classes):
-                rerouted.append(link)
+            continue
         elif (to_cls in reroute_classes and not
             from_cls in reroute_classes):
-                rerouted.append(link)
+            continue
         elif (from_cls in reroute_classes and not
             to_cls in reroute_classes):
-                rerouted_starts.append(link)
+            # from_socket = find_reroute_start_socket(link.from_node)
+
+
+            from ..base_definitions import reroute_cache
+            from .node_common import GraphError
+            # We assume the cache is good, the tree is responsible for this
+            tree = link.from_node.id_data
+            tree_cache = reroute_cache.get(tree.name)
+            cache_item = tree_cache.get(link.from_node.name)
+            if not cache_item or not cache_item.source:
+                raise GraphError(f"There is a missing link in the tree at reroute node: {link.from_node.name}")
+            from_socket = tree.nodes[cache_item.source[0]].outputs[cache_item.source[1]]
+
+
+            new_link = DummyLink(from_socket=from_socket,
+                                to_socket=link.to_socket,
+                                from_mantis_node=None,
+                                to_mantis_node=None,
+                                multi_input_sort_id=link.multi_input_sort_id )
+            kept_links.append(new_link)
         else:
             kept_links.append(link)
-    for start in rerouted_starts:
-        from_socket = find_reroute_start_socket(start.from_node)
-        new_link = DummyLink(from_socket=from_socket,
-                             to_socket=start.to_socket,
-                             from_mantis_node=None,
-                             to_mantis_node=None,
-                             multi_input_sort_id=start.multi_input_sort_id )
-        kept_links.append(new_link)
     return kept_links
 
 def tree_from_mantis_node(sig, base_tree):
@@ -110,6 +120,13 @@ def split_quoted_substrings_to_strings(text):
     ]
     return substrings
 
+def set_bone_selection(pose_bone, selection_state):
+    # in some older blender versions, select was a bone property
+    if hasattr(pose_bone, 'select'):
+        pose_bone.select = selection_state
+    elif hasattr(pose_bone.bone, 'select'):
+        pose_bone.bone.select = selection_state
+
 ##################################################################################################
 # groups and changing sockets -- this is used extensively by Schema.
 ##################################################################################################
@@ -208,25 +225,26 @@ def do_relink(node, socket, map, in_out='INPUT', parent_name = ''):
         for sub_val in val:
             # this will only happen once because it assigns socket, so it is safe to do in the for loop.
             if socket is None:
-                socket = sub_val
+                socket = sub_val; sockets = [socket]
                 if sub_val.bl_idname == "NodeReroute":
                     # we have to trace the reroute node...
                     if in_out == 'INPUT':
-                        socket = find_reroute_start_socket(sub_val)
+                        socket = find_reroute_start_socket(sub_val); sockets = [socket]
                     else:
-                        socket = find_reroute_start_socket(sub_val, track="FORWARD")
-                sock_type = socket.interface_type
-                name = unique_socket_name(node, socket, tree)
-                if parent_name:
-                    interface_socket = update_interface(tree.interface, name, interface_in_out, sock_type, parent_name)
-                if in_out =='INPUT':
-                    socket = node.inputs.new(sock_type, name, identifier=interface_socket.identifier)
-                else:
-                    socket = node.outputs.new(sock_type, name, identifier=interface_socket.identifier)
-                if parent_name == 'Array': socket.display_shape='SQUARE_DOT'
-                if parent_name == 'Constant': socket.display_shape='CIRCLE_DOT'
-                # then move it up and delete the other link.
-                # this also needs to modify the interface of the node tree.
+                        sockets = find_reroute_end_socket(sub_val)
+                for socket in sockets:
+                    sock_type = socket.interface_type
+                    name = unique_socket_name(node, socket, tree)
+                    if parent_name:
+                        interface_socket = update_interface(tree.interface, name, interface_in_out, sock_type, parent_name)
+                    if in_out =='INPUT':
+                        socket = node.inputs.new(sock_type, name, identifier=interface_socket.identifier)
+                    else:
+                        socket = node.outputs.new(sock_type, name, identifier=interface_socket.identifier)
+                    if parent_name == 'Array': socket.display_shape='SQUARE_DOT'
+                    if parent_name == 'Constant': socket.display_shape='CIRCLE_DOT'
+                    # then move it up and delete the other link.
+                    # this also needs to modify the interface of the node tree.
             if isinstance(sub_val, NodeSocket):
                 counter_id=sub_val.node.name+"::"+sub_val.identifier
                 l = None
@@ -831,18 +849,6 @@ def trace_all_nodes_from_root(root, nodes):
 # misc
 ##################################################################################################
 
-# TODO: get the matrix to return a mathutils.Matrix so I don't need a function call here
-def to_mathutils_value(socket):
-    if hasattr(socket, "default_value"):
-        val = socket.default_value
-        if socket.bl_idname in ['MatrixSocket']:
-            return socket.TellValue()
-        else:
-            return val
-    else:
-        return None
-
-
 def all_trees_in_tree(base_tree, selected=False):
     """ Recursively finds all trees referenced in a given base-tree."""
     # note that this is recursive but not by tail-end recursion

+ 58 - 20
versioning.py

@@ -21,6 +21,7 @@ from .utilities.utilities import (prRed, prGreen, prPurple, prWhite,
 #    linked group and schema nodes to update)
 # (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)?
 
 def version_upgrade_very_old(*args, **kwargs):
     node = kwargs['node']
@@ -327,6 +328,8 @@ def up_0_13_0_new_custom_prop_node(*args, **kwargs):
             prop_node.location = prop_node.location - Vector((180, -260*i))
             prop_node.select = False # it is annoying to select them all by default
     else: # just need to add the socket.
+        if node.inputs.get('Custom Properties'):
+            return # already done here
         new_input = node.inputs.new("CustomPropSocket", "Custom Properties", use_multi_input=True)
 
 def schema_enable_custom_interface_types(*args, **kwargs):
@@ -376,6 +379,11 @@ def update_fcurve_and_armature_node_to_multi_socket(*args, **kwargs):
     # First we have to get the socket maps and re-init the sockets
     convert_keys = {'UtilityFCurve' : [ 'Keyframe', ],
                     'LinkArmature'  : [ 'Target', 'Weight', ], }
+    # first test if we need to do the versioning
+    if (socket := node.inputs.get(convert_keys[node.bl_idname][0]+'s')):
+        if socket.is_multi_input: return
+        else: # this will never happen, doesn't hurt to handle it
+            node.inputs.remove(socket) # must be an array
     my_convert_keys = convert_keys[node.bl_idname]
     sockets_in, _sockets_out = get_socket_maps(node,)
     # NOTE IMPORTANT BUG INFO
@@ -438,7 +446,7 @@ def fix_bbone_default_scale_in_and_out(*args, **kwargs):
     current_minor_version = tree.mantis_version[1]
     current_sub_version = tree.mantis_version[2]
     if  current_major_version > 0: return# major version must be 0
-    if current_minor_version >= 13: return# minor version must be 12 or less
+    if current_minor_version > 13: return# minor version must be 12 or less
     if current_minor_version == 13 and current_sub_version >= 5: return# sub version must be 4 or less
     prPurple(f"Fixing BBone Settings for {tree.name}")
     live_updates = tree.do_live_update
@@ -465,8 +473,9 @@ def add_new_display_features_v0_13_5(*args, **kwargs):
     current_major_version = tree.mantis_version[0]
     current_minor_version = tree.mantis_version[1]
     current_sub_version = tree.mantis_version[2]
-    if  current_major_version > 0: return# major version must be 0
-    if current_minor_version >= 13: return# minor version must be 12 or less
+    print(tree.mantis_version[:])
+    if  current_major_version > 0:return# major version must be 0
+    if current_minor_version > 13: return# minor version must be 12 or less
     if current_minor_version == 13 and current_sub_version >= 5: return# sub version must be 4 or less
     prPurple(f"Adding new Display Features to {tree.name}")
     live_updates = tree.do_live_update
@@ -523,20 +532,49 @@ def add_new_display_features_v0_13_5(*args, **kwargs):
                 node.is_updating = False
         tree.do_live_update = live_updates
 
-# def up_0_13_5_modes_for_matrix_from_curve_and_compose_matrix(*args, **kwargs):
-#     node = kwargs['node']
-#     node.is_updating = True
-#     do_live_updates = node.id_data.do_live_updates
-#     node.id_data.do_live_updates = False
-#     # new re_init keyword should use socket template to automate the update.
-#     prGreen("THIS ONE HERE")
-#     print (node.id_data.name, node.name)
-#     print (node.inputs.keys())
-#     if "Mode" not in node.inputs.keys(): # just check here to see if it is needed.
-#         prOrange("DOING THE THING BRO")
-#         node.init_sockets(node.__class__.socket_templates, re_init=True)
-#     node.id_data.do_live_updates = do_live_updates
-#     node.is_updating = False
+def up_0_13_5_modes_for_matrix_from_curve_and_compose_matrix(*args, **kwargs):
+    node = kwargs['node']
+    if 'Mode' in node.inputs:
+        return # done already
+    tree = node.id_data
+    current_major_version = tree.mantis_version[0]
+    current_minor_version = tree.mantis_version[1]
+    current_sub_version = tree.mantis_version[2]
+    if  current_major_version > 0: return# major version must be 0
+    if current_minor_version >= 13: return# minor version must be 12 or less
+    if current_minor_version == 13 and current_sub_version >= 5: return# sub version must be 4 or less
+
+    live_updates = tree.do_live_update
+    tree.do_live_update = False
+
+    for other_node in tree.nodes:
+        if hasattr(other_node, 'is_updating'):
+            other_node.is_updating = True
+    try:
+        socket_templates = node.socket_templates
+        if node.bl_idname == 'UtilityMatrixFromCurve':
+            from .misc_nodes_socket_templates import (MatrixFromCurveMode,
+                                                    MatrixFromCurveFactor,
+                                                    MatrixFromCurveTailFactor, )
+            input_templates = ( MatrixFromCurveMode, MatrixFromCurveFactor,
+                                MatrixFromCurveTailFactor, )
+            node.init_sockets(input_templates)
+            node.inputs.move(len(node.inputs)-3, 0) # move Mode to first place
+        elif node.bl_idname == 'UtilityMatrixCompose':
+            from .misc_nodes_socket_templates import (ComposeMatrixBoneLength,
+                                                    MatrixComposeMode,
+                                                    ComposeMatrixRotation,)
+            input_templates = ( ComposeMatrixBoneLength, MatrixComposeMode,
+                                ComposeMatrixRotation,)
+            node.init_sockets(input_templates)
+            node.inputs.move(len(node.inputs)-1, 0) # These go in the top, so make em in order
+            node.inputs.move(len(node.inputs)-1, 0) # and move 'em one at a time in reverse order
+        node.socket_templates = socket_templates
+    finally:
+        for other_node in tree.nodes:
+            if hasattr(other_node, 'is_updating'):
+                other_node.is_updating = False
+        tree.do_live_update = live_updates
 
 # versioning tasks that involve Blender versions rather than Mantis versions:
 error_description_4_5_0_LTS = 'There is a bug in Blender 4.5.0 LTS, that is why these sockets are blue.'\
@@ -576,9 +614,9 @@ versioning_tasks = [
            update_fcurve_and_armature_node_to_multi_socket, ['node']),
     (['MantisTree', 'SchemaTree'], fix_bbone_default_scale_in_and_out, ['tree']),
     (['MantisTree', 'SchemaTree'], add_new_display_features_v0_13_5, ['tree']),
-    # (['UtilityMatrixFromCurve', 'UtilityMatrixCompose'],
-    #    up_0_13_5_modes_for_matrix_from_curve_and_compose_matrix,
-    #  ['node',], ),
+    (['UtilityMatrixFromCurve', 'UtilityMatrixCompose'],
+       up_0_13_5_modes_for_matrix_from_curve_and_compose_matrix,
+     ['node',], ),
 ]
 
 

+ 61 - 79
xForm_nodes.py

@@ -67,9 +67,12 @@ def set_object_parent(node):
         node_ob.parent_bone = parent_xForm_info.self_edit_name
 
 class xFormNode(MantisNode):
-    def __init__(self, signature, base_tree, socket_templates=[]):
-        super().__init__(signature, base_tree, socket_templates)
-        self.node_type = 'XFORM'
+    __slots__ = ()
+    node_type = 'XFORM'
+    def __init__(self, signature, base_tree, socket_templates=[],
+                 additional_parameters={}):
+        super().__init__(signature, base_tree, socket_templates,
+                         additional_parameters)
         self.bObject=None
 
     # because new objects are created during prep phase
@@ -156,11 +159,11 @@ class xFormNode(MantisNode):
 # TODO this should use the socket templates / prop_sockets system!
 class xFormArmature(xFormNode):
     '''A node representing an armature object'''
+    __slots__ = ()
+    socket_templates = xFormArmatureSockets
 
     def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, xFormArmatureSockets)
-        self.init_parameters()
-        self.set_traverse([("Relationship", "xForm Out")])
+        super().__init__(signature, base_tree, self.socket_templates)
         setup_custom_property_inputs_outputs(self)
 
     def bPrepare(self, bContext=None):
@@ -276,15 +279,11 @@ class xFormArmature(xFormNode):
 
 class xFormBone(xFormNode):
     '''A node representing a bone in an armature'''
+    __slots__ = ()
+    socket_templates = xFormBoneSockets
     # DO: make a way to identify which armature this belongs to
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree, xFormBoneSockets)
-        # TODO: implement socket templates completely for Bone
-        # currently it is waiting on BBone and refactoring/cleanup.
-        # the socket templates are being used now but not for mantis node execution.
-        # because of edit/pose/objcet bone this will be a bit complex
-        self.init_parameters()
-        self.set_traverse([("Relationship", "xForm Out")])
         setup_custom_property_inputs_outputs(self)
 
     def bGetParentArmature(self):
@@ -380,8 +379,8 @@ class xFormBone(xFormNode):
 
         # Setup Deform attributes...
         eb_props_sockets = self.gen_property_socket_map(prop_owner_group="edit_bone")
-        evaluate_sockets(self, eb, eb_props_sockets)
-        
+        b_objects = {'edit_bone':eb, 'pose_bone':None, 'bone':None, 'DEFAULT':None}
+        evaluate_sockets(self, b_objects, eb_props_sockets)
         print( wrapGreen("Created Bone: ") + wrapOrange(eb.name) + wrapGreen(" in ") + wrapWhite(self.bGetParentArmature().name))
 
         parent_xForm_info = get_parent_xForm_info(self)
@@ -455,29 +454,12 @@ class xFormBone(xFormNode):
         except KeyError:
             prRed("Warning: BBone start or end handle not set because of missing bone in armature.")
 
-        import bpy
         pb = self.bGetParentArmature().pose.bones[self.bObject]
-
-        if check_for_driver(self, 'Rotation Order'):
-            # this is stupid and a HACK
-            from .mantis_dataclasses import MantisSocketFiller
-            props_sockets = {"rotation_mode": MantisSocketFiller( socket_name="Rotation Order",
-                property_name="rotation_mode", default_value='XYZ',)}
-            evaluate_sockets(self, pb, props_sockets)
-        else: # This is stupid, but I did it to myself and it will be annoying to fix
-            rotation_mode = self.evaluate_input("Rotation Order")
-            if rotation_mode == "AUTO": rotation_mode = "XYZ"
-            pb.rotation_mode = rotation_mode
-            
-            
-
         pb.id_properties_clear()
         self.setup_custom_props(pb, "Custom Properties")
-        pb_props_sockets = self.gen_property_socket_map(prop_owner_group='pose_bone')
-        evaluate_sockets(self, pb, pb_props_sockets)
-
-        bone_props_socket = self.gen_property_socket_map(prop_owner_group='bone')
-        evaluate_sockets(self, pb.bone, bone_props_socket)
+        b_objects = {'edit_bone':None, 'pose_bone':pb, 'bone':pb.bone, 'DEFAULT':None}
+        props_sockets = self.gen_property_socket_map()
+        evaluate_sockets(self, b_objects, props_sockets)
 
         # and in edit mode we need to explicitly set the bbone properties
         # I don't like this HACK but there seems to be no other way...
@@ -488,8 +470,10 @@ class xFormBone(xFormNode):
 
         finish_drivers(self)
 
+        # TODO: I should not need a special case for this
         pb.custom_shape_transform = None
         pb.custom_shape = None
+        from bpy.types import Object
         if len(self.inputs["Custom Object xForm Override"].links) > 0:
             trace = trace_single_line(self, "Custom Object xForm Override")
             try:
@@ -502,7 +486,7 @@ class xFormBone(xFormNode):
                 ob = trace[0][1].bGetObject()
             except AttributeError:
                 ob=None
-            if type(ob) in [bpy.types.Object]:
+            if type(ob) in {Object}:
                 pb.custom_shape = ob
 
     def bGetObject(self, mode = 'POSE'):
@@ -524,11 +508,10 @@ class xFormBone(xFormNode):
 
 class xFormGeometryObject(xFormNode):
     '''A node representing an armature object'''
+    __slots__ = ("has_shape_keys")
+    socket_templates = xFormGeometryObjectSockets
     def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, xFormGeometryObjectSockets)
-        self.init_parameters()
-        self.set_traverse([("Relationship", "xForm Out")])
-        self.has_shape_keys = False
+        super().__init__(signature, base_tree, self.socket_templates)
         setup_custom_property_inputs_outputs(self)
 
     def bPrepare(self, bContext = None,):
@@ -614,8 +597,8 @@ class xFormGeometryObject(xFormNode):
         self.has_shape_keys = False
         # putting this in bTransformPass simply prevents it from being run more than once.
         # maybe I should do that with the rest of bPrepare, too.
-        props_sockets = self.gen_property_socket_map()
-        evaluate_sockets(self, self.bObject, props_sockets)
+        props_sockets = self.property_socket_map
+        evaluate_sockets(self, {"DEFAULT":self.bObject}, props_sockets)
         self.executed = True
 
     def bFinalize(self, bContext = None):
@@ -624,9 +607,6 @@ class xFormGeometryObject(xFormNode):
         self.bObject.matrix_world = matrix
         # 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)
 
 
@@ -635,12 +615,11 @@ class xFormGeometryObject(xFormNode):
 
 class xFormObjectInstance(xFormNode):
     """Represents an instance of an existing geometry object."""
+    __slots__ = ("has_shape_keys")
+    socket_templates = xFormGeometryObjectInstanceSockets
+
     def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, xFormGeometryObjectInstanceSockets)
-        self.init_parameters()
-        # TODO: I think this field is a leftover from a test or something. see if it can be removed.
-        self.links = {} # leave this empty for now!
-        self.set_traverse([("Relationship", "xForm Out")])
+        super().__init__(signature, base_tree, self.socket_templates)
         self.has_shape_keys = False # Shape Keys will make a dupe so this is OK
         setup_custom_property_inputs_outputs(self)
 
@@ -703,7 +682,7 @@ class xFormObjectInstance(xFormNode):
             'hide_viewport'    : ("Hide in Viewport", False),
             'hide_render'      : ("Hide in Render", False),
         }
-        evaluate_sockets(self, self.bObject, props_sockets)
+        evaluate_sockets(self, {"DEFAULT":self.bObject}, props_sockets)
         self.executed = True
 
     def bFinalize(self, bContext = None):
@@ -732,33 +711,37 @@ class xFormObjectInstance(xFormNode):
     def bGetObject(self, mode = 'POSE'):
         return self.bObject
 
+
 class xFormCurvePin(xFormNode):
     """An xForm pinned to a specific location on a curve."""
+    __slots__ = ()
+    socket_templates = xFormCurvePinSockets
     def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree,xFormCurvePinSockets)
-        self.init_parameters(additional_parameters={"Matrix":None})
+        super().__init__(signature, base_tree, self.socket_templates,
+            additional_parameters={"Matrix":None})
         setup_custom_property_inputs_outputs(self)
 
     def prep_driver_values(self, constraint):
-        from .misc_nodes import UtilityDriver, UtilitySwitch
-        for socket_name in ["Curve Pin Factor", "Forward Axis","Up Axis",]:
-            if self.inputs.get(socket_name) is None: continue # in case it has been bypassed
-            if self.inputs[socket_name].is_linked:
-                node_line, _last_socket = trace_single_line(self, socket_name)
-                driver = None
-                for other_node in node_line:
-                    if other_node.node_type == 'DRIVER':
-                        driver = other_node; break
-                if isinstance(driver, UtilityDriver):
-                    prop_amount = driver.evaluate_input("Property")
-                elif isinstance(driver, UtilitySwitch):
-                    xf=driver.GetxForm()
-                    prop_amount = xf.evaluate_input(driver.evaluate_input('Parameter'))
-                else:
-                    return
-                for template in self.socket_templates:
-                    if template.name == socket_name: break
+        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 bPrepare(self, bContext = None,):
         from bpy import data
@@ -826,18 +809,16 @@ class xFormCurvePin(xFormNode):
         c.use_curve_follow = True
         c.name = "Curve Pin"
 
-        props_sockets = self.gen_property_socket_map()
-        constraint_props_sockets = props_sockets.copy()
-        del constraint_props_sockets['name']; del constraint_props_sockets['empty_display_size']
-        del props_sockets['offset_factor']; del props_sockets['forward_axis']
-        del props_sockets['up_axis']
-        evaluate_sockets(self, c, constraint_props_sockets)
-        evaluate_sockets(self, self.bObject, props_sockets)
+        constraint_props_sockets = self.gen_property_socket_map( prop_owner_group='constraints')
+        props_sockets = self.gen_property_socket_map( prop_owner_group='object')
+        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
@@ -874,9 +855,10 @@ class xFormCurvePin(xFormNode):
 # now you get a special thank-you in Mantis instead!
 class xFormGetBone(xFormNode):
     """Represents an instance of an existing geometry object."""
+    __slots__ = ()
+    socket_templates = xFormGetBoneSockets
     def __init__(self, signature, base_tree):
-        super().__init__(signature, base_tree, xFormGetBoneSockets)
-        self.init_parameters()
+        super().__init__(signature, base_tree, self.socket_templates)
         # this is just a getter
         self.prepared=True; self.executed=True; self.execution_prepared=True
     

+ 3 - 0
xForm_nodes_ui.py

@@ -249,6 +249,9 @@ class xFormCurvePin(Node, xFormNode):
     mantis_node_class_name=bl_idname
     socket_templates = xFormCurvePinSockets
 
+    def display_update(self, parsed_tree, context):
+        object_displaty_update(self, parsed_tree, context)
+
 class xFormGetBone(Node, xFormNode):
     """"A node representing an existing bone"""
     bl_idname = "xFormGetBone"

+ 19 - 14
xForm_socket_templates.py

@@ -9,12 +9,14 @@ xFormArmatureSockets=[
             default_value='Armature', blender_property='name' ),
     RotationOrderTemplate := SockTemplate(
         name="Rotation Order", is_input=True,  bl_idname='RotationOrderSocket',
-            default_value='XYZ', blender_property='rotation_mode' ),
+            default_value='XYZ', blender_property='rotation_mode',
+            property_value_operator = "resolve_rotation_mode_auto", ),
     ObjectMatrixTemplate := SockTemplate(
         name="Matrix", is_input=True,  bl_idname='MatrixSocket',
             blender_property='matrix_world' ),
     RelationshipInTemplate := SockTemplate(
-        name="Relationship", is_input=True,  bl_idname='RelationshipSocket', ),
+        name="Relationship", is_input=True,  bl_idname='RelationshipSocket',
+        traverse_target='xForm Out' ),
     ArmatureDisplayType := SockTemplate(name='Display Type', category='Display',
         default_value='OCTAHEDRAL', blender_property='display_type',
         min_bpy_version=(4,5,0), bl_idname='EnumArmatureDisplayType',  is_input=True,),
@@ -22,7 +24,8 @@ xFormArmatureSockets=[
         name="Custom Properties", is_input=True,  bl_idname='CustomPropSocket',
             use_multi_input=True, is_animatable=False, ),
     xFormOutTemplate := SockTemplate(
-        name="xForm Out", is_input=False,  bl_idname='xFormSocket', ),
+        name="xForm Out", is_input=False,  bl_idname='xFormSocket', 
+        traverse_target='Relationship' ),
 ]
 
 xFormGeometryObjectSockets=[
@@ -35,7 +38,8 @@ xFormGeometryObjectSockets=[
                                        bl_idname='DeformerSocket', ),
     HideTemplate := SockTemplate(name="Hide in Viewport",
         is_input=True, bl_idname='HideSocket', default_value=False,
-        blender_property='hide_viewport' ),
+        blender_property='hide_viewport',
+        property_value_operator='cast_to_bool' ),
     HideRenderTemplate := SockTemplate(name="Hide in Render",
         is_input=True, bl_idname='BooleanSocket', default_value=False,
         blender_property='hide_render' ),
@@ -59,24 +63,25 @@ xFormGeometryObjectInstanceSockets=[
 ]
 
 xFormCurvePinSockets = [
-    replace(NameTemplate, default_value='Curve Pin'),
+    replace(NameTemplate, default_value='Curve Pin',  prop_owner_group='object'),
     ParentCurveTemplate := SockTemplate(
-        name="Parent Curve", is_input=True,  bl_idname='xFormSocket', ),
+        name="Parent Curve", is_input=True,  bl_idname='xFormSocket',),
     SplineIndexTemplate,
     FactorTemplate := SockTemplate(
         name="Curve Pin Factor", is_input=True,  bl_idname='FloatFactorSocket',
-        default_value=0.0, blender_property='offset_factor' ),
+        default_value=0.0, blender_property='offset_factor', prop_owner_group='constraint'),
     ForwardAxisTemplate := SockTemplate(
         name="Forward Axis", is_input=True,  bl_idname='EnumFollowPathForwardAxis',
-        default_value="FORWARD_Y", blender_property='forward_axis' ),
+        default_value="FORWARD_Y", blender_property='forward_axis', prop_owner_group='constraint' ),
     UpAxisTemplate := SockTemplate(
         name="Up Axis", is_input=True,  bl_idname='EnumUpAxis',
-        default_value="UP_Z", blender_property='up_axis' ),
+        default_value="UP_Z", blender_property='up_axis', prop_owner_group='constraint'),
     CurvePinDisplaySize := SockTemplate(
         name="Display Size", is_input=True,  bl_idname='FloatPositiveSocket',
-        default_value=0.05, blender_property='empty_display_size'),
+        default_value=0.05, blender_property='empty_display_size',
+        prop_owner_group='object'),
     CustomPropsTemplate,
-    xFormOutTemplate,
+    replace(xFormOutTemplate, traverse_target = None)
 ]
 
 xFormGetBoneSockets = [
@@ -151,10 +156,10 @@ UseTransformAroundCustomShape = SockTemplate(name="Use as Pivot", category='Disp
 from math import pi
 xFormBoneSockets = [
     replace(NameTemplate, default_value='Bone', category='always_show',
-            is_animatable=False,),
-    replace(RotationOrderTemplate, category='always_show',),
+            is_animatable=False, blender_property=None), # because we have to set the name directly at creation
+    replace(RotationOrderTemplate, category='always_show', prop_owner_group = 'pose_bone'),
     BoneMatrixTemplate := replace(ObjectMatrixTemplate, is_animatable=False,
-            blender_property='matrix', category='always_show',),
+             category='always_show', blender_property=None ,), # None because it is set at creation
     replace(RelationshipInTemplate, category='always_show', is_animatable=False,),
     # IK
     IKStretch := SockTemplate(

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor