Эх сурвалжийг харах

Fix: Node Groups don't work properly in Schema

This commit makes it so Node Groups within Schema are treated as schema with
length == 1
now I am not 100% sure this is going to work in every circumstance but it should
work anywhere schema does

this commit does NOT guarantee that the parsed tree is functional - it does make
sure the tree will parse properly. So there may still be bugs in some nodes and I
will have to fix these on a case-by-case basis.
Joseph Brandenburg 7 сар өмнө
parent
commit
4de2f76294
4 өөрчлөгдсөн 134 нэмэгдсэн , 53 устгасан
  1. 4 4
      internal_containers.py
  2. 18 19
      readtree.py
  3. 30 8
      schema_containers.py
  4. 82 22
      schema_solve.py

+ 4 - 4
internal_containers.py

@@ -9,11 +9,11 @@ class DummyNode(MantisNode):
         self.prototype = prototype
         self.node_type = 'DUMMY'
         self.prepared = True
-        if prototype.bl_idname in ["MantisSchemaGroup"]:
-            self.node_type = 'DUMMY_SCHEMA'
-            self.prepared = False
-            self.uuid = uuid4()
+        self.uuid = uuid4()
         if prototype:
+            if prototype.bl_idname in ["MantisSchemaGroup"]:
+                self.node_type = 'DUMMY_SCHEMA'
+                self.prepared = False
             for sock in prototype.inputs:
                 if sock.identifier == "__extend__" or sock.name == "__extend__":
                     continue

+ 18 - 19
readtree.py

@@ -108,8 +108,8 @@ from .base_definitions import replace_types, NodeSocket
 
 # 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_nc, all_nc, np):
-    nc_to = local_nc[(None, *tree_path_names, np.name)]
+def make_connections_to_ng_dummy(base_tree, tree_path_names, local_nc, all_nc, nc_to):
+    np = nc_to.prototype
     for inp in np.inputs:
         nc_from = None
         if inp.bl_idname in  ['WildcardSocket']:
