1785 lines
62 KiB
Cython
1785 lines
62 KiB
Cython
DEF LINE_CAP_NONE = 0
|
|
DEF LINE_CAP_SQUARE = 1
|
|
DEF LINE_CAP_ROUND = 2
|
|
|
|
DEF LINE_JOINT_NONE = 0
|
|
DEF LINE_JOINT_MITER = 1
|
|
DEF LINE_JOINT_BEVEL = 2
|
|
DEF LINE_JOINT_ROUND = 3
|
|
|
|
DEF LINE_MODE_POINTS = 0
|
|
DEF LINE_MODE_ELLIPSE = 1
|
|
DEF LINE_MODE_CIRCLE = 2
|
|
DEF LINE_MODE_RECTANGLE = 3
|
|
DEF LINE_MODE_ROUNDED_RECTANGLE = 4
|
|
DEF LINE_MODE_BEZIER = 5
|
|
|
|
from kivy.cache import Cache
|
|
from kivy.graphics.stencil_instructions cimport StencilUse, StencilUnUse, StencilPush, StencilPop
|
|
import itertools
|
|
|
|
# register graphics texture cache
|
|
Cache.register('kv.graphics.texture')
|
|
|
|
cdef float PI = <float>3.1415926535
|
|
|
|
cdef inline int line_intersection(double x1, double y1, double x2, double y2,
|
|
double x3, double y3, double x4, double y4, double *px, double *py):
|
|
cdef double u = (x1 * y2 - y1 * x2)
|
|
cdef double v = (x3 * y4 - y3 * x4)
|
|
cdef double denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
|
|
if denom == 0:
|
|
return 0
|
|
px[0] = (u * (x3 - x4) - (x1 - x2) * v) / denom
|
|
py[0] = (u * (y3 - y4) - (y1 - y2) * v) / denom
|
|
return 1
|
|
|
|
cdef class Line(VertexInstruction):
|
|
'''A 2d line.
|
|
|
|
Drawing a line can be done easily::
|
|
|
|
with self.canvas:
|
|
Line(points=[100, 100, 200, 100, 100, 200], width=10)
|
|
|
|
The line has 3 internal drawing modes that you should be aware of
|
|
for optimal results:
|
|
|
|
#. If the :attr:`width` is 1.0 and :attr:`force_custom_drawing_method` is False, then the
|
|
standard GL_LINE drawing from OpenGL will be used. :attr:`dash_length`,
|
|
:attr:`dash_offset`, and :attr:`dashes` will work, while properties for
|
|
cap and joint have no meaning here.
|
|
#. If the :attr:`width` is greater than 1.0 or :attr:`force_custom_drawing_method`
|
|
is True, then a custom drawing method, based on triangulation,
|
|
will be used. :attr:`dash_length`, :attr:`dash_offset`,
|
|
and :attr:`dashes` do not work in this mode.
|
|
Additionally, if the current color has an alpha less than 1.0, a
|
|
stencil will be used internally to draw the line.
|
|
|
|
.. image:: images/line-instruction.png
|
|
:align: center
|
|
|
|
:Parameters:
|
|
`points`: list
|
|
List of points in the format (x1, y1, x2, y2...)
|
|
`dash_length`: int
|
|
Length of a segment (if dashed), defaults to 1.
|
|
`dash_offset`: int
|
|
Offset between the end of a segment and the beginning of the
|
|
next one, defaults to 0. Changing this makes it dashed.
|
|
`dashes`: list of ints
|
|
List of [ON length, offset, ON length, offset, ...]. E.g. ``[2,4,1,6,8,2]``
|
|
would create a line with the first dash length 2 then an offset of 4 then
|
|
a dash length of 1 then an offset of 6 and so on. Defaults to ``[]``.
|
|
Changing this makes it dashed and overrides `dash_length` and `dash_offset`.
|
|
`width`: float
|
|
Width of the line, defaults to 1.0.
|
|
`cap`: str, defaults to 'round'
|
|
See :attr:`cap` for more information.
|
|
`joint`: str, defaults to 'round'
|
|
See :attr:`joint` for more information.
|
|
`cap_precision`: int, defaults to 10
|
|
See :attr:`cap_precision` for more information
|
|
`joint_precision`: int, defaults to 10
|
|
See :attr:`joint_precision` for more information
|
|
See :attr:`cap_precision` for more information.
|
|
`joint_precision`: int, defaults to 10
|
|
See :attr:`joint_precision` for more information.
|
|
`close`: bool, defaults to False
|
|
If True, the line will be closed.
|
|
`circle`: list
|
|
If set, the :attr:`points` will be set to build a circle. See
|
|
:attr:`circle` for more information.
|
|
`ellipse`: list
|
|
If set, the :attr:`points` will be set to build an ellipse. See
|
|
:attr:`ellipse` for more information.
|
|
`rectangle`: list
|
|
If set, the :attr:`points` will be set to build a rectangle. See
|
|
:attr:`rectangle` for more information.
|
|
`bezier`: list
|
|
If set, the :attr:`points` will be set to build a bezier line. See
|
|
:attr:`bezier` for more information.
|
|
`bezier_precision`: int, defaults to 180
|
|
Precision of the Bezier drawing.
|
|
`force_custom_drawing_method`: bool, defaults to False
|
|
Should the custom drawing method be used, instead of it depending on :attr:`width`
|
|
being equal to 1.o or not.
|
|
|
|
.. versionchanged:: 1.0.8
|
|
`dash_offset` and `dash_length` have been added.
|
|
|
|
.. versionchanged:: 1.4.1
|
|
`width`, `cap`, `joint`, `cap_precision`, `joint_precision`, `close`,
|
|
`ellipse`, `rectangle` have been added.
|
|
|
|
.. versionchanged:: 1.4.1
|
|
`bezier`, `bezier_precision` have been added.
|
|
|
|
.. versionchanged:: 1.11.0
|
|
`dashes` have been added
|
|
|
|
.. versionchanged:: 2.3.0
|
|
`force_custom_drawing_method` has been added
|
|
|
|
'''
|
|
cdef int _cap
|
|
cdef int _cap_precision
|
|
cdef int _joint_precision
|
|
cdef int _bezier_precision
|
|
cdef int _joint
|
|
cdef list _points
|
|
cdef list _dash_list
|
|
cdef float _width
|
|
cdef int _dash_offset, _dash_length
|
|
cdef int _use_stencil
|
|
cdef int _close
|
|
cdef str _close_mode
|
|
cdef int _force_custom_drawing_method
|
|
cdef int _mode
|
|
cdef Instruction _stencil_rect
|
|
cdef Instruction _stencil_push
|
|
cdef Instruction _stencil_use
|
|
cdef Instruction _stencil_unuse
|
|
cdef Instruction _stencil_pop
|
|
cdef double _bxmin, _bxmax, _bymin, _bymax
|
|
cdef tuple _mode_args
|
|
cdef tuple _rounded_rectangle, _rectangle, _ellipse, _circle
|
|
|
|
def __init__(self, **kwargs):
|
|
super(Line, self).__init__(**kwargs)
|
|
v = kwargs.get('points')
|
|
self.points = v if v is not None else []
|
|
self.dashes = kwargs.get('dashes', [])
|
|
self.batch.set_mode('line_strip')
|
|
self._dash_length = kwargs.get('dash_length') or 1
|
|
self._dash_offset = kwargs.get('dash_offset') or 0
|
|
self._width = kwargs.get('width') or 1.0
|
|
self.joint = kwargs.get('joint') or 'round'
|
|
self.cap = kwargs.get('cap') or 'round'
|
|
self._cap_precision = kwargs.get('cap_precision') or 10
|
|
self._joint_precision = kwargs.get('joint_precision') or 10
|
|
self._bezier_precision = kwargs.get('bezier_precision') or 180
|
|
self._close = int(bool(kwargs.get('close', 0)))
|
|
self._close_mode = kwargs.get('close_mode', 'straight-line')
|
|
self._force_custom_drawing_method = int(bool(kwargs.get('force_custom_drawing_method', 0)))
|
|
self._stencil_rect = None
|
|
self._stencil_push = None
|
|
self._stencil_use = None
|
|
self._stencil_unuse = None
|
|
self._stencil_pop = None
|
|
self._use_stencil = 0
|
|
|
|
if 'ellipse' in kwargs:
|
|
self.ellipse = kwargs['ellipse']
|
|
if 'circle' in kwargs:
|
|
self.circle = kwargs['circle']
|
|
if 'rectangle' in kwargs:
|
|
self.rectangle = kwargs['rectangle']
|
|
if 'rounded_rectangle' in kwargs:
|
|
self.rounded_rectangle = kwargs['rounded_rectangle']
|
|
if 'bezier' in kwargs:
|
|
self.bezier = kwargs['bezier']
|
|
|
|
cdef void build(self):
|
|
if self._mode == LINE_MODE_ELLIPSE:
|
|
self.prebuild_ellipse()
|
|
elif self._mode == LINE_MODE_CIRCLE:
|
|
self.prebuild_circle()
|
|
elif self._mode == LINE_MODE_RECTANGLE:
|
|
self.prebuild_rectangle()
|
|
elif self._mode == LINE_MODE_ROUNDED_RECTANGLE:
|
|
self.prebuild_rounded_rectangle()
|
|
elif self._mode == LINE_MODE_BEZIER:
|
|
self.prebuild_bezier()
|
|
if self._width == 1.0 and self._force_custom_drawing_method == 0:
|
|
self.build_legacy()
|
|
else:
|
|
self.build_extended()
|
|
|
|
cdef void ensure_stencil(self):
|
|
if self._stencil_rect == None:
|
|
self._stencil_rect = Rectangle()
|
|
self._stencil_push = StencilPush()
|
|
self._stencil_pop = StencilPop()
|
|
self._stencil_use = StencilUse(op='lequal')
|
|
self._stencil_unuse = StencilUnUse()
|
|
|
|
cdef int apply(self) except -1:
|
|
if self._width == 1. and self._force_custom_drawing_method == 0:
|
|
VertexInstruction.apply(self)
|
|
return 0
|
|
|
|
cdef double alpha = getActiveContext()['color'][-1]
|
|
self._use_stencil = alpha < 1
|
|
if self._use_stencil:
|
|
self.ensure_stencil()
|
|
|
|
self._stencil_push.apply()
|
|
VertexInstruction.apply(self)
|
|
self._stencil_use.apply()
|
|
self._stencil_rect.pos = self._bxmin, self._bymin
|
|
self._stencil_rect.size = self._bxmax - self._bxmin, self._bymax - self._bymin
|
|
self._stencil_rect.apply()
|
|
self._stencil_unuse.apply()
|
|
VertexInstruction.apply(self)
|
|
self._stencil_pop.apply()
|
|
else:
|
|
VertexInstruction.apply(self)
|
|
return 0
|
|
|
|
cdef void build_legacy(self):
|
|
cdef int i
|
|
cdef long count = <int>int(len(self.points) / 2.)
|
|
cdef list p = self.points
|
|
cdef vertex_t *vertices = NULL
|
|
cdef unsigned short *indices = NULL
|
|
cdef float tex_x
|
|
cdef char *buf = NULL
|
|
cdef Texture texture = self.texture
|
|
cdef int length = 0
|
|
cdef int position = 0
|
|
cdef int val
|
|
|
|
if count < 2:
|
|
self.batch.clear_data()
|
|
return
|
|
|
|
if self._close and self._close_mode == 'straight-line':
|
|
p = p + [p[0], p[1]]
|
|
count += 1
|
|
|
|
self.batch.set_mode('line_strip')
|
|
|
|
if self._dash_list:
|
|
length = sum(self._dash_list)
|
|
if texture is None or texture._width != length \
|
|
or texture._height != 1:
|
|
self.texture = texture = Texture.create(size=(length, 1))
|
|
texture.wrap = 'repeat'
|
|
|
|
# create a buffer to fill our texture
|
|
buf = <char *>malloc(4 * length)
|
|
memset(buf, 0, 4 * length)
|
|
|
|
for idx, val in enumerate(self._dash_list):
|
|
if idx % 2 == 0:
|
|
memset(buf + position, 255, val * 4)
|
|
position += val * 4
|
|
|
|
p_str = buf[:position]
|
|
try:
|
|
self.texture.blit_buffer(p_str, colorfmt='rgba', bufferfmt='ubyte')
|
|
finally:
|
|
free(buf)
|
|
elif self._dash_offset != 0:
|
|
length = self._dash_length + self._dash_offset
|
|
if texture is None or texture._width != \
|
|
(self._dash_length + self._dash_offset) or \
|
|
texture._height != 1:
|
|
|
|
self.texture = texture = Texture.create(
|
|
size=(self._dash_length + self._dash_offset, 1))
|
|
texture.wrap = 'repeat'
|
|
|
|
# create a buffer to fill our texture
|
|
buf = <char *>malloc(4 * (self._dash_length + self._dash_offset))
|
|
memset(buf, 255, self._dash_length * 4)
|
|
memset(buf + self._dash_length * 4, 0, self._dash_offset * 4)
|
|
p_str = buf[:(self._dash_length + self._dash_offset) * 4]
|
|
|
|
try:
|
|
self.texture.blit_buffer(p_str, colorfmt='rgba', bufferfmt='ubyte')
|
|
finally:
|
|
free(buf)
|
|
|
|
elif texture is not None:
|
|
self.texture = None
|
|
|
|
vertices = <vertex_t *>malloc(count * sizeof(vertex_t))
|
|
if vertices == NULL:
|
|
raise MemoryError('vertices')
|
|
|
|
indices = <unsigned short *>malloc(count * sizeof(unsigned short))
|
|
if indices == NULL:
|
|
free(vertices)
|
|
raise MemoryError('indices')
|
|
|
|
tex_x = 0
|
|
for i in range(count):
|
|
if (self._dash_offset != 0 or self._dash_list) and i > 0:
|
|
tex_x += <float>(sqrt(
|
|
pow(p[i * 2] - p[(i - 1) * 2], 2) +
|
|
pow(p[i * 2 + 1] - p[(i - 1) * 2 + 1], 2)) / length)
|
|
|
|
vertices[i].s0 = tex_x
|
|
vertices[i].t0 = 0
|
|
|
|
vertices[i].x = p[i * 2]
|
|
vertices[i].y = p[i * 2 + 1]
|
|
indices[i] = i
|
|
|
|
self.batch.set_data(vertices, <int>count, indices, <int>count)
|
|
|
|
free(vertices)
|
|
free(indices)
|
|
|
|
cdef void build_extended(self):
|
|
cdef int i, j
|
|
cdef long count = <int>int(len(self.points) / 2.)
|
|
cdef list p = self.points
|
|
cdef vertex_t *vertices = NULL
|
|
cdef unsigned short *indices = NULL
|
|
cdef float tex_x
|
|
cdef int cap
|
|
cdef char *buf = NULL
|
|
self.texture = None
|
|
|
|
self._bxmin = 999999999
|
|
self._bymin = 999999999
|
|
self._bxmax = -999999999
|
|
self._bymax = -999999999
|
|
|
|
if count < 2:
|
|
self.batch.clear_data()
|
|
return
|
|
|
|
cap = self._cap
|
|
if self._close and self._close_mode == 'straight-line' and count > 2:
|
|
p = p + p[0:4]
|
|
count += 2
|
|
cap = LINE_CAP_NONE
|
|
|
|
self.batch.set_mode('triangles')
|
|
cdef unsigned long vertices_count = (count - 1) * 4
|
|
cdef unsigned long indices_count = (count - 1) * 6
|
|
cdef unsigned int iv = 0, ii = 0, siv = 0
|
|
|
|
if self._joint == LINE_JOINT_BEVEL:
|
|
indices_count += (count - 2) * 3
|
|
vertices_count += (count - 2)
|
|
elif self._joint == LINE_JOINT_ROUND:
|
|
indices_count += (self._joint_precision * 3) * (count - 2)
|
|
vertices_count += (self._joint_precision) * (count - 2)
|
|
elif self._joint == LINE_JOINT_MITER:
|
|
indices_count += (count - 2) * 6
|
|
vertices_count += (count - 2) * 2
|
|
|
|
if cap == LINE_CAP_SQUARE:
|
|
indices_count += 12
|
|
vertices_count += 4
|
|
elif cap == LINE_CAP_ROUND:
|
|
indices_count += (self._cap_precision * 3) * 2
|
|
vertices_count += (self._cap_precision) * 2
|
|
|
|
vertices = <vertex_t *>malloc(vertices_count * sizeof(vertex_t))
|
|
if vertices == NULL:
|
|
raise MemoryError('vertices')
|
|
|
|
indices = <unsigned short *>malloc(indices_count * sizeof(unsigned short))
|
|
if indices == NULL:
|
|
free(vertices)
|
|
raise MemoryError('indices')
|
|
|
|
cdef double ax, ay, bx, _by, cx, cy, angle, a1, a2
|
|
cdef double x1, y1, x2, y2, x3, y3, x4, y4
|
|
cdef double sx1, sy1, sx4, sy4, sangle
|
|
cdef double pcx, pcy, px1, py1, px2, py2, px3, py3, px4, py4, pangle = 0, pangle2
|
|
cdef double w = self._width
|
|
cdef double ix, iy
|
|
cdef unsigned int piv, pii2, piv2, skip = 0
|
|
cdef double jangle
|
|
angle = sangle = 0
|
|
piv = pcx = pcy = cx = cy = ii = iv = ix = iy = 0
|
|
px1 = px2 = px3 = px4 = py1 = py2 = py3 = py4 = 0
|
|
sx1 = sy1 = sx4 = sy4 = 0
|
|
x1 = x2 = x3 = x4 = y1 = y2 = y3 = y4 = 0
|
|
cdef double cos1 = 0, cos2 = 0, sin1 = 0, sin2 = 0
|
|
for i in range(0, count - 1):
|
|
ax = p[i * 2]
|
|
ay = p[i * 2 + 1]
|
|
bx = p[i * 2 + 2]
|
|
_by = p[i * 2 + 3]
|
|
|
|
if (ax, ay) == (bx, _by):
|
|
skip += 1
|
|
continue
|
|
|
|
if i - skip > 0 and self._joint != LINE_JOINT_NONE:
|
|
pcx = cx
|
|
pcy = cy
|
|
px1 = x1
|
|
px2 = x2
|
|
px3 = x3
|
|
px4 = x4
|
|
py1 = y1
|
|
py2 = y2
|
|
py3 = y3
|
|
py4 = y4
|
|
|
|
piv2 = piv
|
|
piv = iv
|
|
pangle2 = pangle
|
|
pangle = angle
|
|
|
|
# calculate the orientation of the segment, between pi and -pi
|
|
cx = bx - ax
|
|
cy = _by - ay
|
|
angle = atan2(cy, cx)
|
|
a1 = angle - PI2
|
|
a2 = angle + PI2
|
|
|
|
# calculate the position of the segment
|
|
cos1 = cos(a1) * w
|
|
sin1 = sin(a1) * w
|
|
cos2 = cos(a2) * w
|
|
sin2 = sin(a2) * w
|
|
x1 = ax + cos1
|
|
y1 = ay + sin1
|
|
x4 = ax + cos2
|
|
y4 = ay + sin2
|
|
x2 = bx + cos1
|
|
y2 = _by + sin1
|
|
x3 = bx + cos2
|
|
y3 = _by + sin2
|
|
|
|
if i - skip == 0:
|
|
sx1 = x1
|
|
sy1 = y1
|
|
sx4 = x4
|
|
sy4 = y4
|
|
sangle = angle
|
|
|
|
indices[ii ] = iv
|
|
indices[ii + 1] = iv + 1
|
|
indices[ii + 2] = iv + 2
|
|
indices[ii + 3] = iv
|
|
indices[ii + 4] = iv + 2
|
|
indices[ii + 5] = iv + 3
|
|
ii += 6
|
|
|
|
vertices[iv].x = <float>x1
|
|
vertices[iv].y = <float>y1
|
|
vertices[iv].s0 = 0
|
|
vertices[iv].t0 = 0
|
|
iv += 1
|
|
vertices[iv].x = <float>x2
|
|
vertices[iv].y = <float>y2
|
|
vertices[iv].s0 = 1
|
|
vertices[iv].t0 = 0
|
|
iv += 1
|
|
vertices[iv].x = <float>x3
|
|
vertices[iv].y = <float>y3
|
|
vertices[iv].s0 = 1
|
|
vertices[iv].t0 = 1
|
|
iv += 1
|
|
vertices[iv].x = <float>x4
|
|
vertices[iv].y = <float>y4
|
|
vertices[iv].s0 = 0
|
|
vertices[iv].t0 = 1
|
|
iv += 1
|
|
|
|
# joint generation
|
|
if i - skip == 0 or self._joint == LINE_JOINT_NONE:
|
|
continue
|
|
|
|
# calculate the angle of the previous and current segment
|
|
jangle = atan2(
|
|
cx * pcy - cy * pcx,
|
|
cx * pcx + cy * pcy)
|
|
|
|
# in case of the angle is NULL, avoid the generation
|
|
if jangle == 0:
|
|
if self._joint == LINE_JOINT_ROUND:
|
|
vertices_count -= self._joint_precision
|
|
indices_count -= self._joint_precision * 3
|
|
elif self._joint == LINE_JOINT_BEVEL:
|
|
vertices_count -= 1
|
|
indices_count -= 3
|
|
elif self._joint == LINE_JOINT_MITER:
|
|
vertices_count -= 2
|
|
indices_count -= 6
|
|
continue
|
|
|
|
if self._joint == LINE_JOINT_BEVEL:
|
|
vertices[iv].x = <float>ax
|
|
vertices[iv].y = <float>ay
|
|
vertices[iv].s0 = 0
|
|
vertices[iv].t0 = 0
|
|
if jangle < 0:
|
|
indices[ii] = piv2 + 1
|
|
indices[ii + 1] = piv
|
|
indices[ii + 2] = iv
|
|
else:
|
|
indices[ii] = piv2 + 2
|
|
indices[ii + 1] = piv + 3
|
|
indices[ii + 2] = iv
|
|
ii += 3
|
|
iv += 1
|
|
|
|
elif self._joint == LINE_JOINT_MITER:
|
|
vertices[iv].x = <float>ax
|
|
vertices[iv].y = <float>ay
|
|
vertices[iv].s0 = 0
|
|
vertices[iv].t0 = 0
|
|
if jangle < 0:
|
|
if line_intersection(px1, py1, px2, py2, x1, y1, x2, y2, &ix, &iy) == 0:
|
|
vertices_count -= 2
|
|
indices_count -= 6
|
|
continue
|
|
vertices[iv + 1].x = <float>ix
|
|
vertices[iv + 1].y = <float>iy
|
|
vertices[iv + 1].s0 = 0
|
|
vertices[iv + 1].t0 = 0
|
|
indices[ii] = iv
|
|
indices[ii + 1] = iv + 1
|
|
indices[ii + 2] = piv2 + 1
|
|
indices[ii + 3] = iv
|
|
indices[ii + 4] = piv
|
|
indices[ii + 5] = iv + 1
|
|
ii += 6
|
|
iv += 2
|
|
else:
|
|
if line_intersection(px3, py3, px4, py4, x3, y3, x4, y4, &ix, &iy) == 0:
|
|
vertices_count -= 2
|
|
indices_count -= 6
|
|
continue
|
|
vertices[iv + 1].x = <float>ix
|
|
vertices[iv + 1].y = <float>iy
|
|
vertices[iv + 1].s0 = 0
|
|
vertices[iv + 1].t0 = 0
|
|
indices[ii] = iv
|
|
indices[ii + 1] = iv + 1
|
|
indices[ii + 2] = piv2 + 2
|
|
indices[ii + 3] = iv
|
|
indices[ii + 4] = piv + 3
|
|
indices[ii + 5] = iv + 1
|
|
ii += 6
|
|
iv += 2
|
|
|
|
elif self._joint == LINE_JOINT_ROUND:
|
|
|
|
# cap end
|
|
if jangle < 0:
|
|
a1 = pangle2 - PI2
|
|
a2 = angle + PI2
|
|
a0 = a2
|
|
step = (abs(jangle)) / float(self._joint_precision)
|
|
pivstart = piv + 3
|
|
pivend = piv2 + 1
|
|
else:
|
|
a1 = angle - PI2
|
|
a2 = pangle2 + PI2
|
|
a0 = a1
|
|
step = -(abs(jangle)) / float(self._joint_precision)
|
|
pivstart = piv
|
|
pivend = piv2 + 2
|
|
siv = iv
|
|
vertices[iv].x = <float>ax
|
|
vertices[iv].y = <float>ay
|
|
vertices[iv].s0 = 0
|
|
vertices[iv].t0 = 0
|
|
iv += 1
|
|
for j in xrange(0, self._joint_precision - 1):
|
|
vertices[iv].x = <float>(ax - cos(a0 - step * j) * w)
|
|
vertices[iv].y = <float>(ay - sin(a0 - step * j) * w)
|
|
vertices[iv].s0 = 0
|
|
vertices[iv].t0 = 0
|
|
if j == 0:
|
|
indices[ii] = siv
|
|
indices[ii + 1] = <unsigned short>pivstart
|
|
indices[ii + 2] = iv
|
|
else:
|
|
indices[ii] = siv
|
|
indices[ii + 1] = iv - 1
|
|
indices[ii + 2] = iv
|
|
iv += 1
|
|
ii += 3
|
|
indices[ii] = siv
|
|
indices[ii + 1] = iv - 1
|
|
indices[ii + 2] = <unsigned short>pivend
|
|
ii += 3
|
|
|
|
# caps
|
|
if cap == LINE_CAP_SQUARE:
|
|
vertices[iv].x = <float>(x2 + cos(angle) * w)
|
|
vertices[iv].y = <float>(y2 + sin(angle) * w)
|
|
vertices[iv].s0 = 0
|
|
vertices[iv].t0 = 0
|
|
vertices[iv + 1].x = <float>(x3 + cos(angle) * w)
|
|
vertices[iv + 1].y = <float>(y3 + sin(angle) * w)
|
|
vertices[iv + 1].s0 = 0
|
|
vertices[iv + 1].t0 = 0
|
|
indices[ii] = piv + 1
|
|
indices[ii + 1] = piv + 2
|
|
indices[ii + 2] = iv + 1
|
|
indices[ii + 3] = piv + 1
|
|
indices[ii + 4] = iv
|
|
indices[ii + 5] = iv + 1
|
|
ii += 6
|
|
iv += 2
|
|
vertices[iv].x = <float>(sx1 - cos(sangle) * w)
|
|
vertices[iv].y = <float>(sy1 - sin(sangle) * w)
|
|
vertices[iv].s0 = 0
|
|
vertices[iv].t0 = 0
|
|
vertices[iv + 1].x = <float>(sx4 - cos(sangle) * w)
|
|
vertices[iv + 1].y = <float>(sy4 - sin(sangle) * w)
|
|
vertices[iv + 1].s0 = 0
|
|
vertices[iv + 1].t0 = 0
|
|
indices[ii] = 0
|
|
indices[ii + 1] = 3
|
|
indices[ii + 2] = iv + 1
|
|
indices[ii + 3] = 0
|
|
indices[ii + 4] = iv
|
|
indices[ii + 5] = iv + 1
|
|
ii += 6
|
|
iv += 2
|
|
|
|
elif cap == LINE_CAP_ROUND:
|
|
|
|
# cap start
|
|
a1 = sangle - PI2
|
|
a2 = sangle + PI2
|
|
step = (a1 - a2) / float(self._cap_precision)
|
|
siv = iv
|
|
cx = p[0]
|
|
cy = p[1]
|
|
vertices[iv].x = <float>cx
|
|
vertices[iv].y = <float>cy
|
|
vertices[iv].s0 = 0
|
|
vertices[iv].t0 = 0
|
|
iv += 1
|
|
for i in xrange(0, self._cap_precision - 1):
|
|
vertices[iv].x = <float>(cx + cos(a1 + step * i) * w)
|
|
vertices[iv].y = <float>(cy + sin(a1 + step * i) * w)
|
|
vertices[iv].s0 = 1
|
|
vertices[iv].t0 = 1
|
|
if i == 0:
|
|
indices[ii] = siv
|
|
indices[ii + 1] = 0
|
|
indices[ii + 2] = iv
|
|
else:
|
|
indices[ii] = siv
|
|
indices[ii + 1] = iv - 1
|
|
indices[ii + 2] = iv
|
|
iv += 1
|
|
ii += 3
|
|
indices[ii] = siv
|
|
indices[ii + 1] = iv - 1
|
|
indices[ii + 2] = 3
|
|
ii += 3
|
|
|
|
# cap end
|
|
a1 = angle - PI2
|
|
a2 = angle + PI2
|
|
step = (a2 - a1) / float(self._cap_precision)
|
|
siv = iv
|
|
cx = p[-2]
|
|
cy = p[-1]
|
|
vertices[iv].x = <float>cx
|
|
vertices[iv].y = <float>cy
|
|
vertices[iv].s0 = 0
|
|
vertices[iv].t0 = 0
|
|
iv += 1
|
|
for i in xrange(0, self._cap_precision - 1):
|
|
vertices[iv].x = <float>(cx + cos(a1 + step * i) * w)
|
|
vertices[iv].y = <float>(cy + sin(a1 + step * i) * w)
|
|
vertices[iv].s0 = 0
|
|
vertices[iv].t0 = 0
|
|
if i == 0:
|
|
indices[ii] = siv
|
|
indices[ii + 1] = piv + 1
|
|
indices[ii + 2] = iv
|
|
else:
|
|
indices[ii] = siv
|
|
indices[ii + 1] = iv - 1
|
|
indices[ii + 2] = iv
|
|
iv += 1
|
|
ii += 3
|
|
indices[ii] = siv
|
|
indices[ii + 1] = iv - 1
|
|
indices[ii + 2] = piv + 2
|
|
ii += 3
|
|
|
|
while ii < indices_count:
|
|
# make all the remaining indices point to the last vertice
|
|
indices[ii] = siv
|
|
ii += 1
|
|
|
|
# compute bbox
|
|
cdef unsigned long iul
|
|
for iul in xrange(vertices_count):
|
|
if vertices[iul].x < self._bxmin:
|
|
self._bxmin = vertices[iul].x
|
|
if vertices[iul].x > self._bxmax:
|
|
self._bxmax = vertices[iul].x
|
|
if vertices[iul].y < self._bymin:
|
|
self._bymin = vertices[iul].y
|
|
if vertices[iul].y > self._bymax:
|
|
self._bymax = vertices[iul].y
|
|
|
|
self.batch.set_data(vertices, <int>vertices_count,
|
|
indices, <int>indices_count)
|
|
|
|
free(vertices)
|
|
free(indices)
|
|
|
|
property points:
|
|
'''Property for getting/settings points of the line
|
|
|
|
.. warning::
|
|
|
|
This will always reconstruct the whole graphics from the new points
|
|
list. It can be very CPU expensive.
|
|
'''
|
|
def __get__(self):
|
|
return self._points
|
|
|
|
def __set__(self, points):
|
|
if points and isinstance(points[0], (list, tuple)):
|
|
self._points = list(itertools.chain(*points))
|
|
else:
|
|
self._points = list(points)
|
|
|
|
self._mode = LINE_MODE_POINTS
|
|
self.flag_data_update()
|
|
|
|
property dash_length:
|
|
'''Property for getting/setting the length of the dashes in the curve
|
|
|
|
.. versionadded:: 1.0.8
|
|
'''
|
|
def __get__(self):
|
|
return self._dash_length
|
|
|
|
def __set__(self, value):
|
|
if value < 0:
|
|
raise GraphicException('Invalid dash_length value, must be >= 0')
|
|
self._dash_length = value
|
|
self.flag_data_update()
|
|
|
|
property dash_offset:
|
|
'''Property for getting/setting the offset between the dashes in the curve
|
|
|
|
.. versionadded:: 1.0.8
|
|
'''
|
|
def __get__(self):
|
|
return self._dash_offset
|
|
|
|
def __set__(self, value):
|
|
if value < 0:
|
|
raise GraphicException('Invalid dash_offset value, must be >= 0')
|
|
self._dash_offset = value
|
|
self.flag_data_update()
|
|
|
|
property dashes:
|
|
'''Property for getting/setting ``dashes``.
|
|
|
|
List of [ON length, offset, ON length, offset, ...]. E.g. ``[2,4,1,6,8,2]``
|
|
would create a line with the first dash length 2 then an offset of 4 then
|
|
a dash length of 1 then an offset of 6 and so on.
|
|
|
|
.. versionadded:: 1.11.0
|
|
'''
|
|
def __get__(self):
|
|
return self._dash_list
|
|
|
|
def __set__(self, value):
|
|
self._dash_list = list(value)
|
|
self.flag_data_update()
|
|
|
|
property width:
|
|
'''Determine the width of the line, defaults to 1.0.
|
|
|
|
.. versionadded:: 1.4.1
|
|
'''
|
|
def __get__(self):
|
|
return self._width
|
|
|
|
def __set__(self, value):
|
|
if value <= 0:
|
|
raise GraphicException('Invalid width value, must be > 0')
|
|
self._width = value
|
|
self.flag_data_update()
|
|
|
|
property cap:
|
|
'''Determine the cap of the line, defaults to 'round'. Can be one of
|
|
'none', 'square' or 'round'
|
|
|
|
.. versionadded:: 1.4.1
|
|
'''
|
|
def __get__(self):
|
|
if self._cap == LINE_CAP_SQUARE:
|
|
return 'square'
|
|
elif self._cap == LINE_CAP_ROUND:
|
|
return 'round'
|
|
return 'none'
|
|
|
|
def __set__(self, value):
|
|
if value not in ('none', 'square', 'round'):
|
|
raise GraphicException('Invalid cap, must be one of '
|
|
'"none", "square", "round"')
|
|
if value == 'square':
|
|
self._cap = LINE_CAP_SQUARE
|
|
elif value == 'round':
|
|
self._cap = LINE_CAP_ROUND
|
|
else:
|
|
self._cap = LINE_CAP_NONE
|
|
self.flag_data_update()
|
|
|
|
property joint:
|
|
'''Determine the join of the line, defaults to 'round'. Can be one of
|
|
'none', 'round', 'bevel', 'miter'.
|
|
|
|
.. versionadded:: 1.4.1
|
|
'''
|
|
|
|
def __get__(self):
|
|
if self._joint == LINE_JOINT_ROUND:
|
|
return 'round'
|
|
elif self._joint == LINE_JOINT_BEVEL:
|
|
return 'bevel'
|
|
elif self._joint == LINE_JOINT_MITER:
|
|
return 'miter'
|
|
return 'none'
|
|
|
|
def __set__(self, value):
|
|
if value not in ('none', 'miter', 'bevel', 'round'):
|
|
raise GraphicException('Invalid joint, must be one of '
|
|
'"none", "miter", "bevel", "round"')
|
|
if value == 'round':
|
|
self._joint = LINE_JOINT_ROUND
|
|
elif value == 'bevel':
|
|
self._joint = LINE_JOINT_BEVEL
|
|
elif value == 'miter':
|
|
self._joint = LINE_JOINT_MITER
|
|
else:
|
|
self._joint = LINE_JOINT_NONE
|
|
self.flag_data_update()
|
|
|
|
property cap_precision:
|
|
'''Number of iteration for drawing the "round" cap, defaults to 10.
|
|
The cap_precision must be at least 1.
|
|
|
|
.. versionadded:: 1.4.1
|
|
'''
|
|
|
|
def __get__(self):
|
|
return self._cap_precision
|
|
|
|
def __set__(self, value):
|
|
if value < 1:
|
|
raise GraphicException('Invalid cap_precision value, must be >= 1')
|
|
self._cap_precision = int(value)
|
|
self.flag_data_update()
|
|
|
|
property joint_precision:
|
|
'''Number of iteration for drawing the "round" joint, defaults to 10.
|
|
The joint_precision must be at least 1.
|
|
|
|
.. versionadded:: 1.4.1
|
|
'''
|
|
|
|
def __get__(self):
|
|
return self._joint_precision
|
|
|
|
def __set__(self, value):
|
|
if value < 1:
|
|
raise GraphicException('Invalid joint_precision value, must be >= 1')
|
|
self._joint_precision = int(value)
|
|
self.flag_data_update()
|
|
|
|
property close:
|
|
'''If True, the line will be closed by joining the two ends, according to :attr:`close_mode`.
|
|
|
|
.. versionadded:: 1.4.1
|
|
'''
|
|
|
|
def __get__(self):
|
|
return self._close
|
|
|
|
def __set__(self, value):
|
|
self._close = int(bool(value))
|
|
self.flag_data_update()
|
|
|
|
@property
|
|
def close_mode(self):
|
|
'''Defines how the ends of the line will be connected.
|
|
Defaults to ``"straight-line"``.
|
|
|
|
.. note::
|
|
Support for the different closing modes depends on drawing shapes.
|
|
|
|
Available modes:
|
|
|
|
- ``"straight-line"`` (all drawing shapes): the ends will be closed by a straight line.
|
|
- ``"center-connected"`` (:attr:`ellipse` specific): the ends will be closed by a line passing through the center of the ellipse.
|
|
|
|
.. versionadded:: 2.2.0
|
|
'''
|
|
return self._close_mode
|
|
|
|
@close_mode.setter
|
|
def close_mode(self, value):
|
|
if value not in ("straight-line", "center-connected"):
|
|
raise GraphicException(f'{self.__class__.__name__} - Invalid close_mode, must be one of "straight-line" or "center-connected".')
|
|
self._close_mode = value
|
|
self.flag_data_update()
|
|
|
|
property force_custom_drawing_method:
|
|
'''If True, the line will be drawn using the custom drawing method, no matter what the width is.
|
|
|
|
.. versionadded:: 2.3.0
|
|
'''
|
|
|
|
def __get__(self):
|
|
return self._force_custom_drawing_method
|
|
|
|
def __set__(self, value):
|
|
self._force_custom_drawing_method = int(bool(value))
|
|
self.flag_data_update()
|
|
|
|
property ellipse:
|
|
'''Use this property to build an ellipse, without calculating the
|
|
:attr:`points`.
|
|
|
|
The argument must be a tuple of (x, y, width, height, angle_start,
|
|
angle_end, segments):
|
|
|
|
* x and y represent the bottom left of the ellipse
|
|
* width and height represent the size of the ellipse
|
|
* (optional) angle_start and angle_end are in degree. The default
|
|
value is 0 and 360.
|
|
* (optional) segments is the precision of the ellipse. The default
|
|
value is calculated from the range between angle. You can use this
|
|
property to create polygons with 3 or more sides. Values smaller than
|
|
3 will not be represented and the number of segments will be
|
|
automatically calculated.
|
|
|
|
Note that it's up to you to :attr:`close` or not.
|
|
If you choose to close, use :attr:`close_mode` to define how the figure
|
|
will be closed. Whether it will be by closed by a ``"straight-line"``
|
|
or by ``"center-connected"``.
|
|
|
|
For example, for building a simple ellipse, in python::
|
|
|
|
# simple ellipse
|
|
Line(ellipse=(0, 0, 150, 150))
|
|
|
|
# only from 90 to 180 degrees
|
|
Line(ellipse=(0, 0, 150, 150, 90, 180))
|
|
|
|
# only from 90 to 180 degrees, with few segments
|
|
Line(ellipse=(0, 0, 150, 150, 90, 180, 20))
|
|
|
|
.. versionadded:: 1.4.1
|
|
|
|
.. versionchanged:: 2.2.0
|
|
Now you can get the ellipse generated through the property.
|
|
|
|
The minimum number of segments allowed is 3. Smaller values will be
|
|
ignored and the number of segments will be automatically calculated.
|
|
'''
|
|
|
|
def __get__(self):
|
|
return self._ellipse
|
|
|
|
def __set__(self, args):
|
|
if args == None:
|
|
raise GraphicException(
|
|
'Invalid ellipse value: {0!r}'.format(args))
|
|
if len(args) not in (4, 6, 7):
|
|
raise GraphicException('Invalid number of arguments: '
|
|
'{0} instead of 4, 6 or 7.'.format(len(args)))
|
|
self._mode_args = tuple(args)
|
|
self._mode = LINE_MODE_ELLIPSE
|
|
self.flag_data_update()
|
|
|
|
cdef void prebuild_ellipse(self):
|
|
cdef double x, y, w, h, angle_start = 0, angle_end = 360
|
|
cdef int angle_dir, segments = 0, extra_segments = 0
|
|
cdef double angle_range
|
|
cdef tuple args = self._mode_args
|
|
cdef bint center_connected = self._close and self._close_mode == "center-connected"
|
|
|
|
extra_segments = 3 if center_connected else 1
|
|
|
|
if len(args) == 4:
|
|
x, y, w, h = args
|
|
elif len(args) == 6:
|
|
x, y, w, h, angle_start, angle_end = args
|
|
elif len(args) == 7:
|
|
x, y, w, h, angle_start, angle_end, segments = args
|
|
else:
|
|
x = y = w = h = 0
|
|
assert 0
|
|
|
|
if 0 in (w, h):
|
|
return
|
|
|
|
if segments < 3:
|
|
if segments != 0:
|
|
Logger.warning(f'{self.__class__.__name__} - ellipse: A minimum of 3 segments is required. The default value will be used instead.')
|
|
segments = int(abs(angle_end - angle_start) / 2) + extra_segments
|
|
|
|
segments += extra_segments
|
|
segments *= 2
|
|
|
|
if angle_end > angle_start:
|
|
angle_dir = 1
|
|
else:
|
|
angle_dir = -1
|
|
|
|
# Resulting ellipse
|
|
self._ellipse = (x, y, w, h, angle_start, angle_end, segments)
|
|
# Reset other properties
|
|
self._rounded_rectangle = self._rectangle = self._circle = None
|
|
|
|
# rad = deg * (pi / 180), where pi/180 = 0.0174...
|
|
angle_start = angle_start * 0.017453292519943295
|
|
angle_end = angle_end * 0.017453292519943295
|
|
angle_range = abs(angle_end - angle_start) / (segments - extra_segments * 2)
|
|
|
|
|
|
cdef list points = [0, ] * segments
|
|
cdef double angle
|
|
cdef double rx = w * 0.5
|
|
cdef double ry = h * 0.5
|
|
cdef int inc_x = 0, inc_y = 1
|
|
|
|
if center_connected and angle_start != angle_end:
|
|
points[0] = points[segments - 2] = x + rx
|
|
points[1] = points[segments - 1] = y + ry
|
|
|
|
inc_x = 2
|
|
inc_y = 3
|
|
segments -= 4
|
|
|
|
for i in xrange(0, segments, 2):
|
|
angle = angle_start + (angle_dir * i * angle_range)
|
|
points[i + inc_x] = (x + rx) + (rx * sin(angle))
|
|
points[i + inc_y] = (y + ry) + (ry * cos(angle))
|
|
|
|
self._points = points
|
|
|
|
property circle:
|
|
'''Use this property to build a circle, without calculating the
|
|
:attr:`points`.
|
|
|
|
The argument must be a tuple of (center_x, center_y, radius, angle_start,
|
|
angle_end, segments):
|
|
|
|
* center_x and center_y represent the center of the circle
|
|
* radius represent the radius of the circle
|
|
* (optional) angle_start and angle_end are in degree. The default
|
|
value is 0 and 360.
|
|
* (optional) segments is the precision of the ellipse. The default
|
|
value is calculated from the range between angle.
|
|
|
|
Note that it's up to you to :attr:`close` the circle or not.
|
|
|
|
For example, for building a simple ellipse, in python::
|
|
|
|
# simple circle
|
|
Line(circle=(150, 150, 50))
|
|
|
|
# only from 90 to 180 degrees
|
|
Line(circle=(150, 150, 50, 90, 180))
|
|
|
|
# only from 90 to 180 degrees, with few segments
|
|
Line(circle=(150, 150, 50, 90, 180, 20))
|
|
|
|
.. versionadded:: 1.4.1
|
|
|
|
.. versionchanged:: 2.2.0
|
|
Now you can get the circle generated through the property.
|
|
|
|
'''
|
|
|
|
def __get__(self):
|
|
return self._circle
|
|
|
|
def __set__(self, args):
|
|
if args == None:
|
|
raise GraphicException(
|
|
'Invalid circle value: {0!r}'.format(args))
|
|
if len(args) not in (3, 5, 6):
|
|
raise GraphicException('Invalid number of arguments: '
|
|
'{0} instead of 3, 5 or 6.'.format(len(args)))
|
|
self._mode_args = tuple(args)
|
|
self._mode = LINE_MODE_CIRCLE
|
|
self.flag_data_update()
|
|
|
|
cdef void prebuild_circle(self):
|
|
cdef double x, y, r, angle_start = 0, angle_end = 360
|
|
cdef int angle_dir, segments = 0
|
|
cdef double angle_range
|
|
cdef tuple args = self._mode_args
|
|
|
|
if len(args) == 3:
|
|
x, y, r = args
|
|
elif len(args) == 5:
|
|
x, y, r, angle_start, angle_end = args
|
|
elif len(args) == 6:
|
|
x, y, r, angle_start, angle_end, segments = args
|
|
segments += 1
|
|
else:
|
|
x = y = r = 0
|
|
assert 0
|
|
|
|
if angle_end > angle_start:
|
|
angle_dir = 1
|
|
else:
|
|
angle_dir = -1
|
|
if segments == 0:
|
|
segments = int(abs(angle_end - angle_start) / 2) + 3
|
|
|
|
# Resulting circle
|
|
self._circle = (x, y, r, angle_start, angle_end, segments)
|
|
# Reset other properties
|
|
self._rounded_rectangle = self._rectangle = self._ellipse = None
|
|
|
|
segmentpoints = segments * 2
|
|
|
|
# rad = deg * (pi / 180), where pi/180 = 0.0174...
|
|
angle_start = angle_start * 0.017453292519943295
|
|
angle_end = angle_end * 0.017453292519943295
|
|
angle_range = abs(angle_end - angle_start) / (segmentpoints - 2)
|
|
|
|
cdef list points = [0, ] * segmentpoints
|
|
cdef double angle
|
|
for i in xrange(0, segmentpoints, 2):
|
|
angle = angle_start + (angle_dir * i * angle_range)
|
|
points[i] = x + (r * sin(angle))
|
|
points[i + 1] = y + (r * cos(angle))
|
|
self._points = points
|
|
|
|
property rectangle:
|
|
'''Use this property to build a rectangle, without calculating the
|
|
:attr:`points`.
|
|
|
|
The argument must be a tuple of (x, y, width, height):
|
|
|
|
* x and y represent the bottom-left position of the rectangle
|
|
* width and height represent the size
|
|
|
|
The line is automatically closed.
|
|
|
|
Usage::
|
|
|
|
Line(rectangle=(0, 0, 200, 200))
|
|
|
|
.. versionadded:: 1.4.1
|
|
|
|
.. versionchanged:: 2.2.0
|
|
Now you can get the rectangle generated through the property.
|
|
|
|
'''
|
|
|
|
def __get__(self):
|
|
return self._rectangle
|
|
|
|
def __set__(self, args):
|
|
if args == None:
|
|
raise GraphicException(
|
|
'Invalid rectangle value: {0!r}'.format(args))
|
|
if len(args) != 4:
|
|
raise GraphicException('Invalid number of arguments: '
|
|
'{0} instead of 4.'.format(len(args)))
|
|
self._mode_args = tuple(args)
|
|
self._mode = LINE_MODE_RECTANGLE
|
|
self.flag_data_update()
|
|
|
|
cdef void prebuild_rectangle(self):
|
|
cdef double x, y, width, height
|
|
cdef int angle_dir, segments = 0
|
|
cdef double angle_range
|
|
cdef tuple args = self._mode_args
|
|
|
|
if args == None:
|
|
raise GraphicException(
|
|
'Invalid ellipse value: {0!r}'.format(args))
|
|
|
|
if len(args) == 4:
|
|
x, y, width, height = args
|
|
else:
|
|
x = y = width = height = 0
|
|
assert 0
|
|
|
|
# Resulting rectangle
|
|
self._rectangle = (x, y, width, height)
|
|
# Reset other properties
|
|
self._rounded_rectangle = self._circle = self._ellipse = None
|
|
|
|
self._points = [x, y, x + width, y, x + width, y + height, x, y + height]
|
|
self._close = 1
|
|
|
|
property rounded_rectangle:
|
|
'''Use this property to build a rectangle, without calculating the
|
|
:attr:`points`.
|
|
|
|
The argument must be a tuple of one of the following forms:
|
|
|
|
* (x, y, width, height, corner_radius)
|
|
* (x, y, width, height, corner_radius, resolution)
|
|
* (x, y, width, height, corner_radius1, corner_radius2, corner_radius3, corner_radius4)
|
|
* (x, y, width, height, corner_radius1, corner_radius2, corner_radius3, corner_radius4, resolution)
|
|
|
|
* `x` and `y` represent the bottom-left position of the rectangle.
|
|
* `width` and `height` represent the size.
|
|
* `corner_radius` specifies the radius used for the rounded corners clockwise: top-left, top-right, bottom-right, bottom-left.
|
|
* `resolution` is the number of line segment that will be used to draw the circle arc at each corner (defaults to 45).
|
|
|
|
The line is automatically closed.
|
|
|
|
Usage::
|
|
|
|
Line(rounded_rectangle=(0, 0, 200, 200, 10, 20, 30, 40, 100))
|
|
|
|
.. versionadded:: 1.9.0
|
|
|
|
.. versionchanged:: 2.2.0
|
|
Default value of `resolution` changed from 30 to 45.
|
|
|
|
Now you can get the rounded rectangle generated through the property.
|
|
|
|
The order of `corner_radius` has been changed to match the RoundedRectangle radius property (clockwise).
|
|
It was bottom-left, bottom-right, top-right, top-left in previous versions.
|
|
Now both are clockwise: top-left, top-right, bottom-right, bottom-left.
|
|
To keep the corner radius order without changing the order manually, you can use python's built-in method `reversed` or `[::-1]`,
|
|
to reverse the order of the corner radius.
|
|
|
|
'''
|
|
|
|
def __get__(self):
|
|
return self._rounded_rectangle
|
|
|
|
def __set__(self, args):
|
|
if args == None:
|
|
raise GraphicException(
|
|
'Invalid rounded rectangle value: {0!r}'.format(args))
|
|
if len(args) not in (5, 6, 8, 9):
|
|
raise GraphicException('invalid number of arguments:'
|
|
'{0} not in (5, 6, 8, 9)'.format(len(args)))
|
|
self._mode_args = tuple(args)
|
|
self._mode = LINE_MODE_ROUNDED_RECTANGLE
|
|
self.flag_data_update()
|
|
|
|
cdef void prebuild_rounded_rectangle(self):
|
|
cdef float a, max_a, px, py, x, y, w, h, c1, c2, c3, c4, step, min_dimension, half_min_dimension
|
|
cdef resolution = 45
|
|
cdef int l = <int>len(self._mode_args)
|
|
|
|
self._points = []
|
|
x, y, w, h = self._mode_args [:4]
|
|
|
|
# zero size of the figure + avoid rendering issue with SmoothLine
|
|
if w <= 0 or h <= 0 or isinstance(self, SmoothLine) and (w < 2 or h < 2):
|
|
return
|
|
|
|
if l == 5:
|
|
c1 = c2 = c3 = c4 = self._mode_args[4]
|
|
elif l == 6:
|
|
c1 = c2 = c3 = c4 = self._mode_args[4]
|
|
resolution = self._mode_args[5]
|
|
elif l == 8:
|
|
c1, c2, c3, c4 = self._mode_args[4:]
|
|
else: # l == 9, but else make the compiler happy about uninitialization
|
|
c1, c2, c3, c4 = self._mode_args[4:8]
|
|
resolution = self._mode_args[8]
|
|
|
|
if resolution <= 4:
|
|
resolution = 4
|
|
|
|
# The minimum radius needs to be limited to 1px.
|
|
# This avoid some known rendering issues with Line/SmoothLine.
|
|
c1 = max(c1, 1.0)
|
|
c2 = max(c2, 1.0)
|
|
c3 = max(c3, 1.0)
|
|
c4 = max(c4, 1.0)
|
|
min_dimension = min(w, h)
|
|
half_min_dimension = min_dimension / 2.0
|
|
|
|
# If larger values are passed for each corner, we will have to make some adjustments.
|
|
if c1 > half_min_dimension:
|
|
c2 = min(c2, half_min_dimension)
|
|
c4 = min(c4, half_min_dimension)
|
|
c1 = min(c1, min_dimension - c2, min_dimension - c4)
|
|
|
|
if c2 > half_min_dimension:
|
|
c1 = min(c1, half_min_dimension)
|
|
c3 = min(c3, half_min_dimension)
|
|
c2 = min(c2, min_dimension - c1, min_dimension - c3)
|
|
|
|
if c3 > half_min_dimension:
|
|
c2 = min(c2, half_min_dimension)
|
|
c4 = min(c4, half_min_dimension)
|
|
c3 = min(c3, min_dimension - c2, min_dimension - c4)
|
|
|
|
if c4 > half_min_dimension:
|
|
c3 = min(c3, half_min_dimension)
|
|
c1 = min(c1, half_min_dimension)
|
|
c4 = min(c4, min_dimension - c3, min_dimension - c1)
|
|
|
|
# Resulting rounded_rectangle
|
|
self._rounded_rectangle = (x, y, w, h, c1, c2, c3, c4, resolution)
|
|
# Reset other properties
|
|
self._rectangle = self._ellipse = self._circle = None
|
|
|
|
step = PI / resolution
|
|
max_a = PI / 2.0 - step
|
|
|
|
# top-left
|
|
a = 0.0
|
|
px = x + c1
|
|
py = y + h - c1
|
|
|
|
while a < max_a:
|
|
a += step
|
|
self._points.extend([
|
|
px - cos(a) * c1,
|
|
py + sin(a) * c1
|
|
])
|
|
|
|
# top-right
|
|
a = 0.0
|
|
px = x + w - c2
|
|
py = y + h - c2
|
|
|
|
while a < max_a:
|
|
a += step
|
|
self._points.extend([
|
|
px + sin(a) * c2,
|
|
py + cos(a) * c2
|
|
])
|
|
|
|
# bottom-right
|
|
a = 0.0
|
|
px = x + w - c3
|
|
py = y + c3
|
|
|
|
while a < max_a:
|
|
a += step
|
|
self._points.extend([
|
|
px + cos(a) * c3,
|
|
py - sin(a) * c3
|
|
])
|
|
|
|
# bottom-left
|
|
a = 0.0
|
|
px = x + c4
|
|
py = y + c4
|
|
|
|
while a < max_a:
|
|
a += step
|
|
self._points.extend([
|
|
px - sin(a) * c4,
|
|
py - cos(a) * c4
|
|
])
|
|
|
|
self._close = 1
|
|
|
|
property bezier:
|
|
'''Use this property to build a bezier line, without calculating the
|
|
:attr:`points`. You can only set this property, not get it.
|
|
|
|
The argument must be a tuple of 2n elements, n being the number of points.
|
|
|
|
Usage::
|
|
|
|
Line(bezier=(x1, y1, x2, y2, x3, y3)
|
|
|
|
.. versionadded:: 1.4.2
|
|
|
|
.. note:: Bezier lines calculations are inexpensive for a low number of
|
|
points, but complexity is quadratic, so lines with a lot of points
|
|
can be very expensive to build, use with care!
|
|
'''
|
|
|
|
def __set__(self, args):
|
|
if args == None or len(args) % 2:
|
|
raise GraphicException(
|
|
'Invalid bezier value: {0!r}'.format(args))
|
|
self._mode_args = tuple(args)
|
|
self._mode = LINE_MODE_BEZIER
|
|
self.flag_data_update()
|
|
|
|
cdef void prebuild_bezier(self):
|
|
cdef double x, y, l
|
|
cdef int segments = self._bezier_precision
|
|
cdef list T = list(self._mode_args)[:]
|
|
|
|
self._points = []
|
|
for x in xrange(segments):
|
|
l = x / (1.0 * segments)
|
|
# http://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm
|
|
# as the list is in the form of (x1, y1, x2, y2...) iteration is
|
|
# done on each item and the current item (xn or yn) in the list is
|
|
# replaced with a calculation of "xn + x(n+1) - xn" x(n+1) is
|
|
# placed at n+2. Each iteration makes the list one item shorter
|
|
for i in range(1, len(T)):
|
|
for j in xrange(len(T) - 2*i):
|
|
T[j] = T[j] + (T[j+2] - T[j]) * l
|
|
|
|
# we got the coordinates of the point in T[0] and T[1]
|
|
self._points.append(T[0])
|
|
self._points.append(T[1])
|
|
|
|
# add one last point to join the curve to the end
|
|
self._points.append(T[-2])
|
|
self._points.append(T[-1])
|
|
|
|
property bezier_precision:
|
|
'''Number of iteration for drawing the bezier between 2 segments,
|
|
defaults to 180. The bezier_precision must be at least 1.
|
|
|
|
.. versionadded:: 1.4.2
|
|
'''
|
|
|
|
def __get__(self):
|
|
return self._bezier_precision
|
|
|
|
def __set__(self, value):
|
|
if value < 1:
|
|
raise GraphicException('Invalid bezier_precision value, must be >= 1')
|
|
self._bezier_precision = int(value)
|
|
self.flag_data_update()
|
|
|
|
|
|
cdef class SmoothLine(Line):
|
|
'''Experimental line using over-draw methods to get better anti-aliasing
|
|
results. It has few drawbacks:
|
|
|
|
- drawing a line with alpha will probably not have the intended result if
|
|
the line crosses itself.
|
|
- :attr:`~Line.cap`, :attr:`~Line.joint` and :attr:`~Line.dash` properties
|
|
are not supported.
|
|
- it uses a custom texture with a premultiplied alpha.
|
|
- lines under 1px in width are not supported: they will look the same.
|
|
|
|
.. warning::
|
|
|
|
This is an unfinished work, experimental, and subject to crashes.
|
|
|
|
.. versionadded:: 1.9.0
|
|
'''
|
|
|
|
cdef float _owidth
|
|
|
|
def __init__(self, **kwargs):
|
|
Line.__init__(self, **kwargs)
|
|
self._owidth = kwargs.get("overdraw_width") or <float>1.2
|
|
self.batch.set_mode("triangles")
|
|
self.texture = self.premultiplied_texture()
|
|
|
|
def premultiplied_texture(self):
|
|
texture = Cache.get('kv.graphics.texture', 'smoothline')
|
|
if not texture:
|
|
texture = Texture.create(size=(4, 1), colorfmt="rgba")
|
|
texture.add_reload_observer(self._smooth_reload_observer)
|
|
self._smooth_reload_observer(texture)
|
|
Cache.append('kv.graphics.texture', 'smoothline', texture)
|
|
return texture
|
|
|
|
cpdef _smooth_reload_observer(self, texture):
|
|
cdef bytes GRADIENT_DATA = (
|
|
b"\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00")
|
|
texture.blit_buffer(GRADIENT_DATA, colorfmt="rgba")
|
|
|
|
cdef void build(self):
|
|
if self._mode == LINE_MODE_ELLIPSE:
|
|
self.prebuild_ellipse()
|
|
elif self._mode == LINE_MODE_CIRCLE:
|
|
self.prebuild_circle()
|
|
elif self._mode == LINE_MODE_RECTANGLE:
|
|
self.prebuild_rectangle()
|
|
elif self._mode == LINE_MODE_ROUNDED_RECTANGLE:
|
|
self.prebuild_rounded_rectangle()
|
|
elif self._mode == LINE_MODE_BEZIER:
|
|
self.prebuild_bezier()
|
|
|
|
self.build_smooth()
|
|
|
|
cdef int apply(self) except -1:
|
|
VertexInstruction.apply(self)
|
|
return 0
|
|
|
|
# FIXME: Some artifacts can be observed, depending on the line width,
|
|
# overdraw_width and radius. This occurs due to the way the vertices are
|
|
# built. It is not noticeable when the alpha value of the color of the
|
|
# active context is equal to 1. One solution would be to avoid overlapping
|
|
# vertices.
|
|
cdef void build_smooth(self):
|
|
cdef:
|
|
list p = self.points
|
|
int must_close_line = self._close
|
|
double width = max(0, (self._width - 1.))
|
|
double owidth = width + self._owidth
|
|
vertex_t *vertices = NULL
|
|
unsigned short *indices = NULL
|
|
unsigned short *tindices = NULL
|
|
float min_distance_threshold = 0.1
|
|
double min_angle_threshold = 0.017453292519943295 # 1 degree in radians, determined empirically.
|
|
double ax, ay, bx = 0., by = 0., rx = 0., ry = 0., last_angle = 0., angle, av_angle, ad_angle, angle_diff
|
|
double cos1, sin1, cos2, sin2, ocos1, ocos2, osin1, osin2
|
|
long index, icount, iv, ii, max_vindex, count
|
|
unsigned short i0, i1, i2, i3, i4, i5, i6, i7, vindex, vcount
|
|
|
|
|
|
# Points that are very close (with a distance less than 0.1) will
|
|
# be discarded. This increases the reliability of line rendering.
|
|
p = self._remove_too_nearby_points(p, min_distance_threshold)
|
|
|
|
# If it is just a line segment, there will be no support to close the line.
|
|
if len(p) <= 4:
|
|
must_close_line = False
|
|
|
|
# A new point needs to meet a minimum distance threshold before being added.
|
|
if must_close_line and not (abs(p[-2] - p[0]) < min_distance_threshold and abs(p[-1] - p[1]) < min_distance_threshold):
|
|
p = p + p[:2]
|
|
|
|
iv = vindex = 0
|
|
count = <long>int(len(p) / 2.)
|
|
if count < 2:
|
|
self.batch.clear_data()
|
|
return
|
|
|
|
vcount = <unsigned short>(count * 4)
|
|
icount = (count - 1) * 18
|
|
|
|
vertices = <vertex_t *>malloc(vcount * sizeof(vertex_t))
|
|
if vertices == NULL:
|
|
raise MemoryError("vertices")
|
|
|
|
indices = <unsigned short *>malloc(icount * sizeof(unsigned short))
|
|
if indices == NULL:
|
|
free(vertices)
|
|
raise MemoryError("indices")
|
|
|
|
if must_close_line and self._close_mode == 'straight-line':
|
|
ax = p[-4]
|
|
ay = p[-3]
|
|
bx = p[0]
|
|
by = p[1]
|
|
rx = bx - ax
|
|
ry = by - ay
|
|
last_angle = atan2(ry, rx)
|
|
|
|
max_index = len(p)
|
|
for index in range(0, max_index, 2):
|
|
ax = p[index]
|
|
ay = p[index + 1]
|
|
|
|
if index < max_index - 2:
|
|
bx = p[index + 2]
|
|
by = p[index + 3]
|
|
rx = bx - ax
|
|
ry = by - ay
|
|
angle = atan2(ry, rx)
|
|
|
|
elif must_close_line and index == max_index - 2:
|
|
ax = p[0]
|
|
ay = p[1]
|
|
bx = p[2]
|
|
by = p[3]
|
|
rx = bx - ax
|
|
ry = by - ay
|
|
angle = atan2(ry, rx)
|
|
|
|
else:
|
|
angle = last_angle
|
|
|
|
if index == 0 and (not must_close_line or self._close_mode != 'straight-line'):
|
|
av_angle = angle
|
|
ad_angle = pi
|
|
else:
|
|
av_angle = atan2(
|
|
sin(angle) + sin(last_angle),
|
|
cos(angle) + cos(last_angle))
|
|
|
|
ad_angle = abs(pi - abs(angle - last_angle))
|
|
|
|
a1 = av_angle - PI2
|
|
a2 = av_angle + PI2
|
|
|
|
|
|
if (index == 0 or index >= max_index - 2) and (not must_close_line or self._close_mode != 'straight-line'):
|
|
l = width
|
|
ol = owidth
|
|
else:
|
|
if index == 0:
|
|
ox = p[- 4]
|
|
oy = p[- 3]
|
|
else:
|
|
ox = p[index - 2]
|
|
oy = p[index - 1]
|
|
|
|
la1 = last_angle - PI2
|
|
la2 = angle - PI2
|
|
ra1 = last_angle + PI2
|
|
ra2 = angle + PI2
|
|
|
|
|
|
angle_diff = self._get_angle_diff(
|
|
ox + cos(ra1) * owidth,
|
|
oy + sin(ra1) * owidth,
|
|
ax + cos(ra1) * owidth,
|
|
ay + sin(ra1) * owidth,
|
|
ax + cos(ra2) * owidth,
|
|
ay + sin(ra2) * owidth,
|
|
bx + cos(ra2) * owidth,
|
|
by + sin(ra2) * owidth,
|
|
)
|
|
|
|
# If the angle difference is too small it is not safe to use
|
|
# the calculated values of l or l. Otherwise, l and ol will
|
|
# have extremely high values, causing the line to become
|
|
# excessively thick at some intersections (in specific cases).
|
|
if angle_diff < min_angle_threshold or ad_angle < min_angle_threshold:
|
|
l = width
|
|
ol = owidth
|
|
else:
|
|
if line_intersection(
|
|
ox + cos(la1) * width,
|
|
oy + sin(la1) * width,
|
|
ax + cos(la1) * width,
|
|
ay + sin(la1) * width,
|
|
ax + cos(la2) * width,
|
|
ay + sin(la2) * width,
|
|
bx + cos(la2) * width,
|
|
by + sin(la2) * width,
|
|
&rx, &ry) == 0:
|
|
# print('ERROR LINE INTERSECTION 1')
|
|
pass
|
|
|
|
l = <float>sqrt((ax - rx) ** 2 + (ay - ry) ** 2)
|
|
|
|
if line_intersection(
|
|
ox + cos(ra1) * owidth,
|
|
oy + sin(ra1) * owidth,
|
|
ax + cos(ra1) * owidth,
|
|
ay + sin(ra1) * owidth,
|
|
ax + cos(ra2) * owidth,
|
|
ay + sin(ra2) * owidth,
|
|
bx + cos(ra2) * owidth,
|
|
by + sin(ra2) * owidth,
|
|
&rx, &ry) == 0:
|
|
# print('ERROR LINE INTERSECTION 2')
|
|
pass
|
|
|
|
ol = <float>sqrt((ax - rx) ** 2 + (ay - ry) ** 2)
|
|
|
|
|
|
last_angle = angle
|
|
|
|
cos1 = cos(a1) * l
|
|
sin1 = sin(a1) * l
|
|
cos2 = cos(a2) * l
|
|
sin2 = sin(a2) * l
|
|
|
|
ocos1 = cos(a1) * ol
|
|
osin1 = sin(a1) * ol
|
|
ocos2 = cos(a2) * ol
|
|
osin2 = sin(a2) * ol
|
|
|
|
x1 = ax + cos1
|
|
y1 = ay + sin1
|
|
x2 = ax + cos2
|
|
y2 = ay + sin2
|
|
|
|
ox1 = ax + ocos1
|
|
oy1 = ay + osin1
|
|
ox2 = ax + ocos2
|
|
oy2 = ay + osin2
|
|
|
|
vertices[iv].x = <float>x1
|
|
vertices[iv].y = <float>y1
|
|
vertices[iv].s0 = 0.5
|
|
vertices[iv].t0 = 0.25
|
|
iv += 1
|
|
vertices[iv].x = <float>x2
|
|
vertices[iv].y = <float>y2
|
|
vertices[iv].s0 = 0.5
|
|
vertices[iv].t0 = 0.75
|
|
iv += 1
|
|
vertices[iv].x = <float>ox1
|
|
vertices[iv].y = <float>oy1
|
|
vertices[iv].s0 = 1
|
|
vertices[iv].t0 = 0
|
|
iv += 1
|
|
vertices[iv].x = <float>ox2
|
|
vertices[iv].y = <float>oy2
|
|
vertices[iv].s0 = 1
|
|
vertices[iv].t0 = 1
|
|
iv += 1
|
|
|
|
tindices = indices
|
|
for vindex in range(0, vcount - 4, 4):
|
|
tindices[0] = vindex
|
|
tindices[1] = vindex + 2
|
|
tindices[2] = vindex + 6
|
|
tindices[3] = vindex
|
|
tindices[4] = vindex + 6
|
|
tindices[5] = vindex + 4
|
|
tindices[6] = vindex + 1
|
|
tindices[7] = vindex
|
|
tindices[8] = vindex + 4
|
|
tindices[9] = vindex + 1
|
|
tindices[10] = vindex + 4
|
|
tindices[11] = vindex + 5
|
|
tindices[12] = vindex + 3
|
|
tindices[13] = vindex + 1
|
|
tindices[14] = vindex + 5
|
|
tindices[15] = vindex + 3
|
|
tindices[16] = vindex + 5
|
|
tindices[17] = vindex + 7
|
|
tindices = tindices + 18
|
|
|
|
# print('tindices', <long>tindices, <long>indices, (<long>tindices - <long>indices) / sizeof(unsigned short))
|
|
|
|
self.batch.set_data(vertices, <int>vcount, indices, <int>icount)
|
|
|
|
free(vertices)
|
|
free(indices)
|
|
|
|
cdef list _remove_too_nearby_points(self, list p, float min_distance_threshold):
|
|
cdef int index = 0
|
|
cdef double x1, y1, x2, y2
|
|
|
|
while index < len(p) - 2:
|
|
x1, y1 = p[index], p[index + 1]
|
|
x2, y2 = p[index + 2], p[index + 3]
|
|
if abs(x2 - x1) < min_distance_threshold and abs(y2 - y1) < min_distance_threshold:
|
|
del p[index + 2: index + 4]
|
|
else:
|
|
index += 2
|
|
return p
|
|
|
|
cdef double _get_angle_diff(self, double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4):
|
|
cdef list vector_1, vector_2
|
|
|
|
vector_1 = [x2 - x1, y2 - y1]
|
|
vector_2 = [x4 - x3, y4 - y3]
|
|
|
|
angle_1 = atan2(vector_1[1], vector_1[0])
|
|
angle_2 = atan2(vector_2[1], vector_2[0])
|
|
|
|
angle_diff = abs(angle_1 - angle_2)
|
|
|
|
if angle_diff > pi:
|
|
angle_diff = 2 * pi - angle_diff
|
|
|
|
return angle_diff
|
|
|
|
|
|
property overdraw_width:
|
|
'''Determine the overdraw width of the line, defaults to 1.2.
|
|
'''
|
|
def __get__(self):
|
|
return self._owidth
|
|
|
|
def __set__(self, value):
|
|
if value <= 0:
|
|
raise GraphicException('Invalid width value, must be > 0')
|
|
self._owidth = value
|
|
self.flag_data_update()
|