Procházet zdrojové kódy

I/O Export Curve and MetaRig info

note that this commit does NOT import the info yet.
Joseph Brandenburg před 2 měsíci
rodič
revize
1205e9377e
4 změnil soubory, kde provedl 227 přidání a 29 odebrání
  1. 1 1
      base_definitions.py
  2. 218 27
      i_o.py
  3. 1 1
      ops_nodegroup.py
  4. 7 0
      utilities.py

+ 1 - 1
base_definitions.py

@@ -847,7 +847,7 @@ class MantisNode:
             if ( (self.signature[0] in  ["MANTIS_AUTOGENERATED", "SCHEMA_AUTOGENERATED" ]) or 
                 (self.signature[-1] in ["NodeGroupOutput", "NodeGroupInput"]) ): # I think this is harmless
                 return None
-            else:
+            else: # BUG shouldn't this use ui_signature??
                 ui_node = get_node_prototype(self.signature, self.base_tree)
             if not ui_node:
                 raise RuntimeError(wrapRed("No node prototype found for... %s" % ( [self.base_tree] + list(self.signature[1:]) ) ) )

+ 218 - 27
i_o.py

@@ -5,7 +5,7 @@ from .utilities import (prRed, prGreen, prPurple, prWhite,
                               wrapRed, wrapGreen, wrapPurple, wrapWhite,
                               wrapOrange,)
 
-from mathutils import  Vector
+from mathutils import Vector, Matrix
 
 NODES_REMOVED=["xFormRootNode"]
                  # Node bl_idname, # Socket Name
@@ -114,12 +114,10 @@ def is_jsonable(x):
     except (TypeError, OverflowError):
         return False
 
-
 # https://stackoverflow.com/questions/295135/turn-a-stritree-into-a-valid-filename - thank you user "Sophie Gage"
 def remove_special_characters(name):
     import re; return re.sub('[^\w_.)( -]', '', name)# re = regular expressions
 
-
 def fix_custom_parameter(n, property_definition, ):
     if n.bl_idname in ['xFormNullNode', 'xFormBoneNode', 'xFormArmatureNode', 'xFormGeometryObjectNode',]:
         prop_name = property_definition["name"]
@@ -141,6 +139,188 @@ def fix_custom_parameter(n, property_definition, ):
 
     return None
 
