Jelajahi Sumber

Add Node: Declare Collections Utility

an easy to use UI node for setting up hierarchies of collections
combined with the last commit, it makes it easy to declare
multiple nested collections for a bone
Joseph Brandenburg 3 bulan lalu
induk
melakukan
5b787e427e
5 mengubah file dengan 248 tambahan dan 3 penghapusan
  1. 1 0
      __init__.py
  2. 32 0
      misc_nodes.py
  3. 59 1
      misc_nodes_ui.py
  4. 98 0
      ops_nodegroup.py
  5. 58 2
      socket_definitions.py

+ 1 - 0
__init__.py

@@ -75,6 +75,7 @@ input_category=[
             NodeItem("InputMatrixNode"),
             NodeItem("InputExistingGeometryObject"),
             NodeItem("InputExistingGeometryData"),
+            NodeItem("UtilityDeclareCollections"),
     ]
 link_transform_category = [
         NodeItem("LinkCopyLocation"),

+ 32 - 0
misc_nodes.py

@@ -18,6 +18,7 @@ def TellClasses():
              InputMatrix,
              InputExistingGeometryObject,
              InputExistingGeometryData,
+             UtilityDeclareCollections,
              UtilityGeometryOfXForm,
              UtilityNameOfXForm,
              UtilityPointFromCurve,
@@ -1275,6 +1276,37 @@ class InputExistingGeometryData(MantisNode):
             raise RuntimeError(f"Could not find a mesh or curve datablock named \"{self.evaluate_input('Name')}\" for node {self}")
         return bObject
 
+class UtilityDeclareCollections(MantisNode):
+    '''A node to help manage bone collections'''
+    def __init__(self, signature, base_tree):
+        super().__init__(signature, base_tree)
+        from .utilities import get_node_prototype
+        ui_node = get_node_prototype(self.ui_signature, self.base_tree)
+        self.gen_outputs(ui_node)
+        print(self.outputs)
+        self.init_parameters()
+        self.fill_parameters(ui_node)
+        self.node_type = "UTILITY"
+        self.prepared, self.executed = True, True
+
+    def reset_execution(self):
+        super().reset_execution()
+        self.prepared, self.executed = True, True
+    
+    def gen_outputs(self, ui_node=None):
+        from .base_definitions import MantisSocketTemplate as SockTemplate
+        templates=[]
+        for out in ui_node.outputs:
+            if not (out.name in self.outputs.keys()) :
+                templates.append(SockTemplate(name=out.name,
+                        identifier=out.identifier, is_input=False,))
+        self.outputs.init_sockets(templates)
+    
+    def fill_parameters(self, ui_node=None):
+        if ui_node is None: return
+        for out in ui_node.outputs:
+            self.parameters[out.name] = out.default_value
+
 class UtilityGeometryOfXForm(MantisNode):
     '''A node representing existing object data'''
     def __init__(self, signature, base_tree):

+ 59 - 1
misc_nodes_ui.py

@@ -21,6 +21,7 @@ def TellClasses():
              # InputGeometryNode,
              InputExistingGeometryObjectNode,
              InputExistingGeometryDataNode,
+             UtilityDeclareCollections,
              UtilityGeometryOfXForm,
              UtilityNameOfXForm,
             #  ComposeMatrixNode,
@@ -746,6 +747,64 @@ class InputExistingGeometryDataNode(Node, MantisUINode):
         self.outputs.new("GeometrySocket", "Geometry")
         self.initialized = True
 
+
+def socket_data_from_collection_paths(root_data, root_name, path, socket_data):
+    # so we need to 'push' the socket names and their paths in order
+    # socket_data is a list of tuples of ( name, path, )
+    for key, value in root_data.items():
+        path.append(key)
+        socket_data.append( (key, path))
+        if hasattr(value , 'items'):
+            socket_data = socket_data_from_collection_paths(value, key, path.copy(), socket_data)
+        path.pop()
+    return socket_data
+
+class UtilityDeclareCollections(Node, MantisUINode):
+    """A utility used to declare bone collections."""
+    bl_idname = "UtilityDeclareCollections"
+    bl_label  = "Collections"
+    bl_icon   = "NODE"
+    bl_width_min = 320
+    initialized : bpy.props.BoolProperty(default = False)
+    mantis_node_class_name=bl_idname
+    # Here is the layout of the data:
+    # nested dicts of key:dict ( key = name, dict = children)
+    # the 'leaf nodes' are empty dicts
+    # we'll store it as a JSON string in order to make it a bpy.props 
+    # and still have the ability to use it as a dict and save it
+    # TODO: check and see if these strings have a character limit
+    collection_declarations : bpy.props.StringProperty(default="")
+
+    def update_interface(self):
+        # we need to do dynamic stuff here like with interfaces
+        self.outputs.clear()
+        current_data = self.read_declarations_from_json()
+        socket_data = socket_data_from_collection_paths(current_data, self.name, [], [])
+        for item in socket_data:
+            full_path_name = '>'.join(item[1]+[item[0]])
+            s = self.outputs.new('CollectionDeclarationSocket', name=item[0],identifier=full_path_name )
+            s.collection_path = full_path_name
+        
+    def init(self, context):
+        self.initialized = True
+        if self.collection_declarations == "":
+            self.push_declarations_to_json({})
+    
+    def push_declarations_to_json(self, dict):
+        import json
+        j_str = json.dumps(dict)
+        self.collection_declarations = j_str
+
+    def read_declarations_from_json(self):
+        import json
+        j_data = json.loads(self.collection_declarations)
+        return j_data
+    
+    def draw_buttons(self, context, layout):
+        op_props = layout.operator('mantis.collection_add_new')
+        op_props.socket_invoked = '' # this isn't reset between invocations
+        # so we have to make sure to unset it when running it from the node
+
 class UtilityGeometryOfXForm(Node, MantisUINode):
     """Retrieves a mesh or curve datablock from an xForm."""
     bl_idname = "UtilityGeometryOfXForm"
@@ -1196,7 +1255,6 @@ class UtilityPrint(Node, MantisUINode):
     def init(self, context):
         self.inputs.new("WildcardSocket", "Input")
         self.initialized = True
-    
 
 # Set up the class property that ties the UI classes to the Mantis classes.
 for cls in TellClasses():

+ 98 - 0
ops_nodegroup.py

@@ -788,6 +788,101 @@ class LinkArmatureRemoveTargetInput(bpy.types.Operator):
         return {'FINISHED'}
 
 
+class CollectionAddNewOutput(bpy.types.Operator):
+    """Add a new Collection output to the Driver node"""
+    bl_idname = "mantis.collection_add_new"
+    bl_label = "+ Child"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    collection_name : bpy.props.StringProperty(default='Collection')
+    tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
+    node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
+    socket_invoked : bpy.props.StringProperty(options ={'HIDDEN'}) # set by caller
+
+    @classmethod
+    def poll(cls, context):
+        return True #(hasattr(context, 'active_node') )
+    # DUPLICATED CODE HERE, DUMMY
+    def invoke(self, context, event):
+        self.tree_invoked = context.node.id_data.name
+        self.node_invoked = context.node.name
+        t = context.node.id_data
+        t.nodes.active = context.node
+        context.node.select = True
+        wm = context.window_manager
+        return wm.invoke_props_dialog(self)
+    
+    def execute(self, context):
+        if not self.collection_name:
+            return {'CANCELLED'}
+        n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
+        # we need to know which socket it is called from...
+        s = None
+        for socket in n.outputs:
+            if socket.identifier == self.socket_invoked:
+                s=socket; break
+        parent_path = ''
+        if s is not None and s.collection_path:
+                parent_path = s.collection_path + '>'
+        outer_dict = n.read_declarations_from_json()
+        current_data = outer_dict
+        if parent_path:
+            for name_elem in parent_path.split('>'):
+                if name_elem == '': continue # HACK around being a bad programmer
+                current_data = current_data[name_elem]
+        current_data[self.collection_name] = {}
+        n.push_declarations_to_json(outer_dict)
+        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"""
+    bl_idname = "mantis.collection_remove"
+    bl_label = "X"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
+    node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})
+    socket_invoked : bpy.props.StringProperty(options ={'HIDDEN'}) # set by caller
+
+    @classmethod
+    def poll(cls, context):
+        return True #(hasattr(context, 'active_node') )
+    # DUPLICATED CODE HERE, DUMMY
+    def invoke(self, context, event):
+        self.tree_invoked = context.node.id_data.name
+        self.node_invoked = context.node.name
+        t = context.node.id_data
+        t.nodes.active = context.node
+        context.node.select = True
+        return self.execute(context)
+    
+    def execute(self, context):
+        n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]
+        s = None
+        for socket in n.outputs:
+            if socket.identifier == self.socket_invoked:
+                s=socket; break
+        if not s:
+            return {'CANCELLED'}
+        parent_path = ''
+        if s is not None and s.collection_path:
+                parent_path = s.collection_path + '>'
+        outer_dict = n.read_declarations_from_json()
+        current_data = outer_dict
+        print(parent_path)
+        if parent_path:
+            for name_elem in parent_path.split('>')[:-2]: # just skip the last one
+                print(name_elem)
+                if name_elem == '': continue # HACK around being a bad programmer
+                current_data = current_data[name_elem]
+        del current_data[s.name]
+        n.push_declarations_to_json(outer_dict)
+        n.update_interface()
+        return {'FINISHED'}
+
 def get_socket_enum(operator, context):
     valid_types = []; i = -1
     from .socket_definitions import TellClasses, MantisSocket
@@ -885,6 +980,9 @@ classes = [
         # Armature Link Node
         LinkArmatureAddTargetInput,
         LinkArmatureRemoveTargetInput,
+        # managing collections
+        CollectionAddNewOutput,
+        CollectionRemoveOutput,
         # rigging utilities
         ConvertBezierCurveToNURBS,
         ]

+ 58 - 2
socket_definitions.py

@@ -141,6 +141,7 @@ def TellClasses() -> List[MantisSocket]:
              UnsignedIntSocket,
              IntSocket,
              StringSocket,
+             CollectionDeclarationSocket,
 
              EnumMetaRigSocket,
              EnumMetaBoneSocket,
@@ -383,6 +384,40 @@ def ChooseDraw(self, context, layout, node, text, icon = "NONE", use_enum=True,
         else:
             layout.label(text=text)
 
+def CollectionSocketDraw(socket, context, layout, node, text):
+    # create the UI objects
+    indent_length = len(socket.collection_path.split('>'))
+    layout.alignment = 'EXPAND'
+    # label_col = layout.row()
+    label_col = layout.split(factor=0.20)
+    label_col.alignment = 'LEFT' # seems backwards?
+    label_col.scale_x = 9.0
+    x_split = label_col.split(factor=0.35)
+    x_split.scale_x=2.0
+    x_split.alignment = 'RIGHT'
+    operator_col = layout.row()
+    # operator_col = layout
+    operator_col.alignment = 'RIGHT'  # seems backwards?
+    operator_col.scale_x = 1.0
+    # x_split = operator_col.split(factor=0.5)
+    # x_split.scale_x = 0.5
+    # x_split.alignment = 'RIGHT'
+
+    # Now fill in the text and operators and such
+    label_text = socket.collection_path.split('>')[-1]
+    if indent_length > 1:
+        label_text = '└'+label_text #┈ use this character to extend
+        for indent in range(indent_length):
+            if indent <= 1: continue
+            indent_text = ' ⬞ '
+            label_text=indent_text+label_text
+    op_props = x_split.operator('mantis.collection_remove')
+    op_props.socket_invoked = socket.identifier
+    label_col.label(text=label_text)
+    op_props = operator_col.operator('mantis.collection_add_new')
+    op_props.socket_invoked = socket.identifier
+    # this works well enough!
+
 class RelationshipSocket(MantisSocket):
     # Description string
     '''Relationship'''
@@ -822,6 +857,29 @@ class StringSocket(bpy.types.NodeSocketString, MantisSocket):
     @classmethod
     def draw_color_simple(self):
         return self.color_simple
+    
+def collection_declaration_get_default_value(self):
+    return self.collection_path
+
+class CollectionDeclarationSocket(MantisSocket):
+    """Socket for declaring a collection"""
+    bl_idname = 'CollectionDeclarationSocket'
+    bl_label = "Collection"
+    default_value : bpy.props.StringProperty(get=collection_declaration_get_default_value)
+    collection_path : bpy.props.StringProperty(default="")
+    color_simple = cBoneCollection
+    color : bpy.props.FloatVectorProperty(default=cBoneCollection, size=4)
+    icon : bpy.props.StringProperty(default = "NONE",)
+    input : bpy.props.BoolProperty(default =True,)
+    display_text : bpy.props.StringProperty(default="")
+    is_valid_interface_type=False
+    def draw(self, context, layout, node, text):
+        CollectionSocketDraw(self, context, layout, node, text)
+    def draw_color(self, context, node):
+        return self.color
+    @classmethod
+    def draw_color_simple(self):
+        return self.color_simple
 
 class BoneCollectionSocket(MantisSocket):
     """Bone Collection socket"""
@@ -840,8 +898,6 @@ class BoneCollectionSocket(MantisSocket):
     def draw_color_simple(self):
         return self.color_simple
 
-
-
 eArrayGetOptions =(
         ('CAP', "Cap", "Fail if the index is out of bounds."),
         ('WRAP', "Wrap", "Wrap around to the beginning of the array once the idex goes out of bounds."),