Noodler.py

Hey folks
This is the blender artist topic of Noodler, a free node utility plugin featuring so far:

  • Quick Frames
  • Fast Links/Reroutes
  • Favorites
  • Fast Color Assign
  • Downstream/Upstream Select
  • Reroute Chamfer
  • Advanced Search
  • Purge Unused

Available for free on Gumroad

And for a $ donation to the dev fund on BM

Known Issues:

  • when we set values on python API it sends an update signal, unfortunately moving a node/reroute from a modal operator like these from noodler will also send update signal, slowing down the scene unecessarily. Ive made a thread on the devtalk about this here.
    Sadly this appear to be a limitation to all blender plugins.
Old Post

Hello,
i made a little addon that let you create nodal shortcuts easily.
It’s a behavior similar to what we can see in text editors such as SublimeText3

  • CTRL+F2 to create/remove shortcuts on the active node
  • F2 to loop over your shortcuts
  • If you don’t like F2/CTRL F2 You can change the keymap by changing km.keymap_items.new() function if needed in register() (or simply edit your keymaps) i chose these keys as most programmers are already familiar with them
  • Altho i made it for geonode it It seems to work on any kind of NodeTree types

geonode_shortcut.py (7.9 KB)

ddd

bl_info = {
    "name"        : "Node Shortcuts",
    "author"      : "BD3D",
    "description" : "Create/remove Shortcut to active node with [CTRL+F2], loop Shortcuts with [F2]",
    "blender"     : (2, 92, 0),
    "version"     : (5, 0),
    "wiki_url"    : "", 
    "support"     : "COMMUNITY",
    "category"    : "Node", 
}


import bpy



#  dP"Yb  88""Yb 888888 88""Yb    db    888888  dP"Yb  88""Yb .dP"Y8
# dP   Yb 88__dP 88__   88__dP   dPYb     88   dP   Yb 88__dP `Ybo."
# Yb   dP 88"""  88""   88"Yb   dP__Yb    88   Yb   dP 88"Yb  o.`Y8b
#  YbodP  88     888888 88  Yb dP""""Yb   88    YbodP  88  Yb 8bodP'





def get_active_node():
    return bpy.context.space_data.node_tree.nodes.active 

def set_node_active(node):
    bpy.context.space_data.node_tree.nodes.active = node

def deselect_all():
    node_group = bpy.context.space_data.node_tree
    for nodes in node_group.nodes:
        nodes.select = False 
    return 

def get_selection():
    node_list  = []
    node_group = bpy.context.space_data.node_tree
    for n in node_group.nodes:
        if n.select: 
            node_list.append(n)
    return node_list

def set_selection(node_list):
    node_group = bpy.context.space_data.node_tree
    for n in node_list:
        if n.name in node_group.nodes:
            n.select = True 
    return 

def popup_menu(msgs,title,icon):
    def draw(self, context):
        layout = self.layout
        for msg in msgs:
            layout.label(text=msg)
        return  
    bpy.context.window_manager.popup_menu(draw, title=title, icon=icon)
    return None 



class SHORT_OT_add_remove_shortcuts(bpy.types.Operator):
    """add or remove items from list"""
    bl_idname      = "short.add_remove_shortcuts"
    bl_label       = "Node Shortcuts : add/remove shortcuts from/to list"
    bl_description = "Add or remove shortcuts"

    def execute(self, context):

        node_group = context.space_data.node_tree
        active = get_active_node()
        n = node_group.shortcuts.get(active.name)
        
        if active is None: 
            return {"FINISHED"}

        #if shortcut is none, add, else remove
        if n is None: 
            n = node_group.shortcuts.add()
            n.name = active.name
            popup_menu([f"Shorcut Elements: {len(node_group.shortcuts)}"],"Added a Shortcut","SOLO_ON")
        else:  
            n.trash()
            popup_menu([f"Shorcut Elements: {len(node_group.shortcuts)}"],"Removed a Shortcut","TRASH")

        return {"FINISHED"}


