3
$\begingroup$

I want to draw a diagram using shapes such as triangles and squares in the node editor. How can I do this?

$\endgroup$

2 Answers 2

12
$\begingroup$

enter image description here

Example using only gpu and blf modules for Blender 3.5+. As of 3.5 the bgl module is considered deprecated as OpenGL is just being replaced by Metal and Vulkan:

import bpy
import math
import gpu, blf
from gpu_extras.batch import batch_for_shader
from mathutils import Vector


def draw_mouse_path(coords, color, width=1.0):
    shader = gpu.shader.from_builtin('UNIFORM_COLOR')
    gpu.state.line_width_set(width)
    batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": coords})
    shader.uniform_float("color", color)
    batch.draw(shader)

def draw_poly(coords, color, width=1.0):
    shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
    gpu.state.line_width_set(width)
    batch = batch_for_shader(shader,'LINE_STRIP', {"pos": coords})
    shader.bind()
    shader.uniform_float("color", color)
    batch.draw(shader)

def draw_circle_2d(color, cx, cy, r, num_segments, width=1.0):
    theta = 2 * math.pi / num_segments
    c = math.cos(theta)
    s = math.sin(theta)
    x = r # we start at angle = 0
    y = 0
    
    vector_list = []
    for i in range (num_segments+1):
        vector_list.append(Vector((x + cx, y + cy)))
        t = x
        x = c * x - s * y
        y = s * t + c * y
    draw_poly(vector_list, color, width)
    
def draw_line_2d(color, start, end, width=1.0):
    shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
    gpu.state.line_width_set(width)
    batch = batch_for_shader(shader, 'LINES', {"pos": [start,end]})
    shader.bind()
    shader.uniform_float("color", color)
    batch.draw(shader)

def draw_type_2d(color, text, size=17):
    font_id = 0
    blf.position(font_id, 20, 70, 0)
    blf.color(font_id, *color)
    blf.size(font_id, size*(bpy.context.preferences.system.dpi/72))
    blf.draw(font_id, text)


def draw_callback_2d(self, context):
    # ...api_current/bpy.types.Area.html?highlight=bpy.types.area
    editor_width = context.area.width
    editor_height = context.area.height - context.area.regions[0].height
    
    # set the gpu state
    gpu.state.blend_set('ALPHA')
    
    # draw each shape
    draw_mouse_path(self.mouse_path, (1.0, 1.0, 1.0, 1.0), 1.0)
    draw_line_2d((0.0, 1.0, 0.0, 0.8), (0,0), (editor_width, editor_height), 3.0)
    draw_line_2d((1.0, 1.0, 0.0, 0.8), (editor_width, 0), (0, editor_height), 1.0)
    draw_circle_2d((1.0, 1.0, 1.0, 0.6), editor_width*.5, editor_height*.5, 70, 360, 1)
    draw_circle_2d((1.0, 0.0, 0.0, 0.4), editor_width*.5, editor_height*.5, 230, 5)
    
    # draw the text
    hud = "Hello Word {} {}".format(len(self.mouse_path), self.mouse_path[-1])
    draw_type_2d((1.0, 1.0, 1.0, 0.8), hud)
    
    # restore gpu defaults
    gpu.state.line_width_set(1.0)
    gpu.state.blend_set('NONE')


class ModalDrawOperator(bpy.types.Operator):
    """Draw 2d Operator"""
    bl_idname = "node.modal_draw_operator"
    bl_label = "Simple Modal Node Editor Operator"

    def modal(self, context, event):
        context.area.tag_redraw()

        if event.type == 'MOUSEMOVE':
            self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))

        elif event.type == 'LEFTMOUSE':
            bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
            return {'FINISHED'}

        elif event.type in {'RIGHTMOUSE', 'ESC'}:
            bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
            return {'CANCELLED'}

        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        if context.area.type == 'NODE_EDITOR':
            # the arguments we pass the the callback
            args = (self, context)
            # Add the region OpenGL drawing callback
            # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
            self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_2d, args, 'WINDOW', 'POST_PIXEL')

            self.mouse_path = []

            context.window_manager.modal_handler_add(self)
            return {'RUNNING_MODAL'}
        else:
            self.report({'WARNING'}, "NodeEditor not found, cannot run operator")
            return {'CANCELLED'}


def menu_func(self, context):
    self.layout.operator(ModalDrawOperator.bl_idname, text="Modal Draw Operator")


# Register and add to the "view" menu (required to also use F3 search "Modal Draw Operator" for quick access).
def register():
    bpy.utils.register_class(ModalDrawOperator)
    bpy.types.NODE_MT_view.append(menu_func)


def unregister():
    bpy.utils.unregister_class(ModalDrawOperator)
    bpy.types.NODE_MT_view.remove(menu_func)


if __name__ == "__main__":
    register()

