11 次代码提交 0c37db01a3 ... 555c99e212

作者 SHA1 备注 提交日期
  Joseph Brandenburg 555c99e212 Improve Selection Sync 1 月之前
  Joseph Brandenburg e8d99c3143 cleanup 1 月之前
  Joseph Brandenburg 288d3de6c1 Fix: Don't cause errors for un-jsonable attributes 1 月之前
  Joseph Brandenburg 52310a52c7 Cleanup: Move utility files into folder 1 月之前
  Joseph Brandenburg d59721dc96 Cleanup move visualize.py into dev_helpers 1 月之前
  Joseph Brandenburg f460256de5 New Feature: Selection Syncing 1 月之前
  Joseph Brandenburg 5220714e33 Armature Node handles array inputs 1 月之前
  Joseph Brandenburg f8bd3e9789 cleanup: use better reroute tracing function 1 月之前
  Joseph Brandenburg 0d05416f41 cleanup, use correct interface for bGetObject 1 月之前
  Joseph Brandenburg 9289084226 Fix: versioning broke links when loading tree 1 月之前
  Joseph Brandenburg eef0abb75b Remove old link-tracking code 1 月之前

+ 18 - 6
__init__.py

@@ -8,13 +8,13 @@ from . import ( ops_nodegroup,
                 primitives_nodes_ui,
                 deformer_nodes_ui,
                 math_nodes_ui,
-                i_o,
                 schema_nodes_ui,
                 menu_classes,
               )
 from .ops_generate_tree import GenerateMantisTree
 
-from .utilities import prRed
+from .utilities import i_o
+from .utilities.utilities import prRed
 
 from .base_definitions import (MANTIS_VERSION_MAJOR,
                                MANTIS_VERSION_MINOR,
@@ -48,7 +48,7 @@ classes.append(MantisPreferences)
 
 from os import environ
 if environ.get("ENABLEVIS"):
-    from .visualize import MantisVisualizeNode, MantisVisualizeOutput, MantisVisualizeTree
+    from .dev_helpers.visualize import MantisVisualizeNode, MantisVisualizeOutput, MantisVisualizeTree
     classes.extend([MantisVisualizeTree, MantisVisualizeNode, MantisVisualizeOutput, ])
 
 DOERROR = False
@@ -335,8 +335,8 @@ def autoload_components(filename):
     # this should not be blocking or slow!
     print("Auto-loading components")
     from os import path as os_path
-    from .utilities import get_component_library_items, get_default_collection
-    from .i_o import do_import
+    from .utilities.utilities import get_component_library_items, get_default_collection
+    from .utilities.i_o import do_import
     from .preferences import get_bl_addon_object
     import json
     from bpy import context, data
@@ -371,6 +371,18 @@ def autoload_components(filename):
             bl_addon_object.preferences.MetaArmatureDefaultCollection)):
         context.scene.collection.children.unlink(armature_collection)
 
+# This has to be done after file load because the file load clears the timers
+@persistent
+def begin_selection_sync_timer(filename):
+    # And here I will register any timers used by Mantis.
+    from .timers import timer_update_selection_sync, ENABLED, SELECTION_POLL_RATE
+    from .preferences import get_bl_addon_object
+    bl_mantis_addon = get_bl_addon_object()
+    if bl_mantis_addon and bl_mantis_addon.preferences:
+        ENABLED = bl_mantis_addon.preferences.EnableSelectionSync
+        SELECTION_POLL_RATE = bl_mantis_addon.preferences.SelectionSyncPollRate
+        bpy.app.timers.register(timer_update_selection_sync)
+
 # I'll need to do some fiddling here when it comes time to try
 #   and make rig definitions animatable.
 @persistent
@@ -425,6 +437,7 @@ def register():
     # add the handlers
     bpy.app.handlers.depsgraph_update_pre.insert(0, update_handler)
     bpy.app.handlers.depsgraph_update_post.insert(0, execute_handler)
+    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.animation_playback_pre.insert(0, on_animation_playback_pre_handler)
@@ -434,7 +447,6 @@ def register():
     # but I am a good citizen! so my addon won't mess up yours! probably...
 
 
-
 def unregister():
     bpy.types.NODE_MT_context_menu.remove(node_context_menu_draw)
     bpy.types.NODE_MT_add.remove(node_add_menu_draw)

+ 45 - 31
base_definitions.py