@@ -136,40 +136,39 @@ def make_connections_to_ng_dummy(base_tree, tree_path_names, local_nc, all_nc, n
 
 def gen_node_containers(base_tree, current_tree, tree_path_names, all_nc, local_nc, dummy_nodes, group_nodes, schema_nodes ):
     from .internal_containers import DummyNode
-    from .base_definitions import SchemaUINode
-    for np in current_tree.nodes:
-        if np.bl_idname in ["NodeFrame", "NodeReroute"]:
+    for ui_node in current_tree.nodes:
+        if ui_node.bl_idname in ["NodeFrame", "NodeReroute"]:
             continue # not a Mantis Node
-        if np.bl_idname in ["NodeGroupInput", "NodeGroupOutput"]: # make a Dummy Node
-            # we only want ONE dummy in/out per tree_path, so use the bl_idname
-            sig = (None, *tree_path_names, np.bl_idname)
+        if ui_node.bl_idname in ["NodeGroupInput", "NodeGroupOutput"]:
+            # we only want ONE dummy in/out per tree_path, so use the bl_idname to make a Dummy node
+            sig = (None, *tree_path_names, ui_node.bl_idname)
             if not local_nc.get(sig):
-                nc = DummyNode( signature=sig , base_tree=base_tree, prototype=np )
+                nc = DummyNode( signature=sig , base_tree=base_tree, prototype=ui_node )
                 local_nc[sig] = nc; all_nc[sig] = nc; dummy_nodes[sig] = nc
-                if np.bl_idname in ["NodeGroupOutput"]:
+                if ui_node.bl_idname in ["NodeGroupOutput"]:
                     nc.reroute_links = reroute_links_grpout
-        elif np.bl_idname in  ["MantisNodeGroup", "MantisSchemaGroup"]:
-            nc = DummyNode( signature= (sig := (None, *tree_path_names, np.name) ), base_tree=base_tree, prototype=np )
+        elif ui_node.bl_idname in  ["MantisNodeGroup", "MantisSchemaGroup"]:
+            nc = DummyNode( signature= (sig := (None, *tree_path_names, ui_node.name) ), base_tree=base_tree, prototype=ui_node )
             local_nc[sig] = nc; all_nc[sig] = nc; dummy_nodes[sig] = nc
-            make_connections_to_ng_dummy(base_tree, tree_path_names, local_nc, all_nc, np)
-            if np.bl_idname == "MantisNodeGroup":
+            make_connections_to_ng_dummy(base_tree, tree_path_names, local_nc, all_nc, nc)
+            if ui_node.bl_idname == "MantisNodeGroup":
                 group_nodes.append(nc)
                 nc.reroute_links = reroute_links_grp
             else:
                 group_nodes.append(nc)
                 schema_nodes[sig] = nc
         # if it wasn't the types we ignore or the types we make a Dummy for, use this to catch all non-special cases.
-        elif (nc_cls := np.mantis_class):
-            sig = (None, *tree_path_names, np.name)
-            if np.bl_idname in replace_types:
-                sig = (None, *tree_path_names, np.bl_idname)
+        elif (nc_cls := ui_node.mantis_class):
+            sig = (None, *tree_path_names, ui_node.name)
+            if ui_node.bl_idname in replace_types:
+                sig = (None, *tree_path_names, ui_node.bl_idname)
                 if local_nc.get(sig):
                     continue # already made
             nc = nc_cls( sig , base_tree)
             local_nc[sig] = nc; all_nc[sig] = nc
         else:
             nc = None
-            prRed(f"Can't make nc for.. {np.bl_idname}")
+            prRed(f"Can't make nc for.. {ui_node.bl_idname}")
         # this should be done at init
         if nc.signature[0] not in ['MANTIS_AUTOGENERATED'] and nc.node_type not in ['SCHEMA', 'DUMMY', 'DUMMY_SCHEMA']:
             nc.fill_parameters()

+ 30 - 8
schema_containers.py

@@ -27,10 +27,7 @@ def schema_init_sockets(nc, is_input = True, in_out='INPUT', category=''):
             if item.item_type == 'PANEL': continue
             if item.parent and item.parent.name == category:
                 if item.in_out == in_out:
-                    sockets[item.name] = NodeSocket(
-                        is_input=is_input,
-                        name=item.name,
-                        node=nc)
+                    sockets.init_sockets([item.name])
     nc.init_parameters()
 
 
@@ -74,19 +71,44 @@ class SchemaArrayOutput(SchemaNode):
         schema_init_sockets(self, is_input=True, in_out='OUTPUT', category='Array')
 
 class SchemaConstInput(SchemaNode):
-    def __init__(self, signature, base_tree):
+    def __init__(self, signature, base_tree, parent_schema_node=None):
         super().__init__(signature, base_tree)
-        schema_init_sockets(self, is_input=False, in_out='INPUT', category='Constant')
+        if parent_schema_node:
+            # this allows us to generate the Constant Input from a normal Node Group
+            # and treat the node group as a schema
+            parent_tree = parent_schema_node.prototype.node_tree
+            sockets=self.outputs
+            for item in parent_tree.interface.items_tree:
+                if item.item_type == 'PANEL': continue
+                if item.in_out == 'INPUT':
+                    sockets.init_sockets([item.name])
+            self.init_parameters()
+            
+        else:
+            schema_init_sockets(self, is_input=False, in_out='INPUT', category='Constant')
 
 
 class SchemaConstOutput(SchemaNode):
-    def __init__(self, signature, base_tree):
+    def __init__(self, signature, base_tree, parent_schema_node=None):
         super().__init__(signature, base_tree)
         inputs = [
             "Expose when N==",
         ]
         self.inputs.init_sockets(inputs)
-        schema_init_sockets(self, is_input=True, in_out='OUTPUT', category='Constant')
+        if parent_schema_node:
+            # this allows us to generate the Constant Input from a normal Node Group
+            # and treat the node group as a schema
+            parent_tree = parent_schema_node.prototype.node_tree
+            sockets=self.inputs
+            for item in parent_tree.interface.items_tree:
+                if item.item_type == 'PANEL': continue
+                if item.in_out == 'OUTPUT':
+                    sockets.init_sockets([item.name, "Expose when N==",])
+            self.init_parameters()
+            self.parameters['Expose when N=="']=1
+
+        else:
+            schema_init_sockets(self, is_input=True, in_out='OUTPUT', category='Constant')
 
 
         

+ 82 - 22
schema_solve.py

@@ -3,10 +3,10 @@ from .utilities import (prRed, prGreen, prPurple, prWhite,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
                               wrapOrange,)
 from .utilities import init_connections, init_dependencies
-from .base_definitions import SchemaUINode, GraphError, replace_types, custom_props_types
+from .base_definitions import SchemaUINode, custom_props_types, MantisNodeGroup
 from .node_container_common import setup_custom_props_from_np
 # a class that solves Schema nodes
-
+from bpy.types import NodeGroupInput, NodeGroupOutput
 
 
 
@@ -33,7 +33,13 @@ class SchemaSolver:
         self.held_links = []
         self.tree_path_names  = [*self.node.signature] # same tree as the schema node
         self.autogen_path_names = ['SCHEMA_AUTOGENERATED', *self.node.signature[1:]]
-        self.index_link = self.node.inputs['Schema Length'].links[0]
+        self.is_node_group = False
+        if self.node.prototype.bl_idname == "MantisNodeGroup":
+            self.is_node_group = True
+        if self.node.inputs['Schema Length'].links:
+            self.index_link = self.node.inputs['Schema Length'].links[0]
+        else:
+            self.index_link = None
         self.solve_length = self.node.evaluate_input("Schema Length")
         # I'm making this a property of the solver because the solver's data is modified as it solves each iteration
         self.index = 0
@@ -47,28 +53,43 @@ class SchemaSolver:
             inp.links.sort(key=lambda a : -a.multi_input_sort_id)
 
         for ui_node in self.tree.nodes:
+            # first we need to fill the parameters of the schema nodes.
+            # we use the bl_idname because all schema nodes should be single-instance 
+            signature = (*self.tree_path_names, ui_node.bl_idname)
             if isinstance(ui_node, SchemaUINode):
-                # first we need to fill the parameters of the schema nodes.
-                # we use the bl_idname because all schema nodes should be single-instance 
-                signature = (*self.tree_path_names, ui_node.bl_idname)
                 # We use the solver's signature here because it represents the "original" signature of the schema UI group node
                 # since this schema solver may be in a nested schema, and its node's signature may have uuid/index attached.
                 get_sig = (*self.signature, ui_node.bl_idname) 
                 if not (mantis_node := self.all_nodes.get(get_sig)): raise RuntimeError(wrapRed(f"Not found: {get_sig}"))
                 self.schema_nodes[signature] = mantis_node
                 mantis_node.fill_parameters(ui_node)
+            # HACK to make Group Nodes work
+            if ui_node.bl_idname == "NodeGroupInput":
+                from .schema_containers 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_containers 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)
     
     def set_index_strings(self):
         self.index_str = lambda : '.'+str(self.uuid)+'.'+str(self.index).zfill(4)
         self.prev_index_str = lambda : '.'+str(self.uuid)+'.'+str(self.index-1).zfill(4)