Example using bgl, blf and gpu modules for Blender 2.8+ based on this answer:

import bpy
import bgl, blf, gpu
from gpu_extras.batch import batch_for_shader
from mathutils import Vector
import math


def draw_poly(coords, color, width):
    shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
    batch = batch_for_shader(shader,'LINE_STRIP', {"pos": coords})
    bgl.glLineWidth(width)
    shader.bind()
    shader.uniform_float("color", color)
    batch.draw(shader)

# based on http://slabode.exofire.net/circle_draw.shtml
def draw_circle_2d(color, cx, cy, r, num_segments):
    theta = 2 * math.pi / num_segments
    c = math.cos(theta)
    s = math.sin(theta)
    x = r # we start at angle = 0
    y = 0
    
    vector_list = []
    for i in range (num_segments+1):
        vector_list.append(Vector((x + cx, y + cy)))
        t = x
        x = c * x - s * y
        y = s * t + c * y           
    draw_poly(vector_list, color, 1)
    

def draw_line_2d(color, start, end):
    shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
    batch = batch_for_shader(shader, 'LINES', {"pos": [start,end]})
    shader.bind()
    shader.uniform_float("color", color)
    batch.draw(shader)
    
def draw_typo_2d(color, text):
    font_id = 0
    blf.position(font_id, 20, 70, 0)
    blf.color(font_id, color[0], color[1], color[2], color[3])
    blf.size(font_id, 20, 72)
    blf.draw(font_id, text)


def draw_callback_px(self, context):

    # ...api_current/bpy.types.Area.html?highlight=bpy.types.area
    width = context.area.width
    height = context.area.height - context.area.regions[0].height
    
    # 80% alpha, 2 pixel width line
    bgl.glEnable(bgl.GL_BLEND)
    bgl.glEnable(bgl.GL_LINE_SMOOTH)
    bgl.glEnable(bgl.GL_DEPTH_TEST)

    shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
    batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": self.mouse_path})
    shader.bind()
    shader.uniform_float("color", (1.0, 0.0, 1.0, 0.5))
    batch.draw(shader)

    draw_line_2d((0.0, 1.0, 0.0, 0.8), (0,0), (width, height))

    # yellow line
    draw_line_2d((1.0, 1.0, 0.0, 0.8), (width, 0), (0, height)) 

    # white circle
    draw_circle_2d((1.0, 1.0, 1.0, 0.8), width*.5, height*.5, 70, 360)

    # red circle
    draw_circle_2d((1.0, 0.0, 0.0, 0.4), width*.5, height*.5, 230, 5)

    # draw text
    draw_typo_2d((1.0, 0.0, 0.0, 1), "Hello Word " + str(len(self.mouse_path)))

    # restore opengl defaults
    bgl.glLineWidth(1)
    bgl.glDisable(bgl.GL_BLEND)
    bgl.glDisable(bgl.GL_LINE_SMOOTH)
    bgl.glEnable(bgl.GL_DEPTH_TEST)


class ModalDrawOperator(bpy.types.Operator):
    """Draw a line with the mouse"""
    bl_idname = "node.modal_operator"
    bl_label = "Simple Modal Node Editor Operator"

    def modal(self, context, event):
        context.area.tag_redraw()

        if event.type == 'MOUSEMOVE':
            self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))

        elif event.type == 'LEFTMOUSE':
            bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
            return {'FINISHED'}

        elif event.type in {'RIGHTMOUSE', 'ESC'}:
            bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
            return {'CANCELLED'}

        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        if context.area.type == 'NODE_EDITOR':
            # the arguments we pass the the callback
            args = (self, context)
            # Add the region OpenGL drawing callback
            # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
            self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL')

            self.mouse_path = []

            context.window_manager.modal_handler_add(self)
            return {'RUNNING_MODAL'}
        else:
            self.report({'WARNING'}, "NODE_EDITOR not found, cannot run operator")
            return {'CANCELLED'}


def register():
    bpy.utils.register_class(ModalDrawOperator)

def unregister():
    bpy.utils.unregister_class(ModalDrawOperator)

if __name__ == "__main__":
    register()

Documentation: https://docs.blender.org/api/current/gpu.html


Blender 2.7x

enter image description here

import bpy
import bgl
import blf
import math

# based on http://slabode.exofire.net/circle_draw.shtml
def draw_circle_2d(color, cx, cy, r, num_segments):
    theta = 2 * 3.1415926 / num_segments
    c = math.cos(theta) #precalculate the sine and cosine
    s = math.sin(theta)
    x = r # we start at angle = 0 
    y = 0
    bgl.glColor4f(*color)
    bgl.glBegin(bgl.GL_LINE_LOOP)
    for i in range (num_segments):
        bgl.glVertex2f(x + cx, y + cy) # output vertex 
        # apply the rotation matrix
        t = x
        x = c * x - s * y
        y = s * t + c * y
    bgl.glEnd()

