I want to draw a diagram using shapes such as triangles and squares in the node editor. How can I do this?
2 Answers
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
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
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')