فهرست منبع

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 ماه پیش
والد
کامیت
5b787e427e
5فایلهای تغییر یافته به همراه248 افزوده شده و 3 حذف شده
  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("InputMatrixNode"),
             NodeItem("InputExistingGeometryObject"),
             NodeItem("InputExistingGeometryObject"),
             NodeItem("InputExistingGeometryData"),
             NodeItem("InputExistingGeometryData"),
+            NodeItem("UtilityDeclareCollections"),
     ]
     ]
 link_transform_category = [
 link_transform_category = [
         NodeItem("LinkCopyLocation"),
         NodeItem("LinkCopyLocation"),

+ 32 - 0
misc_nodes.py

@@ -18,6 +18,7 @@ def TellClasses():
              InputMatrix,
              InputMatrix,
              InputExistingGeometryObject,
              InputExistingGeometryObject,
              InputExistingGeometryData,
              InputExistingGeometryData,
+             UtilityDeclareCollections,
              UtilityGeometryOfXForm,
              UtilityGeometryOfXForm,
              UtilityNameOfXForm,
              UtilityNameOfXForm,
              UtilityPointFromCurve,
              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}")
             raise RuntimeError(f"Could not find a mesh or curve datablock named \"{self.evaluate_input('Name')}\" for node {self}")
         return bObject
         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):
 class UtilityGeometryOfXForm(MantisNode):
     '''A node representing existing object data'''
     '''A node representing existing object data'''
     def __init__(self, signature, base_tree):
     def __init__(self, signature, base_tree):

+ 59 - 1
misc_nodes_ui.py

@@ -21,6 +21,7 @@ def TellClasses():
              # InputGeometryNode,
              # InputGeometryNode,
              InputExistingGeometryObjectNode,
              InputExistingGeometryObjectNode,
              InputExistingGeometryDataNode,
              InputExistingGeometryDataNode,
+             UtilityDeclareCollections,
              UtilityGeometryOfXForm,
              UtilityGeometryOfXForm,
              UtilityNameOfXForm,
              UtilityNameOfXForm,
             #  ComposeMatrixNode,
             #  ComposeMatrixNode,
@@ -746,6 +747,64 @@ class InputExistingGeometryDataNode(Node, MantisUINode):
         self.outputs.new("GeometrySocket", "Geometry")
         self.outputs.new("GeometrySocket", "Geometry")
         self.initialized = True
         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):
 class UtilityGeometryOfXForm(Node, MantisUINode):
     """Retrieves a mesh or curve datablock from an xForm."""
     """Retrieves a mesh or curve datablock from an xForm."""
     bl_idname = "UtilityGeometryOfXForm"
     bl_idname = "UtilityGeometryOfXForm"
@@ -1196,7 +1255,6 @@ class UtilityPrint(Node, MantisUINode):
     def init(self, context):
     def init(self, context):
         self.inputs.new("WildcardSocket", "Input")
         self.inputs.new("WildcardSocket", "Input")
         self.initialized = True
         self.initialized = True
-    
 
 
 # Set up the class property that ties the UI classes to the Mantis classes.
 # Set up the class property that ties the UI classes to the Mantis classes.
 for cls in TellClasses():
 for cls in TellClasses():

+ 98 - 0
ops_nodegroup.py

@@ -788,6 +788,101 @@ class LinkArmatureRemoveTargetInput(bpy.types.Operator):
         return {'FINISHED'}
         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):
 def get_socket_enum(operator, context):
     valid_types = []; i = -1
     valid_types = []; i = -1
     from .socket_definitions import TellClasses, MantisSocket
     from .socket_definitions import TellClasses, MantisSocket
@@ -885,6 +980,9 @@ classes = [
         # Armature Link Node
         # Armature Link Node
         LinkArmatureAddTargetInput,
         LinkArmatureAddTargetInput,
         LinkArmatureRemoveTargetInput,
         LinkArmatureRemoveTargetInput,
+        # managing collections
+        CollectionAddNewOutput,
+        CollectionRemoveOutput,
         # rigging utilities
         # rigging utilities
         ConvertBezierCurveToNURBS,
         ConvertBezierCurveToNURBS,
         ]
         ]

+ 58 - 2
socket_definitions.py

@@ -141,6 +141,7 @@ def TellClasses() -> List[MantisSocket]:
              UnsignedIntSocket,
              UnsignedIntSocket,
              IntSocket,
              IntSocket,
              StringSocket,
              StringSocket,
+             CollectionDeclarationSocket,
 
 
              EnumMetaRigSocket,
              EnumMetaRigSocket,
              EnumMetaBoneSocket,
              EnumMetaBoneSocket,
@@ -383,6 +384,40 @@ def ChooseDraw(self, context, layout, node, text, icon = "NONE", use_enum=True,
         else:
         else:
             layout.label(text=text)
             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):
 class RelationshipSocket(MantisSocket):
     # Description string
     # Description string
     '''Relationship'''
     '''Relationship'''
@@ -822,6 +857,29 @@ class StringSocket(bpy.types.NodeSocketString, MantisSocket):
     @classmethod
     @classmethod
     def draw_color_simple(self):
     def draw_color_simple(self):
         return self.color_simple
         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):
 class BoneCollectionSocket(MantisSocket):
     """Bone Collection socket"""
     """Bone Collection socket"""
@@ -840,8 +898,6 @@ class BoneCollectionSocket(MantisSocket):
     def draw_color_simple(self):
     def draw_color_simple(self):
         return self.color_simple
         return self.color_simple
 
 
-
-
 eArrayGetOptions =(
 eArrayGetOptions =(
         ('CAP', "Cap", "Fail if the index is out of bounds."),
         ('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."),
         ('WRAP', "Wrap", "Wrap around to the beginning of the array once the idex goes out of bounds."),