914 lines
35 KiB
Python
914 lines
35 KiB
Python
'''
|
|
Copyright (C) 2023 CG Cookie
|
|
http://cgcookie.com
|
|
hello@cgcookie.com
|
|
|
|
Created by Jonathan Denning, Jonathan Williamson
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
'''
|
|
|
|
import os
|
|
import re
|
|
import math
|
|
import time
|
|
import ctypes
|
|
import random
|
|
from typing import List
|
|
import traceback
|
|
import functools
|
|
import contextlib
|
|
import urllib.request
|
|
from functools import wraps
|
|
from itertools import chain
|
|
|
|
import bpy
|
|
import gpu
|
|
from bpy.types import BoolProperty
|
|
from mathutils import Matrix, Vector
|
|
from bpy_extras.view3d_utils import location_3d_to_region_2d, region_2d_to_vector_3d
|
|
from bpy_extras.view3d_utils import region_2d_to_location_3d, region_2d_to_origin_3d
|
|
|
|
from .blender import bversion, get_path_from_addon_root, get_path_from_addon_common
|
|
from .blender_cursors import Cursors
|
|
from .blender_preferences import get_preferences
|
|
from .debug import dprint, debugger
|
|
from .decorators import blender_version_wrapper, add_cache, only_in_blender_version
|
|
from .fontmanager import FontManager as fm
|
|
from .functools import find_fns
|
|
from .globals import Globals
|
|
from .hasher import Hasher
|
|
from .maths import Point2D, Vec2D, Point, Ray, Direction, mid, Color, Normal, Frame
|
|
from .profiler import profiler
|
|
from .utils import iter_pairs
|
|
from . import gpustate
|
|
|
|
|
|
class Drawing:
|
|
_instance = None
|
|
_dpi_mult = 1
|
|
_custom_dpi_mult = 1
|
|
_prefs = get_preferences()
|
|
_error_check = True
|
|
_error_count = 0
|
|
_error_limit = 10 # after this many check errors, no more will be reported to console
|
|
|
|
@staticmethod
|
|
def get_custom_dpi_mult():
|
|
return Drawing._custom_dpi_mult
|
|
@staticmethod
|
|
def set_custom_dpi_mult(v):
|
|
Drawing._custom_dpi_mult = v
|
|
Drawing.update_dpi()
|
|
|
|
@staticmethod
|
|
def update_dpi():
|
|
# print(f'view.ui_scale={Drawing._prefs.view.ui_scale}, system.ui_scale={Drawing._prefs.system.ui_scale}, system.dpi={Drawing._prefs.system.dpi}')
|
|
Drawing._dpi_mult = (
|
|
1.0
|
|
* Drawing._custom_dpi_mult
|
|
# * Drawing._prefs.view.ui_scale
|
|
* max(0.25, Drawing._prefs.system.ui_scale) # math.floor(Drawing._prefs.system.ui_scale))
|
|
# * (72.0 / Drawing._prefs.system.dpi)
|
|
# * Drawing._prefs.system.pixel_size
|
|
)
|
|
|
|
@staticmethod
|
|
def initialize():
|
|
Drawing.update_dpi()
|
|
if Globals.is_set('drawing'): return
|
|
Drawing._creating = True
|
|
Globals.set(Drawing())
|
|
del Drawing._creating
|
|
Drawing._instance = Globals.drawing
|
|
|
|
def __init__(self):
|
|
assert hasattr(self, '_creating'), "Do not instantiate directly. Use Drawing.get_instance()"
|
|
|
|
self.area,self.space,self.rgn,self.r3d,self.window = None,None,None,None,None
|
|
# self.font_id = 0
|
|
self.last_font_key = None
|
|
self.fontid = 0
|
|
self.fontsize = None
|
|
self.fontsize_scaled = None
|
|
self.line_cache = {}
|
|
self.size_cache = {}
|
|
self.set_font_size(12)
|
|
self._pixel_matrix = None
|
|
|
|
def set_region(self, area, space, rgn, r3d, window):
|
|
self.area = area
|
|
self.space = space
|
|
self.rgn = rgn
|
|
self.r3d = r3d
|
|
self.window = window
|
|
|
|
@staticmethod
|
|
def set_cursor(cursor): Cursors.set(cursor)
|
|
|
|
def scale(self, s): return s * self._dpi_mult if s is not None else None
|
|
def unscale(self, s): return s / self._dpi_mult if s is not None else None
|
|
def get_dpi_mult(self): return self._dpi_mult
|
|
def get_pixel_size(self): return self._pixel_size
|
|
def line_width(self, width): gpustate.line_width(max(1, self.scale(width)))
|
|
def point_size(self, size): gpustate.point_size(max(1, self.scale(size)))
|
|
|
|
def set_font_color(self, fontid, color):
|
|
fm.color(color, fontid=fontid)
|
|
|
|
def set_font_size(self, fontsize, fontid=None, force=False):
|
|
if fontid is None: fontid = fm._last_fontid
|
|
else: fontid = fm.load(fontid)
|
|
fontsize_prev = self.fontsize
|
|
fontsize, fontsize_scaled = int(fontsize), int(int(fontsize) * self._dpi_mult)
|
|
cache_key = (fontid, fontsize_scaled)
|
|
if self.last_font_key == cache_key and not force: return fontsize_prev
|
|
fm.size(fontsize_scaled, fontid=fontid)
|
|
if cache_key not in self.line_cache:
|
|
# cache away useful details about font (line height, line base)
|
|
# dprint('Caching new scaled font size:', cache_key)
|
|
pass
|
|
all_chars = ''.join([
|
|
'abcdefghijklmnopqrstuvwxyz',
|
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
|
'0123456789',
|
|
'!@#$%%^&*()`~[}{]/?=+\\|-_\'",<.>',
|
|
'ΑαΒβΓγΔδΕεΖζΗηΘθΙιΚκΛλΜμΝνΞξΟοΠπΡρΣσςΤτΥυΦφΧχΨψΩω',
|
|
])
|
|
all_caps = all_chars.upper()
|
|
self.line_cache[cache_key] = {
|
|
'line height': math.ceil(fm.dimensions(all_chars, fontid=fontid)[1] + self.scale(4)),
|
|
'line base': math.ceil(fm.dimensions(all_caps, fontid=fontid)[1]),
|
|
}
|
|
info = self.line_cache[cache_key]
|
|
self.line_height = info['line height']
|
|
self.line_base = info['line base']
|
|
self.fontid = fontid
|
|
self.fontsize = fontsize
|
|
self.fontsize_scaled = fontsize_scaled
|
|
self.last_font_key = cache_key
|
|
|
|
return fontsize_prev
|
|
|
|
def get_text_size_info(self, text, item, fontsize=None, fontid=None):
|
|
if fontsize or fontid: size_prev = self.set_font_size(fontsize, fontid=fontid)
|
|
|
|
if text is None: text, lines = '', []
|
|
elif type(text) is list: text, lines = '\n'.join(text), text
|
|
else: text, lines = text, text.splitlines()
|
|
|
|
fontid = fm.load(fontid)
|
|
key = (text, self.fontsize_scaled, fontid)
|
|
# key = (text, self.fontsize_scaled, self.font_id)
|
|
if key not in self.size_cache:
|
|
d = {}
|
|
if not text:
|
|
d['width'] = 0
|
|
d['height'] = 0
|
|
d['line height'] = self.line_height
|
|
else:
|
|
get_width = lambda t: math.ceil(fm.dimensions(t, fontid=fontid)[0])
|
|
get_height = lambda t: math.ceil(fm.dimensions(t, fontid=fontid)[1])
|
|
d['width'] = max(get_width(l) for l in lines)
|
|
d['height'] = get_height(text)
|
|
d['line height'] = self.line_height * len(lines)
|
|
self.size_cache[key] = d
|
|
if False:
|
|
print('')
|
|
print('--------------------------------------')
|
|
print('> computed new size')
|
|
print('> key: %s' % str(key))
|
|
print('> size: %s' % str(d))
|
|
print('--------------------------------------')
|
|
print('')
|
|
if fontsize: self.set_font_size(size_prev, fontid=fontid)
|
|
return self.size_cache[key][item]
|
|
|
|
def get_text_width(self, text, fontsize=None, fontid=None):
|
|
return self.get_text_size_info(text, 'width', fontsize=fontsize, fontid=fontid)
|
|
def get_text_height(self, text, fontsize=None, fontid=None):
|
|
return self.get_text_size_info(text, 'height', fontsize=fontsize, fontid=fontid)
|
|
def get_line_height(self, text=None, fontsize=None, fontid=None):
|
|
return self.get_text_size_info(text, 'line height', fontsize=fontsize, fontid=fontid)
|
|
|
|
def set_clipping(self, xmin, ymin, xmax, ymax, fontid=None):
|
|
fm.clipping((xmin, ymin), (xmax, ymax), fontid=fontid)
|
|
# blf.clipping(self.font_id, xmin, ymin, xmax, ymax)
|
|
self.enable_clipping()
|
|
def enable_clipping(self, fontid=None):
|
|
fm.enable_clipping(fontid=fontid)
|
|
# blf.enable(self.font_id, blf.CLIPPING)
|
|
def disable_clipping(self, fontid=None):
|
|
fm.disable_clipping(fontid=fontid)
|
|
# blf.disable(self.font_id, blf.CLIPPING)
|
|
|
|
def text_color_set(self, color, fontid):
|
|
if color is not None: fm.color(color, fontid=fontid)
|
|
|
|
def text_draw2D(self, text, pos:Point2D, *, color=None, dropshadow=None, fontsize=None, fontid=None, lineheight=True):
|
|
if fontsize: size_prev = self.set_font_size(fontsize, fontid=fontid)
|
|
|
|
lines = str(text).splitlines()
|
|
l,t = round(pos[0]),round(pos[1])
|
|
lh,lb = self.line_height,self.line_base
|
|
|
|
if dropshadow:
|
|
self.text_draw2D(text, (l+1,t-1), color=dropshadow, fontsize=fontsize, fontid=fontid, lineheight=lineheight)
|
|
|
|
gpustate.blend('ALPHA')
|
|
self.text_color_set(color, fontid)
|
|
for line in lines:
|
|
fm.draw(line, xyz=(l, t - lb, 0), fontid=fontid)
|
|
t -= lh if lineheight else self.get_text_height(line)
|
|
|
|
if fontsize: self.set_font_size(size_prev, fontid=fontid)
|
|
|
|
def text_draw2D_simple(self, text, pos:Point2D):
|
|
l,t = round(pos[0]),round(pos[1])
|
|
lb = self.line_base
|
|
fm.draw_simple(text, xyz=(l, t - lb, 0))
|
|
|
|
|
|
def get_mvp_matrix(self, view3D=True):
|
|
'''
|
|
if view3D == True: returns MVP for 3D view
|
|
else: returns MVP for pixel view
|
|
TODO: compute separate M,V,P matrices
|
|
'''
|
|
if not self.r3d: return None
|
|
if view3D:
|
|
# 3D view
|
|
return self.r3d.perspective_matrix
|
|
else:
|
|
# pixel view
|
|
return self.get_pixel_matrix()
|
|
|
|
mat_model = Matrix()
|
|
mat_view = Matrix()
|
|
mat_proj = Matrix()
|
|
|
|
view_loc = self.r3d.view_location # vec
|
|
view_rot = self.r3d.view_rotation # quat
|
|
view_per = self.r3d.view_perspective # 'PERSP' or 'ORTHO'
|
|
|
|
return mat_model,mat_view,mat_proj
|
|
|
|
def get_pixel_matrix_list(self):
|
|
if not self.r3d: return None
|
|
x,y = self.rgn.x,self.rgn.y
|
|
w,h = self.rgn.width,self.rgn.height
|
|
ww,wh = self.window.width,self.window.height
|
|
return [[2/w,0,0,-1], [0,2/h,0,-1], [0,0,1,0], [0,0,0,1]]
|
|
|
|
def load_pixel_matrix(self, m):
|
|
self._pixel_matrix = m
|
|
|
|
@add_cache('_cache', {'w':-1, 'h':-1, 'm':None})
|
|
def get_pixel_matrix(self):
|
|
'''
|
|
returns MVP for pixel view
|
|
TODO: compute separate M,V,P matrices
|
|
'''
|
|
if not self.r3d: return None
|
|
if self._pixel_matrix: return self._pixel_matrix
|
|
w,h = self.rgn.width,self.rgn.height
|
|
cache = self.get_pixel_matrix._cache
|
|
if cache['w'] != w or cache['h'] != h:
|
|
mx, my, mw, mh = -1, -1, 2 / w, 2 / h
|
|
cache['w'],cache['h'] = w,h
|
|
cache['m'] = Matrix([
|
|
[ mw, 0, 0, mx],
|
|
[ 0, mh, 0, my],
|
|
[ 0, 0, 1, 0],
|
|
[ 0, 0, 0, 1]
|
|
])
|
|
return cache['m']
|
|
|
|
def get_view_matrix_list(self):
|
|
return list(self.get_view_matrix()) if self.r3d else None
|
|
|
|
def get_view_matrix(self):
|
|
return self.r3d.perspective_matrix if self.r3d else None
|
|
|
|
def get_view_version(self):
|
|
if not self.r3d: return None
|
|
return Hasher(self.r3d.view_matrix, self.space.lens, self.r3d.view_distance)
|
|
|
|
@staticmethod
|
|
def glCheckError(title, **kwargs):
|
|
return gpustate.get_glerror(title, **kwargs)
|
|
|
|
@staticmethod
|
|
@contextlib.contextmanager
|
|
def glCheckError_wrap(title, *, stop_on_error=False):
|
|
if Drawing.glCheckError(f'addon common: pre {title}') and stop_on_error: return True
|
|
yield None
|
|
if Drawing.glCheckError(f'addon common: post {title}') and stop_on_error: return True
|
|
return False
|
|
|
|
def get_view_origin(self, *, orthographic_distance=1000):
|
|
focus = self.r3d.view_location
|
|
rot = self.r3d.view_rotation
|
|
dist = self.r3d.view_distance if self.r3d.is_perspective else orthographic_distance
|
|
return focus + (rot @ Vector((0, 0, dist)))
|
|
|
|
# # the following fails in weird ways when in orthographic projection
|
|
# center = Point2D((self.area.width / 2, self.area.height / 2))
|
|
# return Point(region_2d_to_origin_3d(self.rgn, self.r3d, center))
|
|
|
|
def Point2D_to_Ray(self, p2d):
|
|
o = Point(region_2d_to_origin_3d(self.rgn, self.r3d, p2d))
|
|
d = Direction(region_2d_to_vector_3d(self.rgn, self.r3d, p2d))
|
|
return Ray(o, d)
|
|
|
|
def Point_to_Point2D(self, p3d):
|
|
return Point2D(location_3d_to_region_2d(self.rgn, self.r3d, p3d))
|
|
|
|
@blender_version_wrapper('>=', '2.80')
|
|
def draw2D_point(self, pt:Point2D, color:Color, *, radius=1, border=0, borderColor=None):
|
|
radius = self.scale(radius)
|
|
border = self.scale(border)
|
|
if borderColor is None: borderColor = (0,0,0,0)
|
|
shader_2D_point.bind()
|
|
ubos_2D_point.options.screensize = (self.area.width, self.area.height, 0, 0)
|
|
ubos_2D_point.options.mvpmatrix = self.get_pixel_matrix()
|
|
ubos_2D_point.options.radius_border = (radius, border, 0, 0)
|
|
ubos_2D_point.options.color = color
|
|
ubos_2D_point.options.colorBorder = borderColor
|
|
ubos_2D_point.options.center = (*pt, 0, 1)
|
|
ubos_2D_point.update_shader()
|
|
batch_2D_point.draw(shader_2D_point)
|
|
gpu.shader.unbind()
|
|
|
|
@blender_version_wrapper('>=', '2.80')
|
|
def draw2D_points(self, pts:[Point2D], color:Color, *, radius=1, border=0, borderColor=None):
|
|
radius = self.scale(radius)
|
|
border = self.scale(border)
|
|
if borderColor is None: borderColor = (0,0,0,0)
|
|
shader_2D_point.bind()
|
|
ubos_2D_point.options.screensize = (self.area.width, self.area.height, 0, 0)
|
|
ubos_2D_point.options.mvpmatrix = self.get_pixel_matrix()
|
|
ubos_2D_point.options.radius_border = (radius, border, 0, 0)
|
|
ubos_2D_point.options.color = color
|
|
ubos_2D_point.options.colorBorder = borderColor
|
|
for pt in pts:
|
|
ubos_2D_point.options.center = (*pt, 0, 1)
|
|
ubos_2D_point.update_shader()
|
|
batch_2D_point.draw(shader_2D_point)
|
|
gpu.shader.unbind()
|
|
|
|
# draw line segment in screen space
|
|
def draw2D_line(self, p0:Point2D, p1:Point2D, color0:Color, *, color1=None, width=1, stipple=None, offset=0):
|
|
if color1 is None: color1 = (color0[0],color0[1],color0[2],0)
|
|
width = self.scale(width)
|
|
stipple = [self.scale(v) for v in stipple] if stipple else [1.0, 0.0]
|
|
offset = self.scale(offset)
|
|
shader_2D_lineseg.bind()
|
|
ubos_2D_lineseg.options.MVPMatrix = self.get_pixel_matrix()
|
|
ubos_2D_lineseg.options.screensize = (self.area.width, self.area.height, 0, 0)
|
|
ubos_2D_lineseg.options.pos0 = (*p0, 0, 1)
|
|
ubos_2D_lineseg.options.color0 = color0
|
|
ubos_2D_lineseg.options.pos1 = (*p1, 0, 1)
|
|
ubos_2D_lineseg.options.color1 = color1
|
|
ubos_2D_lineseg.options.stipple_width = (stipple[0], stipple[1], offset, width)
|
|
ubos_2D_lineseg.update_shader()
|
|
batch_2D_lineseg.draw(shader_2D_lineseg)
|
|
gpu.shader.unbind()
|
|
|
|
def draw2D_lines(self, points, color0:Color, *, color1=None, width=1, stipple=None, offset=0):
|
|
self.glCheckError('starting draw2D_lines')
|
|
if color1 is None: color1 = (color0[0],color0[1],color0[2],0)
|
|
width = self.scale(width)
|
|
stipple = [self.scale(v) for v in stipple] if stipple else [1.0, 0.0]
|
|
offset = self.scale(offset)
|
|
shader_2D_lineseg.bind()
|
|
ubos_2D_lineseg.options.MVPMatrix = self.get_pixel_matrix()
|
|
ubos_2D_lineseg.options.screensize = (self.area.width, self.area.height, 0, 0)
|
|
ubos_2D_lineseg.options.color0 = color0
|
|
ubos_2D_lineseg.options.color1 = color1
|
|
ubos_2D_lineseg.options.stipple_width = (stipple[0], stipple[1], offset, width)
|
|
for i in range(len(points)//2):
|
|
p0,p1 = points[i*2:i*2+2]
|
|
if p0 is None or p1 is None: continue
|
|
ubos_2D_lineseg.options.pos0 = (*p0, 0, 1)
|
|
ubos_2D_lineseg.options.pos1 = (*p1, 0, 1)
|
|
ubos_2D_lineseg.update_shader()
|
|
batch_2D_lineseg.draw(shader_2D_lineseg)
|
|
gpu.shader.unbind()
|
|
self.glCheckError('done with draw2D_lines')
|
|
|
|
def draw3D_lines(self, points, color0:Color, *, color1=None, width=1, stipple=None, offset=0):
|
|
self.glCheckError('starting draw3D_lines')
|
|
if color1 is None: color1 = (color0[0],color0[1],color0[2],0)
|
|
width = self.scale(width)
|
|
stipple = [self.scale(v) for v in stipple] if stipple else [1.0, 0.0]
|
|
offset = self.scale(offset)
|
|
shader_2D_lineseg.bind()
|
|
ubos_2D_lineseg.options.screensize = (self.area.width, self.area.height)
|
|
ubos_2D_lineseg.options.color0 = color0
|
|
ubos_2D_lineseg.options.color1 = color1
|
|
ubos_2D_lineseg.options.stipple_width = (stipple[0], stipple[1], offset, width)
|
|
ubos_2D_lineseg.options.MVPMatrix = self.get_view_matrix()
|
|
for i in range(len(points)//2):
|
|
p0,p1 = points[i*2:i*2+2]
|
|
if p0 is None or p1 is None: continue
|
|
ubos_2D_lineseg.options.pos0 = (*p0, 0, 1)
|
|
ubos_2D_lineseg.options.pos1 = (*p1, 0, 1)
|
|
ubos_2D_lineseg.update_shader()
|
|
batch_2D_lineseg.draw(shader_2D_lineseg)
|
|
gpu.shader.unbind()
|
|
self.glCheckError('done with draw3D_lines')
|
|
|
|
def draw2D_linestrip(self, points, color0:Color, *, color1=None, width=1, stipple=None, offset=0):
|
|
if color1 is None: color1 = (color0[0],color0[1],color0[2],0)
|
|
width = self.scale(width)
|
|
stipple = [self.scale(v) for v in stipple] if stipple else [1.0, 0.0]
|
|
offset = self.scale(offset)
|
|
shader_2D_lineseg.bind()
|
|
ubos_2D_lineseg.options.MVPMatrix = self.get_pixel_matrix()
|
|
ubos_2D_lineseg.options.screensize = (self.area.width, self.area.height)
|
|
ubos_2D_lineseg.options.color0 = color0
|
|
ubos_2D_lineseg.options.color1 = color1
|
|
for p0,p1 in iter_pairs(points, False):
|
|
ubos_2D_lineseg.options.pos0 = (*p0, 0, 1)
|
|
ubos_2D_lineseg.options.pos1 = (*p1, 0, 1)
|
|
ubos_2D_lineseg.options.stipple_width = (stipple[0], stipple[1], offset, width)
|
|
ubos_2D_lineseg.update_shader()
|
|
batch_2D_lineseg.draw(shader_2D_lineseg)
|
|
offset += (p1 - p0).length
|
|
gpu.shader.unbind()
|
|
|
|
# draw circle in screen space
|
|
def draw2D_circle(self, center:Point2D, radius:float, color0:Color, *, color1=None, width=1, stipple=None, offset=0):
|
|
if color1 is None: color1 = (color0[0],color0[1],color0[2],0)
|
|
radius = self.scale(radius)
|
|
width = self.scale(width)
|
|
stipple = [self.scale(v) for v in stipple] if stipple else [1,0]
|
|
offset = self.scale(offset)
|
|
shader_2D_circle.bind()
|
|
ubos_2D_circle.options.MVPMatrix = self.get_pixel_matrix()
|
|
ubos_2D_circle.options.screensize = (self.area.width, self.area.height, 0.0, 0.0)
|
|
ubos_2D_circle.options.center = (center.x, center.y, 0.0, 0.0)
|
|
ubos_2D_circle.options.color0 = color0
|
|
ubos_2D_circle.options.color1 = color1
|
|
ubos_2D_circle.options.radius_width = (radius, width, 0.0, 0.0)
|
|
ubos_2D_circle.options.stipple_data = (*stipple, offset, 0.0)
|
|
ubos_2D_circle.update_shader()
|
|
batch_2D_circle.draw(shader_2D_circle)
|
|
gpu.shader.unbind()
|
|
|
|
def draw3D_circle(self, center:Point, radius:float, color:Color, *, width=1, n:Normal=None, x:Direction=None, y:Direction=None, depth_near=0, depth_far=1):
|
|
assert n is not None or x is not None or y is not None, 'Must specify at least one of n,x,y'
|
|
f = Frame(o=center, x=x, y=y, z=n)
|
|
radius = self.scale(radius)
|
|
width = self.scale(width)
|
|
shader_3D_circle.bind()
|
|
ubos_3D_circle.options.MVPMatrix = self.get_view_matrix()
|
|
ubos_3D_circle.options.screensize = (self.area.width, self.area.height, 0.0, 0.0)
|
|
ubos_3D_circle.options.center = f.o
|
|
ubos_3D_circle.options.color = color
|
|
ubos_3D_circle.options.plane_x = f.x
|
|
ubos_3D_circle.options.plane_y = f.y
|
|
ubos_3D_circle.options.settings = (radius, width, depth_near, depth_far)
|
|
ubos_3D_circle.update_shader()
|
|
batch_3D_circle.draw(shader_3D_circle)
|
|
gpu.shader.unbind()
|
|
|
|
def draw3D_triangles(self, points:[Point], colors:[Color]):
|
|
self.glCheckError('starting draw3D_triangles')
|
|
shader_3D_triangle.bind()
|
|
ubos_3D_triangle.options.MVPMatrix = self.get_view_matrix()
|
|
for i in range(0, len(points), 3):
|
|
p0,p1,p2 = points[i:i+3]
|
|
c0,c1,c2 = colors[i:i+3]
|
|
if p0 is None or p1 is None or p2 is None: continue
|
|
if c0 is None or c1 is None or c2 is None: continue
|
|
ubos_3D_triangle.options.pos0 = p0
|
|
ubos_3D_triangle.options.color0 = c0
|
|
ubos_3D_triangle.options.pos1 = p1
|
|
ubos_3D_triangle.options.color1 = c1
|
|
ubos_3D_triangle.options.pos2 = p2
|
|
ubos_3D_triangle.options.color2 = c2
|
|
ubos_3D_triangle.update_shader()
|
|
batch_3D_triangle.draw(shader_3D_triangle)
|
|
gpu.shader.unbind()
|
|
self.glCheckError('done with draw3D_triangles')
|
|
|
|
@contextlib.contextmanager
|
|
def draw(self, draw_type:"CC_DRAW"):
|
|
assert getattr(self, '_draw', None) is None, 'Cannot nest Drawing.draw calls'
|
|
self._draw = draw_type
|
|
self.glCheckError('starting draw')
|
|
try:
|
|
draw_type.begin()
|
|
yield draw_type
|
|
draw_type.end()
|
|
except Exception as e:
|
|
print('Exception caught while in Drawing.draw with %s' % str(draw_type))
|
|
debugger.print_exception()
|
|
self.glCheckError('done with draw')
|
|
self._draw = None
|
|
|
|
if not bpy.app.background:
|
|
Drawing.glCheckError(f'pre-init check: Drawing')
|
|
Drawing.initialize()
|
|
Drawing.glCheckError(f'post-init check: Drawing')
|
|
|
|
|
|
|
|
|
|
if not bpy.app.background and bpy.app.version >= (3, 2, 0):
|
|
import gpu
|
|
from gpu_extras.batch import batch_for_shader
|
|
|
|
# https://docs.blender.org/api/blender2.8/gpu.html#triangle-with-custom-shader
|
|
|
|
def create_shader(fn_glsl):
|
|
path_glsl = get_path_from_addon_common('common', 'shaders', fn_glsl)
|
|
txt = open(path_glsl, 'rt').read()
|
|
vert_source, frag_source = gpustate.shader_parse_string(txt)
|
|
try:
|
|
Drawing.glCheckError(f'pre-compile check: {fn_glsl}')
|
|
ret = gpustate.gpu_shader(f'drawing {fn_glsl}', vert_source, frag_source)
|
|
Drawing.glCheckError(f'post-compile check: {fn_glsl}')
|
|
return ret
|
|
except Exception as e:
|
|
print('ERROR WHILE COMPILING SHADER %s' % fn_glsl)
|
|
assert False
|
|
|
|
Drawing.glCheckError(f'Pre-compile check: point, lineseg, circle, triangle shaders')
|
|
|
|
# 2D point
|
|
shader_2D_point, ubos_2D_point = create_shader('point_2D.glsl')
|
|
batch_2D_point = batch_for_shader(shader_2D_point, 'TRIS', {"pos": [(0,0), (1,0), (1,1), (0,0), (1,1), (0,1)]})
|
|
|
|
# 2D line segment
|
|
shader_2D_lineseg, ubos_2D_lineseg = create_shader('lineseg_2D.glsl')
|
|
batch_2D_lineseg = batch_for_shader(shader_2D_lineseg, 'TRIS', {"pos": [(0,0), (1,0), (1,1), (0,0), (1,1), (0,1)]})
|
|
|
|
# 2D circle
|
|
shader_2D_circle, ubos_2D_circle = create_shader('circle_2D.glsl')
|
|
# create batch to draw large triangle that covers entire clip space (-1,-1)--(+1,+1)
|
|
cnt = 100
|
|
pts = [
|
|
p for i0 in range(cnt)
|
|
for p in [
|
|
((i0+0)/cnt,0), ((i0+1)/cnt,0), ((i0+1)/cnt,1),
|
|
((i0+0)/cnt,0), ((i0+1)/cnt,1), ((i0+0)/cnt,1),
|
|
]
|
|
]
|
|
batch_2D_circle = batch_for_shader(shader_2D_circle, 'TRIS', {"pos": pts})
|
|
|
|
# 3D circle
|
|
shader_3D_circle, ubos_3D_circle = create_shader('circle_3D.glsl')
|
|
# create batch to draw large triangle that covers entire clip space (-1,-1)--(+1,+1)
|
|
cnt = 100
|
|
pts = [
|
|
p for i0 in range(cnt)
|
|
for p in [
|
|
((i0+0)/cnt,0), ((i0+1)/cnt,0), ((i0+1)/cnt,1),
|
|
((i0+0)/cnt,0), ((i0+1)/cnt,1), ((i0+0)/cnt,1),
|
|
]
|
|
]
|
|
batch_3D_circle = batch_for_shader(shader_3D_circle, 'TRIS', {"pos": pts})
|
|
|
|
# 3D triangle
|
|
shader_3D_triangle, ubos_3D_triangle = create_shader('triangle_3D.glsl')
|
|
batch_3D_triangle = batch_for_shader(shader_3D_triangle, 'TRIS', {'pos': [(1,0), (0,1), (0,0)]})
|
|
|
|
# 3D triangle
|
|
shader_2D_triangle, ubos_2D_triangle = create_shader('triangle_2D.glsl')
|
|
batch_2D_triangle = batch_for_shader(shader_2D_triangle, 'TRIS', {'pos': [(1,0), (0,1), (0,0)]})
|
|
|
|
Drawing.glCheckError(f'Compiled point, lineseg, circle shaders')
|
|
|
|
|
|
######################################################################################################
|
|
# The following classes mimic the immediate mode for (old-school way of) drawing geometry
|
|
# glBegin(GL_TRIANGLES)
|
|
# glColor3f(p)
|
|
# glVertex3f(p)
|
|
# glEnd()
|
|
|
|
class CC_DRAW:
|
|
_point_size:float = 1
|
|
_line_width:float = 1
|
|
_border_width:float = 0
|
|
_border_color:Color = Color((0, 0, 0, 0))
|
|
_stipple_pattern:List[float] = [1,0]
|
|
_stipple_offset:float = 0
|
|
_stipple_color:Color = Color((0, 0, 0, 0))
|
|
|
|
_default_color = Color((1, 1, 1, 1))
|
|
_default_point_size = 1
|
|
_default_line_width = 1
|
|
_default_border_width = 0
|
|
_default_border_color = Color((0, 0, 0, 0))
|
|
_default_stipple_pattern = [1,0]
|
|
_default_stipple_color = Color((0, 0, 0, 0))
|
|
|
|
@classmethod
|
|
def reset(cls):
|
|
s = Drawing._instance.scale
|
|
CC_DRAW._point_size = s(CC_DRAW._default_point_size)
|
|
CC_DRAW._line_width = s(CC_DRAW._default_line_width)
|
|
CC_DRAW._border_width = s(CC_DRAW._default_border_width)
|
|
CC_DRAW._border_color = CC_DRAW._default_border_color
|
|
CC_DRAW._stipple_offset = 0
|
|
CC_DRAW._stipple_pattern = [s(v) for v in CC_DRAW._default_stipple_pattern]
|
|
CC_DRAW._stipple_color = CC_DRAW._default_stipple_color
|
|
cls.update()
|
|
|
|
@classmethod
|
|
def update(cls): pass
|
|
|
|
@classmethod
|
|
def point_size(cls, size):
|
|
s = Drawing._instance.scale
|
|
CC_DRAW._point_size = s(size)
|
|
cls.update()
|
|
|
|
@classmethod
|
|
def line_width(cls, width):
|
|
s = Drawing._instance.scale
|
|
CC_DRAW._line_width = s(width)
|
|
cls.update()
|
|
|
|
@classmethod
|
|
def border(cls, *, width=None, color=None):
|
|
s = Drawing._instance.scale
|
|
if width is not None:
|
|
CC_DRAW._border_width = s(width)
|
|
if color is not None:
|
|
CC_DRAW._border_color = color
|
|
cls.update()
|
|
|
|
@classmethod
|
|
def stipple(cls, *, pattern=None, offset=None, color=None):
|
|
s = Drawing._instance.scale
|
|
if pattern is not None:
|
|
CC_DRAW._stipple_pattern = [s(v) for v in pattern]
|
|
if offset is not None:
|
|
CC_DRAW._stipple_offset = s(offset)
|
|
if color is not None:
|
|
CC_DRAW._stipple_color = color
|
|
cls.update()
|
|
|
|
@classmethod
|
|
def end(cls):
|
|
gpu.shader.unbind()
|
|
|
|
if not bpy.app.background:
|
|
CC_DRAW.reset()
|
|
|
|
|
|
class CC_2D_POINTS(CC_DRAW):
|
|
@classmethod
|
|
def begin(cls):
|
|
shader_2D_point.bind()
|
|
ubos_2D_point.options.mvpmatrix = Drawing._instance.get_pixel_matrix()
|
|
ubos_2D_point.options.screensize = (Drawing._instance.area.width, Drawing._instance.area.height, 0, 0)
|
|
ubos_2D_point.options.color = cls._default_color
|
|
cls.update()
|
|
|
|
@classmethod
|
|
def update(cls):
|
|
ubos_2D_point.options.radius_border = (cls._point_size, cls._border_width, 0, 0)
|
|
ubos_2D_point.options.colorBorder = cls._border_color
|
|
|
|
@classmethod
|
|
def color(cls, c:Color):
|
|
ubos_2D_point.options.color = c
|
|
|
|
@classmethod
|
|
def vertex(cls, p:Point2D):
|
|
if p:
|
|
ubos_2D_point.options.center = (*p, 0, 1)
|
|
ubos_2D_point.options.update_shader()
|
|
batch_2D_point.draw(shader_2D_point)
|
|
|
|
|
|
class CC_2D_LINES(CC_DRAW):
|
|
@classmethod
|
|
def begin(cls):
|
|
shader_2D_lineseg.bind()
|
|
mvpmatrix = Drawing._instance.get_pixel_matrix()
|
|
ubos_2D_lineseg.options.MVPMatrix = mvpmatrix
|
|
ubos_2D_lineseg.options.screensize = (Drawing._instance.area.width, Drawing._instance.area.height, 0, 0)
|
|
ubos_2D_lineseg.options.color0 = cls._default_color
|
|
cls.stipple(offset=0)
|
|
cls._c = 0
|
|
cls._last_p = None
|
|
|
|
@classmethod
|
|
def update(cls):
|
|
ubos_2D_lineseg.options.color1 = cls._stipple_color
|
|
ubos_2D_lineseg.options.stipple_width = (cls._stipple_pattern[0], cls._stipple_pattern[1], cls._stipple_offset, cls._line_width)
|
|
|
|
@classmethod
|
|
def color(cls, c:Color):
|
|
ubos_2D_lineseg.options.color0 = c
|
|
|
|
@classmethod
|
|
def vertex(cls, p:Point2D):
|
|
if p: ubos_2D_lineseg.options.assign(f'pos{cls._c}', (*p, 0, 1))
|
|
cls._c = (cls._c + 1) % 2
|
|
if cls._c == 0 and cls._last_p and p:
|
|
ubos_2D_lineseg.update_shader()
|
|
batch_2D_lineseg.draw(shader_2D_lineseg)
|
|
cls._last_p = p
|
|
|
|
class CC_2D_LINE_STRIP(CC_2D_LINES):
|
|
@classmethod
|
|
def begin(cls):
|
|
super().begin()
|
|
cls._last_p = None
|
|
|
|
@classmethod
|
|
def vertex(cls, p:Point2D):
|
|
if cls._last_p is None:
|
|
cls._last_p = p
|
|
else:
|
|
if cls._last_p and p:
|
|
ubos_2D_lineseg.options.pos0 = (*cls._last_p, 0, 1)
|
|
ubos_2D_lineseg.options.pos1 = (*p, 0, 1)
|
|
ubos_2D_lineseg.update_shader()
|
|
batch_2D_lineseg.draw(shader_2D_lineseg)
|
|
cls._last_p = p
|
|
|
|
class CC_2D_LINE_LOOP(CC_2D_LINES):
|
|
@classmethod
|
|
def begin(cls):
|
|
super().begin()
|
|
cls._first_p = None
|
|
cls._last_p = None
|
|
|
|
@classmethod
|
|
def vertex(cls, p:Point2D):
|
|
if cls._first_p is None:
|
|
cls._first_p = cls._last_p = p
|
|
else:
|
|
if cls._last_p and p:
|
|
ubos_2D_lineseg.options.pos0 = (*cls._last_p, 0, 1)
|
|
ubos_2D_lineseg.options.pos1 = (*p, 0, 1)
|
|
ubos_2D_lineseg.update_shader()
|
|
batch_2D_lineseg.draw(shader_2D_lineseg)
|
|
cls._last_p = p
|
|
|
|
@classmethod
|
|
def end(cls):
|
|
if cls._last_p and cls._first_p:
|
|
ubos_2D_lineseg.options.pos0 = (*cls._last_p, 0, 1)
|
|
ubos_2D_lineseg.options.pos1 = (*cls._first_p, 0, 1)
|
|
ubos_2D_lineseg.update_shader()
|
|
batch_2D_lineseg.draw(shader_2D_lineseg)
|
|
super().end()
|
|
|
|
|
|
class CC_2D_TRIANGLES(CC_DRAW):
|
|
@classmethod
|
|
def begin(cls):
|
|
shader_2D_triangle.bind()
|
|
#shader_2D_triangle.uniform_float('screensize', (Drawing._instance.area.width, Drawing._instance.area.height))
|
|
ubos_2D_triangle.options.MVPMatrix = Drawing._instance.get_pixel_matrix()
|
|
cls._c = 0
|
|
cls._last_color = None
|
|
cls._last_p0 = None
|
|
cls._last_p1 = None
|
|
|
|
@classmethod
|
|
def color(cls, c:Color):
|
|
if c is None: return
|
|
ubos_2D_triangle.options.assign(f'color{cls._c}', c)
|
|
cls._last_color = c
|
|
|
|
@classmethod
|
|
def vertex(cls, p:Point2D):
|
|
if p: ubos_2D_triangle.options.assign(f'pos{cls._c}', (*p, 0, 1))
|
|
cls._c = (cls._c + 1) % 3
|
|
if cls._c == 0 and p and cls._last_p0 and cls._last_p1:
|
|
ubos_2D_triangle.update_shader()
|
|
batch_2D_triangle.draw(shader_2D_triangle)
|
|
cls.color(cls._last_color)
|
|
cls._last_p1 = cls._last_p0
|
|
cls._last_p0 = p
|
|
|
|
class CC_2D_TRIANGLE_FAN(CC_DRAW):
|
|
@classmethod
|
|
def begin(cls):
|
|
shader_2D_triangle.bind()
|
|
ubos_2D_triangle.options.MVPMatrix = Drawing._instance.get_pixel_matrix()
|
|
cls._c = 0
|
|
cls._last_color = None
|
|
cls._first_p = None
|
|
cls._last_p = None
|
|
cls._is_first = True
|
|
|
|
@classmethod
|
|
def color(cls, c:Color):
|
|
if c is None: return
|
|
ubos_2D_triangle.options.assign(f'color{cls._c}', c)
|
|
cls._last_color = c
|
|
|
|
@classmethod
|
|
def vertex(cls, p:Point2D):
|
|
if p: ubos_2D_triangle.options.assign(f'pos{cls._c}', (*p, 0, 1))
|
|
cls._c += 1
|
|
if cls._c == 3:
|
|
if p and cls._first_p and cls._last_p:
|
|
ubos_2D_triangle.update_shader()
|
|
batch_2D_triangle.draw(shader_2D_triangle)
|
|
cls._c = 1
|
|
cls.color(cls._last_color)
|
|
if cls._is_first:
|
|
cls._first_p = p
|
|
cls._is_first = False
|
|
else: cls._last_p = p
|
|
|
|
class CC_3D_TRIANGLES(CC_DRAW):
|
|
@classmethod
|
|
def begin(cls):
|
|
shader_3D_triangle.bind()
|
|
ubos_3D_triangle.options.MVPMatrix = Drawing._instance.get_view_matrix()
|
|
cls._c = 0
|
|
cls._last_color = None
|
|
cls._last_p0 = None
|
|
cls._last_p1 = None
|
|
|
|
@classmethod
|
|
def color(cls, c:Color):
|
|
if c is None: return
|
|
ubos_3D_triangle.options.assign(f'color{cls._c}', c)
|
|
cls._last_color = c
|
|
|
|
@classmethod
|
|
def vertex(cls, p:Point):
|
|
if p: ubos_3D_triangle.options.assign(f'pos{cls._c}', p)
|
|
cls._c = (cls._c + 1) % 3
|
|
if cls._c == 0 and p and cls._last_p0 and cls._last_p1:
|
|
ubos_3D_triangle.update_shader()
|
|
batch_3D_triangle.draw(shader_3D_triangle)
|
|
cls.color(cls._last_color)
|
|
cls._last_p1 = cls._last_p0
|
|
cls._last_p0 = p
|
|
|
|
|
|
class DrawCallbacks:
|
|
@staticmethod
|
|
def on_draw(mode):
|
|
def wrapper(fn):
|
|
nonlocal mode
|
|
assert mode in {'predraw', 'pre3d', 'post3d', 'post2d'}, f'DrawCallbacks: unexpected draw mode {mode} for {fn}'
|
|
@wraps(fn)
|
|
def wrapped(*args, **kwargs):
|
|
try:
|
|
return fn(*args, **kwargs)
|
|
except Exception as e:
|
|
print(f'DrawCallbacks: caught exception in on_draw with {fn}')
|
|
debugger.print_exception()
|
|
print(e)
|
|
return
|
|
setattr(wrapped, f'_on_{mode}', True)
|
|
return wrapped
|
|
return wrapper
|
|
|
|
@staticmethod
|
|
def on_predraw():
|
|
return DrawCallbacks.on_draw('predraw')
|
|
|
|
def __init__(self, obj):
|
|
self.obj = obj
|
|
self._fns = {
|
|
'pre': [ fn for (_, fn) in find_fns(obj, '_on_predraw') ],
|
|
'pre3d': [ fn for (_, fn) in find_fns(obj, '_on_pre3d' ) ],
|
|
'post3d': [ fn for (_, fn) in find_fns(obj, '_on_post3d' ) ],
|
|
'post2d': [ fn for (_, fn) in find_fns(obj, '_on_post2d' ) ],
|
|
}
|
|
self.reset_pre()
|
|
|
|
def reset_pre(self):
|
|
self._called_pre = False
|
|
|
|
def _call(self, n, *, call_predraw=True):
|
|
if not self._called_pre:
|
|
self._called_pre = True
|
|
for fn in self._fns['pre']: fn(self.obj)
|
|
for fn in self._fns[n]: fn(self.obj)
|
|
|
|
def pre3d(self): self._call('pre3d')
|
|
def post3d(self): self._call('post3d')
|
|
def post2d(self): self._call('post2d')
|
|
|
|
|