class SHORT_OT_loop(bpy.types.Operator):
    """add or remove items from list"""
    bl_idname      = "short.loop"
    bl_label       = "Loop over shortcut list"
    bl_description = "Add or remove shortcuts"

    def execute(self, context):

        node_group = context.space_data.node_tree
        
        if len(node_group.shortcuts)==0:
            return {"FINISHED"}            

        active = get_active_node()
        selection = get_selection()
        deselect_all()

        #if not already in loop then set to first 
        node_group.shortcuts_idx +=1
        if node_group.shortcuts_idx>=len(node_group.shortcuts):
            node_group.shortcuts_idx = 0
        n = node_group.nodes.get(node_group.shortcuts[node_group.shortcuts_idx].name)
        if n is not None:
            n.select = True 
            set_node_active(n)
            bpy.ops.node.view_selected()

        #Restore selection
        set_selection(selection)

        return {"FINISHED"}


class SHORT_OT_clear_shortcuts(bpy.types.Operator):
    """add or remove items from list"""
    bl_idname      = "short.clear"
    bl_label       = "Clear all shortcut list"
    bl_description = "Clear all shortcut list"

    def execute(self, context):

        node_group = context.space_data.node_tree
        node_group.shortcuts.clear()

        return {"FINISHED"}


#  dP""b8 88   88 88    dP 88     88 .dP"Y8 888888
# dP   `" 88   88 88   dP  88     88 `Ybo."   88
# Yb  "88 Y8   8P 88  dP   88  .o 88 o.`Y8b   88
#  YboodP `YbodP' 88 dP    88ood8 88 8bodP'   88


class SHORT_PT_side_panel(bpy.types.Panel):

    bl_idname      = "SHORT_PT_side_panel"
    bl_label       = "Shortcuts"
    bl_category    = "View"
    bl_space_type  = "NODE_EDITOR"
    bl_region_type = "UI"

    def draw(self, context):
        layout = self.layout
        active = get_active_node()
        node_group = context.space_data.node_tree
        layout.template_list("SHORT_UL_shortcuts_listgui", "", node_group, "shortcuts", node_group, "shortcuts_idx")

        if active is not None: 
            if active.name in node_group.shortcuts:
                  layout.operator("short.add_remove_shortcuts", text="Remove Shortcut", icon="TRASH")
            else: layout.operator("short.add_remove_shortcuts", text="Add Shortcut", icon="SOLO_ON")

        layout.operator("short.loop", text="Loop", icon="TRACKING_FORWARDS_SINGLE")

        layout.operator(SHORT_OT_clear_shortcuts.bl_idname, text="Clear List", icon="TRASH")

        return None 


class SHORT_shortcuts(bpy.types.PropertyGroup):
    name : bpy.props.StringProperty()

    def trash(self):
        node_group = self.id_data
        for i,n in enumerate(node_group.shortcuts):
            if n == self:
                node_group.shortcuts.remove(i)
                break
        return None 


class SHORT_UL_shortcuts_listgui(bpy.types.UIList):
    """selection area"""

    def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
        node_group = context.space_data.node_tree
        #layout.prop_search(item, "name", node_group, "nodes", icon="NODE", text="")
        if item.name in node_group.nodes:
            active = get_active_node()
            if active is not None and active.name==item.name: 
                  layout.prop(item, "name", text="", emboss=False, icon="SOLO_ON")
            else: layout.prop(item, "name", text="", emboss=False, icon="SOLO_OFF") 
        else: 
            row= layout.row()
            row.active = False 
            row.prop(item, "name", text="", emboss=False, icon="SOLO_OFF") 
        return None 


# 88""Yb 888888  dP""b8 88 .dP"Y8 888888 888888 88""Yb
# 88__dP 88__   dP   `" 88 `Ybo."   88   88__   88__dP
# 88"Yb  88""   Yb  "88 88 o.`Y8b   88   88""   88"Yb
# 88  Yb 888888  YboodP 88 8bodP'   88   888888 88  Yb