def draw_line_2d(color, start, end):
    bgl.glColor4f(*color)
    bgl.glBegin(bgl.GL_LINES)
    bgl.glVertex2f(*start)
    bgl.glVertex2f(*end)
    bgl.glEnd()

def draw_typo_2d(color, text):
    font_id = 0  # XXX, need to find out how best to get this.
    # draw some text
    bgl.glColor4f(*color)
    blf.position(font_id, 20, 70, 0)
    blf.size(font_id, 20, 72)
    blf.draw(font_id, text)
    
def draw_callback_px(self, context):

    bgl.glPushAttrib(bgl.GL_ENABLE_BIT)
    # glPushAttrib is done to return everything to normal after drawing

    bgl.glLineStipple(10, 0x9999)
    bgl.glEnable(bgl.GL_LINE_STIPPLE)

    # 50% alpha, 2 pixel width line
    bgl.glEnable(bgl.GL_BLEND)
    bgl.glColor4f(1.0, 1.0, 1.0, 0.8)
    bgl.glLineWidth(5)

    bgl.glBegin(bgl.GL_LINE_STRIP)
    for x, y in self.mouse_path:
        bgl.glVertex2i(x, y)

    bgl.glEnd()
    bgl.glPopAttrib()
    
    bgl.glEnable(bgl.GL_BLEND)
    
    # ...api_current/bpy.types.Area.html?highlight=bpy.types.area
    header_height = context.area.regions[0].height # 26px
    width = context.area.width
    height = context.area.height - header_height
    
    p1_2d = (0,0)
    p2_2d = (width, height)
    p3_2d = (width, 0)
    p4_2d = (0, height)
    
    # green line
    bgl.glLineWidth(3)
    
    draw_line_2d((0.0, 1.0, 0.0, 0.8), p1_2d, p2_2d)
    
    # yellow line
    bgl.glLineWidth(5)
    draw_line_2d((1.0, 1.0, 0.0, 0.8), p3_2d, p4_2d) 
    
    # white circle
    bgl.glLineWidth(4)
    draw_circle_2d((1.0, 1.0, 1.0, 0.8), width/2, height/2, 70, 360)
    
    # red circle
    bgl.glLineWidth(5)
    draw_circle_2d((1.0, 0.0, 0.0, 0.4), width/2, height/2, 230, 5)
    
    # draw text
    draw_typo_2d((1.0, 1.0, 1.0, 1), "Hello Word " + str(len(self.mouse_path)))
    
    # restore opengl defaults
    bgl.glLineWidth(1)
    bgl.glDisable(bgl.GL_BLEND)
    bgl.glColor4f(0.0, 0.0, 0.0, 1.0)


class ModalDrawOperator(bpy.types.Operator):
    """Draw a line with the mouse"""
    bl_idname = "node.modal_operator"
    bl_label = "Simple Modal Node Editor Operator"

    def modal(self, context, event):
        context.area.tag_redraw()

        if event.type == 'MOUSEMOVE':
            self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))

        elif event.type == 'LEFTMOUSE':
            bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
            return {'FINISHED'}

        elif event.type in {'RIGHTMOUSE', 'ESC'}:
            bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
            return {'CANCELLED'}

        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        if context.area.type == 'NODE_EDITOR':
            # the arguments we pass the the callback
            args = (self, context)
            # Add the region OpenGL drawing callback
            # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
            self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL')

            self.mouse_path = []

            context.window_manager.modal_handler_add(self)
            return {'RUNNING_MODAL'}
        else:
            self.report({'WARNING'}, "NODE_EDITOR not found, cannot run operator")
            return {'CANCELLED'}


def register():
    bpy.utils.register_class(ModalDrawOperator)

def unregister():
    bpy.utils.unregister_class(ModalDrawOperator)

if __name__ == "__main__":
    register()

Documentation: https://docs.blender.org/api/current/bgl.html#module-bgl

$\endgroup$
3
$\begingroup$

Another take, from this answer, slightly modified for the node editor. The call to bpy.ops.object.modal_operator('INVOKE_SCREEN') is only needed to add the mouse event UI.

import bpy
import blf
from bpy.props import IntProperty, FloatProperty
import bgl

