Browse Source

Fix: Sockets data wiped when opening file

There were a whole host of errors
 - some caused by the register/unregister code happening in the wrong order
 - some caused by update() running before initialization
 - some caused by genuinely bad programming in utilities.py
Joseph Brandenburg 7 months ago
parent
commit
e1fbca847e
6 changed files with 128 additions and 58 deletions
  1. 20 12
      __init__.py
  2. 7 6
      base_definitions.py
  3. 4 1
      deformer_definitions.py
  4. 35 7
      schema_definitions.py
  5. 20 0
      socket_definitions.py
  6. 42 32
      utilities.py

+ 20 - 12
__init__.py

@@ -21,7 +21,6 @@ MANTIS_VERSION_SUB=1
 classLists = [module.TellClasses() for module in [
  link_definitions,
  xForm_definitions,
- base_definitions,
  nodes_generic,
  socket_definitions,
  ops_nodegroup,
@@ -30,6 +29,7 @@ classLists = [module.TellClasses() for module in [
  math_definitions,
  i_o,
  schema_definitions,
+ base_definitions,
 ]]
 classLists.append( [GenerateMantisTree] )
 #
@@ -313,6 +313,10 @@ def do_version_update(node_tree):
 
 @persistent
 def version_update_handler(filename):
+    for node_tree in bpy.data.node_groups: # ensure it can update again after file load.
+        if node_tree.bl_idname in ["MantisTree", "SchemaTree"]:
+                node_tree.is_exporting=False; node_tree.is_executing=False
+
     for node_tree in bpy.data.node_groups:
         if node_tree.bl_idname in ["MantisTree", "SchemaTree"]:
             if (node_tree.mantis_version[0] < MANTIS_VERSION_MAJOR) or \
@@ -365,9 +369,8 @@ def register():
         try:
             register_class(cls)
         except RuntimeError as e:
-            prRed(cls.__name__)
+            prRed(f"Registration error for class: {cls.__name__}")
             raise e
-
     nodeitems_utils.register_node_categories('MantisNodeCategories', node_categories)
     nodeitems_utils.register_node_categories('SchemaNodeCategories', schema_categories)
 
@@ -377,19 +380,24 @@ def register():
         k.active = True
         addon_keymaps.append((km, k))
     # add the handlers
-    bpy.app.handlers.depsgraph_update_pre.append(update_handler)
-    bpy.app.handlers.depsgraph_update_post.append(execute_handler)
-    bpy.app.handlers.save_pre.append(on_save_pre_handler)
-    bpy.app.handlers.save_post.append(on_save_post_handler)
-    bpy.app.handlers.load_post.append(version_update_handler)
-    bpy.app.handlers.animation_playback_pre.append(on_animation_playback_pre_handler)
-    bpy.app.handlers.animation_playback_post.append(on_animation_playback_post_handler)
-    bpy.app.handlers.undo_pre.append(on_undo_pre_handler)
+    bpy.app.handlers.depsgraph_update_pre.insert(0, update_handler)
+    bpy.app.handlers.depsgraph_update_post.insert(0, execute_handler)
+    bpy.app.handlers.save_pre.insert(0, on_save_pre_handler)
+    bpy.app.handlers.save_post.insert(0, on_save_post_handler)
+    bpy.app.handlers.load_post.insert(0, version_update_handler)
+    bpy.app.handlers.animation_playback_pre.insert(0, on_animation_playback_pre_handler)
+    bpy.app.handlers.animation_playback_post.insert(0, on_animation_playback_post_handler)
+    bpy.app.handlers.undo_pre.insert(0, on_undo_pre_handler)
+    # I'm adding mine in first to ensure other addons don't mess up mine
+    # but I am a good citizen! so my addon won't mess up yours! probably...
 
 
     
 
 def unregister():
+    for tree in bpy.data.node_groups: # ensure it doesn't try to update while quitting.
+        if tree.bl_idname in ['MantisTree, SchemaTree']:
+            tree.is_exporting=True; tree.is_executing=True
     nodeitems_utils.unregister_node_categories('MantisNodeCategories')
     nodeitems_utils.unregister_node_categories('SchemaNodeCategories')
 
@@ -399,4 +407,4 @@ def unregister():
     
     for km, kmi in addon_keymaps:
         km.keymap_items.remove(kmi)
-    addon_keymaps.clear()
+    addon_keymaps.clear()

+ 7 - 6
base_definitions.py

@@ -61,7 +61,7 @@ class MantisTree(NodeTree):
         @classmethod
         def valid_socket_type(cls : NodeTree, socket_idname: str):
             return valid_interface_types(cls, socket_idname)
-            
+
     def update_tree(self, context = None):
         if self.is_exporting:
             return
@@ -101,9 +101,6 @@ class MantisTree(NodeTree):
         #    - Non-hierarchy links should be ignored in the circle-check and so the links should be marked valid in such a circle
         #    - hierarchy-links should be marked invalid and prevent the tree from executing.
 
-
-        
-    
     def execute_tree(self,context, error_popups = False):
         self.prevent_next_exec = False
         if self.is_exporting:
@@ -119,7 +116,6 @@ class MantisTree(NodeTree):
         finally:
             self.is_executing = False
 
-
 class SchemaTree(NodeTree):
     '''A node tree representing a schema to generate a Mantis tree'''
     bl_idname = 'SchemaTree'
@@ -216,6 +212,7 @@ def node_group_update(node, force = False):
         if (node.id_data.do_live_update == False) or (node.id_data.is_executing == True):
             return
     # note: if (node.id_data.is_exporting == True) I need to be able to update so I can make links.
+
     toggle_update = node.id_data.do_live_update
     node.id_data.do_live_update = False
 
@@ -273,7 +270,11 @@ def node_group_update(node, force = False):
         return
 
     if update_input or update_output:
-        socket_map_in, socket_map_out = get_socket_maps(node)
+        socket_maps = get_socket_maps(node)
+        if socket_maps is None:
+            node.id_data.do_live_update = toggle_update
+            return
+        socket_map_in, socket_map_out = socket_maps
 
         if update_input :
             if node.bl_idname == 'MantisSchemaGroup':

+ 4 - 1
deformer_definitions.py

@@ -134,7 +134,10 @@ class DeformerMorphTargetDeform(Node, DeformerNode):
     def update_morph_deformer(self, force=False):
         self.initialized = False
         # use_offset = self.inputs["Use Offset"].default_value
-        input_map = get_socket_maps(self)[0]
+        socket_maps = get_socket_maps(self)
+        if socket_maps is None:
+            return
+        input_map = socket_maps[0]
         # checc to see if targets have been removed... then modify the input map if necessary
         targets_deleted = 0 # this should usually be either 0 or 1
         for i in range(self.num_targets):

+ 35 - 7
schema_definitions.py

@@ -54,8 +54,12 @@ class SchemaArrayInput(Node, SchemaUINode):
         self.update()
 
     def update(self):
+        if len(self.id_data.interface.items_tree) == 0: return
         # self.initialized = False
-        output_map = get_socket_maps(self)[1]
+        socket_maps = get_socket_maps(self)
+        if socket_maps is None:
+            return
+        output_map = socket_maps[1]
         self.outputs.clear()
         for item in self.id_data.interface.items_tree:
             if item.item_type == 'PANEL': continue
@@ -81,8 +85,12 @@ class SchemaArrayInputGet(Node, SchemaUINode):
         self.update()
 
     def update(self):
+        if len(self.id_data.interface.items_tree) == 0: return
         # self.initialized = False
-        output_map = get_socket_maps(self)[1]
+        socket_maps = get_socket_maps(self)
+        if socket_maps is None:
+            return
+        output_map = socket_maps[1]
         self.outputs.clear()
         for item in self.id_data.interface.items_tree:
             if item.item_type == 'PANEL': continue
@@ -106,8 +114,12 @@ class SchemaArrayOutput(Node, SchemaUINode):
         self.update()
 
     def update(self):
+        if len(self.id_data.interface.items_tree) == 0: return
         self.initialized = False
-        input_map = get_socket_maps(self)[0]
+        socket_maps = get_socket_maps(self)
+        if socket_maps is None:
+            return
+        input_map = socket_maps[0]
         self.inputs.clear()
         for item in self.id_data.interface.items_tree:
             if item.item_type == 'PANEL': continue
@@ -133,8 +145,12 @@ class SchemaConstInput(Node, SchemaUINode):
         self.update()
 
     def update(self):
+        if len(self.id_data.interface.items_tree) == 0: return
         self.initialized = False
-        output_map = get_socket_maps(self)[1]
+        socket_maps = get_socket_maps(self)
+        if socket_maps is None:
+            return
+        output_map = socket_maps[1]
         self.outputs.clear()
         for item in self.id_data.interface.items_tree:
             if item.item_type == 'PANEL': continue
@@ -160,8 +176,12 @@ class SchemaConstOutput(Node, SchemaUINode):
         self.update()
 
     def update(self):
+        if len(self.id_data.interface.items_tree) == 0: return
         self.initialized = False
-        input_map = get_socket_maps(self)[0]
+        socket_maps = get_socket_maps(self)
+        if socket_maps is None:
+            return
+        input_map = socket_maps[0]
         self.inputs.clear()
         s = self.inputs.new('IntSocket', "Expose when N==")
         for item in self.id_data.interface.items_tree:
@@ -192,8 +212,12 @@ class SchemaOutgoingConnection(Node, SchemaUINode):
         self.update()
 
     def update(self):
+        if len(self.id_data.interface.items_tree) == 0: return
         self.initialized = False
-        input_map = get_socket_maps(self)[0]
+        socket_maps = get_socket_maps(self)
+        if socket_maps is None:
+            return
+        input_map = socket_maps[0]
         self.inputs.clear()
         for item in self.id_data.interface.items_tree:
             if item.item_type == 'PANEL': continue
@@ -221,8 +245,12 @@ class SchemaIncomingConnection(Node, SchemaUINode):
         self.update()
 
     def update(self):
+        if len(self.id_data.interface.items_tree) == 0: return
         self.initialized = False
-        output_map = get_socket_maps(self)[1]
+        socket_maps = get_socket_maps(self)
+        if socket_maps is None:
+            return
+        output_map = socket_maps[1]
         self.outputs.clear()
         for item in self.id_data.interface.items_tree:
             if item.item_type == 'PANEL': continue

+ 20 - 0
socket_definitions.py

@@ -1,6 +1,26 @@
 import bpy
 from bpy.types import NodeSocket, NodeSocketStandard
 
+# Classes which do not have default_value
+# needed to detect when there is an error updating dynamic nodes
+no_default_value= [
+            'MantisSocket',
+            'RelationshipSocket',
+            'DeformerSocket',
+            'xFormSocket',
+            'GeometrySocket',
+            'GenericRotationSocket',
+            'FCurveSocket',
+            'DriverSocket',
+            'DriverVariableSocket',
+            'xFormParameterSocket',
+            'MorphTargetSocket',
+            'KeyframeSocket',
+            'WildcardSocket',
+]
+# the sockets that do not have this field do not transfer data.
+# instead, it is the link itself which is meaningful.
+
 class MantisSocket(NodeSocket):
     is_valid_interface_type=False
     

+ 42 - 32
utilities.py

@@ -85,6 +85,7 @@ def get_node_prototype(sig, base_tree):
 # groups and changing sockets -- this is used extensively by Schema.
 ##################################################################################################
 
+# this one returns None if there is an error.
 def get_socket_maps(node):
     maps = [{}, {}]
     node_collection = ["inputs", "outputs"]
@@ -93,11 +94,24 @@ def get_socket_maps(node):
         for sock in getattr(node, collection):
             if sock.is_linked:
                 map[sock.identifier]=[ getattr(l, link) for l in sock.links ]
+            elif hasattr(sock, "default_value"):
+                try:
+                    val = sock["default_value"]
+                    if val is None:
+                        raise RuntimeError(f"ERROR: Could not get socket data for socket of type: {sock.bl_idname}")
+                    map[sock.identifier]=sock["default_value"]
+                except KeyError: # The node socket is not initialized yet.
+                    return None
             else:
-                map[sock.identifier]=sock.get("default_value")
+                from .socket_definitions import no_default_value
+                if sock.bl_idname in no_default_value:
+                    map[sock.identifier]=None
+                else:
+                    raise RuntimeError(f"ERROR: Could not get socket data for socket of type: {sock.bl_idname}")
     return maps
 
 def do_relink(node, s, map, in_out='INPUT', parent_name = ''):
+    if not node.__class__.is_registered_node_type(): return
     tree = node.id_data; interface_in_out = 'OUTPUT' if in_out == 'INPUT' else 'INPUT'
     if hasattr(node, "node_tree"):
         tree = node.node_tree
@@ -105,32 +119,32 @@ def do_relink(node, s, map, in_out='INPUT', parent_name = ''):
     from bpy.types import NodeSocket
     get_string = '__extend__'
     if s: get_string = s.identifier
-    if val := map.get(get_string):
-        if isinstance(val, list):
-            for sub_val in val:
-                # this will only happen once because it assigns s, so it is safe to do in the for loop.
-                if s is None:
-                    # prGreen("zornpt")
-                    name = unique_socket_name(node, sub_val, tree)
-                    sock_type = sub_val.bl_idname
-                    if parent_name:
-                        interface_socket = update_interface(tree.interface, name, interface_in_out, sock_type, parent_name)
-                    if in_out =='INPUT':
-                        s = node.inputs.new(sock_type, name, identifier=interface_socket.identifier)
-                    else:
-                        s = node.outputs.new(sock_type, name, identifier=interface_socket.identifier)
-                    if parent_name == 'Array': s.display_shape='SQUARE_DOT'
-                    # then move it up and delete the other link.
-                    # this also needs to modify the interface of the node tree.
-                    
-                    
-                #
-                if isinstance(sub_val, NodeSocket):
-                    if in_out =='INPUT':
-                        node.id_data.links.new(input=sub_val, output=s)
-                    else:
-                        node.id_data.links.new(input=s, output=sub_val)
-        else:
+    if hasattr(node, "node_tree") 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
+    val = map[get_string] # this will throw an error if the socket isn't there. Good!
+    if isinstance(val, list):
+        for sub_val in val:
+            # this will only happen once because it assigns s, so it is safe to do in the for loop.
+            if s is None:
+                name = unique_socket_name(node, sub_val, tree)
+                sock_type = sub_val.bl_idname
+                if parent_name:
+                    interface_socket = update_interface(tree.interface, name, interface_in_out, sock_type, parent_name)
+                if in_out =='INPUT':
+                    s = node.inputs.new(sock_type, name, identifier=interface_socket.identifier)
+                else:
+                    s = node.outputs.new(sock_type, name, identifier=interface_socket.identifier)
+                if parent_name == 'Array': s.display_shape='SQUARE_DOT'
+                # then move it up and delete the other link.
+                # this also needs to modify the interface of the node tree.
+            if isinstance(sub_val, NodeSocket):
+                if in_out =='INPUT':
+                    node.id_data.links.new(input=sub_val, output=s)
+                else:
+                    node.id_data.links.new(input=s, output=sub_val)
+    elif get_string != "__extend__":
+        if not s.is_output:
             try:
                 s.default_value = val
             except (AttributeError, ValueError): # must be readonly or maybe it doesn't have a d.v.
@@ -149,14 +163,10 @@ def update_interface(interface, name, in_out, sock_type, parent_name):
         raise RuntimeError(wrapRed("Cannot add interface item to tree without specifying type."))
 
 def relink_socket_map(node, socket_collection, map, item, in_out=None):
-    from bpy.types import NodeSocket
     if not in_out: in_out=item.in_out
     if node.bl_idname in ['MantisSchemaGroup'] and item.parent and item.parent.name == 'Array':
-        multi = False
-        if in_out == 'INPUT':
-            multi=True
+        multi = True if in_out == 'INPUT' else False
         s = socket_collection.new(type=item.socket_type, name=item.name, identifier=item.identifier,  use_multi_input=multi)
-        # s.link_limit = node.schema_length TODO
     else:
         s = socket_collection.new(type=item.socket_type, name=item.name, identifier=item.identifier)
     if item.parent.name == 'Array': s.display_shape = 'SQUARE_DOT'