''' 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 . ''' 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')