classes = [

    SHORT_shortcuts,
    SHORT_UL_shortcuts_listgui,
    SHORT_PT_side_panel,
    SHORT_OT_add_remove_shortcuts,
    SHORT_OT_loop,
    SHORT_OT_clear_shortcuts,
    ]

addon_keymaps = []



def register():

    #Classes
    for cls in classes: 
        bpy.utils.register_class(cls)

    #Properties
    bpy.types.NodeTree.shortcuts = bpy.props.CollectionProperty(type=SHORT_shortcuts)
    bpy.types.NodeTree.shortcuts_idx = bpy.props.IntProperty()
    
    #Add the hotkey
    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon
    if kc: #vvv CHANGE SHORTCUT HERE vvv
        km = wm.keyconfigs.addon.keymaps.new(name='Node Editor', space_type='NODE_EDITOR')
        kmi = km.keymap_items.new(SHORT_OT_add_remove_shortcuts.bl_idname, type='F2', value='PRESS', ctrl=True) #CTRL F2 to set/unset shortcut
        addon_keymaps.append((km, kmi)) 

        km = wm.keyconfigs.addon.keymaps.new(name='Node Editor', space_type='NODE_EDITOR')
        kmi = km.keymap_items.new(SHORT_OT_loop.bl_idname, type='F2', value='PRESS') #F2 to loop over shortcuts
        addon_keymaps.append((km, kmi))

    return  

def unregister():

    #Remove the hotkey
    for km, kmi in addon_keymaps:
        km.keymap_items.remove(kmi)
    addon_keymaps.clear()

    #Properties
    del bpy.types.NodeTree.shortcuts
    del bpy.types.NodeTree.shortcuts_idx

    #Classes
    for cls in reversed(classes): 
        bpy.utils.unregister_class(cls)

    return  


23 Likes

If you comply with naming conventions you can do this:

classes = [module for name, module in locals().items() if name.startswith("SHORT")]
3 Likes

Ha! Yes, forgot we can do that
I’m usually working with much bigger multi-module addon so i took the habit of writing them directly i guess

1 Like

Wow, looks very useful :slight_smile:

2 Likes

Super Cool

1 Like

Very Very Very useful!

1 Like

Just Created a “Select Dependency” plugin:
Very useful !

update

Once my Scatter5 plugin is finally released I’ll try to regroup all my free stuff on gumroad or something. for the time being here is the script:

node_dependecy_select.py (12.8 KB)

12 Likes

love your ascii comment
also your “support” : “COMMUNITY”

Thank you a lot!! super super handy!!

Super Cool x2

Say hello to Noodler, updated the first post :slight_smile:

3 Likes

Helllooo Noodler!

Have been looking for a way to switch between different materials or other node setups without having to drag around to “reframe”, so looks like the favorites is gonna help me out with that, or maybe an idea would be an option to “remember last “framing”” of a node setup. Looking forward to taking this for a real spin soon. Thanks

1 Like

Thanks man! Very useful stuff :+1:

1 Like

this needs to be default.

Noodler is fantastic! I love it, but is there a way to remove these arrows, please? Could we have an option to have the regular (no arrow) connections?

Those arrows are normal Blender behavior. They appear on any links that aren’t directly connected to a node.

Yes, I’ve noticed, after 5 years of using Blender. This is the intended behavior. However everyone out there recording videos, and posting images: you never see those arrows, because they divide the lines, and a reroute appears. Which means it is possible to not necessarily have them displayed on reroutes, hence my question.

Can you show an example of this?

like this? - No arrows. I’ve seen 5 years of blender videos, and even I didn’t know about the arrows.

…there are 10 of those arrow in the screenshot. If you’re referring to the linked reroute nodes on the right, those don’t have arrows because each link has a node connected to it directly much like I said.

It’s likely you’ve never noticed the arrows because a lot of people don’t chain reroutes due to the curve interpolation of links making it unnecessary.