class BGLWidget:
    handle = None

    def __init__(self, op, context, areatype):

        # Calculate scroller width, dpi and pixelsize dependent
        self.pixel_size = context.user_preferences.system.pixel_size
        self.dpi = context.user_preferences.system.dpi
        self.dpi_fac = self.pixel_size * self.dpi / 72
        # A normal widget unit is 20, but the scroller is apparently 16
        self.scroller_width = 16 * self.dpi_fac

        self.op = op
        self.areatype = areatype

        self.handle = self.create_handle(context)
        theme = context.user_preferences.themes[0]
        self.theme = theme

    def create_handle(self, context):
        handle = self.areatype.draw_handler_add(
            self.draw_region,
            (context,),
           'WINDOW', 'POST_PIXEL')  
        return handle     

    def remove_handle(self):
        if self.handle:
            self.areatype.draw_handler_remove(self.handle, 'WINDOW') 
            self.handle = None   

    def draw_region(self, context):
        # check validity
        self.visualise(context)


    def draw_box(self, x, y, w, h, color=(0.0, 0.0, 0.0, 1.0)):
        #bgl.glDepthRange (0.1, 1.0)
        bgl.glColor4f(*color)
        bgl.glBegin(bgl.GL_QUADS)

        bgl.glVertex2f(x+w, y+h)
        bgl.glVertex2f(x, y+h) 
        bgl.glVertex2f(x, y) 
        bgl.glVertex2f(x+w, y)      
        bgl.glEnd()

    def visualise(self, context):
        # used to draw override in class def
        pass

class Button:
    def __init__(self, x, y, w, h, color=(1,1,1,1)):
        #draw a box
        self.x = 0
        self.y = 0
        self.w = w
        self.h = h
        self.color = color   

    def __str__(self):
        return "Button %s" % str(self.color)

    def __repr__(self):
        return "Button %d %d color(%s)" % (self.x, self.y, str(self.color))

    def in_box(self, x, y):
        return (self.x < x < self.x + self.w
                and self.y < y < self.y + self.h)


class ButtonWidget(BGLWidget):
    help_screen = -1
    buttons = []
    screen_buttons = {}
    def button(self, w, h):
        # add a new button
        b = Button(0, 0, w, h)
        self.buttons.append(b)
        return b


    def visualise(self, context):
        if self.help_screen > -1:
            print("HELP", self.help_screen)
            if context.screen.areas[self.help_screen] == context.area:
                self.draw_box(0, 0, 10000, 10000, color=(0, 1, 0, 1))
                context.area.tag_redraw()
        for b in self.buttons:
            self.draw_button(b, context)

    def draw_button(self, box, context):
        m = [i for i, a in enumerate(context.screen.areas) if a == context.area]
        if not len(m):
            return None
        key = "area%d" % m[0]
        b = self.screen_buttons.setdefault(key, Button(box.x, box.y, box.w, box.h, color=box.color))
        b.x = context.region.width - b.w - self.scroller_width
        b.y = context.region.height - b.h - self.scroller_width
        #print(b.x, b.y, b.w, b.h)  # debug shows box coords on draw
        self.draw_box(b.x, b.y, b.w, b.h, color=b.color)
        #self.screen_buttons[key] = b

    def mouse_over(self, screen, area_index, x, y):
        key = "area%d" % area_index
        box = self.screen_buttons.get(key, None)
        if box:
            area = screen.areas[area_index]
            if box.in_box(x, y):
                box.color = (1, 0, 0, 0)
                self.help_screen = area_index
                area.tag_redraw()
            else:
                self.help_screen = -1
                box.color = (0, 0, 1, 0)
            #self.screen_buttons[key] = box
            area.tag_redraw()


class ModalOperator(bpy.types.Operator):
    bl_idname = "object.modal_operator"
    bl_label = "Simple Modal Operator"

    def modal(self, context, event):
        def in_area(area, x, y):

            return (area.x < x < area.x + area.width 
                and area.y < y < area.y + area.height)

        screen = context.screen
        mx = event.mouse_x
        my = event.mouse_y
        #print(mx, my)        
        areas = [i for i, a in enumerate(screen.areas) if a.type.startswith('NODE_EDITOR')
                 and in_area(a, mx, my)]        

        for i in areas:
            a = screen.areas[i]
            region = a.regions[-1]
            x = mx - region.x
            y = my - region.y
            ui.mouse_over(screen, i, x, y)
            if event.type == 'LEFTMOUSE':

                print('PRESS in screen["%s"].areas["%d"]' % (screen.name, i))
                #click events ???

        if event.type in {'ESC'}:
            # dont have to remove the UI here

            ui.remove_handle()
            return {'CANCELLED'}

        return {'PASS_THROUGH'}

    def invoke(self, context, event):
        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}

def register():
    bpy.utils.register_class(ModalOperator)


def unregister():
    bpy.utils.unregister_class(ModalOperator)

if __name__ == "__main__":
    register()

    # create a UI
    context = bpy.context                 
    h = 50
    w = 200
    ui = ButtonWidget(None, context, bpy.types.SpaceNodeEditor)
    button = ui.button(w, h)
    for a in context.screen.areas:
        if a.type == 'NODE_EDITOR':
            a.tag_redraw()
    bpy.ops.object.modal_operator('INVOKE_SCREEN')
$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .