├── README.md └── node_tabber ├── nt_extras.py ├── __init__.py └── operators.py /README.md: -------------------------------------------------------------------------------- 1 | # node_tabber 2 | 3 | Node Tabber is a Houdini/Nuke style node adding tool for Blender's Node tree graphs; namely the Shader, Compositing and Texture graphs. 4 | 5 | Instead of having to press SHIFT+A and then scrolling the sub menus to fins the correct node, or even clicking on the search menu and maybe getting the correct node; you can just press the TAB button which will bring up an intelligent search list which even supports node acronyms. For example, typing SX would bring up the Seperate XYZ node. 6 | This saves a lot of time when working with large node trees. 7 | 8 | ## Installation 9 | 10 | Head over to [releases](https://github.com/jiggymoon69/node_tabber/releases) and download the zip file, but do not unzip it, just save it somewhere local.In blender preferences, go to the addon section and select "install ...", locate the downloaded zip and it will appear under the addons. Just enable it and change any preferences you wish. 11 | 12 | 13 | [![IMAGE ALT TEXT](https://user-images.githubusercontent.com/48856925/95994231-54459f00-0e30-11eb-95b2-0fbbf1770121.png)](https://www.youtube.com/watch?v=Dd6lQCX4x3U "Preview 0.1.3") 14 | 15 | -------------------------------------------------------------------------------- /node_tabber/nt_extras.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | extra_math = [[" M ADD", "Add (A) MATH"], [" M SUBTRACT", "Subtract (S) MATH"], [" M MULTIPLY", "Multiply (M) MATH"], [" M DIVIDE", "Divide (D) MATH"], 4 | [" M MULTIPLY_ADD", "Multiply Add (MA) MATH"], [" M POWER", "Power (P) MATH"], [" M LOGARITHM", "Logarithm (L) MATH"], [" M SQRT", "Square Root (SQ) MATH"], 5 | [" M INVERSE_SQRT", "Inverse Square Root (ISQ) MATH"], [" M ABSOLUTE", "Absolute (A) MATH"], [" M EXPONENT", "Exponent (E) MATH"], [" M MINIMUM", "Minimum (M) MATH"], 6 | [" M MAXIMUM", "Maximum (M) MATH"], [" M LESS_THAN", "Less Than (LT) MATH"], [" M GREATER_THAN", "Greater Than (GT) MATH"], [" M SIGN", "Sign (S) MATH"], 7 | [" M COMPARE", "Compare (C) MATH"], [" M SMOOTH_MIN", "Smooth Minimum (SM) MATH"], [" M SMOOTH_MAX", "Smooth Maximum (SM) MATH"], [" M ROUND", "Round (R) MATH"], 8 | [" M FLOOR", "Floor (F) MATH"], [" M CEIL", "Ceiling (C) MATH"], [" M TRUNC", "Truncate (T) MATH"], [" M FRACT", "Fraction (F) MATH"], 9 | [" M MODULO", "Modulo (M) MATH"], [" M WRAP", "Wrap (W) MATH"], [" M SNAP", "Snap (S) MATH"], [" M PINGPONG", "Ping-Pong (PP) MATH"], 10 | [" M SINE", "Sine (S) MATH"], [" M COSINE", "Cosine (C) MATH"], [" M TANGENT", "Tangent (T) MATH"], [" M ARCSINE", "ArcSine (AS) MATH"], 11 | [" M ARCCOSINE", "Arccosine (AC) MATH"], [" M ARCTANGENT", "Arctangent (AT) MATH"], [" M ARCTAN2", "Arctan2 (AT) MATH"], [" M SINH", "Hyperbolic Sine (HS) MATH"], 12 | [" M COSH", "Hyperbolic Cosine (HC) MATH"], [" M TANH", "Hyperbolic Tangent (HT) MATH"], [" M RADIANS", "To Radians (TR) MATH"], [" M DEGREES", "To Degrees (TD) MATH"], 13 | ] 14 | 15 | extra_vector_math = [[" VM ADD", "Add (A) VEC MATH"], [" VM SUBTRACT", "Subtract (S) VEC MATH"], [" VM MULTIPLY", "Multiply (M) VEC MATH"], [" VM DIVIDE", "Divide (D) VEC MATH"], 16 | [" VM CROSS_PRODUCT", "Cross Product (CP) VEC MATH"], [" VM PROJECT", "Project (P) VEC MATH"], [" VM REFLECT", "Reflect (R) VEC MATH"], [" VM DOT_PRODUCT", "Dot Product (DP) VEC MATH"], 17 | [" VM DISTANCE", "Distance (D) VEC MATH"], [" VM LENGTH", "Length (L) VEC MATH"], [" VM SCALE", "Scale (S) VEC MATH"], [" VM NORMALIZE", "Normalize (N) VEC MATH"], 18 | [" VM ABSOLUTE", "Absolute (A) VEC MATH"], [" VM MINIMUM", "Minimum (M) VEC MATH"], 19 | [" VM MAXIMUM", "Maximum (M) VEC MATH"], 20 | [" VM FLOOR", "Floor (F) VEC MATH"], [" VM CEIL", "Ceiling (C) VEC MATH"], [" VM FRACTION", "Fraction (F) VEC MATH"], 21 | [" VM MODULO", "Modulo (M) VEC MATH"], [" VM WRAP", "Wrap (W) VEC MATH"], [" VM SNAP", "Snap (S) VEC MATH"], 22 | [" VM SINE", "Sine (S) VEC MATH"], [" VM COSINE", "Cosine (C) VEC MATH"], [" VM TANGENT", "Tangent (T) VEC MATH"], 23 | ] 24 | 25 | extra_color = [[" C VALUE", "Value (V) COLOR"], [" C COLOR", "Color (C) COLOR"], [" C SATURATION", "Saturation (S) COLOR"], [" C HUE", "Hue (H) COLOR"], 26 | [" C DIVIDE", "Divide (D) COLOR"], [" C SUBTRACT", "Subtract (S) COLOR"], [" C DIFFERENCE", "Difference (D) COLOR"], 27 | [" C LINEAR_LIGHT", "Linear Light (LL) COLOR"], [" C SOFT_LIGHT", "Soft Light (SL) COLOR"], [" C OVERLAY", "Overlay (O) COLOR"], 28 | [" C ADD", "Add (A) COLOR"], [" C DODGE", "Dodge (D) COLOR"], [" C SCREEN", "Screen (S) COLOR"], [" C LIGHTEN", "Lighten (L) COLOR"], 29 | [" C BURN", "Burn (B) COLOR"], [" C MULTIPLY", "Multiply (M) COLOR"], [" C DARKEN", "Darken (D) COLOR"], [" C MIX", "Mix (M) COLOR"], 30 | ] 31 | 32 | -------------------------------------------------------------------------------- /node_tabber/__init__.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | 20 | bl_info = { 21 | "name": "Node Tabber", 22 | "author": "Richard Lyons ", 23 | "version": (0, 1, 4), 24 | "blender": (2, 83, 7), 25 | "description": "Allows quick smart searching of node types.", 26 | "category": "Node", 27 | } 28 | 29 | import bpy 30 | from bpy.types import ( 31 | AddonPreferences, 32 | ) 33 | from bpy.props import ( 34 | BoolProperty, 35 | IntProperty, 36 | ) 37 | from . import operators 38 | import rna_keymap_ui 39 | 40 | 41 | addon_keymaps = [] 42 | 43 | 44 | class node_tabberPreferences(AddonPreferences): 45 | # This must match the addon name, use '__package__' 46 | # when defining this in a submodule of a python package. 47 | 48 | bl_idname = __name__ 49 | 50 | tally: BoolProperty( 51 | name="Enable tally count", 52 | default=True, 53 | description="Enables Node Tabber to keep a tally of most used nodes, and populate popup accordingly.", 54 | ) 55 | tally_weight: IntProperty( 56 | name="Tally Weight", 57 | default = 35, 58 | description="Maximum number of tallies for each node selected. Affects the \"weighting\" of the order of tallied results in the node list." 59 | ) 60 | quick_place: BoolProperty( 61 | name="Enable \"Quick Place\"", 62 | default=False, 63 | description="Allows immediate placement of selected node.", 64 | ) 65 | nt_debug: BoolProperty( 66 | name="Debug Output", 67 | default=False, 68 | description="Prints Node Tabber debug to console.", 69 | ) 70 | sub_search: BoolProperty( 71 | name="Enable Sub Searching", 72 | default=True, 73 | description="Allows searching within node operations. Eg. PP could return Ping-Pong in the Math node.", 74 | ) 75 | 76 | 77 | 78 | def draw(self, context): 79 | layout = self.layout 80 | 81 | # Prefs 82 | 83 | box = layout.box() 84 | row1 = box.row() 85 | row2 = box.row() 86 | row3 = box.row() 87 | row4 = box.row() 88 | row1.prop(self, "sub_search") 89 | row1.prop(self, "quick_place") 90 | row2.prop(self, "tally") 91 | row2.operator('node.reset_tally', 92 | text = 'Reset Tally') 93 | row2.prop(self, "tally_weight") 94 | #row2.prop(self, "nt_debug") 95 | #row4.label(text="NOTE: CTRL + TAB : Performs \"Edit Group\" functionality.") 96 | 97 | 98 | # Keymaps 99 | 100 | #box = layout.box() 101 | col = box.column() 102 | col.label(text="Keymap List:",icon="KEYINGSET") 103 | 104 | wm = bpy.context.window_manager 105 | kc = wm.keyconfigs.user 106 | old_km_name = "" 107 | get_kmi_l = [] 108 | for km_add, kmi_add in addon_keymaps: 109 | for km_con in kc.keymaps: 110 | if km_add.name == km_con.name: 111 | km = km_con 112 | break 113 | 114 | for kmi_con in km.keymap_items: 115 | if kmi_add.idname == kmi_con.idname: 116 | if kmi_add.name == kmi_con.name: 117 | get_kmi_l.append((km,kmi_con)) 118 | 119 | get_kmi_l = sorted(set(get_kmi_l), key=get_kmi_l.index) 120 | 121 | for km, kmi in get_kmi_l: 122 | if not km.name == old_km_name: 123 | col.label(text=str(km.name),icon="DOT") 124 | col.context_pointer_set("keymap", km) 125 | rna_keymap_ui.draw_kmi([], kc, km, kmi, col, 0) 126 | col.separator() 127 | old_km_name = km.name 128 | 129 | 130 | 131 | 132 | 133 | def register(): 134 | operators.register() 135 | bpy.utils.register_class(node_tabberPreferences) 136 | 137 | wm = bpy.context.window_manager 138 | kc = wm.keyconfigs.addon 139 | if kc: 140 | km = wm.keyconfigs.addon.keymaps.new(name='Node Editor', space_type='NODE_EDITOR') 141 | kmi = km.keymap_items.new("node.add_tabber_search", type = 'TAB', value= 'PRESS') 142 | addon_keymaps.append((km, kmi)) 143 | kmi = km.keymap_items.new("node.group_edit", type = 'TAB', value= 'PRESS', ctrl= True) 144 | addon_keymaps.append((km, kmi)) 145 | 146 | def unregister(): 147 | for km, kmi in addon_keymaps: 148 | km.keymap_items.remove(kmi) 149 | addon_keymaps.clear() 150 | 151 | operators.unregister() 152 | bpy.utils.unregister_class(node_tabberPreferences) 153 | 154 | 155 | -------------------------------------------------------------------------------- /node_tabber/operators.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import json 3 | import os 4 | import time 5 | import nodeitems_utils 6 | from . import nt_extras 7 | from bpy.types import ( 8 | Operator, 9 | PropertyGroup, 10 | ) 11 | from bpy.props import ( 12 | BoolProperty, 13 | CollectionProperty, 14 | EnumProperty, 15 | IntProperty, 16 | StringProperty, 17 | ) 18 | 19 | 20 | def nt_debug(msg): 21 | addon = bpy.context.preferences.addons['node_tabber'] 22 | prefs = addon.preferences 23 | 24 | if prefs.nt_debug: 25 | print(str(msg)) 26 | 27 | return 28 | 29 | def take_fifth(elem): 30 | return int(elem[2]) 31 | 32 | def write_score(category, enum_items): 33 | addon = bpy.context.preferences.addons['node_tabber'] 34 | prefs = addon.preferences 35 | 36 | if (category == "S"): 37 | category = "shader.json" 38 | if (category == "C"): 39 | category = "compositor.json" 40 | if (category == "T"): 41 | category = "texture.json" 42 | 43 | 44 | path = os.path.dirname(__file__) + "/" + category 45 | if not os.path.exists(path): 46 | content = {} 47 | content[enum_items]={'tally': 1} 48 | 49 | with open(path, "w") as f: 50 | json.dump(content, f) 51 | 52 | print ("Nodetabber created :" + path) 53 | else: 54 | with open(path, "r") as f: 55 | content = json.load(f) 56 | 57 | if enum_items in content: 58 | if (content[enum_items]['tally'] < prefs.tally_weight): 59 | content[enum_items]['tally'] += 1 60 | else: 61 | content[enum_items]={'tally': 1} 62 | 63 | with open(path, "w") as f: 64 | json.dump(content, f) 65 | 66 | return 67 | 68 | 69 | class NodeTabSetting(PropertyGroup): 70 | value: StringProperty( 71 | name="Value", 72 | description="Python expression to be evaluated " 73 | "as the initial node setting", 74 | default="", 75 | ) 76 | 77 | 78 | 79 | 80 | class NODE_OT_add_tabber_search(bpy.types.Operator): 81 | '''Add a node to the active tree using node tabber''' 82 | bl_idname = "node.add_tabber_search" 83 | bl_label = "Search and Add Node" 84 | bl_options = {'REGISTER', 'UNDO'} 85 | bl_property = "node_item" 86 | 87 | _enum_item_hack = [] 88 | 89 | def node_enum_items2(self, context): 90 | for index, item in enumerate(nodeitems_utils.node_items_iter(context)): 91 | if isinstance(item, nodeitems_utils.NodeItem): 92 | print(str(index) + " : " + str(item.label)) 93 | 94 | return None 95 | 96 | # Create an enum list from node items 97 | def node_enum_items(self, context): 98 | nt_debug("DEF: node_enum_items") 99 | enum_items = NODE_OT_add_tabber_search._enum_item_hack 100 | 101 | addon = bpy.context.preferences.addons['node_tabber'] 102 | prefs = addon.preferences 103 | 104 | enum_items.clear() 105 | category = context.space_data.tree_type[0] 106 | 107 | if (category == "S"): 108 | category = "shader.json" 109 | if (category == "C"): 110 | category = "compositor.json" 111 | if (category == "T"): 112 | category = "texture.json" 113 | 114 | path = os.path.dirname(__file__) + "/" + category 115 | if not os.path.exists(path): 116 | content = {} 117 | else: 118 | with open(path, "r") as f: 119 | content = json.load(f) 120 | 121 | 122 | 123 | index_offset = 0 124 | math_index = -1 125 | vector_math_index = -1 126 | mix_rgb_index = -1 127 | 128 | for index, item in enumerate(nodeitems_utils.node_items_iter(context)): 129 | #nt_debug("DEF: node_enum_items") 130 | if isinstance(item, nodeitems_utils.NodeItem): 131 | 132 | #nt_debug(str(item.label)) 133 | short = '' 134 | tally = 0 135 | words = item.label.split() 136 | for word in words: 137 | short += word[0] 138 | match = item.label+" ("+short+")" 139 | if match in content: 140 | tally = content[match]['tally'] 141 | 142 | enum_items.append( 143 | (str(index) + " 0 0", 144 | item.label+" ("+short+")", 145 | str(tally), 146 | index, 147 | )) 148 | index_offset = index 149 | 150 | if item.label == "Math": 151 | math_index = index 152 | if item.label == "Vector Math": 153 | vector_math_index = index 154 | if item.label == "MixRGB": 155 | mix_rgb_index = index 156 | 157 | #Add sub node searching if enabled 158 | if prefs.sub_search: 159 | if math_index > -1: 160 | nt_debug("Adding math nodes") 161 | for index2, subname in enumerate(nt_extras.extra_math): 162 | tally = 0 163 | if subname[1] in content: 164 | tally = content[subname[1]]['tally'] 165 | enum_items.append( 166 | (str(math_index) + subname[0] + " " + subname[1], 167 | subname[1], 168 | str(tally), 169 | index_offset+1+index2, 170 | )) 171 | index_offset += index2 172 | 173 | if vector_math_index > -1: 174 | nt_debug("Adding vector math nodes") 175 | for index2, subname in enumerate(nt_extras.extra_vector_math): 176 | tally = 0 177 | if subname[1] in content: 178 | tally = content[subname[1]]['tally'] 179 | enum_items.append( 180 | (str(vector_math_index) + subname[0] + " " + subname[1], 181 | subname[1], 182 | str(tally), 183 | index_offset+1+index2, 184 | )) 185 | index_offset += index2 186 | 187 | if mix_rgb_index > -1: 188 | nt_debug("Adding mix rgb nodes") 189 | for index2, subname in enumerate(nt_extras.extra_color): 190 | tally = 0 191 | if subname[1] in content: 192 | tally = content[subname[1]]['tally'] 193 | enum_items.append( 194 | (str(mix_rgb_index) + subname[0] + " " + subname[1], 195 | subname[1], 196 | str(tally), 197 | index_offset+1+index2, 198 | )) 199 | index_offset += index2 200 | 201 | 202 | 203 | 204 | if prefs.tally: 205 | tmp = enum_items 206 | tmp = sorted(enum_items, key=take_fifth, reverse=True) 207 | # print("\n\n" + str(tmp) + "\n\n") 208 | else: 209 | tmp = enum_items 210 | return tmp 211 | #return enum_items 212 | 213 | 214 | # Look up the item based on index 215 | def find_node_item(self, context): 216 | nt_debug("DEF: find_node_item") 217 | tmp = int(self.node_item.split()[0]) 218 | nt_debug("FIND_NODE_ITEM: Tmp : " + str(self.node_item.split())) 219 | 220 | node_item = tmp 221 | extra = [self.node_item.split()[1], self.node_item.split()[2]] 222 | #nt_debug ("First extra :" + str(extra)) 223 | #nt_debug ("Third ? :" + str(self.node_item.split()[3:])) 224 | nice_name = ' '.join(self.node_item.split()[3:]) 225 | 226 | 227 | for index, item in enumerate(nodeitems_utils.node_items_iter(context)): 228 | #nt_debug("DEF: find_node_item") 229 | if index == node_item: 230 | return [item, extra, nice_name] 231 | return None 232 | 233 | 234 | 235 | def execute(self, context): 236 | nt_debug("DEF: execute") 237 | startTime = time.perf_counter() 238 | addon = bpy.context.preferences.addons['node_tabber'] 239 | prefs = addon.preferences 240 | 241 | item = self.find_node_item(context)[0] 242 | extra = self.find_node_item(context)[1] 243 | nice_name = self.find_node_item(context)[2] 244 | #Add to tally 245 | #write_score(item.nodetype[0], self._enum_item_hack[int(self.node_item)][1]) 246 | short = '' 247 | words = item.label.split() 248 | nt_debug("EXECUTE: Item label : " + str(item.label)) 249 | for word in words: 250 | short += word[0] 251 | match = item.label+" ("+short+")" 252 | 253 | type = self.node_item.split()[1] 254 | nt_debug("Checking type : " + str(type)) 255 | 256 | if (type == "0"): 257 | nt_debug ("Writing normal node tally") 258 | write_score(item.nodetype[0], match) 259 | else: 260 | nt_debug ("Writing sub node tally") 261 | write_score(item.nodetype[0], nice_name) 262 | 263 | 264 | # print ("Hack0 : " + str(self._enum_item_hack)[]) 265 | nt_debug ("Hack") 266 | #print (self.node_item) 267 | #print (self._enum_item_hack[int(self.node_item[0]) -0][1]) 268 | #nt_debug(item.label) 269 | 270 | # no need to keep 271 | self._enum_item_hack.clear() 272 | 273 | if item: 274 | # apply settings from the node item 275 | for setting in item.settings.items(): 276 | ops = self.settings.add() 277 | ops.name = setting[0] 278 | ops.value = setting[1] 279 | 280 | self.create_node(context, item.nodetype) 281 | #print("Added node in node tabber") 282 | 283 | nt_debug(str(item.nodetype)) 284 | #print(str(item.nodename)) 285 | nt_debug("extra 0: " + str(extra[0])) 286 | nt_debug("extra 1: " + str(extra[1])) 287 | 288 | 289 | space = context.space_data 290 | node_tree = space.node_tree 291 | node_active = context.active_node 292 | node_selected = context.selected_nodes 293 | 294 | if (extra[0] == "M"): 295 | node_active.operation = extra[1] 296 | 297 | if (extra[0] == "VM"): 298 | node_active.operation = extra[1] 299 | 300 | if (extra[0] == "C"): 301 | node_active.blend_type = extra[1] 302 | 303 | if not prefs.quick_place: 304 | bpy.ops.node.translate_attach_remove_on_cancel('INVOKE_DEFAULT') 305 | 306 | nt_debug("Time taken: " + str(time.perf_counter() - startTime)) 307 | return {'FINISHED'} 308 | else: 309 | return {'CANCELLED'} 310 | 311 | def create_node(self, context, node_type=None): 312 | nt_debug("DEF: create_node") 313 | space = context.space_data 314 | tree = space.edit_tree 315 | 316 | if node_type is None: 317 | node_type = self.type 318 | 319 | #print("Node Type: " + str(node_type)) 320 | # select only the new node 321 | for n in tree.nodes: 322 | n.select = False 323 | 324 | node = tree.nodes.new(type=node_type) 325 | 326 | node.select = True 327 | tree.nodes.active = node 328 | node.location = space.cursor_location 329 | return node 330 | 331 | def invoke(self, context, event): 332 | #self.store_mouse_cursor(context, event) 333 | # Delayed execution in the search popup 334 | context.window_manager.invoke_search_popup(self) 335 | return {'CANCELLED'} 336 | 337 | populate = node_enum_items 338 | 339 | node_item: EnumProperty( 340 | name="Node Type", 341 | description="Node type", 342 | items=populate, 343 | ) 344 | 345 | 346 | class NODE_OT_reset_tally(bpy.types.Operator): 347 | '''Reset the tally count''' 348 | bl_idname = "node.reset_tally" 349 | bl_label = "Reset node tally count" 350 | 351 | def execute(self, context): 352 | categories = ["shader.json", "compositor.json", "texture.json"] 353 | reset = False 354 | for cat in categories: 355 | path = os.path.dirname(__file__) + "/" + cat 356 | if os.path.exists(path): 357 | reset = True 358 | # delete file 359 | os.remove(path) 360 | 361 | if reset: 362 | info = ("Reset Tallies") 363 | self.report({'INFO'}, info) 364 | else: 365 | info = ("No tallies to reset.") 366 | self.report({'INFO'}, info) 367 | 368 | 369 | return {'FINISHED'} 370 | 371 | 372 | #addon_keymaps = [] 373 | 374 | def register(): 375 | bpy.utils.register_class(NodeTabSetting) 376 | bpy.utils.register_class(NODE_OT_add_tabber_search) 377 | bpy.utils.register_class(NODE_OT_reset_tally) 378 | 379 | def unregister(): 380 | bpy.utils.unregister_class(NodeTabSetting) 381 | bpy.utils.unregister_class(NODE_OT_add_tabber_search) 382 | bpy.utils.unregister_class(NODE_OT_reset_tally) 383 | 384 | 385 | --------------------------------------------------------------------------------