+# def scan_tree_for_objects(base_tree, current_tree):
+#     # goal: find all referenced armature and curve objects
+#     # return [set(armatures), set(curves)]
+#     armatures = set()
+#     curves    = set()
+#     for node in base_tree.parsed_tree.values():
+#         from .utilities import get_node_prototype
+#         if node.ui_signature is None:
+#             continue
+#         ui_node = get_node_prototype(node.ui_signature, node.base_tree)
+#         if ui_node is None or ui_node.id_data != current_tree:
+#             continue
+#         if hasattr(node, "bGetObject"):
+#             ob = node.bGetObject()
+#             print(node, ob)
+#             if ob is None:
+#                 continue
+#             if not hasattr(node, "type"):
+#                 continue
+#             if ob.type == 'ARMATURE':
+#                 armatures.add(ob)
+#             if ob.type == 'CURVE':
+#                 curves.add(ob)
+#     return (armatures, curves)
+
+# Currently this isn't very robust and doesn't seek backwards
+# to see if a dependency is created by node connections.
+# TODO it remains to be seen if that is even a desirable behaviour.
+def scan_tree_for_objects(base_tree, current_tree):
+    from bpy import data
+    armatures, curves    = set(), set()
+    for node in current_tree.nodes:
+        match node.bl_idname:
+            case "UtilityMetaRig":
+                if node.inputs[0].is_linked:
+                    continue
+                if (armature := node.inputs[0].search_prop) is not None:
+                    armatures.add(armature)
+            case "InputExistingGeometryObjectNode":
+                if node.inputs["Name"].is_linked:
+                    continue
+                ob_name = node.inputs["Name"].default_value
+                if ob := data.objects.get(ob_name):
+                    if ob.type == "ARMATURE":
+                        armatures.add(ob)
+                    elif ob.type == "CURVE":
+                        curves.add(ob)
+            case "xFormArmatureNode":
+                if node.inputs["Name"].is_linked:
+                    continue
+                armature_name = node.inputs["Name"].default_value
+                armature = data.objects.get(armature_name)
+                if armature:
+                    armatures.add(armature)
+            case "xFormGeometryObjectNode":
+                if node.inputs["Name"].is_linked:
+                    continue
+                ob_name = node.inputs["Name"].default_value
+                if ob := data.objects.get(ob_name):
+                    if ob.type == "ARMATURE":
+                        armatures.add(ob)
+                    elif ob.type == "CURVE":
+                        curves.add(ob)
+        for input in node.inputs:
+            if input.bl_idname in ["EnumCurveSocket"]:
+                if input.search_prop is not None:
+                    curves.add(input.search_prop)
+    # NOW check the parsed_tree and see if it is possible to find any other
+    # objects referred to/provided by the tree
+    return (curves, armatures )
+
+from dataclasses import dataclass, field, asdict
+# some basic classes to define curve point types
+@dataclass
+class crv_pnt_data():
+     co                 : tuple[float, float, float] = field(default=(0,0,0,))
+     handle_left        : tuple[float, float, float] = field(default=(0,0,0,))
+     handle_right       : tuple[float, float, float] = field(default=(0,0,0,))
+     handle_left_type   : str = field(default="ALIGNED")
+     handle_right_type  : str = field(default="ALIGNED")
+     radius             : float = field(default=0.0)
+     tilt               : float = field(default=0.0)
+     w                  : float = field(default=0.0)
+
+@dataclass
+class spline_data():
+    type                  : str = field(default='POLY')
+    points                : list[dict] = field(default_factory=[])
+    order_u               : int = field(default=4)
+    radius_interpolation  : str = field(default="LINEAR")
+    tilt_interpolation    : str = field(default="LINEAR")
+    resolution_u          : int = field(default=12)
+    use_bezier_u          : bool = field(default=False)
+    use_cyclic_u          : bool = field(default=False)
+    use_endpoint_u        : bool = field(default=False)
+    index                 : int = field(default=0)
+    object_name           : str = field(default='Curve')
+
+def get_curve_for_pack(object):
+    splines = []
+    for i, spline in enumerate(object.data.splines):
+        points = []
+        if spline.type == 'BEZIER':
+            for point in spline.bezier_points:
+                export_pnt = crv_pnt_data(
+                        co                = tuple(point.co),
+                        radius            = point.radius,
+                        tilt              = point.tilt,
+                        handle_left       = tuple(point.handle_left),
+                        handle_right      = tuple(point.handle_right),
+                        handle_left_type  = point.handle_left_type,
+                        handle_right_type = point.handle_right_type,
+                )
+                points.append(asdict(export_pnt))
+        else:
+            for point in spline.points:
+                export_pnt = crv_pnt_data(
+                        co     = point.co[:3], # exclude the w value
+                        radius = point.radius,
+                        tilt   = point.tilt,
+                        w      = point.co[3],
+                )
+                points.append(export_pnt)
+        export_spl = spline_data(
+                type                  = spline.type,
+                points                = points,
+                order_u               = spline.order_u,
+                radius_interpolation  = spline.radius_interpolation,
+                tilt_interpolation    = spline.tilt_interpolation,
+                resolution_u          = spline.resolution_u,
+                use_bezier_u          = spline.use_bezier_u,
+                use_cyclic_u          = spline.use_cyclic_u,
+                use_endpoint_u        = spline.use_endpoint_u,
+                index                 = i,
+                object_name           = object.name,)
+        splines.append(asdict(export_spl))
+    return splines
+
+def matrix_as_tuple(matrix):
+    return ( matrix[0][0], matrix[0][1], matrix[0][2], matrix[0][3],
+             matrix[1][0], matrix[1][1], matrix[1][2], matrix[1][3], 
+             matrix[2][0], matrix[2][1], matrix[2][2], matrix[2][3],
+             matrix[3][0], matrix[3][1], matrix[3][2], matrix[3][3], )
+
+@dataclass
+class metabone_data:
+    object_name           : str = field(default='')
+    name                  : str = field(default=''),
+    type                  : str = field(default='BONE'),
+    matrix                : tuple[float] = field(default=()),
+    parent                : str = field(default=''),
+    length                : float = field(default=-1.0),
+    children              : list[str] = field(default_factory=[]), 
+# keep it really simple for now. I'll add BBone and envelope later on
+# when I make them accessible from the meta-rig
+
+def get_armature_for_pack(object):
+    metarig_data = {}
+    armature_data = metabone_data( object_name = object.name,
+        name=object.name, type='ARMATURE',
+        matrix=matrix_as_tuple(object.matrix_world),
+        parent="", # NOTE that this is not always a fair assumption!
+        length = -1.0, children =[],)
+    metarig_data[object.name] = asdict(armature_data)
+    metarig_data["MANTIS_RESERVED"] = asdict(armature_data) # just in case a bone is named the same as the armature
+    for bone in object.data.bones:
+        parent_name = ''
+        if bone.parent is None:
+            armature_data.children.append(bone.name)
+        else:
+            parent_name=bone.parent.name
+        children=[]
+        for c in bone.children:
+            children.append(c.name)
+        bone_data = metabone_data( object_name = object.name,
+            name=bone.name, type='BONE',
+            matrix=matrix_as_tuple(bone.matrix_local),
+            parent=parent_name, length = bone.length, children = children,
+        )
+        metarig_data[bone.name]=asdict(bone_data)
+    return metarig_data
+
 def get_socket_data(socket, ignore_if_default=False):
     # TODO: don't get stuff in the socket templates
     # PROBLEM: I don't have easy access to this from the ui class (or mantis class)
