| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888 | # this is the I/O part of mantis. I eventually intend to make this a markup language. not right now tho lolfrom .utilities import (prRed, prGreen, prPurple, prWhite,                              prOrange,                              wrapRed, wrapGreen, wrapPurple, wrapWhite,                              wrapOrange,)from mathutils import  Vectorfrom .base_definitions import NODES_REMOVED, SOCKETS_REMOVEDfrom bpy.app import versionadd_inputs_bl_idnames = [    "UtilityDriver", "UtilityFCurve", "DeformerMorphTargetDeform",    "LinkArmature",    ]# this works but it is really ugly and probably quite inneficient# TODO: make hotkeys for export and import and reload from file   # we need to give the tree a filepath attribute and update it on saving   # then we need to use the filepath attribute to load from   # finally we need to use a few operators to choose whether to open a menu or not   # and we need a message to display on save/load so that the user knows it is happening# TODO:   # Additionally export MetaRig and Curve and other referenced data   # Meshes can be exported as .obj and imported via GNdef TellClasses():    return [ MantisExportNodeTreeSaveAs, MantisExportNodeTreeSave, MantisExportNodeTree, MantisImportNodeTree, MantisReloadNodeTree]# https://stackoverflow.com/questions/42033142/is-there-an-easy-way-to-check-if-an-object-is-json-serializable-in-python - thanks!def is_jsonable(x):    import json    try:        json.dumps(x)        return True    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 expressionsdef fix_custom_parameter(n, property_definition, ):    if n.bl_idname in ['xFormNullNode', 'xFormBoneNode', 'xFormArmatureNode', 'xFormGeometryObjectNode',]:        prop_name = property_definition["name"]        prop_type = property_definition["bl_idname"]                if prop_type in ['ParameterBoolSocket', 'ParameterIntSocket', 'ParameterFloatSocket', 'ParameterVectorSocket' ]:            # is it good to make both of them?            input = n.inputs.new( prop_type, prop_name)            output = n.outputs.new( prop_type, prop_name)            if property_definition["is_output"] == True:                return output            return input        elif n.bl_idname in ['LinkArmature']:        prop_name = property_definition["name"]        prop_type = property_definition["bl_idname"]        input = n.inputs.new( prop_type, prop_name)        return input    return None        def get_socket_data(socket):    socket_data = {}    socket_data["name"] = socket.name    socket_data["bl_idname"] = socket.bl_idname    socket_data["is_output"] = socket.is_output    socket_data["is_multi_input"] = socket.is_multi_input    # if socket.bl_idname == 'TransformSpaceSocket':    #     prGreen(socket.default_value)        # here is where we'll handle a socket_data'socket special data    if socket.bl_idname == "EnumMetaBoneSocket":        socket_data["bone"] = socket.bone    if socket.bl_idname in ["EnumMetaBoneSocket", "EnumMetaRigSocket", "EnumCurveSocket"]:        if sp := socket.get("search_prop"): # may be None            socket_data["search_prop"] = sp.name # this is an object.    #    # v = socket.get("default_value") # this doesn't seem to work, see below    if hasattr(socket, "default_value"):        v = socket.default_value    else:        v = None    v_type = type(v)    if v is None:        return socket_data # we don't need to store this.    if not is_jsonable(v):        v = tuple(v)    if not is_jsonable(v):        raise RuntimeError(f"Error serializing data in {socket.node.name}::{socket.name} for value of type {v_type}")    socket_data["default_value"] = v    # at this point we can get the custom parameter ui hints if we want    if not socket.is_output:        # try and get this data        if v := getattr(socket,'min', None):            socket_data["min"] = v        if v := getattr(socket,'max', None):            socket_data["max"] = v        if v := getattr(socket,'soft_min', None):            socket_data["soft_min"] = v        if v := getattr(socket,'soft_max', None):            socket_data["soft_max"] = v        if v := getattr(socket,'description', None):            socket_data["description"] = v    return socket_data    #def export_to_json(trees, path="", write_file=True, only_selected=False):    # ignore these because they are either unrelated python stuff or useless or borked    prop_ignore = [ "__dict__", "__doc__", "__module__", "__weakref__",# "name",                    "bl_height_default", "bl_height_max", "bl_height_min",                    "bl_icon", "bl_rna", "bl_static_type", "bl_description",                    "bl_width_default", "bl_width_max", "bl_width_min",                    "__annotations__", "original", "rna_type", "view_center",                    "links", "nodes", "internal_links", "inputs", "outputs",                    "__slots__", "dimensions", "type", "interface",                    "library_weak_reference", "parsed_tree", "node_tree_updater",                    "asset_data", "preview",  # blender asset stuff                    "object_reference", # this one is here to hold on to widgets when appending                    "color_tag" , # added in blender 4.4, not used by Mantis, readonly.                    ]     # don't ignore: "bl_idname", "bl_label",    # ignore the name, it's the dict - key for the node props     # no that's stupid don't ignore the name good grief    # I am doing this because these are interactions with other addons that cause problems and probably don't exist for any given user    prop_ignore.extend(['keymesh'])    export_data = {}    for tree in trees:        base_tree = False        if tree is trees[-1]:            base_tree = True        tree_info, tree_in_out = {}, {}        for propname  in dir(tree):            # if getattr(tree, propname):            #     pass            if (propname in prop_ignore) or ( callable(getattr(tree, propname)) ):                continue            v = getattr(tree, propname)            if isinstance(getattr(tree, propname), bpy.types.bpy_prop_array):                v = tuple(getattr(tree, propname))            if not is_jsonable( v  ):                raise RuntimeError(f"Not JSON-able: {propname}, type: {type(v)}")            tree_info[propname] = v        tree_info["name"] = tree.name        # if only_selected:        #     # all in/out links, relative to the selection, should be marked and used to initialize tree properties        #     pass                            if not only_selected: # we'll handle this later with the links            for sock in tree.interface.items_tree:                sock_data={}                if sock.item_type == 'PANEL':                    sock_data["name"] = sock.name                    sock_data["item_type"] = sock.item_type                    sock_data["description"] = sock.description                    sock_data["default_closed"] = sock.default_closed                    tree_in_out[sock.name] = sock_data                # if it is a socket....                else:                    sock_parent = None                    if sock.parent:                        sock_parent = sock.parent.name                    for propname  in dir(sock):                        if (propname in prop_ignore) or ( callable(v) ):                            continue                        if (propname == "parent"):                            sock_data[propname] = sock_parent                            continue                        v = getattr(sock, propname)                        if isinstance(getattr(sock, propname), bpy.types.bpy_prop_array):                            v = tuple(getattr(sock, propname))                        if not is_jsonable( v ):                            raise RuntimeError(f"{propname}, {type(v)}")                        sock_data[propname] = v                                    tree_in_out[sock.identifier] = sock_data        nodes = {}        for n in tree.nodes:            # if this is a node-group, force it to update its interface, because it may be messed up.            # can remove this HACK when I have stronger guarentees about node-group's keeping the interface            from .base_definitions import node_group_update            if hasattr(n, "node_tree"):                n.is_updating = True                try: # HERE BE DANGER                    node_group_update(n, force=True)                finally: # ensure this line is run even if there is an error                    n.is_updating = False            if only_selected and n.select == False:                continue            node_props, sockets = {}, {}            for propname  in dir(n):                v = getattr(n, propname)                if propname in ['fake_fcurve_ob']:                    v=v.name                if (propname in prop_ignore) or ( callable(v) ):                    continue                if v.__class__.__name__ in ["Vector", "Color"]:                    v = tuple(v)                if isinstance(v, bpy.types.NodeTree):                    v = v.name                if isinstance(v, bpy.types.bpy_prop_array):                    v = tuple(v)                if propname == "parent" and v:                    v = v.name                if not is_jsonable(v):                    raise RuntimeError(f"Could not export...  {n.name}, {propname}, {type(v)}")                if v is None:                    continue                node_props[propname] = v                # so we have to accumulate the parent location because the location is not absolute                if propname == "location" and n.parent is not None:                    location_acc = Vector((0,0))                    parent = n.parent                    while (parent):                        location_acc += parent.location                        parent = parent.parent                    location_acc += getattr(n, propname)                    node_props[propname] = tuple(location_acc)                    # this works!            for i, s in enumerate(n.inputs):                socket = get_socket_data(s)                socket["index"]=i                sockets[s.identifier] = socket            for i, s in enumerate(n.outputs):                socket = get_socket_data(s)                socket["index"]=i                sockets[s.identifier] = socket                        node_props["sockets"] = sockets            nodes[n.name] = node_props                            links = []        in_sockets = {}        out_sockets = {}        in_node = {"name":"MANTIS_AUTOGEN_GROUP_INPUT", "bl_idname":"NodeGroupInput", "sockets":in_sockets}        out_node = {"name":"MANTIS_AUTOGEN_GROUP_OUTPUT", "bl_idname":"NodeGroupOutput", "sockets":out_sockets}        add_input_node, add_output_node = False, False        unique_sockets_from={}        unique_sockets_to={}        for l in tree.links:            a, b = l.from_node.name, l.from_socket.identifier            c, d = l.to_node.name, l.to_socket.identifier            # get the indices of the sockets to be absolutely sure            for e, outp in enumerate(l.from_node.outputs):                # for some reason, 'is' does not return True no matter what...                # so we are gonn compare the memory address directly, this is stupid                if (outp.as_pointer() == l.from_socket.as_pointer()): break            else:                problem=l.from_node.name + "::" + l.from_socket.name                raise RuntimeError(wrapRed(f"Error saving index of socket: {problem}"))            for f, inp in enumerate(l.to_node.inputs):                if (inp.as_pointer() == l.to_socket.as_pointer()): break            else:                problem = l.to_node.name + "::" + l.to_socket.name                raise RuntimeError(wrapRed(f"Error saving index of socket: {problem}"))            g, h = l.from_socket.name, l.to_socket.name            if base_tree:                if (only_selected and l.from_node.select) and (not l.to_node.select):                    # handle an output in the tree                    add_output_node=True                    if not (sock_name := unique_sockets_to.get(l.from_socket.node.name+l.from_socket.identifier)):                        sock_name = l.to_socket.name; name_stub = sock_name                        used_names = list(tree_in_out.keys()); i=0                        while sock_name in used_names:                            sock_name=name_stub+'.'+str(i).zfill(3); i+=1                        unique_sockets_to[l.from_socket.node.name+l.from_socket.identifier]=sock_name                    out_sock = out_sockets.get(sock_name)                    if not out_sock:                        out_sock = {}; out_sockets[sock_name] = out_sock                        out_sock["index"]=len(out_sockets) # zero indexed, so zero length makes zero the first index and so on, this works                    out_sock["name"] = sock_name                    out_sock["identifier"] = sock_name                    out_sock["bl_idname"] = l.to_socket.bl_idname                    out_sock["is_output"] = False                    out_sock["source"]=[l.to_socket.node.name,l.to_socket.identifier]                    out_sock["is_multi_input"] = False # this is not something I can even set on tree interface items, and this code is not intended for making Schema                    sock_data={}                    sock_data["name"] = sock_name                    sock_data["item_type"] = "SOCKET"                    sock_data["default_closed"] = False                    sock_data["socket_type"] = l.from_socket.bl_idname                    sock_data["identifier"] = sock_name                    sock_data["in_out"]="OUTPUT"                    sock_data["index"]=out_sock["index"]                    tree_in_out[sock_name] = sock_data                    c=out_node["name"]                    d=out_sock["identifier"]                    f=out_sock["index"]                    h=out_sock["name"]                elif (only_selected and (not l.from_node.select)) and l.to_node.select:                    add_input_node=True                    # we need to get a unique name for this                    # use the Tree IN/Out because we are dealing with Group in/out                    if not (sock_name := unique_sockets_from.get(l.from_socket.node.name+l.from_socket.identifier)):                        sock_name = l.from_socket.name; name_stub = sock_name                        used_names = list(tree_in_out.keys()); i=0                        while sock_name in used_names:                            sock_name=name_stub+'.'+str(i).zfill(3); i+=1                        unique_sockets_from[l.from_socket.node.name+l.from_socket.identifier]=sock_name                    in_sock = in_sockets.get(sock_name)                    if not in_sock:                        in_sock = {}; in_sockets[sock_name] = in_sock                        in_sock["index"]=len(in_sockets) # zero indexed, so zero length makes zero the first index and so on, this works                        #                        in_sock["name"] = sock_name                        in_sock["identifier"] = sock_name                        in_sock["bl_idname"] = l.from_socket.bl_idname                        in_sock["is_output"] = True                        in_sock["is_multi_input"] = False # this is not something I can even set on tree interface items, and this code is not intended for making Schema                        in_sock["source"] = [l.from_socket.node.name,l.from_socket.identifier]                        sock_data={}                        sock_data["name"] = sock_name                        sock_data["item_type"] = "SOCKET"                        sock_data["default_closed"] = False                        sock_data["socket_type"] = l.from_socket.bl_idname                        sock_data["identifier"] = sock_name                        sock_data["in_out"]="INPUT"                        sock_data["index"]=in_sock["index"]                                                tree_in_out[sock_name] = sock_data                    a=in_node.get("name")                    b=in_sock["identifier"]                    e=in_sock["index"]                    g=in_node.get("name")                # parentheses matter here...                elif (only_selected and not (l.from_node.select and l.to_node.select)):                    continue            elif only_selected and not (l.from_node.select and l.to_node.select):                continue # pass if both links are not selected            links.append( (a,b,c,d,e,f,g,h) ) # it's a tuple                        if add_input_node or add_output_node:            all_nodes_bounding_box=[Vector((float("inf"),float("inf"))), Vector((-float("inf"),-float("inf")))]            for n in nodes.values():                if n["location"][0] < all_nodes_bounding_box[0].x:                    all_nodes_bounding_box[0].x = n["location"][0]                if n["location"][1] < all_nodes_bounding_box[0].y:                    all_nodes_bounding_box[0].y = n["location"][1]                #                if n["location"][0] > all_nodes_bounding_box[1].x:                    all_nodes_bounding_box[1].x = n["location"][0]                if n["location"][1] > all_nodes_bounding_box[1].y:                    all_nodes_bounding_box[1].y = n["location"][1]        if add_input_node:            in_node["location"] = Vector((all_nodes_bounding_box[0].x-400, all_nodes_bounding_box[0].lerp(all_nodes_bounding_box[1], 0.5).y))            nodes["MANTIS_AUTOGEN_GROUP_INPUT"]=in_node        if add_output_node:            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)        import json    if not write_file:        return export_data # gross to have a different type of return value... but I don't care    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    def do_import_from_file(filepath, context):    import json    all_trees = [n_tree for n_tree in bpy.data.node_groups if n_tree.bl_idname in ["MantisTree", "SchemaTree"]]    for tree in all_trees:        tree.is_exporting = True        tree.do_live_update = False        def do_cleanup(tree):        tree.is_exporting = False        tree.do_live_update = True        tree.prevent_next_exec = True    with open(filepath, 'r', encoding='utf-8') as f:        data = json.load(f)        do_import(data,context)        for tree in all_trees:            do_cleanup(tree)        tree = bpy.data.node_groups[list(data.keys())[-1]]        try:            context.space_data.node_tree = tree        except AttributeError: # not hovering over the Node Editor            pass        return {'FINISHED'}    # otherwise:    # repeat this because we left the with, this is bad and ugly but I don't care    for tree in all_trees:            do_cleanup(tree)    return {'CANCELLED'}def do_import(data, context):    trees = []    tree_sock_id_maps = {}    # First: init the interface of the node graph    for tree_name, tree_data in data.items():        tree_info = tree_data[0]        tree_in_out = tree_data[1]        # need to make a new tree; first, try to get it:        tree = bpy.data.node_groups.get(tree_info["name"])        if tree is None:            tree = bpy.data.node_groups.new(tree_info["name"], tree_info["bl_idname"])        tree.nodes.clear(); tree.links.clear(); tree.interface.clear()        # this may be a bad bad thing to do without some kind of warning TODO TODO        tree.is_executing = True        tree.do_live_update = False        trees.append(tree)                tree_sock_id_map = {}        tree_sock_id_maps[tree.name] = tree_sock_id_map                interface_parent_me = {}        # I need to guarantee that the interface items are in the right order.        interface_sockets = [] # I'll just sort them afterwards so I hold them here.        default_position=0 # We'll use this if the position attribute is not set when e.g. making groups.                for s_name, s_props in tree_in_out.items():            if s_props["item_type"] == 'SOCKET':                if s_props["socket_type"] == "LayerMaskSocket":                    continue                if (socket_type := s_props["socket_type"]) == "NodeSocketColor":                    socket_type = "VectorSocket"                sock = tree.interface.new_socket(s_props["name"], in_out=s_props["in_out"], socket_type=socket_type)                tree_sock_id_map[s_name] = sock.identifier                if not (socket_position := s_props.get('position')):                    socket_position=default_position; default_position+=1                interface_sockets.append( (sock, socket_position) )                    # TODO: set whatever properties are needed (default, etc)                if panel := s_props.get("parent"): # this get is just to maintain compatibility with an older form of this script... and it is harmless                    interface_parent_me[sock] = (panel, s_props["position"])            else: # it's a panel                panel = tree.interface.new_panel(s_props["name"], description=s_props.get("description"), default_closed=s_props.get("default_closed"))            for socket, (panel, index) in interface_parent_me.items():            tree.interface.move_to_parent(                                    socket,                                    tree.interface.items_tree.get(panel),                                    index,                                    )                # BUG this was screwing up the order of things        # so I wan tot fix it and re-enable it        if False:            # Go BACK through and set the index/position now that all items exist.            interface_sockets.sort(key=lambda a : a[1])            for (socket, position) in interface_sockets:                tree.interface.move(socket, position)            # Now go and do nodes and links    for tree_name, tree_data in data.items():        print ("Importing sub-graph: %s with %s nodes" % (wrapGreen(tree_name), wrapPurple(len(tree_data[2]))) )        tree_info = tree_data[0]        nodes = tree_data[2]        links = tree_data[3]                parent_me = []        tree = bpy.data.node_groups.get(tree_info["name"])        tree.is_executing = True        tree.do_live_update = False        trees.append(tree)        tree_sock_id_map=tree_sock_id_maps[tree.name]                interface_parent_me = {}        #        from mantis.utilities import prRed, prWhite, prOrange, prGreen        for name, propslist in nodes.items():            bl_idname = propslist["bl_idname"]            if bl_idname in NODES_REMOVED:                prWhite(f"INFO: Ignoring import of node {name} of type {bl_idname}; it has been removed.")                continue            n = tree.nodes.new(bl_idname)            if bl_idname in ["DeformerMorphTargetDeform"]:                n.inputs.remove(n.inputs[-1]) # get rid of the wildcard            if n.bl_idname in [ "SchemaArrayInput",                                "SchemaArrayInputGet",                                "SchemaArrayOutput",                                "SchemaConstInput",                                "SchemaConstOutput",                                "SchemaOutgoingConnection",                                "SchemaIncomingConnection",]:                n.update()                        if sub_tree := propslist.get("node_tree"):                n.node_tree = bpy.data.node_groups.get(sub_tree)                from .base_definitions import node_group_update                n.is_updating = True                try:                    node_group_update(n, force = True)                finally:                    n.is_updating=False                        sockets_removed = []            for i, (s_id, s_val) in enumerate(propslist["sockets"].items()):                for socket_removed in SOCKETS_REMOVED:                    if n.bl_idname == socket_removed[0] and s_id == socket_removed[1]:                        prWhite(f"INFO: Ignoring import of socket {s_id}; it has been removed.")                        sockets_removed.append(s_val["index"])                        sockets_removed.sort()                        continue                try:                    if s_val["is_output"]: # for some reason it thinks the index is a string?                        if n.bl_idname == "MantisSchemaGroup":                            n.is_updating = True                            try:                                socket = n.outputs.new(s_val["bl_idname"], s_val["name"], identifier=s_id)                            finally:                                n.is_updating=False                        else:                            socket = n.outputs[int(s_val["index"])]                    else:                        for removed_index in sockets_removed:                            if s_val["index"] > removed_index:                                s_val["index"]-=1                        if s_val["index"] >= len(n.inputs):                            if n.bl_idname in add_inputs_bl_idnames:                                socket = n.inputs.new(s_val["bl_idname"], s_val["name"], identifier=s_id, use_multi_input=s_val["is_multi_input"])                            elif n.bl_idname in ["MantisSchemaGroup"]:                                n.is_updating = True                                try:                                    socket = n.inputs.new(s_val["bl_idname"], s_val["name"], identifier=s_id, use_multi_input=s_val["is_multi_input"])                                finally:                                    n.is_updating=False                            elif n.bl_idname in ["NodeGroupOutput"]:                                pass # this is dealt with separately                            else:                                prWhite("Not found: ", n.name, s_val["name"], s_id)                                prRed("Index: ", s_val["index"], "Number of inputs", len(n.inputs))                                raise NotImplementedError(wrapRed(f"{n.bl_idname} needs to be handled in JSON load."))                        else: # most of the time                            socket = n.inputs[int(s_val["index"])]                except IndexError:                    socket = fix_custom_parameter(n, propslist["sockets"][s_id])                    if socket is None:                        is_output = "output" if {s_val["is_output"]} else "input"                        prRed(s_val, type(s_val))                        raise RuntimeError(is_output, n.name, s_val["name"], s_id, len(n.inputs))                                for s_p, s_v in s_val.items():                    if s_p not in ["default_value"]:                        if s_p == "search_prop" and n.bl_idname == 'UtilityMetaRig':                            socket.node.armature= s_v                            socket.search_prop=bpy.data.objects.get(s_v)                        if s_p == "search_prop" and n.bl_idname in ['UtilityMatrixFromCurve', 'UtilityMatricesFromCurve']:                            socket.search_prop=bpy.data.objects.get(s_v)                        elif s_p == "bone" and socket.bl_idname == 'EnumMetaBoneSocket':                            socket.bone = s_v                            socket.node.pose_bone = s_v                        continue # not editable and NOT SAFE                    #                    if socket.bl_idname in ["BooleanThreeTupleSocket"]:                        value = bool(s_v[0]), bool(s_v[1]), bool(s_v[2]),                        s_v = value                    try:                        setattr(socket, s_p , s_v)                    except TypeError as e:                        prRed("Can't set socket due to type mismatch: ", n.name, socket.name, s_p, s_v)                        # raise e                    except ValueError as e:                        prRed("Can't set socket due to type mismatch: ", n.name,  socket.name, s_p, s_v)                        # raise e                    except AttributeError as e:                        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}")            for p, v in propslist.items():                if p in ["node_tree", "sockets", "warning_propagation",  "socket_idname"]:                    continue                # will throw AttributeError if read-only                # will throw TypeError if wrong type...                if n.bl_idname == "NodeFrame" and p in ["width, height, location"]:                    continue                 if version  < (4,4,0) and p == 'location_absolute':                    continue                if p == "parent" and v is not None:                    parent_me.append( (n.name, v) )                    v = None # for now) #TODO                try:                    setattr(n, p, v)                except Exception as e:                    print (p)                    raise e                for l in links:            id1 = l[1]            id2 = l[3]            #            name1=l[6]            name2=l[7]                        # if the from/to socket or node has been removed, continue            from_node = tree.nodes.get(l[0])            if not from_node:                prWhite(f"INFO: cannot create link {l[0]}:{l[1]} -->  {l[2]}:{l[3]}")                continue            if hasattr(from_node, "node_tree"): # now we have to map by name actually                try:                    id1 = from_node.outputs[l[4]].identifier                except IndexError:                    prRed ("Index incorrect")                    id1 = None            elif from_node.bl_idname in ["NodeGroupInput"]:                id1 = tree_sock_id_map.get(l[1])                if id1 is None:                    prRed(l[1])#                prOrange (l[1], id1)            elif from_node.bl_idname in ["SchemaArrayInput", "SchemaConstInput", "SchemaIncomingConnection"]:                # try the index instead                id1 = from_node.outputs[l[4]].identifier                        for from_sock in from_node.outputs:                if from_sock.identifier == id1: break            else: # we can raise a runtime error here actually                from_sock = None                            to_node = tree.nodes[l[2]]            if hasattr(to_node, "node_tree"):                try:                    id2 = to_node.inputs[l[5]].identifier                except IndexError:                    prRed ("Index incorrect")                    id2 = None            elif to_node.bl_idname in ["NodeGroupOutput"]:                id2 = tree_sock_id_map.get(l[3])            elif to_node.bl_idname in ["SchemaArrayOutput", "SchemaConstOutput", "SchemaOutgoingConnection"]:                # try the index instead                id2 = to_node.inputs[l[5]].identifier            for to_sock in to_node.inputs:                if to_sock.identifier == id2: break            else:                to_sock = None                        try:                link = tree.links.new(from_sock, to_sock)            except TypeError:                if ((id1 is not None) and ("Layer Mask" in id1)) or ((id2 is not None) and ("Layer Mask" in id2)):                    pass                else:                    prWhite(f"looking for... {name1}:{id1}, {name2}:{id2}")                    prRed (f"Failed: {l[0]}:{l[1]} --> {l[2]}:{l[3]}")                    prRed (f" got node: {from_node.name}, {to_node.name}")                    prRed (f" got socket: {from_sock}, {to_sock}")                    prOrange(to_node.inputs.keys())                    if from_sock is None:                        prOrange ("Candidates...")                        for out in from_node.outputs:                            prOrange("   %s, id=%s" % (out.name, out.identifier))                        for k, v in tree_sock_id_map.items():                            print (wrapOrange(k), wrapPurple(v))                    if to_sock is None:                        prOrange ("Candidates...")                        for inp in to_node.inputs:                            prOrange("   %s, id=%s" % (inp.name, inp.identifier))                        for k, v in tree_sock_id_map.items():                            print (wrapOrange(k), wrapPurple(v))                    raise RuntimeError                        # if at this point it doesn't work... we need to fix                    for name, p in parent_me:            if (n := tree.nodes.get(name)) and (p := tree.nodes.get(p)):                n.parent = p            # otherwise the frame node is missing because it was not included in the data e.g. when grouping nodes.                tree.is_executing = False        tree.do_live_update = True        import bpyfrom bpy_extras.io_utils import ImportHelper, ExportHelperfrom bpy.props import StringProperty, BoolProperty, EnumPropertyfrom bpy.types import Operator# Save Asclass MantisExportNodeTreeSaveAs(Operator, ExportHelper):    """Export a Mantis Node Tree by filename."""    bl_idname = "mantis.export_save_as"    bl_label = "Export Mantis Tree as ...(JSON)"    # ExportHelper mix-in class uses this.    filename_ext = ".rig"    filter_glob: StringProperty(        default="*.rig",        options={'HIDDEN'},        maxlen=255,  # Max internal buffer length, longer would be clamped.    )    @classmethod    def poll(cls, context):        return hasattr(context.space_data, 'path')    def execute(self, context):        # we need to get the dependent trees from self.tree...        # there is no self.tree        # how do I choose a tree?                base_tree=context.space_data.path[-1].node_tree        from .utilities import all_trees_in_tree        trees = all_trees_in_tree(base_tree)[::-1]        prGreen("Exporting node graph with dependencies...")        for t in trees:            prGreen ("Node graph: \"%s\"" % (t.name))        base_tree.is_exporting = True        export_to_json(trees, self.filepath)        base_tree.is_exporting = False        base_tree.prevent_next_exec = True        return {'FINISHED'}# Saveclass MantisExportNodeTreeSave(Operator):    """Save a Mantis Node Tree to disk."""    bl_idname = "mantis.export_save"    bl_label = "Export Mantis Tree (JSON)"    @classmethod    def poll(cls, context):        return hasattr(context.space_data, 'path')    def execute(self, context):                base_tree=context.space_data.path[-1].node_tree        from .utilities import all_trees_in_tree        trees = all_trees_in_tree(base_tree)[::-1]        prGreen("Exporting node graph with dependencies...")        for t in trees:            prGreen ("Node graph: \"%s\"" % (t.name))        base_tree.is_exporting = True        export_to_json(trees, self.filepath)        base_tree.is_exporting = False        base_tree.prevent_next_exec = True        return {'FINISHED'}# Save Choose:class MantisExportNodeTree(Operator):    """Save a Mantis Node Tree to disk."""    bl_idname = "mantis.export_save_choose"    bl_label = "Export Mantis Tree (JSON)"    @classmethod    def poll(cls, context):        return hasattr(context.space_data, 'path')    def execute(self, context):        base_tree=context.space_data.path[-1].node_tree        if base_tree.filepath:            prRed(base_tree.filepath)            return bpy.ops.mantis.export_save()        else:            return bpy.ops.mantis.export_save_as('INVOKE_DEFAULT')# here is what needs to be done...#   - modify this to work with a sort of parsed-tree instead (sort of)#        - this needs to treat each sub-graph on its own#        - is this a problem? do I need to reconsider how I treat the graph data in mantis?#        - I should learn functional programming / currying#   - then the parsed-tree this builds must be executed as Blender nodes#   - I think... this is not important right now. not yet.#   -  KEEP IT SIMPLE, STUPIDclass MantisImportNodeTree(Operator, ImportHelper):    """Import a Mantis Node Tree."""    bl_idname = "mantis.import_tree"    bl_label = "Import Mantis Tree (JSON)"    # ImportHelper mixin class uses this    filename_ext = ".rig"    filter_glob : StringProperty(        default="*.rig",        options={'HIDDEN'},        maxlen=255,  # Max internal buffer length, longer would be clamped.    )    def execute(self, context):        return do_import_from_file(self.filepath, context)# this is useful:# https://blender.stackexchange.com/questions/73286/how-to-call-a-confirmation-dialog-box# class MantisReloadConfirmMenu(bpy.types.Panel):#     bl_label = "Confirm?"#     bl_idname = "OBJECT_MT_mantis_reload_confirm"#     def draw(self, context):#         layout = self.layout#         layout.operator("mantis.reload_tree")class MantisReloadNodeTree(Operator):    # """Import a Mantis Node Tree."""    # bl_idname = "mantis.reload_tree"    # bl_label = "Import Mantis Tree"    """Reload Mantis Tree"""    bl_idname = "mantis.reload_tree"    bl_label = "Confirm reload tree?"    bl_options = {'REGISTER', 'INTERNAL'}    @classmethod    def poll(cls, context):        if hasattr(context.space_data, 'path'):            return True        return False    def invoke(self, context, event):        return context.window_manager.invoke_confirm(self, event)    def execute(self, context):        base_tree=context.space_data.path[-1].node_tree        if not base_tree.filepath:            self.report({'ERROR'}, "Tree has not been saved - so it cannot be reloaded.")            return {'CANCELLED'}        self.report({'INFO'}, "reloading tree")        return do_import_from_file(base_tree.filepath, context)# todo:#  - export metarig and option to import it#  - same with controls#  - it would be nice to have a library of these that can be imported alongside the mantis graph
 |