├── resources ├── W_TIME_0.png ├── W_TIME_1.png ├── W_TIME_2.png ├── W_TIME_3.png ├── W_TIME_4.png ├── W_TIME_5.png ├── W_TIME_6.png ├── W_TIME_7.png ├── __init__.py └── NexDemo.py ├── .gitignore ├── customnodes ├── interpolation │ ├── interpolation_nodegroups.blend │ ├── interpolation_nodegroups.blend1 │ ├── interpolation_nodegroups.blend.clean │ ├── spline2dmonotonic.py │ ├── spline2dmix.py │ ├── spline2dextend.py │ ├── interpolationloop.py │ ├── spline2dinput.py │ ├── spline2dsubd.py │ ├── interpolationinput.py │ └── interpolationremap.py ├── evaluator │ └── __init__.py ├── README.txt ├── sockets │ └── custom_sockets.py ├── isrenderedview.py ├── sceneinfo.py ├── renderinfo.py ├── __init__.py ├── camerainfo.py └── colorpalette.py ├── nex ├── ABOUT.txt └── pytonode.py ├── blender_manifest.toml ├── properties ├── __init__.py ├── addon_sett.py ├── windows_sett.py └── scene_sett.py ├── README.md ├── operators ├── codetemplates.py ├── bake.py ├── __init__.py ├── search.py ├── purge.py ├── drawframes.py └── chamfer.py ├── utils ├── draw_utils.py ├── nbr_utils.py ├── fct_utils.py └── str_utils.py ├── ui ├── __init__.py └── menus.py ├── gpudraw └── __init__.py ├── handlers └── __init__.py └── __init__.py /resources/W_TIME_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DB3D/Node-Booster/HEAD/resources/W_TIME_0.png -------------------------------------------------------------------------------- /resources/W_TIME_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DB3D/Node-Booster/HEAD/resources/W_TIME_1.png -------------------------------------------------------------------------------- /resources/W_TIME_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DB3D/Node-Booster/HEAD/resources/W_TIME_2.png -------------------------------------------------------------------------------- /resources/W_TIME_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DB3D/Node-Booster/HEAD/resources/W_TIME_3.png -------------------------------------------------------------------------------- /resources/W_TIME_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DB3D/Node-Booster/HEAD/resources/W_TIME_4.png -------------------------------------------------------------------------------- /resources/W_TIME_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DB3D/Node-Booster/HEAD/resources/W_TIME_5.png -------------------------------------------------------------------------------- /resources/W_TIME_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DB3D/Node-Booster/HEAD/resources/W_TIME_6.png -------------------------------------------------------------------------------- /resources/W_TIME_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DB3D/Node-Booster/HEAD/resources/W_TIME_7.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __pycache__ 3 | .vs 4 | *.ffs_db 5 | sync.ffs_lock 6 | .tm_properties 7 | .vscode/settings.json 8 | -------------------------------------------------------------------------------- /customnodes/interpolation/interpolation_nodegroups.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DB3D/Node-Booster/HEAD/customnodes/interpolation/interpolation_nodegroups.blend -------------------------------------------------------------------------------- /customnodes/interpolation/interpolation_nodegroups.blend1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DB3D/Node-Booster/HEAD/customnodes/interpolation/interpolation_nodegroups.blend1 -------------------------------------------------------------------------------- /customnodes/interpolation/interpolation_nodegroups.blend.clean: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DB3D/Node-Booster/HEAD/customnodes/interpolation/interpolation_nodegroups.blend.clean -------------------------------------------------------------------------------- /nex/ABOUT.txt: -------------------------------------------------------------------------------- 1 | 2 | In this folder you'll find: 3 | - `nodesetter.py` 4 | A function system to easily set up a nodetree from function execution, passing and returning sockets. 5 | - `nextypes.py` 6 | A module containing our socket classes for the python nex-script node. When the user create an input or 7 | output socket using `myvar:infloat` it init a NexType. 8 | - `pytonode.py` 9 | A utility type-conversion module for converting python values to socket-types. -------------------------------------------------------------------------------- /blender_manifest.toml: -------------------------------------------------------------------------------- 1 | schema_version = "2.0.0" 2 | id = "nodebooster" 3 | name = "Node-Booster" 4 | version = "2.0.0" 5 | tagline = "NodeBooster 2.0. Extra Nodes and Functionalities for NodeEditors" 6 | maintainer = "bd3d" 7 | type = "add-on" 8 | tags = ["Geometry Nodes","Node"] 9 | blender_version_min = "4.2.0" 10 | license = ["SPDX:GPL-2.0-or-later"] 11 | website = "https://blenderartists.org/t/node-booster-extending-blender-node-editors" 12 | copyright = ["2025 BD3D DIGITAL DESIGN"] -------------------------------------------------------------------------------- /properties/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | 8 | from .addon_sett import NODEBOOSTER_AddonPref 9 | from .scene_sett import NODEBOOSTER_PR_scene, NODEBOOSTER_PR_scene_favorites_data 10 | from .windows_sett import NODEBOOSTER_PR_Window 11 | 12 | 13 | classes = ( 14 | 15 | NODEBOOSTER_AddonPref, 16 | NODEBOOSTER_PR_scene_favorites_data, 17 | NODEBOOSTER_PR_scene, 18 | NODEBOOSTER_PR_Window, 19 | 20 | ) 21 | 22 | 23 | def load_properties(): 24 | 25 | bpy.types.Scene.nodebooster = bpy.props.PointerProperty(type=NODEBOOSTER_PR_scene) 26 | bpy.types.WindowManager.nodebooster = bpy.props.PointerProperty(type=NODEBOOSTER_PR_Window) 27 | 28 | return None 29 | 30 | def unload_properties(): 31 | 32 | del bpy.types.Scene.nodebooster 33 | del bpy.types.WindowManager.nodebooster 34 | 35 | return None -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![mainvignette](https://github.com/user-attachments/assets/f60b03bd-0c16-480f-a47a-7f1bc1d9972d) 2 | Enjoy a free collection of new nodes, and functionalities for blender NodeEditors! 3 | 4 | # Support and Documentation 5 | Read the documentation, or engage with other users on this [blender artist topic](https://blenderartists.org/t/node-booster-extending-blender-node-editors) 6 | 7 | # Issues 8 | Report only bugs on the 'Issues' github page, no support or general questions are allowed! 9 | gGo on the forum thread to ask questions, thank you! 10 | 11 | # Versions 12 | All versions: 13 | https://github.com/DB3D/NodeBooster/releases 14 | 15 | Current released version is V1. 16 | V2 coming soon, with a lot of changes.. 17 | You can test out the WIP, being the last commit. 18 | 19 | # Changes 20 | Stick to your version from project end to finish. 21 | It is **not** advised to update blender or plugins versions midproject! 22 | Always keep track of which version of blender/addons you used for a project. 23 | -------------------------------------------------------------------------------- /operators/codetemplates.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | 8 | import os 9 | 10 | 11 | class NODEBOOSTER_OT_text_templates(bpy.types.Operator): 12 | 13 | bl_idname = "nodebooster.import_template" 14 | bl_label = "Code Example" 15 | bl_description = "Import example code" 16 | 17 | filepath : bpy.props.StringProperty(subtype="FILE_PATH") 18 | 19 | def execute(self, context): 20 | 21 | if not os.path.isfile(self.filepath): 22 | self.report({'ERROR'}, f"File not found: {self.filepath}") 23 | return {'CANCELLED'} 24 | 25 | dataname = os.path.basename(self.filepath) 26 | 27 | with open(self.filepath, "r", encoding="utf-8") as file: 28 | file_content = file.read() 29 | 30 | text_block = bpy.data.texts.new(name=dataname) 31 | text_block.write(file_content) 32 | context.space_data.text = text_block 33 | 34 | self.report({'INFO'}, f"Imported '{dataname}' into Blender text editor") 35 | 36 | return {'FINISHED'} -------------------------------------------------------------------------------- /utils/draw_utils.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | 8 | 9 | def get_dpifac(): 10 | """get user dpi""" 11 | prefs = bpy.context.preferences.system 12 | dpi = prefs.dpi 13 | #dpi *= prefs.pixel_size 14 | return dpi / 72 15 | 16 | 17 | def ensure_mouse_cursor(context, event): 18 | """function needed to get cursor location""" 19 | 20 | space = context.space_data 21 | tree = space.edit_tree 22 | 23 | # convert mouse position to the View2D 24 | if (context.region.type == 'WINDOW'): 25 | space.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y) 26 | else: space.cursor_location = tree.view_center 27 | 28 | return None 29 | 30 | 31 | def popup_menu(msgs, title, icon): 32 | """pop a menu message for the user""" 33 | 34 | def draw(self, context): 35 | layout = self.layout 36 | for msg in msgs: 37 | layout.label(text=msg) 38 | return 39 | 40 | bpy.context.window_manager.popup_menu(draw, title=title, icon=icon) 41 | return None 42 | -------------------------------------------------------------------------------- /utils/nbr_utils.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | from mathutils import Vector 7 | import numpy as np 8 | 9 | 10 | def map_range(value, from_min, from_max, to_min, to_max,): 11 | if (from_max - from_min == 0): 12 | return to_min if (value <= from_min) else to_max 13 | result = (value - from_min) / (from_max - from_min) * (to_max - to_min) + to_min 14 | return result 15 | 16 | 17 | def map_positions(old_positions:np.array, old_bounds:tuple[Vector, Vector], new_bounds:tuple[Vector, Vector],) -> np.array: 18 | """map positions from old bounds to new bounds""" 19 | 20 | # Extract bounds 21 | old_min, old_max = old_bounds 22 | new_min, new_max = new_bounds 23 | 24 | # Calculate the range of the old and new bounds 25 | old_range = np.array([old_max.x - old_min.x, old_max.y - old_min.y]) 26 | new_range = np.array([new_max.x - new_min.x, new_max.y - new_min.y]) 27 | 28 | # Normalize positions to 0-1 range 29 | normalized = (old_positions - np.array([old_min.x, old_min.y])) / old_range 30 | 31 | # Scale to new range and offset to new minimum 32 | new_positions = normalized * new_range + np.array([new_min.x, new_min.y]) 33 | 34 | return new_positions 35 | -------------------------------------------------------------------------------- /ui/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | import bpy 6 | 7 | from .menus import ( 8 | #NOTE: menu is doing some procedural submenu cls registering 9 | NODEBOOSTER_MT_textemplate, 10 | ) 11 | 12 | from .panels import ( 13 | NODEBOOSTER_PT_tool_search, 14 | NODEBOOSTER_PT_tool_color_palette, 15 | NODEBOOSTER_PT_tool_frame, 16 | NODEBOOSTER_PT_minimap, 17 | NODEBOOSTER_PT_shortcuts_memo, 18 | NODEBOOSTER_PT_active_node, 19 | ) 20 | 21 | classes = ( 22 | NODEBOOSTER_MT_textemplate, 23 | NODEBOOSTER_PT_tool_search, 24 | NODEBOOSTER_PT_tool_color_palette, 25 | NODEBOOSTER_PT_shortcuts_memo, 26 | NODEBOOSTER_PT_tool_frame, 27 | NODEBOOSTER_PT_minimap, 28 | NODEBOOSTER_PT_active_node, 29 | ) 30 | 31 | from .menus import append_menus, remove_menus 32 | from ..operators.favorites import favorite_popover_draw_header 33 | 34 | def load_ui(): 35 | 36 | #add the menus to the nodes shift a menu 37 | append_menus() 38 | 39 | #add the favorite popover 40 | bpy.types.NODE_HT_header.append(favorite_popover_draw_header) 41 | 42 | return None 43 | 44 | def unload_ui(): 45 | 46 | #remove the menus from the nodes shift a menu 47 | remove_menus() 48 | 49 | #remove the favorite popover 50 | bpy.types.NODE_HT_header.remove(favorite_popover_draw_header) 51 | 52 | return None 53 | -------------------------------------------------------------------------------- /properties/addon_sett.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | 8 | 9 | class NODEBOOSTER_AddonPref(bpy.types.AddonPreferences): 10 | 11 | from .. import __package__ as base_package 12 | bl_idname = base_package 13 | 14 | debug : bpy.props.BoolProperty( 15 | name="Debug Mode", 16 | default=False, 17 | ) 18 | debug_depsgraph : bpy.props.BoolProperty( 19 | name="Depsgraph Debug", 20 | default=False, 21 | ) 22 | #not exposed 23 | ui_word_wrap_max_char_factor : bpy.props.FloatProperty( 24 | default=1.0, 25 | soft_min=0.3, 26 | soft_max=3, 27 | description="ui 'word_wrap' layout funciton, max characters per lines", 28 | ) 29 | ui_word_wrap_y : bpy.props.FloatProperty( 30 | default=0.8, 31 | soft_min=0.1, 32 | soft_max=3, 33 | description="ui 'word_wrap' layout funciton, max height of the lines", 34 | ) 35 | 36 | #minimap 37 | auto_launch_minimap_navigation : bpy.props.BoolProperty( 38 | default=True, 39 | name="Auto Enable", 40 | description="Automatically launch the minimap navigation modal when loading the addon and loading new .blend files.", 41 | ) 42 | 43 | #interpolation demo 44 | experimental_mode : bpy.props.BoolProperty( 45 | default=False, 46 | name="Expermimental Mode", 47 | description="Enable experimental features.", 48 | ) 49 | 50 | def draw(self,context): 51 | 52 | layout = self.layout 53 | 54 | layout.prop(self,"experimental_mode",) 55 | 56 | layout.separator(type='LINE') 57 | 58 | layout.prop(self,"debug",) 59 | layout.prop(self,"debug_depsgraph",) 60 | 61 | return None 62 | -------------------------------------------------------------------------------- /properties/windows_sett.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | 8 | 9 | class NODEBOOSTER_PR_Window(bpy.types.PropertyGroup): 10 | """sett_win = bpy.context.window_manager.nodebooster""" 11 | #Properties in there will always be temporary and reset to their default values on each blender startup 12 | 13 | #for python nodes 14 | authorize_automatic_execution : bpy.props.BoolProperty( 15 | default=False, 16 | name="Allow Automatic Executions", 17 | description="Automatically running a foreign python script is dangerous. Do you know the content of this .blend file? If not, do you trust the authors? When this button is enabled python expressions or scripts from the nodebooster plugin will never execute automatically, you will need to engage with the node properties to trigger an execution", 18 | ) 19 | 20 | #for minimap modal navitation 21 | 22 | def launch_minimap_modal_operator(self, context): 23 | """lauch a modal navigation operator on each window""" 24 | 25 | if (self.minimap_modal_operator_is_active): 26 | wm = context.window_manager 27 | for win in wm.windows: 28 | if ((win) and (win.screen) and (not win.screen.is_temporary) and (context.temp_override)): 29 | with context.temp_override(window=win, screen=win.screen): 30 | bpy.ops.nodebooster.minimap_interaction('INVOKE_DEFAULT',) #modal are tied per windows. 31 | return None 32 | 33 | minimap_modal_operator_is_active : bpy.props.BoolProperty( 34 | default=False, 35 | name="Modal Navigations", 36 | description="When enabled, the minimap modal operator will be active. This will allow you to move the minimap around the node editor area with your mouse when hovering over the minimap.", 37 | update=launch_minimap_modal_operator, 38 | ) 39 | 40 | -------------------------------------------------------------------------------- /resources/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | import bpy.utils.previews 8 | 9 | import os 10 | 11 | 12 | PREVIEWS_ICONS = {} #Our custom "W_" Icons are stored here 13 | CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) 14 | 15 | 16 | def get_previews_from_directory(directory, extension=".png", previews=None,): 17 | """install previews with bpy.utils.preview, will try to search for all image file inside given directory""" 18 | 19 | if (previews is None): 20 | previews = bpy.utils.previews.new() 21 | 22 | for f in os.listdir(directory): 23 | if (f.endswith(extension)): 24 | 25 | icon_name = f[:-len(extension)] 26 | path = os.path.abspath(os.path.join(directory, f)) 27 | 28 | if (not os.path.isfile(path)): 29 | print(f"ERROR: get_previews_from_directory(): File not found: {path}") # Debugging aid 30 | continue 31 | try: 32 | previews.load(icon_name, path, "IMAGE") 33 | except Exception as e: 34 | print(f"ERROR: get_previews_from_directory(): loading icon {icon_name} from '{path}':\n{e}") 35 | continue 36 | 37 | continue 38 | 39 | return previews 40 | 41 | def remove_previews(previews): 42 | """remove previews wuth bpy.utils.preview""" 43 | 44 | bpy.utils.previews.remove(previews) 45 | previews.clear() 46 | 47 | return None 48 | 49 | 50 | def cust_icon(str_value): 51 | 52 | if (str_value.startswith("W_")): 53 | global PREVIEWS_ICONS 54 | if (str_value in PREVIEWS_ICONS): 55 | return PREVIEWS_ICONS[str_value].icon_id 56 | return 1 57 | 58 | return 0 59 | 60 | 61 | def load_icons(): 62 | 63 | global PREVIEWS_ICONS 64 | PREVIEWS_ICONS = get_previews_from_directory(CURRENT_DIR, extension=".png",) 65 | 66 | return None 67 | 68 | def unload_icons(): 69 | 70 | global PREVIEWS_ICONS 71 | remove_previews(PREVIEWS_ICONS) 72 | 73 | return None -------------------------------------------------------------------------------- /operators/bake.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | 8 | from ..utils.node_utils import replace_node_by_ng 9 | 10 | 11 | class NODEBOOSTER_OT_bake_customnode(bpy.types.Operator): 12 | """Replace the custom node with a nodegroup, preserve values and links""" 13 | 14 | bl_idname = "extranode.bake_customnode" 15 | bl_label = "Bake Custom Node" 16 | bl_options = {'REGISTER', 'UNDO'} 17 | 18 | nodegroup_name: bpy.props.StringProperty() 19 | node_name: bpy.props.StringProperty() 20 | 21 | @classmethod 22 | def poll(cls, context): 23 | return (context.space_data.type=='NODE_EDITOR') and (context.space_data.node_tree is not None) 24 | 25 | def execute(self, context): 26 | 27 | space = context.space_data 28 | node_tree = space.edit_tree 29 | 30 | old_node = node_tree.nodes.get(self.node_name) 31 | if (old_node is None): 32 | self.report({'ERROR'}, "Node with given name not found") 33 | return {'CANCELLED'} 34 | 35 | node_group = bpy.data.node_groups.get(self.nodegroup_name) 36 | if (node_group is None): 37 | self.report({'ERROR'}, "Node group with given name not found") 38 | return {'CANCELLED'} 39 | 40 | name = None 41 | if hasattr(old_node,"user_mathexp"): 42 | name = str(old_node.user_mathexp) 43 | if hasattr(old_node,"user_textdata"): 44 | if (old_node.user_textdata): 45 | name = old_node.user_textdata.name 46 | 47 | node_group = node_group.copy() 48 | node_group.name = f'{node_group.name}.Baked' 49 | 50 | new_node = replace_node_by_ng(node_tree, old_node, node_group,) 51 | if (new_node is None): 52 | self.report({'ERROR'}, "Current NodeTree type not supported") 53 | return {'FINISHED'} 54 | 55 | if (name): 56 | new_node.label = name 57 | 58 | self.report({'INFO'}, f"Replaced node '{self.node_name}' with node group '{self.node_name}'") 59 | 60 | return {'FINISHED'} 61 | -------------------------------------------------------------------------------- /utils/fct_utils.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import types 7 | import typing 8 | from collections import namedtuple 9 | 10 | 11 | ColorRGBA = namedtuple('ColorRGBA', ['r','g','b','a']) 12 | 13 | def anytype(*args, types:tuple=None) -> bool: 14 | """Returns True if any argument in *args is an instance of any type in the 'types' tuple.""" 15 | return any(isinstance(arg, types) for arg in args) 16 | 17 | 18 | def alltypes(*args, types: tuple = None) -> bool: 19 | """Returns True if all arguments in *args are instances of any type in the 'types' tuple.""" 20 | return all(isinstance(arg, types) for arg in args) 21 | 22 | 23 | def functioncopy(fct, new_name=''): 24 | """Creates a new function with exactly the same behavior, arguments, defaults, closure, and attributes but with a new __name__.""" 25 | assert new_name!='', "Please define a 'new_name' parameter for functioncopy()" 26 | new = types.FunctionType(fct.__code__, fct.__globals__, name=new_name, argdefs=fct.__defaults__, closure=fct.__closure__,) 27 | new.__dict__.update(fct.__dict__) 28 | new.__annotations__ = fct.__annotations__ 29 | return new 30 | 31 | 32 | def is_annotation_compliant(value, annotated_type) -> bool: 33 | """Recursively check if 'value' is an instance of 'annotated_type', 34 | which may be a simple type, a PEP 604 union (X|Y|Z), or a typing.Union.""" 35 | 36 | # If there's no 'origin', it's a normal type (e.g. int, float, etc.) 37 | origin = typing.get_origin(annotated_type) 38 | if (origin is None): 39 | # A direct type, so just do an isinstance check 40 | return isinstance(value, annotated_type) 41 | 42 | # If it's a union (PEP 604 or typing.Union), check each sub‐type 43 | if (origin is typing.Union): 44 | sub_types = typing.get_args(annotated_type) 45 | # Return True if *any* of the sub_types matches 46 | return any(is_annotation_compliant(value, st) for st in sub_types) 47 | 48 | # Otherwise (e.g. a generic like List[str]), fallback to a direct check 49 | # or you could expand this if you need deeper generics logic 50 | return isinstance(value, annotated_type) 51 | -------------------------------------------------------------------------------- /resources/NexDemo.py: -------------------------------------------------------------------------------- 1 | 2 | import bpy 3 | 4 | # Initialization 5 | # first we need to define our socket variables! 6 | myFloatA:infloat 7 | myFloatB:infloat = 0.123 #you can also assign a default value! 8 | myV:invec 9 | myBoo:inbool = True 10 | sockMatrix:inmat 11 | 12 | # Do math between types using '+' '-' '*' '/' '%' '**' '//' symbols, 13 | # or functions. (see the full list in 'NodeBooster > Glossary' panel) 14 | c = (myFloatA + myFloatB)/2 15 | c = nroot(sin(c),cos(123)) // myFloatB 16 | 17 | # Do comparisons between types using '>' '<' '!=' '==' '<=' '>=' 18 | # result will be a socket bool 19 | isequal = myFloatA == myFloatB 20 | islarger = myV > myBoo 21 | # Do bitwise operation with symbols '&', or '|' on socket bool 22 | bothtrue = isequal & islarger 23 | 24 | # You can evaluate any python types you wish to, and do operations between socket and python types 25 | frame = bpy.context.scene.frame_current 26 | ActiveLoc = bpy.context.object.location 27 | c += abs(frame) 28 | 29 | # Easily access Vector or Matrix componements. 30 | c += (myV.x ** myV.length) + sockMatrix.translation.z 31 | newvec = combixyz(c, frame, ActiveLoc.x) 32 | 33 | # Do Advanced Matrix and Vector operation 34 | ActiveMat = bpy.context.object.matrix_world 35 | pytuple = (1,2,3) 36 | TransVec = (sockMatrix.inverted() @ ActiveMat) @ newvec.normalized() 37 | TransVec = sockMatrix @ cross(pytuple,TransVec,) 38 | TransVec = sqrt(TransVec) #math operations can also work entry-wise on vectors. 39 | minElement = min(sepamatrix(sockMatrix)) #get lowest socketfloat element of Matrix. 40 | 41 | # types can be itterable 42 | newvalues = [] 43 | for i,component in enumerate(TransVec): 44 | newval = component + minElement + i 45 | newvalues.append(newval) 46 | TransVec[:] = newvalues 47 | 48 | # Because we are using python you can create functions you can reuse too 49 | def entrywise_sinus_on_matrix_elements(M): 50 | new = [] 51 | for f in sepamatrix(M): 52 | new.append(sin(f)) 53 | return combimatrix(new) 54 | 55 | newMat = entrywise_sinus_on_matrix_elements(sockMatrix) 56 | 57 | # Then we assign the socket to an output 58 | # you can define a strict output type 59 | # or auutomatically define the output scoket with 'outauto' 60 | BoolOut:outbool = bothtrue 61 | TransVec:outvec = TransVec 62 | myMatrix:outauto = newMat -------------------------------------------------------------------------------- /operators/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | 8 | from .drawroute import NODEBOOSTER_OT_draw_route 9 | from .bake import NODEBOOSTER_OT_bake_customnode 10 | from .purge import NODEBOOSTER_OT_node_purge_unused 11 | from .favorites import ( 12 | NODEBOOSTER_OT_favorite_add, 13 | NODEBOOSTER_OT_favorite_teleport, 14 | NODEBOOSTER_OT_favorite_remove, 15 | NODEBOOSTER_PT_favorites_popover, 16 | ) 17 | from .drawframes import NODEBOOSTER_OT_draw_frame 18 | from .chamfer import NODEBOOSTER_OT_chamfer 19 | from .palette import ( 20 | NODEBOOSTER_OT_palette_msg_bus_actions, 21 | NODEBOOSTER_OT_palette_reset_color, 22 | NODEBOOSTER_OT_initalize_palette, 23 | ) 24 | from .codetemplates import NODEBOOSTER_OT_text_templates 25 | from ..gpudraw.minimap import NODEBOOSTER_OT_MinimapInteraction 26 | 27 | classes = ( 28 | NODEBOOSTER_OT_draw_route, 29 | NODEBOOSTER_OT_bake_customnode, 30 | NODEBOOSTER_OT_node_purge_unused, 31 | NODEBOOSTER_OT_favorite_add, 32 | NODEBOOSTER_OT_favorite_teleport, 33 | NODEBOOSTER_OT_favorite_remove, 34 | NODEBOOSTER_PT_favorites_popover, 35 | NODEBOOSTER_OT_draw_frame, 36 | NODEBOOSTER_OT_chamfer, 37 | NODEBOOSTER_OT_palette_msg_bus_actions, 38 | NODEBOOSTER_OT_palette_reset_color, 39 | NODEBOOSTER_OT_initalize_palette, 40 | NODEBOOSTER_OT_text_templates, 41 | NODEBOOSTER_OT_MinimapInteraction, 42 | ) 43 | 44 | ADDON_KEYMAPS = [] 45 | KMI_DEFS = ( 46 | # Operator.bl_idname, Key, Action, Ctrl, Shift, Alt, props(name,value) Name, Icon, Enable 47 | ( NODEBOOSTER_OT_draw_route.bl_idname, "E", "PRESS", False, False, False, (), "Draw Route", "TRACKING", True, ), 48 | ( NODEBOOSTER_OT_favorite_add.bl_idname, "Y", "PRESS", True, False, False, (), "Add Favorite", "SOLO_OFF", True, ), 49 | ( NODEBOOSTER_OT_favorite_teleport.bl_idname, "Y", "PRESS", False, False, False, (), "Loop Favorites", "SOLO_OFF", True, ), 50 | ( NODEBOOSTER_OT_draw_frame.bl_idname, "J", "PRESS", False, False, False, (), "Draw Frame", "ALIGN_TOP", True, ), 51 | ( NODEBOOSTER_OT_chamfer.bl_idname, "B", "PRESS", True, False, False, (), "Reroute Chamfer", "MOD_BEVEL", True, ), 52 | ) 53 | 54 | def load_operators_keymaps(): 55 | 56 | #TODO, ideally we need to save these keys on addonprefs somehow, it will reset per blender sessions. 57 | 58 | ADDON_KEYMAPS.clear() 59 | 60 | kc = bpy.context.window_manager.keyconfigs.addon 61 | if (not kc): 62 | return None 63 | 64 | km = kc.keymaps.new(name="Node Editor", space_type='NODE_EDITOR',) 65 | for (identifier, key, action, ctrl, shift, alt, props, name, icon, enable) in KMI_DEFS: 66 | kmi = km.keymap_items.new(identifier, key, action, ctrl=ctrl, shift=shift, alt=alt,) 67 | kmi.active = enable 68 | if (props): 69 | for prop, value in props: 70 | setattr(kmi.properties, prop, value) 71 | ADDON_KEYMAPS.append((km, kmi, name, icon)) 72 | 73 | return None 74 | 75 | def unload_operators_keymaps(): 76 | 77 | for km, kmi, _, _ in ADDON_KEYMAPS: 78 | km.keymap_items.remove(kmi) 79 | ADDON_KEYMAPS.clear() 80 | 81 | return None 82 | -------------------------------------------------------------------------------- /operators/search.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | 8 | 9 | #NOTE this functinality is implemented on an property update level 10 | 11 | #TODO Requires rework 12 | # - would be nice to add a recursive search feature in there 13 | # - perhaps better to use a search operator instead of using a prop update 14 | # - perhaps it would be nicer to confirm if user want to select (or enter a subgroup) 15 | # right now we directly select everything after search, perhaps 1) search, then display 16 | # the found match on the ui, then 2) propose operators to select, recenter view, and enter nodetrees? 17 | # - instead of simple boolean for types, we should have enum with type match.. 18 | # - add a case for matching nodetree names 19 | # - add a recursive toggle option 20 | 21 | 22 | def search_upd(self, context): 23 | """search in context nodetree for nodes""" 24 | 25 | ng = context.space_data.edit_tree 26 | 27 | keywords = self.search_keywords.lower().replace(","," ").split(" ") 28 | keywords = set(keywords) 29 | 30 | def is_matching(keywords,terms): 31 | matches = [] 32 | for k in keywords: 33 | for t in terms: 34 | matches.append(k in t) 35 | return any(matches) 36 | 37 | found = [] 38 | for n in ng.nodes: 39 | terms = [] 40 | 41 | if (self.search_labels): 42 | name = n.label.lower() 43 | if not name: 44 | name = n.bl_label.lower() 45 | terms += name.split(" ") 46 | 47 | if (self.search_types): 48 | terms += n.type.lower().split(" ") 49 | 50 | if (self.search_names): 51 | name = n.name + " " + n.bl_idname 52 | terms += name.replace("_"," ").lower().split(" ") 53 | 54 | if (self.search_socket_names): 55 | for s in [*list(n.inputs),*list(n.outputs)]: 56 | name = s.name.lower() 57 | if name not in terms: 58 | terms += name.split(" ") 59 | 60 | if (self.search_socket_types): 61 | for s in [*list(n.inputs),*list(n.outputs)]: 62 | name = s.type.lower() 63 | if name not in terms: 64 | terms += name.split(" ") 65 | 66 | if (not is_matching(keywords,terms)): 67 | continue 68 | 69 | found.append(n) 70 | 71 | continue 72 | 73 | #unselect all 74 | for n in ng.nodes: 75 | n.select = False 76 | 77 | self.search_found = len(found) 78 | if (self.search_found==0): 79 | return None 80 | 81 | if (self.search_input_only): 82 | for n in found.copy(): 83 | if (len(n.inputs)==0 and (n.type!="FRAME")): 84 | continue 85 | found.remove(n) 86 | continue 87 | 88 | if (self.search_frame_only): 89 | for n in found.copy(): 90 | if (n.type!="FRAME"): 91 | found.remove(n) 92 | continue 93 | 94 | for n in found: 95 | n.select = True 96 | 97 | if (self.search_center): 98 | with bpy.context.temp_override(area=context.area, space=context.area.spaces[0], region=context.area.regions[3]): 99 | bpy.ops.node.view_selected() 100 | 101 | return None -------------------------------------------------------------------------------- /customnodes/evaluator/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | # NOTE this module is related to the interpolation folder, experimental test of GN CustomNode and customSocketTypes.. 6 | 7 | # NOTE: 8 | # the evaluator module role is to evaluate the value of a socket upstream. 9 | # it will assume that the node class upstream possess 'node.evaluator_properties' set, 10 | # and an evaluator() function that accepts a socket as argument. 11 | # the evaluator shall return the value for the equivalent passed socket. 12 | 13 | # TODO: 14 | # - need some sort of system to see which node is dirty. 15 | # - we could store the evaluated values as a node python numpy property 16 | 17 | 18 | import bpy 19 | from ...utils.node_utils import socket_intersections 20 | 21 | 22 | def evaluate_upstream_value(sock, match_evaluator_properties:set=None, set_link_invalid:bool=False, cached_values:dict=None): 23 | """evaluate the value of a socket upstream, fallback to None if the node upstream is not compatible or not linked. 24 | -Pass a match_evaluator_properties set to check if the node upstream node is compatible. ex: {'INTERPOLATION_NODE',} 25 | -Pass a set_link_invalid to set the link invalid if the node upstream is not compatible. 26 | -Pass a cached_values dict to cache the evaluated values, in order to avoid redundand evaluations calculations. 27 | """ 28 | 29 | # if the socket is not links, there's nothing to fetch upstream.. 30 | if (not sock.links): 31 | return None 32 | 33 | #get colliding nodes upstream, on the left in {socket:links} 34 | #return a dictionary of {colliding_socket:parcoured_links[]} 35 | parcour_info = socket_intersections(sock, direction='LEFT') 36 | # print(f"DEBUG: parcour_info: {parcour_info}, len: {len(parcour_info)}") 37 | 38 | #nothing hit? 39 | if (not parcour_info): 40 | # print("DEBUG: no parcour info.") 41 | return None 42 | 43 | #get our colliding socket. when parcouring right to left, we expect only one collision. 44 | if (len(parcour_info) > 1): 45 | raise Exception(f"It should not be possible to collide with more than one socket type, when parcouring from right to left. how did you manage that?\n{parcour_info}") 46 | 47 | # Extract the first (and only) item from the dictionary 48 | colliding_socket = list(parcour_info.keys())[0] 49 | parcoured_links = parcour_info[colliding_socket] 50 | colliding_node = colliding_socket.node 51 | 52 | #we are expecting to collide with specific socket types! 53 | if (not hasattr(colliding_node,'evaluator_properties')) \ 54 | or (not hasattr(colliding_node,'evaluator')) \ 55 | or (not match_evaluator_properties.intersection(colliding_node.evaluator_properties)): 56 | 57 | # print(f"DEBUG: parcour not successful.\n{colliding_node}") 58 | if (set_link_invalid and parcoured_links): 59 | first_link = parcoured_links[0] 60 | first_link.is_valid = False 61 | return None 62 | 63 | #caching system, perhaps multiple input sockets links to the same out socket.. 64 | if (cached_values): 65 | cachekey = colliding_node.name + ':' + colliding_socket.identifier 66 | r = cached_values.get(cachekey) 67 | else: r = None 68 | 69 | if (r is None): 70 | r = colliding_node.evaluator(colliding_socket) 71 | if (cached_values): 72 | cached_values[cachekey] = r 73 | 74 | return r -------------------------------------------------------------------------------- /gpudraw/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | import bpy 6 | import gpu 7 | from gpu_extras.batch import batch_for_shader 8 | 9 | from .minimap import draw_minimap 10 | 11 | from ..utils.draw_utils import get_dpifac 12 | from ..customnodes.interpolation.spline2dpreview import draw_interpolation_preview 13 | 14 | 15 | def get_draw_function_handler(mode='OVERLAY'): 16 | """function factory for draw functions, overlay or underlay mode..""" 17 | 18 | def draw_function(): 19 | 20 | # Exit early if we don't have proper context 21 | context = bpy.context 22 | if not context: 23 | return None 24 | 25 | # check if we are in node editor and overlays are enabled 26 | space_data = context.space_data 27 | if (not space_data) or (space_data.type!='NODE_EDITOR'): 28 | return None 29 | 30 | # Get the active node tree 31 | node_tree = space_data.edit_tree if space_data.edit_tree else space_data.node_tree 32 | if (not node_tree): 33 | return None 34 | 35 | # Find the main region 36 | area = context.area 37 | window_region = None 38 | for r in area.regions: 39 | if (r.type == 'WINDOW'): 40 | window_region = r 41 | break 42 | if (not window_region): 43 | print("ERROR: draw_minimap(): Could not find 'WINDOW' region in the provided space.") 44 | return None 45 | 46 | # Get DPI factor 47 | region = context.region 48 | view2d = region.view2d 49 | dpi_fac = get_dpifac() 50 | zoom = abs((view2d.view_to_region(0, 0, clip=False)[0] - view2d.view_to_region(10, 10, clip=False)[0]) / 10) 51 | 52 | match mode: 53 | case 'OVERLAY': 54 | 55 | #draw our interpolation preview 56 | draw_interpolation_preview(node_tree, view2d, dpi_fac, zoom) 57 | 58 | #draw our minimap 59 | if (context.scene.nodebooster.minimap_draw_type == 'OVERLAY'): 60 | draw_minimap(node_tree, area, window_region, view2d, space_data, dpi_fac, zoom) 61 | 62 | case 'UNDERLAY': 63 | 64 | #draw our minimap 65 | if (context.scene.nodebooster.minimap_draw_type == 'UNDERLAY'): 66 | draw_minimap(node_tree, area, window_region, view2d, space_data, dpi_fac, zoom) 67 | 68 | return None 69 | 70 | return draw_function 71 | 72 | 73 | OVELAY_FCT, UNDERLAY_FCT = None, None 74 | 75 | def register_gpu_drawcalls(): 76 | 77 | if (bpy.app.background) or (bpy.context.window is None): 78 | return None 79 | 80 | global OVELAY_FCT 81 | if (OVELAY_FCT is None): 82 | OVELAY_FCT = bpy.types.SpaceNodeEditor.draw_handler_add( 83 | get_draw_function_handler(mode='OVERLAY'), 84 | (), 'WINDOW', 'POST_PIXEL', ) 85 | 86 | global UNDERLAY_FCT 87 | if (UNDERLAY_FCT is None): 88 | UNDERLAY_FCT = bpy.types.SpaceNodeEditor.draw_handler_add( 89 | get_draw_function_handler(mode='UNDERLAY'), 90 | (), 'WINDOW', 'BACKDROP', ) 91 | 92 | return None 93 | 94 | def unregister_gpu_drawcalls(): 95 | 96 | global OVELAY_FCT, UNDERLAY_FCT 97 | 98 | if (OVELAY_FCT is not None): 99 | bpy.types.SpaceNodeEditor.draw_handler_remove(OVELAY_FCT, 'WINDOW') 100 | OVELAY_FCT = None 101 | 102 | if (UNDERLAY_FCT is not None): 103 | bpy.types.SpaceNodeEditor.draw_handler_remove(UNDERLAY_FCT, 'WINDOW') 104 | UNDERLAY_FCT = None 105 | 106 | return None -------------------------------------------------------------------------------- /customnodes/interpolation/spline2dmonotonic.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | import os 8 | 9 | from ...__init__ import get_addon_prefs 10 | from ...utils.str_utils import word_wrap 11 | from ...utils.bezier2d_utils import ensure_monotonic_bezsegs 12 | from ..evaluator import evaluate_upstream_value 13 | from ...utils.node_utils import ( 14 | cache_booster_nodes_parent_tree, 15 | ) 16 | 17 | # ooooo ooo .o8 18 | # `888b. `8' "888 19 | # 8 `88b. 8 .ooooo. .oooo888 .ooooo. 20 | # 8 `88b. 8 d88' `88b d88' `888 d88' `88b 21 | # 8 `88b.8 888 888 888 888 888ooo888 22 | # 8 `888 888 888 888 888 888 .o 23 | # o8o `8 `Y8bod8P' `Y8bod88P" `Y8bod8P' 24 | 25 | class NODEBOOSTER_ND_EnsureMonotonicity(bpy.types.Node): 26 | 27 | bl_idname = "NodeBoosterMonotonic" 28 | bl_label = "Monotonicity" 29 | bl_description = """Ensure a 2D curve is monotonic on the X axis (no segments crossovers). This will effectively convert any 2D curve into a typical 2D curve interpreted for an interpolation.""" 30 | nb_menu_path = ['NodeBooster','Experimental','Interpolation',bl_label,] #path in add menu 31 | auto_upd_flags = {'NONE',} 32 | tree_type = "AnyNodeTree" 33 | 34 | evaluator_properties = {'INTERPOLATION_NODE',} 35 | 36 | @classmethod 37 | def poll(cls, context): 38 | """mandatory poll""" 39 | return True 40 | 41 | def init(self, context): 42 | """this fct run when appending the node for the first time""" 43 | 44 | self.inputs.new('NodeBoosterCustomSocketInterpolation', "2D Curve") 45 | self.outputs.new('NodeBoosterCustomSocketInterpolation', "Interpolation") 46 | 47 | self.width = 120 48 | 49 | return None 50 | 51 | def copy(self, node): 52 | """fct run when dupplicating the node""" 53 | 54 | return None 55 | 56 | def update(self): 57 | """generic update function""" 58 | 59 | cache_booster_nodes_parent_tree(self.id_data) 60 | 61 | return None 62 | 63 | def draw_label(self,): 64 | """node label""" 65 | if (self.label==''): 66 | return 'Monotonicity' 67 | return self.label 68 | 69 | def evaluator(self, socket_output)->None: 70 | """evaluator the node required for the output evaluator""" 71 | 72 | val = evaluate_upstream_value(self.inputs[0], 73 | match_evaluator_properties={'INTERPOLATION_NODE',}, 74 | set_link_invalid=True, 75 | ) 76 | if (val is None): 77 | return None 78 | return ensure_monotonic_bezsegs(val) 79 | 80 | def draw_buttons(self, context, layout): 81 | """node interface drawing""" 82 | 83 | return None 84 | 85 | def draw_panel(self, layout, context): 86 | """draw in the nodebooster N panel 'Active Node'""" 87 | 88 | n = self 89 | 90 | header, panel = layout.panel("doc_panelid", default_closed=True,) 91 | header.label(text="Documentation",) 92 | if (panel): 93 | word_wrap(layout=panel, alert=False, active=True, max_char='auto', 94 | char_auto_sidepadding=0.9, context=context, string=n.bl_description, 95 | ) 96 | panel.operator("wm.url_open", text="Documentation",).url = "https://blenderartists.org/t/node-booster-extending-blender-node-editors" 97 | 98 | # header, panel = layout.panel("dev_panelid", default_closed=True,) 99 | # header.label(text="Development",) 100 | # if (panel): 101 | # panel.active = False 102 | 103 | # col = panel.column(align=True) 104 | # col.label(text="NodeTree:") 105 | # col.template_ID(n, "node_tree") 106 | 107 | return None 108 | -------------------------------------------------------------------------------- /customnodes/README.txt: -------------------------------------------------------------------------------- 1 | ABOUT: Custom Nodes for Blender’s Existing NodeEditors 2 | ================================================ 3 | 4 | This plugin extends Blender’s native NodeTree system by adding custom nodes 5 | to the Geometry Nodes and/or Shader Node and/or Compositor Nodes editors. 6 | It builds on top of existing editors and enhances them 7 | with new node types and/or socket types. 8 | 9 | 10 | Limitations 11 | ----------- 12 | 13 | Please note the following restrictions, due to how Blender handles node evaluation. 14 | - You cannot: 15 | - Have access the value of an existing native socket. no API currently exists for that. 16 | This imply that you'll never be able to, for example, recieve a float value from an input 17 | and spit out a processed value out. Consider the values of any native input sockets as unknown. 18 | Why: The native nodetrees C++ evaluators of the various tree types do not support evaluating 19 | a node.execute() function for example. Maybe it will added one day. 20 | - You can: 21 | - Arrange a hidden nodegroup nodetree nodes, links, and parameters value. 22 | - Spit out a constant value. 23 | - For Shader and Compositor: Add your own separate nodes evaluation process, which outputs will automatically arrange a hidden node_tree 24 | and/or spit out a constant value. for geometry node this PR is required because unknown socketypes implementation in GN source is 25 | not as flexible as the other editors https://projects.blender.org/blender/blender/pulls/136968. 26 | 27 | 28 | Folder Structure & Contribution 29 | ------------------------------- 30 | 31 | You can implement two types of nodes: 32 | - CustomNodeGroup: 33 | See it as a NodeGroup with python properties and extra interface abilities. 34 | In the NodeBooster N Panel > Active Node > Development, you can see the hidden NodeGroup data. 35 | - CustomNode: 36 | TODO 37 | write about this.. 38 | How it can be used, how it requires an evaluator. 39 | explain how they can implement in the current evaluator system. 40 | 41 | To contribute a custom node, follow these steps: 42 | 43 | 1. Create your node file: 44 | Create a new 'mynodename.py' file in this folder. 45 | Please follow the structure/examples of the existing nodes. 46 | - On init(), if you are using a CustomNodeGroup, you'll need to assign 47 | your hidden node.node_tree. 48 | - Please follow the established naming conventions. 49 | - always start your class with NODEBOOSTER_ 50 | - bl_idname should contain the keyword 'NodeBooster'. 51 | - Use _NG_ for NodeCustomGroup and _ND_ for NodeCustom. 52 | - Use the 'node.auto_upd_flags = {}' attribute to automatically run 'cls.update_all()' on depsgraph. 53 | - node.update() will run when the user is adding new links in the node_tree. 54 | We generally dont use this for CustomNodeGroup. 55 | 56 | 2. Register your new node: 57 | - Import your node class(es) from your new module in the main '__init__.py'. 58 | - In the same '__init__.py' file, add your new classes in the 'classes' object for registration 59 | - and define the placement in the menu tuple object for adding the node in the add menu 60 | interface (The submenus are automatically registered on plugin load depending on that tuple object) . 61 | 62 | Tips: 63 | - Do not directly start messing with new custom socket types & NodeCustom, it's more difficult. 64 | Keep it simple, implement a NodeCustomGroup that just spit out outputs at first. 65 | Then second, you can try implementing a NodeCustomGroup that arrange the node.node_tree nodes and links automatically (ex: math expression node). 66 | - Please scoot out functions available in 'node_utils.py' module. There are useful functions in there to easily manipulate sockets and nodegroups. 67 | See how the existing nodes use these functions. Its best to centralized these actions, a boilerplate for the nodetree API is better for fixing API changes down the line. 68 | - Try out the Python Nex Script node! Perhaps your need can be met and you wont need to implement a new node. Nex Script is pretty powerful. 69 | -------------------------------------------------------------------------------- /customnodes/sockets/custom_sockets.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | # NOTE WARNING DON'T CHANGE bl_idname or class name in here. can break user files. 6 | # NOTE this module is related to the interpolation folder, experimental test of GN CustomNode and customSocketTypes.. 7 | 8 | import bpy 9 | 10 | 11 | class Base(): 12 | 13 | bl_idname = "*ChildrenDefined*" 14 | bl_label = "*ChildrenDefined*" 15 | bl_description = "Custom Datatype" 16 | 17 | nodebooster_socket_type = "*ChildrenDefined*" 18 | nodebooster_socket_color = (0.8, 0.2, 0.2, 1) 19 | 20 | default_value : bpy.props.FloatProperty( 21 | default=0.0, 22 | ) 23 | socket_color : bpy.props.FloatVectorProperty( 24 | subtype='COLOR', 25 | default=nodebooster_socket_color, 26 | size=4, 27 | ) 28 | socket_label : bpy.props.StringProperty( 29 | default=nodebooster_socket_type.title(), 30 | ) 31 | display_label : bpy.props.BoolProperty( 32 | default=False, 33 | name="Display Label", 34 | description="do we display the socket_label or the native socket name attribute?" 35 | ) 36 | 37 | def draw(self, context, layout, node, text): 38 | 39 | if (self.display_label): 40 | layout.label(text=self.socket_label) 41 | else: layout.label(text=self.name) 42 | 43 | return None 44 | 45 | def draw_color(self, context, node): 46 | return self.socket_color 47 | 48 | @classmethod 49 | def draw_color_simple(cls,): 50 | return cls.nodebooster_socket_color 51 | 52 | 53 | class NODEBOOSTER_SK_Interpolation(Base, bpy.types.NodeSocket): 54 | 55 | # NOTE technically speaking, this type can be used for generic curvesn as it manipualtes a list of points coordinates with handles info. 56 | # the name has been chosen like that, and it's too late to change it to something like NODEBOOSTER_SK_Curve anyway. 57 | # that doesn't change the fact that we can use it in other context, just using another label and 58 | # we should be good, the user will never know.. it's just a class name.. 59 | 60 | bl_idname = "NodeBoosterCustomSocketInterpolation" 61 | bl_label = "Interpolation" 62 | bl_description = "Interpolation Data Type" 63 | nodebooster_socket_type = "INTERPOLATION" 64 | nodebooster_socket_color = (0.713274, 0.432440, 0.349651, 1.0) 65 | 66 | socket_color : bpy.props.FloatVectorProperty( 67 | subtype='COLOR', 68 | default=nodebooster_socket_color, 69 | size=4, 70 | ) 71 | socket_label : bpy.props.StringProperty( 72 | default=nodebooster_socket_type.title(), 73 | ) 74 | 75 | class NODEBOOSTER_ND_CustomSocketUtility(bpy.types.Node): 76 | 77 | bl_idname = "CustomSocketUtility" 78 | bl_label = "Socket Utility (Dev)" 79 | bl_description = "an internal utility node, for creating our customgroups" 80 | bl_icon = 'NODE' 81 | nb_menu_path = ['NodeBooster','Experimental','Interpolation',bl_label,] #path in add menu 82 | auto_upd_flags = {'NONE',} 83 | 84 | def init(self, context): 85 | self.inputs.new('NodeBoosterCustomSocketInterpolation', "Interpolation") 86 | self.outputs.new('NodeBoosterCustomSocketInterpolation', "Interpolation") 87 | return None 88 | 89 | def update(self): 90 | 91 | return None 92 | 93 | def draw_buttons(self, context, layout): 94 | 95 | col = layout.column(align=True) 96 | col.label(text="Outputs") 97 | col.prop(self.outputs[0], "display_label") 98 | col.prop(self.outputs[0], "socket_label", text="") 99 | col.prop(self.outputs[0], "socket_color", text="") 100 | 101 | col = layout.column(align=True) 102 | col.label(text="Inputs") 103 | col.prop(self.outputs[0], "display_label") 104 | col.prop(self.inputs[0], "socket_label", text="") 105 | col.prop(self.inputs[0], "socket_color", text="") 106 | 107 | return None 108 | 109 | def draw_label(self): 110 | return 'SocketUtility' 111 | -------------------------------------------------------------------------------- /operators/purge.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | 8 | 9 | def is_node_used(node): 10 | """check if node is reaching output""" 11 | 12 | found_output = False 13 | 14 | def recur_node(n): 15 | 16 | #reached destination? 17 | if (n.type == "GROUP_OUTPUT"): 18 | nonlocal found_output 19 | found_output = True 20 | return 21 | 22 | #else continue parcour 23 | for out in n.outputs: 24 | for link in out.links: 25 | recur_node(link.to_node) 26 | 27 | return None 28 | 29 | recur_node(node) 30 | 31 | return found_output 32 | 33 | 34 | def purge_unused_nodes(node_group, delete_muted=True, delete_reroute=True, delete_frame=True): 35 | """delete all unused nodes, using 'ops.node.delete_reconnect' operator""" 36 | 37 | for n in list(node_group.nodes): 38 | 39 | #deselct all 40 | n.select = False 41 | 42 | #delete if muted? 43 | if (delete_muted==True and n.mute==True): 44 | n.select = True 45 | continue 46 | 47 | #delete if reroute? 48 | if (delete_reroute==True and n.type=="REROUTE"): 49 | n.select = True 50 | continue 51 | 52 | #don't delete if frame? 53 | if (delete_frame==False and n.type=="FRAME"): 54 | continue 55 | 56 | #delete if unconnected 57 | if not is_node_used(n): 58 | node_group.nodes.remove(n) 59 | 60 | continue 61 | 62 | if (delete_muted or delete_reroute): 63 | bpy.ops.node.delete_reconnect() 64 | 65 | return None 66 | 67 | 68 | def re_arrange_nodes(node_group, Xmultiplier=1): 69 | """re-arrange node by sorting them in X location, (could improve)""" 70 | 71 | nodes = { n.location.x:n for n in node_group.nodes } 72 | nodes = { k:nodes[k] for k in sorted(nodes) } 73 | 74 | for i,n in enumerate(nodes.values()): 75 | n.location.x = i*200*Xmultiplier 76 | n.width = 150 77 | 78 | return None 79 | 80 | 81 | class NODEBOOSTER_OT_node_purge_unused(bpy.types.Operator): 82 | 83 | bl_idname = "nodebooster.node_purge_unused" 84 | bl_label = "Purge Unused Nodes" 85 | bl_description = "" 86 | bl_options = {'REGISTER','UNDO',} 87 | 88 | delete_frame : bpy.props.BoolProperty( 89 | default=True, 90 | name="Remove Frame(s)", 91 | ) 92 | delete_muted : bpy.props.BoolProperty( 93 | default=True, 94 | name="Remove Muted Node(s)", 95 | ) 96 | delete_reroute : bpy.props.BoolProperty( 97 | default=True, 98 | name="Remove Reroute(s)", 99 | ) 100 | 101 | re_arrange : bpy.props.BoolProperty( 102 | default=False, 103 | name="Re-Arrange Nodes", 104 | ) 105 | re_arrange_fake : bpy.props.BoolProperty( 106 | default=False, 107 | name="Re-Arrange (not possible with frames)", 108 | ) 109 | 110 | @classmethod 111 | def poll(cls, context): 112 | return (context.space_data.type=='NODE_EDITOR') and (context.space_data.node_tree is not None) 113 | 114 | def execute(self, context): 115 | node_group = context.space_data.node_tree 116 | 117 | purge_unused_nodes( 118 | node_group, 119 | delete_muted=self.delete_muted, 120 | delete_reroute=self.delete_reroute, 121 | delete_frame=self.delete_frame, 122 | ) 123 | 124 | if (self.re_arrange and self.delete_frame): 125 | re_arrange_nodes(node_group) 126 | 127 | return {'FINISHED'} 128 | 129 | def invoke(self, context, event): 130 | return bpy.context.window_manager.invoke_props_dialog(self) 131 | 132 | def draw(self, context): 133 | layout = self.layout 134 | 135 | layout.prop(self, "delete_muted") 136 | layout.prop(self, "delete_reroute") 137 | layout.prop(self, "delete_frame") 138 | 139 | match self.delete_frame: 140 | case True: 141 | layout.prop(self, "re_arrange") 142 | case False: 143 | re = layout.row() 144 | re.enabled = False 145 | re.prop(self, "re_arrange_fake") 146 | 147 | return None -------------------------------------------------------------------------------- /customnodes/interpolation/spline2dmix.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | import os 8 | import numpy as np 9 | 10 | from ... import get_addon_prefs 11 | from ...utils.str_utils import word_wrap 12 | from ...utils.bezier2d_utils import lerp_bezsegs 13 | from ..evaluator import evaluate_upstream_value 14 | from ...utils.node_utils import ( 15 | send_refresh_signal, 16 | cache_booster_nodes_parent_tree, 17 | ) 18 | 19 | # ooooo ooo .o8 20 | # `888b. `8' "888 21 | # 8 `88b. 8 .ooooo. .oooo888 .ooooo. 22 | # 8 `88b. 8 d88' `88b d88' `888 d88' `88b 23 | # 8 `88b.8 888 888 888 888 888ooo888 24 | # 8 `888 888 888 888 888 888 .o 25 | # o8o `8 `Y8bod8P' `Y8bod88P" `Y8bod8P' 26 | 27 | 28 | class NODEBOOSTER_ND_2DCurvesMix(bpy.types.Node): 29 | 30 | bl_idname = "NodeBooster2DCurvesMix" 31 | bl_label = "Mix 2D Curves" 32 | bl_description = """Mix two 2D curves linearly. Ideally the numbers of segments should be similar. If not, we'll try to match them by subdividing more segments at projected locations.""" 33 | nb_menu_path = ['NodeBooster','Experimental','Interpolation',bl_label,] #path in add menu 34 | auto_upd_flags = {'NONE',} 35 | tree_type = "AnyNodeTree" 36 | 37 | evaluator_properties = {'INTERPOLATION_NODE',} 38 | 39 | mixfac: bpy.props.FloatProperty( 40 | name="Factor", 41 | default=0.5, 42 | min=0.0, 43 | max=1.0, 44 | step=0.01, 45 | update=lambda self, context: self.update_trigger(), 46 | ) 47 | 48 | @classmethod 49 | def poll(cls, context): 50 | """mandatory poll""" 51 | return True 52 | 53 | def init(self, context): 54 | """this fct run when appending the node for the first time""" 55 | 56 | self.inputs.new('NodeBoosterCustomSocketInterpolation', "2D Curve") 57 | self.inputs.new('NodeBoosterCustomSocketInterpolation', "2D Curve") 58 | self.outputs.new('NodeBoosterCustomSocketInterpolation', "2D Curve") 59 | 60 | return None 61 | 62 | def copy(self, node): 63 | """fct run when dupplicating the node""" 64 | 65 | return None 66 | 67 | def update(self): 68 | """generic update function""" 69 | 70 | cache_booster_nodes_parent_tree(self.id_data) 71 | 72 | return None 73 | 74 | def draw_label(self,): 75 | """node label""" 76 | if (self.label==''): 77 | return 'Mix' 78 | return self.label 79 | 80 | def update_trigger(self,): 81 | """send an update trigger to the whole node_tree""" 82 | 83 | send_refresh_signal(self.outputs[0]) 84 | 85 | return None 86 | 87 | def evaluator(self, socket_output)->None: 88 | """evaluator the node required for the output evaluator""" 89 | 90 | val1 = evaluate_upstream_value(self.inputs[0], 91 | match_evaluator_properties={'INTERPOLATION_NODE',}, 92 | set_link_invalid=True, 93 | ) 94 | val2 = evaluate_upstream_value(self.inputs[1], 95 | match_evaluator_properties={'INTERPOLATION_NODE',}, 96 | set_link_invalid=True, 97 | ) 98 | 99 | if (val1 is None) or (val2 is None): 100 | return None 101 | 102 | match self.mixfac: 103 | case 0.0: final = val1 104 | case 1.0: final = val2 105 | case _: final = lerp_bezsegs(val1, val2, self.mixfac) 106 | 107 | return final 108 | 109 | def draw_buttons(self, context, layout): 110 | """node interface drawing""" 111 | 112 | layout.prop(self, "mixfac") 113 | 114 | return None 115 | 116 | def draw_panel(self, layout, context): 117 | """draw in the nodebooster N panel 'Active Node'""" 118 | 119 | n = self 120 | 121 | header, panel = layout.panel("doc_panelid", default_closed=True,) 122 | header.label(text="Documentation",) 123 | if (panel): 124 | word_wrap(layout=panel, alert=False, active=True, max_char='auto', 125 | char_auto_sidepadding=0.9, context=context, string=n.bl_description, 126 | ) 127 | panel.operator("wm.url_open", text="Documentation",).url = "https://blenderartists.org/t/node-booster-extending-blender-node-editors" 128 | 129 | # header, panel = layout.panel("dev_panelid", default_closed=True,) 130 | # header.label(text="Development",) 131 | # if (panel): 132 | # panel.active = False 133 | 134 | # col = panel.column(align=True) 135 | # col.label(text="NodeTree:") 136 | # col.template_ID(n, "node_tree") 137 | 138 | return None 139 | -------------------------------------------------------------------------------- /customnodes/interpolation/spline2dextend.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | import os 8 | import numpy as np 9 | 10 | from ...__init__ import get_addon_prefs 11 | from ...utils.str_utils import word_wrap 12 | from ...utils.bezier2d_utils import extend_bezsegs 13 | from ..evaluator import evaluate_upstream_value 14 | from ...utils.node_utils import ( 15 | send_refresh_signal, 16 | cache_booster_nodes_parent_tree, 17 | ) 18 | 19 | # ooooo ooo .o8 20 | # `888b. `8' "888 21 | # 8 `88b. 8 .ooooo. .oooo888 .ooooo. 22 | # 8 `88b. 8 d88' `88b d88' `888 d88' `88b 23 | # 8 `88b.8 888 888 888 888 888ooo888 24 | # 8 `888 888 888 888 888 888 .o 25 | # o8o `8 `Y8bod8P' `Y8bod88P" `Y8bod8P' 26 | 27 | 28 | class NODEBOOSTER_ND_2DCurveExtend(bpy.types.Node): 29 | 30 | bl_idname = "NodeBooster2DCurveExtend" 31 | bl_label = "Extend 2D Curve" 32 | bl_description = """Extend a 2D curve at a specific X location by adding a new segment""" 33 | nb_menu_path = ['NodeBooster','Experimental','Interpolation',bl_label,] #path in add menu 34 | auto_upd_flags = {'NONE',} 35 | tree_type = "AnyNodeTree" 36 | 37 | evaluator_properties = {'INTERPOLATION_NODE',} 38 | 39 | mode : bpy.props.EnumProperty( 40 | name="Mode", 41 | description="The mode to use for the extension", 42 | items=(('HANDLE', 'Handle', 'Extend the curve as the continuation of the last segment'), 43 | ('HORIZONTAL', 'Horizontal', 'Extend the curve horizontally'), 44 | ), 45 | default='HANDLE', 46 | update=lambda self, context: self.update_trigger() 47 | ) 48 | xloc : bpy.props.FloatProperty( 49 | name="X Location", 50 | description="The X location to cut the curve at", 51 | default=0.0, 52 | soft_min=-2.0, 53 | soft_max=2.0, 54 | update=lambda self, context: self.update_trigger() 55 | ) 56 | 57 | @classmethod 58 | def poll(cls, context): 59 | """mandatory poll""" 60 | return True 61 | 62 | def init(self, context): 63 | """this fct run when appending the node for the first time""" 64 | 65 | self.inputs.new('NodeBoosterCustomSocketInterpolation', "2D Curve") 66 | self.outputs.new('NodeBoosterCustomSocketInterpolation', "2D Curve") 67 | 68 | self.width = 145 69 | 70 | return None 71 | 72 | def copy(self, node): 73 | """fct run when dupplicating the node""" 74 | 75 | return None 76 | 77 | def update(self): 78 | """generic update function""" 79 | 80 | cache_booster_nodes_parent_tree(self.id_data) 81 | 82 | return None 83 | 84 | def draw_label(self,): 85 | """node label""" 86 | if (self.label==''): 87 | return 'Extend' 88 | return self.label 89 | 90 | def update_trigger(self,): 91 | """send an update trigger to the whole node_tree""" 92 | 93 | send_refresh_signal(self.outputs[0]) 94 | 95 | return None 96 | 97 | def evaluator(self, socket_output)->None: 98 | """evaluator the node required for the output evaluator""" 99 | 100 | val = evaluate_upstream_value(self.inputs[0], 101 | match_evaluator_properties={'INTERPOLATION_NODE',}, 102 | set_link_invalid=True, 103 | ) 104 | if (val is None): 105 | return None 106 | 107 | return extend_bezsegs(val, self.xloc, mode=self.mode,) 108 | 109 | def draw_buttons(self, context, layout): 110 | """node interface drawing""" 111 | 112 | layout.prop(self, "mode", text="",) 113 | layout.prop(self, "xloc") 114 | 115 | return None 116 | 117 | def draw_panel(self, layout, context): 118 | """draw in the nodebooster N panel 'Active Node'""" 119 | 120 | n = self 121 | 122 | header, panel = layout.panel("doc_panelid", default_closed=True,) 123 | header.label(text="Documentation",) 124 | if (panel): 125 | word_wrap(layout=panel, alert=False, active=True, max_char='auto', 126 | char_auto_sidepadding=0.9, context=context, string=n.bl_description, 127 | ) 128 | panel.operator("wm.url_open", text="Documentation",).url = "https://blenderartists.org/t/node-booster-extending-blender-node-editors" 129 | 130 | # header, panel = layout.panel("dev_panelid", default_closed=True,) 131 | # header.label(text="Development",) 132 | # if (panel): 133 | # panel.active = False 134 | 135 | # col = panel.column(align=True) 136 | # col.label(text="NodeTree:") 137 | # col.template_ID(n, "node_tree") 138 | 139 | return None 140 | -------------------------------------------------------------------------------- /customnodes/isrenderedview.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | 8 | from ..__init__ import get_addon_prefs 9 | from ..utils.str_utils import word_wrap 10 | from ..utils.node_utils import ( 11 | create_new_nodegroup, 12 | set_ng_socket_defvalue, 13 | cache_booster_nodes_parent_tree, 14 | ) 15 | 16 | def all_3d_viewports(): 17 | """return generator of all 3d view space""" 18 | for window in bpy.context.window_manager.windows: 19 | for area in window.screen.areas: 20 | if (area.type == 'VIEW_3D'): 21 | for space in area.spaces: 22 | if (space.type == 'VIEW_3D'): 23 | yield space 24 | 25 | def all_3d_viewports_shading_type(): 26 | """return generator of all shading type str""" 27 | for space in all_3d_viewports(): 28 | yield space.shading.type 29 | 30 | def is_rendered_view(): 31 | """check if is rendered view in a 3d view somewhere""" 32 | for shading_type in all_3d_viewports_shading_type(): 33 | if (shading_type == 'RENDERED'): 34 | return True 35 | return False 36 | 37 | 38 | # ooooo ooo .o8 39 | # `888b. `8' "888 40 | # 8 `88b. 8 .ooooo. .oooo888 .ooooo. 41 | # 8 `88b. 8 d88' `88b d88' `888 d88' `88b 42 | # 8 `88b.8 888 888 888 888 888ooo888 43 | # 8 `888 888 888 888 888 888 .o 44 | # o8o `8 `Y8bod8P' `Y8bod88P" `Y8bod8P' 45 | 46 | class NODEBOOSTER_NG_GN_IsRenderedView(bpy.types.GeometryNodeCustomGroup): 47 | """Custom Nodgroup: Evaluate if any 3Dviewport is in rendered view mode. 48 | • The value is evaluated from depsgraph post update signals""" 49 | 50 | bl_idname = "GeometryNodeNodeBoosterIsRenderedView" 51 | bl_label = "Is Rendered View" 52 | nb_menu_path = ['NodeBooster','Inputs',bl_label,] #path in add menu 53 | auto_upd_flags = {'*CUSTOM_IMPLEMENTATION*',} #NOTE: This node is manually implemented at a handler level. 54 | tree_type = "GeometryNodeTree" 55 | 56 | @classmethod 57 | def poll(cls, context): 58 | """mandatory poll""" 59 | return True 60 | 61 | def init(self, context,): 62 | """this fct run when appending the node for the first time""" 63 | 64 | name = f".{self.bl_idname}" 65 | 66 | ng = bpy.data.node_groups.get(name) 67 | if (ng is None): 68 | ng = create_new_nodegroup(name, 69 | out_sockets={ 70 | "Is Rendered View" : "NodeSocketBool", 71 | }, 72 | ) 73 | 74 | self.node_tree = ng 75 | 76 | set_ng_socket_defvalue(ng, 0, value=is_rendered_view(),) 77 | return None 78 | 79 | def update(self): 80 | """generic update function""" 81 | 82 | cache_booster_nodes_parent_tree(self.id_data) 83 | 84 | return None 85 | 86 | # def sync_out_values(self): 87 | # """sync output socket values with data""" 88 | # set_ng_socket_defvalue(self.node_tree, 0, value=is_rendered_view(),) 89 | # return None 90 | 91 | def draw_label(self,): 92 | """node label""" 93 | if (self.label==''): 94 | return 'Is Rendered View' 95 | return self.label 96 | 97 | def draw_buttons(self, context, layout,): 98 | """node interface drawing""" 99 | 100 | return None 101 | 102 | def draw_panel(self, layout, context): 103 | """draw in the nodebooster N panel 'Active Node'""" 104 | 105 | n = self 106 | 107 | header, panel = layout.panel("doc_panelid", default_closed=True,) 108 | header.label(text="Documentation",) 109 | if (panel): 110 | word_wrap(layout=panel, alert=False, active=True, max_char='auto', 111 | char_auto_sidepadding=0.9, context=context, string=n.bl_description, 112 | ) 113 | panel.operator("wm.url_open", text="Documentation",).url = "https://blenderartists.org/t/node-booster-extending-blender-node-editors" 114 | 115 | header, panel = layout.panel("dev_panelid", default_closed=True,) 116 | header.label(text="Development",) 117 | if (panel): 118 | panel.active = False 119 | 120 | col = panel.column(align=True) 121 | col.label(text="NodeTree:") 122 | col.template_ID(n, "node_tree") 123 | 124 | return None 125 | 126 | @classmethod 127 | def update_all(cls, using_nodes=None, signal_from_handlers=False,): 128 | """search for all node instances of this type and refresh them. Will be called automatically if .auto_upd_flags's are defined""" 129 | 130 | # actually we don't need to update all instances. 131 | # for this special node who always use the same node_tree for all nodes, 132 | # we simply have to update one nodetree. 133 | 134 | name = f".{cls.bl_idname}" 135 | ng = bpy.data.node_groups.get(name) 136 | if (ng): 137 | set_ng_socket_defvalue(ng, 0, value=is_rendered_view(),) 138 | 139 | return None 140 | -------------------------------------------------------------------------------- /customnodes/sceneinfo.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | 8 | from ..__init__ import get_addon_prefs 9 | from ..utils.str_utils import word_wrap 10 | from ..utils.node_utils import ( 11 | create_new_nodegroup, 12 | set_ng_socket_defvalue, 13 | get_booster_nodes, 14 | cache_booster_nodes_parent_tree, 15 | ) 16 | 17 | 18 | # ooooo ooo .o8 19 | # `888b. `8' "888 20 | # 8 `88b. 8 .ooooo. .oooo888 .ooooo. 21 | # 8 `88b. 8 d88' `88b d88' `888 d88' `88b 22 | # 8 `88b.8 888 888 888 888 888ooo888 23 | # 8 `888 888 888 888 888 888 .o 24 | # o8o `8 `Y8bod8P' `Y8bod88P" `Y8bod8P' 25 | 26 | class Base(): 27 | 28 | bl_idname = "NodeBoosterSceneInfo" 29 | bl_label = "Scene Info" 30 | bl_description = """Gather informations about your active scene. 31 | • Expect updates on each depsgraph post and frame_pre update signals""" 32 | nb_menu_path = ['NodeBooster','Inputs',bl_label,] #path in add menu 33 | auto_upd_flags = {'FRAME_PRE','DEPS_POST',} 34 | tree_type = "*ChildrenDefined*" 35 | 36 | @classmethod 37 | def poll(cls, context): 38 | """mandatory poll""" 39 | return True 40 | 41 | def init(self, context): 42 | """this fct run when appending the node for the first time""" 43 | 44 | name = f".{self.bl_idname}" 45 | 46 | ng = bpy.data.node_groups.get(name) 47 | if (ng is None): 48 | ng = create_new_nodegroup(name, 49 | tree_type=self.tree_type, 50 | out_sockets={ 51 | "Use Gravity": "NodeSocketBool", 52 | "Gravity": "NodeSocketVector", 53 | }, 54 | ) 55 | 56 | ng = ng.copy() #always using a copy of the original ng 57 | self.node_tree = ng 58 | 59 | return None 60 | 61 | def copy(self, node): 62 | """fct run when dupplicating the node""" 63 | 64 | #NOTE: copy/paste can cause crashes, we use a timer to delay the action 65 | def delayed_copy(): 66 | self.node_tree = node.node_tree.copy() 67 | bpy.app.timers.register(delayed_copy, first_interval=0.01) 68 | 69 | return None 70 | 71 | def update(self): 72 | """generic update function""" 73 | 74 | cache_booster_nodes_parent_tree(self.id_data) 75 | 76 | return None 77 | 78 | def sync_out_values(self): 79 | """sync output socket values with data""" 80 | 81 | scene = bpy.context.scene 82 | 83 | set_ng_socket_defvalue(self.node_tree, 0, value=scene.use_gravity) 84 | set_ng_socket_defvalue(self.node_tree, 1, value=scene.gravity) 85 | 86 | return None 87 | 88 | def draw_label(self,): 89 | """node label""" 90 | if (self.label==''): 91 | return 'Scene Info' 92 | return self.label 93 | 94 | def draw_buttons(self, context, layout): 95 | """node interface drawing""" 96 | 97 | return None 98 | 99 | def draw_panel(self, layout, context): 100 | """draw in the nodebooster N panel 'Active Node'""" 101 | 102 | n = self 103 | 104 | header, panel = layout.panel("doc_panelid", default_closed=True,) 105 | header.label(text="Documentation",) 106 | if (panel): 107 | word_wrap(layout=panel, alert=False, active=True, max_char='auto', 108 | char_auto_sidepadding=0.9, context=context, string=n.bl_description, 109 | ) 110 | panel.operator("wm.url_open", text="Documentation",).url = "https://blenderartists.org/t/node-booster-extending-blender-node-editors" 111 | 112 | header, panel = layout.panel("dev_panelid", default_closed=True,) 113 | header.label(text="Development",) 114 | if (panel): 115 | panel.active = False 116 | 117 | col = panel.column(align=True) 118 | col.label(text="NodeTree:") 119 | col.template_ID(n, "node_tree") 120 | 121 | return None 122 | 123 | @classmethod 124 | def update_all(cls, using_nodes=None, signal_from_handlers=False,): 125 | """search for all node instances of this type and refresh them. Will be called automatically if .auto_upd_flags's are defined""" 126 | 127 | if (using_nodes is None): 128 | nodes = get_booster_nodes(by_idnames={cls.bl_idname},) 129 | else: nodes = [n for n in using_nodes if (n.bl_idname==cls.bl_idname)] 130 | 131 | for n in nodes: 132 | n.sync_out_values() 133 | 134 | return None 135 | 136 | #Per Node-Editor Children: 137 | #Respect _NG_ + _GN_/_SH_/_CP_ nomenclature 138 | 139 | class NODEBOOSTER_NG_GN_SceneInfo(Base, bpy.types.GeometryNodeCustomGroup): 140 | tree_type = "GeometryNodeTree" 141 | bl_idname = "GeometryNode" + Base.bl_idname 142 | 143 | class NODEBOOSTER_NG_SH_SceneInfo(Base, bpy.types.ShaderNodeCustomGroup): 144 | tree_type = "ShaderNodeTree" 145 | bl_idname = "ShaderNode" + Base.bl_idname 146 | 147 | class NODEBOOSTER_NG_CP_SceneInfo(Base, bpy.types.CompositorNodeCustomGroup): 148 | tree_type = "CompositorNodeTree" 149 | bl_idname = "CompositorNode" + Base.bl_idname 150 | -------------------------------------------------------------------------------- /customnodes/interpolation/interpolationloop.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | import os 8 | import numpy as np 9 | 10 | from ... import get_addon_prefs 11 | from ...utils.str_utils import word_wrap 12 | from ...utils.bezier2d_utils import looped_offset_bezsegs 13 | from ..evaluator import evaluate_upstream_value 14 | from ...utils.node_utils import ( 15 | send_refresh_signal, 16 | cache_booster_nodes_parent_tree, 17 | ) 18 | 19 | 20 | #TODO IMPORTANT: 21 | # - fix when cut occurs right in a anchor. will create a None graph suddenly.. see todo in 'looped_offset_bezsegs' 22 | # - we'll need to improve the evaluation system for supporting animation mode 23 | 24 | # ooooo ooo .o8 25 | # `888b. `8' "888 26 | # 8 `88b. 8 .ooooo. .oooo888 .ooooo. 27 | # 8 `88b. 8 d88' `88b d88' `888 d88' `88b 28 | # 8 `88b.8 888 888 888 888 888ooo888 29 | # 8 `888 888 888 888 888 888 .o 30 | # o8o `8 `Y8bod8P' `Y8bod88P" `Y8bod8P' 31 | 32 | 33 | class NODEBOOSTER_ND_InterpolationLoop(bpy.types.Node): 34 | 35 | bl_idname = "NodeBooster2DCurveLoop" 36 | bl_label = "Loop Interpolation" 37 | bl_description = """Loop an interpolation 2D curve from a given offset or speed. If the passed 2D curve is not monotonic, we'll make it monotonic first.""" 38 | nb_menu_path = ['NodeBooster','Experimental','Interpolation',bl_label,] #path in add menu 39 | auto_upd_flags = {'NONE',} 40 | tree_type = "AnyNodeTree" 41 | 42 | evaluator_properties = {'INTERPOLATION_NODE',} 43 | 44 | mode : bpy.props.EnumProperty( 45 | name="Mode", 46 | description="Loop an interpolation 2D curve from a given offset or speed.", 47 | items=(('OFFSET', 'Offset', 'Loop-Offset the curve by a given amount'), 48 | ('ANIMATION', 'Animation', 'Loop an animation curve'), 49 | ), 50 | default='OFFSET', 51 | update=lambda self, context: self.update_trigger() 52 | ) 53 | offset : bpy.props.FloatProperty( 54 | name="Offset", 55 | description="The offset to loop the curve by", 56 | default=0.0, 57 | update=lambda self, context: self.update_trigger() 58 | ) 59 | speed : bpy.props.FloatProperty( 60 | name="Speed", 61 | description="The speed to loop the curve at, scaling the current framerate", 62 | default=1.0, 63 | soft_min=-10.0, 64 | soft_max=10.0, 65 | update=lambda self, context: self.update_trigger() 66 | ) 67 | 68 | @classmethod 69 | def poll(cls, context): 70 | """mandatory poll""" 71 | return True 72 | 73 | def init(self, context): 74 | """this fct run when appending the node for the first time""" 75 | 76 | self.inputs.new('NodeBoosterCustomSocketInterpolation', "Interpolation") 77 | self.outputs.new('NodeBoosterCustomSocketInterpolation', "Interpolation") 78 | 79 | return None 80 | 81 | def copy(self, node): 82 | """fct run when dupplicating the node""" 83 | 84 | return None 85 | 86 | def update(self): 87 | """generic update function""" 88 | 89 | cache_booster_nodes_parent_tree(self.id_data) 90 | 91 | return None 92 | 93 | def draw_label(self,): 94 | """node label""" 95 | if (self.label==''): 96 | return 'Loop' 97 | return self.label 98 | 99 | def update_trigger(self,): 100 | """send an update trigger to the whole node_tree""" 101 | 102 | send_refresh_signal(self.outputs[0]) 103 | 104 | return None 105 | 106 | def evaluator(self, socket_output)->None: 107 | """evaluator the node required for the output evaluator""" 108 | 109 | val = evaluate_upstream_value(self.inputs[0], 110 | match_evaluator_properties={'INTERPOLATION_NODE',}, 111 | set_link_invalid=True, 112 | ) 113 | if (val is None): 114 | return None 115 | 116 | match self.mode: 117 | case 'OFFSET': 118 | offset = self.offset 119 | case 'ANIMATION': 120 | offset = self.speed * (bpy.context.scene.frame_current / bpy.context.scene.render.fps) 121 | 122 | return looped_offset_bezsegs(val, offset=offset) 123 | 124 | def draw_buttons(self, context, layout): 125 | """node interface drawing""" 126 | 127 | layout.prop(self, "mode", text="",) 128 | if (self.mode == 'OFFSET'): 129 | layout.prop(self, "offset") 130 | else: layout.prop(self, "speed") 131 | 132 | return None 133 | 134 | def draw_panel(self, layout, context): 135 | """draw in the nodebooster N panel 'Active Node'""" 136 | 137 | n = self 138 | 139 | header, panel = layout.panel("doc_panelid", default_closed=True,) 140 | header.label(text="Documentation",) 141 | if (panel): 142 | word_wrap(layout=panel, alert=False, active=True, max_char='auto', 143 | char_auto_sidepadding=0.9, context=context, string=n.bl_description, 144 | ) 145 | panel.operator("wm.url_open", text="Documentation",).url = "https://blenderartists.org/t/node-booster-extending-blender-node-editors" 146 | 147 | # header, panel = layout.panel("dev_panelid", default_closed=True,) 148 | # header.label(text="Development",) 149 | # if (panel): 150 | # panel.active = False 151 | 152 | # col = panel.column(align=True) 153 | # col.label(text="NodeTree:") 154 | # col.template_ID(n, "node_tree") 155 | 156 | return None 157 | -------------------------------------------------------------------------------- /customnodes/renderinfo.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | 8 | from ..__init__ import get_addon_prefs 9 | from ..utils.str_utils import word_wrap 10 | from ..utils.node_utils import ( 11 | create_new_nodegroup, 12 | set_ng_socket_defvalue, 13 | get_booster_nodes, 14 | cache_booster_nodes_parent_tree, 15 | ) 16 | 17 | 18 | # ooooo ooo .o8 19 | # `888b. `8' "888 20 | # 8 `88b. 8 .ooooo. .oooo888 .ooooo. 21 | # 8 `88b. 8 d88' `88b d88' `888 d88' `88b 22 | # 8 `88b.8 888 888 888 888 888ooo888 23 | # 8 `888 888 888 888 888 888 .o 24 | # o8o `8 `Y8bod8P' `Y8bod88P" `Y8bod8P' 25 | 26 | class Base(): 27 | 28 | bl_idname = "NodeBoosterRenderInfo" 29 | bl_label = "Render Info" 30 | bl_description = """Custom Nodgroup: Gather informations about your active scene render info. 31 | • Expect updates on each depsgraph post and frame_pre update signals""" 32 | nb_menu_path = ['NodeBooster','Inputs',bl_label,] #path in add menu 33 | auto_upd_flags = {'FRAME_PRE','DEPS_POST',} 34 | tree_type = "*ChildrenDefined*" 35 | 36 | @classmethod 37 | def poll(cls, context): 38 | """mandatory poll""" 39 | return True 40 | 41 | def init(self, context): 42 | """this fct run when appending the node for the first time""" 43 | 44 | name = f".{self.bl_idname}" 45 | 46 | ng = bpy.data.node_groups.get(name) 47 | if (ng is None): 48 | ng = create_new_nodegroup(name, 49 | tree_type=self.tree_type, 50 | out_sockets={ 51 | "Resolution X" : "NodeSocketInt", 52 | "Resolution Y" : "NodeSocketInt", 53 | "Resolution %" : "NodeSocketFloat", 54 | "Aspect X" : "NodeSocketFloat", 55 | "Aspect Y" : "NodeSocketFloat", 56 | "Frame Start" : "NodeSocketInt", 57 | "Frame End" : "NodeSocketInt", 58 | "Frame Step" : "NodeSocketInt", 59 | }, 60 | ) 61 | 62 | ng = ng.copy() #always using a copy of the original ng 63 | self.node_tree = ng 64 | 65 | return None 66 | 67 | def copy(self, node): 68 | """fct run when dupplicating the node""" 69 | 70 | #NOTE: copy/paste can cause crashes, we use a timer to delay the action 71 | def delayed_copy(): 72 | self.node_tree = node.node_tree.copy() 73 | bpy.app.timers.register(delayed_copy, first_interval=0.01) 74 | 75 | return None 76 | 77 | def update(self): 78 | """generic update function""" 79 | 80 | cache_booster_nodes_parent_tree(self.id_data) 81 | 82 | return None 83 | 84 | def sync_out_values(self): 85 | """sync output socket values with data""" 86 | 87 | scene = bpy.context.scene 88 | 89 | set_ng_socket_defvalue(self.node_tree, 0, value=scene.render.resolution_x) 90 | set_ng_socket_defvalue(self.node_tree, 1, value=scene.render.resolution_y) 91 | set_ng_socket_defvalue(self.node_tree, 2, value=scene.render.resolution_percentage) 92 | set_ng_socket_defvalue(self.node_tree, 3, value=scene.render.pixel_aspect_x) 93 | set_ng_socket_defvalue(self.node_tree, 4, value=scene.render.pixel_aspect_y) 94 | set_ng_socket_defvalue(self.node_tree, 5, value=scene.frame_start) 95 | set_ng_socket_defvalue(self.node_tree, 6, value=scene.frame_end) 96 | set_ng_socket_defvalue(self.node_tree, 7, value=scene.frame_step) 97 | 98 | return None 99 | 100 | def draw_label(self,): 101 | """node label""" 102 | if (self.label==''): 103 | return 'Render Info' 104 | return self.label 105 | 106 | def draw_buttons(self, context, layout): 107 | """node interface drawing""" 108 | 109 | return None 110 | 111 | def draw_panel(self, layout, context): 112 | """draw in the nodebooster N panel 'Active Node'""" 113 | 114 | n = self 115 | 116 | header, panel = layout.panel("doc_panelid", default_closed=True,) 117 | header.label(text="Documentation",) 118 | if (panel): 119 | word_wrap(layout=panel, alert=False, active=True, max_char='auto', 120 | char_auto_sidepadding=0.9, context=context, string=n.bl_description, 121 | ) 122 | panel.operator("wm.url_open", text="Documentation",).url = "https://blenderartists.org/t/node-booster-extending-blender-node-editors" 123 | 124 | header, panel = layout.panel("dev_panelid", default_closed=True,) 125 | header.label(text="Development",) 126 | if (panel): 127 | panel.active = False 128 | 129 | col = panel.column(align=True) 130 | col.label(text="NodeTree:") 131 | col.template_ID(n, "node_tree") 132 | 133 | return None 134 | 135 | @classmethod 136 | def update_all(cls, using_nodes=None, signal_from_handlers=False,): 137 | """search for all node instances of this type and refresh them. Will be called automatically if .auto_upd_flags's are defined""" 138 | 139 | if (using_nodes is None): 140 | nodes = get_booster_nodes(by_idnames={cls.bl_idname},) 141 | else: nodes = [n for n in using_nodes if (n.bl_idname==cls.bl_idname)] 142 | 143 | for n in nodes: 144 | n.sync_out_values() 145 | 146 | return None 147 | 148 | #Per Node-Editor Children: 149 | #Respect _NG_ + _GN_/_SH_/_CP_ nomenclature 150 | 151 | class NODEBOOSTER_NG_GN_RenderInfo(Base, bpy.types.GeometryNodeCustomGroup): 152 | tree_type = "GeometryNodeTree" 153 | bl_idname = "GeometryNode" + Base.bl_idname 154 | 155 | class NODEBOOSTER_NG_SH_RenderInfo(Base, bpy.types.ShaderNodeCustomGroup): 156 | tree_type = "ShaderNodeTree" 157 | bl_idname = "ShaderNode" + Base.bl_idname 158 | 159 | class NODEBOOSTER_NG_CP_RenderInfo(Base, bpy.types.CompositorNodeCustomGroup): 160 | tree_type = "CompositorNodeTree" 161 | bl_idname = "CompositorNode" + Base.bl_idname -------------------------------------------------------------------------------- /operators/drawframes.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | # TODO Improvements: 6 | # - would be nice to also support add frames to another frames 7 | # - would be nice so the frame don't wrap around the selection as an option. 'fixed frames' option 8 | 9 | 10 | import bpy 11 | 12 | from datetime import datetime 13 | 14 | from ..utils.node_utils import get_node_absolute_location 15 | from ..utils.draw_utils import ensure_mouse_cursor 16 | 17 | 18 | def get_nodes_in_frame_box(frame, nodes, frame_support=True,): 19 | """search node that can potentially be inside this frame created box""" 20 | 21 | bounds_left = frame.location.x 22 | bounds_right = frame.location.x + frame.dimensions.x 23 | bounds_top = frame.location.y 24 | bounds_bottom = frame.location.y - frame.dimensions.y 25 | 26 | for n in nodes: 27 | if ((n==frame) or (n.parent==frame)): 28 | continue 29 | if (n.type=="FRAME"): 30 | continue 31 | 32 | x, y = get_node_absolute_location(n) 33 | if (bounds_left <= x <= bounds_right) and \ 34 | (bounds_top >= y >= bounds_bottom): 35 | yield n 36 | 37 | 38 | class NODEBOOSTER_OT_draw_frame(bpy.types.Operator): 39 | """modal operator to easily draw frames by keep pressing the J key""" 40 | 41 | bl_idname = "nodebooster.draw_frame" 42 | bl_label = "Draw Frames" 43 | bl_options = {'REGISTER'} 44 | 45 | def __init__(self, *args, **kwargs): 46 | """get var user selection""" 47 | 48 | super().__init__(*args, **kwargs) 49 | 50 | #We only store blender data here when the operator is active, we should be totally fine! 51 | self.node_tree = None 52 | self.boxf = None 53 | self.old = (0,0) 54 | self.timer = None 55 | self.init_time = None 56 | self.selframerate = 0.350 #selection refreshrate in s, const 57 | 58 | @classmethod 59 | def poll(cls, context): 60 | return (context.space_data.type=='NODE_EDITOR') and (context.space_data.node_tree is not None) 61 | 62 | def invoke(self, context, event): 63 | 64 | ng = context.space_data.edit_tree 65 | self.node_tree = ng 66 | 67 | ensure_mouse_cursor(context, event) 68 | self.old = context.space_data.cursor_location.copy() 69 | 70 | boxf = ng.nodes.new("NodeFrame") 71 | self.boxf = boxf 72 | boxf.bl_width_min = boxf.bl_height_min = 20 73 | boxf.width = boxf.height = 0 74 | boxf.select = False 75 | boxf.location = self.old 76 | 77 | sett_scene = context.scene.nodebooster 78 | boxf.use_custom_color = sett_scene.frame_use_custom_color 79 | boxf.color = sett_scene.frame_color 80 | boxf.label = sett_scene.frame_label 81 | boxf.label_size = sett_scene.frame_label_size 82 | 83 | #start timer, needed to regulate a function refresh rate 84 | self.timer = context.window_manager.event_timer_add(self.selframerate, window=context.window) 85 | self.init_time = datetime.now() 86 | 87 | #Write status bar 88 | context.workspace.status_text_set_internal("Confirm: 'Release' 'Left-Click' | Cancel: 'Right-Click'") 89 | 90 | #start modal 91 | context.window_manager.modal_handler_add(self) 92 | 93 | return {'RUNNING_MODAL'} 94 | 95 | def modal(self, context, event): 96 | 97 | try: 98 | # context.area.tag_redraw() 99 | 100 | #if user confirm: 101 | if ((event.value=="RELEASE") or (event.type=="LEFTMOUSE")): 102 | return self.confirm(context) 103 | 104 | #if user cancel: 105 | elif event.type in ("ESC","RIGHTMOUSE"): 106 | return self.cancel(context) 107 | 108 | #only start if user is pressing a bit longer 109 | time_diff = datetime.now()-self.init_time 110 | if time_diff.total_seconds()<0.150: 111 | return {'RUNNING_MODAL'} 112 | 113 | #else, adjust frame location/width/height: 114 | 115 | #else recalculate position & frame dimensions 116 | ensure_mouse_cursor(context, event) 117 | new = context.space_data.cursor_location 118 | old = self.old 119 | 120 | #new y above init y 121 | if (old.y<=new.y): 122 | self.boxf.location.y = new.y 123 | self.boxf.height = (new.y-old.y) 124 | else: self.boxf.height = (old.y-new.y) 125 | 126 | #same principle as above for width 127 | if (old.x>=new.x): 128 | self.boxf.location.x = new.x 129 | self.boxf.width = (old.x-new.x) 130 | else: self.boxf.width = (new.x-old.x) 131 | 132 | #dynamic selection: 133 | 134 | #enable every 100ms, too slow for python.. 135 | if (event.type != 'TIMER'): 136 | return {'RUNNING_MODAL'} 137 | 138 | #show user a preview off the future node by selecting them 139 | for n in self.node_tree.nodes: 140 | n.select = False 141 | for n in get_nodes_in_frame_box(self.boxf,self.node_tree.nodes): 142 | n.select = True 143 | continue 144 | 145 | except Exception as e: 146 | print(e) 147 | self.report({'ERROR'},"An Error Occured during DrawFrame modal") 148 | return self.cancel(context) 149 | 150 | return {'RUNNING_MODAL'} 151 | 152 | def confirm(self, context): 153 | 154 | #if box is too small, just cancel 155 | if (self.boxf.dimensions.x <30 and self.boxf.dimensions.y <30): 156 | self.cancel(context) 157 | return {'CANCELLED'} 158 | 159 | nodes = list(get_nodes_in_frame_box(self.boxf,self.node_tree.nodes)) 160 | 161 | #add the nodes 162 | if (nodes): 163 | for n in nodes: 164 | n.parent = self.boxf 165 | continue 166 | self.node_tree.nodes.active = self.boxf 167 | self.boxf.select = True 168 | 169 | self.cleanup(context) 170 | return {'FINISHED'} 171 | 172 | def cancel(self, context): 173 | 174 | self.node_tree.nodes.remove(self.boxf) 175 | 176 | self.cleanup(context) 177 | return {'CANCELLED'} 178 | 179 | def cleanup(self,context): 180 | 181 | for n in self.node_tree.nodes: 182 | n.select = False 183 | 184 | # context.area.tag_redraw() 185 | context.window_manager.event_timer_remove(self.timer) 186 | 187 | #cleanup status bar 188 | context.workspace.status_text_set_internal(None) 189 | 190 | return None 191 | -------------------------------------------------------------------------------- /customnodes/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | from .sockets.custom_sockets import ( 7 | NODEBOOSTER_SK_Interpolation, 8 | NODEBOOSTER_ND_CustomSocketUtility, 9 | ) 10 | from . camerainfo import ( 11 | NODEBOOSTER_NG_GN_CameraInfo, 12 | NODEBOOSTER_NG_SH_CameraInfo, 13 | NODEBOOSTER_NG_CP_CameraInfo, 14 | ) 15 | from . lightinfo import ( 16 | NODEBOOSTER_NG_GN_LightInfo, 17 | NODEBOOSTER_NG_SH_LightInfo, 18 | NODEBOOSTER_NG_CP_LightInfo, 19 | ) 20 | from . sceneinfo import ( 21 | NODEBOOSTER_NG_GN_SceneInfo, 22 | NODEBOOSTER_NG_SH_SceneInfo, 23 | NODEBOOSTER_NG_CP_SceneInfo, 24 | ) 25 | from . renderinfo import ( 26 | NODEBOOSTER_NG_GN_RenderInfo, 27 | NODEBOOSTER_NG_SH_RenderInfo, 28 | NODEBOOSTER_NG_CP_RenderInfo, 29 | ) 30 | from .rnainfo import ( 31 | NODEBOOSTER_NG_GN_RNAInfo, 32 | NODEBOOSTER_NG_SH_RNAInfo, 33 | NODEBOOSTER_NG_CP_RNAInfo, 34 | ) 35 | from . isrenderedview import ( 36 | NODEBOOSTER_NG_GN_IsRenderedView, 37 | ) 38 | from . sequencervolume import ( 39 | NODEBOOSTER_NG_GN_SequencerSound, 40 | NODEBOOSTER_NG_SH_SequencerSound, 41 | NODEBOOSTER_NG_CP_SequencerSound, 42 | ) 43 | from . mathexpression import ( 44 | NODEBOOSTER_NG_GN_MathExpression, 45 | NODEBOOSTER_NG_SH_MathExpression, 46 | NODEBOOSTER_NG_CP_MathExpression, 47 | ) 48 | from . pyexpression import ( 49 | NODEBOOSTER_NG_GN_PyExpression, 50 | NODEBOOSTER_NG_SH_PyExpression, 51 | NODEBOOSTER_NG_CP_PyExpression, 52 | ) 53 | from . pynexscript import ( 54 | NODEBOOSTER_NG_GN_PyNexScript, 55 | NODEBOOSTER_NG_SH_PyNexScript, 56 | NODEBOOSTER_NG_CP_PyNexScript, 57 | ) 58 | from . keyboardinput import ( 59 | NODEBOOSTER_NG_GN_KeyboardAndMouse, 60 | NODEBOOSTER_NG_SH_KeyboardAndMouse, 61 | NODEBOOSTER_NG_CP_KeyboardAndMouse, 62 | ) 63 | from . controllerinput import ( 64 | NODEBOOSTER_NG_GN_XboxPadInput, 65 | NODEBOOSTER_NG_SH_XboxPadInput, 66 | NODEBOOSTER_NG_CP_XboxPadInput, 67 | ) 68 | from . objectvelocity import ( 69 | NODEBOOSTER_NG_GN_ObjectVelocity, 70 | NODEBOOSTER_NG_SH_ObjectVelocity, 71 | NODEBOOSTER_NG_CP_ObjectVelocity, 72 | ) 73 | from . interpolation.interpolationinput import ( 74 | NODEBOOSTER_OT_interpolation_input_update, 75 | NODEBOOSTER_NG_GN_InterpolationInput, 76 | NODEBOOSTER_NG_SH_InterpolationInput, 77 | NODEBOOSTER_NG_CP_InterpolationInput, 78 | ) 79 | from . interpolation.interpolationmap import ( 80 | NODEBOOSTER_NG_GN_InterpolationMap, 81 | NODEBOOSTER_NG_SH_InterpolationMap, 82 | NODEBOOSTER_NG_CP_InterpolationMap, 83 | ) 84 | from . interpolation.interpolationremap import ( 85 | NODEBOOSTER_NG_GN_InterpolationRemap, 86 | NODEBOOSTER_NG_SH_InterpolationRemap, 87 | NODEBOOSTER_NG_CP_InterpolationRemap, 88 | ) 89 | from .interpolation.spline2dpreview import ( 90 | NODEBOOSTER_PT_2DCurvePreviewOptions, 91 | NODEBOOSTER_ND_2DCurvePreview, 92 | ) 93 | from . interpolation.interpolationloop import ( 94 | NODEBOOSTER_ND_InterpolationLoop, 95 | ) 96 | from . interpolation.spline2dinput import ( 97 | NODEBOOSTER_ND_2DCurveInput, 98 | ) 99 | from . interpolation.spline2dsubd import ( 100 | NODEBOOSTER_ND_2DCurveSubdiv, 101 | ) 102 | from . interpolation.spline2dextend import ( 103 | NODEBOOSTER_ND_2DCurveExtend, 104 | ) 105 | from . interpolation.spline2dmix import ( 106 | NODEBOOSTER_ND_2DCurvesMix, 107 | ) 108 | from . interpolation.spline2dmonotonic import ( 109 | NODEBOOSTER_ND_EnsureMonotonicity, 110 | ) 111 | from . colorpalette import ( 112 | NODEBOOSTER_NG_GN_ColorPalette, 113 | NODEBOOSTER_NG_SH_ColorPalette, 114 | NODEBOOSTER_NG_CP_ColorPalette, 115 | ) 116 | 117 | # for registration 118 | classes = ( 119 | NODEBOOSTER_SK_Interpolation, 120 | NODEBOOSTER_ND_CustomSocketUtility, 121 | NODEBOOSTER_NG_GN_CameraInfo, 122 | NODEBOOSTER_NG_SH_CameraInfo, 123 | NODEBOOSTER_NG_CP_CameraInfo, 124 | NODEBOOSTER_NG_GN_LightInfo, 125 | NODEBOOSTER_NG_SH_LightInfo, 126 | NODEBOOSTER_NG_CP_LightInfo, 127 | NODEBOOSTER_NG_GN_SceneInfo, 128 | NODEBOOSTER_NG_SH_SceneInfo, 129 | NODEBOOSTER_NG_CP_SceneInfo, 130 | NODEBOOSTER_NG_GN_RenderInfo, 131 | NODEBOOSTER_NG_SH_RenderInfo, 132 | NODEBOOSTER_NG_CP_RenderInfo, 133 | NODEBOOSTER_NG_GN_RNAInfo, 134 | NODEBOOSTER_NG_SH_RNAInfo, 135 | NODEBOOSTER_NG_CP_RNAInfo, 136 | NODEBOOSTER_NG_GN_IsRenderedView, 137 | NODEBOOSTER_NG_GN_SequencerSound, 138 | NODEBOOSTER_NG_SH_SequencerSound, 139 | NODEBOOSTER_NG_CP_SequencerSound, 140 | NODEBOOSTER_NG_GN_MathExpression, 141 | NODEBOOSTER_NG_SH_MathExpression, 142 | NODEBOOSTER_NG_CP_MathExpression, 143 | NODEBOOSTER_NG_GN_PyExpression, 144 | NODEBOOSTER_NG_SH_PyExpression, 145 | NODEBOOSTER_NG_CP_PyExpression, 146 | NODEBOOSTER_NG_GN_PyNexScript, 147 | NODEBOOSTER_NG_SH_PyNexScript, 148 | NODEBOOSTER_NG_CP_PyNexScript, 149 | NODEBOOSTER_NG_GN_KeyboardAndMouse, 150 | NODEBOOSTER_NG_SH_KeyboardAndMouse, 151 | NODEBOOSTER_NG_CP_KeyboardAndMouse, 152 | NODEBOOSTER_NG_GN_XboxPadInput, 153 | NODEBOOSTER_NG_SH_XboxPadInput, 154 | NODEBOOSTER_NG_CP_XboxPadInput, 155 | NODEBOOSTER_NG_GN_ObjectVelocity, 156 | NODEBOOSTER_NG_SH_ObjectVelocity, 157 | NODEBOOSTER_NG_CP_ObjectVelocity, 158 | NODEBOOSTER_NG_GN_InterpolationInput, 159 | NODEBOOSTER_NG_SH_InterpolationInput, 160 | NODEBOOSTER_NG_CP_InterpolationInput, 161 | NODEBOOSTER_NG_GN_InterpolationMap, 162 | NODEBOOSTER_NG_SH_InterpolationMap, 163 | NODEBOOSTER_NG_CP_InterpolationMap, 164 | NODEBOOSTER_NG_GN_InterpolationRemap, 165 | NODEBOOSTER_NG_SH_InterpolationRemap, 166 | NODEBOOSTER_NG_CP_InterpolationRemap, 167 | NODEBOOSTER_ND_2DCurvePreview, 168 | NODEBOOSTER_PT_2DCurvePreviewOptions, 169 | NODEBOOSTER_OT_interpolation_input_update, 170 | NODEBOOSTER_ND_2DCurveInput, 171 | NODEBOOSTER_ND_2DCurveSubdiv, 172 | NODEBOOSTER_ND_2DCurveExtend, 173 | NODEBOOSTER_ND_2DCurvesMix, 174 | NODEBOOSTER_ND_EnsureMonotonicity, 175 | NODEBOOSTER_ND_InterpolationLoop, 176 | NODEBOOSTER_NG_GN_ColorPalette, 177 | NODEBOOSTER_NG_SH_ColorPalette, 178 | NODEBOOSTER_NG_CP_ColorPalette, 179 | ) 180 | 181 | #for utility. handlers.py module will use this list. 182 | allcustomnodes = tuple(cls for cls in classes if 183 | (('_NG_' in cls.__name__) or 184 | ('_ND_' in cls.__name__)) ) -------------------------------------------------------------------------------- /customnodes/interpolation/spline2dinput.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | import bpy 6 | import os 7 | import numpy as np 8 | 9 | from ...utils.bezier2d_utils import reverseengineer_curvemapping_to_bezsegs 10 | from ...utils.str_utils import word_wrap # Added for draw_panel 11 | from ...utils.node_utils import ( 12 | send_refresh_signal, 13 | cache_booster_nodes_parent_tree, 14 | ) 15 | 16 | 17 | class NODEBOOSTER_ND_2DCurveInput(bpy.types.Node): 18 | 19 | bl_idname = "NodeBooster2DCurveInput" 20 | bl_label = "2D Curve Input" 21 | bl_description = "Interpret a spline of a 3D curve object into a 2D curve depending on the chosen axis." 22 | nb_menu_path = ['NodeBooster','Experimental','Interpolation',bl_label,] #path in add menu 23 | auto_upd_flags = {'NONE',} 24 | tree_type = "AnyNodeTree" 25 | 26 | evaluator_properties = {'INTERPOLATION_NODE',} 27 | 28 | curve_object: bpy.props.PointerProperty( 29 | type=bpy.types.Object, 30 | name="Curve Object", 31 | description="Select the 3D Curve object to sample", 32 | poll=lambda self, object: object.type == 'CURVE', 33 | update=lambda self, context: self.update_trigger() 34 | ) 35 | axis_source: bpy.props.EnumProperty( 36 | items=[ 37 | ('X', 'X', 'Use world X axis for Y value'), 38 | ('Y', 'Y', 'Use world Y axis for Y value'), 39 | ('Z', 'Z', 'Use world Z axis for Y value') 40 | ], 41 | name="Source Axis", 42 | description="Which axis of the 3D curve points maps to the 2D curve's Y value", 43 | default='Z', 44 | update=lambda self, context: self.update_trigger() 45 | ) 46 | spline_index: bpy.props.IntProperty( 47 | name="Spline Index", 48 | description="Index of the spline to use within the curve object", 49 | default=0, 50 | min=0, 51 | update=lambda self, context: self.update_trigger() 52 | ) 53 | space: bpy.props.EnumProperty( 54 | name="Space", 55 | description="Evaluate curve points in Local (Original) or World (Relative) space", 56 | items=[ 57 | ('LOCAL', 'Original', 'Use curve points relative to the object origin'), 58 | ('WORLD', 'Relative', 'Use curve points in world space') 59 | ], 60 | default='LOCAL', 61 | update=lambda self, context: self.update_trigger() 62 | ) 63 | 64 | @classmethod 65 | def poll(cls, context): 66 | return True 67 | 68 | def init(self, context): 69 | """this fct run when appending the node for the first time""" 70 | 71 | self.outputs.new('NodeBoosterCustomSocketInterpolation', "2D Curve") 72 | 73 | self.width = 150 74 | 75 | return None 76 | 77 | def copy(self, node): 78 | """fct run when dupplicating the node""" 79 | 80 | return None 81 | 82 | def update(self): 83 | """generic update function""" 84 | 85 | cache_booster_nodes_parent_tree(self.id_data) 86 | 87 | return None 88 | 89 | def update_trigger(self,): 90 | """send an update trigger to the whole node_tree""" 91 | 92 | send_refresh_signal(self.outputs[0]) 93 | 94 | return None 95 | 96 | def draw_label(self,): 97 | """node label""" 98 | if (self.label==''): 99 | return '2D Curve' 100 | return self.label 101 | 102 | def draw_buttons(self, context, layout): 103 | 104 | layout.prop(self, "curve_object", text="") 105 | 106 | # Check if spline_index is valid and set alert if not 107 | alert_spline = False 108 | curve_obj = self.curve_object 109 | if curve_obj and curve_obj.type == 'CURVE' and curve_obj.data.splines: 110 | if self.spline_index >= len(curve_obj.data.splines): 111 | alert_spline = True 112 | elif curve_obj and curve_obj.type == 'CURVE' and not curve_obj.data.splines: 113 | alert_spline = True # No splines exist, so index 0 is invalid 114 | 115 | col = layout.column() 116 | col.use_property_split = True 117 | col.use_property_decorate = False 118 | coll = col.column(align=True) 119 | coll.alert = alert_spline # Set alert state for the col 120 | coll.prop(self, "spline_index", text="Spline") 121 | col.prop(self, "axis_source", text="Axis") 122 | 123 | row = layout.row(align=True) 124 | row.prop(self, "space", expand=True) 125 | 126 | def draw_panel(self, layout, context): 127 | pass 128 | 129 | def evaluator(self, socket_output)->list: 130 | 131 | curve_obj = self.curve_object 132 | if (not curve_obj or curve_obj.type != 'CURVE'): 133 | return None 134 | 135 | curve_data = curve_obj.data 136 | if (not curve_data.splines or self.spline_index >= len(curve_data.splines)): 137 | return None 138 | 139 | spline = curve_data.splines[self.spline_index] 140 | 141 | if (spline.type != 'BEZIER' or not spline.bezier_points): 142 | print(f"Warning: Spline {self.spline_index} in '{curve_obj.name}' is not a Bezier spline. Cannot extract handle data.") 143 | return None 144 | 145 | bezier_points = spline.bezier_points 146 | if (len(bezier_points) < 2): 147 | return None 148 | 149 | # Get the transformation matrix based on selected space 150 | transform_matrix = curve_obj.matrix_world if (self.space == 'WORLD') else None 151 | 152 | # Determine which axes map to 2D X and Y 153 | match self.axis_source: 154 | case 'X': idx_2d_x, idx_2d_y = 1, 2 # Use World Y, Z 155 | case 'Y': idx_2d_x, idx_2d_y = 0, 2 # Use World X, Z 156 | case 'Z': idx_2d_x, idx_2d_y = 0, 1 # Use World X, Y 157 | 158 | # Build Bezier Segment Array Directly 159 | bezsegs_list = [] 160 | num_segments = len(bezier_points) - 1 161 | for i in range(num_segments): 162 | bp_i = bezier_points[i] 163 | bp_i1 = bezier_points[i+1] 164 | 165 | # Get the 4 relevant 3D world-space points for the segment 166 | P0_3d = bp_i.co 167 | P1_3d = bp_i.handle_right 168 | P2_3d = bp_i1.handle_left 169 | P3_3d = bp_i1.co 170 | 171 | # Apply transform if needed 172 | if (transform_matrix): 173 | P0_3d = transform_matrix @ P0_3d 174 | P1_3d = transform_matrix @ P1_3d 175 | P2_3d = transform_matrix @ P2_3d 176 | P3_3d = transform_matrix @ P3_3d 177 | 178 | # Extract the chosen 2 axes for each point 179 | P0 = np.array([P0_3d[idx_2d_x], P0_3d[idx_2d_y]]) 180 | P1 = np.array([P1_3d[idx_2d_x], P1_3d[idx_2d_y]]) 181 | P2 = np.array([P2_3d[idx_2d_x], P2_3d[idx_2d_y]]) 182 | P3 = np.array([P3_3d[idx_2d_x], P3_3d[idx_2d_y]]) 183 | 184 | segment = np.concatenate((P0, P1, P2, P3)) 185 | bezsegs_list.append(segment) 186 | 187 | continue 188 | 189 | if (not bezsegs_list): 190 | return None 191 | return np.array(bezsegs_list, dtype=float) 192 | 193 | -------------------------------------------------------------------------------- /customnodes/interpolation/spline2dsubd.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | import os 8 | import numpy as np 9 | 10 | from ... import get_addon_prefs 11 | from ...utils.str_utils import word_wrap 12 | from ...utils.bezier2d_utils import casteljau_subdiv_bezsegs, cut_bezsegs, subdiv_project_bezsegs 13 | from ..evaluator import evaluate_upstream_value 14 | from ...utils.node_utils import ( 15 | send_refresh_signal, 16 | cache_booster_nodes_parent_tree, 17 | ) 18 | 19 | # ooooo ooo .o8 20 | # `888b. `8' "888 21 | # 8 `88b. 8 .ooooo. .oooo888 .ooooo. 22 | # 8 `88b. 8 d88' `88b d88' `888 d88' `88b 23 | # 8 `88b.8 888 888 888 888 888ooo888 24 | # 8 `888 888 888 888 888 888 .o 25 | # o8o `8 `Y8bod8P' `Y8bod88P" `Y8bod8P' 26 | 27 | 28 | class NODEBOOSTER_ND_2DCurveSubdiv(bpy.types.Node): 29 | 30 | bl_idname = "NodeBooster2DCurveSubdiv" 31 | bl_label = "Subdivide 2D Curve" 32 | bl_description = """Subdivide a 2D curve. Either by a specific number of subdivisions, or at a specific X location, or by projecting the curve along the reference curve tangent space.""" 33 | nb_menu_path = ['NodeBooster','Experimental','Interpolation',bl_label,] #path in add menu 34 | auto_upd_flags = {'NONE',} 35 | tree_type = "AnyNodeTree" 36 | 37 | evaluator_properties = {'INTERPOLATION_NODE',} 38 | 39 | mode : bpy.props.EnumProperty( 40 | name="Mode", 41 | description="Subdivide a 2D curve with the given methods", 42 | items=(('CUT', 'Cut', 'Cut the curve at the X location'), 43 | ('SUBDIV', 'Subdivide', 'Subdivide the curve at N subdivisions levels'), 44 | ('PROJECT', 'Project', "Subdivide the chosen curve for every projected anchor of the reference curve along their relative tangent distance"), 45 | ), 46 | default='SUBDIV', 47 | update=lambda self, context: self.update_trigger() 48 | ) 49 | subdiv_level : bpy.props.IntProperty( 50 | name="Level", 51 | description="The number of subdivisions to perform", 52 | default=1, 53 | min=1, 54 | soft_max=3, 55 | update=lambda self, context: self.update_trigger() 56 | ) 57 | xloc : bpy.props.FloatProperty( 58 | name="X Location", 59 | description="The X location to cut the curve at", 60 | default=0.0, 61 | soft_min=-2.0, 62 | soft_max=2.0, 63 | update=lambda self, context: self.update_trigger() 64 | ) 65 | 66 | @classmethod 67 | def poll(cls, context): 68 | """mandatory poll""" 69 | return True 70 | 71 | def init(self, context): 72 | """this fct run when appending the node for the first time""" 73 | 74 | self.inputs.new('NodeBoosterCustomSocketInterpolation', "2D Curve") 75 | self.outputs.new('NodeBoosterCustomSocketInterpolation', "2D Curve") 76 | 77 | self.inputs.new('NodeBoosterCustomSocketInterpolation', "Reference Curve") 78 | self.inputs[1].enabled = False 79 | 80 | self.width = 145 81 | 82 | return None 83 | 84 | def copy(self, node): 85 | """fct run when dupplicating the node""" 86 | 87 | return None 88 | 89 | def update(self): 90 | """generic update function""" 91 | 92 | cache_booster_nodes_parent_tree(self.id_data) 93 | 94 | return None 95 | 96 | def draw_label(self,): 97 | """node label""" 98 | if (self.label==''): 99 | match self.mode: 100 | case 'SUBDIV': return 'Subdivide' 101 | case 'CUT': return 'Subdivide Cut' 102 | case 'PROJECT': return 'Subdivide Project' 103 | return self.label 104 | 105 | def update_trigger(self,): 106 | """send an update trigger to the whole node_tree""" 107 | 108 | if (self.mode in {'SUBDIV', 'CUT'}): 109 | self.inputs[0].name = '2D Curve' 110 | self.inputs[1].enabled = False 111 | elif (self.mode == 'PROJECT'): 112 | self.inputs[0].name = 'To Subdivide' 113 | self.inputs[1].enabled = True 114 | 115 | send_refresh_signal(self.outputs[0]) 116 | 117 | return None 118 | 119 | def evaluator(self, socket_output)->None: 120 | """evaluator the node required for the output evaluator""" 121 | 122 | if (self.mode in {'SUBDIV', 'CUT'}): 123 | 124 | val = evaluate_upstream_value(self.inputs[0], 125 | match_evaluator_properties={'INTERPOLATION_NODE',}, 126 | set_link_invalid=True, 127 | ) 128 | 129 | if (val is None): 130 | return None 131 | 132 | match self.mode: 133 | 134 | case 'SUBDIV': 135 | for _ in range(self.subdiv_level): 136 | t_map = np.full(len(val), 0.5) 137 | val = casteljau_subdiv_bezsegs(val, t_map) 138 | continue 139 | return val 140 | 141 | case 'CUT': 142 | return cut_bezsegs(val, self.xloc, sampling_rate=100,) 143 | 144 | elif (self.mode == 'PROJECT'): 145 | 146 | val1 = evaluate_upstream_value(self.inputs[0], 147 | match_evaluator_properties={'INTERPOLATION_NODE',}, 148 | set_link_invalid=True, 149 | ) 150 | val2 = evaluate_upstream_value(self.inputs[1], 151 | match_evaluator_properties={'INTERPOLATION_NODE',}, 152 | set_link_invalid=True, 153 | ) 154 | if (val1 is None) or (val2 is None): 155 | return None 156 | 157 | return subdiv_project_bezsegs(val1, val2) 158 | 159 | return None 160 | 161 | def draw_buttons(self, context, layout): 162 | """node interface drawing""" 163 | 164 | layout.prop(self, "mode", text="",) 165 | match self.mode: 166 | case 'CUT': 167 | layout.prop(self, "xloc") 168 | case 'SUBDIV': 169 | layout.prop(self, "subdiv_level") 170 | case 'PROJECT': 171 | pass 172 | 173 | return None 174 | 175 | def draw_panel(self, layout, context): 176 | """draw in the nodebooster N panel 'Active Node'""" 177 | 178 | n = self 179 | 180 | header, panel = layout.panel("doc_panelid", default_closed=True,) 181 | header.label(text="Documentation",) 182 | if (panel): 183 | word_wrap(layout=panel, alert=False, active=True, max_char='auto', 184 | char_auto_sidepadding=0.9, context=context, string=n.bl_description, 185 | ) 186 | panel.operator("wm.url_open", text="Documentation",).url = "https://blenderartists.org/t/node-booster-extending-blender-node-editors" 187 | 188 | # header, panel = layout.panel("dev_panelid", default_closed=True,) 189 | # header.label(text="Development",) 190 | # if (panel): 191 | # panel.active = False 192 | 193 | # col = panel.column(align=True) 194 | # col.label(text="NodeTree:") 195 | # col.template_ID(n, "node_tree") 196 | 197 | return None 198 | -------------------------------------------------------------------------------- /nex/pytonode.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | 8 | bpy_array = bpy.types.bpy_prop_array 9 | from mathutils import Color, Euler, Matrix, Quaternion, Vector 10 | from ..utils.fct_utils import ColorRGBA 11 | 12 | 13 | def py_to_Vec3(value): 14 | match value: 15 | 16 | case Vector(): 17 | if (len(value)!=3): raise TypeError(f"Vector({value[:]}) should have 3 elements for 'SocketVector' compatibility.") 18 | return value 19 | 20 | case ColorRGBA() | Color(): 21 | return Vector((value[0],value[1],value[2])) 22 | 23 | case list() | set() | tuple() | bpy_array(): 24 | if (len(value)!=3): raise TypeError(f"{type(value).__name__}({value[:]}) should have 3 float elements for 'SocketVector' compatibility.") 25 | return Vector(value) 26 | 27 | case int() | float() | bool(): 28 | v = float(value) 29 | return Vector((v,v,v)) 30 | 31 | case _: raise TypeError(f"type {type(value).__name__}({value[:]}) is not compatible with 'SocketVector'.") 32 | 33 | def py_to_Quat4(value): 34 | match value: 35 | 36 | case Quaternion(): 37 | return value 38 | 39 | case ColorRGBA(): 40 | return Quaternion(value) 41 | 42 | case list() | set() | tuple() | bpy_array() | Vector() | Color(): 43 | if len(value) not in (3, 4): 44 | raise TypeError(f"{type(value).__name__}({list(value)}) should have 3 or 4 elements for 'ColorRGBA' compatibility.") 45 | if (len(value)==3): 46 | return Quaternion((value[0], value[1], value[2], 1.0)) 47 | else: return Quaternion((value[0], value[1], value[2], value[3],)) 48 | 49 | case int() | float() | bool(): 50 | v = float(value) 51 | return Quaternion((v,v,v,v)) 52 | 53 | case _: 54 | extra = value[:] if hasattr(value, '__getitem__') else value 55 | raise TypeError(f"type {type(value).__name__}({extra}) is not compatible with 'ColorRGBA'.") 56 | 57 | def py_to_RGBA(value): 58 | match value: 59 | 60 | case ColorRGBA(): 61 | return value 62 | 63 | case Color(): 64 | return ColorRGBA(value[0], value[1], value[2], 1.0) 65 | 66 | case list() | set() | tuple() | bpy_array() | Vector(): 67 | if len(value) not in (3, 4): 68 | raise TypeError(f"{type(value).__name__}({list(value)}) should have 3 or 4 elements for 'ColorRGBA' compatibility.") 69 | if (len(value)==3): 70 | return ColorRGBA(value[0], value[1], value[2], 1.0) 71 | else: return ColorRGBA(value[0], value[1], value[2], value[3]) 72 | 73 | case int() | float() | bool(): 74 | v = float(value) 75 | return ColorRGBA(v,v,v,1.0) 76 | 77 | case _: 78 | extra = value[:] if hasattr(value, '__getitem__') else value 79 | raise TypeError(f"type {type(value).__name__}({extra}) is not compatible with 'ColorRGBA'.") 80 | 81 | def py_to_Mtx16(value): 82 | match value: 83 | 84 | case Matrix(): 85 | rowflatten = [v for row in value for v in row] 86 | if (len(value)!=4): raise TypeError(f"type Matrix({rowflatten[:]}) type should have 4 rows or 4 elements of float values for 'SocketMatrix' compatibility.") 87 | if (len(rowflatten)!=16): raise TypeError(f"type Matrix({rowflatten[:]}) should contain a total of 16 elements for 'SocketMatrix' compatibility. {len(rowflatten)} found.") 88 | return value 89 | 90 | case list() | set() | tuple(): 91 | if (len(value)!=16): raise TypeError(f"{type(value).__name__}({value[:]}) should contain 16 float elements for 'SocketMatrix' compatibility. {len(value)} elements found.") 92 | rows = [value[i*4:(i+1)*4] for i in range(4)] 93 | return Matrix(rows) 94 | 95 | case _: raise TypeError(f"Cannot convert type {type(value).__name__}({value[:]}) to Matrix().") 96 | 97 | def py_to_Sockdata(value, return_value_only=False): 98 | """Convert a given python variable into data we can use to create and assign sockets default value, type and label.""" 99 | 100 | matrix_label = '' 101 | 102 | #we sanatize out possible types depending on their length 103 | if (type(value) in {tuple, list, set, Vector, Euler, bpy_array}): 104 | 105 | if type(value) in {tuple, list, set}: 106 | if any('Nex' in type(e).__name__ for e in value) or any('Node' in type(e).__name__ for e in value): 107 | raise TypeError(f"Cannot assign a '{type(value).__name__}' containing a SocketTypes to a Socket default value.") 108 | 109 | value = list(value) 110 | n = len(value) 111 | 112 | match n: 113 | case 0: value = Vector((0,0,0)) 114 | case 1: value = Vector((value[0],0,0)) 115 | case 2: value = Vector((value[0],value[1],0)) 116 | case 3: value = Vector(value) 117 | case 4: value = ColorRGBA(*value) 118 | 119 | case _ if n <= 16: 120 | if (n != 16): 121 | matrix_label = f'List[{len(value)}]' 122 | nulmatrix = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 123 | value.extend(nulmatrix[len(value):]) 124 | flatTorows = [value[i*4:(i+1)*4] for i in range(4)] 125 | value = Matrix(flatTorows) 126 | 127 | case _: 128 | raise TypeError(f"Cannot assign a '{type(value).__name__}' of lenght {n} to a Socket default value.") 129 | 130 | # then we define the socket type string & the potential socket label 131 | match value: 132 | 133 | case bool(): 134 | repr_label = str(value) 135 | socket_type = 'NodeSocketBool' 136 | 137 | case int(): 138 | repr_label = str(value) 139 | socket_type = 'NodeSocketInt' 140 | 141 | case float(): 142 | repr_label = str(round(value,4)) 143 | socket_type = 'NodeSocketFloat' 144 | 145 | case str(): 146 | repr_label = '"'+value+'"' 147 | socket_type = 'NodeSocketString' 148 | 149 | case Vector(): 150 | repr_label = str(tuple(round(n,4) for n in value)) 151 | socket_type = 'NodeSocketVector' 152 | 153 | case Color(): 154 | value = ColorRGBA(*value,1) #add alpha channel 155 | repr_label = str(tuple(round(n,4) for n in value)) 156 | socket_type = 'NodeSocketColor' 157 | 158 | case ColorRGBA(): 159 | repr_label = str(tuple(round(n,4) for n in value)) 160 | socket_type = 'NodeSocketColor' 161 | 162 | case Quaternion(): 163 | repr_label = str(tuple(round(n,4) for n in value)) 164 | socket_type = 'NodeSocketRotation' 165 | 166 | case Matrix(): 167 | repr_label = "4x4Matrix" if (not matrix_label) else matrix_label 168 | socket_type = 'NodeSocketMatrix' 169 | 170 | case bpy.types.Object(): 171 | repr_label = f'D.objects["{value.name}"]' 172 | socket_type = 'NodeSocketObject' 173 | 174 | case bpy.types.Collection(): 175 | repr_label = f'D.collections["{value.name}"]' 176 | socket_type = 'NodeSocketCollection' 177 | 178 | case bpy.types.Material(): 179 | repr_label = f'D.materials["{value.name}"]' 180 | socket_type = 'NodeSocketMaterial' 181 | 182 | case bpy.types.Image(): 183 | repr_label = f'D.images["{value.name}"]' 184 | socket_type = 'NodeSocketImage' 185 | 186 | case _: 187 | raise TypeError(f"Assigning a '{type(value).__name__}' to a Socket default value is not possible.") 188 | 189 | if (return_value_only): 190 | return value 191 | return value, repr_label, socket_type 192 | -------------------------------------------------------------------------------- /customnodes/interpolation/interpolationinput.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | import os 8 | 9 | from ...__init__ import get_addon_prefs 10 | from ...utils.str_utils import word_wrap 11 | from ...utils.bezier2d_utils import reverseengineer_curvemapping_to_bezsegs 12 | from ...utils.node_utils import ( 13 | import_new_nodegroup, 14 | send_refresh_signal, 15 | cache_booster_nodes_parent_tree, 16 | ) 17 | 18 | # TODO 19 | # - IMPORTANT: Support hidden blender feature: 20 | # - Either the interpolation graph is in 'Extend extrapolated' or in 'Extend horizontal'... 21 | # need to support this.. how? 22 | # - Could simply create two new segment if in horizontal mode, that's it would work. 23 | # - Lifeupdate feature: Add a EnumOperator that apply various presets. 24 | # - No update button. Automatic update on graph interaction with graph. 25 | # How? which callback? will msgbus work for once? ehm.. 26 | # Could store a cache of numpy hash in the node, and check if new graph val is same. 27 | 28 | class NODEBOOSTER_OT_interpolation_input_update(bpy.types.Operator): 29 | """Update the interpolation output. Cheap trick: we unlink and relink""" 30 | 31 | bl_idname = "nodebooster.update_interpolation" 32 | bl_label = "Update Interpolation" 33 | bl_description = "Apply the new graph values" 34 | bl_options = {'REGISTER', 'INTERNAL'} 35 | 36 | node_name : bpy.props.StringProperty() 37 | 38 | def execute(self, context): 39 | context.space_data.edit_tree.nodes[self.node_name].update_trigger() 40 | return {'FINISHED'} 41 | 42 | # ooooo ooo .o8 43 | # `888b. `8' "888 44 | # 8 `88b. 8 .ooooo. .oooo888 .ooooo. 45 | # 8 `88b. 8 d88' `88b d88' `888 d88' `88b 46 | # 8 `88b.8 888 888 888 888 888ooo888 47 | # 8 `888 888 888 888 888 888 .o 48 | # o8o `8 `Y8bod8P' `Y8bod88P" `Y8bod8P' 49 | 50 | class Base(): 51 | 52 | bl_idname = "NodeBoosterInterpolationInput" 53 | bl_label = "Interpolation Input" 54 | bl_description = """Create a 2D curve from an interpolation curve mapping graph""" 55 | nb_menu_path = ['NodeBooster','Experimental','Interpolation',bl_label,] #path in add menu 56 | auto_upd_flags = {'NONE',} 57 | tree_type = "*ChildrenDefined*" 58 | 59 | evaluator_properties = {'INTERPOLATION_NODE',} 60 | 61 | graph_type : bpy.props.EnumProperty( 62 | name="Graph Type", 63 | description="Which kind of data do we process ?", 64 | items=[ 65 | ("float_mapping", "Unsigned Values", "Delimit your graph to unsigned values, ranging from 0 to 1"), 66 | ("vector_mapping", "Signed Values", "Delimit your graph to signed values, ranging from -1 to 1"), 67 | ], 68 | default="float_mapping", 69 | update=lambda self, context: self.update_trigger(), 70 | ) #item names are name of internal nodes as well. 71 | 72 | @classmethod 73 | def poll(cls, context): 74 | """mandatory poll""" 75 | return True 76 | 77 | def init(self, context): 78 | """this fct run when appending the node for the first time""" 79 | 80 | name = f".{self.bl_idname}" 81 | 82 | ng = bpy.data.node_groups.get(name) 83 | if (ng is None): 84 | 85 | # NOTE we cannot create a new ng with custom socket types for now unfortunately. 86 | # it's possible to do it with the link_drag operator on a socketcustom to the grey 87 | # input/output sockets of a ng, but not via the python API. 88 | # see notes in 'node_utils.create_ng_socket'. This solution is a workaround, hopefully, temporary.. 89 | blendfile = os.path.join(os.path.dirname(__file__), "interpolation_nodegroups.blend") 90 | ng = import_new_nodegroup(blendpath=blendfile, ngname=self.bl_idname,) 91 | 92 | # set the name of the ng 93 | ng.name = name 94 | 95 | 96 | ng = ng.copy() #always using a copy of the original ng 97 | self.node_tree = ng 98 | 99 | self.width = 240 100 | 101 | return None 102 | 103 | def copy(self, node): 104 | """fct run when dupplicating the node""" 105 | 106 | #NOTE: copy/paste can cause crashes, we use a timer to delay the action 107 | def delayed_copy(): 108 | self.node_tree = node.node_tree.copy() 109 | bpy.app.timers.register(delayed_copy, first_interval=0.01) 110 | 111 | return None 112 | 113 | def update(self): 114 | """generic update function""" 115 | 116 | cache_booster_nodes_parent_tree(self.id_data) 117 | 118 | return None 119 | 120 | def draw_label(self,): 121 | """node label""" 122 | if (self.label==''): 123 | return 'Interpolation' 124 | return self.label 125 | 126 | def draw_buttons(self, context, layout): 127 | """node interface drawing""" 128 | 129 | layout.prop(self, 'graph_type', text="") 130 | 131 | data = self.node_tree.nodes[self.graph_type] 132 | layout.template_curve_mapping(data, "mapping", type='NONE',) 133 | 134 | row = layout.row(align=True) 135 | row.scale_y = 1.2 136 | row.operator("nodebooster.update_interpolation", text="Apply",).node_name = self.name 137 | 138 | return None 139 | 140 | def draw_panel(self, layout, context): 141 | """draw in the nodebooster N panel 'Active Node'""" 142 | 143 | n = self 144 | 145 | header, panel = layout.panel("params_panelid", default_closed=False) 146 | header.label(text="Parameters") 147 | if panel: 148 | 149 | panel.prop(self, 'graph_type', text="") 150 | 151 | data = self.node_tree.nodes[self.graph_type] 152 | panel.template_curve_mapping(data, "mapping", type='NONE',) 153 | panel.operator("nodebooster.update_interpolation", text="Apply",).node_name = self.name 154 | panel.separator(factor=0.3) 155 | 156 | header, panel = layout.panel("doc_panelid", default_closed=True,) 157 | header.label(text="Documentation",) 158 | if (panel): 159 | 160 | word_wrap(layout=panel, alert=False, active=True, max_char='auto', 161 | char_auto_sidepadding=0.9, context=context, string=n.bl_description, 162 | ) 163 | panel.operator("wm.url_open", text="Documentation",).url = "https://blenderartists.org/t/node-booster-extending-blender-node-editors" 164 | 165 | header, panel = layout.panel("dev_panelid", default_closed=True,) 166 | header.label(text="Development",) 167 | if (panel): 168 | panel.active = False 169 | 170 | col = panel.column(align=True) 171 | col.label(text="NodeTree:") 172 | col.template_ID(n, "node_tree") 173 | 174 | return None 175 | 176 | def update_trigger(self,): 177 | """send an update trigger to the whole node_tree""" 178 | 179 | send_refresh_signal(self.outputs[0]) 180 | 181 | return None 182 | 183 | def evaluator(self, socket_output)->list: 184 | """evaluator the node required for the output evaluator""" 185 | 186 | # NOTE the evaluator works based on socket output passed in args. 187 | # but here, there's only one output..(simpler) 188 | 189 | result = reverseengineer_curvemapping_to_bezsegs(self.node_tree.nodes[self.graph_type].mapping.curves[0]) 190 | 191 | return result 192 | 193 | #Per Node-Editor Children: 194 | #Respect _NG_ + _GN_/_SH_/_CP_ nomenclature 195 | 196 | class NODEBOOSTER_NG_GN_InterpolationInput(Base, bpy.types.GeometryNodeCustomGroup): 197 | tree_type = "GeometryNodeTree" 198 | bl_idname = "GeometryNodeNodeBoosterInterpolationInput" 199 | 200 | class NODEBOOSTER_NG_SH_InterpolationInput(Base, bpy.types.ShaderNodeCustomGroup): 201 | tree_type = "ShaderNodeTree" 202 | bl_idname = "ShaderNodeNodeBoosterInterpolationInput" 203 | 204 | class NODEBOOSTER_NG_CP_InterpolationInput(Base, bpy.types.CompositorNodeCustomGroup): 205 | tree_type = "CompositorNodeTree" 206 | bl_idname = "CompositorNodeNodeBoosterInterpolationInput" 207 | -------------------------------------------------------------------------------- /operators/chamfer.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | 8 | import numpy 9 | from mathutils import Vector 10 | 11 | from ..utils.node_utils import get_node_absolute_location 12 | from ..utils.draw_utils import ensure_mouse_cursor 13 | 14 | 15 | def get_rr_links_info(n,mode): 16 | """get links information from given node, we do all this because we can't store socket object directly, too dangerous, cause bug as object pointer change in memory often""" 17 | 18 | links_info=[] 19 | is_input = (mode=='IN') 20 | 21 | sockets = n.inputs[0] if is_input else n.outputs[0] 22 | for i,l in enumerate(sockets.links): 23 | s = l.from_socket if is_input else l.to_socket 24 | dn = s.node 25 | 26 | #retrieve socket index 27 | sidx = None #should never be none 28 | nsocks = dn.outputs if is_input else dn.inputs 29 | for sidx,sout in enumerate(nsocks): 30 | if (sout==s): 31 | break 32 | 33 | info = (dn.name,s.name,sidx) 34 | links_info.append(info) 35 | continue 36 | 37 | return links_info 38 | 39 | 40 | def restore_links_to_original(n,ng,links_info,mode): 41 | """restore links from given info list""" 42 | 43 | is_input = (mode=='IN') 44 | 45 | for elem in links_info: 46 | 47 | nn, _, sidx = elem 48 | dn = ng.nodes.get(nn) 49 | s = dn.outputs[sidx] if is_input else dn.inputs[sidx] 50 | 51 | args = (s, n.inputs[0]) if is_input else (n.outputs[0], s,) 52 | ng.links.new(*args) 53 | 54 | continue 55 | 56 | return None 57 | 58 | 59 | class ChamferItem(): 60 | 61 | init_rr = "" #Initial Reroute Node. Storing names to avoid crash 62 | init_loc_local = (0,0) #Initial Local Location Vector. 63 | added_rr = "" #all added reroute, aka the reroute added before init rr. Storing names to avoid crash 64 | fromvec = (0,0) #downstream chamfer direction Vector 65 | tovec = (0,0) #upstream chamfer direction Vector 66 | 67 | 68 | class NODEBOOSTER_OT_chamfer(bpy.types.Operator): 69 | 70 | #NOTE not sure how real bevel algo works, but this is a 'naive' approach, we create a new vert, new edges 71 | # and moving their location from the origin origin point 72 | #NOTE that local/global space can be problematic. Here we are only working in local space, if parent. 73 | 74 | bl_idname = "nodebooster.chamfer" 75 | bl_label = "Reroute Chamfer" 76 | bl_options = {'REGISTER'} 77 | 78 | def __init__(self, *args, **kwargs): 79 | 80 | super().__init__(*args, **kwargs) 81 | 82 | #We only store blender data here when the operator is active, we should be totally fine! 83 | self.node_tree = None 84 | self.init_click = (0,0) 85 | self.chamfer_data = [] 86 | self.init_state = {} 87 | 88 | @classmethod 89 | def poll(cls, context): 90 | return (context.space_data.type=='NODE_EDITOR') and (context.space_data.node_tree is not None) 91 | 92 | def chamfer_setup(self, n): 93 | 94 | ng = self.node_tree 95 | 96 | Chamf = ChamferItem() #Using custom class may cause crashes? i had one... perhaps it would be best to switch to nested lists or dicts? 97 | Chamf.init_rr = n.name #we are storing objects directly and their adress may change 98 | 99 | #get initial node location 100 | Chamf.init_loc_local = n.location.copy() 101 | 102 | left_link = n.inputs[0].links[0] 103 | from_sock = left_link.from_socket 104 | right_link = n.outputs[0].links[0] 105 | to_sock = right_link.to_socket 106 | 107 | #get chamfer directions, in global space 108 | loc_init_global = get_node_absolute_location(n).copy() 109 | #get chamfer direction from 110 | if (from_sock.node.type == 'REROUTE'): 111 | Chamf.fromvec = get_node_absolute_location(from_sock.node) - loc_init_global 112 | Chamf.fromvec.normalize() 113 | else: Chamf.fromvec = Vector((-1,0)) 114 | #get chamfer direction to 115 | if (to_sock.node.type == 'REROUTE'): 116 | Chamf.tovec = get_node_absolute_location(to_sock.node) - loc_init_global 117 | Chamf.tovec.normalize() 118 | else: Chamf.tovec = Vector((1,0)) 119 | 120 | #add new reroute 121 | rra = ng.nodes.new("NodeReroute") 122 | rra.location = n.location 123 | rra.parent = n.parent 124 | Chamf.added_rr = rra.name 125 | 126 | #remove old link 127 | ng.links.remove(left_link) 128 | #add new links 129 | ng.links.new(from_sock, rra.inputs[0],) 130 | ng.links.new(rra.outputs[0], n.inputs[0],) 131 | 132 | #set selection visual cue 133 | n.select = rra.select = True 134 | 135 | self.chamfer_data.append(Chamf) 136 | return None 137 | 138 | def invoke(self, context, event): 139 | 140 | ng = context.space_data.edit_tree 141 | self.node_tree = ng 142 | 143 | #get initial mouse position 144 | ensure_mouse_cursor(context, event) 145 | self.init_click = context.space_data.cursor_location.copy() 146 | 147 | selected = [n for n in ng.nodes if n.select and (n.type=='REROUTE') \ 148 | and not ((len(n.inputs[0].links)==0) or (len(n.outputs[0].links)==0))] 149 | 150 | if (len(selected)==0): 151 | return {'FINISHED'} 152 | 153 | #save state to data later 154 | for n in selected: 155 | self.init_state[n.name]={ 156 | "location":n.location.copy(), 157 | "IN":get_rr_links_info(n,"IN"), 158 | "OUT":get_rr_links_info(n,"OUT"), 159 | } 160 | 161 | #set selection 162 | for n in self.node_tree.nodes: 163 | n.select = False 164 | 165 | #set up chamfer 166 | for n in selected: 167 | self.chamfer_setup(n) 168 | 169 | #Write status bar 170 | context.workspace.status_text_set_internal("Confirm: 'Enter' 'Space' 'Left-Click' | Cancel: 'Right-Click' 'Esc'") 171 | 172 | #start modal 173 | context.window_manager.modal_handler_add(self) 174 | 175 | return {'RUNNING_MODAL'} 176 | 177 | def modal(self, context, event): 178 | 179 | try: 180 | # context.area.tag_redraw() 181 | 182 | #if user confirm: 183 | if (event.type in ("LEFTMOUSE","RET","SPACE")): 184 | bpy.ops.ed.undo_push(message="Reroute Chamfer", ) 185 | context.workspace.status_text_set_internal(None) 186 | return {'FINISHED'} 187 | 188 | #if user cancel: 189 | elif event.type in ("ESC","RIGHTMOUSE"): 190 | 191 | #remove all newly created items 192 | for Chamfer in self.chamfer_data: 193 | self.node_tree.nodes.remove(self.node_tree.nodes.get(Chamfer.added_rr)) 194 | 195 | #restore init state 196 | for k,v in self.init_state.items(): 197 | n = self.node_tree.nodes[k] 198 | n.location = v["location"] 199 | restore_links_to_original(n, self.node_tree, v["IN"], "IN") 200 | restore_links_to_original(n, self.node_tree, v["OUT"], "OUT") 201 | continue 202 | 203 | context.workspace.status_text_set_internal(None) 204 | # context.area.tag_redraw() 205 | return {'CANCELLED'} 206 | 207 | #else move position of all chamfer items 208 | 209 | #get distance data from cursor 210 | ensure_mouse_cursor(context, event) 211 | distance = numpy.linalg.norm(context.space_data.cursor_location - self.init_click) 212 | 213 | #move chamfer vertex 214 | for Chamf in self.chamfer_data: 215 | #need global to local 216 | self.node_tree.nodes.get(Chamf.init_rr).location = Chamf.init_loc_local + ( Chamf.tovec * distance ) 217 | self.node_tree.nodes.get(Chamf.added_rr).location = Chamf.init_loc_local + ( Chamf.fromvec * distance ) 218 | continue 219 | 220 | except Exception as e: 221 | print(e) 222 | context.workspace.status_text_set_internal(None) 223 | self.report({'ERROR'},"An Error Occured during Chamfer modal") 224 | return {'CANCELLED'} 225 | 226 | return {'RUNNING_MODAL'} -------------------------------------------------------------------------------- /ui/menus.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | 8 | import re 9 | import os 10 | 11 | from ..__init__ import get_addon_prefs 12 | 13 | 14 | class NODEBOOSTER_MT_textemplate(bpy.types.Menu): 15 | bl_idname = "NODEBOOSTER_MT_textemplate" 16 | bl_label = "Booster Nodes" 17 | 18 | def draw(self, context): 19 | current_dir = os.path.dirname(os.path.abspath(__file__)) 20 | parent_dir = os.path.dirname(current_dir) 21 | external_dir = os.path.join(parent_dir, "resources") 22 | 23 | file_path = os.path.join(external_dir, "NexDemo.py") 24 | layout = self.layout 25 | layout.separator() 26 | op = layout.operator("nodebooster.import_template", text=os.path.basename(file_path),) 27 | op.filepath = file_path 28 | 29 | return None 30 | 31 | # .o. .o8 .o8 ooo ooooo 32 | # .888. "888 "888 `88. .888' 33 | # .8"888. .oooo888 .oooo888 888b d'888 .ooooo. ooo. .oo. oooo oooo 34 | # .8' `888. d88' `888 d88' `888 8 Y88. .P 888 d88' `88b `888P"Y88b `888 `888 35 | # .88ooo8888. 888 888 888 888 8 `888' 888 888ooo888 888 888 888 888 36 | # .8' `888. 888 888 888 888 8 Y 888 888 .o 888 888 888 888 37 | # o88o o8888o `Y8bod88P" `Y8bod88P" o8o o888o `Y8bod8P' o888o o888o `V88V"V8P' 38 | 39 | from ..customnodes import allcustomnodes 40 | 41 | #list of menus class we are about to create 42 | PROCEDURAL_ADDMENUS = [] 43 | #list of nodes or menus to call from 'draw_booster_nodes_add_menu' 44 | MAIN_LAYOUT_CONTENT = [] 45 | 46 | def auto_register_submenus(self, context): 47 | """all our nodes have a 'nb_menu_path', being lists of strings. 48 | The last element of the list is the name of the node, to call `layout.operator("node.add_node")` with 49 | all other elements are submenus, we need to procedurally create and register these menus and drawing functions with branching menus""" 50 | 51 | global PROCEDURAL_ADDMENUS, MAIN_LAYOUT_CONTENT 52 | 53 | # reset any previous content 54 | PROCEDURAL_ADDMENUS = [] 55 | MAIN_LAYOUT_CONTENT = [] 56 | 57 | def make_class_name_from_path(path_tuple): 58 | def sanitize_token(token: str) -> str: 59 | t = re.sub(r"[^0-9a-zA-Z]+", "_", str(token)).strip("_") 60 | if (not t): t = "UNNAMED" 61 | return t.upper() 62 | tokens = [sanitize_token(p) for p in path_tuple] 63 | return "NODEBOOSTER_MT_SUBMENU_" + "_".join(tokens) 64 | 65 | # Collect all menu paths and node attachments 66 | menu_paths_set = set() # set[tuple[str,...]] of submenu paths 67 | menu_label_map = {} # path_tuple -> label (last token) 68 | node_map = {} # parent_menu_path_tuple -> list[(cls, node_label)] 69 | 70 | for cls in allcustomnodes: 71 | if (not hasattr(cls, 'nb_menu_path')): 72 | print("WARNING: Node", getattr(cls, 'bl_label', getattr(cls, '__name__', str(cls))), "has no 'nb_menu_path'") 73 | continue 74 | menu_paths = getattr(cls, 'nb_menu_path', []) 75 | if (len(menu_paths) == 0): 76 | print("WARNING: Node", getattr(cls, 'bl_label', getattr(cls, '__name__', str(cls))), "has empty 'nb_menu_path'") 77 | continue 78 | 79 | # If it's a single path element, directly display this node in main layout 80 | if (len(menu_paths) == 1): 81 | cls.nb_menu_path = menu_paths[0] 82 | MAIN_LAYOUT_CONTENT.append(cls) 83 | continue 84 | 85 | # All prefixes (except the final node label) form the submenu hierarchy 86 | menus, node_operator_label = menu_paths[:-1], menu_paths[-1] 87 | 88 | # Track all submenu prefixes 89 | for i in range(1, len(menus)+0): # +0 explicit; up to full submenu path 90 | prefix = tuple(menus[:i]) 91 | if len(prefix) == 0: 92 | continue 93 | if prefix not in menu_paths_set: 94 | menu_paths_set.add(prefix) 95 | menu_label_map[prefix] = prefix[-1] 96 | 97 | full_menu_path = tuple(menus) 98 | if full_menu_path: 99 | menu_paths_set.add(full_menu_path) 100 | menu_label_map[full_menu_path] = full_menu_path[-1] 101 | 102 | # Attach node to its direct parent submenu path 103 | parent_path = tuple(menus) 104 | node_map.setdefault(parent_path, []).append((cls, node_operator_label)) 105 | 106 | # Build direct-children mapping for submenus 107 | children_menus_map = {} # parent_path_tuple (or ()) -> set(child_path_tuple) 108 | for path in menu_paths_set: 109 | parent = tuple(path[:-1]) 110 | children_menus_map.setdefault(parent, set()).add(path) 111 | 112 | # Create classes for each submenu path 113 | path_to_class = {} 114 | # Sort paths for deterministic creation order 115 | for path in sorted(menu_paths_set, key=lambda p: (len(p), [str(x).lower() for x in p])): 116 | class_name = make_class_name_from_path(path) 117 | bl_label = menu_label_map.get(path, path[-1] if path else "Menu") 118 | 119 | #procedurally gen the draw function 120 | def draw(self, context): 121 | layout = self.layout 122 | # Draw submenu entries first 123 | for cls in getattr(self, 'children_menus', []): 124 | if (cls.bl_label=='Experimental' and not get_addon_prefs().experimental_mode): 125 | continue 126 | layout.menu(cls.bl_idname) 127 | # Then draw node entries 128 | for cls, node_label in getattr(self, 'children_nodes', []): 129 | if (not hasattr(cls, 'tree_type')): 130 | print(f"WARNING: Node '{cls.bl_label}' has no attribute 'tree_type'") 131 | continue 132 | elif (cls.tree_type in {context.space_data.tree_type,'AnyNodeTree'}): #filter add node operator depending on the editor type.. 133 | op = layout.operator("node.add_node", text=node_label) 134 | op.type = cls.bl_idname 135 | op.use_transform = True 136 | return None 137 | 138 | #build the class from attributes 139 | attrs = { 140 | 'bl_idname': class_name, 141 | 'bl_label': bl_label, 142 | 'bl_description': "", 143 | 'children_menus': [], 144 | 'children_nodes': [], 145 | 'draw': draw, } 146 | NewMenuClass = type(class_name, (bpy.types.Menu,), attrs) 147 | 148 | #mark for registration 149 | path_to_class[path] = NewMenuClass 150 | PROCEDURAL_ADDMENUS.append(NewMenuClass) 151 | 152 | # Wire children relationships for each submenu class 153 | for path, MenuClass in path_to_class.items(): 154 | # child menus (direct children only) 155 | child_paths = sorted(children_menus_map.get(path, []), key=lambda p: menu_label_map.get(p, p[-1]).lower()) 156 | MenuClass.children_menus = [path_to_class[p] for p in child_paths if p in path_to_class] 157 | 158 | # child nodes under this submenu 159 | node_entries = sorted(node_map.get(path, []), key=lambda e: str(e[1]).lower()) 160 | MenuClass.children_nodes = node_entries 161 | 162 | # Add root-level menus to the main layout 163 | top_level_menu_paths = sorted(children_menus_map.get((), []), key=lambda p: menu_label_map.get(p, p[-1]).lower()) 164 | for p in top_level_menu_paths: 165 | if p in path_to_class: 166 | MAIN_LAYOUT_CONTENT.append(path_to_class[p]) 167 | 168 | return None 169 | 170 | def draw_booster_nodes_add_menu(self, context): 171 | """append booster nodes to the main nodetree add menu common across all nodetrees editors""" 172 | 173 | if (context.space_data.tree_type not in {'GeometryNodeTree','ShaderNodeTree','CompositorNodeTree'}): 174 | return None 175 | layout = self.layout 176 | layout.separator() 177 | 178 | for cls in MAIN_LAYOUT_CONTENT: 179 | is_submenu, is_nd_node, is_ng_node = cls.__name__.startswith('NODEBOOSTER_MT_SUBMENU_'), cls.__name__.startswith('NODEBOOSTER_ND_'), cls.__name__.startswith('NODEBOOSTER_NG_') 180 | if (is_nd_node or is_ng_node): 181 | if (not hasattr(cls, 'tree_type')): 182 | print(f"WARNING: Node '{cls.bl_label}' has no attribute 'tree_type'") 183 | continue 184 | elif (cls.tree_type in {context.space_data.tree_type,'AnyNodeTree'}): #filter add node operator depending on the editor type.. 185 | op = layout.operator("node.add_node", text=cls.bl_label,) 186 | op.type = cls.bl_idname 187 | op.use_transform = True 188 | elif (is_submenu): 189 | if (cls.bl_label=='Experimental' and not get_addon_prefs().experimental_mode): 190 | continue 191 | layout.menu(cls.bl_idname) 192 | else: 193 | print("WARNING: Node", cls.bl_label, "has an unknown type") 194 | 195 | return None 196 | 197 | def append_menus(): 198 | 199 | # Build dynamic submenu classes and main layout content 200 | auto_register_submenus(None, bpy.context) 201 | 202 | bpy.types.NODE_MT_add.append(draw_booster_nodes_add_menu) 203 | 204 | # Register dynamically generated submenu classes 205 | for cls in PROCEDURAL_ADDMENUS: 206 | bpy.utils.register_class(cls) 207 | 208 | return None 209 | 210 | def remove_menus(): 211 | 212 | # Unregister dynamically generated submenu classes first 213 | for cls in reversed(PROCEDURAL_ADDMENUS): 214 | bpy.utils.unregister_class(cls) 215 | 216 | bpy.types.NODE_MT_add.remove(draw_booster_nodes_add_menu) 217 | 218 | return None -------------------------------------------------------------------------------- /utils/str_utils.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | 8 | import os 9 | import re 10 | import traceback 11 | 12 | from .. import get_addon_prefs 13 | 14 | 15 | def is_float_compatible(string): 16 | """ check if a string can be converted to a float value""" 17 | 18 | assert type(string) is str 19 | if (string[0]=='.'): 20 | return False 21 | try: 22 | float(string) 23 | return True 24 | except (ValueError, TypeError): 25 | return False 26 | 27 | 28 | def match_exact_tokens(string:str, tokenlist:list) -> list: 29 | """ 30 | Get a list of matching token, if any token in our token list match in our string list 31 | 32 | A token is matched exactly: 33 | - For numbers (integer/float), it won't match if the token is part of a larger number. 34 | - For alphabetic tokens, word boundaries are used. 35 | """ 36 | def build_token_pattern(tokens): 37 | def boundary(token): 38 | # For numbers, ensure the token isn't part of a larger number. 39 | if re.fullmatch(r'\d+(?:\.\d+)?', token): 40 | return r'(? str: 51 | """Replace any token in the given string with new values as defined by the tokens_mapping dictionary.""" 52 | 53 | def build_token_pattern(tokens): 54 | def boundary(token): 55 | # If token is a number (integer or float) 56 | if re.fullmatch(r'\d+(?:\.\d+)?', token): 57 | # Use negative lookbehind and lookahead to ensure the token isn't part of a larger number. 58 | return r'(? max_char): 97 | 98 | # find position of nearest whitespace char to the left of "width" 99 | marker = max_char - 1 100 | while (marker >= 0 and not string[marker].isspace()): 101 | marker = marker - 1 102 | 103 | # If no space was found, just split at max_char 104 | if (marker==-1): 105 | marker = max_char 106 | 107 | # remove line from original string and add it to the new string 108 | newline = string[0:marker] + "\n" 109 | newstring = newstring + newline 110 | string = string[marker + 1:] 111 | 112 | return newstring + string 113 | 114 | #Multiline string? 115 | if ("\n" in string): 116 | wrapped = "\n".join([wrap(l,max_char) for l in string.split("\n")]) 117 | else: wrapped = wrap(string,max_char) 118 | 119 | #UI Layout Draw? 120 | if (layout is not None): 121 | 122 | lbl = layout.column() 123 | lbl.active = active 124 | lbl.alert = alert 125 | lbl.scale_y = scale_y 126 | 127 | for i,l in enumerate(wrapped.split("\n")): 128 | 129 | if (l=='*SEPARATOR_LINE*'): 130 | lbl.separator(type='LINE') 131 | continue 132 | 133 | if (alignment): 134 | line = lbl.row() 135 | line.alignment = alignment 136 | else: line = lbl 137 | 138 | if (icon and (i==0)): 139 | line.label(text=l, icon=icon) 140 | continue 141 | 142 | line.label(text=l) 143 | continue 144 | 145 | return wrapped 146 | 147 | 148 | def prettyError(e: BaseException, userfilename='',): 149 | """ 150 | Return a multiline string describing the given exception `e` in a 151 | more readable format. If it's a SyntaxError, includes line text 152 | with a caret at the .offset. Otherwise, falls back to the last 153 | traceback frame and includes the file name, line, and code snippet. 154 | """ 155 | 156 | etypename = type(e).__name__ 157 | 158 | match etypename: 159 | 160 | #Synthax error? 161 | case 'SyntaxError': 162 | # e.text is the source line, e.offset is the column offset (1-based) 163 | # e.lineno is line number, e.filename is file name, e.msg is short message 164 | faulty_line = e.text or "" 165 | faulty_line = faulty_line.rstrip("\n") 166 | 167 | # offset can be None or out-of-range 168 | offset = e.offset or 1 169 | if offset < 1: 170 | offset = 1 171 | if offset > len(faulty_line): 172 | offset = len(faulty_line) 173 | 174 | highlight = "" 175 | if (faulty_line): 176 | highlight = " " * (offset - 1) + "^"*5 177 | 178 | full_error = ( 179 | f"{type(e).__name__}: {e.msg}\n" 180 | f"File '{e.filename}' At line {e.lineno}.\n" 181 | f" {faulty_line}\n" 182 | f" {highlight}" 183 | ) 184 | small_error = ( 185 | f"PythonSynthaxError. {e.msg}. Line {e.lineno}." 186 | ) 187 | return full_error, small_error 188 | 189 | #Nex Error? 190 | case 'NexError': 191 | 192 | tb = e.__traceback__ 193 | filtered_tb = tb 194 | faultyfilename = 'Unknown' 195 | faultyline = 'Unknown' 196 | 197 | while (filtered_tb is not None): 198 | # Extract a 1-frame summary for the current node in the traceback 199 | frame_summaries = traceback.extract_tb(filtered_tb, limit=1) 200 | if (not frame_summaries): 201 | # Something went wrong or we reached the end 202 | break 203 | 204 | frame_info = frame_summaries[0] # A FrameSummary object 205 | 206 | filename = frame_info.filename 207 | lineno = frame_info.lineno 208 | 209 | if (userfilename in filename): 210 | faultyline = lineno 211 | faultyfilename = filename 212 | break 213 | 214 | filtered_tb = filtered_tb.tb_next 215 | continue 216 | 217 | full_error = ( 218 | f"NexError: {e}\n" 219 | f"File '{faultyfilename}' At line {faultyline}.\n" 220 | ) 221 | small_error = ( 222 | f"{e} Line {faultyline}." 223 | ) 224 | return full_error, small_error 225 | 226 | # Other exceptions 227 | case _: 228 | 229 | # We'll extract the traceback frames. The last frame is typically where 230 | # the exception occurred. We can show file, line, and snippet if available. 231 | frames = traceback.extract_tb(e.__traceback__) 232 | if not frames: 233 | # If there's no traceback info at all, just show type + message 234 | return f"{type(e).__name__}: {e}", f"{type(e).__name__}. {e}" 235 | 236 | # The last frame is typically the innermost call where the error happened 237 | last_frame = frames[-1] 238 | filename = last_frame.filename 239 | lineno = last_frame.lineno 240 | code_line = last_frame.line or "" 241 | 242 | if (filename == userfilename): 243 | full_error = ( 244 | f"UserSideError. {type(e).__name__}: {e}\n" 245 | f"File \"{filename}\", At line {lineno}\n\n" 246 | f" {code_line}" 247 | ) 248 | small_error = ( 249 | f"{type(e).__name__}. {e} Line {lineno}." 250 | ) 251 | return full_error, small_error 252 | 253 | full_error = ( 254 | f"InternalError. Please report! {type(e).__name__}: {e}\n" 255 | f"File \"{filename}\", At line {lineno}\n\n" 256 | f" {code_line}" 257 | ) 258 | small_error = ( 259 | f"InternalError. {type(e).__name__}. {e}. File '{os.path.basename(filename)}' line {lineno}." 260 | ) 261 | return full_error, small_error 262 | -------------------------------------------------------------------------------- /customnodes/camerainfo.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.), Andrew Stevenson 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | 8 | from ..__init__ import get_addon_prefs 9 | from ..utils.str_utils import word_wrap 10 | from ..utils.node_utils import ( 11 | create_new_nodegroup, 12 | set_ng_socket_defvalue, 13 | get_booster_nodes, 14 | cache_booster_nodes_parent_tree, 15 | ) 16 | 17 | # ooooo ooo .o8 18 | # `888b. `8' "888 19 | # 8 `88b. 8 .ooooo. .oooo888 .ooooo. 20 | # 8 `88b. 8 d88' `88b d88' `888 d88' `88b 21 | # 8 `88b.8 888 888 888 888 888ooo888 22 | # 8 `888 888 888 888 888 888 .o 23 | # o8o `8 `Y8bod8P' `Y8bod88P" `Y8bod8P' 24 | 25 | class Base(): 26 | 27 | bl_idname = "NodeBoosterCameraInfoV2" 28 | bl_label = "Camera Info" 29 | bl_description = """Gather informations about any camera. 30 | • By default the camera will always use the active camera. 31 | • Expect updates on each depsgraph post and frame_pre update signals""" 32 | nb_menu_path = ['NodeBooster','Inputs',bl_label,] #path in add menu 33 | auto_upd_flags = {'FRAME_PRE','DEPS_POST',} 34 | tree_type = "*ChildrenDefined*" 35 | 36 | def update_signal(self,context): 37 | self.sync_out_values() 38 | return None 39 | 40 | use_scene_cam: bpy.props.BoolProperty( 41 | default=True, 42 | name="Use Active Camera", 43 | description="Automatically update the pointer to the active scene camera", 44 | update=update_signal, 45 | ) 46 | 47 | def camera_obj_poll(self, obj): 48 | return (obj.type == 'CAMERA') 49 | 50 | camera_obj: bpy.props.PointerProperty( 51 | type=bpy.types.Object, 52 | poll=camera_obj_poll, 53 | update=update_signal, 54 | ) 55 | 56 | @classmethod 57 | def poll(cls, context): 58 | """mandatory poll""" 59 | return True 60 | 61 | def init(self, context): 62 | """this fct run when appending the node for the first time""" 63 | 64 | name = f".{self.bl_idname}" 65 | 66 | match self.tree_type: 67 | case 'GeometryNodeTree': 68 | sockets = { 69 | "Object": "NodeSocketObject", 70 | "Field of View": "NodeSocketFloat", 71 | "Shift X": "NodeSocketFloat", 72 | "Shift Y": "NodeSocketFloat", 73 | "Clip Start": "NodeSocketFloat", 74 | "Clip End": "NodeSocketFloat", 75 | "Sensor Type": "NodeSocketString", 76 | "Sensor Width": "NodeSocketFloat", 77 | "Sensor Height": "NodeSocketFloat", 78 | } 79 | 80 | case 'ShaderNodeTree' | 'CompositorNodeTree': 81 | sockets = { 82 | "Location": "NodeSocketVector", #object transforms instead. 83 | "Rotation": "NodeSocketVector", #object transforms instead. 84 | "Scale": "NodeSocketVector", #object transforms instead. 85 | "Field of View": "NodeSocketFloat", 86 | "Shift X": "NodeSocketFloat", 87 | "Shift Y": "NodeSocketFloat", 88 | "Clip Start": "NodeSocketFloat", 89 | "Clip End": "NodeSocketFloat", 90 | "Sensor Type": "NodeSocketInt", #int instead 91 | "Sensor Width": "NodeSocketFloat", 92 | "Sensor Height": "NodeSocketFloat", 93 | } 94 | 95 | ng = bpy.data.node_groups.get(name) 96 | if (ng is None): 97 | ng = create_new_nodegroup(name, 98 | tree_type=self.tree_type, 99 | out_sockets=sockets, 100 | ) 101 | 102 | ng = ng.copy() #always using a copy of the original ng 103 | self.node_tree = ng 104 | 105 | return None 106 | 107 | def copy(self, node): 108 | """fct run when dupplicating the node""" 109 | 110 | #NOTE: copy/paste can cause crashes, we use a timer to delay the action 111 | def delayed_copy(): 112 | self.node_tree = node.node_tree.copy() 113 | bpy.app.timers.register(delayed_copy, first_interval=0.01) 114 | 115 | return None 116 | 117 | def update(self): 118 | """generic update function""" 119 | 120 | cache_booster_nodes_parent_tree(self.id_data) 121 | 122 | return None 123 | 124 | def sync_out_values(self): 125 | """sync output socket values with data""" 126 | 127 | scene = bpy.context.scene 128 | co = scene.camera if (self.use_scene_cam) else self.camera_obj 129 | cd = co.data if co else None 130 | valid = (co and cd) 131 | 132 | values = { 133 | "Field of View": cd.angle if (valid) else 0.0, 134 | "Shift X": cd.shift_x if (valid) else 0.0, 135 | "Shift Y": cd.shift_y if (valid) else 0.0, 136 | "Clip Start": cd.clip_start if (valid) else 0.0, 137 | "Clip End": cd.clip_end if (valid) else 0.0, 138 | "Sensor Width": cd.sensor_width if (valid) else 0.0, 139 | "Sensor Height": cd.sensor_height if (valid) else 0.0, 140 | } 141 | 142 | #different behavior and sockets depending on editor type 143 | match self.tree_type: 144 | 145 | case 'GeometryNodeTree': 146 | values["Sensor Type"] = cd.sensor_fit if (valid) else "" 147 | #Support for old socket name, previous version of node. 148 | camvalue = co if (valid) else None 149 | if ("Camera Object" in self.outputs): 150 | values["Camera Object"] = camvalue 151 | elif ("Object" in self.outputs): 152 | values["Object"] = camvalue 153 | 154 | case 'ShaderNodeTree' | 'CompositorNodeTree': 155 | values["Location"] = co.location if (valid) else (0,0,0) 156 | values["Rotation"] = co.rotation_euler if (valid) else (0,0,0) 157 | values["Scale"] = co.scale if (valid) else (0,0,0) 158 | values["Sensor Type"] = 0 if (cd.sensor_fit=='AUTO') else 2 if (cd.sensor_fit=='HORIZONTAL') else 3 159 | 160 | for k,v in values.items(): 161 | set_ng_socket_defvalue(self.node_tree, socket_name=k, value=v,) 162 | 163 | return None 164 | 165 | def draw_label(self,): 166 | """node label""" 167 | 168 | if (self.label==''): 169 | return 'Camera Info' 170 | 171 | return self.label 172 | 173 | def draw_buttons(self, context, layout): 174 | """node interface drawing""" 175 | 176 | row = layout.row(align=True) 177 | sub = row.row(align=True) 178 | 179 | if (self.use_scene_cam): 180 | sub.enabled = False 181 | sub.prop(bpy.context.scene, "camera", text="", icon="CAMERA_DATA") 182 | else: sub.prop(self, "camera_obj", text="", icon="CAMERA_DATA") 183 | 184 | row.prop(self, "use_scene_cam", text="", icon="SCENE_DATA") 185 | 186 | return None 187 | 188 | def draw_panel(self, layout, context): 189 | """draw in the nodebooster N panel 'Active Node'""" 190 | 191 | n = self 192 | 193 | header, panel = layout.panel("params_panelid", default_closed=False,) 194 | header.label(text="Parameters",) 195 | if (panel): 196 | 197 | row = panel.row(align=True) 198 | sub = row.row(align=True) 199 | 200 | if (n.use_scene_cam): 201 | sub.enabled = False 202 | sub.prop(bpy.context.scene, "camera", text="", icon="CAMERA_DATA") 203 | else: sub.prop(n, "camera_obj", text="", icon="CAMERA_DATA") 204 | 205 | panel.prop(n, "use_scene_cam",) 206 | 207 | header, panel = layout.panel("doc_panelid", default_closed=True,) 208 | header.label(text="Documentation",) 209 | if (panel): 210 | word_wrap(layout=panel, alert=False, active=True, max_char='auto', 211 | char_auto_sidepadding=0.9, context=context, string=n.bl_description, 212 | ) 213 | panel.operator("wm.url_open", text="Documentation",).url = "https://blenderartists.org/t/node-booster-extending-blender-node-editors" 214 | 215 | header, panel = layout.panel("dev_panelid", default_closed=True,) 216 | header.label(text="Development",) 217 | if (panel): 218 | panel.active = False 219 | 220 | col = panel.column(align=True) 221 | col.label(text="NodeTree:") 222 | col.template_ID(n, "node_tree") 223 | 224 | return None 225 | 226 | @classmethod 227 | def update_all(cls, using_nodes=None, signal_from_handlers=False,): 228 | """search for all node instances of this type and refresh them. Will be called automatically if .auto_upd_flags's are defined""" 229 | 230 | if (using_nodes is None): 231 | nodes = get_booster_nodes(by_idnames={cls.bl_idname},) 232 | else: nodes = [n for n in using_nodes if (n.bl_idname==cls.bl_idname)] 233 | 234 | for n in nodes: 235 | n.sync_out_values() 236 | 237 | return None 238 | 239 | #Per Node-Editor Children: 240 | #Respect _NG_ + _GN_/_SH_/_CP_ nomenclature 241 | 242 | class NODEBOOSTER_NG_GN_CameraInfo(Base, bpy.types.GeometryNodeCustomGroup): 243 | tree_type = "GeometryNodeTree" 244 | bl_idname = "GeometryNode" + Base.bl_idname 245 | 246 | class NODEBOOSTER_NG_SH_CameraInfo(Base, bpy.types.ShaderNodeCustomGroup): 247 | tree_type = "ShaderNodeTree" 248 | bl_idname = "ShaderNode" + Base.bl_idname 249 | 250 | class NODEBOOSTER_NG_CP_CameraInfo(Base, bpy.types.CompositorNodeCustomGroup): 251 | tree_type = "CompositorNodeTree" 252 | bl_idname = "CompositorNode" + Base.bl_idname 253 | -------------------------------------------------------------------------------- /handlers/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | 8 | from collections.abc import Iterable 9 | 10 | from ..gpudraw import register_gpu_drawcalls 11 | from ..__init__ import get_addon_prefs, dprint 12 | from ..operators.palette import msgbus_palette_callback 13 | from ..utils.node_utils import get_booster_nodes, cache_all_booster_nodes_parent_trees 14 | from ..customnodes import allcustomnodes 15 | from ..customnodes import NODEBOOSTER_NG_GN_IsRenderedView 16 | 17 | 18 | # oooooooooo. 19 | # `888' `Y8b 20 | # 888 888 oooo oooo .oooo.o .oooo.o .ooooo. .oooo.o 21 | # 888oooo888' `888 `888 d88( "8 d88( "8 d88' `88b d88( "8 22 | # 888 `88b 888 888 `"Y88b. `"Y88b. 888ooo888 `"Y88b. 23 | # 888 .88P 888 888 o. )88b o. )88b 888 .o o. )88b 24 | # o888bood8P' `V88V"V8P' 8""888P' 8""888P' `Y8bod8P' 8""888P' 25 | 26 | 27 | MSGBUSOWNER_VIEWPORT_SHADING = object() 28 | MSGBUSOWNER_PALETTE = object() 29 | 30 | def msgbus_viewportshading_callback(*args): 31 | 32 | if (get_addon_prefs().debug_depsgraph): 33 | print("msgbus_viewportshading_callback(): msgbus signal") 34 | 35 | NODEBOOSTER_NG_GN_IsRenderedView.update_all(signal_from_handlers=True) 36 | 37 | return None 38 | 39 | def register_msgbusses(): 40 | 41 | bpy.msgbus.subscribe_rna( 42 | key=(bpy.types.View3DShading, "type"), 43 | owner=MSGBUSOWNER_VIEWPORT_SHADING, 44 | notify=msgbus_viewportshading_callback, 45 | args=(None,), 46 | options={"PERSISTENT"}, 47 | ) 48 | bpy.msgbus.subscribe_rna( 49 | key=bpy.types.PaletteColor, 50 | owner=MSGBUSOWNER_PALETTE, 51 | notify=msgbus_palette_callback, 52 | args=(None,), 53 | options={"PERSISTENT"}, 54 | ) 55 | 56 | return None 57 | 58 | def unregister_msgbusses(): 59 | 60 | bpy.msgbus.clear_by_owner(MSGBUSOWNER_VIEWPORT_SHADING) 61 | bpy.msgbus.clear_by_owner(MSGBUSOWNER_PALETTE) 62 | 63 | return None 64 | 65 | 66 | def on_plugin_installation(): 67 | """is executed either right after plugin installation (when user click on install checkbox), 68 | or when blender is booting, it will also load plugin""" 69 | 70 | def wait_restrict_state_timer(): 71 | """wait until bpy.context is not bpy_restrict_state._RestrictContext anymore 72 | BEWARE: this is a function from a bpy.app timer, context is trickier to handle""" 73 | 74 | dprint(f"HANDLER: on_plugin_installation(): Still in restrict state?",) 75 | 76 | #don't do anything until context is cleared out 77 | if (str(bpy.context).startswith("None: 104 | """evaluator the node required for the output evaluator""" 105 | 106 | set_all_sockets_enabled(self, inputs=True, outputs=True) #reset all sockets enabled status 107 | 108 | #1: We make some sockets invisible depending on user mode and tree type 109 | 110 | # NOTE commented out. normally remap node has no factor option. 111 | # match self.tree_type: 112 | # case 'GeometryNodeTree' | 'ShaderNodeTree': 113 | # set_node_socketattr(self, socket_name="Factor", attribute='enabled', value=True, in_out='INPUT',) 114 | # case 'CompositorNodeTree': 115 | # if (self.mode == "VECTOR"): 116 | # set_node_socketattr(self, socket_name="Factor", attribute='enabled', value=False, in_out='INPUT',) 117 | # else: set_node_socketattr(self, socket_name="Factor", attribute='enabled', value=True, in_out='INPUT',) 118 | set_node_socketattr(self, socket_name="Factor", attribute='enabled', value=False, in_out='INPUT',) 119 | 120 | #hide some sockets depending on the mode 121 | match self.mode: 122 | 123 | case "FLOAT": 124 | set_node_socketattr(self, socket_name="Interpolation", attribute='enabled', value=True, in_out='INPUT',) 125 | set_node_socketattr(self, socket_name="Interpolation X", attribute='enabled', value=False, in_out='INPUT',) 126 | set_node_socketattr(self, socket_name="Interpolation Y", attribute='enabled', value=False, in_out='INPUT',) 127 | set_node_socketattr(self, socket_name="Interpolation Z", attribute='enabled', value=False, in_out='INPUT',) 128 | set_node_socketattr(self, socket_name="Float", attribute='enabled', value=True, in_out='INPUT',) 129 | set_node_socketattr(self, socket_name="Float", attribute='enabled', value=True, in_out='OUTPUT',) 130 | set_node_socketattr(self, socket_name="Vector", attribute='enabled', value=False, in_out='INPUT',) 131 | set_node_socketattr(self, socket_name="Vector", attribute='enabled', value=False, in_out='OUTPUT',) 132 | self.inputs[6].enabled = True 133 | self.inputs[7].enabled = True 134 | self.inputs[8].enabled = True 135 | self.inputs[9].enabled = True 136 | self.inputs[11].enabled = False 137 | self.inputs[12].enabled = False 138 | self.inputs[13].enabled = False 139 | self.inputs[14].enabled = False 140 | 141 | sock_to_evaluate = { 142 | 'Interpolation':self.node_tree.nodes['float_map'].mapping.curves[0], 143 | } 144 | 145 | case "VECTOR": 146 | set_node_socketattr(self, socket_name="Interpolation", attribute='enabled', value=False, in_out='INPUT',) 147 | set_node_socketattr(self, socket_name="Interpolation X", attribute='enabled', value=True, in_out='INPUT',) 148 | set_node_socketattr(self, socket_name="Interpolation Y", attribute='enabled', value=True, in_out='INPUT',) 149 | set_node_socketattr(self, socket_name="Interpolation Z", attribute='enabled', value=True, in_out='INPUT',) 150 | set_node_socketattr(self, socket_name="Float", attribute='enabled', value=False, in_out='INPUT',) 151 | set_node_socketattr(self, socket_name="Float", attribute='enabled', value=False, in_out='OUTPUT',) 152 | set_node_socketattr(self, socket_name="Vector", attribute='enabled', value=True, in_out='INPUT',) 153 | set_node_socketattr(self, socket_name="Vector", attribute='enabled', value=True, in_out='OUTPUT',) 154 | self.inputs[6].enabled = False 155 | self.inputs[7].enabled = False 156 | self.inputs[8].enabled = False 157 | self.inputs[9].enabled = False 158 | self.inputs[11].enabled = True 159 | self.inputs[12].enabled = True 160 | self.inputs[13].enabled = True 161 | self.inputs[14].enabled = True 162 | 163 | sock_to_evaluate = { 164 | 'Interpolation X':self.node_tree.nodes['vector_map'].mapping.curves[0], 165 | 'Interpolation Y':self.node_tree.nodes['vector_map'].mapping.curves[1], 166 | 'Interpolation Z':self.node_tree.nodes['vector_map'].mapping.curves[2], 167 | } 168 | 169 | #get all nodes connected to the value socket 170 | cache = {} 171 | for k,c in sock_to_evaluate.items(): 172 | sock = self.inputs[k] 173 | # retrieve the value from the node behind. 174 | # the node might do a similar operation, and so on. 175 | val = evaluate_upstream_value(sock, 176 | match_evaluator_properties={'INTERPOLATION_NODE',}, 177 | set_link_invalid=True, 178 | cached_values=cache, 179 | ) 180 | # if the evaluator system failed to retrieve a value, we reset the curve to default.. 181 | # else we set the points. 182 | if (val is None): 183 | reset_curvemapping(c) 184 | else: bezsegs_to_curvemapping(c, val) 185 | continue 186 | 187 | # NOTE unfortunately python API for curve mapping is meh.. 188 | # we need to send an update trigger. Maybe there's a solution for this?. 189 | for nd in self.node_tree.nodes: 190 | if nd.name in {'float_map', 'vector_map'}: 191 | #send a hard refresh trigger. 192 | nd.mapping.update() 193 | nd.mute = not nd.mute 194 | nd.mute = not nd.mute 195 | nd.update() 196 | continue 197 | 198 | return None 199 | 200 | def draw_buttons(self, context, layout): 201 | """node interface drawing""" 202 | 203 | layout.prop(self, 'mode' ,text="") 204 | 205 | col = layout.column().box() 206 | word_wrap(layout=col, alert=False, active=True, max_char=self.width/6.65, 207 | string="Geometry-Node is not very tolerent to unrecognized sockets currently, see PR #136968.", 208 | ) 209 | 210 | return None 211 | 212 | def draw_panel(self, layout, context): 213 | """draw in the nodebooster N panel 'Active Node'""" 214 | 215 | n = self 216 | 217 | header, panel = layout.panel("params_panelid", default_closed=False) 218 | header.label(text="Parameters") 219 | if panel: 220 | 221 | panel.prop(self, 'mode', text="") 222 | 223 | header, panel = layout.panel("doc_panelid", default_closed=True,) 224 | header.label(text="Documentation",) 225 | if (panel): 226 | word_wrap(layout=panel, alert=False, active=True, max_char='auto', 227 | char_auto_sidepadding=0.9, context=context, string=n.bl_description, 228 | ) 229 | panel.operator("wm.url_open", text="Documentation",).url = "https://blenderartists.org/t/node-booster-extending-blender-node-editors" 230 | 231 | header, panel = layout.panel("dev_panelid", default_closed=True,) 232 | header.label(text="Development",) 233 | if (panel): 234 | panel.active = False 235 | 236 | col = panel.column(align=True) 237 | col.label(text="NodeTree:") 238 | col.template_ID(n, "node_tree") 239 | 240 | return None 241 | 242 | 243 | #Per Node-Editor Children: 244 | #Respect _NG_ + _GN_/_SH_/_CP_ nomenclature 245 | 246 | class NODEBOOSTER_NG_GN_InterpolationRemap(Base, bpy.types.GeometryNodeCustomGroup): 247 | tree_type = "GeometryNodeTree" 248 | bl_idname = "GeometryNodeNodeBoosterInterpolationRemap" 249 | 250 | class NODEBOOSTER_NG_SH_InterpolationRemap(Base, bpy.types.ShaderNodeCustomGroup): 251 | tree_type = "ShaderNodeTree" 252 | bl_idname = "ShaderNodeNodeBoosterInterpolationRemap" 253 | 254 | class NODEBOOSTER_NG_CP_InterpolationRemap(Base, bpy.types.CompositorNodeCustomGroup): 255 | tree_type = "CompositorNodeTree" 256 | bl_idname = "CompositorNodeNodeBoosterInterpolationRemap" 257 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | # --------------------------------------------------------------------------------------------- 6 | 7 | # TODO v2.0 release 8 | # - Custom operator shortcuts are not saved, they reset on each blender sessions. 9 | # - Functions should always check if a value or type isn't already set before setting it. 10 | # I believe the tool is currently sending a lot of useless update signals by setting the same values 11 | # (see compositor refresh, perhaps it's because of node.update()?? need to investigate) 12 | # - Finalize NexScript for Shader/compositor. Need to overview functions.. 13 | # - Velocity Node: Better history calculations just doing last middle first is not precise enough. 14 | # - Experiment with custom Socket Types: 15 | # - For this to work in geometry node, we'll need this PR to get accepted 16 | # https://projects.blender.org/blender/blender/pulls/136968 17 | # - Experimental Interpolation 18 | # - supported nested ng? or not. 19 | # - Document why it crash on blender reload. Specific to geomtry node not supporting custom NodeSocket. 20 | # Report once the proof of concept is done. devs need to see it's worth it. 21 | # - Why is there a triple update signal when adding a new node? 22 | # - Implement transform curves evaluators. 23 | # - Better structure for the node_tree evaluator system? Centralized the functions in one place maybe? 24 | # Do more tests with InterpolationSocket transformers. 25 | # what about storing the evaluated values somewhere, and only recalculate when needed? maybe add a is_dirty flag? 26 | # maybe could simply store a 'StringProperty' .evaluator_cache per sockets? 27 | # - if possible, then we can cross the todo in 'Change to C blender code' for custom socket types. 28 | # - Start with custom interpolation types. See if MapRange can be ported. Could linearmaprange to 01 then use the FloatMapCurve then map range to custom values. 29 | # The final nodes would simply do the evaluation. would not be nodegroup compatible tho. Problem: 30 | 31 | # --------------------------------------------------------------------------------------------- 32 | 33 | # NOTE Ideas for changes of blender C source code: 34 | # - There's a little message box above a node in GN, how can we write one via python API? 35 | # - Color of some nodes should'nt be red. sometimes blue for converter (math expression) or script color.. 36 | # Unfortunately the API si not Exposed. It would be nice to have custom colors for our nodes.. Or at least choose in existing colortype list. 37 | # - Eval socket_value API??? ex `my_node.inputs[0].eval_value()` would return a single value, or a numpy array (if possible?) 38 | # So far in this plugin we can only pass information to a socket, or arrange nodes. 39 | # What would be an extremely useful functionality, woould be to sample a socket value from a socket.evaluate_value() 40 | # integrated directly in blender. Unfortunately there are no plans to implement such API. 41 | # - CustomSocketTypes API? 42 | # https://projects.blender.org/blender/blender/pulls/136968 43 | # - Nodes Consistencies: Generally speaking, nodes are not consistent from one editor to another. 44 | # For example ShaderNodeValue becomes CompositorNodeValue. Ect.. a lot of Native socket types could be ported to 45 | # all editors as well. For example, SocketBool can be in the compositor. 46 | # - NodeSocket position should definitely be exposed for custom noodle drawing. or socket overdrawings. 47 | 48 | # --------------------------------------------------------------------------------------------- 49 | 50 | # TODO To Improve: 51 | # - CustomSocket Evaluation system: 52 | # - timer that check is_dirty flag of all evaluator nodes and update the output when dirty. 53 | # - is dirty should only refresh concerned nodes outputs. right now an update signal is sent to all output evaluators. 54 | # - curve 2d input and interpolation input need a way to check if the data is modified. 55 | # - check for muted status as well? doesn't send a native update signal it seems. 56 | # 57 | # --------------------------------------------------------------------------------------------- 58 | 59 | # TODO Ideas: 60 | # 61 | # Generic Functionalities Ideas: 62 | # - Maybe copy some nodewrangler functionality such as quick mix so user stick to our extrusion style workflow? 63 | # - Could have an operator for quickly editing a frame description? Either full custom python editor, or popup a new small window. 64 | # - Could implement background reference image. there's even a special drawing method for that in the API. 65 | # - could implement a tab switch in the header for quickly switching between different the big 3 editors? 66 | # - favorite system improvements: 67 | # - could take a snapshot of the view location and zoom level. 68 | # - could have a global list of favorites, and directly change nodetree or editor to reach it from one click!!!!! cross editor/ng favorites that way from Npanel.. 69 | # - node suggestions: depending on what's selected, propose a new node with a gpu draw transparent preview hint. user would click ESC to confirm and create it. maybe use arrows to swap proposal types ect.. 70 | # - replace a node with anohter, keep links at same place if possible, same for default values. 71 | 72 | # Nodes Ideas: 73 | # - Could design portal node. There are ways to hide sockets, even from user CTRL+H, this node could simply pass hidden sockets around? 74 | # Do some tests. Note: would only be nice if we draw a heavy 'portal line' effect from node A to node B. Bonus: animation of the direction. 75 | # - Material Info node? gather informations about the material? if so, what? 76 | # - Transform Is rendered view into a View3D info node, where user can get info about a or active 3Dview informations. Optional index to select which 3Dview he's talking about. 77 | # - Color Palette Node? easily swap between color palettes? 78 | # - Armature/Bone nodes? Will need to learn about rigging to do that tho.. 79 | # - File IO: For geometry node, could create a mesh on the fly from a file and set up as field attributes. 80 | # - View3D Info node: Like camera info, but for the 3d view (location/rotation/fov/clip/) 81 | # Problem: what if there are many? Perhaps should use context. 82 | # - MetaBall Info node? 83 | # - Evaluate sequencer images? Possible to feed the sequencer render to the nodes? Hmm 84 | # - SoundData Info Node: Sample the sound? Generate a sound geometry curve? Evaluate Sound at time? If we work based on sound, perhaps it's for the best isn't it? 85 | # - See if it's possible to imitate a multi-socket like the geometry join node, in customNode, and in customNodegroup. multi math ect would be nice. 86 | # - Interpolation Socket: 87 | # - add mirror node 88 | # - fit bounds node 89 | # - interpolation loop could have a mirror mode! instread or repeating from start/end it would be original/mirror/original ect.. 90 | # - sethandle auto/vectorized 91 | # - add interpolation make noise node? or 2d curve noise with option for noise in X or Y or both directions. 92 | # - Custom Sockets: 93 | # - We could have some sort of gamelogic nodes? 94 | # - mess with multi sockets like the join node. Check if we can use a native socket with this option?? 95 | # how could we possibly do that? 96 | # then we could do multi-math / multi Bool logic. Min/Max All Any, all eq, in between ect.. 97 | # - We could re-implement 98 | # - We could have a Shader PBR material 99 | # - Could have a sprite sheet socket type. random sprite selection. 100 | # - See inspirations from other softs: AnimationNodes/Svershock/Houdini/Ue5/MayaFrost/ect.. see what can be extending GeoNode/Shader/Compositor. 101 | # - MaterialMaker/ SubstanceDesigner import/livelink would be nice. 102 | 103 | # --------------------------------------------------------------------------------------------- 104 | 105 | # TODO Bugs: 106 | # To Fix: 107 | # - copy/pasting a node with ctrlc/v is not working, even crashing. Unsure it's us tho. Maybe it's blender bug. 108 | # Known: 109 | # - You might stumble into this crash when hot-reloading (enable/disable) the plugin on blender 4.2/4.2 110 | # https://projects.blender.org/blender/blender/issues/134669 Has been fixed in 4.4. 111 | # Only impacts developers hotreloading. 112 | 113 | # --------------------------------------------------------------------------------------------- 114 | 115 | import bpy 116 | 117 | #This is only here for supporting blender 4.1 118 | bl_info = { 119 | "name": "Node Booster (Experimental 4.1+)", 120 | "author": "BD3D DIGITAL DESIGN (Dorian B.)", 121 | "version": (2, 0, 0), 122 | "blender": (4, 1, 0), 123 | "location": "Node Editor", 124 | "description": "Please install this addon as a blender extension instead of a legacy addon!", 125 | "warning": "", 126 | "doc_url": "https://blenderartists.org/t/node-booster-extending-blender-node-editors", 127 | "category": "Node", 128 | } 129 | 130 | def get_addon_prefs(): 131 | """get preferences path from base_package, __package__ path change from submodules""" 132 | return bpy.context.preferences.addons[__package__].preferences 133 | 134 | def isdebug(): 135 | return get_addon_prefs().debug 136 | 137 | def dprint(thing): 138 | if isdebug(): 139 | print(thing) 140 | 141 | def cleanse_modules(): 142 | """remove all plugin modules from sys.modules for a clean uninstall (dev hotreload solution)""" 143 | # See https://devtalk.blender.org/t/plugin-hot-reload-by-cleaning-sys-modules/20040 fore more details. 144 | 145 | import sys 146 | 147 | all_modules = sys.modules 148 | all_modules = dict(sorted(all_modules.items(),key= lambda x:x[0])) #sort them 149 | 150 | for k,v in all_modules.items(): 151 | if k.startswith(__package__): 152 | del sys.modules[k] 153 | 154 | return None 155 | 156 | 157 | def get_addon_classes(revert=False): 158 | """gather all classes of this plugin that have to be reg/unreg""" 159 | 160 | from .properties import classes as sett_classes 161 | from .operators import classes as ope_classes 162 | from .customnodes import classes as nodes_classes 163 | from .ui import classes as ui_classes 164 | 165 | classes = sett_classes + ope_classes + nodes_classes + ui_classes 166 | 167 | if (revert): 168 | return reversed(classes) 169 | 170 | return classes 171 | 172 | 173 | def register(): 174 | """main addon register""" 175 | 176 | from .resources import load_icons 177 | load_icons() 178 | 179 | #register every single addon classes here 180 | for cls in get_addon_classes(): 181 | bpy.utils.register_class(cls) 182 | 183 | from .properties import load_properties 184 | load_properties() 185 | 186 | from .customnodes.keyboardinput import register_listener 187 | register_listener() 188 | 189 | from .customnodes.controllerinput import register_controller_listener 190 | register_controller_listener() 191 | 192 | from .handlers import load_handlers 193 | load_handlers() 194 | 195 | from .ui import load_ui 196 | load_ui() 197 | 198 | from .operators import load_operators_keymaps 199 | load_operators_keymaps() 200 | 201 | return None 202 | 203 | 204 | def unregister(): 205 | """main addon un-register""" 206 | 207 | from .operators import unload_operators_keymaps 208 | unload_operators_keymaps() 209 | 210 | from .ui import unload_ui 211 | unload_ui() 212 | 213 | from .handlers import unload_handlers 214 | unload_handlers() 215 | 216 | from .gpudraw import unregister_gpu_drawcalls 217 | unregister_gpu_drawcalls() 218 | 219 | from .properties import unload_properties 220 | unload_properties() 221 | 222 | #unregister every single addon classes here 223 | for cls in get_addon_classes(revert=True): 224 | bpy.utils.unregister_class(cls) 225 | 226 | from .customnodes.keyboardinput import unregister_listener 227 | unregister_listener() 228 | 229 | from .customnodes.controllerinput import unregister_controller_listener 230 | unregister_controller_listener() 231 | 232 | from .resources import unload_icons 233 | unload_icons() 234 | 235 | cleanse_modules() 236 | 237 | return None 238 | -------------------------------------------------------------------------------- /properties/scene_sett.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | 8 | from ..operators.search import search_upd 9 | from ..operators.palette import palette_active_upd 10 | 11 | 12 | class NODEBOOSTER_PR_scene_favorites_data(bpy.types.PropertyGroup): 13 | """favorites_data = bpy.context.scene.nodebooster.favorites_data""" 14 | 15 | name : bpy.props.StringProperty( 16 | ) 17 | nodetree_reference : bpy.props.PointerProperty( 18 | type=bpy.types.NodeTree, 19 | ) 20 | material_reference : bpy.props.PointerProperty( 21 | type=bpy.types.Material, #NOTE we need this in case user place a favorite to a material.node_tree, material.node_tree are special for some reasons.. can't put them in a Pointer.. 22 | ) 23 | active : bpy.props.BoolProperty( 24 | default=False, #TODO replace index system with active system 25 | description="Informal read-only property to track the current active favorite", 26 | ) 27 | 28 | def get_ng(self): 29 | if (self.nodetree_reference): 30 | return self.nodetree_reference 31 | if (self.material_reference): 32 | return self.material_reference.node_tree 33 | return None 34 | 35 | def get_node(self): 36 | ng = self.get_ng() 37 | if (ng is None): 38 | return None 39 | return ng.nodes.get(self.name) 40 | 41 | def get_label(self): 42 | nd = self.get_node() 43 | if (nd is None): 44 | return 'Deleted?' 45 | return nd.label 46 | 47 | class NODEBOOSTER_PR_scene(bpy.types.PropertyGroup): 48 | """sett_scene = bpy.context.scene.nodebooster""" 49 | 50 | #frame tool 51 | frame_use_custom_color : bpy.props.BoolProperty( 52 | default=False, 53 | name="Frame Color") 54 | frame_color : bpy.props.FloatVectorProperty( 55 | default=(0,0,0), 56 | subtype="COLOR", 57 | name="Color", 58 | min=0, 59 | max=1, 60 | ) 61 | frame_sync_color : bpy.props.BoolProperty( 62 | default=True, 63 | name="Sync Palette", 64 | description="Synchronize with palette", 65 | ) 66 | frame_label : bpy.props.StringProperty( 67 | default=" ", 68 | name="Label", 69 | ) 70 | frame_label_size : bpy.props.IntProperty( 71 | default=16,min=0, 72 | name="Label Size", 73 | ) 74 | 75 | #palette tool 76 | palette_active : bpy.props.FloatVectorProperty( 77 | default=(0,0,0), 78 | subtype='COLOR', 79 | name="Color", 80 | min=0, 81 | max=1, 82 | update=palette_active_upd, 83 | ) 84 | palette_assign_ptr: bpy.props.PointerProperty( 85 | type=bpy.types.Palette, 86 | name="Palette", 87 | ) 88 | palette_old : bpy.props.FloatVectorProperty( 89 | default=(0,0,0), 90 | subtype='COLOR', 91 | name="Color", 92 | min=0, 93 | max=1, 94 | ) 95 | palette_older : bpy.props.FloatVectorProperty( 96 | default=(0,0,0), 97 | subtype='COLOR', 98 | name="Color", 99 | min=0, 100 | max=1, 101 | ) 102 | 103 | #search tool 104 | search_keywords : bpy.props.StringProperty( 105 | default=" ", 106 | name="Keywords", 107 | update=search_upd, 108 | ) 109 | search_center : bpy.props.BoolProperty( 110 | default=True, 111 | name="Recenter View", 112 | update=search_upd, 113 | ) 114 | search_labels : bpy.props.BoolProperty( 115 | default=True, 116 | name="Label", 117 | update=search_upd, 118 | ) 119 | search_types : bpy.props.BoolProperty( 120 | default=True, 121 | name="Type", 122 | update=search_upd, 123 | ) 124 | search_names : bpy.props.BoolProperty( 125 | default=False, 126 | name="Internal Name", 127 | update=search_upd, 128 | ) 129 | search_socket_names : bpy.props.BoolProperty( 130 | default=False, 131 | name="Socket Names", 132 | update=search_upd, 133 | ) 134 | search_socket_types : bpy.props.BoolProperty( 135 | default=False, 136 | name="Socket Types", 137 | update=search_upd, 138 | ) 139 | search_input_only : bpy.props.BoolProperty( 140 | default=False, 141 | name="Input Nodes Only", 142 | update=search_upd, 143 | ) 144 | search_frame_only : bpy.props.BoolProperty( 145 | default=False, 146 | name="Frame Only", 147 | update=search_upd, 148 | ) 149 | search_found : bpy.props.IntProperty( 150 | default=0, 151 | ) 152 | 153 | #favorite tool 154 | favorites_data : bpy.props.CollectionProperty( 155 | type=NODEBOOSTER_PR_scene_favorites_data, 156 | name="Favorites Data", 157 | ) 158 | favorite_show_filter : bpy.props.EnumProperty( 159 | items=[ 160 | ("GEOMETRY,SHADER,COMPOSITOR","All","All",'SORTALPHA',0), 161 | None, 162 | ("GEOMETRY","Geometry","Geometry",'GEOMETRY_NODES',1), 163 | ("SHADER","Shader","Shader",'NODE_MATERIAL',2), 164 | ("COMPOSITOR","Compositor","Compositor",'NODE_COMPOSITING',3), 165 | ], 166 | default="GEOMETRY,SHADER,COMPOSITOR", 167 | name="Show Filter", 168 | ) 169 | 170 | #minimap 171 | minimap_show : bpy.props.BoolProperty( 172 | default=True, 173 | name="Show", 174 | ) 175 | minimap_auto_tool_panel_collapse : bpy.props.BoolProperty( 176 | default=True, 177 | name="Auto Collapse", 178 | description="Automatically collapse the Tool panel when the minimap is enabled.", 179 | ) 180 | minimap_auto_aspect_ratio : bpy.props.BoolProperty( 181 | default=True, 182 | name="Auto Aspect Ratio", 183 | description="Automatically adjust the aspect ratio of the minimap to fit all nodes. If disabled, use Size X/Y percentages directly.", 184 | ) 185 | minimap_draw_type : bpy.props.EnumProperty( 186 | name="DrawType", 187 | description="Choose whenever this drawing is done on the background or on the foreground of the node editor space, either as an underlay or an overlay.", 188 | items=( ("UNDERLAY","Back",""), ("OVERLAY","Front",""),), 189 | default="OVERLAY", 190 | ) 191 | # minimap_emplacement : bpy.props.EnumProperty( 192 | # items=[("BOTTOM_LEFT","Bottom Left","Bottom Left"),("TOP_LEFT","Top Left","Top Left"),("TOP_RIGHT","Top Right","Top Right"),("BOTTOM_RIGHT","Bottom Right","Bottom Right")], 193 | # name="Emplacement", 194 | # ) 195 | # minimap_width_pixels : bpy.props.IntVectorProperty( 196 | minimap_width_percentage : bpy.props.FloatVectorProperty( 197 | default=(0.25,0.50), 198 | name="Minimap Size", 199 | description="Set the max size of your minimap, in width/height percentage of the editor area. The ratio will automatically adjust itself to fit within these max percentages.", 200 | min=0.01, 201 | max=1, 202 | size=2, 203 | ) 204 | minimap_fill_color : bpy.props.FloatVectorProperty( 205 | default=(0.120647, 0.120647, 0.120647, 0.990), 206 | subtype="COLOR", 207 | name="Fill Color", 208 | min=0, 209 | max=1, 210 | size=4, 211 | ) 212 | minimap_outline_width : bpy.props.FloatProperty( 213 | default=1.5, 214 | name="Outline Width", 215 | min=0, 216 | soft_max=5, 217 | ) 218 | minimap_outline_color : bpy.props.FloatVectorProperty( 219 | default=(0.180751, 0.180751, 0.180751, 0.891667), 220 | subtype="COLOR", 221 | name="Outline Color", 222 | min=0, 223 | max=1, 224 | size=4, 225 | ) 226 | minimap_border_radius : bpy.props.FloatProperty( 227 | default=10, 228 | name="Border Radius", 229 | min=0, 230 | soft_max=50, 231 | ) 232 | minimap_padding : bpy.props.IntVectorProperty( 233 | default=(14,19), 234 | name="Padding", 235 | min=0, 236 | size=2, 237 | soft_max=50, 238 | ) 239 | #minimap node 240 | minimap_node_draw_typecolor : bpy.props.BoolProperty( 241 | default=True, 242 | name="Draw TypeColor", 243 | ) 244 | minimap_node_draw_nodecustomcolor : bpy.props.BoolProperty( 245 | default=False, 246 | name="Draw Custom Color", 247 | ) 248 | minimap_node_draw_selection : bpy.props.BoolProperty( 249 | default=True, 250 | name="Draw Selection", 251 | ) 252 | minimap_node_outline_width : bpy.props.FloatProperty( 253 | default=1.5, 254 | name="Outline Width", 255 | min=0, 256 | ) 257 | minimap_node_border_radius : bpy.props.FloatProperty( 258 | default=3, 259 | name="Border Radius", 260 | min=0, 261 | soft_max=20, 262 | ) 263 | minimap_node_draw_header : bpy.props.BoolProperty( 264 | default=True, 265 | name="Draw Header", 266 | ) 267 | minimap_node_draw_frames : bpy.props.BoolProperty( 268 | default=True, 269 | name="Draw Frames", 270 | ) 271 | minimap_node_draw_framecustomcolor : bpy.props.BoolProperty( 272 | default=True, 273 | name="Draw Frame Custom Color", 274 | ) 275 | minimap_node_draw_frames_detail : bpy.props.BoolProperty( 276 | default=False, 277 | name="Draw Frames within Frames", 278 | ) 279 | minimap_node_header_height : bpy.props.FloatProperty( 280 | default=12, 281 | name="Header Height", 282 | min=0, 283 | ) 284 | minimap_node_header_minheight : bpy.props.FloatProperty( 285 | default=6, 286 | name="Header Min Height when zoomed out", 287 | min=0, 288 | ) 289 | minimap_node_body_color : bpy.props.FloatVectorProperty( 290 | default=(0.172937, 0.172937, 0.172937, 1.000000), 291 | subtype="COLOR", 292 | name="Body Color", 293 | min=0, 294 | max=1, 295 | size=4, 296 | ) 297 | #view outline 298 | minimap_view_enable : bpy.props.BoolProperty( 299 | default=True, 300 | name="Enable", 301 | ) 302 | minimap_view_fill_color : bpy.props.FloatVectorProperty( 303 | default=(0.296174, 0.040511, 0.027817, 0.0), 304 | subtype="COLOR", 305 | name="Fill Color", 306 | min=0, 307 | max=1, 308 | size=4, 309 | ) 310 | minimap_view_outline_color : bpy.props.FloatVectorProperty( 311 | default=(1.0, 0.180751, 0.180751, 0.395833), 312 | subtype="COLOR", 313 | name="Outline Color", 314 | min=0, 315 | max=1, 316 | size=4, 317 | ) 318 | minimap_view_outline_width : bpy.props.FloatProperty( 319 | default=1.0, 320 | name="Outline Width", 321 | min=0, 322 | ) 323 | minimap_view_border_radius : bpy.props.FloatProperty( 324 | default=4, 325 | name="Border Radius", 326 | min=0, 327 | ) 328 | #cursor 329 | minimap_cursor_show : bpy.props.BoolProperty( 330 | default=False, 331 | name="Show", 332 | ) 333 | minimap_cursor_radius : bpy.props.FloatProperty( 334 | default=1.5, 335 | name="Radius", 336 | min=0.5, 337 | soft_max=25, 338 | ) 339 | minimap_cursor_color : bpy.props.FloatVectorProperty( 340 | default=(1.0, 0.180751, 0.180751, 1.0), 341 | subtype="COLOR", 342 | name="Color", 343 | min=0, 344 | max=1, 345 | size=4, 346 | ) 347 | #favorites 348 | minimap_fav_show : bpy.props.BoolProperty( 349 | default=True, 350 | name="Show", 351 | ) 352 | minimap_fav_size : bpy.props.FloatProperty( 353 | default=20, 354 | name="Size", 355 | ) 356 | #navigation shortcuts 357 | minimap_triple_click_dezoom : bpy.props.BoolProperty( 358 | default=True, 359 | name="Quick Dezoom", 360 | description="Quickly dezoom the node editor view to fit the integrity of the nodes by clicking 3 times on the minimap.", 361 | ) 362 | 363 | 364 | 365 | -------------------------------------------------------------------------------- /customnodes/colorpalette.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 BD3D DIGITAL DESIGN (Dorian B.) 2 | # 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | 6 | import bpy 7 | 8 | 9 | from mathutils import Color 10 | 11 | from ..utils.node_utils import ( 12 | create_new_nodegroup, 13 | set_ng_socket_defvalue, 14 | set_ng_socket_type, 15 | set_ng_socket_label, 16 | cache_booster_nodes_parent_tree, 17 | ) 18 | 19 | 20 | def gamma_correct(color): 21 | rgb, alpha = color[:3], color[3] if (len(color)>3) else 1.0 22 | rgb = Color(rgb).from_srgb_to_scene_linear() 23 | return [*rgb, alpha] 24 | 25 | class Base(): 26 | 27 | bl_idname = "NodeBoosterColorPalette" 28 | bl_label = "Color Palette" 29 | bl_description = "Output the active palette color. Shows a palette UI and a Create Palette button." 30 | nb_menu_path = ['NodeBooster','Inputs',bl_label,] #path in add menu 31 | auto_upd_flags = {'DEPS_POST','LOAD_POST',} 32 | tree_type = "*ChildrenDefined*" 33 | 34 | def update_signal(self,context): 35 | self.sync_palette_to_outputs() 36 | return None 37 | 38 | def update_colors(self,context): 39 | ng = self.node_tree 40 | if (self.gamma_correction): 41 | set_ng_socket_defvalue(ng, 0, value=gamma_correct(self.color_active),) 42 | set_ng_socket_defvalue(ng, 1, value=gamma_correct(self.color_after1),) 43 | set_ng_socket_defvalue(ng, 2, value=gamma_correct(self.color_after2),) 44 | set_ng_socket_defvalue(ng, 3, value=gamma_correct(self.color_after3),) 45 | set_ng_socket_defvalue(ng, 4, value=gamma_correct(self.color_after4),) 46 | set_ng_socket_defvalue(ng, 5, value=gamma_correct(self.color_after5),) 47 | else: 48 | set_ng_socket_defvalue(ng, 0, value=self.color_active) 49 | set_ng_socket_defvalue(ng, 1, value=self.color_after1) 50 | set_ng_socket_defvalue(ng, 2, value=self.color_after2) 51 | set_ng_socket_defvalue(ng, 3, value=self.color_after3) 52 | set_ng_socket_defvalue(ng, 4, value=self.color_after4) 53 | set_ng_socket_defvalue(ng, 5, value=self.color_after5) 54 | return None 55 | 56 | palette_ptr : bpy.props.PointerProperty(type=bpy.types.Palette, update=update_signal) 57 | 58 | color_active : bpy.props.FloatVectorProperty( 59 | subtype='COLOR', 60 | default=(0,0,0,0), 61 | min=0, max=1, 62 | size=4, 63 | update=update_colors, 64 | ) 65 | color_active_viewer : bpy.props.FloatVectorProperty( 66 | subtype='COLOR_GAMMA', size=4, min=0, max=1, get=lambda s: s.color_active, set=lambda s,v: setattr(s, 'color_active', v),) 67 | color_after1 : bpy.props.FloatVectorProperty( 68 | subtype='COLOR', 69 | default=(0,0,0,0), 70 | min=0, max=1, 71 | size=4, 72 | update=update_colors, 73 | ) 74 | color_after1_viewer : bpy.props.FloatVectorProperty( 75 | subtype='COLOR_GAMMA', size=4, min=0, max=1, get=lambda s: s.color_after1, set=lambda s,v: setattr(s, 'color_after1', v),) 76 | color_after2 : bpy.props.FloatVectorProperty( 77 | subtype='COLOR', 78 | default=(0,0,0,0), 79 | min=0, max=1, 80 | size=4, 81 | update=update_colors, 82 | ) 83 | color_after2_viewer : bpy.props.FloatVectorProperty( 84 | subtype='COLOR_GAMMA', size=4, min=0, max=1, get=lambda s: s.color_after2, set=lambda s,v: setattr(s, 'color_after2', v),) 85 | color_after3 : bpy.props.FloatVectorProperty( 86 | subtype='COLOR', 87 | default=(0,0,0,0), 88 | min=0, max=1, 89 | size=4, 90 | update=update_colors, 91 | ) 92 | color_after3_viewer : bpy.props.FloatVectorProperty( 93 | subtype='COLOR_GAMMA', size=4, min=0, max=1, get=lambda s: s.color_after3, set=lambda s,v: setattr(s, 'color_after3', v),) 94 | color_after4 : bpy.props.FloatVectorProperty( 95 | subtype='COLOR', 96 | default=(0,0,0,0), 97 | min=0, max=1, 98 | size=4, 99 | update=update_colors, 100 | ) 101 | color_after4_viewer : bpy.props.FloatVectorProperty( 102 | subtype='COLOR_GAMMA', size=4, min=0, max=1, get=lambda s: s.color_after4, set=lambda s,v: setattr(s, 'color_after4', v),) 103 | color_after5 : bpy.props.FloatVectorProperty( 104 | subtype='COLOR', 105 | default=(0,0,0,0), 106 | min=0, max=1, 107 | size=4, 108 | update=update_colors, 109 | ) 110 | color_after5_viewer : bpy.props.FloatVectorProperty( 111 | subtype='COLOR_GAMMA', size=4, min=0, max=1, get=lambda s: s.color_after5, set=lambda s,v: setattr(s, 'color_after5', v),) 112 | gamma_correction : bpy.props.BoolProperty( 113 | default=True, 114 | name="Gamma Correction", 115 | description="Apply gamma correction to the color", 116 | update=update_colors, 117 | ) 118 | 119 | @classmethod 120 | def poll(cls, context): 121 | """mandatory poll""" 122 | return True 123 | 124 | def init(self, context,): 125 | """this fct run when appending the node for the first time""" 126 | 127 | name = f".{self.bl_idname}" 128 | 129 | ng = bpy.data.node_groups.get(name) 130 | if (ng is None): 131 | ng = create_new_nodegroup(name, 132 | tree_type=self.tree_type, 133 | out_sockets={ 134 | "Color Active" : "NodeSocketColor", 135 | "Color Neighbor 1" : "NodeSocketColor", 136 | "Color Neighbor 2" : "NodeSocketColor", 137 | "Color Neighbor 3" : "NodeSocketColor", 138 | "Color Neighbor 4" : "NodeSocketColor", 139 | "Color Neighbor 5" : "NodeSocketColor", 140 | }, 141 | ) 142 | 143 | ng = ng.copy() #always using a copy of the original ng 144 | self.node_tree = ng 145 | 146 | # assign black defaults.. 147 | self.color_active, self.color_after1, self.color_after2, self.color_after3, self.color_after4, self.color_after5 = self.get_active_palette_colors() 148 | 149 | self.width = 190 150 | 151 | return None 152 | 153 | def copy(self,node,): 154 | """fct run when dupplicating the node""" 155 | 156 | #NOTE: copy/paste can cause crashes, we use a timer to delay the action 157 | def delayed_copy(): 158 | self.node_tree = node.node_tree.copy() 159 | bpy.app.timers.register(delayed_copy, first_interval=0.01) 160 | 161 | return None 162 | 163 | def update(self): 164 | """generic update function""" 165 | 166 | cache_booster_nodes_parent_tree(self.id_data) 167 | 168 | return None 169 | 170 | def get_active_palette_colors(self) -> tuple: 171 | """Return the active palette color followed by five subsequent colors (RGBA). 172 | Wrap to the start if there are not enough colors after the active one. 173 | """ 174 | pal = self.palette_ptr 175 | if (pal is not None) and (pal.colors): 176 | colors = pal.colors 177 | 178 | # Find the active color and its index 179 | active_idx = 0 180 | active_color = None 181 | for i, color in enumerate(colors): 182 | if (color == colors.active): 183 | active_idx = i 184 | active_color = color 185 | break 186 | 187 | # Fallback: if active not found use first 188 | if (active_color is None): 189 | active_idx = 0 190 | active_color = colors[0] 191 | 192 | def to_rgba(pcol): 193 | r, g, b = pcol.color[:] 194 | return (float(r), float(g), float(b), 1.0) 195 | 196 | result = [to_rgba(active_color)] 197 | total = len(colors) 198 | for step in range(1, 6): 199 | idx = (active_idx + step) % total 200 | result.append(to_rgba(colors[idx])) 201 | return tuple(result) 202 | 203 | # Fallback: return six black colors 204 | black = (0.0, 0.0, 0.0, 1.0) 205 | return black, black, black, black, black, black 206 | 207 | def sync_palette_to_outputs(self) -> None: 208 | """Read active palette color and assign it to the output socket.""" 209 | 210 | ng = self.node_tree 211 | if (ng is None): 212 | return None 213 | self.color_active, self.color_after1, self.color_after2, self.color_after3, self.color_after4, self.color_after5 = self.get_active_palette_colors() 214 | 215 | return None 216 | 217 | def draw_label(self,): 218 | """node label""" 219 | if (self.label==''): 220 | return 'Color Palette' 221 | return self.label 222 | 223 | def draw_buttons(self, context, layout,): 224 | """node interface drawing""" 225 | 226 | row = layout.row(align=True) 227 | row.context_pointer_set("node_context", self) 228 | row.template_ID(self, "palette_ptr", new="nodebooster.initalize_palette") 229 | 230 | row = layout.row(align=True) 231 | addend = '_viewer' if self.gamma_correction else '' 232 | row.prop(self, f"color_active{addend}", text="") 233 | row.prop(self, f"color_after1{addend}", text="") 234 | row.prop(self, f"color_after2{addend}", text="") 235 | row.prop(self, f"color_after3{addend}", text="") 236 | row.prop(self, f"color_after4{addend}", text="") 237 | row.prop(self, f"color_after5{addend}", text="") 238 | 239 | row = layout.row(align=True) 240 | row.prop(self, "gamma_correction",) 241 | 242 | layout.template_palette(self, "palette_ptr",) 243 | 244 | return None 245 | 246 | def draw_panel(self, layout, context): 247 | """draw in the nodebooster N panel 'Active Node'""" 248 | 249 | header, panel = layout.panel("palette_panelid", default_closed=False,) 250 | header.label(text="Palette",) 251 | if (panel): 252 | row = panel.row(align=True) 253 | row.context_pointer_set("node_context", self) 254 | row.template_ID(self, "palette_ptr", new="nodebooster.initalize_palette") 255 | 256 | row = layout.row(align=True) 257 | addend = '_viewer' if self.gamma_correction else '' 258 | row.prop(self, f"color_active{addend}", text="") 259 | row.prop(self, f"color_after1{addend}", text="") 260 | row.prop(self, f"color_after2{addend}", text="") 261 | row.prop(self, f"color_after3{addend}", text="") 262 | row.prop(self, f"color_after4{addend}", text="") 263 | row.prop(self, f"color_after5{addend}", text="") 264 | 265 | row = layout.row(align=True) 266 | row.prop(self, "gamma_correction",) 267 | 268 | panel.template_palette(self, "palette_ptr", color=True) 269 | 270 | return None 271 | 272 | def _draw_palette_swatches(self, layout, pal): 273 | """Draw a compact grid of palette color swatches editable in-place.""" 274 | if (pal is None) or (not pal.colors): 275 | return None 276 | columns = 8 277 | col = layout.column(align=True) 278 | row = None 279 | for i, pcol in enumerate(pal.colors): 280 | if (i % columns) == 0: 281 | row = col.row(align=True) 282 | cell = row.column(align=True) 283 | cell.scale_x = 0.6 284 | cell.scale_y = 0.7 285 | cell.prop(pcol, "color", text="") 286 | return None 287 | 288 | @classmethod 289 | def update_all(cls, using_nodes=None, signal_from_handlers=False,): 290 | """Refresh all nodes outputs with current active palette color.""" 291 | 292 | # defer import to avoid circular issues 293 | from ..utils.node_utils import get_booster_nodes 294 | 295 | if (using_nodes is None): 296 | nodes = get_booster_nodes(by_idnames={cls.bl_idname},) 297 | else: nodes = [n for n in using_nodes if (n.bl_idname==cls.bl_idname)] 298 | 299 | for n in nodes: 300 | if (n.mute): 301 | continue 302 | n.sync_palette_to_outputs() 303 | continue 304 | 305 | return None 306 | 307 | 308 | #Per Node-Editor Children: 309 | #Respect _NG_ + _GN_/_SH_/_CP_ nomenclature 310 | 311 | class NODEBOOSTER_NG_GN_ColorPalette(Base, bpy.types.GeometryNodeCustomGroup): 312 | tree_type = "GeometryNodeTree" 313 | bl_idname = "GeometryNode" + Base.bl_idname 314 | 315 | class NODEBOOSTER_NG_SH_ColorPalette(Base, bpy.types.ShaderNodeCustomGroup): 316 | tree_type = "ShaderNodeTree" 317 | bl_idname = "ShaderNode" + Base.bl_idname 318 | 319 | class NODEBOOSTER_NG_CP_ColorPalette(Base, bpy.types.CompositorNodeCustomGroup): 320 | tree_type = "CompositorNodeTree" 321 | bl_idname = "CompositorNode" + Base.bl_idname 322 | 323 | 324 | --------------------------------------------------------------------------------