+        if self.is_node_group:
+            self.index_str=lambda : ''
+            self.prev_index_str=lambda : ''
     
     def init_schema_links(self,):
         """ 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
-            if not item.parent:
-                raise GraphError("ERROR: Schema tree has inputs that are not in categories. This is not supported")
-            match item.parent.name:
+            parent_name='Constant'
+            if item.parent.name != '': # in an "prphan" item this is left blank , it is not None or an AttributeError.
+                parent_name = item.parent.name
+            match parent_name:
                 case 'Connection':
                     if item.in_out == 'INPUT':
                         if incoming_links := self.node.inputs[item.identifier].links:
@@ -108,23 +129,38 @@ class SchemaSolver:
 
     def gen_solve_iteration_mantis_nodes(self, frame_mantis_nodes, unprepared):
         for prototype_ui_node in self.tree.nodes:
+            mantis_node_name = prototype_ui_node.name
+            index_str = self.index_str()
             if isinstance(prototype_ui_node, SchemaUINode):
                 continue  # IGNORE the schema interface nodes, we already made them in __init__()
                 # they are reused for each iteration.
             elif prototype_ui_node.bl_idname in ['NodeFrame', 'NodeReroute']:
                 continue  # IGNORE stuff that is purely UI - frames, reroutes.
-            signature = (*self.autogen_path_names, prototype_ui_node.name+self.index_str())
-            prototype_mantis_node = self.all_nodes[(*self.signature, prototype_ui_node.name)]
+            elif prototype_ui_node.bl_idname in ['NodeGroupInput', 'NodeGroupOutput']:
+                continue # we converted these to Schema Nodes because they represent a Group input.
+            signature = (*self.autogen_path_names, mantis_node_name+index_str)
+            prototype_mantis_node = self.all_nodes[(*self.signature, mantis_node_name)]
             # the prototype_mantis_node was generated inside the schema when we parsed the tree.
             # it is the prototype of the mantis node which we make for this iteration
             # for Schema sub-nodes ... they need a prototype to init.
             if prototype_mantis_node.node_type in ['DUMMY', 'DUMMY_SCHEMA']:
-                from .utilities import get_node_prototype
-                ui_node = get_node_prototype(prototype_mantis_node.signature, prototype_mantis_node.base_tree)
-                if prototype_mantis_node.node_type == 'DUMMY_SCHEMA':
+                # We stored the prototype ui_node when creating the Mantis node.
+                ui_node = prototype_mantis_node.prototype
+                # if prototype_mantis_node is a group or schema: TODO changes are needed elsewhere to make this easier to read. LEGIBILITY
+                if ui_node.bl_idname in ["MantisNodeGroup", "SchemaGroup"]:
                     mantis_node = prototype_mantis_node.__class__(
                         signature, prototype_mantis_node.base_tree, prototype=ui_node,
                         natural_signature = (*self.node.signature, ui_node.name) )
+                    # now let's copy the links from the prototype node
+                    if ui_node.bl_idname in ["MantisNodeGroup"]:
+                        mantis_node.prepared = False
+                        mantis_node.node_type = 'DUMMY_SCHEMA' # we promote it to a schema for now
+                        mantis_node.inputs.init_sockets(['Schema Length']) # add a Schema Length socket
+                        mantis_node.parameters['Schema Length'] = 1 # set the length to 1 since it is a single group instance
+                        # we'll make the autogenerated nodes for constant inputs. It doesn't matter that there is technically
+                        #  a prototype available for each one  -- these are cheap and I want this to be easy.
+                        from .readtree import make_connections_to_ng_dummy
+                        make_connections_to_ng_dummy(self.node.base_tree, self.autogen_path_names, frame_mantis_nodes, self.all_nodes, mantis_node)
                 else:
                     mantis_node = prototype_mantis_node.__class__(signature, prototype_mantis_node.base_tree, prototype=ui_node)
             else:
@@ -145,13 +181,22 @@ class SchemaSolver:
             from .utilities import gen_nc_input_for_data
             nc_cls = gen_nc_input_for_data(ui_link.from_socket)
             if (nc_cls): #HACK
-                sig = ("MANTIS_AUTOGENERATED", *self.tree_path_names[1:-1], self.index_str(), ui_link.from_socket.name, ui_link.from_socket.identifier)
-                nc_from = nc_cls(sig, self.node.base_tree)
+                unique_name = "".join([
+                    ui_link.to_socket.node.name+self.index_str(),
+                    ui_link.from_socket.name,
+                    ui_link.from_socket.identifier,
+                    "==>",
+                    ui_link.to_socket.name,
+                    ui_link.to_socket.identifier,
+                    ])
+                sig = ("MANTIS_AUTOGENERATED", *self.tree_path_names[1:-1], unique_name)
+                nc_from = frame_mantis_nodes.get(sig)
+                if not nc_from:
+                    nc_from = nc_cls(sig, self.node.base_tree)
                 # ugly! maybe even a HACK!
                 nc_from.inputs = {}
                 from .base_definitions import NodeSocket
                 nc_from.outputs = {ui_link.from_socket.name:NodeSocket(name = ui_link.from_socket.name, node=nc_from)}
-                from .node_container_common import get_socket_value
                 nc_from.parameters = {ui_link.from_socket.name:index}
                 frame_mantis_nodes[sig]=nc_from
                 from_node = nc_from
@@ -168,6 +213,9 @@ class SchemaSolver:
         _from_name, to_name = get_link_in_out(ui_link)
         to_node = frame_mantis_nodes[(*self.autogen_path_names, to_name+self.index_str())]
         # this self.index_link is only used here?
+        if self.index_link is None:
+            # this should be impossible because the Schema gets an auto-generated Int input.
+            raise NotImplementedError("This code should be unreachable. Please report this as a bug!")
         if (self.index_link.from_node):
             connection = self.index_link.from_node.outputs[self.index_link.from_socket].connect(node=to_node, socket=ui_link.to_socket.name)
         # otherwise we can autogen an int input I guess...?
@@ -235,6 +283,10 @@ class SchemaSolver:
         from .utilities import get_link_in_out
         to_node = self.schema_nodes[(*self.tree_path_names, to_ui_node.bl_idname)]
         expose_when = to_node.evaluate_input('Expose when N==')
+        # HACK here to force it to work with ordinary node groups, which don't seem to set this value correctly.
+        if to_ui_node.bl_idname == "NodeGroupOutput":
+            expose_when = index # just set it directly since it is getting set to None somewhere (I should find out where tho)
+        # end HACK
         if index == expose_when:
             for outgoing in self.constant_out[ui_link.to_socket.name]:
                 to_node = outgoing.to_node
@@ -332,7 +384,8 @@ class SchemaSolver:
             if sum([dep.prepared for dep in nc.hierarchy_dependencies]) == len(nc.hierarchy_dependencies):
                 nc.bPrepare()
                 if nc.node_type == 'DUMMY_SCHEMA':
-                    self.solve_nested_schema(nc)
+                    schema_solver = self.solve_nested_schema(nc)
+
             else: # Keeping this for-loop as a fallback, it should never add dependencies though
                 for dep in nc.hierarchy_dependencies:
                     if not dep.prepared and dep not in unprepared:
@@ -401,6 +454,7 @@ class SchemaSolver:
         awaiting_prep_stage = []
         for ui_link in ui_links:
             to_ui_node = ui_link.to_socket.node; from_ui_node = ui_link.from_socket.node
+
             if isinstance(from_ui_node, SchemaIndex):
                 if ui_link.from_socket.name == "Index":
                     self.handle_link_from_index_input(self.index, frame_mantis_nodes, ui_link)
@@ -411,7 +465,7 @@ class SchemaSolver:
                 if ui_link.from_socket.name in self.incoming_connections.keys():
                     self.handle_link_from_incoming_connection_input(frame_mantis_nodes, ui_link)
                 continue
-            if isinstance(from_ui_node, SchemaConstInput):
+            if isinstance(from_ui_node, (SchemaConstInput, NodeGroupInput)):
                 if ui_link.from_socket.name in self.constant_in.keys():
                     self.handle_link_from_constant_input( frame_mantis_nodes, ui_link, to_ui_node)
                 continue 
@@ -426,12 +480,13 @@ class SchemaSolver:
                 self.held_links.append(ui_link)
                 continue 
             # HOLD these links until prep is done a little later
-            if isinstance(to_ui_node, SchemaConstOutput) or isinstance(to_ui_node, SchemaArrayOutput) or \
+            if isinstance(to_ui_node, (SchemaConstOutput, NodeGroupOutput)) or isinstance(to_ui_node, SchemaArrayOutput) or \
                 isinstance(from_ui_node, SchemaArrayInputGet):
                 awaiting_prep_stage.append(ui_link)
                 continue
             # for any of the special cases, we hit a 'continue' block. So this connection is not special, and is made here.
             connection = link_node_containers(self.autogen_path_names, ui_link, frame_mantis_nodes, from_suffix=self.index_str(), to_suffix=self.index_str())
+
         for k,v in frame_mantis_nodes.items():
             self.solved_nodes[k]=v
             init_dependencies(v) # it is hard to overstate how important this single line of code is
@@ -441,7 +496,7 @@ class SchemaSolver:
         while(awaiting_prep_stage):
             ui_link = awaiting_prep_stage.pop()
             to_ui_node = ui_link.to_socket.node; from_ui_node = ui_link.from_socket.node
-            if isinstance(to_ui_node, SchemaConstOutput):
+            if isinstance(to_ui_node, (SchemaConstOutput, NodeGroupOutput)):
                 self.handle_link_to_constant_output(frame_mantis_nodes, self.index, ui_link,  to_ui_node)
             if isinstance(to_ui_node, SchemaArrayOutput):
                 self.handle_link_to_array_output(frame_mantis_nodes, self.index, ui_link, to_ui_node, from_ui_node)
@@ -454,17 +509,22 @@ class SchemaSolver:
         """ Solves all schema node groups found in this Schema. This is a recursive function, which will
             solve all levels of nested schema - since this function is called by solver.solve().
         """
+        solver=None
         if schema_nc.prepared == False:
             all_nodes = self.all_nodes.copy()
             ui_node = schema_nc.prototype
             length = schema_nc.evaluate_input("Schema Length")
             tree = ui_node.node_tree
-            prOrange(f"Expanding schema {tree.name} in node {schema_nc} with length {length}.")
+            if schema_nc.prototype.bl_idname == "MantisNodeGroup":
+                prOrange(f"Expanding Node Group {tree.name} in node {schema_nc}.")
+            else:
+                prOrange(f"Expanding schema {tree.name} in node {schema_nc} with length {length}.")
             solver = SchemaSolver(schema_nc, all_nodes, ui_node, schema_nc.natural_signature)
             solved_nodes = solver.solve()
             schema_nc.prepared = True
             for k,v in solved_nodes.items():
                 self.solved_nodes[k]=v
+        return solver
 
 
     def finalize(self, frame_nc):