@@ -292,7 +472,7 @@ def get_interface_data(tree, tree_in_out):
             tree_in_out[sock.identifier] = sock_data
             
 
-def export_to_json(trees, path="", write_file=True, only_selected=False):
+def export_to_json(trees, base_tree=None, path="", write_file=True, only_selected=False, ):
     export_data = {}
     for tree in trees:
         current_tree_is_base_tree = False
@@ -301,6 +481,15 @@ def export_to_json(trees, path="", write_file=True, only_selected=False):
         
         tree_info, tree_in_out = {}, {}
         tree_info = get_tree_data(tree)
+        curves, metarig_data = {}, {}
+
+        embed_metarigs=True
+        if base_tree and embed_metarigs:
+            curves_in_tree, metarigs_in_tree = scan_tree_for_objects(base_tree, tree)
+            for crv in curves_in_tree:
+                curves[crv.name] = get_curve_for_pack(crv)
+            for mr in metarigs_in_tree:
+                metarig_data[mr.name] = get_armature_for_pack(mr)
 
         # if only_selected:
         #     # all in/out links, relative to the selection, should be marked and used to initialize tree properties
@@ -462,19 +651,15 @@ def export_to_json(trees, path="", write_file=True, only_selected=False):
             out_node["location"] = Vector((all_nodes_bounding_box[1].x+400, all_nodes_bounding_box[0].lerp(all_nodes_bounding_box[1], 0.5).y))
             nodes["MANTIS_AUTOGEN_GROUP_OUTPUT"]=out_node
 
-        export_data[tree.name] = (tree_info, tree_in_out, nodes, links,) # f_curves)
+        export_data[tree.name] = (tree_info, tree_in_out, nodes, links, curves, metarig_data,) # f_curves)
     
-    import json
-
-    if not write_file:
-        return export_data # gross to have a different type of return value... but I don't care
+    return export_data
 
+def write_json_data(data, path):
+    import json
     with open(path, "w") as file:
         print(wrapWhite("Writing mantis tree data to: "), wrapGreen(file.name))
-        file.write( json.dumps(export_data, indent = 4) )
-    # I'm gonna do this in a totally naive way, because this should already be sorted properly
-    #   for the sake of dependency satisfaction. So the current "tree" should be the "main" tree
-    tree.filepath = path
+        file.write( json.dumps(data, indent = 4) )
         
 def get_link_sockets(link, tree, tree_socket_id_map):
     from_node_name = link[0]
