blender-addons/archipack/archipack_gl.py

1433 lines
43 KiB
Python

# -*- 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 #####
# <pep8 compliant>
# ----------------------------------------------------------
# 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)