# -*- coding:utf-8 -*- # ##### BEGIN GPL LICENSE BLOCK ##### # # 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 2 # 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, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ##### END GPL LICENSE BLOCK ##### # # ---------------------------------------------------------- # Author: Stephen Leger (s-leger) # # ---------------------------------------------------------- import bgl import blf import gpu from gpu_extras.batch import batch_for_shader import bpy import numpy as np from math import sin, cos, atan2, pi from mathutils import Vector, Matrix from bpy_extras import view3d_utils, object_utils # ------------------------------------------------------------------ # Define Gl Handle types # ------------------------------------------------------------------ class DefaultColorScheme: """ Font sizes and basic colour scheme default to this when not found in addon prefs Colors are FloatVectorProperty of size 4 and type COLOR_GAMMA """ text_size = 14 feedback_size_main = 16 feedback_size_title = 14 feedback_size_shortcut = 11 feedback_colour_main = (0.95, 0.95, 0.95, 1.0) feedback_colour_key = (0.67, 0.67, 0.67, 1.0) feedback_colour_shortcut = (0.51, 0.51, 0.51, 1.0) feedback_shortcut_area = (0, 0.4, 0.6, 0.2) feedback_title_area = (0, 0.4, 0.6, 0.5) handle_colour_normal = (1.0, 1.0, 1.0, 1.0) handle_colour_hover = (1.0, 1.0, 0.0, 1.0) handle_colour_active = (1.0, 0.0, 0.0, 1.0) handle_colour_selected = (0.0, 0.0, 0.7, 1.0) handle_colour_inactive = (0.3, 0.3, 0.3, 1.0) def get_prefs(context): global __name__ try: addon_name = __name__.split('.')[0] prefs = context.preferences.addons[addon_name].preferences except: prefs = DefaultColorScheme pass return prefs g_poly = None g_image = None g_line = None line_vertSrc = ''' uniform mat4 ModelViewProjectionMatrix; in vec2 pos; void main() { gl_Position = ModelViewProjectionMatrix * vec4(pos, 0.0, 1.0); } ''' line_fragSrc = ''' uniform vec4 color; out vec4 fragColor; void main() { fragColor = color; } ''' """ # TESTS _format = gpu.types.GPUVertFormat() _pos_id = _format.attr_add( id="pos", comp_type="F32", len=2, fetch_mode="FLOAT") coords = [(0,0), (200,100)] vbo = gpu.types.GPUVertBuf(len=len(coords), format=_format) """ class GPU_Line: def __init__(self): # never call this in -b mode if bpy.app.background: return self._format = gpu.types.GPUVertFormat() self._pos_id = self._format.attr_add( id="pos", comp_type="F32", len=2, fetch_mode="FLOAT") self.shader = gpu.types.GPUShader(line_vertSrc, line_fragSrc) self.unif_color = self.shader.uniform_from_name("color") self.color = np.array([0.0, 0.0, 0.0, 1.0], 'f') def batch_line_strip_create(self, coords): vbo = gpu.types.GPUVertBuf(len=len(coords), format=self._format) vbo.attr_fill(id=self._pos_id, data=coords) batch_lines = gpu.types.GPUBatch(type="LINE_STRIP", buf=vbo) batch_lines.program_set(self.shader) return batch_lines def draw(self, color, list_verts_co): if list_verts_co: batch = self.batch_line_strip_create(list_verts_co) self.color[0:4] = color[0:4] self.shader.uniform_vector_float(self.unif_color, self.color, 4) batch.draw() del batch class GPU_Poly: def __init__(self): if bpy.app.background: return self._format = gpu.types.GPUVertFormat() self._pos_id = self._format.attr_add( id="pos", comp_type="F32", len=2, fetch_mode="FLOAT") self.shader = gpu.types.GPUShader(line_vertSrc, line_fragSrc) self.unif_color = self.shader.uniform_from_name("color") self.color = np.array([0.0, 0.0, 0.0, 0.5], 'f') def batch_create(self, coords): vbo = gpu.types.GPUVertBuf(len=len(coords), format=self._format) vbo.attr_fill(id=self._pos_id, data=coords) batch = gpu.types.GPUBatch(type="TRI_FAN", buf=vbo) # batch.program_set(self.shader) # batch = batch_for_shader(self.shader, 'TRI_FAN', {"position": coords}) return batch def draw(self, color, list_verts_co): if list_verts_co: self.shader.bind() batch = self.batch_create(list_verts_co) self.color[0:4] = color[0:4] self.shader.uniform_vector_float(self.unif_color, self.color, 4) batch.draw(self.shader) class GPU_Image: uvs = [(0, 0), (1, 0), (0, 1), (1, 1)] indices = [(0, 1, 2), (2, 1, 3)] def __init__(self): if bpy.app.background: return self._format = gpu.types.GPUVertFormat() self._pos_id = self._format.attr_add( id="pos", comp_type="F32", len=2, fetch_mode="FLOAT") self.shader = gpu.shader.from_builtin('2D_IMAGE') def batch_create(self, coords): batch = batch_for_shader(self.shader, 'TRIS', {"pos": coords, "texCoord": self.uvs}, indices=self.indices) return batch def draw(self, texture_id, list_verts_co): if list_verts_co: batch = self.batch_create(list_verts_co) # in case someone disabled it before bgl.glEnable(bgl.GL_TEXTURE_2D) # bind texture to image unit 0 bgl.glActiveTexture(bgl.GL_TEXTURE0) bgl.glBindTexture(bgl.GL_TEXTURE_2D, texture_id) self.shader.bind() # tell shader to use the image that is bound to image unit 0 self.shader.uniform_int("image", 0) batch.draw(self.shader) bgl.glDisable(bgl.GL_TEXTURE_2D) class Gl(): """ handle 3d -> 2d gl drawing d : dimensions 3 to convert pos from 3d 2 to keep pos as 2d absolute screen position """ def __init__(self, d=3, colour=(0.0, 0.0, 0.0, 1.0)): global g_poly, g_image, g_line if g_poly is None: g_poly = GPU_Poly() if g_image is None: g_image = GPU_Image() if g_line is None: g_line = GPU_Line() # nth dimensions of input coords 3=word coords 2=pixel screen coords self.d = d self.pos_2d = Vector((0, 0)) self.colour_inactive = colour @property def colour(self): return self.colour_inactive def position_2d_from_coord(self, context, coord, render=False): """ coord given in local input coordsys """ if self.d == 2: return Vector(coord) if render: return self.get_render_location(context, coord) region = context.region rv3d = context.region_data loc = view3d_utils.location_3d_to_region_2d(region, rv3d, coord, self.pos_2d) return Vector(loc) def get_render_location(self, context, coord): scene = context.scene co_2d = object_utils.world_to_camera_view(scene, scene.camera, coord) # Get pixel coords render_scale = scene.render.resolution_percentage / 100 render_size = (int(scene.render.resolution_x * render_scale), int(scene.render.resolution_y * render_scale)) return Vector(round(co_2d.x * render_size[0]), round(co_2d.y * render_size[1])) class GlText(Gl): def __init__(self, d=3, label="", value=None, precision=4, unit_mode='AUTO', unit_type='SIZE', dimension=1, angle=0, font_size=None, colour=(1, 1, 1, 1), z_axis=Vector((0, 0, 1))): """ d: [2|3] coords type: 2 for coords in screen pixels, 3 for 3d world location label : string label value : float value (will add unit according following settings) precision : integer rounding for values dimension : [1 - 3] nth dimension of unit (single, square, cubic) unit_mode : ['ADAPTIVE','METER','CENTIMETER','MILIMETER','FEET','INCH','RADIANS','DEGREE'] unit type to use to postfix values ADAPTIVE use scene units setup unit_type : ['SIZE','ANGLE'] unit type to add to value angle : angle to rotate text """ self.z_axis = z_axis # text, add as prefix to value self.label = label # value with unit related self.value = value self.precision = precision self.dimension = dimension self.unit_type = unit_type self.unit_mode = unit_mode if font_size is None: prefs = get_prefs(bpy.context) self.font_size = prefs.text_size else: self.font_size = font_size self.angle = angle Gl.__init__(self, d) self.colour_inactive = colour # store text with units self._text = "" self.cbuff = bgl.Buffer(bgl.GL_FLOAT, 4) def text_size(self, context): """ overall on-screen size in pixels """ dpi, font_id = context.preferences.system.dpi, 0 if self.angle != 0: blf.enable(font_id, blf.ROTATION) blf.rotation(font_id, self.angle) blf.aspect(font_id, 1.0) blf.size(font_id, self.font_size, dpi) x, y = blf.dimensions(font_id, self.text) if self.angle != 0: blf.disable(font_id, blf.ROTATION) return Vector((x, y)) @property def pts(self): return [self.pos_3d] @property def text(self): s = self.label + self._text return s.strip() def add_units(self, context): if self.value is None: return "" system = context.scene.unit_settings.system if self.unit_type == 'ANGLE': scale = 1 mode = 'ADAPTIVE' else: scale = context.scene.unit_settings.scale_length mode = self.unit_mode if mode == 'AUTO': mode = context.scene.unit_settings.length_unit.upper() val = self.value * scale if mode == 'ADAPTIVE': if self.unit_type == 'ANGLE': mode = context.scene.unit_settings.system_rotation else: if system == "IMPERIAL": if round(val * (3.2808399 ** self.dimension), 2) >= 1.0: mode = 'FOOT' else: mode = 'INCH' elif context.scene.unit_settings.system == "METRIC": if round(val, 2) >= 1.0: mode = 'METER' else: if round(val, 2) >= 0.01: mode = 'CENTIMETER' else: mode = 'MILIMETER' # TODO: support for separate units (through 2.8 api) # convert values unit = "" if mode == 'METER': unit = "m" elif mode == 'CENTIMETER': val *= (100 ** self.dimension) unit = "cm" elif mode == 'MILIMETER': val *= (1000 ** self.dimension) unit = 'mm' elif mode in {'FOOT', 'FEET'}: val *= (3.2808399 ** self.dimension) unit = "ft" elif mode == 'INCH': val *= (39.3700787 ** self.dimension) unit = "in" elif mode == 'RADIANS': unit = "" elif mode == 'DEGREES': val = self.value / pi * 180 unit = "°" if system == 'IMPERIAL': if self.dimension == 2: unit = "sq " + unit elif self.dimension == 3: unit = "cu " + unit elif system == 'METRIC': if self.dimension == 2: unit += "\u00b2" # Superscript two elif self.dimension == 3: unit += "\u00b3" # Superscript three fmt = "%1." + str(self.precision) + "f" # remove trailing zeros res = fmt % val while res[-1] == '0': res = res[:-1] if res[-1] == ".": res = res + '0' return "{} {}".format(res, unit) def set_pos(self, context, value, pos_3d, direction, angle=0, normal=Vector((0, 0, 1))): self.up_axis = direction.normalized() self.c_axis = self.up_axis.cross(normal) self.pos_3d = pos_3d self.value = value self.angle = angle self._text = self.add_units(context) def draw(self, context, render=False): # print("draw_text %s %s" % (self.text, type(self).__name__)) self.render = render p = self.position_2d_from_coord(context, self.pts[0], render) # dirty fast assignment dpi, font_id = context.preferences.system.dpi, 0 # self.cbuff[0:4] = self.colour # bgl.glEnableClientState(bgl.GL_COLOR_ARRAY) # bgl.glColorPointer(4, bgl.GL_FLOAT, 0, self.cbuff) blf.color(0, *self.colour) if self.angle != 0: blf.enable(font_id, blf.ROTATION) blf.rotation(font_id, self.angle) blf.size(font_id, self.font_size, dpi) blf.position(font_id, p.x, p.y, 0) blf.draw(font_id, self.text) if self.angle != 0: blf.disable(font_id, blf.ROTATION) # bgl.glDisableClientState(bgl.GL_COLOR_ARRAY) class GlBaseLine(Gl): def __init__(self, d=3, width=1, style=bgl.GL_LINE, closed=False, n_pts=2): Gl.__init__(self, d) # default line width self.width = width # default line style self.style = style # allow closed lines self.closed = closed self.n_pts = n_pts def draw(self, context, render=False): """ render flag when rendering """ self.render = render bgl.glEnable(bgl.GL_BLEND) bgl.glLineWidth(self.width) list_verts_co = [ tuple(self.position_2d_from_coord(context, pt, render)[0:2]) for i, pt in enumerate(self.pts)] if self.closed: list_verts_co.append(list_verts_co[0]) g_line.draw(self.colour, list_verts_co) bgl.glLineWidth(1.0) bgl.glDisable(bgl.GL_BLEND) class GlLine(GlBaseLine): """ 2d/3d Line """ def __init__(self, d=3, p=None, v=None, p0=None, p1=None, z_axis=None): """ d=3 use 3d coords, d=2 use 2d pixels coords Init by either p: Vector or tuple origin v: Vector or tuple size and direction or p0: Vector or tuple 1 point location p1: Vector or tuple 2 point location Will convert any into Vector 3d both optionnals """ if p is not None and v is not None: self.p = Vector(p) self.v = Vector(v) elif p0 is not None and p1 is not None: self.p = Vector(p0) self.v = Vector(p1) - self.p else: self.p = Vector() self.v = Vector() if z_axis is not None: self.z_axis = z_axis else: self.z_axis = Vector((0, 0, 1)) GlBaseLine.__init__(self, d, n_pts=2) @property def p0(self): return self.p @property def p1(self): return self.p + self.v @p0.setter def p0(self, p0): """ Note: setting p0 move p0 only """ p1 = self.p1 self.p = Vector(p0) self.v = p1 - p0 @p1.setter def p1(self, p1): """ Note: setting p1 move p1 only """ self.v = Vector(p1) - self.p @property def length(self): return self.v.length @property def angle(self): return atan2(self.v.y, self.v.x) @property def cross(self): """ Vector perpendicular on plane defined by z_axis lie on the right side p1 |--x p0 """ return self.v.cross(self.z_axis) def normal(self, t=0): """ Line perpendicular on plane defined by z_axis lie on the right side p1 |--x p0 """ n = GlLine() n.p = self.lerp(t) n.v = self.cross return n def sized_normal(self, t, size): """ GlLine perpendicular on plane defined by z_axis and of given size positioned at t in current line lie on the right side p1 |--x p0 """ n = GlLine() n.p = self.lerp(t) n.v = size * self.cross.normalized() return n def lerp(self, t): """ Interpolate along segment t parameter [0, 1] where 0 is start of arc and 1 is end """ return self.p + self.v * t def offset(self, offset): """ offset > 0 on the right part """ self.p += offset * self.cross.normalized() def point_sur_segment(self, pt): """ point_sur_segment (2d) point: Vector 3d t: param t de l'intersection sur le segment courant d: distance laterale perpendiculaire positif a droite """ dp = (pt - self.p).to_2d() v2d = self.v.to_2d() dl = v2d.length d = (self.v.x * dp.y - self.v.y * dp.x) / dl t = v2d.dot(dp) / (dl * dl) return t > 0 and t < 1, d, t @property def pts(self): return [self.p0, self.p1] class GlCircle(GlBaseLine): def __init__(self, d=3, radius=0, center=Vector((0, 0, 0)), z_axis=Vector((0, 0, 1))): self.r = radius self.c = center z = z_axis if z.z < 1: x = z.cross(Vector((0, 0, 1))) y = x.cross(z) else: x = Vector((1, 0, 0)) y = Vector((0, 1, 0)) self.rM = Matrix([ Vector((x.x, y.x, z.x)), Vector((x.y, y.y, z.y)), Vector((x.z, y.z, z.z)) ]) self.z_axis = z self.a0 = 0 self.da = 2 * pi GlBaseLine.__init__(self, d, n_pts=60) def lerp(self, t): """ Linear interpolation """ a = self.a0 + t * self.da return self.c + self.rM @ Vector((self.r * cos(a), self.r * sin(a), 0)) @property def pts(self): n_pts = max(1, int(round(abs(self.da) / pi * 30, 0))) self.n_pts = n_pts t_step = 1 / n_pts return [self.lerp(i * t_step) for i in range(n_pts + 1)] class GlArc(GlCircle): def __init__(self, d=3, radius=0, center=Vector((0, 0, 0)), z_axis=Vector((0, 0, 1)), a0=0, da=0): """ a0 and da arguments are in radians a0 = 0 on the x+ axis side a0 = pi on the x- axis side da > 0 CCW contrary-clockwise da < 0 CW clockwise """ GlCircle.__init__(self, d, radius, center, z_axis) self.da = da self.a0 = a0 @property def length(self): return self.r * abs(self.da) def normal(self, t=0): """ perpendicular line always on the right side """ n = GlLine(d=self.d, z_axis=self.z_axis) n.p = self.lerp(t) if self.da < 0: n.v = self.c - n.p else: n.v = n.p - self.c return n def sized_normal(self, t, size): n = GlLine(d=self.d, z_axis=self.z_axis) n.p = self.lerp(t) if self.da < 0: n.v = size * (self.c - n.p).normalized() else: n.v = size * (n.p - self.c).normalized() return n def tangeant(self, t, length): a = self.a0 + t * self.da ca = cos(a) sa = sin(a) n = GlLine(d=self.d, z_axis=self.z_axis) n.p = self.c + self.rM @ Vector((self.r * ca, self.r * sa, 0)) n.v = self.rM @ Vector((length * sa, -length * ca, 0)) if self.da > 0: n.v = -n.v return n def offset(self, offset): """ offset > 0 on the right part """ if self.da > 0: radius = self.r + offset else: radius = self.r - offset return GlArc(d=self.d, radius=radius, center=self.c, a0=self.a0, da=self.da, z_axis=self.z_axis) class GlPolygon(Gl): def __init__(self, colour=(0.3, 0.3, 0.3, 1.0), d=3, n_pts=60): self.pts_3d = [] Gl.__init__(self, d, colour) self.n_pts = min(n_pts, 60) def set_pos(self, pts_3d): self.pts_3d = pts_3d @property def pts(self): return self.pts_3d def draw(self, context, render=False): """ render flag when rendering """ # return self.render = render if self.n_pts == 0: return pts = self.pts g_vertices = [ tuple(self.position_2d_from_coord(context, pt, render)[0:2]) for i, pt in enumerate(pts)] bgl.glEnable(bgl.GL_BLEND) g_poly.draw(self.colour, g_vertices) bgl.glDisable(bgl.GL_BLEND) class GlRect(GlPolygon): def __init__(self, colour=(0.0, 0.0, 0.0, 1.0), d=2): GlPolygon.__init__(self, colour, d, n_pts=4) @property def pts(self): return self.pts_2d def draw(self, context, render=False): pts = [ self.position_2d_from_coord(context, pt, render) for pt in self.pts_3d ] x0, y0 = pts[0] x1, y1 = pts[1] self.pts_2d = [Vector((x, y)) for x, y in [(x0, y0), (x0, y1), (x1, y1), (x1, y0)]] GlPolygon.draw(self, context, render) class GlImage(Gl): def __init__(self, d=2, image=None): # GImage bindcode[0] self.image = image self.colour_inactive = (1, 1, 1, 1) Gl.__init__(self, d) self.pts_2d = [Vector((0, 0)), Vector((10, 10))] self.n_pts = 4 def set_pos(self, pts): self.pts_2d = pts @property def pts(self): return self.pts_2d def draw(self, context, render=False): if self.image is None: return p0 = self.pts[0] p1 = self.pts[1] coords = [(p0.x, p0.y), (p1.x, p0.y), (p0.x, p1.y), (p1.x, p1.y)] g_image.draw(self.image.bindcode, coords) class GlPolyline(GlBaseLine): def __init__(self, colour, d=3, n_pts=60): self.pts_3d = [] GlBaseLine.__init__(self, d, n_pts=n_pts) self.colour_inactive = colour def set_pos(self, pts_3d): self.pts_3d = pts_3d # self.pts_3d.append(pts_3d[0]) @property def pts(self): return self.pts_3d class GlHandle(GlPolygon): def __init__(self, sensor_size, size, draggable=False, selectable=False, d=3, n_pts=4): """ sensor_size : 2d size in pixels of sensor area size : 3d size of handle """ GlPolygon.__init__(self, d=d, n_pts=n_pts) prefs = get_prefs(bpy.context) self.colour_active = prefs.handle_colour_active self.colour_hover = prefs.handle_colour_hover self.colour_normal = prefs.handle_colour_normal self.colour_selected = prefs.handle_colour_selected self.colour_inactive = prefs.handle_colour_inactive # variable symbol size in world coords self.size = size # constant symbol size in pixels self.symbol_size = sensor_size # scale factor for constant symbol pixel size self.scale = 1 # sensor size in pixels self.sensor_width = sensor_size self.sensor_height = sensor_size self.pos_3d = Vector((0, 0, 0)) self.up_axis = Vector((0, 0, 0)) self.c_axis = Vector((0, 0, 0)) self.hover = False self.active = False self.draggable = draggable self.selectable = selectable self.selected = False def scale_factor(self, context, pts): """ Compute screen scale factor for symbol given 2 points in symbol direction (eg start and end of arrow) """ prefs = get_prefs(context) if not prefs.constant_handle_size: return 1 p2d = [] for pt in pts: p2d.append(self.position_2d_from_coord(context, pt)) sx = [p.x for p in p2d] sy = [p.y for p in p2d] x = max(sx) - min(sx) y = max(sy) - min(sy) s = max(x, y) if s > 0: fac = self.symbol_size / s else: fac = 1 return fac def set_pos(self, context, pos_3d, direction, normal=Vector((0, 0, 1))): self.up_axis = direction.normalized() self.c_axis = self.up_axis.cross(normal) self.pos_3d = pos_3d self.pos_2d = self.position_2d_from_coord(context, self.sensor_center) x = self.up_axis * 0.5 * self.size y = self.c_axis * 0.5 * self.size pts = [self.sensor_center + v for v in [-x, x, -y, y]] self.scale = self.scale_factor(context, pts) def check_hover(self, pos_2d): if self.draggable: dp = pos_2d - self.pos_2d self.hover = abs(dp.x) < self.sensor_width and abs(dp.y) < self.sensor_height @property def sensor_center(self): pts = self.pts n = len(pts) x, y, z = 0, 0, 0 for pt in pts: x += pt.x y += pt.y z += pt.z return Vector((x / n, y / n, z / n)) @property def pts(self): raise NotImplementedError @property def colour(self): if self.render: return self.colour_inactive elif self.draggable: if self.active: return self.colour_active elif self.hover: return self.colour_hover elif self.selected: return self.colour_selected return self.colour_normal else: return self.colour_inactive class SquareHandle(GlHandle): def __init__(self, sensor_size, size, draggable=False, selectable=False): GlHandle.__init__(self, sensor_size, size, draggable, selectable, n_pts=4) @property def pts(self): n = self.up_axis c = self.c_axis if self.selected or self.hover or self.active: scale = 1 else: scale = 0.5 x = n * self.scale * self.size * scale y = c * self.scale * self.size * scale return [self.pos_3d - x - y, self.pos_3d + x - y, self.pos_3d + x + y, self.pos_3d - x + y] class TriHandle(GlHandle): def __init__(self, sensor_size, size, draggable=False, selectable=False): GlHandle.__init__(self, sensor_size, size, draggable, selectable, n_pts=3) @property def pts(self): n = self.up_axis c = self.c_axis # does move sensitive area so disable for tri handle # may implement sensor_center property to fix this # if self.selected or self.hover or self.active: scale = 1 # else: # scale = 0.5 x = n * self.scale * self.size * 4 * scale y = c * self.scale * self.size * scale return [self.pos_3d - x + y, self.pos_3d - x - y, self.pos_3d] class CruxHandle(GlHandle): def __init__(self, sensor_size, size, draggable=True, selectable=False): GlHandle.__init__(self, sensor_size, size, draggable, selectable, n_pts=0) self.branch_0 = GlPolygon((1, 1, 1, 1), d=3, n_pts=4) self.branch_1 = GlPolygon((1, 1, 1, 1), d=3, n_pts=4) def set_pos(self, context, pos_3d, direction, normal=Vector((0, 0, 1))): self.pos_3d = pos_3d self.pos_2d = self.position_2d_from_coord(context, self.sensor_center) o = self.pos_3d d = 0.5 * self.size x = direction.normalized() y = x.cross(normal) sx = x * 0.5 * self.size sy = y * 0.5 * self.size pts = [o + v for v in [-sx, sx, -sy, sy]] self.scale = self.scale_factor(context, pts) c = self.scale * d / 1.4242 w = self.scale * self.size s = w - c xs = x * s xw = x * w ys = y * s yw = y * w p0 = o + xs + yw p1 = o + xw + ys p2 = o - xs - yw p3 = o - xw - ys p4 = o - xs + yw p5 = o + xw - ys p6 = o + xs - yw p7 = o - xw + ys self.branch_0.set_pos([p0, p1, p2, p3]) self.branch_1.set_pos([p4, p5, p6, p7]) @property def pts(self): return [self.pos_3d] def draw(self, context, render=False): self.render = render self.branch_0.colour_inactive = self.colour self.branch_1.colour_inactive = self.colour self.branch_0.draw(context) self.branch_1.draw(context) class PlusHandle(GlHandle): def __init__(self, sensor_size, size, draggable=True, selectable=False): GlHandle.__init__(self, sensor_size, size, draggable, selectable, n_pts=0) self.branch_0 = GlPolygon((1, 1, 1, 1), d=3, n_pts=4) self.branch_1 = GlPolygon((1, 1, 1, 1), d=3, n_pts=4) def set_pos(self, context, pos_3d, direction, normal=Vector((0, 0, 1))): self.pos_3d = pos_3d self.pos_2d = self.position_2d_from_coord(context, self.sensor_center) o = self.pos_3d x = direction.normalized() y = x.cross(normal) sx = x * 0.5 * self.size sy = y * 0.5 * self.size pts = [o + v for v in [-sx, sx, -sy, sy]] self.scale = self.scale_factor(context, pts) w = self.scale * self.size s = self.scale * 0.25 * w xs = x * s xw = x * w ys = y * s yw = y * w p0 = o - xw + ys p1 = o + xw + ys p2 = o + xw - ys p3 = o - xw - ys p4 = o - xs + yw p5 = o + xs + yw p6 = o + xs - yw p7 = o - xs - yw self.branch_0.set_pos([p0, p1, p2, p3]) self.branch_1.set_pos([p4, p5, p6, p7]) @property def pts(self): return [self.pos_3d] def draw(self, context, render=False): self.render = render self.branch_0.colour_inactive = self.colour self.branch_1.colour_inactive = self.colour self.branch_0.draw(context) self.branch_1.draw(context) class EditableText(GlText, GlHandle): def __init__(self, sensor_size, size, draggable=False, selectable=False): GlHandle.__init__(self, sensor_size, size, draggable, selectable) GlText.__init__(self, colour=(0, 0, 0, 1)) def set_pos(self, context, value, pos_3d, direction, normal=Vector((0, 0, 1))): self.up_axis = direction.normalized() self.c_axis = self.up_axis.cross(normal) self.pos_3d = pos_3d self.value = value self._text = self.add_units(context) ts = self.text_size(context) self.pos_2d = self.position_2d_from_coord(context, pos_3d) self.pos_2d.x += 0.5 * ts.x self.sensor_width, self.sensor_height = 0.5 * ts.x, ts.y @property def sensor_center(self): return self.pos_3d class ThumbHandle(GlHandle): def __init__(self, size_2d, label, image=None, draggable=False, selectable=False, d=2): GlHandle.__init__(self, size_2d, size_2d, draggable, selectable, d, n_pts=4) self.image = GlImage(image=image) self.label = GlText(d=2, label=label.replace("_", " ").capitalize()) self.frame = GlPolyline((1, 1, 1, 1), d=2, n_pts=4) self.frame.closed = True self.size_2d = size_2d self.sensor_width = 0.5 * size_2d.x self.sensor_height = 0.5 * size_2d.y self.colour_normal = (0.715, 0.905, 1, 0.9) self.colour_hover = (1, 1, 1, 1) def set_pos(self, context, pos_2d): """ pos 2d is center !! """ self.pos_2d = pos_2d ts = self.label.text_size(context) self.label.pos_3d = pos_2d + Vector((-0.5 * ts.x, ts.y - 0.5 * self.size_2d.y)) p0, p1 = self.pts self.image.set_pos(self.pts) self.frame.set_pos([p0, Vector((p1.x, p0.y)), p1, Vector((p0.x, p1.y))]) @property def pts(self): s = 0.5 * self.size_2d return [self.pos_2d - s, self.pos_2d + s] @property def sensor_center(self): return self.pos_2d + 0.5 * self.size_2d def draw(self, context, render=False): self.render = render self.image.colour_inactive = self.colour # GlHandle.draw(self, context, render=False) self.image.draw(context, render=False) self.label.draw(context, render=False) self.frame.draw(context, render=False) class Screen(): def __init__(self, margin): self.margin = margin def size(self, context): system = context.preferences.system w = context.region.width h = context.region.height y_min = self.margin y_max = h - self.margin x_min = self.margin x_max = w - self.margin if system.use_region_overlap: # system.window_draw_method in {'TRIPLE_BUFFER', 'ADAPTIVEMATIC'}): area = context.area for r in area.regions: if r.type == 'TOOLS': x_min += r.width elif r.type == 'UI': x_max -= r.width return x_min, x_max, y_min, y_max class FeedbackPanel(): """ Feed-back panel inspired by np_station """ def __init__(self, title='Archipack'): prefs = get_prefs(bpy.context) self.main_title = GlText(d=2, label=title + " : ", font_size=prefs.feedback_size_main, colour=prefs.feedback_colour_main ) self.title = GlText(d=2, font_size=prefs.feedback_size_title, colour=prefs.feedback_colour_main ) self.spacing = Vector(( 0.5 * prefs.feedback_size_shortcut, 0.5 * prefs.feedback_size_shortcut)) self.margin = 50 self.explanation = GlText(d=2, font_size=prefs.feedback_size_shortcut, colour=prefs.feedback_colour_main ) self.shortcut_area = GlPolygon(colour=prefs.feedback_shortcut_area, d=2, n_pts=4) self.title_area = GlPolygon(colour=prefs.feedback_title_area, d=2, n_pts=4) self.shortcuts = [] self.on = False self.show_title = True self.show_main_title = True # read only, when enabled, after draw() the top left coord of info box self.top = Vector((0, 0)) self.screen = Screen(self.margin) def disable(self): self.on = False def enable(self): self.on = True def instructions(self, context, title, explanation, shortcuts): """ position from bottom to top """ prefs = get_prefs(context) self.explanation.label = explanation self.title.label = title self.shortcuts = [] for key, label in shortcuts: key = GlText(d=2, label=key, font_size=prefs.feedback_size_shortcut, colour=prefs.feedback_colour_key) label = GlText(d=2, label=' : ' + label, font_size=prefs.feedback_size_shortcut, colour=prefs.feedback_colour_shortcut) ks = key.text_size(context) ls = label.text_size(context) self.shortcuts.append([key, ks, label, ls]) def draw(self, context, render=False): if self.on: """ draw from bottom to top so we are able to always fit needs """ x_min, x_max, y_min, y_max = self.screen.size(context) available_w = x_max - x_min - 2 * self.spacing.x main_title_size = self.main_title.text_size(context) + Vector((5, 0)) # h = context.region.height # 0,0 = bottom left pos = Vector((x_min + self.spacing.x, y_min)) shortcuts = [] # sort by lines lines = [] line = [] space = 0 sum_txt = 0 for key, ks, label, ls in self.shortcuts: space += ks.x + ls.x + self.spacing.x if pos.x + space > available_w: txt_spacing = (available_w - sum_txt) / (max(1, len(line) - 1)) sum_txt = 0 space = ks.x + ls.x + self.spacing.x lines.append((txt_spacing, line)) line = [] sum_txt += ks.x + ls.x line.append([key, ks, label, ls]) if len(line) > 0: txt_spacing = (available_w - sum_txt) / (max(1, len(line) - 1)) lines.append((txt_spacing, line)) # reverse lines to draw from bottom to top lines = list(reversed(lines)) for spacing, line in lines: pos.y += self.spacing.y pos.x = x_min + self.spacing.x for key, ks, label, ls in line: key.pos_3d = pos.copy() pos.x += ks.x label.pos_3d = pos.copy() pos.x += ls.x + spacing shortcuts.extend([key, label]) pos.y += ks.y + self.spacing.y n_shortcuts = len(shortcuts) # shortcut area self.shortcut_area.pts_3d = [ (x_min, self.margin), (x_max, self.margin), (x_max, pos.y), (x_min, pos.y) ] # small space between shortcut area and main title bar if n_shortcuts > 0: pos.y += 0.5 * self.spacing.y self.title_area.pts_3d = [ (x_min, pos.y), (x_max, pos.y), (x_max, pos.y + main_title_size.y + 2 * self.spacing.y), (x_min, pos.y + main_title_size.y + 2 * self.spacing.y) ] pos.y += self.spacing.y title_size = self.title.text_size(context) # check for space available: # if explanation + title + main_title are too big # 1 remove main title # 2 remove title explanation_size = self.explanation.text_size(context) self.show_title = True self.show_main_title = True if title_size.x + explanation_size.x > available_w: # keep only explanation self.show_title = False self.show_main_title = False elif main_title_size.x + title_size.x + explanation_size.x > available_w: # keep title + explanation self.show_main_title = False self.title.pos_3d = (x_min + self.spacing.x, pos.y) else: self.title.pos_3d = (x_min + self.spacing.x + main_title_size.x, pos.y) self.explanation.pos_3d = (x_max - self.spacing.x - explanation_size.x, pos.y) self.main_title.pos_3d = (x_min + self.spacing.x, pos.y) self.shortcut_area.draw(context) self.title_area.draw(context) if self.show_title: self.title.draw(context) if self.show_main_title: self.main_title.draw(context) self.explanation.draw(context) for s in shortcuts: s.draw(context) self.top = Vector((x_min, pos.y + main_title_size.y + self.spacing.y)) class GlCursorFence(): """ Cursor crossing Fence """ def __init__(self, width=1, colour=(1.0, 1.0, 1.0, 0.5), style=2852): self.line_x = GlLine(d=2, n_pts=2) self.line_x.style = style self.line_x.width = width self.line_x.colour_inactive = colour self.line_y = GlLine(d=2, n_pts=2) self.line_y.style = style self.line_y.width = width self.line_y.colour_inactive = colour self.on = True def set_location(self, context, location): w = context.region.width h = context.region.height p = Vector(location) x, y = p.x, p.y self.line_x.p = Vector((0, y)) self.line_x.v = Vector((w, 0)) self.line_y.p = Vector((x, 0)) self.line_y.v = Vector((0, h)) def enable(self): self.on = True def disable(self): self.on = False def draw(self, context, render=False): if self.on: self.line_x.draw(context) self.line_y.draw(context) class GlCursorArea(): def __init__(self, width=1, bordercolour=(1.0, 1.0, 1.0, 0.5), areacolour=(0.5, 0.5, 0.5, 0.08), style=2852): self.border = GlPolyline(bordercolour, d=2, n_pts=4) self.border.style = style self.border.width = width self.border.closed = True self.area = GlPolygon(areacolour, d=2, n_pts=4) self.min = Vector((0, 0)) self.max = Vector((0, 0)) self.on = False def in_area(self, pt): return (self.min.x <= pt.x and self.max.x >= pt.x and self.min.y <= pt.y and self.max.y >= pt.y) def set_location(self, context, p0, p1): p = Vector(p0) x0, y0 = p.x, p.y p = Vector(p1) x1, y1 = p.x, p.y if x0 > x1: x1, x0 = x0, x1 if y0 > y1: y1, y0 = y0, y1 self.min = Vector((x0, y0)) self.max = Vector((x1, y1)) pos = [ Vector((x0, y0)), Vector((x0, y1)), Vector((x1, y1)), Vector((x1, y0))] self.area.set_pos(pos) self.border.set_pos(pos) def enable(self): self.on = True def disable(self): self.on = False def draw(self, context, render=False): if self.on: # print("GlCursorArea.draw()") self.area.draw(context) self.border.draw(context)