3

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

David
  • 49,291
  • 38
  • 159
  • 317
John Roper
  • 695
  • 11
  • 22

2 Answers2

12

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

p2or
  • 15,860
  • 10
  • 83
  • 143
3

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')
batFINGER
  • 84,216
  • 10
  • 108
  • 233