@@ -649,6 +834,7 @@ def setup_sockets(node, propslist, in_out="inputs"):
                         prWhite("Tried to write a read-only property, ignoring...")
                         prWhite(f"{socket.node.name}[{socket.name}].{s_p} is read only, cannot set value to {s_v}")
 
+
 def do_import_from_file(filepath, context):
     import json
 
@@ -800,15 +986,8 @@ def do_import(data, context):
                 finally:
                     n.is_updating=False
             # set up sockets
-            try:
-                setup_sockets(n, propslist, in_out="inputs")
-            except IndexError:
-                prRed("asdasdasda")
-            try:
-                setup_sockets(n, propslist, in_out="outputs")
-            except IndexError:
-                prRed("dfdfdfddfd")
-
+            setup_sockets(n, propslist, in_out="inputs")
+            setup_sockets(n, propslist, in_out="outputs")
             for p, v in propslist.items():
                 if p in ["node_tree",
                          "sockets",
@@ -861,12 +1040,21 @@ def do_import(data, context):
                 n.parent = p
             # otherwise the frame node is missing because it was not included in the data e.g. when grouping nodes.
         
+        # TODO: IMPORT THIS DATA HERE!!!
+        # try:
+        #     curves = tree_data[4]
+        #     armatures = tree_data[5]
+        # except KeyError: # shouldn't happen but maybe someone has an old file
+        #     curves = {}
+        #     armatures = {}
+        
+        # for curve_name, curve_data in curves.items():
+        #     from .utilities import import_curve_data_to_object
+        #     import_curve_data_to_object(curve_name, curve_data)
+        
         tree.is_executing = False
         tree.do_live_update = True
         
-
-
-
 import bpy
 
 from bpy_extras.io_utils import ImportHelper, ExportHelper
@@ -904,9 +1092,11 @@ class MantisExportNodeTreeSaveAs(Operator, ExportHelper):
         for t in trees:
             prGreen ("Node graph: \"%s\"" % (t.name))
         base_tree.is_exporting = True
-        export_to_json(trees, self.filepath)
+        export_data = export_to_json(trees, base_tree, self.filepath)
+        write_json_data(export_data, self.filepath)
         base_tree.is_exporting = False
         base_tree.prevent_next_exec = True
+        base_tree.filepath = self.filepath
         return {'FINISHED'}
 
 # Save
@@ -928,7 +1118,8 @@ class MantisExportNodeTreeSave(Operator):
         for t in trees:
             prGreen ("Node graph: \"%s\"" % (t.name))
         base_tree.is_exporting = True
-        export_to_json(trees, self.filepath)
+        export_to_json(trees, base_tree, base_tree.filepath)
+        write_json_data(export_data, base_tree.filepath)
         base_tree.is_exporting = False
         base_tree.prevent_next_exec = True
         return {'FINISHED'}

+ 1 - 1
ops_nodegroup.py

@@ -44,7 +44,7 @@ class MantisGroupNodes(Operator):
             from random import random
             grp_name = "".join([chr(int(random()*30)+35) for i in range(20)])
             trees=[base_tree]
-            selected_nodes=export_to_json(trees, write_file=False, only_selected=True)
+            selected_nodes=export_to_json(trees, base_tree=None, write_file=False, only_selected=True)
             # this snippet of confusing indirection edits the name of the base tree in the JSON data
             selected_nodes[base_tree.name][0]["name"]=grp_name
             do_import(selected_nodes, context)

+ 7 - 0
utilities.py

@@ -408,6 +408,13 @@ def import_object_from_file(path):
     else:
         raise RuntimeError(f"Failed to parse filename {path}")
         
+def import_curve_data_to_object(curve_name, curve_data):
+    # the curve data will come as a single curve's data
+    from bpy import data
+    curve_object = data.objects.new(curve_name, data.curves.new(name=curve_name, type='CURVE'))
+
+    for spline in curve_data:
+        curve_object.data.splines.new(type=spline['type'])
 
 ##############################
 #  READ TREE and also Schema Solve!