@@ -5,17 +5,20 @@ from bpy.props import (BoolProperty, StringProperty, EnumProperty, CollectionPro
 from . import ops_nodegroup
 from bpy.types import NodeTree, Node, PropertyGroup, Operator, UIList, Panel
 
-from .utilities import (prRed, prGreen, prPurple, prWhite,
+from .utilities.utilities import (prRed, prGreen, prPurple, prWhite,
                               prOrange,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
                               wrapOrange,)
 
-from .utilities import get_socket_maps, relink_socket_map, do_relink
+from .utilities.utilities import get_socket_maps, relink_socket_map, do_relink
 from .mantis_dataclasses import MantisSocketTemplate
 
 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
+
 def TellClasses():
     #Why use a function to do this? Because I don't need every class to register.
     return [ MantisTree,
@@ -47,7 +50,7 @@ def fix_reroute_colors(tree):
     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 import socket_seek
+    from .utilities.utilities import find_reroute_start_socket
     from .socket_definitions import MantisSocket
     reroutes_without_color = deque()
     for n in tree.nodes:
@@ -58,7 +61,7 @@ def fix_reroute_colors(tree):
             rr = reroutes_without_color.pop()
             if rr.inputs[0].is_linked:
                 link = rr.inputs[0].links[0]
-                socket = socket_seek(link, tree.links)
+                socket = find_reroute_start_socket(rr)
                 if isinstance(socket, MantisSocket):
                     rr.socket_idname = socket.bl_idname
     except Exception as e:
@@ -94,7 +97,6 @@ class MantisTree(NodeTree):
     tree_valid:BoolProperty(default=False)
     hash:StringProperty(default='')
     do_live_update:BoolProperty(default=True) # use this to disable updates for e.g. scripts
-    num_links:IntProperty(default=-1)
     # operator settings for re-exporting the tree.
     filepath:StringProperty(default="", subtype='FILE_PATH')
     export_all_subtrees_together:BoolProperty(default=True)
@@ -114,6 +116,14 @@ class MantisTree(NodeTree):
 
     parsed_tree={}
 
+    # NOTE that these are class properties that should be serialized
+    #      to per-instance bpy properties when switching trees.
+    #      The class properties should, however, be associated to specific
+    #      trees by name so that the user can edit multiple trees at once.
+    # Selection Sync Dicts
+    node_to_object_map = {}
+    object_to_node_map = {}
+
     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):
@@ -130,7 +140,7 @@ class MantisTree(NodeTree):
         if (my_hash != self.hash) or force:
             self.hash = my_hash
             self.is_executing = True
-            from . import readtree
+            from .execution import readtree
             prGreen("Validating Tree: %s" % self.name)
             try:
                 import bpy # I am importing here so that the context passed in
@@ -177,7 +187,7 @@ class MantisTree(NodeTree):
             return
         prGreen("Executing Tree: %s" % self.name)
         self.is_executing = True
-        from . import readtree
+        from .execution import readtree
         try:
             context.scene.render.use_lock_interface = True
             readtree.execute_tree(self.parsed_tree, self, context, error_popups, profile=profile)
@@ -246,19 +256,6 @@ class MantisUINode:
         self.init_sockets(self.socket_templates)
         self.initialized = True
 
-    def insert_link(self, link):
-        if (bpy.app.version >= (4, 4, 0)):
-            return # this causes a crash due to a bug.
-        context = bpy.context
-        if context.space_data:
-            node_tree = context.space_data.path[0].node_tree
-            if node_tree.do_live_update:
-                node_tree.update_tree(context)
-                if (link.to_socket.is_linked == False):
-                    node_tree.num_links+=1
-                elif (link.to_socket.is_multi_input):
-                    node_tree.num_links+=1
-
     def init_sockets(self, socket_templates : tuple[MantisSocketTemplate]):
         for template in socket_templates:
             collection = self.outputs
@@ -482,7 +479,7 @@ def node_group_update(node, force = False):
                 node.inputs.remove(remove_me.pop())
 
 
-        from .utilities import relink_socket_map_add_socket
+        from .utilities.utilities import relink_socket_map_add_socket
 
         reorder_me_input = []; input_index = 0
         reorder_me_output = []; output_index = 0
@@ -764,7 +761,7 @@ class MantisNode:
 
     @property
     def bl_idname(self): # this and the above exist solely to maintain interface w/bpy.types.Node
-        from .utilities import get_ui_node
+        from .utilities.utilities import get_ui_node
         return get_ui_node(self.ui_signature, self.base_tree).bl_idname
     
     def reset_execution(self) -> None:
@@ -847,7 +844,7 @@ class MantisNode:
         # apply overlay variables here so this updates predictably
         if (isinstance(value, str) and
                 (self.signature, ui_socket.identifier) in self.mContext.tagged_for_overlay):
-            from .readtree import node_scan_overlays
+            from .execution.readtree import node_scan_overlays
             value = node_scan_overlays(self, ui_socket.identifier)
         # TODO: I need to make Node Groups apply overlays downstream, but since
         #       they don't update live anyway, no need just yet
@@ -885,7 +882,7 @@ class MantisNode:
             conn.reset_execution_recursive()
 
     def evaluate_input(self, input_name, index=0)  -> Any:
-        from .node_common import trace_single_line
+        from .utilities.node_common import trace_single_line
         if not (self.inputs.get(input_name)): # get the named parameter if there is no input
             return self.parameters.get(input_name) # this will return None if the parameter does not exist.
         # this trace() should give a key error if there is a problem
@@ -895,8 +892,8 @@ class MantisNode:
         return prop
 
     def fill_parameters(self, ui_node=None)  -> None:
-        from .utilities import get_ui_node
-        from .node_common import get_socket_value
+        from .utilities.utilities import get_ui_node
+        from .utilities.node_common import get_socket_value
         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
@@ -924,7 +921,7 @@ class MantisNode:
                 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 import handle_overlay_detect
+                            from .utilities.utilities import handle_overlay_detect
                             value = handle_overlay_detect(self, node_socket.identifier, value)
                         self.parameters[key] = value
                     else:
@@ -938,7 +935,7 @@ class MantisNode:
            The first argument MUST be the name of the method as a string.
         """
         if args[0] == 'call_on_all_ancestors': raise RuntimeError("Very funny!")
-        from .utilities import get_all_dependencies
+        from .utilities.utilities import get_all_dependencies
         from collections import deque
         # get all dependencies by walking backward through the tree.
         all_dependencies = get_all_dependencies(self)
@@ -956,9 +953,9 @@ class MantisNode:
         return
 
     # gets targets for constraints and deformers and should handle all cases
-    def get_target_and_subtarget(self, constraint_or_deformer, input_name = "Target"):
+    def get_target_and_subtarget(self, constraint_or_deformer, input_name = "Target", index = 0,):
         from bpy.types import PoseBone, Object, SplineIKConstraint
-        subtarget = ''; target = self.evaluate_input(input_name)
+        subtarget = ''; target = self.evaluate_input(input_name, index)
         if target:
             if not hasattr(target, "bGetObject"):
                 if hasattr(constraint_or_deformer, 'name'):
@@ -990,6 +987,23 @@ class MantisNode:
             if hasattr(constraint_or_deformer, "subtarget"):
                 constraint_or_deformer.subtarget = subtarget
 
+    # This function is for registering the objects a node creates in the global dict
+    # The goal is to have a global map of node <==> object and object <==> node
+    # so that we can do e.g. selection syncing, etc.
+    # NOTE: I'm not registering with mContext here, that happens elsewhere
+    #       but it is probably a good idea to refactor this together.
+    def bRegisterObject(self, ob_type, ob_value, sub_ob_value = None):
+        ui_signature = self.ui_signature
+        if selection_sync_node_to_object.get(ui_signature) is None:
+            selection_sync_node_to_object[ui_signature] = []
+        ob_name = ob_value.name
+        sub_ob_name = sub_ob_value.name if sub_ob_value else None
+        key = (ob_name, sub_ob_name)
+        if ob_type in ['OBJECT', 'BONE']:
+            selection_sync_object_to_node[key] = ui_signature
+            selection_sync_node_to_object[ui_signature].append( key )
+
+
 # PASSES DEFINED HERE!
     def bPrepare(self, bContext=None):
         return # This one runs BEFORE anything else
@@ -1086,7 +1100,7 @@ class NodeLink:
         self.to_socket = middle_node_in
         middle_node.outputs[middle_node_out].connect(to_node, to_socket)
         if re_init_hierarchy:
-            from .utilities import init_connections, init_dependencies
+            from .utilities.utilities import init_connections, init_dependencies
             init_connections(self.from_node)
             init_connections(middle_node)
             init_dependencies(middle_node)

+ 6 - 6
deformer_nodes.py

@@ -1,9 +1,9 @@
-from .node_common import *
+from .utilities.node_common import *
 from .xForm_nodes import xFormGeometryObject, xFormObjectInstance
 from .misc_nodes import InputExistingGeometryObject
 from .base_definitions import MantisNode
 from .mantis_dataclasses import MantisSocketTemplate
-from .utilities import (prRed, prGreen, prPurple, prWhite, prOrange,
+from .utilities.utilities import (prRed, prGreen, prPurple, prWhite, prOrange,
                         wrapRed, wrapGreen, wrapPurple, wrapWhite,
                         wrapOrange,)
 
@@ -74,7 +74,7 @@ def standard_modifier_bind(self, bContext=None, operator=None):
         # we'll only bind it if it is un-muted.
         if self.evaluate_input("Enable in Viewport") == False:
             continue
-        from .utilities import bind_modifier_operator
+        from .utilities.utilities import bind_modifier_operator
         bind_modifier_operator(d, operator)
 
 class DeformerArmature(MantisDeformerNode):
@@ -333,7 +333,7 @@ class DeformerHook(MantisDeformerNode):
         if isinstance(hook, (Bone, PoseBone)):
             driver['type']='SCRIPTED'
             driver['expression']="(((1/b)*a)+((1/b_001)*a_001)+((1/b_002)*a_002))/3"
-        from .drivers import CreateDrivers
+        from .utilities.drivers import CreateDrivers
         axes='XYZ'
         for i in range(3):
             suffix = "" if i == 0 else f"_{i:03d}" # explicit names -- 5.2 no longer uniquifies duplicates
@@ -366,7 +366,7 @@ class DeformerHook(MantisDeformerNode):
             ob=xf.bGetObject()
             if ob.type == 'CURVE':
                 spline_index = self.evaluate_input("Spline Index")
-                from .utilities import get_extracted_spline_object
+                from .utilities.utilities import get_extracted_spline_object
                 ob = get_extracted_spline_object(ob, spline_index, self.mContext)
             
             reuse = False
@@ -504,7 +504,7 @@ class DeformerMorphTargetDeform(MantisDeformerNode):
             return # nothing to do here.
         
         # at this point we make the node tree
-        from .geometry_node_graphgen import gen_morph_target_nodes
+        from .utilities.geometry_node_graphgen import gen_morph_target_nodes
         m, props_sockets = gen_morph_target_nodes(
                             self.evaluate_input("Name"),
                             xf.bGetObject(),

+ 2 - 2
deformer_nodes_ui.py

@@ -3,7 +3,7 @@ from bpy.types import NodeTree, Node, NodeSocket
 from .base_definitions import MantisUINode, DeformerNode, get_signature_from_edited_tree
 from.deformer_socket_templates import *
 
-from .utilities import (prRed, prGreen, prPurple, prWhite, prOrange,
+from .utilities.utilities import (prRed, prGreen, prPurple, prWhite, prOrange,
                         wrapRed, wrapGreen, wrapPurple, wrapWhite,
                         wrapOrange,)
 
@@ -70,7 +70,7 @@ class DeformerHook(Node, DeformerNode):
             socket.hide=not is_curve_hook
 
 
-from .utilities import get_socket_maps, relink_socket_map
+from .utilities.utilities import get_socket_maps, relink_socket_map
 
 # TODO this should probably not be in this file but intstead in Utilities or something
 # NOTE this is a re-implementation that I had to do here.

+ 7 - 7
visualize.py → dev_helpers/visualize.py

@@ -3,7 +3,7 @@
 from bpy.types import Node, NodeTree, Operator
 from bpy.props import StringProperty
 
-from .utilities import (prRed, prGreen, prPurple, prWhite,
+from ..utilities.utilities import (prRed, prGreen, prPurple, prWhite,
                               prOrange,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
                               wrapOrange,)
@@ -34,7 +34,7 @@ class MantisVisualizeNode(Node):
         return label
 
     def gen_data(self, mantis_node, mode='DEBUG_CONNECTIONS'):
-        from .utilities import get_ui_node
+        from ..utilities.utilities import get_ui_node
         if mantis_node.node_type in ['SCHEMA', 'DUMMY']:
             np=None
         elif mantis_node.ui_signature is None:
@@ -114,7 +114,7 @@ def gen_vis_node( mantis_node,
                   links,
                   omit_simple=False,
                  ):
-    from .base_definitions import array_output_types
+    from ..base_definitions import array_output_types
     # if mantis_node.node_type == 'UTILITY' and \
     #      mantis_node.execution_prepared == True:
     #         return
@@ -133,8 +133,8 @@ def gen_vis_node( mantis_node,
 
 def visualize_tree(m_nodes, base_tree, context):
     # first create a MantisVisualizeTree
-    from .readtree import check_and_add_root
-    from .utilities import trace_all_nodes_from_root
+    from ..execution.readtree import check_and_add_root
+    from ..utilities.utilities import trace_all_nodes_from_root
     import bpy
 
     base_tree.is_executing=True
@@ -215,7 +215,7 @@ def visualize_tree(m_nodes, base_tree, context):
             base_tree.is_executing=False
 
 
-from .ops_nodegroup import mantis_tree_poll_op
+from ..ops_nodegroup import mantis_tree_poll_op
 
 class MantisVisualizeOutput(Operator):
     """Mantis Visualize Output Operator"""
@@ -228,7 +228,7 @@ class MantisVisualizeOutput(Operator):
 
     def execute(self, context):
         from time import time
-        from .utilities import wrapGreen, prGreen
+        from ..utilities.utilities import wrapGreen, prGreen
 
         tree=context.space_data.path[0].node_tree
         tree.update_tree(context)

+ 26 - 26
readtree.py → execution/readtree.py

@@ -1,4 +1,4 @@
-from .utilities import prRed, prGreen, prPurple, prWhite, prOrange, \
+from ..utilities.utilities import prRed, prGreen, prPurple, prWhite, prOrange, \
                         wrapRed, wrapGreen, wrapPurple, wrapWhite, wrapOrange
 
 
@@ -23,7 +23,7 @@ from .utilities import prRed, prGreen, prPurple, prWhite, prOrange, \
 # 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
+    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
@@ -46,7 +46,7 @@ def grp_node_reroute_common(in_node, out_node, interface):
             out_link.die()
 
 def reroute_links_grp(group, all_nodes):
-    from .internal_nodes import GroupInterface
+    from ..internal_nodes import GroupInterface
     interface = GroupInterface(
         ( *group.signature, "InputInterface"),
         group.base_tree, group.ui_node, 'INPUT',)
@@ -59,7 +59,7 @@ def reroute_links_grp(group, all_nodes):
 
 def reroute_links_grpout(group_output, all_nodes):
     if (group := all_nodes.get( ( *group_output.signature[:-1],) )):
-        from .internal_nodes import GroupInterface
+        from ..internal_nodes import GroupInterface
         interface = GroupInterface(
             ( *group.signature, "OutputInterface"),
             group.base_tree, group.ui_node, 'OUTPUT',)
@@ -72,10 +72,10 @@ def reroute_links_grpout(group_output, all_nodes):
 # 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
+    from ..link_nodes import LinkInherit
     inherit_node = None
     if mantis_node.inputs["Relationship"].is_connected:
-        from .node_common import trace_single_line
+        from ..utilities.node_common import trace_single_line
         node_line, last_socket = trace_single_line(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.
@@ -105,11 +105,11 @@ def insert_lazy_parents(mantis_node):
 #                                  DATA FROM NODES                                  #
 # *** # *** # *** # *** # *** # *** # *** # *** # *** # *** # *** # *** # *** # *** #
 
-from .base_definitions import replace_types, NodeSocket
+from ..base_definitions import replace_types, NodeSocket
 
 def autogen_node(base_tree, ui_socket, signature, mContext):
     mantis_node=None
-    from .internal_nodes import AutoGenNode
+    from ..internal_nodes import AutoGenNode
     mantis_node = AutoGenNode(signature, base_tree)
     mantis_node.mContext = mContext
     mantis_node.outputs.init_sockets([ui_socket.name])
@@ -119,7 +119,7 @@ def autogen_node(base_tree, ui_socket, signature, mContext):
 # TODO: investigate whether I can set the properties in the downstream nodes directly.
 #       I am doing this in Schema Solver and it seems to work quite efficiently.
 def make_connections_to_ng_dummy(base_tree, tree_path_names, local_mantis_nodes, all_mantis_nodes, to_mantis_node):
-    from .socket_definitions import no_default_value
+    from ..socket_definitions import no_default_value
     for inp in to_mantis_node.ui_node.inputs:
         if inp.bl_idname in no_default_value:
             continue
@@ -133,7 +133,7 @@ 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 .node_common import get_socket_value
+            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)}
                 local_mantis_nodes[signature] = from_mantis_node; all_mantis_nodes[signature] = from_mantis_node
@@ -142,7 +142,7 @@ def make_connections_to_ng_dummy(base_tree, tree_path_names, local_mantis_nodes,
                 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 ):
-    from .internal_nodes import DummyNode
+    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
@@ -196,11 +196,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.
-        from .utilities import clear_reroutes
+        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)
 
-        from .utilities import link_mantis_nodes
+        from ..utilities.utilities import link_mantis_nodes
         for link in links:
             link_mantis_nodes((None, *tree_path_names), link, local_mantis_nodes)
         if current_tree == base_tree:
@@ -216,7 +216,7 @@ forbidden_key_names = ["",]
 forbidden_in_key_name = ['$',]
 def node_scan_overlays(node, socket_id,):
     interface_nodes = node.mContext.interface_nodes
-    from .utilities import get_ui_node
+    from ..utilities.utilities import get_ui_node
     ui_node = get_ui_node(node.ui_signature, node.base_tree)
     # we'll assume that the overlay tag system checked if this is linked.
     # First let's check if it is a simple input node, by checking outputs that are marked "input"
@@ -296,7 +296,7 @@ def node_scan_overlays(node, socket_id,):
     node.parameters[socket_id] = result
     return result # because sometimes I need to get the result, too
 
-from .utilities import check_and_add_root, init_connections, init_dependencies, init_schema_dependencies
+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
@@ -305,7 +305,7 @@ def is_signature_in_other_signature(parent_signature, child_signature):
     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 import get_ui_node
+    from ..utilities.utilities import get_ui_node
     np = get_ui_node(mantis_node.signature, mantis_node.base_tree)
     from .schema_solve import SchemaSolver
     solver = SchemaSolver(mantis_node, all_mantis_nodes.copy(), np, error_popups=error_popups)
@@ -347,7 +347,7 @@ schema_bl_idnames = [   "SchemaIndex",
                         "SchemaIncomingConnection",
                     ]
 
-from .utilities import get_all_dependencies
+from ..utilities.utilities import get_all_dependencies
 def get_schema_length_dependencies(node, all_nodes={}):
     """ Get a list of all dependencies for the given node's length or array properties.
         This function will also recursively search for dependencies in its sub-trees.
@@ -438,7 +438,7 @@ def insert_default_values_base_tree(base_tree, all_mantis_nodes):
             signature = ("MANTIS_AUTOGENERATED", "MANTIS_DEFAULT_ARMATURE",)
             autogen_mantis_node = all_mantis_nodes.get(signature)
             if autogen_mantis_node is None:
-                from .xForm_nodes import xFormArmature
+                from ..xForm_nodes import xFormArmature
                 autogen_mantis_node = xFormArmature(signature, base_tree)
                 autogen_mantis_node.parameters['Name']=base_tree.name+'_MANTIS_AUTOGEN'
                 autogen_mantis_node.mContext =  InputNode.mContext
@@ -457,7 +457,7 @@ def parse_tree(base_tree, error_popups=False):
     from uuid import uuid4
     base_tree.execution_id = uuid4().__str__() # set the unique id of this execution
 
-    from .base_definitions import MantisExecutionContext
+    from ..base_definitions import MantisExecutionContext
     mContext = MantisExecutionContext(base_tree=base_tree)
 
     import time
@@ -478,7 +478,7 @@ def parse_tree(base_tree, error_popups=False):
     from collections import deque
     unsolved_schema = deque()
 
-    from .base_definitions import array_output_types, GraphError
+    from ..base_definitions import array_output_types, GraphError
     for mantis_node in all_mantis_nodes.values():
         # TODO I still need to assign mContext here because of autogen_node
         # so I have to do a deeper refactor to ensure autogen nodes get this at creation time.
@@ -578,7 +578,7 @@ def parse_tree(base_tree, error_popups=False):
             continue # screen out the ui_node schema nodes, group in/out, and group placeholders
         # cleanup autogen nodes
         if mantis_node.signature[0] == "MANTIS_AUTOGENERATED" and len(mantis_node.inputs) == 0 and len(mantis_node.outputs) == 1:
-            from .base_definitions import can_remove_socket_for_autogen
+            from ..base_definitions import can_remove_socket_for_autogen
             output=list(mantis_node.outputs.values())[0]
             value=list(mantis_node.parameters.values())[0]
             # We can remove this node if it is safe to push it into the other node's socket.
@@ -606,7 +606,7 @@ def parse_tree(base_tree, error_popups=False):
     prWhite("Number of Nodes: %s" % (len(kept_mantis_node)))
     return kept_mantis_node
 
-from .utilities import switch_mode
+from ..utilities.utilities import switch_mode
 
 def execution_error_cleanup(node, exception, switch_objects = [], show_error=False ):
     from bpy import  context
@@ -644,7 +644,7 @@ def execution_error_cleanup(node, exception, switch_objects = [], show_error=Fal
     return exception
 
 def sort_execution(nodes):
-    from .node_common import GraphError
+    from ..utilities.node_common import GraphError
     from collections import deque
     xForm_pass = deque()
     execution_failed = False
@@ -689,7 +689,7 @@ def execute_tree(nodes, base_tree, context, error_popups = False, profile=False)
     mContext.execution_failed = False
 
     interface_nodes = {}
-    from .internal_nodes import GroupInterface
+    from ..internal_nodes import GroupInterface
     for node in nodes.values():
         if isinstance(node, GroupInterface):
             interface_nodes[node.signature] = node
@@ -709,7 +709,7 @@ def execute_tree(nodes, base_tree, context, error_popups = False, profile=False)
     select_me = []
     profiler = None
     if profile:
-        from .dev_helpers.profile_nodes import NodeProfiler
+        from ..dev_helpers.profile_nodes import NodeProfiler
         profiler = NodeProfiler()
         profiler.new_session(mContext.execution_id)
         sort_key = 'total'
@@ -794,7 +794,7 @@ def execute_tree(nodes, base_tree, context, error_popups = False, profile=False)
         if not execution_failed:
             prGreen(f"Executed tree of {len(sorted_nodes)} nodes in {tot_time} seconds")
         if profiler:
-            from .dev_helpers.profile_nodes import summarize_profile
+            from ..dev_helpers.profile_nodes import summarize_profile
             summarize_profile(profiler._current, pass_name='bPrepare', sort_key = sort_key)
             summarize_profile(profiler._current, pass_name='bTransformPass', sort_key = sort_key)
             summarize_profile(profiler._current, pass_name='bRelationshipPass', sort_key = sort_key)

+ 20 - 20
schema_solve.py → execution/schema_solve.py

@@ -1,11 +1,11 @@
-from .utilities import (prRed, prGreen, prPurple, prWhite,
+from ..utilities.utilities import (prRed, prGreen, prPurple, prWhite,
                               prOrange,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
                               wrapOrange,)
-from .utilities import init_connections, init_dependencies, get_link_in_out
-from .base_definitions import (SchemaUINode, custom_props_types, \
+from ..utilities.utilities import init_connections, init_dependencies, get_link_in_out
+from ..base_definitions import (SchemaUINode, custom_props_types, \
     MantisNodeGroup, SchemaGroup, replace_types, GraphError, links_sort_key)
-from .node_common import setup_custom_props_from_np
+from ..utilities.node_common import setup_custom_props_from_np
 # a class that solves Schema nodes
 from bpy.types import NodeGroupInput, NodeGroupOutput
 
@@ -80,12 +80,12 @@ class SchemaSolver:
                 mantis_node.fill_parameters(ui_node)
             # HACK to make Group Nodes work
             if ui_node.bl_idname == "NodeGroupInput":
-                from .schema_nodes import SchemaConstInput
+                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
+                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)
@@ -101,7 +101,7 @@ class SchemaSolver:
         """ Sort and store the links to/from the Schema group node."""
         for item in self.tree.interface.items_tree:
             if item.item_type == 'PANEL': continue
-            from .utilities import read_schema_type
+            from ..utilities.utilities import read_schema_type
             parent_name = read_schema_type(item)
             # just gonna try and make the 
             match parent_name:
@@ -201,7 +201,7 @@ class SchemaSolver:
             mantis_node.mContext=mContext
 
     def handle_link_from_index_input(self, index, frame_mantis_nodes, ui_link):
-        from .base_definitions import can_remove_socket_for_autogen
+        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()) ]
 
@@ -302,7 +302,7 @@ class SchemaSolver:
         to_name = get_link_in_out(ui_link)[1]
         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
+        from ..base_definitions import MantisNodeGroup, SchemaGroup
         if isinstance(to_ui_node, (SchemaGroup, MantisNodeGroup)):
             to_socket=ui_link.to_socket.identifier
         connection = from_node.outputs[incoming.from_socket].connect(node=to_node, socket=to_socket)
@@ -314,7 +314,7 @@ class SchemaSolver:
         from collections import deque
         unprepared = deque(from_node.hierarchy_dependencies)
         self.prepare_nodes(unprepared)
-        from .utilities import cap, wrap
+        from ..utilities.utilities import cap, wrap
         get_index = from_node.evaluate_input("Index", self.index) # get the most recent link
         # getting the link at self.index just saves the trouble of killing the old links
         # that are left because this node is reused in each iteration of the schema
@@ -382,7 +382,7 @@ class SchemaSolver:
         # 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
-            from .base_definitions import NodeLink
+            from ..base_definitions import NodeLink
             to_socket_name=ui_link.to_socket.name
             if to_node.node_type in ['DUMMY_SCHEMA']:
                 to_socket_name=ui_link.to_socket.identifier
@@ -450,7 +450,7 @@ class SchemaSolver:
             if not from_node:
                 from_node = self.schema_nodes[(*self.tree_path_names, from_ui_node.bl_idname)]
             to_node = outgoing.to_node
-            from .schema_nodes import SchemaIndex
+            from ..schema_nodes import SchemaIndex
             if isinstance(from_node, SchemaIndex):
                 signature = ("MANTIS_AUTOGENERATED", *self.tree_path_names[1:-1], self.index_str(),
                              ui_link.from_socket.name, ui_link.from_socket.identifier)
@@ -498,7 +498,7 @@ class SchemaSolver:
         # connected up correctly. between the output and the schema's generated nodes.
         # so seek BACK from the output node and grab the from-node that is connected to
         #  the link. then modify the ui_link to point to that node.
-        from .base_definitions import DummyLink
+        from ..base_definitions import DummyLink
         if not isinstance(ui_link, DummyLink): # make it a Dummy so i can modify it
             ui_link = DummyLink(ui_link.from_socket, ui_link.to_socket,
                                 multi_input_sort_id=ui_link.multi_input_sort_id)
@@ -562,7 +562,7 @@ class SchemaSolver:
              This function also adds to held_links to pass data between iterations.
         """
 
-        from .schema_nodes_ui import (SchemaIndex,
+        from ..schema_nodes_ui import (SchemaIndex,
                                         SchemaArrayInput,
                                         SchemaArrayInputGet,
                                         SchemaArrayInputAll,
@@ -571,8 +571,8 @@ class SchemaSolver:
                                         SchemaConstOutput,
                                         SchemaOutgoingConnection,
                                         SchemaIncomingConnection,)
-        from .utilities import clear_reroutes, link_mantis_nodes
-        from .base_definitions import array_output_types
+        from ..utilities.utilities import clear_reroutes, link_mantis_nodes
+        from ..base_definitions import array_output_types
         self.set_index_strings()
         frame_mantis_nodes = {}
 
@@ -660,7 +660,7 @@ class SchemaSolver:
                     {}, # just pass an empty dict, this argument is not needed in this context
                     self.all_nodes,
                     node)
-                from .utilities import init_schema_dependencies
+                from ..utilities.utilities import init_schema_dependencies
                 init_schema_dependencies(node, self.all_nodes)
             else:
                 init_dependencies(node) # it is hard to overstate how important this single line of code is
@@ -707,7 +707,7 @@ class SchemaSolver:
                 self.handle_link_to_constant_output(frame_mantis_nodes, self.index, ui_link,  to_ui_node)
             if isinstance(to_ui_node, SchemaArrayOutput):
                 if from_node.bl_idname in array_output_types:
-                    from .base_definitions import DummyLink
+                    from ..base_definitions import DummyLink
                     for l in from_node.rerouted:
                         new_link = DummyLink(
                             from_socket=l.from_node.outputs[l.from_socket],
@@ -743,7 +743,7 @@ class SchemaSolver:
 
 
     def finalize(self, mantis_nodes_in_frame):
-        from .schema_nodes_ui import (SchemaOutgoingConnection,)
+        from ..schema_nodes_ui import (SchemaOutgoingConnection,)
         for i in range(len(self.held_links)):
             link = self.held_links.pop()
             to_np = link.to_socket.node; from_np = link.from_socket.node
@@ -797,7 +797,7 @@ class SchemaSolver:
 
     def solve(self):
         if self.solve_length < 1:
-            from .base_definitions import GraphError
+            from ..base_definitions import GraphError
             for o in self.node.outputs:
                 if o.is_linked:
                     raise GraphError(f"ERROR: Schema {self} has a length"

+ 1 - 1
internal_nodes.py

@@ -1,4 +1,4 @@
-from .node_common import *
+from .utilities.node_common import *
 from bpy.types import Node
 from .base_definitions import MantisNode, MantisSocketTemplate
 from uuid import uuid4

+ 9 - 11
link_nodes.py

@@ -1,4 +1,4 @@
-from .node_common import *
+from .utilities.node_common import *
 from bpy.types import Bone, NodeTree
 from .base_definitions import MantisNode, GraphError, FLOAT_EPSILON
 from .link_socket_templates import *
@@ -559,7 +559,7 @@ class LinkInverseKinematics(MantisLinkNode):
     # not very efficient but it's OK
     def set_pole_angle(self, constraint, angle: float) -> None:
         from math import pi
-        from .utilities import wrap
+        from .utilities.utilities import wrap
         constraint.pole_angle = wrap(-pi, pi, angle)
 
     def calc_pole_angle_pre(self, c, ik_bone):
@@ -592,7 +592,7 @@ 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
         ik_axis = (end_effector-start_effector).normalized()
-        from .utilities import project_point_to_plane
+        from .utilities.utilities import project_point_to_plane
         pole_planar_projection = project_point_to_plane(pole_location, start_effector, ik_axis)
         # this planar projection is necessary because the IK axis is different than the base_bone's y axis
         planar_projection = project_point_to_plane(knee_location, start_effector, ik_axis)
@@ -777,7 +777,7 @@ class LinkDrivenParameter(MantisLinkNode):
         driver = self.evaluate_input("Value")
         try:
             for i, val in enumerate(self.parameters["Value"]):
-                from .drivers import MantisDriver
+                from .utilities.drivers import MantisDriver
                 if isinstance(val, MantisDriver):
                     driver["ind"] = i
                     val = driver
@@ -807,7 +807,7 @@ class LinkArmature(MantisLinkNode):
                 c.name = constraint_name
             self.bObject.append(c)
             # get number of targets
-            num_targets = len( list(self.inputs.values())[6:] )//2
+            num_targets = len( self.inputs['Targets'].links )
 
             props_sockets = self.gen_property_socket_map()
             # TODO: currently this will only work if the constraint bones
@@ -817,14 +817,12 @@ class LinkArmature(MantisLinkNode):
             targets_weights = {}
             for i in range(num_targets):
                 target = c.targets.new()
-                target_input_name = list(self.inputs.keys())[i*2+6  ]
-                weight_input_name = list(self.inputs.keys())[i*2+6+1]
-                self.get_target_and_subtarget(target, target_input_name)
-                weight_value=self.evaluate_input(weight_input_name)
+                self.get_target_and_subtarget(target, 'Targets', i)
+                weight_value=self.evaluate_input("Weights", i)
                 if not isinstance(weight_value, float):
                     weight_value=0
                 targets_weights[i]=weight_value
-                props_sockets["targets[%d].weight" % i] = (weight_input_name, 0)
+                props_sockets["targets[%d].weight" % i] = ("Weights", i)
             evaluate_sockets(self, c, props_sockets)
             for target, value in targets_weights.items():
                 c.targets[target].weight=value
@@ -854,7 +852,7 @@ class LinkSplineIK(MantisLinkNode):
             for other_node in node_line: # trace and get the input
                 if other_node.node_type == 'XFORM':
                     proto_curve = other_node.bGetObject(); break
-            from .utilities import get_extracted_spline_object
+            from .utilities.utilities import get_extracted_spline_object
             curve = get_extracted_spline_object(proto_curve, spline_index, self.mContext)
             # link it to the view layer
             if (curve.name not in bContext.view_layer.active_layer_collection.collection.objects):

+ 1 - 1
link_nodes_ui.py

@@ -1,7 +1,7 @@
 import bpy
 from bpy.types import NodeTree, Node, NodeSocket
 from .base_definitions import LinkNode, GraphError
-from .utilities import (prRed, prGreen, prPurple, prWhite,
+from .utilities.utilities import (prRed, prGreen, prPurple, prWhite,
                               prOrange,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
                               wrapOrange,)

+ 1 - 1
math_nodes.py

@@ -1,4 +1,4 @@
-from .node_common import *
+from .utilities.node_common import *
 from .base_definitions import MantisNode
 from .math_nodes_socket_templates import *
 

+ 1 - 1
math_nodes_ui.py

@@ -1,7 +1,7 @@
 import bpy
 from .base_definitions import MantisMathUINode
 from bpy.types import Node
-from .utilities import (prRed, prGreen, prPurple, prWhite,
+from .utilities.utilities import (prRed, prGreen, prPurple, prWhite,
                               prOrange,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
                               wrapOrange,)

+ 5 - 2
menu_classes.py

@@ -83,7 +83,7 @@ class MantisAddArmature(Menu):
             components_path = bl_mantis_addon.preferences.ComponentsLibraryFolder
             # now we're clear to do the menu function
             layout = self.layout
-            from .utilities import get_component_library_items
+            from .utilities.utilities import get_component_library_items
             component_items = get_component_library_items()
             for component_item in component_items:
                 # how to "EXEC_DEFAULT" here?
@@ -125,4 +125,7 @@ class MantisActiveTreePanel(Panel):
         layout.operator("mantis.execute_node_tree")
         layout.operator("mantis.force_display_update", text='Force Display Update')
         layout.operator("mantis.import_from_component_library")
-        
+        #
+        from .preferences import get_bl_addon_object
+        bl_mantis_addon = get_bl_addon_object()
+        layout.prop(bl_mantis_addon.preferences, "EnableSelectionSync", text='Sync Selection',)

+ 26 - 25
misc_nodes.py

@@ -1,4 +1,4 @@
-from .node_common import *
+from .utilities.node_common import *
 from .base_definitions import MantisNode, NodeSocket, FLOAT_EPSILON
 from .xForm_nodes import xFormArmature, xFormBone
 from .misc_nodes_socket_templates import *
@@ -93,7 +93,7 @@ def get_mesh_from_curve(curve_name : str, execution_id : str, bContext, ribbon=T
         from bpy import data
         curve = data.objects.get(curve_name)
         assert curve.type == 'CURVE', f"ERROR: object is not a curve: {curve_name}"
-        from .utilities import mesh_from_curve
+        from .utilities.utilities import mesh_from_curve
         curve_type='ribbon' if ribbon else 'wire'
         m_name = curve_name+'.'+str(hash(curve_name+'.'+curve_type+'.'+execution_id))
         if not (m := data.meshes.get(m_name)):
@@ -289,7 +289,7 @@ class InputThemeBoneColorSets(SimpleInputNode):
 
     def fill_parameters(self, ui_node=None):
         if not ui_node:
-            from .utilities import get_ui_node
+            from .utilities.utilities import get_ui_node
             ui_node = get_ui_node(self.ui_signature, self.base_tree)
         for i in range(20):
             self.parameters[f"Color {str(i).zfill(2)}"] = ui_node.outputs[i].default_value
@@ -302,7 +302,7 @@ class InputColorSetPallete(SimpleInputNode):
 
     def fill_parameters(self, ui_node=None):
         if not ui_node:
-            from .utilities import get_ui_node
+            from .utilities.utilities import get_ui_node
             ui_node = get_ui_node(self.ui_signature, self.base_tree)
         from .mantis_dataclasses import MantisSocketTemplate
         outputs = []
@@ -384,7 +384,7 @@ class UtilityMatrixFromCurve(MantisNode):
         else:
             if bContext is None: bContext = bpy.context # is this wise?
             m = get_mesh_from_curve(curve.name, self.base_tree.execution_id, bContext)
-            from .utilities import data_from_ribbon_mesh
+            from .utilities.utilities import data_from_ribbon_mesh
             #
             num_divisions = self.evaluate_input("Total Divisions")
             if num_divisions <= 0:
@@ -411,7 +411,7 @@ class UtilityMatrixFromCurve(MantisNode):
                 raise RuntimeError(f"Failed to read the curve {curve.name}.")
             normal=data[spline_index][2][0]
             # make sure the normal is perpendicular to the tail
-            from .utilities import make_perpendicular
+            from .utilities.utilities import make_perpendicular
             normal = make_perpendicular(axis, normal)
             mat = matrix_from_head_tail(head, tail, normal)
             # this is in world space... let's just convert it back
@@ -447,7 +447,7 @@ class UtilityPointFromCurve(MantisNode):
         else:
             if bContext is None: bContext = bpy.context # is this wise?
             m = get_mesh_from_curve(curve.name, self.base_tree.execution_id, bContext)
-            from .utilities import data_from_ribbon_mesh
+            from .utilities.utilities import data_from_ribbon_mesh
             #
             num_divisions = 1
             spline_index = self.evaluate_input("Spline Index")
@@ -488,7 +488,7 @@ class UtilityMatricesFromCurve(MantisNode):
         else:
             if bContext is None: bContext = bpy.context # is this wise?
             mesh = get_mesh_from_curve(curve.name, self.base_tree.execution_id, bContext)
-            from .utilities import data_from_ribbon_mesh
+            from .utilities.utilities import data_from_ribbon_mesh
             num_divisions = self.evaluate_input("Total Divisions")
             spline_index = self.evaluate_input("Spline Index")
             splines_factors = [ [] for i in range (spline_index)]
@@ -496,7 +496,7 @@ class UtilityMatricesFromCurve(MantisNode):
             splines_factors.append(factors)
             data = data_from_ribbon_mesh(mesh, splines_factors, curve.matrix_world)
             # [spline_index][points,tangents,normals][datapoint_index]
-            from .utilities import make_perpendicular
+            from .utilities.utilities import make_perpendicular
             matrices=[]
             for i in range(num_divisions):
                 if data[spline_index][1][i] < FLOAT_EPSILON: # radius is None:
@@ -577,7 +577,7 @@ class UtilityMatrixFromCurveSegment(MantisNode):
         else:
             if bContext is None: bContext = bpy.context # is this wise?
             m = get_mesh_from_curve(curve.name, self.base_tree.execution_id, bContext)
-            from .utilities import data_from_ribbon_mesh
+            from .utilities.utilities import data_from_ribbon_mesh
             # this section is dumb, but it is because the data_from_ribbon_mesh
             #  function is designed to pull data from many splines at once (for optimization)
             #  so we have to give it empty splines for each one we skip.
@@ -608,7 +608,7 @@ class UtilityMatrixFromCurveSegment(MantisNode):
             axis = (tail-head).normalized()
             normal=data[spline_index][2][segment_index]
             # make sure the normal is perpendicular to the tail
-            from .utilities import make_perpendicular
+            from .utilities.utilities import make_perpendicular
             normal = make_perpendicular(axis, normal)
             m = matrix_from_head_tail(head, tail, normal)
             m.translation = head - curve.location
@@ -663,7 +663,7 @@ class UtilityGetNearestFactorOnCurve(MantisNode):
                                     self.base_tree.execution_id,
                                     bContext, ribbon=False)
             # this is confusing but I am not re-writing these old functions
-            from .utilities import FindNearestPointOnWireMesh as nearest_point
+            from .utilities.utilities import FindNearestPointOnWireMesh as nearest_point
             spline_index = self.evaluate_input("Spline Index")
             ref_pt = self.evaluate_input("Reference Point")
             splines_points = [ [] for i in range (spline_index)]
@@ -927,7 +927,7 @@ class UtilityDriver(MantisNode):
 
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree, DriverSockets)
-        from .drivers import MantisDriver
+        from .utilities.drivers import MantisDriver
         additional_parameters = {
           "Driver":MantisDriver(),
         }
@@ -938,13 +938,13 @@ class UtilityDriver(MantisNode):
 
     def reset_execution(self):
         super().reset_execution()
-        from .drivers import MantisDriver
+        from .utilities.drivers import MantisDriver
         self.parameters["Driver"]=MantisDriver()
         self.prepared=True
 
     def bRelationshipPass(self, bContext = None,):
         prepare_parameters(self)
-        from .drivers import MantisDriver
+        from .utilities.drivers import MantisDriver
         #prPurple("Executing Driver Node")
         my_vars = []
         keys = self.evaluate_input("fCurve")
@@ -982,7 +982,7 @@ class UtilitySwitch(MantisNode):
 
     def __init__(self, signature, base_tree):
         super().__init__(signature, base_tree, SwitchSockets)
-        from .drivers import MantisDriver
+        from .utilities.drivers import MantisDriver
         additional_parameters = {
           "Driver":MantisDriver(),
         }
@@ -1008,7 +1008,7 @@ class UtilitySwitch(MantisNode):
 
     def reset_execution(self):
         super().reset_execution()
-        from .drivers import MantisDriver
+        from .utilities.drivers import MantisDriver
         self.parameters["Driver"]=MantisDriver()
         self.prepared=True
 
@@ -1019,7 +1019,7 @@ class UtilitySwitch(MantisNode):
         if xForm : xForm = xForm.bGetObject()
         if not xForm:
             raise RuntimeError("Could not evaluate xForm for %s" % self)
-        from .drivers import MantisDriver
+        from .utilities.drivers import MantisDriver
         my_driver ={ "owner" : None,
                      "prop"  : None, # will be filled out in the node that uses the driver
                      "ind"   : -1, # same here
@@ -1155,7 +1155,7 @@ class InputWidget(MantisNode):
         widgets_path = bl_mantis_addon.preferences.WidgetsLibraryFolder
         path = widgets_path+path # this guards the widgets root so the end-user
         #  can easily change the widgets directory without breaking things
-        from .utilities import get_default_collection
+        from .utilities.utilities import get_default_collection
         collection = get_default_collection(collection_type='WIDGET')
         file_name = os_path.split(path)[-1]
         obj_name = os_path.splitext(file_name)[0]
@@ -1181,7 +1181,7 @@ class InputWidget(MantisNode):
                 if len(self.bObject.modifiers) > 1 and self.bObject.modifiers[-1].name == "Simple Flip":
                     do_mirror=False
         else:
-            from .utilities import import_object_from_file
+            from .utilities.utilities import import_object_from_file
             self.bObject = import_object_from_file(path)
             if any(axes_flipped) or add_scale_modifier:
                 self.bObject = self.bObject.copy()
@@ -1193,7 +1193,7 @@ class InputWidget(MantisNode):
                 scale_modifier = self.bObject.modifiers.new("Scale Object Data", type='NODES')
             ng = data.node_groups.get("Scale Object Data")
             if ng is None:
-                from .geometry_node_graphgen import gen_scale_object_data_modifier
+                from .utilities.geometry_node_graphgen import gen_scale_object_data_modifier
                 ng = gen_scale_object_data_modifier()
             scale_modifier.node_group = ng
             scale_modifier['Socket_2']=scaling
@@ -1203,7 +1203,7 @@ class InputWidget(MantisNode):
                 flip_modifier = self.bObject.modifiers.new("Simple Flip", type="NODES")
             ng = data.node_groups.get("Simple Flip")
             if ng is None:
-                from .geometry_node_graphgen import gen_simple_flip_modifier
+                from .utilities.geometry_node_graphgen import gen_simple_flip_modifier
                 ng = gen_simple_flip_modifier()
             flip_modifier.node_group = ng
             # this changed at some point in Blender's versions ehhh
@@ -1236,6 +1236,7 @@ class InputExistingGeometryObject(MantisNode):
         if ob is None and name:
             prRed(f"No object found with name {name} in {self}")
         self.bObject=ob
+        self.bRegisterObject('OBJECT', ob)
         self.prepared, self.executed = True, True
 
     def bGetObject(self, mode=''):
@@ -1280,7 +1281,7 @@ class UtilityDeclareCollections(MantisNode):
 
     def fill_parameters(self, ui_node=None):
         if ui_node is None:
-            from .utilities import get_ui_node
+            from .utilities.utilities import get_ui_node
             ui_node = get_ui_node(self.ui_signature, self.base_tree)
         templates=[]
         for out in ui_node.outputs:
@@ -1609,7 +1610,7 @@ class UtilityMatrixAlignRoll(MantisNode):
                                 " because the alignment vector is zero.")
         input=self.evaluate_input('Matrix').copy()
         y_axis= input.to_3x3().transposed()[1]
-        from .utilities import project_point_to_plane
+        from .utilities.utilities import project_point_to_plane
         projected=project_point_to_plane(
             align_axis.normalized(), Vector((0,0,0)), y_axis).normalized()
         # now that we have the projected vector, transform the points from
@@ -1684,7 +1685,7 @@ class UtilityArrayGet(MantisNode):
             oob   = self.evaluate_input("OoB Behaviour")
             index = self.evaluate_input("Index")
 
-            from .utilities import cap, wrap
+            from .utilities.utilities import cap, wrap
             # we must assume that the array has sent the correct number of links
             if oob == 'WRAP':
                 index = wrap(0, len(self.inputs['Array'].links), index)

+ 2 - 2
misc_nodes_ui.py

@@ -3,7 +3,7 @@ from bpy.types import Node
 from .base_definitions import MantisMiscNode, get_signature_from_edited_tree
 from .misc_nodes_socket_templates import *
 
-from .utilities import (prRed, prGreen, prPurple, prWhite,
+from .utilities.utilities import (prRed, prGreen, prPurple, prWhite,
                               prOrange,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
                               wrapOrange,)
@@ -578,7 +578,7 @@ class UtilityDeclareCollections(Node, MantisMiscNode):
     collection_declarations : bpy.props.StringProperty(default="")
 
     def update_interface(self):
-        from .utilities import get_socket_maps, do_relink
+        from .utilities.utilities import get_socket_maps, do_relink
         # we need to do dynamic stuff here like with interfaces
         socket_map_out = get_socket_maps(self)[1]
         self.outputs.clear()

+ 2 - 2
ops_generate_tree.py

@@ -1,6 +1,6 @@
 from bpy.types import Operator
 import bpy
-from .utilities import (prRed, prGreen, prPurple, prWhite,
+from .utilities.utilities import (prRed, prGreen, prPurple, prWhite,
                               prOrange,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
                               wrapOrange,)
@@ -1093,7 +1093,7 @@ class GenerateMantisTree(Operator):
                     print(s.getvalue())
             else:
                 do_generate_graph(context, node_tree, include_links_and_deforms =  self.include_links_and_deforms, generate_children=self.generate_children)
-            from .utilities import SugiyamaGraph
+            from .utilities.utilities import SugiyamaGraph
             try: # the graph gen code has selected the nodes to sort.
                 SugiyamaGraph(node_tree, 16)
             except ImportError: # if for some reason Sugiyama isn't available

+ 6 - 309
ops_nodegroup.py

@@ -2,13 +2,11 @@ import bpy
 from bpy.types import Operator
 from mathutils import Vector
 
-from .utilities import (prRed, prGreen, prPurple, prWhite,
+from .utilities.utilities import (prRed, prGreen, prPurple, prWhite,
                               prOrange,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
                               wrapOrange,)
 
-
-
 def mantis_tree_poll_op(context):
     space = context.space_data
     if hasattr(space, "node_tree"):
@@ -50,7 +48,7 @@ class MantisGroupNodes(Operator):
             for path_item in context.space_data.path:
                 path_item.node_tree.is_exporting = True
 
-            from .i_o import export_to_json, do_import
+            from .utilities.i_o import export_to_json, do_import
             from random import random
             grp_name = "".join([chr(int(random()*30)+35) for i in range(20)])
             trees=[base_tree]
@@ -171,7 +169,7 @@ class MantisEditGroup(Operator):
                     active.is_updating = False
                 # call update to force the node group to check if its tree has changed
                 # now we need to loop through the tree and update all node groups of this type.
-                from .utilities import get_all_nodes_of_type
+                from .utilities.utilities import get_all_nodes_of_type
                 for g in get_all_nodes_of_type(base_tree, "MantisNodeGroup") + \
                          get_all_nodes_of_type(base_tree, "MantisSchemaGroup"):
                     if g.node_tree == active.node_tree:
@@ -273,7 +271,7 @@ class ExecuteNodeTree(Operator):
 
     def execute(self, context):
         from time import time
-        from .utilities import wrapGreen
+        from .utilities.utilities import wrapGreen
 
         tree=context.space_data.path[0].node_tree
 
@@ -306,7 +304,6 @@ class ExecuteNodeTree(Operator):
         prGreen("Finished executing tree in %f seconds" % (time() - start_time))
         return {"FINISHED"}
 
-
 class SelectNodesOfType(Operator):
     """Selects all nodes of same type as active node."""
     bl_idname = "mantis.select_nodes_of_type"
@@ -329,7 +326,6 @@ class SelectNodesOfType(Operator):
 
         return {"FINISHED"}
 
-
 def get_parent_tree_interface_enum(operator, context):
     ret = []; i = -1
     tree = bpy.data.node_groups[operator.tree_invoked]
@@ -425,7 +421,6 @@ class ConnectNodeToInput(Operator):
             t.is_executing = False
         return {"FINISHED"}
 
-
 class QueryNodeSockets(Operator):
     """Utility Operator for querying the data in a socket"""
     bl_idname = "mantis.query_sockets"
@@ -478,7 +473,6 @@ class ForceDisplayUpdate(Operator):
         base_tree.display_update(context)
         return {"FINISHED"}
 
-
 class CleanUpNodeGraph(bpy.types.Operator):
     """Clean Up Node Graph"""
     bl_idname = "mantis.nodes_cleanup"
@@ -494,12 +488,10 @@ class CleanUpNodeGraph(bpy.types.Operator):
 
     def execute(self, context):
         base_tree=context.space_data.path[-1].node_tree
-        from .utilities import SugiyamaGraph
+        from .utilities.utilities import SugiyamaGraph
         SugiyamaGraph(base_tree, 12)
         return {'FINISHED'}
 
-
-
 class MantisMuteNode(Operator):
     """Mantis Test Operator"""
     bl_idname = "mantis.mute_node"
@@ -528,254 +520,6 @@ class MantisMuteNode(Operator):
                 hide.default_value = node.mute
         return {"FINISHED"}
 
-ePropertyType =(
-        ('BOOL'  , "Boolean", "Boolean", 0),
-        ('INT'   , "Integer", "Integer", 1),
-        ('FLOAT' , "Float"  , "Float"  , 2),
-        ('VECTOR', "Vector" , "Vector" , 3),
-        ('STRING', "String" , "String" , 4),
-        #('ENUM'  , "Enum"   , "Enum"   , 5),
-    )
-
-
-from .base_definitions import xFormNode
-
-
-class AddCustomProperty(bpy.types.Operator):
-    """Add Custom Property to xForm Node"""
-    bl_idname = "mantis.add_custom_property"
-    bl_label = "Add Custom Property"
-    bl_options = {'REGISTER', 'UNDO'}
-
-
-    prop_type : bpy.props.EnumProperty(
-        items=ePropertyType,
-        name="New Property Type",
-        description="Type of data for new Property",
-        default = 'BOOL',)
-    prop_name  : bpy.props.StringProperty(default='Prop')
-
-    min:bpy.props.FloatProperty(default = 0)
-    max:bpy.props.FloatProperty(default = 1)
-    soft_min:bpy.props.FloatProperty(default = 0)
-    soft_max:bpy.props.FloatProperty(default = 1)
-    description:bpy.props.StringProperty(default = "")
-
-    tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
-    node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
-
-    @classmethod
-    def poll(cls, context):
-        return True #( hasattr(context, 'node') )
-
-    def invoke(self, context, event):
-        self.tree_invoked = context.node.id_data.name
-        self.node_invoked = context.node.name
-        wm = context.window_manager
-        return wm.invoke_props_dialog(self)
-
-    def execute(self, context):
-        n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
-        # For whatever reason, context.node doesn't exist anymore
-        #   (probably because I use a window to execute)
-        # so as a sort of dumb workaround I am saving it to a hidden
-        #  property of the operator... it works but Blender complains.
-        socktype = ''
-        if not (self.prop_name):
-            self.report({'ERROR_INVALID_INPUT'}, "Must name the property.")
-            return {'CANCELLED'}
-        if self.prop_type == 'BOOL':
-            socktype = 'ParameterBoolSocket'
-        if self.prop_type == 'INT':
-            socktype = 'ParameterIntSocket'
-        if self.prop_type == 'FLOAT':
-            socktype = 'ParameterFloatSocket'
-        if self.prop_type == 'VECTOR':
-            socktype = 'ParameterVectorSocket'
-        if self.prop_type == 'STRING':
-            socktype = 'ParameterStringSocket'
-        #if self.prop_type == 'ENUM':
-        #    sock_type = 'ParameterStringSocket'
-        if (s := n.inputs.get(self.prop_name)):
-            try:
-                number = int(self.prop_name[-3:])
-                # see if it has a number
-                number+=1
-                self.prop_name = self.prop_name[:-3] + str(number).zfill(3)
-            except ValueError:
-                self.prop_name+='.001'
-                # WRONG # HACK # TODO # BUG #
-        new_prop = n.inputs.new( socktype, self.prop_name)
-        if self.prop_type in ['INT','FLOAT']:
-            new_prop.min = self.min
-            new_prop.max = self.max
-            new_prop.soft_min = self.soft_min
-            new_prop.soft_max = self.soft_max
-        new_prop.description = self.description
-        # now do the output
-        n.outputs.new( socktype, self.prop_name)
-        tag_mantis_node_for_update(context, n)
-        return {'FINISHED'}
-
-def main_get_existing_custom_properties(operator, context):
-    ret = []; i = -1
-    n = bpy.data.node_groups[operator.tree_invoked].nodes[operator.node_invoked]
-    for inp in n.inputs:
-        if 'Parameter' in inp.bl_idname:
-            ret.append( (inp.identifier, inp.name, "Custom Property to Modify", i := i + 1), )
-    return ret
-
-
-class EditCustomProperty(bpy.types.Operator):
-    """Edit Custom Property"""
-    bl_idname = "mantis.edit_custom_property"
-    bl_label = "Edit Custom Property"
-    bl_options = {'REGISTER', 'UNDO'}
-
-    def get_existing_custom_properties(self, context):
-        return main_get_existing_custom_properties(self, context)
-
-    prop_edit : bpy.props.EnumProperty(
-        items=get_existing_custom_properties,
-        name="Property to Edit?",
-        description="Select which property to edit",)
-    prop_type : bpy.props.EnumProperty(
-        items=ePropertyType,
-        name="New Property Type",
-        description="Type of data for new Property",
-        default = 'BOOL',)
-    prop_name  : bpy.props.StringProperty(default='Prop')
-    min:bpy.props.FloatProperty(default = 0)
-    max:bpy.props.FloatProperty(default = 1)
-    soft_min:bpy.props.FloatProperty(default = 0)
-    soft_max:bpy.props.FloatProperty(default = 1)
-    description:bpy.props.StringProperty(default = "") # TODO: use getters to fill these automatically
-
-    tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
-    node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
-
-    @classmethod
-    def poll(cls, context):
-        return True #( hasattr(context, 'node') )
-
-    def invoke(self, context, event):
-        self.tree_invoked = context.node.id_data.name
-        self.node_invoked = context.node.name
-        wm = context.window_manager
-        return wm.invoke_props_dialog(self)
-
-    def execute(self, context):
-        n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
-        prop = n.inputs.get( self.prop_edit )
-        if prop:
-            prop.name = self.prop_name
-            if (s := n.inputs.get(self.prop_edit)):
-                if self.prop_type in ['INT','FLOAT']:
-                    prop.min = self.min
-                    prop.max = self.max
-                    prop.soft_min = self.soft_min
-                    prop.soft_max = self.soft_max
-                prop.description = self.description
-            tag_mantis_node_for_update(context, n)
-            return {'FINISHED'}
-        else:
-            self.report({'ERROR_INVALID_INPUT'}, "Cannot edit a property that does not exist.")
-
-
-
-class RemoveCustomProperty(bpy.types.Operator):
-    """Remove a Custom Property from an xForm Node"""
-    bl_idname = "mantis.remove_custom_property"
-    bl_label = "Remove Custom Property"
-    bl_options = {'REGISTER', 'UNDO'}
-
-    def get_existing_custom_properties(self, context):
-        return main_get_existing_custom_properties(self, context)
-
-    prop_remove : bpy.props.EnumProperty(
-        items=get_existing_custom_properties,
-        name="Property to remove?",
-        description="Select which property to remove",)
-    tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
-    node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
-
-    @classmethod
-    def poll(cls, context):
-        return True #(hasattr(context, 'active_node') )
-
-    def invoke(self, context, event):
-        self.tree_invoked = context.node.id_data.name
-        self.node_invoked = context.node.name
-        t = context.node.id_data
-        # HACK the props dialog makes this necesary
-        #  because context.node only exists during the event that
-        #  was created by clicking on the node.
-        t.nodes.active = context.node # HACK
-        context.node.select = True # HACK
-        # I need this bc of the callback for the enum property.
-        #  for whatever reason I can't use tree_invoked there
-        wm = context.window_manager
-        return wm.invoke_props_dialog(self)
-
-    def execute(self, context):
-        n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
-        # For whatever reason, context.node doesn't exist anymore
-        #   (probably because I use a window to execute)
-        # so as a sort of dumb workaround I am saving it to a hidden
-        #  property of the operator... it works.
-        for i, inp in enumerate(n.inputs):
-            if inp.identifier == self.prop_remove:
-                break
-        else:
-            self.report({'ERROR'}, "Input not found")
-            return {'CANCELLED'}
-        # it's possible that the output property's identifier isn't the
-        #   exact same... but I don' care. Shouldn't ever happen. TODO
-        for j, out in enumerate(n.outputs):
-            if out.identifier == self.prop_remove:
-                break
-        else:
-            self.report({'ERROR'}, "Output not found")
-            raise RuntimeError("This should not happen!")
-        n.inputs.remove ( n.inputs [i] )
-        n.outputs.remove( n.outputs[j] )
-        tag_mantis_node_for_update(context, n)
-        return {'FINISHED'}
-
-
-
-# SIMPLE node operators...
-# May rewrite these in a more generic way later
-class FcurveAddKeyframeInput(bpy.types.Operator):
-    """Add a keyframe input to the fCurve node"""
-    bl_idname = "mantis.fcurve_node_add_kf"
-    bl_label = "Add Keyframe"
-    bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
-
-    @classmethod
-    def poll(cls, context):
-        return (hasattr(context, 'active_node') )
-
-    def execute(self, context):
-        num_keys = len( context.node.inputs)-1
-        context.node.inputs.new("KeyframeSocket", "Keyframe."+str(num_keys).zfill(3))
-        return {'FINISHED'}
-
-class FcurveRemoveKeyframeInput(bpy.types.Operator):
-    """Remove a keyframe input from the fCurve node"""
-    bl_idname = "mantis.fcurve_node_remove_kf"
-    bl_label = "Remove Keyframe"
-    bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
-
-    @classmethod
-    def poll(cls, context):
-        return (hasattr(context, 'active_node') )
-
-    def execute(self, context):
-        n = context.node
-        n.inputs.remove(n.inputs[-1])
-        return {'FINISHED'}
-
 class DriverAddDriverVariableInput(bpy.types.Operator):
     """Add a Driver Variable input to the Driver node"""
     bl_idname = "mantis.driver_node_add_variable"
@@ -791,7 +535,6 @@ class DriverAddDriverVariableInput(bpy.types.Operator):
         context.node.inputs.new("DriverVariableSocket", chr(i))
         return {'FINISHED'}
 
-
 class DriverRemoveDriverVariableInput(bpy.types.Operator):
     """Remove a DriverVariable input from the active Driver node"""
     bl_idname = "mantis.driver_node_remove_variable"
@@ -807,39 +550,6 @@ class DriverRemoveDriverVariableInput(bpy.types.Operator):
         n.inputs.remove(n.inputs[-1])
         return {'FINISHED'}
 
-class LinkArmatureAddTargetInput(bpy.types.Operator):
-    """Add a Driver Variable input to the Driver node"""
-    bl_idname = "mantis.link_armature_node_add_target"
-    bl_label = "Add Target"
-    bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
-
-    @classmethod
-    def poll(cls, context):
-        return hasattr(context, 'node')
-
-    def execute(self, context):           # unicode for 'a'
-        num_targets = len( list(context.node.inputs)[6:])//2
-        context.node.inputs.new("xFormSocket", "Target."+str(num_targets).zfill(3))
-        context.node.inputs.new("FloatFactorSocket", "Weight."+str(num_targets).zfill(3))
-        return {'FINISHED'}
-
-
-class LinkArmatureRemoveTargetInput(bpy.types.Operator):
-    """Remove a DriverVariable input from the active Driver node"""
-    bl_idname = "mantis.link_armature_node_remove_target"
-    bl_label = "Remove Target"
-    bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
-
-    @classmethod
-    def poll(cls, context):
-        return hasattr(context, 'node')
-
-    def execute(self, context):
-        n = context.node
-        n.inputs.remove(n.inputs[-1]); n.inputs.remove(n.inputs[-1])
-        return {'FINISHED'}
-
-
 class CollectionAddNewOutput(bpy.types.Operator):
     """Add a new Collection output to the Driver node"""
     bl_idname = "mantis.collection_add_new"
@@ -887,7 +597,6 @@ class CollectionAddNewOutput(bpy.types.Operator):
         n.update_interface()
         return {'FINISHED'}
 
-
 # TODO: should this prune the children, too?
 class CollectionRemoveOutput(bpy.types.Operator):
     """Remove a Collection output to the Driver node"""
@@ -943,7 +652,6 @@ def get_socket_enum(operator, context):
             valid_types.append( (cls.bl_idname, cls.bl_label, "Socket Type", i := i + 1), )
     return valid_types
 
-
 class B4_4_0_Workaround_NodeTree_Interface_Update(Operator):
     """Selects all nodes of same type as active node."""
     bl_idname = "mantis.node_tree_interface_update_4_4_0_workaround"
@@ -980,7 +688,6 @@ class B4_4_0_Workaround_NodeTree_Interface_Update(Operator):
             tree.prevent_next_exec=True
         return {"FINISHED"}
 
-
 class ConvertBezierCurveToNURBS(Operator):
     """Converts all bezier splines of curve to NURBS."""
     bl_idname = "mantis.convert_bezcrv_to_nurbs"
@@ -992,7 +699,7 @@ class ConvertBezierCurveToNURBS(Operator):
         return (context.active_object is not None) and (context.active_object.type=='CURVE')
 
     def execute(self, context):
-        from .utilities import nurbs_copy_bez_spline
+        from .utilities.utilities import nurbs_copy_bez_spline
         curve = context.active_object
         bez=[]
         for spl in curve.data.splines:
@@ -1089,19 +796,9 @@ classes = [
         MantisMuteNode,
         SelectNodesOfType,
         ConnectNodeToInput,
-        # xForm
-        AddCustomProperty,
-        EditCustomProperty,
-        RemoveCustomProperty,
-        # EditFCurveNode,
-        FcurveAddKeyframeInput,
-        FcurveRemoveKeyframeInput,
         # Driver
         DriverAddDriverVariableInput,
         DriverRemoveDriverVariableInput,
-        # Armature Link Node
-        LinkArmatureAddTargetInput,
-        LinkArmatureRemoveTargetInput,
         # managing collections
         CollectionAddNewOutput,
         CollectionRemoveOutput,

+ 1 - 1
ops_ui.py

@@ -3,7 +3,7 @@
 
 import bpy
 from bpy.types import Operator
-from .utilities import (prRed, prGreen, prPurple, prWhite,
+from .utilities.utilities import (prRed, prGreen, prPurple, prWhite,
                               prOrange,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
                               wrapOrange,)

+ 33 - 4
preferences.py

@@ -39,7 +39,7 @@ def filepath_idiot_test(path):
     def do_error_popup():
             def error_popup_draw(self, context):
                 self.layout.label(text="A maximum of 1000 widget files is allowed in the Widget Library.")
-                self.layout.label(text="Make sure the WIdget Library does not scan a huge number of files/folders.")
+                self.layout.label(text="Make sure the Widget Library does not scan a huge number of files/folders.")
             from bpy import context
             context.window_manager.popup_menu(error_popup_draw, title="Error", icon='ERROR')
     try:
@@ -59,9 +59,25 @@ def widget_library_get(self):
 def widget_library_idiot_test(self, value):
     self.widget_library_path = filepath_idiot_test(value)
 
+
 class MantisPreferences(bpy.types.AddonPreferences):
     bl_idname = __package__
 
+    def callback_enable_selection_state(self, context):
+        from . import timers
+        timers.ENABLED = self.EnableSelectionSync
+        if timers.ENABLED == True:
+            bpy.app.timers.register(timers.timer_update_selection_sync)
+        else:
+            try:
+                bpy.app.timers.unregister(timers.timer_update_selection_sync)
+            except ValueError:
+                pass # It's not registered anymore. that's OK
+
+    def callback_sync_selection_poll_rate(self, context):
+        from . import timers
+        timers.SELECTION_POLL_RATE = self.SelectionSyncPollRate
+        
     # JSONprefix: bpy.props.StringProperty(
     #     name = "Prefix code file",
     #     subtype = 'FILE_PATH',
@@ -100,13 +116,26 @@ class MantisPreferences(bpy.types.AddonPreferences):
         description = "Location of .rig files to load automatically.",
         subtype = 'FILE_PATH',
         default = os.path.join(dir_path, 'auto_load_components'),)
-
+    EnableSelectionSync:bpy.props.BoolProperty(
+        name = "Enable Selection Sync",
+        description = "Sync selection of nodes and scene objects.",
+        default = True,
+        update = callback_enable_selection_state)
+    SelectionSyncPollRate:bpy.props.FloatProperty(
+        name = "Selection Sync Poll Rate",
+        description = "How often to check for changes in selection.",
+        default = 0.01,
+        update = callback_sync_selection_poll_rate)
+    
     def draw(self, context):
         layout = self.layout
         layout.label(text="Mantis Preferences")
-        layout.prop(self, "WidgetsLibraryFolder", icon='FILE_FOLDER')
+        layout.prop(self, "EnableSelectionSync")
+        layout.prop(self, "SelectionSyncPollRate")
         layout.prop(self, "WidgetDefaultCollection")
-        layout.prop(self, "ComponentsLibraryFolder", icon='FILE_FOLDER')
         layout.prop(self, "CurveDefaultCollection")
         layout.prop(self, "MetaArmatureDefaultCollection")
+        layout.label(text="Paths:")
+        layout.prop(self, "WidgetsLibraryFolder", icon='FILE_FOLDER')
+        layout.prop(self, "ComponentsLibraryFolder", icon='FILE_FOLDER')
         layout.prop(self, "ComponentsAutoLoadFolder", icon='FILE_FOLDER')

+ 6 - 4
primitives_nodes.py

@@ -1,4 +1,4 @@
-from .node_common import *
+from .utilities.node_common import *
 from .base_definitions import MantisNode, NodeSocket
 
 def TellClasses():
@@ -41,7 +41,8 @@ class CirclePrimitive(PrimitiveNode):
         self.outputs.init_sockets(outputs)
         self.init_parameters(additional_parameters=additional_parameters)
 
-    def bGetObject(self):
+    # mode is unused but needed for interface consistency
+    def bGetObject(self, mode=''):
         from bpy import data
         # first try Curve, then try Mesh
         bObject = data.curves.get(self.evaluate_input("Name"))
@@ -85,8 +86,9 @@ class GeometryLattice(PrimitiveNode):
         super().reset_execution()
         self.prepared=False
         self.executed=False
-
-    def bGetObject(self):
+        
+    # mode is unused but needed for interface consistency
+    def bGetObject(self, mode=''):
         from bpy import data
         bObject = data.lattices.get(self.evaluate_input("Name"))
         return bObject

+ 2 - 2
schema_nodes.py

@@ -1,4 +1,4 @@
-from .node_common import *
+from .utilities.node_common import *
 from math import pi, tau
 from .base_definitions import MantisNode
 
@@ -18,7 +18,7 @@ def TellClasses():
     ]
 
 def schema_init_sockets(mantis_node, is_input = True, in_out='INPUT', category=''):
-    from .utilities import tree_from_mantis_node, read_schema_type
+    from .utilities.utilities import tree_from_mantis_node, read_schema_type
     parent_tree = tree_from_mantis_node(mantis_node.signature, mantis_node.base_tree)
     if is_input:
         sockets=mantis_node.inputs

+ 2 - 2
schema_nodes_ui.py

@@ -1,13 +1,13 @@
 import bpy
 from .base_definitions import SchemaUINode
 from bpy.types import Node
-from .utilities import (prRed, prGreen, prPurple, prWhite,
+from .utilities.utilities import (prRed, prGreen, prPurple, prWhite,
                               prOrange,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
                               wrapOrange,)
 from bpy.props import BoolProperty
 
-from .utilities import (get_socket_maps, relink_socket_map,
+from .utilities.utilities import (get_socket_maps, relink_socket_map,
                         do_relink, read_schema_type)
 
 from .schema_nodes_socket_templates import *

+ 2 - 2
socket_definitions.py

@@ -47,7 +47,7 @@ else:
 
 
 
-from .utilities import (prRed, prGreen, prPurple, prWhite,
+from .utilities.utilities import (prRed, prGreen, prPurple, prWhite,
                               prOrange,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
                               wrapOrange,)
@@ -493,7 +493,7 @@ def default_update(ui_socket, context, do_execute=True):
         node_tree.update_tree(context, force=True)
     elif hasattr(ui_socket, 'default_value'):
         # we may not have to regenerate the tree; try and update the socket
-        from .utilities import tree_from_mantis_node
+        from .utilities.utilities import tree_from_mantis_node
         for mantis_node in node_tree.parsed_tree.values():
             # check to see if the mantis node is in the same ui-tree as this ui_socket
             if mantis_node.ui_signature is None: continue # autogenerated nodes

+ 145 - 0
timers.py

@@ -0,0 +1,145 @@
+from .utilities.utilities import (prRed, prGreen, prPurple, prWhite,
+                              prOrange,
+                              wrapRed, wrapGreen, wrapPurple, wrapWhite,
+                              wrapOrange,)
+
+# the first step is to get selection syncing working
+import bpy
+from bpy import context
+from .base_definitions import get_signature_from_edited_tree 
+
+from bpy.types import Bone, PoseBone
+# just to prevent confusing with Python object:
+from bpy.types import Object as bpy_object
+
+SELECTION_POLL_RATE = 0.01
+ENABLED = True
+cached_selection_state = {}
+
+import bpy
+
+# The following node types represent objects that can be selected.
+WATCH_FOR_SELECTION = [ 'xFormBoneNode', 'xFormArmatureNode',
+        'xFormGeometryObject', 'xFormObjectInstance', 'xFormCurvePin',
+        'xFormGetBone', 'InputExistingGeometryObject']
+
+
+def get_signature_from_space(node, space):
+    sig_path=[None,]
+    for item in space.path[:-1]:
+        sig_path.append(item.node_tree.nodes.active.name)
+    return tuple(sig_path+[node.name])
+
+def get_visible_space_data(space_types={'NODE_EDITOR'}):
+    visible_spaces = []
+    for wm in bpy.data.window_managers:
+        for window in wm.windows:
+            for area in window.screen.areas:
+                if area.type in space_types:
+                    if area.spaces.active:
+                        visible_spaces.append(area.spaces.active)
+                        
+    return visible_spaces
+
+# Example usage: Get all active 3D Viewport space_data objects
+spaces = get_visible_space_data('VIEW_3D')
+
+for space in spaces:
+    print(f"Found 3D Viewport: shading mode = {space.shading.type}")
+
+# TODO: now that I have this working...
+#  - Selection State should be tracked and handled on a per-object basis
+#     so Mantis should update selection state one item at a time
+#  - finally, I want to extend this selection sync to show active constraints and modifiers
+#     and to link up with viewport gizmo tools.
+#  - Serialize this so that it sticks around when saving/loading file
+# Eventually:
+#  - sync scene/object selection to node graph. I have the structure in place
+#     but I still have to write the selection tracking code.
+# Potential small BUG?
+#  - I am not ensuring the base/root tree is the same for the space and the nodes
+#  - because I straight up do not store that in the selection_sync dicts
+#  - easy to fix, will do it when clearing TODO's here.
+
+def timer_update_selection_sync():
+    if not ENABLED: return None
+    root_tree = None
+    tree_path = None
+    parsed_tree = None
+    spaces = get_visible_space_data()
+    if not spaces:
+        return SELECTION_POLL_RATE
+
+    selection_state_start = cached_selection_state.copy()
+    selection_state = cached_selection_state.copy()
+    current_tree_selected_total = 0
+
+    # we can't get UI context in this function so we have to checc all of them
+    #maybe the tree itself can load the timer when it is opened in the node editor?
+    for space in spaces:
+        # if we are in the node editor, manage node selection
+        if space.type == "NODE_EDITOR":
+            root_tree = space.path[0].node_tree
+            parsed_tree = root_tree.parsed_tree
+            tree_path = [path_item.node_tree for path_item in space.path]
+            if root_tree is None:
+                return SELECTION_POLL_RATE
+            if root_tree.bl_idname != 'MantisTree':
+                return SELECTION_POLL_RATE
+            active_ui_signature = (get_signature_from_space(tree_path[-1].nodes.active, space))
+            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
+                current_tree_selected_total += int(n.select)
+    
+    if parsed_tree is None:
+        return SELECTION_POLL_RATE
+    cached_selection_state.update(selection_state)
+    # We only fire the update if things have changed.
+    if selection_state != selection_state_start:
+        
+        from bpy import context
+        view_layer = context.view_layer
+        deselect = not bool(current_tree_selected_total)
+        try:
+            update_scene_selection_from_node_graph_selection_sync(
+                active_ui_signature, view_layer,
+                selection_state, deselect)
+        except Exception as e:
+            prRed (e)
+            return SELECTION_POLL_RATE
+    return SELECTION_POLL_RATE
+
+def update_scene_selection_from_node_graph_selection_sync(
+        active_ui_signature, view_layer,
+        selection_state, deselect ):
+    from .base_definitions import ( selection_sync_node_to_object,
+                                    selection_sync_object_to_node, )
+    prWhite("Updating Scene Selection from Node Graph Selection")
+    deselect_do_once = []
+    for ui_signature, selected in selection_state.items():
+        this_one_is_active = False
+        if ui_signature == active_ui_signature: this_one_is_active = True
+        for object_key in selection_sync_node_to_object.get(ui_signature, []):
+            id_data = bpy.data.objects.get(object_key[0])
+            if object_key[1]: # we have a sub_value, it's a bone
+                if deselect and object_key[0] not in deselect_do_once:
+                    for pb in id_data.pose.bones:
+                        pb.select = False
+                    deselect_do_once.append(object_key[0])
+                else:
+                    ob = id_data.pose.bones[object_key[1]]
+                    ob.select = selected
+                    if this_one_is_active: # set the active bone
+                        id_data.data.bones.active = ob.bone
+                    # now do its parent object...
+                    if selected:
+                        id_data.select_set(True, view_layer=view_layer)
+                        if this_one_is_active:
+                            view_layer.objects.active = id_data
+            else: # or the sub_value is None, this is an object
+                ob = id_data
+                if deselect: selected = False
+                ob.select_set(selected, view_layer=view_layer)
+                if this_one_is_active and not deselect:
+                    view_layer.objects.active = ob

+ 0 - 0
drivers.py → utilities/drivers.py


+ 0 - 0
geometry_node_graphgen.py → utilities/geometry_node_graphgen.py


+ 7 - 6
i_o.py → utilities/i_o.py

@@ -194,7 +194,7 @@ def scan_tree_dependencies(base_tree, curves:set, armatures:set, ):
     for mantis_node in nodes.values():
         mantis_node.reset_execution()
         check_and_add_root(mantis_node, xForm_pass)
-    from .readtree import sort_execution
+    from ..execution.readtree import sort_execution
     from .utilities import get_ui_node
     sorted_nodes, execution_failed = sort_execution(nodes, xForm_pass)
     if execution_failed:
@@ -400,7 +400,7 @@ def get_socket_data(socket, ignore_if_default=False):
 def get_node_data(ui_node):
     # if this is a node-group, force it to update its interface, because it may be messed up.
     # can remove this HACK when I have stronger guarentees about node-group's keeping the interface
-    from .base_definitions import node_group_update
+    from ..base_definitions import node_group_update
     if hasattr(ui_node, "node_tree"):
         ui_node.is_updating = True
         try: # HERE BE DANGER
@@ -424,7 +424,8 @@ def get_node_data(ui_node):
         if propname == "parent" and value:
             value = value.name
         if not is_jsonable(value):
-            raise RuntimeError(f"Could not export...  {ui_node.name}, {propname}, {type(value)}")
+            prOrange(f"Warning: Could not export...  {ui_node.name}, {propname}, {type(value)}")
+            continue
         if value is None:
             continue
         node_props[propname] = value
@@ -482,7 +483,7 @@ def get_interface_data(tree, tree_in_out):
             # we need to get the socket class from the bl_idname
             bl_socket_idname = sock.bl_socket_idname
             # try and import it
-            from . import socket_definitions
+            from .. import socket_definitions
             # some mistakes in versioning makes it possible for older trees to have
             # standard Blender types instead of Mantis types
             if bl_socket_idname == 'NodeSocketFloat':
@@ -1006,7 +1007,7 @@ def do_import(data, context, search_multi_files=False, filepath='', skip_existin
                 if bpy.app.version != (4,5,0):
                     sock = tree.interface.new_socket(s_props["name"], in_out=s_props["in_out"], socket_type=socket_type)
                 else: # blender 4.5.0 LTS, have to workaround a bug!
-                    from .versioning import workaround_4_5_0_interface_update
+                    from ..versioning import workaround_4_5_0_interface_update
                     sock = workaround_4_5_0_interface_update(tree=tree, name=s_props["name"], in_out=s_props["in_out"],
                                                             sock_type=socket_type, parent_name=s_props.get("parent", ''))
 
@@ -1115,7 +1116,7 @@ def do_import(data, context, search_multi_files=False, filepath='', skip_existin
                         raise RuntimeError(f"Tree {sub_tree} not available to import.")
 
                 n.node_tree = grp_tree
-                from .base_definitions import node_group_update
+                from ..base_definitions import node_group_update
                 n.is_updating = True
                 try:
                     node_group_update(n, force = True)

+ 3 - 3
node_common.py → utilities/node_common.py

@@ -2,7 +2,7 @@ from .utilities import (prRed, prGreen, prPurple, prWhite,
                               prOrange,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
                               wrapOrange,)
-from .base_definitions import GraphError, NodeSocket, MantisNode
+from ..base_definitions import GraphError, NodeSocket, MantisNode
 from collections.abc import Callable
 # BE VERY CAREFUL
 # the x_containers files import * from this file
@@ -90,7 +90,7 @@ def trace_line_up_branching(mantis_node : MantisNode, output_name : str,
 # 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'):
-    from .mantis_dataclasses import xForm_info
+    from ..mantis_dataclasses import xForm_info
     # we re-implement the trace logic here for performance.
     # because this kind of connection has traversal (for Links to find their children), we must
     # simply seek back to the first available xForm or Link and get its xForm info.\
@@ -122,7 +122,7 @@ def get_parent_xForm_info(mantis_node, socket_name = 'Relationship'):
 def setup_custom_property_inputs_outputs(mantis_node):
     from .utilities import get_ui_node
     if mantis_node.signature[0] == 'SCHEMA_AUTOGENERATED':
-        from .base_definitions import custom_props_types
+        from ..base_definitions import custom_props_types
         if mantis_node.__class__.__name__ not in custom_props_types:
             # prRed(f"Reminder: figure out how to deal with custom property setting for Schema Node {mantis_node}")
             raise RuntimeError(wrapRed(f"Custom Properties not allowed for class {mantis_node.__class__.__name__}"))

+ 39 - 33
utilities.py → utilities/utilities.py

@@ -26,23 +26,11 @@ 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)
 
-# A fuction for getting to the end of a Reroute.
-# TODO: this seems really inefficient!
-def socket_seek(start_link, links):
-    link = start_link
-    while(link.from_socket):
-        for newlink in links:
-            if link.from_socket.node.inputs:
-                if link.from_node.bl_idname != 'NodeReroute':
-                    return link.from_socket
-                if newlink.to_socket == link.from_socket.node.inputs[0]:
-                    link=newlink; break
-        else:
-            break
-    return link.from_socket
-
-# THIS ONE is better. I don't know what I was thinking up above.
-# TODO: try and refactor to use this function instead
+# 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
@@ -68,7 +56,7 @@ def find_reroute_start_socket(reroute, track='BACK'):
 # 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
+    from ..base_definitions import DummyLink
     kept_links, rerouted_starts = [], []
     rerouted = []
     all_links = links.copy()
@@ -89,8 +77,12 @@ def clear_reroutes(links):
         else:
             kept_links.append(link)
     for start in rerouted_starts:
-        from_socket = socket_seek(start, rerouted)
-        new_link = DummyLink(from_socket=from_socket, to_socket=start.to_socket, from_mantis_node=None, to_mantis_node=None, multi_input_sort_id=start.multi_input_sort_id )
+        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
 
@@ -160,7 +152,7 @@ def get_socket_maps(node, force=False):
                     continue
                 map[sock.identifier]=val
             else:
-                from .socket_definitions import no_default_value
+                from ..socket_definitions import no_default_value
                 if sock.bl_idname in no_default_value:
                     map[sock.identifier]=None
                 else:
@@ -193,7 +185,7 @@ def do_relink(node, socket, map, in_out='INPUT', parent_name = ''):
     from bpy.types import NodeSocket, Node
     get_string = '__extend__'
     if socket: get_string = socket.identifier
-    from .base_definitions import SchemaUINode
+    from ..base_definitions import SchemaUINode
     if (hasattr(node, "node_tree") or isinstance(node, SchemaUINode)) and get_string not in map.keys():
         # this happens when we are creating a new node group and need to update it from nothing.
         return
@@ -311,7 +303,7 @@ def update_interface(interface, name, in_out, sock_type, parent_name):
         if bpy_version != (4,5,0):
             socket = interface.new_socket(name=name,in_out=in_out, socket_type=sock_type, parent=interface_parent)
         else: # blender 4.5.0 LTS, have to workaround a bug!
-            from .versioning import workaround_4_5_0_interface_update
+            from ..versioning import workaround_4_5_0_interface_update
             socket = workaround_4_5_0_interface_update(tree=interface.id_data, name=name, in_out=in_out,
                                                        sock_type=sock_type, parent_name=parent_name, do_parent=True)
         if parent_name == 'Connection':
@@ -333,7 +325,7 @@ def relink_socket_map_add_socket(node, socket_collection, item,  in_out=None,):
     # have to work around a bug in 4.5.0 that prevents me from declaring custom socket types
     # I have arbitrarily chosen to use the NodeSocketGeometry type to signal that this one is affected.
     if bpy_version == (4, 5, 0) and item.bl_socket_idname == 'NodeSocketGeometry':
-        from .versioning import socket_add_workaround_for_4_5_0_LTS
+        from ..versioning import socket_add_workaround_for_4_5_0_LTS
         s = socket_add_workaround_for_4_5_0_LTS(item, socket_collection, multi)
     else:
         s = socket_collection.new(type=item.bl_socket_idname, name=item.name, identifier=item.identifier,  use_multi_input=multi)
@@ -429,7 +421,7 @@ def bind_modifier_operator(modifier, operator):
             operator(modifier=modifier.name)
 
 def get_default_collection(collection_type='WIDGET'):
-    from .preferences import get_bl_addon_object
+    from ..preferences import get_bl_addon_object
     from bpy import data, context
     mantis_addon = get_bl_addon_object(raise_error=True)
     match collection_type:
@@ -607,7 +599,7 @@ def import_curve_data_to_object(curve_name, curve_data):
 
 def get_component_library_items(path='ADD_ARMATURE'):
     from os import path as os_path
-    from .preferences import get_bl_addon_object
+    from ..preferences import get_bl_addon_object
     bl_mantis_addon = get_bl_addon_object()
     return_value=[]
     if bl_mantis_addon:
@@ -659,7 +651,7 @@ def init_dependencies(mantis_node):
 
 def schema_dependency_handle_item(schema, all_mantis_nodes, item,):
     hierarchy = True
-    from .base_definitions import from_name_filter, to_name_filter
+    from ..base_definitions import from_name_filter, to_name_filter
     if item.in_out == 'INPUT':
         dependencies = schema.dependencies
         hierarchy_dependencies = schema.hierarchy_dependencies
@@ -733,7 +725,7 @@ def check_and_add_root(n, roots, include_non_hierarchy=False):
     roots.append(n)
 
 def get_link_in_out(link):
-    from .base_definitions import replace_types
+    from ..base_definitions import replace_types
     from_name, to_name = link.from_socket.node.name, link.to_socket.node.name
     # catch special bl_idnames and bunch the connections up
     if link.from_socket.node.bl_idname in replace_types:
@@ -743,6 +735,12 @@ def get_link_in_out(link):
     return from_name, to_name
 
 def link_mantis_nodes(tree_path_names, link, local_mantis_nodes, from_suffix='', to_suffix=''):
+    links = clear_reroutes([link])
+    if links:
+        link = links[0]
+    else:
+        return
+
     dummy_types = ["DUMMY", "DUMMY_SCHEMA"]
     from_name, to_name = get_link_in_out(link)
     from_mantis_node = local_mantis_nodes.get( (*tree_path_names, from_name+from_suffix) )
@@ -763,10 +761,18 @@ def link_mantis_nodes(tree_path_names, link, local_mantis_nodes, from_suffix='',
             raise e
     else:
         prRed(from_mantis_node, to_mantis_node, (*tree_path_names, from_name+from_suffix), (*tree_path_names, to_name+to_suffix))
+        if to_mantis_node is None:
+            prRed( "Error: mantis node with signature "
+                    f"{(*tree_path_names, to_name+to_suffix)} " 
+                    "does not exist.")
+        if from_mantis_node is None:
+            prRed("Error: mantis node with signature "
+                  f"{(*tree_path_names, from_name+from_suffix)} " 
+                   "does not exist.")
         raise RuntimeError(wrapRed(f"Link not connected: {from_mantis_node} -> {to_mantis_node} in tree" ))
     
 def get_all_dependencies(mantis_node):
-    from .base_definitions import GraphError
+    from ..base_definitions import GraphError
     """ find all dependencies for a mantis node"""
     nodes = []
     check_nodes = [mantis_node]
@@ -795,7 +801,7 @@ def get_all_nodes_of_type(base_tree, bl_idname):
     return nodes
 
 def trace_all_nodes_from_root(root, nodes):
-    from .base_definitions import GraphError
+    from ..base_definitions import GraphError
     """ find all dependencies for a mantis node"""
     nodes.add(root); check_nodes = [root]
     nodes_checked = set()
@@ -965,7 +971,7 @@ def handle_overlay_detect(node, socket_id, value) -> str:
 ####################################
 
 def make_perpendicular(v1, v2):
-    from .base_definitions import FLOAT_EPSILON
+    from ..base_definitions import FLOAT_EPSILON
     if (v1.length_squared < FLOAT_EPSILON) or (v2.length_squared < FLOAT_EPSILON):
         raise RuntimeError("Cannot generate perpendicular vetor for zero-length vector")
     projected = (v2.dot(v1) / v1.dot(v1)) * v1
@@ -1036,7 +1042,7 @@ def bind_extracted_spline_to_curve(new_ob, curve):
     # now, weirdly enough - we can't use parenting very easily because Blender
     # defines the parent on a curve relative to the evaluated path animation
     # Setting the inverse matrix is too much work. Use Copy Transforms instead.
-    from .xForm_nodes import reset_object_data
+    from ..xForm_nodes import reset_object_data
     reset_object_data(new_ob)
     c = new_ob.constraints.new("COPY_TRANSFORMS"); c.target=curve
     new_ob.parent=curve
@@ -1099,7 +1105,7 @@ def RibbonMeshEdgeLengths(m, ribbon):
     return lengths
 
 def EnsureCurveIsRibbon(crv, defaultRadius = 0.1):
-    from .base_definitions import FLOAT_EPSILON
+    from ..base_definitions import FLOAT_EPSILON
     crvRadius = 0
     crv.data.offset = 0
     if (crv.data.bevel_depth < FLOAT_EPSILON):

+ 20 - 18
versioning.py

@@ -4,7 +4,7 @@
 from bpy.types import Node, NodeSocket
 from bpy.app import version as bpy_version
 
-from .utilities import (prRed, prGreen, prPurple, prWhite,
+from .utilities.utilities import (prRed, prGreen, prPurple, prWhite,
                               prOrange,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
                               wrapOrange,)
@@ -78,12 +78,12 @@ def version_upgrade_very_old(*args, **kwargs):
                 node.inputs.move(len(node.inputs)-1, index)
         socket_map = None
         if rename_jobs:
-            from .utilities import get_socket_maps
+            from .utilities.utilities import get_socket_maps
             socket_maps = get_socket_maps(node)
         for socket, socket_index, new_bl_idname, new_name, multi in rename_jobs:
             old_id = socket.identifier
             print (f"Renaming socket {socket.identifier} to {new_name} in node {node.name}")
-            from .utilities import do_relink
+            from .utilities.utilities import do_relink
             if socket.is_output:
                 index = 1
                 in_out = "OUTPUT"
@@ -121,7 +121,7 @@ def version_upgrade_bone_0_12_0_from_older(*args, **kwargs):
         collection_input_is_array = node.inputs['Bone Collection'].is_multi_input
         if not collection_input_is_array: # it must be made into an array!
             prPurple(f"Updating \"Bone Collection\" Socket in {node.name}")
-            from .utilities import get_socket_maps
+            from .utilities.utilities import get_socket_maps
             socket_maps = get_socket_maps(node)
             socket_map = socket_maps[0]
             for i, socket in enumerate(node.inputs):
@@ -233,7 +233,7 @@ def up_0_12_25_replace_floor_offset_type(*args, **kwargs):
     if  current_major_version > 0: return# major version must be 0
     if current_minor_version > 12: return# minor version must be 12 or less
     if current_minor_version == 12 and current_sub_version > 24: return 
-    from .utilities import get_socket_maps, do_relink
+    from .utilities.utilities import get_socket_maps, do_relink
     socket_maps = get_socket_maps(node)
     prPurple(f"Fixing \"Offset\" socket in {node.name}")
     try:
@@ -357,20 +357,20 @@ def schema_enable_custom_interface_types(*args, **kwargs):
 # since this is basically the same operation, just do it all together
 def update_fcurve_and_armature_node_to_multi_socket(*args, **kwargs):
     node = kwargs['node']
-    prRed("BEPIS")
     live_updates = node.id_data.do_live_update
     node.id_data.do_live_update = False
     # First we have to get the socket maps and re-init the sockets
-    from .utilities import get_socket_maps, do_relink
-    sockets_in, _sockets_out = get_socket_maps(node,)
-    node.inputs.clear(); node.outputs.clear()
-    node.init_sockets(node.socket_templates)
+    from .utilities.utilities import get_socket_maps, do_relink
     convert_keys = {'UtilityFCurve' : [ 'Keyframe', ],
                     'LinkArmature'  : [ 'Target', 'Weight', ], }
+    my_convert_keys = convert_keys[node.bl_idname]
+    sockets_in, _sockets_out = get_socket_maps(node,)
+    node.inputs.clear()
+    input_templates = [st for st in node.socket_templates if st.is_input == True]
+    node.init_sockets(input_templates)
     try:
         new_sockets_in = {}
-        print (convert_keys[node.bl_idname])
-        for convert_key in convert_keys[node.bl_idname]:
+        for convert_key in my_convert_keys:
             for socket_name, links in sockets_in.items():
                 if links is None: continue
                 if convert_key+'.' in socket_name:
@@ -383,12 +383,14 @@ def update_fcurve_and_armature_node_to_multi_socket(*args, **kwargs):
                         new_sockets_in[convert_key+'s'] = sockets_in[socket_name]
                     prWhite (new_sockets_in[convert_key+'s'])
             do_relink(node, node.inputs[convert_key+'s'], new_sockets_in, in_out="INPUT",)
-            delete_me = []
-            for socket in node.inputs:
-                if convert_key+'.' in socket.name:
-                    delete_me.append(socket)
-            for socket in delete_me:
-                node.inputs.remove(socket)
+        for socket_name, links in sockets_in.items():
+            proceed = True
+            for convert_key in my_convert_keys:
+                if convert_key in socket_name:
+                    proceed = False
+            if proceed:
+                do_relink(node, node.inputs[socket_name], sockets_in, in_out='INPUT')
+
     finally:
         node.id_data.do_live_update = live_updates
 

+ 10 - 8
xForm_nodes.py

@@ -1,4 +1,4 @@
-from .node_common import *
+from .utilities.node_common import *
 from .base_definitions import MantisNode, NodeSocket
 from .xForm_socket_templates import *
 from .mantis_dataclasses import xForm_info
@@ -84,7 +84,7 @@ class xFormNode(MantisNode):
             # we need to trace back all inputs
             custom_prop_data = self.evaluate_input(input, i)
             if not isinstance(custom_prop_data, custom_prop_template): continue
-            from .drivers import MantisDriver
+            from .utilities.drivers import MantisDriver
             name = custom_prop_data.name
             if name in ob.keys():
                 raise RuntimeError("Error: cannot create a custom property with "
@@ -181,6 +181,7 @@ class xFormArmature(xFormNode):
         ob.id_properties_clear()
 
         self.bObject = ob.name
+        self.bRegisterObject('OBJECT', ob)
         ob.matrix_world = matrix.copy()
         ob.data.pose_position = 'REST'
         set_object_parent(self)
@@ -331,6 +332,7 @@ class xFormBone(xFormNode):
             raise RuntimeError("Could not create edit bone: ", name)
         assert (eb.name), "Bone must have a name."
         self.bObject = eb.name
+        self.bRegisterObject('BONE', xF, eb)
         # The bone should have relationships going in at this point.
 
         self.bSetParent(eb)
@@ -655,7 +657,7 @@ class xFormGeometryObject(xFormNode):
             self_edit_name=self.bObject.name,
         )
         self.parameters['xForm Out'] = my_info
-
+        self.bRegisterObject('OBJECT', self.bObject)
         self.prepared = True
 
     def bTransformPass(self, bContext = None,):
@@ -743,7 +745,7 @@ class xFormObjectInstance(xFormNode):
             self_edit_name=self.bObject.name,
         )
         self.parameters['xForm Out'] = my_info
-
+        self.bRegisterObject('OBJECT', self.bObject)
         self.prepared = True
 
     def bTransformPass(self, bContext = None,):
@@ -772,7 +774,7 @@ class xFormObjectInstance(xFormNode):
         modifier = self.bObject.modifiers.new("Object Instance", type='NODES')
         ng = data.node_groups.get("Object Instance")
         if ng is None:
-            from .geometry_node_graphgen import gen_object_instance_node_group
+            from .utilities.geometry_node_graphgen import gen_object_instance_node_group
             ng = gen_object_instance_node_group()
         modifier.node_group = ng
         modifier["Socket_0"] = source_ob
@@ -832,7 +834,7 @@ class xFormCurvePin(xFormNode):
             ob.empty_display_size = 0.10
 
         self.bObject = ob
-
+        self.bRegisterObject('OBJECT', self.bObject)
         reset_object_data(ob)
 
         node_line = trace_single_line(self, "Parent Curve")[0][1:] # slice excludes self
@@ -853,7 +855,7 @@ class xFormCurvePin(xFormNode):
         #
         # always do this so that everything stays consistent.
         spline_index = self.evaluate_input("Spline Index")
-        from .utilities import get_extracted_spline_object
+        from .utilities.utilities import get_extracted_spline_object
         curve = get_extracted_spline_object(curve, spline_index, self.mContext)
         # Link to Scene:
         for link_me in [ob, curve]:
@@ -922,7 +924,6 @@ class xFormCurvePin(xFormNode):
     def bGetObject(self, mode = 'POSE'):
         return self.bObject
 
-
 # special thank-you to Natalie Cuthbert, who submitted a patch for this
 # it turned out, I already had this patch laying around
 # because I was too busy with grad school I didn't succeed in collaborating
@@ -958,4 +959,5 @@ class xFormGetBone(xFormNode):
                 case 'POSE':
                     bone = armature.pose.bones.get(self.evaluate_input("Bone"))
         assert self.bObject is not None, f"{self} failed to get the desired bone. Check if the bone name exists."
+        self.bRegisterObject('BONE', armature, bone)
         return bone

+ 3 - 3
xForm_nodes_ui.py

@@ -1,7 +1,7 @@
 import bpy
 from .base_definitions import xFormNode
 from bpy.types import Node
-from .utilities import (prRed, prGreen, prPurple, prWhite,
+from .utilities.utilities import (prRed, prGreen, prPurple, prWhite,
                               prOrange,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
                               wrapOrange,)
@@ -87,7 +87,7 @@ def custom_prop_display_update(self, mantis_node = None):
         if len(mantis_node.inputs['Custom Properties'].links) != len(self.inputs['Custom Properties'].links):
             return # there is a problem somewhere
         for i, link in enumerate(mantis_node.inputs['Custom Properties'].links):
-            from .node_common import trace_single_line
+            from .utilities.node_common import trace_single_line
             node_line, _last_socket = trace_single_line(mantis_node, 'Custom Properties', i)
             if not node_line[-1].prepared:
                 return
@@ -100,7 +100,7 @@ def custom_prop_display_update(self, mantis_node = None):
             name = custom_prop_data.name
             type = custom_prop_data.prop_type
             custom_props[name] = type_map[type]
-    from .utilities import get_socket_maps, do_relink
+    from .utilities.utilities import get_socket_maps, do_relink
     _socket_map_in, socket_map_out = get_socket_maps(self)
     for name, type in custom_props.items():
         if socket_map_out.get(name) is None: