| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018 | import bpyfrom bpy.types import Operatorfrom mathutils import Vectorfrom .utilities import (prRed, prGreen, prPurple, prWhite,                              prOrange,                              wrapRed, wrapGreen, wrapPurple, wrapWhite,                              wrapOrange,)def mantis_tree_poll_op(context):    space = context.space_data    if hasattr(space, "node_tree"):        if (space.node_tree):            return (space.tree_type in ["MantisTree", "SchemaTree"])    return Falsedef any_tree_poll(context):    space = context.space_data    if hasattr(space, "node_tree"):        return True    return False#########################################################################3class MantisGroupNodes(Operator):    """Create node-group from selected nodes"""    bl_idname = "mantis.group_nodes"    bl_label = "Group Nodes"    bl_options = {'REGISTER', 'UNDO'}    @classmethod    def poll(cls, context):        return mantis_tree_poll_op(context)    def execute(self, context):        base_tree=context.space_data.path[-1].node_tree        try:            for path_item in context.space_data.path:                path_item.node_tree.is_exporting = True            from .i_o import export_to_json, do_import            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)            # 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)            affected_links_in = []            affected_links_out = []            for l in base_tree.links:                if l.from_node.select and not l.to_node.select: affected_links_out.append(l)                if not l.from_node.select and l.to_node.select: affected_links_in.append(l)            delete_me = []            all_nodes_bounding_box=[Vector((float("inf"),float("inf"))), Vector((-float("inf"),-float("inf")))]            for n in base_tree.nodes:                if n.select:                    node_loc = (0,0,0)                    if bpy.app.version <= (4, 4):                        node_loc = n.location                        parent = n.parent                        while (parent): # accumulate parent offset                            node_loc += parent.location                            parent = parent.parent                    else: # there is a new location_absolute property in 4.4                        node_loc = n.location_absolute                    if node_loc.x < all_nodes_bounding_box[0].x:                        all_nodes_bounding_box[0].x = node_loc.x                    if node_loc.y < all_nodes_bounding_box[0].y:                        all_nodes_bounding_box[0].y = node_loc.y                    #                    if node_loc.x > all_nodes_bounding_box[1].x:                        all_nodes_bounding_box[1].x = node_loc.x                    if node_loc.y > all_nodes_bounding_box[1].y:                        all_nodes_bounding_box[1].y = node_loc.y                    delete_me.append(n)            grp_node = base_tree.nodes.new('MantisNodeGroup')            grp_node.node_tree = bpy.data.node_groups[grp_name]            bb_center = all_nodes_bounding_box[0].lerp(all_nodes_bounding_box[1],0.5)            for n in grp_node.node_tree.nodes:                n.location -= bb_center            grp_node.location = Vector((all_nodes_bounding_box[0].x+200, all_nodes_bounding_box[0].lerp(all_nodes_bounding_box[1], 0.5).y))            from .base_definitions import node_group_update            grp_node.is_updating=True            try:                node_group_update(grp_node, force=True)            finally:                grp_node.is_updating=False            # for each node in the JSON            for n in selected_nodes[base_tree.name][2].values():                for s in n["sockets"].values(): # for each socket in the node                    if source := s.get("source"):                        prGreen (s["name"], source[0], source[1])                        base_tree_node=base_tree.nodes.get(source[0])                        if s["is_output"]:                            for output in base_tree_node.outputs:                                if output.identifier == source[1]:                                    break                            else:                                raise RuntimeError(wrapRed("Socket not found when grouping"))                            base_tree.links.new(input=output, output=grp_node.inputs[s["name"]])                        else:                            for s_input in base_tree_node.inputs:                                if s_input.identifier == source[1]:                                    break                            else:                                raise RuntimeError(wrapRed("Socket not found when grouping"))                            base_tree.links.new(input=grp_node.outputs[s["name"]], output=s_input)            for n in delete_me: base_tree.nodes.remove(n)            base_tree.nodes.active = grp_node        finally: # MAKE SURE to turn it back to not exporting            for path_item in context.space_data.path:                path_item.node_tree.is_exporting = False                grp_node.node_tree.name = "Group_Node.000"        return {'FINISHED'}class MantisEditGroup(Operator):    """Edit the group referenced by the active node (or exit the current node-group)"""    bl_idname = "mantis.edit_group"    bl_label = "Edit Group"    bl_options = {'REGISTER', 'UNDO'}    @classmethod    def poll(cls, context):        return (            mantis_tree_poll_op(context)        )    def execute(self, context):        space = context.space_data        path = space.path        node = path[len(path)-1].node_tree.nodes.active        base_tree = path[0].node_tree        live_update_start_state=base_tree.do_live_update        base_tree.do_live_update = False        base_tree.is_executing = True        try:            if hasattr(node, "node_tree"):                if (node.node_tree):                    path.append(node.node_tree, node=node)                    path[0].node_tree.display_update(context)                    return {"FINISHED"}            elif len(path) > 1:                path.pop()                # get the active node in the current path                active = path[len(path)-1].node_tree.nodes.active                from .base_definitions import node_group_update                active.is_updating = True                try:                    node_group_update(active, force = True)                finally:                    active.is_updating = False                # call update to force the node group to check if its tree has changed                # now we need to loop through the tree and update all node groups of this type.                from .utilities import get_all_nodes_of_type                for g in get_all_nodes_of_type(base_tree, "MantisNodeGroup") + \                         get_all_nodes_of_type(base_tree, "MantisSchemaGroup"):                    if g.node_tree == active.node_tree:                        g.is_updating = True                        active.is_updating = True                        try:                            node_group_update(g, force = True)                        finally:                            g.is_updating = False                            active.is_updating = False                base_tree.display_update(context)                base_tree.is_executing = True                # base_tree.is_executing = True # because it seems display_update unsets this.        finally:            base_tree.do_live_update = live_update_start_state            base_tree.is_executing = False            # HACK            base_tree.handler_flip = True # HACK            # HACK            # I have no idea why but the operator finishing causes the exeuction handler to fire            # I have no control over this since it happens after the execution returns...            # so I have to do this ridiculous hack with a Boolean flip bit.            return {"FINISHED"}class MantisNewNodeTree(Operator):    """Add a new Mantis tree."""    bl_idname = "mantis.new_node_tree"    bl_label = "New Node Group"    bl_options = {'REGISTER', 'UNDO'}    tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})    node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})    @classmethod    def poll(cls, context):        return (            mantis_tree_poll_op(context) and \                context.node.bl_idname in ["MantisNodeGroup", "MantisSchemaGroup"]        )    @classmethod    def poll(cls, context):        return True #(hasattr(context, 'active_node') )    def invoke(self, context, event):        self.tree_invoked = context.node.id_data.name        self.node_invoked = context.node.name        return self.execute(context)            def execute(self, context):        node = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]        if node.bl_idname == "MantisSchemaGroup":            if (node.node_tree):                print('a')                return {"CANCELLED"}            else:                from bpy import data                print('b')                node.node_tree = data.node_groups.new(name='Schema Group', type='SchemaTree')                return {'FINISHED'}        elif node.bl_idname == "MantisNodeGroup":            if (node.node_tree):                print('c')                return {"CANCELLED"}            else:                from bpy import data                print('d')                node.node_tree = data.node_groups.new(name='Mantis Group', type='MantisTree')                return {'FINISHED'}        else:            return {"CANCELLED"}class InvalidateNodeTree(Operator):    """Invalidates this node tree, forcing it to read all data again."""    bl_idname = "mantis.invalidate_node_tree"    bl_label = "Clear Node Tree Cache"    bl_options = {'REGISTER', 'UNDO'}    @classmethod    def poll(cls, context):        return (mantis_tree_poll_op(context))    def execute(self, context):        print("Clearing Active Node Tree cache")        tree=context.space_data.path[0].node_tree        tree.execution_id=''; tree.hash=''        return {"FINISHED"}class ExecuteNodeTree(Operator):    """Execute this node tree"""    bl_idname = "mantis.execute_node_tree"    bl_label = "Execute Node Tree"    bl_options = {'REGISTER', 'UNDO'}    @classmethod    def poll(cls, context):        return (mantis_tree_poll_op(context))    def execute(self, context):        from time import time        from .utilities import wrapGreen                tree=context.space_data.path[0].node_tree                import cProfile        from os import environ        start_time = time()        do_profile=False        if environ.get("DOPROFILE"):            do_profile=True        pass_error = True        if environ.get("DOERROR"):            pass_error=False        if do_profile:            import pstats, io            from pstats import SortKey            with cProfile.Profile() as pr:                tree.update_tree(context, error_popups = pass_error)                tree.execute_tree(context, error_popups = pass_error)                # from the Python docs at https://docs.python.org/3/library/profile.html#module-cProfile                s = io.StringIO()                sortby = SortKey.TIME                # sortby = SortKey.CUMULATIVE                ps = pstats.Stats(pr, stream=s).strip_dirs().sort_stats(sortby)                ps.print_stats(20) # print the top 20                print(s.getvalue())        else:            tree.update_tree(context, error_popups = pass_error)            tree.execute_tree(context, error_popups = pass_error)        prGreen("Finished executing tree in %f seconds" % (time() - start_time))        return {"FINISHED"}class SelectNodesOfType(Operator):    """Selects all nodes of same type as active node."""    bl_idname = "mantis.select_nodes_of_type"    bl_label = "Select Nodes of Same Type as Active"    bl_options = {'REGISTER', 'UNDO'}    @classmethod    def poll(cls, context):        return (any_tree_poll(context))    def execute(self, context):        active_node = context.active_node        tree = active_node.id_data        if not hasattr(active_node, "node_tree"):            for node in tree.nodes:                node.select = (active_node.bl_idname == node.bl_idname)        else:            for node in tree.nodes:                node.select = (active_node.bl_idname == node.bl_idname) and (active_node.node_tree == node.node_tree)        return {"FINISHED"}def get_parent_tree_interface_enum(operator, context):    ret = []; i = -1    tree = bpy.data.node_groups[operator.tree_invoked]    for sock in tree.interface.items_tree:        if sock.item_type == 'PANEL': continue        if sock.in_out == "OUTPUT": continue        ret.append( (sock.identifier, sock.name, "Socket from Node Group Input", i := i + 1), )    return retdef get_node_inputs_enum(operator, context):    ret = []; i = -1    n = bpy.data.node_groups[operator.tree_invoked].nodes[operator.node_invoked]    for inp in n.inputs:        ret.append( (inp.identifier, inp.name, "Socket of node to connect to.", i := i + 1), )    return retclass ConnectNodeToInput(Operator):    """Connects a Node Group Input socket to specified socket of active node and all selected same-type nodes."""    bl_idname = "mantis.connect_nodes_to_input"    bl_label = "Connect Socket to Input for Selected Nodes"    bl_options = {'REGISTER', 'UNDO'}    group_output : bpy.props.EnumProperty(        items=get_parent_tree_interface_enum,        name="Node Group Input Socket",        description="Select which socket from the Node Group Input to connect to this node",)    node_input : bpy.props.EnumProperty(        items=get_node_inputs_enum,        name="Node Input Socket",        description="Select which of this node's sockets to recieve the connection",)    tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})    world_invoked : bpy.props.StringProperty(options ={'HIDDEN'})    material_invoked : bpy.props.StringProperty(options ={'HIDDEN'})    node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})    @classmethod    def poll(cls, context):        is_tree = (any_tree_poll(context))        is_material = False        if hasattr(context, 'space_data') and context.space_data.type == 'NODE_EDITOR':            is_material = context.space_data.node_tree.bl_idname == 'ShaderNodeTree'        return is_tree and not is_material # doesn't work for Material right now TODO    def invoke(self, context, event):        self.material_invoked=''; self.world_invoked=''        self.tree_invoked = context.active_node.id_data.name        if context.space_data.node_tree.bl_idname == 'ShaderNodeTree':            if context.space_data.shader_type == 'WORLD':                self.world_invoked = context.space_data.id.name            elif context.space_data.shader_type == 'OBJECT':                self.material_invoked = context.space_data.id.name            else:                return {'CANCELLED'} # what is the LINESTYLE menu? TODO        self.node_invoked = context.active_node.name        # we use active_node here ^ because we are comparing the active node to the selection.        wm = context.window_manager        return wm.invoke_props_dialog(self)        def execute(self, context):        if context.space_data.node_tree.bl_idname == 'ShaderNodeTree':            if context.space_data.shader_type == 'WORLD':                t = bpy.data.worlds[self.material_invoked].node_tree            elif context.space_data.shader_type == 'OBJECT':                t = bpy.data.materials[self.material_invoked].node_tree        else:            t = bpy.data.node_groups.get(self.tree_invoked)        if hasattr(t, "is_executing"): # for Mantis trees, but this function should just work anywhere.            t.is_executing = True        n = t.nodes[self.node_invoked]        for node in t.nodes:            if n.bl_idname == node.bl_idname and node.select:                # the bl_idname is the same so they both have node_tree                if hasattr(n, "node_tree") and n.node_tree != node.node_tree: continue                # TODO: maybe I should try and find a nearby input node and reuse it                # doing these identifier lookups again and again is slow, whatever. faster than doing it by hand                for connect_to_me in node.inputs:                    if connect_to_me.identifier == self.node_input: break                if connect_to_me.is_linked: connect_to_me = None                if connect_to_me: # only make the node if the socket is there and free                    inp = t.nodes.new("NodeGroupInput")                    connect_me = None                    for s in inp.outputs:                        if s.identifier != self.group_output: s.hide = True                        else: connect_me = s                        inp.location = node.location                        inp.location.x-=200                    if connect_me is None:                        continue # I don't know how this happens.                    # TODO: this bit doesn't work in shader trees for some reason, fix it                    t.links.new(input=connect_me, output=connect_to_me)        if hasattr(t, "is_executing"):            t.is_executing = False        return {"FINISHED"}class QueryNodeSockets(Operator):    """Utility Operator for querying the data in a socket"""    bl_idname = "mantis.query_sockets"    bl_label = "Query Node Sockets"    bl_options = {'REGISTER', 'UNDO'}    @classmethod    def poll(cls, context):        return (mantis_tree_poll_op(context))    def execute(self, context):        active_node = context.active_node        tree = active_node.id_data        for node in tree.nodes:            if not node.select: continue                return {"FINISHED"}class ForceDisplayUpdate(Operator):    """Utility Operator for querying the data in a socket"""    bl_idname = "mantis.force_display_update"    bl_label = "Force Mantis Display Update"    bl_options = {'REGISTER', 'UNDO'}    @classmethod    def poll(cls, context):        return (mantis_tree_poll_op(context))    def execute(self, context):        base_tree = bpy.context.space_data.path[0].node_tree        base_tree.display_update(context)        return {"FINISHED"}class CleanUpNodeGraph(bpy.types.Operator):    """Clean Up Node Graph"""    bl_idname = "mantis.nodes_cleanup"    bl_label = "Clean Up Node Graph"    bl_options = {'REGISTER', 'UNDO'}    # num_iterations=bpy.props.IntProperty(default=8)    @classmethod    def poll(cls, context):        return hasattr(context, 'active_node')    def execute(self, context):        base_tree=context.space_data.path[-1].node_tree        from .utilities import SugiyamaGraph        SugiyamaGraph(base_tree, 12)        return {'FINISHED'}class MantisMuteNode(Operator):    """Mantis Test Operator"""    bl_idname = "mantis.mute_node"    bl_label = "Mute Node"    bl_options = {'REGISTER', 'UNDO'}    @classmethod    def poll(cls, context):        return (mantis_tree_poll_op(context))    def execute(self, context):        path = context.space_data.path        node = path[len(path)-1].node_tree.nodes.active        node.mute = not node.mute        # There should only be one of these        if (enable := node.inputs.get("Enable")):                # annoyingly, 'mute' and 'enable' are opposites                enable.default_value = not node.mute        # this one is for Deformers        elif (enable := node.inputs.get("Enable in Viewport")):                # annoyingly, 'mute' and 'enable' are opposites                enable.default_value = not node.mute        elif (hide := node.inputs.get("Hide")):                hide.default_value = node.mute        elif (hide := node.inputs.get("Hide in Viewport")):                hide.default_value = node.mute        return {"FINISHED"}ePropertyType =(        ('BOOL'  , "Boolean", "Boolean", 0),        ('INT'   , "Integer", "Integer", 1),        ('FLOAT' , "Float"  , "Float"  , 2),        ('VECTOR', "Vector" , "Vector" , 3),        ('STRING', "String" , "String" , 4),        #('ENUM'  , "Enum"   , "Enum"   , 5),    )    from .base_definitions import xFormNodeclass AddCustomProperty(bpy.types.Operator):    """Add Custom Property to xForm Node"""    bl_idname = "mantis.add_custom_property"    bl_label = "Add Custom Property"    bl_options = {'REGISTER', 'UNDO'}    prop_type : bpy.props.EnumProperty(        items=ePropertyType,        name="New Property Type",        description="Type of data for new Property",        default = 'BOOL',)    prop_name  : bpy.props.StringProperty(default='Prop')        min:bpy.props.FloatProperty(default = 0)    max:bpy.props.FloatProperty(default = 1)    soft_min:bpy.props.FloatProperty(default = 0)    soft_max:bpy.props.FloatProperty(default = 1)    description:bpy.props.StringProperty(default = "")        tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})    node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})    @classmethod    def poll(cls, context):        return True #( hasattr(context, 'node') )     def invoke(self, context, event):        self.tree_invoked = context.node.id_data.name        self.node_invoked = context.node.name        wm = context.window_manager        return wm.invoke_props_dialog(self)            def execute(self, context):        n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]        # For whatever reason, context.node doesn't exist anymore        #   (probably because I use a window to execute)        # so as a sort of dumb workaround I am saving it to a hidden        #  property of the operator... it works but Blender complains.        socktype = ''        if not (self.prop_name):            self.report({'ERROR_INVALID_INPUT'}, "Must name the property.")            return {'CANCELLED'}        if self.prop_type == 'BOOL':            socktype = 'ParameterBoolSocket'        if self.prop_type == 'INT':            socktype = 'ParameterIntSocket'        if self.prop_type == 'FLOAT':            socktype = 'ParameterFloatSocket'        if self.prop_type == 'VECTOR':            socktype = 'ParameterVectorSocket'        if self.prop_type == 'STRING':            socktype = 'ParameterStringSocket'        #if self.prop_type == 'ENUM':        #    sock_type = 'ParameterStringSocket'        if (s := n.inputs.get(self.prop_name)):            try:                number = int(self.prop_name[-3:])                # see if it has a number                number+=1                self.prop_name = self.prop_name[:-3] + str(number).zfill(3)            except ValueError:                self.prop_name+='.001'                # WRONG # HACK # TODO # BUG #        new_prop = n.inputs.new( socktype, self.prop_name)        if self.prop_type in ['INT','FLOAT']:            new_prop.min = self.min            new_prop.max = self.max            new_prop.soft_min = self.soft_min            new_prop.soft_max = self.soft_max        new_prop.description = self.description        # now do the output        n.outputs.new( socktype, self.prop_name)                return {'FINISHED'}def main_get_existing_custom_properties(operator, context):    ret = []; i = -1    n = bpy.data.node_groups[operator.tree_invoked].nodes[operator.node_invoked]    for inp in n.inputs:        if 'Parameter' in inp.bl_idname:            ret.append( (inp.identifier, inp.name, "Custom Property to Modify", i := i + 1), )    return ret            class EditCustomProperty(bpy.types.Operator):    """Edit Custom Property"""    bl_idname = "mantis.edit_custom_property"    bl_label = "Edit Custom Property"    bl_options = {'REGISTER', 'UNDO'}    def get_existing_custom_properties(self, context):        return main_get_existing_custom_properties(self, context)    prop_edit : bpy.props.EnumProperty(        items=get_existing_custom_properties,        name="Property to Edit?",        description="Select which property to edit",)    prop_type : bpy.props.EnumProperty(        items=ePropertyType,        name="New Property Type",        description="Type of data for new Property",        default = 'BOOL',)    prop_name  : bpy.props.StringProperty(default='Prop')    min:bpy.props.FloatProperty(default = 0)    max:bpy.props.FloatProperty(default = 1)    soft_min:bpy.props.FloatProperty(default = 0)    soft_max:bpy.props.FloatProperty(default = 1)    description:bpy.props.StringProperty(default = "") # TODO: use getters to fill these automatically        tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})    node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})    @classmethod    def poll(cls, context):        return True #( hasattr(context, 'node') )     def invoke(self, context, event):        self.tree_invoked = context.node.id_data.name        self.node_invoked = context.node.name        wm = context.window_manager        return wm.invoke_props_dialog(self)            def execute(self, context):        n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]        prop = n.inputs.get( self.prop_edit )        if prop:            prop.name = self.prop_name            if (s := n.inputs.get(self.prop_edit)):                if self.prop_type in ['INT','FLOAT']:                    prop.min = self.min                    prop.max = self.max                    prop.soft_min = self.soft_min                    prop.soft_max = self.soft_max                prop.description = self.description            return {'FINISHED'}        else:            self.report({'ERROR_INVALID_INPUT'}, "Cannot edit a property that does not exist.")class RemoveCustomProperty(bpy.types.Operator):    """Remove a Custom Property from an xForm Node"""    bl_idname = "mantis.remove_custom_property"    bl_label = "Remove Custom Property"    bl_options = {'REGISTER', 'UNDO'}    def get_existing_custom_properties(self, context):        return main_get_existing_custom_properties(self, context)        prop_remove : bpy.props.EnumProperty(        items=get_existing_custom_properties,        name="Property to remove?",        description="Select which property to remove",)    tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})    node_invoked : bpy.props.StringProperty(options ={'HIDDEN'})    @classmethod    def poll(cls, context):        return True #(hasattr(context, 'active_node') )    def invoke(self, context, event):        self.tree_invoked = context.node.id_data.name        self.node_invoked = context.node.name        t = context.node.id_data        # HACK the props dialog makes this necesary        #  because context.node only exists during the event that        #  was created by clicking on the node.        t.nodes.active = context.node # HACK        context.node.select = True # HACK        # I need this bc of the callback for the enum property.        #  for whatever reason I can't use tree_invoked there        wm = context.window_manager        return wm.invoke_props_dialog(self)            def execute(self, context):        n = bpy.data.node_groups[self.tree_invoked].nodes[self.node_invoked]        # For whatever reason, context.node doesn't exist anymore        #   (probably because I use a window to execute)        # so as a sort of dumb workaround I am saving it to a hidden        #  property of the operator... it works.        for i, inp in enumerate(n.inputs):            if inp.identifier == self.prop_remove:                break        else:            self.report({'ERROR'}, "Input not found")            return {'CANCELLED'}        # it's possible that the output property's identifier isn't the        #   exact same... but I don' care. Shouldn't ever happen. TODO        for j, out in enumerate(n.outputs):            if out.identifier == self.prop_remove:                break        else:            self.report({'ERROR'}, "Output not found")            raise RuntimeError("This should not happen!")        n.inputs.remove ( n.inputs [i] )        n.outputs.remove( n.outputs[j] )        return {'FINISHED'}# SIMPLE node operators...# May rewrite these in a more generic way laterclass FcurveAddKeyframeInput(bpy.types.Operator):    """Add a keyframe input to the fCurve node"""    bl_idname = "mantis.fcurve_node_add_kf"    bl_label = "Add Keyframe"    bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}        @classmethod    def poll(cls, context):        return (hasattr(context, 'active_node') )    def execute(self, context):        num_keys = len( context.node.inputs)-1        context.node.inputs.new("KeyframeSocket", "Keyframe."+str(num_keys).zfill(3))        return {'FINISHED'}class FcurveRemoveKeyframeInput(bpy.types.Operator):    """Remove a keyframe input from the fCurve node"""    bl_idname = "mantis.fcurve_node_remove_kf"    bl_label = "Remove Keyframe"    bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}            @classmethod    def poll(cls, context):        return (hasattr(context, 'active_node') )    def execute(self, context):        n = context.node        n.inputs.remove(n.inputs[-1])        return {'FINISHED'}class DriverAddDriverVariableInput(bpy.types.Operator):    """Add a Driver Variable input to the Driver node"""    bl_idname = "mantis.driver_node_add_variable"    bl_label = "Add Driver Variable"    bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}        @classmethod    def poll(cls, context):        return (hasattr(context, 'active_node') )    def execute(self, context):           # unicode for 'a'        i = len (context.node.inputs) - 2 + 96        context.node.inputs.new("DriverVariableSocket", chr(i))        return {'FINISHED'}class DriverRemoveDriverVariableInput(bpy.types.Operator):    """Remove a DriverVariable input from the active Driver node"""    bl_idname = "mantis.driver_node_remove_variable"    bl_label = "Remove Driver Variable"    bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}            @classmethod    def poll(cls, context):        return (hasattr(context, 'active_node') )    def execute(self, context):        n = context.node        n.inputs.remove(n.inputs[-1])        return {'FINISHED'}        class LinkArmatureAddTargetInput(bpy.types.Operator):    """Add a Driver Variable input to the Driver node"""    bl_idname = "mantis.link_armature_node_add_target"    bl_label = "Add Target"    bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}        @classmethod    def poll(cls, context):        return hasattr(context, 'node')    def execute(self, context):           # unicode for 'a'        num_targets = len( list(context.node.inputs)[6:])//2        context.node.inputs.new("xFormSocket", "Target."+str(num_targets).zfill(3))        context.node.inputs.new("FloatFactorSocket", "Weight."+str(num_targets).zfill(3))        return {'FINISHED'}class LinkArmatureRemoveTargetInput(bpy.types.Operator):    """Remove a DriverVariable input from the active Driver node"""    bl_idname = "mantis.link_armature_node_remove_target"    bl_label = "Remove Target"    bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}            @classmethod    def poll(cls, context):        return hasattr(context, 'node')    def execute(self, context):        n = context.node        n.inputs.remove(n.inputs[-1]); n.inputs.remove(n.inputs[-1])        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    for cls in TellClasses():        if cls.is_valid_interface_type:            valid_types.append( (cls.bl_idname, cls.bl_label, "Socket Type", i := i + 1), )    return valid_typesclass B4_4_0_Workaround_NodeTree_Interface_Update(Operator):    """Selects all nodes of same type as active node."""    bl_idname = "mantis.node_tree_interface_update_4_4_0_workaround"    bl_label = "Add Socket to Node Tree"    bl_options = {'REGISTER', 'UNDO'}    socket_name : bpy.props.StringProperty()    output : bpy.props.BoolProperty()    socket_type  : bpy.props.EnumProperty(        name="Socket Type",        description="Socket Type",        items=get_socket_enum,        default=0,)    tree_invoked : bpy.props.StringProperty(options ={'HIDDEN'})    @classmethod    def poll(cls, context):        return (any_tree_poll(context))    def invoke(self, context, event):        self.tree_invoked = context.active_node.id_data.name        # we use active_node here ^ because we are comparing the active node to the selection.        wm = context.window_manager        return wm.invoke_props_dialog(self)        def execute(self, context):        tree = bpy.data.node_groups[self.tree_invoked]        in_out = 'OUTPUT' if self.output else 'INPUT'        tree.interface.new_socket(self.socket_name, in_out=in_out, socket_type=self.socket_type)        # try to prevent the next execution        # because updating the interface triggers a depsgraph update.        # this doesn't actually work though...TODO        if tree.bl_idname == "MantisTree":            tree.prevent_next_exec=True         return {"FINISHED"}class ConvertBezierCurveToNURBS(Operator):    """Converts all bezier splines of curve to NURBS."""    bl_idname = "mantis.convert_bezcrv_to_nurbs"    bl_label = "Convert Bezier Curve to NURBS"    bl_options = {'REGISTER', 'UNDO'}    @classmethod    def poll(cls, context):        return (context.active_object is not None) and (context.active_object.type=='CURVE')    def execute(self, context):        from .utilities import nurbs_copy_bez_spline        curve = context.active_object        bez=[]        for spl in curve.data.splines:            if spl.type=='BEZIER':                bez.append(spl)        for bez_spline in bez:            new_spline=nurbs_copy_bez_spline(curve, bez_spline)                        curve.data.splines.remove(bez_spline)        return {"FINISHED"}# this has to be down here for some reason. what a painclasses = [        MantisGroupNodes,        MantisEditGroup,        MantisNewNodeTree,        InvalidateNodeTree,        ExecuteNodeTree,        # CreateMetaGroup,        QueryNodeSockets,        ForceDisplayUpdate,        CleanUpNodeGraph,        MantisMuteNode,        SelectNodesOfType,        ConnectNodeToInput,        # xForm        AddCustomProperty,        EditCustomProperty,        RemoveCustomProperty,        # EditFCurveNode,        FcurveAddKeyframeInput,        FcurveRemoveKeyframeInput,        # Driver        DriverAddDriverVariableInput,        DriverRemoveDriverVariableInput,        # Armature Link Node        LinkArmatureAddTargetInput,        LinkArmatureRemoveTargetInput,        # managing collections        CollectionAddNewOutput,        CollectionRemoveOutput,        # rigging utilities        ConvertBezierCurveToNURBS,        ]if (bpy.app.version >= (4, 4, 0)):    classes.append(B4_4_0_Workaround_NodeTree_Interface_Update)def TellClasses():    return classes
 |