瀏覽代碼

Update to Blender 4.2 API and Blender Extension format

Joseph Brandenburg 1 年之前
父節點
當前提交
dd451fe156
共有 20 個文件被更改,包括 436 次插入213 次删除
  1. 2 0
      .gitignore
  2. 24 35
      __init__.py
  3. 77 50
      base_definitions.py
  4. 74 0
      blender_manifest.toml
  5. 14 15
      deformer_containers.py
  6. 1 1
      drivers.py
  7. 39 0
      i_o.py
  8. 2 2
      link_containers.py
  9. 1 1
      link_definitions.py
  10. 7 7
      misc_containers.py
  11. 3 3
      node_container_common.py
  12. 1 1
      nodes_generic.py
  13. 1 1
      ops_generate_tree.py
  14. 34 3
      ops_nodegroup.py
  15. 1 1
      primitives_containers.py
  16. 13 7
      readtree.py
  17. 52 21
      socket_definitions.py
  18. 3 3
      utilities.py
  19. 78 52
      xForm_containers.py
  20. 9 10
      xForm_definitions.py

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+*__pycache__*
+grandalf/*

+ 24 - 35
__init__.py

@@ -1,16 +1,3 @@
-bl_info = {
-    "name": "Mantis Rigging Nodes",
-    "author": "Joseph Brandenburg",
-    "version": (000, 000, 000),
-    "blender": (3, 00, 0),
-    "location": "todo",
-    "description": "todo",
-    "warning": "experimental, likely to freeze or crash. Use at your own risk!",
-    "wiki_url": "",
-    "tracker_url": "",
-    "category": "Rigging"}
-
-
 from . import ( ops_nodegroup,
                 base_definitions,
                 socket_definitions,
@@ -20,7 +7,7 @@ from . import ( ops_nodegroup,
                 primitives_definitions,
                 deformer_definitions,
               )
-from mantis.ops_generate_tree import CreateMantisTree
+from .ops_generate_tree import CreateMantisTree
 from bpy.types import NodeSocket
 
 
@@ -43,27 +30,29 @@ while (classLists):
     classes.extend(classLists.pop())
 
 interface_classes = []
-for cls in [cls for cls in socket_definitions.TellClasses() if issubclass(cls, NodeSocket)]:
-    name = cls.__name__+"Interface"
-    from bpy.types import NodeSocketInterface
-    def default_draw_color(self, context,):
-        return self.color
-    def default_draw(self, context, layout):
-        return
-    interface = type(
-                  name,
-                  (NodeSocketInterface,),
-                  {
-                      "color"            : cls.color,
-                      "draw_color"       : default_draw_color,
-                      "draw"             : default_draw,
-                      "bl_idname"        : name,
-                      "bl_socket_idname" : cls.__name__,
-                  },
-              )
-    interface_classes.append(interface)
-
-classes.extend(interface_classes)
+from bpy import app
+if app.version[0]  == 3:
+    for cls in [cls for cls in socket_definitions.TellClasses() if issubclass(cls, NodeSocket)]:
+        name = cls.__name__+"Interface"
+        from bpy.types import NodeSocketInterface
+        def default_draw_color(self, context,):
+            return self.color
+        def default_draw(self, context, layout):
+            return
+        interface = type(
+                      name,
+                      (NodeSocketInterface,),
+                      {
+                          "color"            : cls.color,
+                          "draw_color"       : default_draw_color,
+                          "draw"             : default_draw,
+                          "bl_idname"        : name,
+                          "bl_socket_idname" : cls.__name__,
+                      },
+                  )
+        interface_classes.append(interface)
+
+    classes.extend(interface_classes)
 
 import nodeitems_utils
 from nodeitems_utils import NodeCategory, NodeItem

+ 77 - 50
base_definitions.py

@@ -4,7 +4,7 @@ from bpy.props import BoolProperty, StringProperty, EnumProperty, CollectionProp
 from . import ops_nodegroup
 from bpy.types import NodeTree, Node, PropertyGroup, Operator, UIList, Panel
 
-from mantis.utilities import (prRed, prGreen, prPurple, prWhite,
+from .utilities import (prRed, prGreen, prPurple, prWhite,
                               prOrange,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
                               wrapOrange,)
@@ -44,14 +44,14 @@ class MantisTree(NodeTree):
         @classmethod
         def valid_socket_type(cls, socket_type: str):
             # https://docs.blender.org/api/master/bpy.types.NodeTree.html#bpy.types.NodeTree.valid_socket_type
-            from mantis.socket_definitions import Tell_bl_idnames
+            from .socket_definitions import Tell_bl_idnames
             return socket_type in Tell_bl_idnames()
             # thank you, Sverchok
             
     def update_tree(self, context):
         if self.do_live_update == False:
             return
-        from mantis import readtree
+        from . import readtree
         prGreen("Validating Tree: %s" % self.name)
         parsed_tree = readtree.parse_tree(self)
         self.parsed_tree=parsed_tree
@@ -75,7 +75,7 @@ class MantisTree(NodeTree):
     
     def execute_tree(self,context):
         prGreen("Executing Tree: %s" % self.name)
-        from mantis import readtree
+        from . import readtree
         readtree.execute_tree(self.parsed_tree, self, context)
 
     
@@ -92,7 +92,7 @@ def update_handler(scene):
             if prev_links != node_tree.num_links:
                 node_tree.tree_valid = False
             if node_tree.tree_valid == False:
-                from mantis import readtree
+                from . import readtree
                 node_tree.update_tree(context)
 
 def execute_handler(scene):
@@ -106,7 +106,7 @@ def execute_handler(scene):
 # bpy.app.handlers.load_post.append(set_tree_invalid)
 bpy.app.handlers.depsgraph_update_pre.append(update_handler)
 bpy.app.handlers.depsgraph_update_post.append(execute_handler)
-    
+
 
 class MantisNode:
     num_links:IntProperty(default=-1)
@@ -119,7 +119,7 @@ class MantisNode:
         context = bpy.context
         if context.space_data:
             node_tree = context.space_data.path[0].node_tree
-            from mantis import readtree
+            from . import readtree
             prOrange("Updating from insert_link callback")
             node_tree.update_tree(context)
             if (link.to_socket.is_linked == False):
@@ -150,10 +150,13 @@ class DeformerNode(MantisNode):
 
 from bpy.types import NodeCustomGroup
 # TODO: make this one's traverse() function actually work
+def poll_node_tree(self, tree):
+    return True #TODO: prevent circular group ofc
 class MantisNodeGroup(NodeCustomGroup, MantisNode):
     bl_idname = "MantisNodeGroup"
     bl_label = "Node Group"
     
+    node_tree_updater : bpy.props.PointerProperty(type=bpy.types.NodeTree, poll=poll_node_tree)
     # def poll_node_tree(self, object):
         # if object.bl_idname not in "MantisTree":
             # return False
@@ -165,55 +168,79 @@ class MantisNodeGroup(NodeCustomGroup, MantisNode):
                 # return False
     # node_tree:bpy.props.PointerProperty(type=bpy.types.NodeTree, poll=poll_node_tree)
     
-    def init(self, context):
-        pass
+    # def init(self, context):
+        # pass
+        
+        
+    def socket_value_update(self, context):
+        prGreen("updating...")
+    
+    
+    # this is a total HACK
+    def update(self):
+        if self.node_tree_updater is not self.node_tree:
+            self.update_node_tree()
+            self.node_tree_updater = self.node_tree
+    
+    
+    def update_node_tree(self):
+        self.inputs.clear()
+        self.outputs.clear()
+        for item in self.node_tree.interface.items_tree:
+            if item.item_type != "SOCKET":
+                continue
+            s = None
+            if item.in_out == 'OUTPUT':
+                s = self.outputs.new(type=item.socket_type, name=item.name, identifier=item.identifier)
+            else:
+                s = self.inputs.new(type=item.socket_type, name=item.name, identifier=item.identifier)
     
     def draw_buttons(self, context, layout):
         row = layout.row(align=True)
         row.prop(self, "node_tree", text="")
         row.operator("mantis.edit_group", text="", icon='NODETREE', emboss=True)
         
-# I don't remember why I need this?
-class GroupOutputDummySocket:
-    # a dummy class for acting like a socket that is coming from every
-    #  group output node
-    def __init__(self, tree, identifier, is_input=True):
-        #
-        # So, we need to go through the node tree and find all 
-        #   the Group Input sockets that match this
-        #   socket's identifier
-        #
-        sockets = []
-        s = None
-        for node in tree.nodes:
-            if (is_input):
-                if node.bl_idname == 'NodeGroupInput':
-                    # Group Inputs have outputs... confusing.
-                    for s in node.outputs:
-                        if (s.identifier == identifier):
-                            sockets.append(s)
-            else:
-                if node.bl_idname == 'NodeGroupOutput':
-                    for s in node.inputs:
-                        if (s.identifier == identifier):
-                            sockets.append(s)
-        sock = sockets[-1]
-        # whatever the last socket is should be OK for most of this stuff
-        self.bl_idname=sock.bl_idname
-        self.identifier = identifier
-        self.name = sock.name
-        is_linked = False
-        for s in sockets:
-            if s.is_linked:
-                is_linked = True; break
-        self.is_linked = is_linked
-        self.is_output = not is_input
-        # hopefully this doesn't matter, since it is a group node...
-        self.node = sock.node
-        self.links = []
-        for s in sockets:
-            self.links.extend(s.links)
-        # seems to werk
+# # I don't remember why I need this?
+# class GroupOutputDummySocket:
+    # # a dummy class for acting like a socket that is coming from every
+    # #  group output node
+    # def __init__(self, tree, identifier, is_input=True):
+        # #
+        # # So, we need to go through the node tree and find all 
+        # #   the Group Input sockets that match this
+        # #   socket's identifier
+        # #
+        # sockets = []
+        # s = None
+        # for node in tree.nodes:
+            # if (is_input):
+                # if node.bl_idname == 'NodeGroupInput':
+                    # # Group Inputs have outputs... confusing.
+                    # for s in node.outputs:
+                        # if (s.identifier == identifier):
+                            # sockets.append(s)
+            # else:
+                # if node.bl_idname == 'NodeGroupOutput':
+                    # for s in node.inputs:
+                        # if (s.identifier == identifier):
+                            # sockets.append(s)
+        # sock = sockets[-1]
+        # # whatever the last socket is should be OK for most of this stuff
+        # self.bl_idname=sock.bl_idname
+        # self.identifier = identifier
+        # self.name = sock.name
+        # is_linked = False
+        # for s in sockets:
+            # if s.is_linked:
+                # is_linked = True; break
+        # self.is_linked = is_linked
+        # self.is_output = not is_input
+        # # hopefully this doesn't matter, since it is a group node...
+        # self.node = sock.node
+        # self.links = []
+        # for s in sockets:
+            # self.links.extend(s.links)
+        # # seems to werk
 
 class CircularDependencyError(Exception):
     pass

+ 74 - 0
blender_manifest.toml

@@ -0,0 +1,74 @@
+schema_version = "1.0.0"
+
+# Example of manifest file for a Blender extension
+# Change the values according to your extension
+id = "mantis"
+version = "0.0.0"
+name = "Mantis"
+tagline = "Rigging Nodes"
+maintainer = "Nodespaghetti <josephbburg@protonmail.com>"
+# Supported types: "add-on", "theme"
+type = "add-on"
+
+# Optional link to documentation, support, source files, etc
+# website = "https://extensions.blender.org/add-ons/my-example-package/"
+
+# Optional list defined by Blender and server, see:
+# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
+tags = ["Rigging", "Node"]
+
+blender_version_min = "4.2.0"
+# # Optional: Blender version that the extension does not support, earlier versions are supported.
+# # This can be omitted and defined later on the extensions platform if an issue is found.
+# blender_version_max = "5.1.0"
+
+# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
+# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
+license = [
+  "SPDX:AGPL-3.0-or-later",
+]
+# Optional: required by some licenses.
+# copyright = [
+#   "2002-2024 Developer Name",
+#   "1998 Company Name",
+# ]
+
+# Optional list of supported platforms. If omitted, the extension will be available in all operating systems.
+# platforms = ["windows-x64", "macos-arm64", "linux-x64"]
+platforms = ["linux-x64"]
+# Other supported platforms: "windows-arm64", "macos-x64"
+
+# Optional: bundle 3rd party Python modules.
+# https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html
+# wheels = [
+#   "./wheels/hexdump-3.3-py3-none-any.whl",
+#   "./wheels/jsmin-3.0.1-py3-none-any.whl",
+# ]
+
+# # Optional: add-ons can list which resources they will require:
+# # * files (for access of any filesystem operations)
+# # * network (for internet access)
+# # * clipboard (to read and/or write the system clipboard)
+# # * camera (to capture photos and videos)
+# # * microphone (to capture audio)
+# #
+# # If using network, remember to also check `bpy.app.online_access`
+# # https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access
+# #
+# # For each permission it is important to also specify the reason why it is required.
+# # Keep this a single short sentence without a period (.) at the end.
+# # For longer explanations use the documentation or detail page.
+#
+# [permissions]
+# network = "Need to sync motion-capture data to server"
+# files = "Import/export FBX from/to disk"
+# clipboard = "Copy and paste bone transforms"
+
+# Optional: build settings.
+# https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build
+# [build]
+# paths_exclude_pattern = [
+#   "__pycache__/",
+#   "/.git/",
+#   "/*.zip",
+# ]

+ 14 - 15
deformer_containers.py

@@ -1,5 +1,5 @@
-from mantis.node_container_common import *
-from mantis.xForm_containers import xFormGeometryObject
+from .node_container_common import *
+from .xForm_containers import xFormGeometryObject
 from bpy.types import Node
 from .base_definitions import MantisNode
 
@@ -127,19 +127,18 @@ class DeformerArmature:
                                   'active_pose_bone':deform_bones[0],
                                   'selected_pose_bones':deform_bones,}
             #
-            bpy.ops.object.mode_set({'active_object':armOb}, mode='POSE')
-            bpy.ops.pose.select_all(action='SELECT')
-            #
-            bpy.ops.paint.weight_paint_toggle(context_override)
-            bpy.ops.paint.weight_from_bones(context_override, type='AUTOMATIC')
-            # this is not working right now but I would like to normalize stuff.
-            # hmm. I think it normalizes automatically?
-            # bpy.ops.object.vertex_group_normalize_all({'active_object':ob}, group_select_mode='BONE_DEFORM', lock_active=False)
-            bpy.ops.paint.weight_paint_toggle(context_override)
-            #
-            bpy.ops.object.mode_set({'active_object':armOb}, mode='POSE')
-            bpy.ops.pose.select_all(action='DESELECT')
-            bpy.ops.object.mode_set({'active_object':armOb}, mode='OBJECT')
+            with bContext.temp_override(**{'active_object':armOb}):
+                bpy.ops.object.mode_set(mode='POSE')
+                bpy.ops.pose.select_all(action='SELECT')
+            with bContext.temp_override(**context_override):
+                bpy.ops.paint.weight_paint_toggle()
+                bpy.ops.paint.weight_from_bones(type='AUTOMATIC')
+                bpy.ops.paint.weight_paint_toggle()
+                #
+            with bContext.temp_override(**{'active_object':armOb}):
+                bpy.ops.object.mode_set(mode='POSE')
+                bpy.ops.pose.select_all(action='DESELECT') 
+                bpy.ops.object.mode_set(mode='OBJECT')
             # TODO: modify Blender to make this available as a Python API function.
         if skin_method == "EXISTING_GROUPS":
             self.initialize_vgroups(use_existing = True)

+ 1 - 1
drivers.py

@@ -1,4 +1,4 @@
-from mantis.utilities import (prRed, prGreen, prPurple, prWhite,
+from .utilities import (prRed, prGreen, prPurple, prWhite,
                               prOrange,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
                               wrapOrange,)

+ 39 - 0
i_o.py

@@ -0,0 +1,39 @@
+# this is the I/O part of mantis. I eventually intend to make this a markup language. not right now tho lol
+
+# https://stackoverflow.com/questions/42033142/is-there-an-easy-way-to-check-if-an-object-is-json-serializable-in-python
+# thanks!
+def is_jsonable(x):
+    import json
+    try:
+        json.dumps(x)
+        return True
+    except (TypeError, OverflowError):
+        return False
+
+
+
+def export_to_json(tree, path):
+    # takes a parsed mantis tree and returns a JSON
+    tree_data = tree.parsed_tree
+
+    # we have to prepare the tree data.
+    #   SO: we can't use tuples as keys in JSON
+    #   but the tree uses a lot of tuple keys in Python dicts
+    #   so we can map the tuples to UUIDs instead
+    #  then we can store the list of nodes mapped to the same UUIDs
+    # the goal is to store the tree in a way that makes reproduction possible
+
+
+    import json
+    with open(path, "w") as file:
+        print("Writing mantis tree data to: ", file.name)
+        file.write( json.dumps(json_data, indent = 4) )
+    
+
+def import_from_json(json_data):
+    #reads a JSON file and makes a tree from it.
+    pass
+
+def tree_to_b_nodes(tree):
+    # creates a node tree in Blender from a mantis tree
+    pass

+ 2 - 2
link_containers.py

@@ -1,4 +1,4 @@
-from mantis.node_container_common import *
+from .node_container_common import *
 from bpy.types import Node
 from .base_definitions import MantisNode, GraphError
 
@@ -1440,7 +1440,7 @@ class LinkDrivenParameter:
     def bFinalize(self, bContext = None):
         # TODO HACK BUG
         # This probably no longer works
-        from mantis.drivers import CreateDrivers
+        from .drivers import CreateDrivers
         CreateDrivers( [ self.parameters["Driver"] ] )
         
     def __repr__(self):

+ 1 - 1
link_definitions.py

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

+ 7 - 7
misc_containers.py

@@ -1,8 +1,8 @@
-from mantis.node_container_common import *
+from .node_container_common import *
 
 # The fact that I need this means that some of these classes should
 #  probably be moved to link_containers.py
-from mantis.xForm_containers import xFormRoot, xFormArmature, xFormBone
+from .xForm_containers import xFormRoot, xFormArmature, xFormBone
 
 def TellClasses():
     return [
@@ -521,7 +521,7 @@ class UtilityFCurve:
 
     def bExecute(self, bContext = None,):
         prepare_parameters(self)
-        from mantis.utilities import get_node_prototype
+        from .utilities import get_node_prototype
         np = get_node_prototype(self.signature, self.base_tree)
         keys = []
         #['amplitude', 'back', 'bl_rna', 'co', 'co_ui', 'easing', 'handle_left', 'handle_left_type', 'handle_right', 'handle_right_type',
@@ -569,7 +569,7 @@ class UtilityDriver:
         self.outputs = {
           "Driver" : NodeSocket(name = "Driver", node=self),
         }
-        from mantis.drivers import MantisDriver
+        from .drivers import MantisDriver
         self.parameters = {
           "Driver Type":None, 
           "Expression":None, 
@@ -584,7 +584,7 @@ class UtilityDriver:
 
     def bExecute(self, bContext = None,):
         prepare_parameters(self)
-        from mantis.drivers import MantisDriver
+        from .drivers import MantisDriver
         #prPurple("Executing Driver Node")
         my_vars = []
         
@@ -629,7 +629,7 @@ class UtilitySwitch:
         self.outputs = {
           "Driver" : NodeSocket(name = "Driver", node=self),
         }
-        from mantis.drivers import MantisDriver
+        from .drivers import MantisDriver
         self.parameters = {
           "xForm":None, 
           "Parameter":None,
@@ -661,7 +661,7 @@ class UtilitySwitch:
         if xForm : xForm = xForm.bGetObject() 
         if not xForm:
             raise RuntimeError("Could not evaluate xForm for %s" % self)
-        from mantis.drivers import MantisDriver
+        from .drivers import MantisDriver
         my_driver ={ "owner" : None,
                      "prop"  : None, # will be filled out in the node that uses the driver 
                      "ind"   : -1, # same here

+ 3 - 3
node_container_common.py

@@ -1,8 +1,8 @@
-from mantis.utilities import (prRed, prGreen, prPurple, prWhite,
+from .utilities import (prRed, prGreen, prPurple, prWhite,
                               prOrange,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
                               wrapOrange,)
-from mantis.base_definitions import GraphError, CircularDependencyError
+from .base_definitions import GraphError, CircularDependencyError
 # BE VERY CAREFUL
 # the x_containers files import * from this file
 # so all the top-level imports are carried over
@@ -396,7 +396,7 @@ def finish_drivers(self):
             drivers.append(driver)
         else:
             prRed("Failed to create driver for %s" % prop)
-    from mantis.drivers import CreateDrivers
+    from .drivers import CreateDrivers
     CreateDrivers(drivers)
 
 

+ 1 - 1
nodes_generic.py

@@ -426,7 +426,7 @@ class UtilityDriverNode(Node, MantisNode):
         except AttributeError:
             proceed = False
         if proceed:
-            from mantis.f_nodegraph import (GetDownstreamXFormNodes, get_node_container)
+            from .f_nodegraph import (GetDownstreamXFormNodes, get_node_container)
             if (node_container := get_node_container(self, context)[0]):
                 dType = node_container.evaluate_input("Driver Type")
             else:

+ 1 - 1
ops_generate_tree.py

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

+ 34 - 3
ops_nodegroup.py

@@ -24,7 +24,8 @@ def TellClasses():
         DriverRemoveDriverVariableInput,
         # Armature Link Node
         LinkArmatureAddTargetInput,
-        LinkArmatureRemoveTargetInput,]
+        LinkArmatureRemoveTargetInput,
+        ExportNodeTreeToJSON,]
 
 def mantis_tree_poll_op(context):
     # return True
@@ -78,6 +79,8 @@ class MantisGroupNodes(Operator):
         return mantis_tree_poll_op(context)
 
 
+# IMPORTANT TODO: re-write this because it no longer work
+
 # source is https://github.com/aachman98/Sorcar/blob/master/operators/ScGroupNodes.py
 # checc here: https://github.com/nortikin/sverchok/blob/9002fd4af9ec8603e86f86ed7e567a4ed0d2e07c/core/node_group.py#L568
 
@@ -301,7 +304,7 @@ class CleanUpNodeGraph(bpy.types.Operator):
         
         base_tree=context.space_data.path[-1].node_tree
         
-        from mantis.grandalf.graphs import Vertex, Edge, Graph, graph_core
+        from .grandalf.graphs import Vertex, Edge, Graph, graph_core
         
         class defaultview(object):
             w,h = 1,1
@@ -336,7 +339,7 @@ class CleanUpNodeGraph(bpy.types.Operator):
         
 
         
-        from mantis.grandalf.layouts import SugiyamaLayout
+        from .grandalf.layouts import SugiyamaLayout
         
         
         sug = SugiyamaLayout(graph.C[0]) # no idea what .C[0] is
@@ -844,3 +847,31 @@ class LinkArmatureRemoveTargetInput(bpy.types.Operator):
         n = context.node
         n.inputs.remove(n.inputs[-1]); n.inputs.remove(n.inputs[-1])
         return {'FINISHED'}
+
+
+
+class ExportNodeTreeToJSON(Operator):
+    """Export this node tree as a JSON file"""
+    bl_idname = "mantis.export_node_tree_json"
+    bl_label = "Export Mantis Tree to JSON"
+
+    @classmethod
+    def poll(cls, context):
+        return (mantis_tree_poll_op(context))
+
+    def execute(self, context):
+        from .i_o import export_to_json
+        import bpy
+
+        tree = context.space_data.path[0].node_tree
+        tree.update_tree(context)
+
+        def remove_special_characters(stritree):
+            # https://stackoverflow.com/questions/295135/turn-a-stritree-into-a-valid-filename
+            # thank you user "Sophie Gage"
+            import re # regular expressions
+            return re.sub('[^\w_.)( -]', '', stritree)
+
+        path = bpy.path.abspath('//')+remove_special_characters(tree.name)+".json"
+        export_to_json(tree, path)
+        return {"FINISHED"}

+ 1 - 1
primitives_containers.py

@@ -1,4 +1,4 @@
-from mantis.node_container_common import *
+from .node_container_common import *
 from bpy.types import Node
 from .base_definitions import MantisNode
 

+ 13 - 7
readtree.py

@@ -6,7 +6,7 @@ def class_for_mantis_prototype_node(prototype_node):
     """ This is a class which returns a class to instantiate for
         the given prototype node."""
     #from .node_container_classes import TellClasses
-    from mantis import xForm_containers, link_containers, misc_containers, primitives_containers, deformer_containers
+    from . import xForm_containers, link_containers, misc_containers, primitives_containers, deformer_containers
     classes = {}
     for module in [xForm_containers, link_containers, misc_containers, primitives_containers, deformer_containers]:
         for cls in module.TellClasses():
@@ -121,7 +121,7 @@ def class_for_mantis_prototype_node(prototype_node):
 # This is really, really stupid HACK
 def gen_nc_input_for_data(socket):
     # Class List #TODO deduplicate
-    from mantis import xForm_containers, link_containers, misc_containers, primitives_containers, deformer_containers
+    from . import xForm_containers, link_containers, misc_containers, primitives_containers, deformer_containers
     classes = {}
     for module in [xForm_containers, link_containers, misc_containers, primitives_containers, deformer_containers]:
         for cls in module.TellClasses():
@@ -288,7 +288,7 @@ def reroute_links_grpout(nc, all_nc):
 
 
 def data_from_tree(base_tree, tree_path = [None], dummy_nodes = {}, all_nc = {}):
-    from mantis.node_container_common import NodeSocket
+    from .node_container_common import NodeSocket
     from .internal_containers import DummyNode
     nc_dict = {}
     tree_path_names = [tree.name for tree in tree_path if hasattr(tree, "name")]
@@ -358,6 +358,8 @@ def data_from_tree(base_tree, tree_path = [None], dummy_nodes = {}, all_nc = {})
             to_s = inp.identifier
             if not inp.is_linked:
                 nc_cls = gen_nc_input_for_data(inp)
+                print (inp)
+                prRed(nc_cls)
                 # at this point we also need to get the "Dummy Node" for
                 #  this node group.
                 if (nc_cls):
@@ -385,6 +387,8 @@ def data_from_tree(base_tree, tree_path = [None], dummy_nodes = {}, all_nc = {})
                         sig =  tuple( [None] + tree_path_names +[from_socket.node.bl_idname])
                         from_s = from_socket.identifier
                     nc_from = nc_dict.get(sig)
+            # this can be None. Why?
+            prRed (sig)
             nc_from.outputs[from_s].connect(node=nc_to, socket=to_s)
             nc_from = None
         nc_to = None
@@ -492,7 +496,7 @@ to_name_filter = [
 
 def sort_tree_into_layers(nodes, context):
     from time import time
-    from mantis.node_container_common import (get_depth_lines,
+    from .node_container_common import (get_depth_lines,
       node_depth)
     # All this function needs to do is sort out the hierarchy and
     #  get things working in order of their dependencies.
@@ -565,7 +569,7 @@ def sort_tree_into_layers(nodes, context):
 def execute_tree(nodes, base_tree, context):
     import bpy
     from time import time
-    from mantis.node_container_common import GraphError
+    from .node_container_common import GraphError
     start_time  = time()
     original_active = context.view_layer.objects.active
     
@@ -600,7 +604,8 @@ def execute_tree(nodes, base_tree, context):
     # TODO it's possible but unlikely that the user will try to run a 
     #    graph with no armature nodes in it.
     if (active):
-        bpy.ops.object.mode_set({'active_object':active, 'selected_objects':switch_me}, mode='POSE')
+        with context.temp_override(**{'active_object':active, 'selected_objects':switch_me}):
+            bpy.ops.object.mode_set(mode='POSE')
     
     #               Execute second pass (Link, Driver)                 #
     for i in range(len(layers)):
@@ -634,7 +639,8 @@ def execute_tree(nodes, base_tree, context):
                 switch_me.append(ob)
                 active = ob
     if (active):
-        bpy.ops.object.mode_set({'active_object':active, 'selected_objects':switch_me}, mode='OBJECT')
+        with context.temp_override(**{'active_object':active, 'selected_objects':switch_me}):
+            bpy.ops.object.mode_set(mode='OBJECT')
     
     prGreen("Executed Tree in %s seconds" % (time() - start_execution_time))
     prGreen("Finished executing tree in %f seconds" % (time() - start_time))

+ 52 - 21
socket_definitions.py

@@ -2,7 +2,7 @@ import bpy
 from bpy.types import NodeSocket, NodeSocketStandard
 
 
-from mantis.utilities import (prRed, prGreen, prPurple, prWhite,
+from .utilities import (prRed, prGreen, prPurple, prWhite,
                               prOrange,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
                               wrapOrange,)
@@ -50,7 +50,7 @@ cDriverVariable = (0.66, 0.33, 0.04, 1.0)
 cFCurve         = (0.77, 0.77, 0.11, 1.0)
 cKeyframe       = (0.06, 0.22, 0.88, 1.0)
 cEnable         = (0.92, 0.92, 0.92, 1.0)
-cLayerMask      = (0.82, 0.82, 0.82, 1.0)
+cBoneCollection      = (0.82, 0.82, 0.82, 1.0)
 cDeformer       = (0.05, 0.08, 0.45, 1.0)
 
 
@@ -94,8 +94,10 @@ def TellClasses():
              DriverVariableSocket,
              FCurveSocket,
              KeyframeSocket,
-             LayerMaskSocket,
-             LayerMaskInputSocket,
+            #  LayerMaskSocket,
+            #  LayerMaskInputSocket,
+            BoneCollectionSocket,
+            BoneCollectionInputSocket,
              
              xFormParameterSocket,
              ParameterBoolSocket,
@@ -176,7 +178,11 @@ def default_update(socket, context, do_execute=True):
         return
     if not hasattr(context.space_data, "path"):
         return
-    node_tree = context.space_data.path[0].node_tree
+    try:
+        node_tree = context.space_data.path[0].node_tree
+    except IndexError: # not in the UI, for example, in a script instead.
+        node_tree = None
+        return
     if node_tree.do_live_update:
         # I don't know how the tree can be valid at 0 nodes but doesn't hurt
         #  to force it if this somehow happens.
@@ -187,7 +193,7 @@ def default_update(socket, context, do_execute=True):
         elif (node_tree.tree_valid == True):
             # prGreen("Partial Update From Socket Change.")
             # We don't have to update the whole thing, just the socket
-            from mantis.utilities import tree_from_nc
+            from .utilities import tree_from_nc
             for nc in node_tree.parsed_tree.values():
                 try:
                     if (tree_from_nc(nc.signature, nc.base_tree) == socket.node.id_data):
@@ -637,32 +643,55 @@ class StringSocket(bpy.types.NodeSocketString):
     def draw_color(self, context, node):
         return self.color
 
-class LayerMaskSocket(bpy.types.NodeSocket):
-    """Layer Mask Input socket"""
-    bl_idname = 'LayerMaskSocket'
-    bl_label = "Layer Mask"
-    default_value: bpy.props.BoolVectorProperty(subtype = "LAYER", update = update_socket, size=32)
-    color = cLayerMask
+# class LayerMaskSocket(bpy.types.NodeSocket):
+#     """Layer Mask Input socket"""
+#     bl_idname = 'LayerMaskSocket'
+#     bl_label = "Layer Mask"
+#     default_value: bpy.props.BoolVectorProperty(subtype = "LAYER", update = update_socket, size=32)
+#     color = cBoneCollection
+#     input : bpy.props.BoolProperty(default =False,)
+#     def draw(self, context, layout, node, text):
+#         ChooseDraw(self, context, layout, node, text)
+#     def draw_color(self, context, node):
+#         return self.color
+        
+# class LayerMaskInputSocket(bpy.types.NodeSocket): # I can probably use inheritance somehow lol
+#     """Layer Mask Input socket"""
+#     bl_idname = 'LayerMaskInputSocket'
+#     bl_label = "Layer Mask"
+#     default_value: bpy.props.BoolVectorProperty(subtype = "LAYER", update = update_socket, size=32)
+#     color = cBoneCollection
+#     input : bpy.props.BoolProperty(default =True,)
+#     def draw(self, context, layout, node, text):
+#         ChooseDraw(self, context, layout, node, text)
+#     def draw_color(self, context, node):
+#         return self.color
+
+
+class BoneCollectionSocket(bpy.types.NodeSocket):
+    """Bone Collection socket"""
+    bl_idname = 'BoneCollectionSocket'
+    bl_label = "Bone Collection"
+    default_value: bpy.props.StringProperty(default = "Collection", update = update_socket,)
     input : bpy.props.BoolProperty(default =False,)
+    color = cBoneCollection
     def draw(self, context, layout, node, text):
         ChooseDraw(self, context, layout, node, text)
     def draw_color(self, context, node):
         return self.color
         
-class LayerMaskInputSocket(bpy.types.NodeSocket): # I can probably use inheritance somehow lol
-    """Layer Mask Input socket"""
-    bl_idname = 'LayerMaskInputSocket'
-    bl_label = "Layer Mask"
-    default_value: bpy.props.BoolVectorProperty(subtype = "LAYER", update = update_socket, size=32)
-    color = cLayerMask
+class BoneCollectionInputSocket(bpy.types.NodeSocket):
+    """Bone Collection Input Socket"""
+    bl_idname = 'BoneCollectionInputSocket'
+    bl_label = "Bone Collection"
+    default_value: bpy.props.StringProperty(default = "Collection", update = update_socket,)
     input : bpy.props.BoolProperty(default =True,)
+    color = cBoneCollection
     def draw(self, context, layout, node, text):
         ChooseDraw(self, context, layout, node, text)
     def draw_color(self, context, node):
         return self.color
 
-
-
 #####################################################################################
 # Parameters
 #####################################################################################
@@ -813,7 +842,9 @@ class EnumMetaRigSocket(NodeSocket):
     search_prop:PointerProperty(type=bpy.types.Object, poll=poll_is_armature, update=update_metarig_armature)
     
     def get_default_value(self):
-        return self.search_prop.name
+        if self.search_prop:
+            return self.search_prop.name
+        return ""
     
     default_value  : StringProperty(name = "", get=get_default_value)
     

+ 3 - 3
utilities.py

@@ -407,9 +407,9 @@ def to_mathutils_value(socket):
             return val
         if (isinstance(socket, socket_definitions.BooleanThreeTupleSocket)):
             return (val[0], val[1], val[2]) # we'll send a tuple out
-        if ((isinstance(socket, socket_definitions.LayerMaskSocket)) or
-            (isinstance(socket, socket_definitions.LayerMaskInputSocket))):
-            return tuple(val) # should werk
+        # if ((isinstance(socket, socket_definitions.LayerMaskSocket)) or
+        #     (isinstance(socket, socket_definitions.LayerMaskInputSocket))):
+        #     return tuple(val) # should werk
     else:
         return None
 

+ 78 - 52
xForm_containers.py

@@ -1,4 +1,4 @@
-from mantis.node_container_common import *
+from .node_container_common import *
 from bpy.types import Node
 from .base_definitions import MantisNode
 
@@ -124,7 +124,7 @@ class xFormArmature:
                         ob.animation_data.drivers.remove(ob.animation_data.drivers[-1])
             for pb in ob.pose.bones:
                 # clear it, even after deleting the edit bones, 
-                #  if we create them again the pose bones will be reused
+                #  if we create them again the pose bones will be reused 
                 while (pb.constraints):
                     pb.constraints.remove(pb.constraints[-1])
                 pb.location = (0,0,0)
@@ -132,6 +132,16 @@ class xFormArmature:
                 pb.rotation_quaternion = (1.0,0,0,0)
                 pb.rotation_axis_angle = (0,0,1.0,0)
                 pb.scale = (1.0,1.0,1.0)
+            # feels ugly and bad, whatever
+            collections = []
+            for bc in ob.data.collections:
+                collections.append(bc)
+            for bc in collections:
+                ob.data.collections.remove(bc)
+            del collections
+            # end ugly/bad
+
+
         else:
             # Create the Object
             ob = bpy.data.objects.new(name, bpy.data.armatures.new(name)) #create ob
@@ -176,7 +186,8 @@ class xFormArmature:
         selected.append(ob)
         context_override = {"active_object":ob, "selected_objects":selected}
         print("Changing Armature Mode to " +wrapPurple("EDIT"))
-        bpy.ops.object.mode_set(context_override, mode='EDIT')
+        with bContext.temp_override(**context_override):
+            bpy.ops.object.mode_set(mode='EDIT')
         if ob.mode != "EDIT":
             prRed("eh?")
         # clear it
@@ -232,7 +243,7 @@ class xFormBone:
          "Z Min"          : NodeSocket(is_input = True, name = "Z Min", node = self,),
          "Z Max"          : NodeSocket(is_input = True, name = "Z Max", node = self,),
          # Visual stuff
-         "Layer Mask"                         : NodeSocket(is_input = True, name = "Layer Mask", node = self,),
+         "Bone Collection"                         : NodeSocket(is_input = True, name = "Bone Collection", node = self,),
          "Hide"                               : NodeSocket(is_input = True, name = "Hide", node = self,),
          "Custom Object"                      : NodeSocket(is_input = True, name = "Custom Object", node = self,),
          "Custom Object xForm Override"       : NodeSocket(is_input = True, name = "Custom Object xForm Override", node = self,),
@@ -241,7 +252,6 @@ class xFormBone:
          "Custom Object Scale"                : NodeSocket(is_input = True, name = "Custom Object Scale", node = self,),
          "Custom Object Translation"          : NodeSocket(is_input = True, name = "Custom Object Translation", node = self,),
          "Custom Object Rotation"             : NodeSocket(is_input = True, name = "Custom Object Rotation", node = self,),
-         "Bone Group"                         : NodeSocket(is_input = True, name = "Bone Group", node = self,),
          # Deform Stuff
          "Deform"               : NodeSocket(is_input = True, name = "Deform", node = self,),
          "Envelope Distance"    : NodeSocket(is_input = True, name = "Envelope Distance", node = self,),
@@ -270,9 +280,8 @@ class xFormBone:
          "Y Max":None,
          "Z Min":None,
          "Z Max":None,
-         "Layer Mask":None,
          "Hide":None,
-         "Layer Mask":None,
+         "Bone Collection":None,
          "Hide":None,
          "Custom Object":None,
          "Custom Object xForm Override":None,
@@ -281,7 +290,6 @@ class xFormBone:
          "Custom Object Scale":None,
          "Custom Object Translation":None,
          "Custom Object Rotation":None,
-         "Bone Group"           : None,
          "Deform"               : None,
          "Envelope Distance"    : None,
          "Envelope Weight"      : None,
@@ -355,6 +363,23 @@ class xFormBone:
         # Create the Object
         d = xF.data
         eb = d.edit_bones.new(name)
+
+        # Bone Collections:
+        #    We treat each separate string as a Bone Collection that this object belongs to
+        #    Bone Collections are fully qualified by their hierarchy.
+        #    Separate Strings with "|" and indicate hierarchy with ">". These are special characters.
+        # NOTE: if the user names the collections differently at different times, this will take the FIRST definition and go with it
+        sCols = self.evaluate_input("Bone Collection")
+        bone_collections = sCols.split("|")
+        for collection_list in bone_collections:
+            hierarchy = collection_list.split(">")
+            col_parent = None
+            for i, sCol in enumerate(hierarchy):
+                if ( col := d.collections.get(sCol) ) is None:
+                    col = d.collections.new(sCol)
+                col.parent = col_parent
+                col_parent = col
+            col.assign(eb)
         
         if (eb.name != name):
             raise RuntimeError("Could not create bone ", name, "; Perhaps there is a duplicate bone name in the node tree?")
@@ -372,7 +397,6 @@ class xFormBone:
         assert (self.bObject), "eh? %s" % eb.name
         
         self.bSetParent(eb)
-        eb.layers = self.evaluate_input("Layer Mask")
         
         
         # Setup Deform attributes...
@@ -388,7 +412,7 @@ class xFormBone:
 
     def bFinalize(self, bContext = None):
         import bpy
-        from mantis.drivers import MantisDriver
+        from .drivers import MantisDriver
         # prevAct = bContext.view_layer.objects.active
         # bContext.view_layer.objects.active = ob
         # bpy.ops.object.mode_set(mode='OBJECT')
@@ -530,49 +554,51 @@ class xFormBone:
         pb.custom_shape_rotation_euler = self.evaluate_input("Custom Object Rotation")
         pb.use_custom_shape_bone_size = self.evaluate_input("Custom Object Scale to Bone Length")
         pb.bone.show_wire = self.evaluate_input("Custom Object Wireframe")
-        #
-        # Bone Groups
-        if bg_name := self.evaluate_input("Bone Group"): # this is a string
-            obArm = self.bGetParentArmature()
-            # Temporary! Temporary! HACK
-            color_set_items= [
-                               "DEFAULT",
-                               "THEME01",
-                               "THEME02",
-                               "THEME03",
-                               "THEME04",
-                               "THEME05",
-                               "THEME06",
-                               "THEME07",
-                               "THEME08",
-                               "THEME09",
-                               "THEME10",
-                               "THEME11",
-                               "THEME12",
-                               "THEME13",
-                               "THEME14",
-                               "THEME15",
-                               "THEME16",
-                               "THEME17",
-                               "THEME18",
-                               "THEME19",
-                               "THEME20",
-                               # "CUSTOM",
-                             ]
-            try:
-                bg = obArm.pose.bone_groups.get(bg_name)
-            except SystemError:
-                bg = None
-                pass # no clue why this happens. uninitialzied?
-            if not bg:
-                bg = obArm.pose.bone_groups.new(name=bg_name)
-                #HACK lol
-                from random import randint
-                bg.color_set = color_set_items[randint(0,14)]
-                #15-20 are black by default, gross
-                # this is good enough for now!
+        # #
+        # # D E P R E C A T E D
+        # #
+        # # Bone Groups
+        # if bg_name := self.evaluate_input("Bone Group"): # this is a string
+        #     obArm = self.bGetParentArmature()
+        #     # Temporary! Temporary! HACK
+        #     color_set_items= [
+        #                        "DEFAULT",
+        #                        "THEME01",
+        #                        "THEME02",
+        #                        "THEME03",
+        #                        "THEME04",
+        #                        "THEME05",
+        #                        "THEME06",
+        #                        "THEME07",
+        #                        "THEME08",
+        #                        "THEME09",
+        #                        "THEME10",
+        #                        "THEME11",
+        #                        "THEME12",
+        #                        "THEME13",
+        #                        "THEME14",
+        #                        "THEME15",
+        #                        "THEME16",
+        #                        "THEME17",
+        #                        "THEME18",
+        #                        "THEME19",
+        #                        "THEME20",
+        #                        # "CUSTOM",
+        #                      ]
+        #     try:
+        #         bg = obArm.pose.bone_groups.get(bg_name)
+        #     except SystemError:
+        #         bg = None
+        #         pass # no clue why this happens. uninitialzied?
+        #     if not bg:
+        #         bg = obArm.pose.bone_groups.new(name=bg_name)
+        #         #HACK lol
+        #         from random import randint
+        #         bg.color_set = color_set_items[randint(0,14)]
+        #         #15-20 are black by default, gross
+        #         # this is good enough for now!
             
-            pb.bone_group = bg
+        #     pb.bone_group = bg
             
         
         

+ 9 - 10
xForm_definitions.py

@@ -1,7 +1,7 @@
 import bpy
 from .base_definitions import xFormNode
 from bpy.types import Node
-from mantis.utilities import (prRed, prGreen, prPurple, prWhite,
+from .utilities import (prRed, prGreen, prPurple, prWhite,
                               prOrange,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
                               wrapOrange,)
@@ -99,7 +99,7 @@ class xFormBoneNode(Node, xFormNode):
         
         # visual settings:
         b = []
-        b.append(self.inputs.new ('LayerMaskSocket', "Layer Mask"))
+        b.append(self.inputs.new ('BoneCollectionInputSocket', "Bone Collection"))
         b.append(self.inputs.new ('xFormSocket', "Custom Object"))
         b.append(self.inputs.new ('xFormSocket', "Custom Object xForm Override"))
         b.append(self.inputs.new ('BooleanSocket', "Custom Object Scale to Bone Length"))
@@ -107,8 +107,7 @@ class xFormBoneNode(Node, xFormNode):
         b.append(self.inputs.new ('VectorScaleSocket', "Custom Object Scale"))
         b.append(self.inputs.new ('VectorSocket', "Custom Object Translation"))
         b.append(self.inputs.new ('VectorEulerSocket', "Custom Object Rotation"))
-        b.append(self.inputs.new ('StringSocket', "Bone Group"))
-        # 16-22
+        # 16-21
         # Deform Settings:
         c = []
         c.append(self.inputs.new ('BooleanSocket', "Deform"))
@@ -117,7 +116,7 @@ class xFormBoneNode(Node, xFormNode):
         c.append(self.inputs.new ('BooleanSocket', "Envelope Multiply"))
         c.append(self.inputs.new ('FloatPositiveSocket', "Envelope Head Radius"))
         c.append(self.inputs.new ('FloatPositiveSocket', "Envelope Tail Radius"))
-        #24-28
+        #22-27
         
         # c[0].default_value=False
         
@@ -128,7 +127,7 @@ class xFormBoneNode(Node, xFormNode):
         for sock in a:
             sock.hide = True
         for sock in b:
-            if sock.name in ['Custom Object', 'Layer Mask']:
+            if sock.name in ['Custom Object', 'Bone Collection']:
                 continue
             sock.hide = True
         for sock in c:
@@ -221,17 +220,17 @@ class xFormBoneNode(Node, xFormNode):
                 for inp in self.inputs[4:14]:
                     inp.hide = True
             if self.display_vp_settings == True:
-                for inp in self.inputs[16:22]:
+                for inp in self.inputs[16:21]:
                     inp.hide = False
             else:
-                for inp in self.inputs[16:22]:
+                for inp in self.inputs[16:21]:
                     inp.hide = True
             #
             if self.display_def_settings == True:
-                for inp in self.inputs[24:29]:
+                for inp in self.inputs[23:28]:
                     inp.hide = False
             else:
-                for inp in self.inputs[24:29]:
+                for inp in self.inputs[23:28]:
                     inp.hide = True