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 = 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(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 = 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 = 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 = malloc(count * sizeof(vertex_t)) if vertices == NULL: raise MemoryError('vertices') indices = 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 += (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, count, indices, count) free(vertices) free(indices) cdef void build_extended(self): cdef int i, j cdef long count = 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 = malloc(vertices_count * sizeof(vertex_t)) if vertices == NULL: raise MemoryError('vertices') indices = 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 = x1 vertices[iv].y = y1 vertices[iv].s0 = 0 vertices[iv].t0 = 0 iv += 1 vertices[iv].x = x2 vertices[iv].y = y2 vertices[iv].s0 = 1 vertices[iv].t0 = 0 iv += 1 vertices[iv].x = x3 vertices[iv].y = y3 vertices[iv].s0 = 1 vertices[iv].t0 = 1 iv += 1 vertices[iv].x = x4 vertices[iv].y = 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 = ax vertices[iv].y = 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 = ax vertices[iv].y = 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 = ix vertices[iv + 1].y = 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 = ix vertices[iv + 1].y = 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 = ax vertices[iv].y = ay vertices[iv].s0 = 0 vertices[iv].t0 = 0 iv += 1 for j in xrange(0, self._joint_precision - 1): vertices[iv].x = (ax - cos(a0 - step * j) * w) vertices[iv].y = (ay - sin(a0 - step * j) * w) vertices[iv].s0 = 0 vertices[iv].t0 = 0 if j == 0: indices[ii] = siv indices[ii + 1] = 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] = pivend ii += 3 # caps if cap == LINE_CAP_SQUARE: vertices[iv].x = (x2 + cos(angle) * w) vertices[iv].y = (y2 + sin(angle) * w) vertices[iv].s0 = 0 vertices[iv].t0 = 0 vertices[iv + 1].x = (x3 + cos(angle) * w) vertices[iv + 1].y = (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 = (sx1 - cos(sangle) * w) vertices[iv].y = (sy1 - sin(sangle) * w) vertices[iv].s0 = 0 vertices[iv].t0 = 0 vertices[iv + 1].x = (sx4 - cos(sangle) * w) vertices[iv + 1].y = (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 = cx vertices[iv].y = cy vertices[iv].s0 = 0 vertices[iv].t0 = 0 iv += 1 for i in xrange(0, self._cap_precision - 1): vertices[iv].x = (cx + cos(a1 + step * i) * w) vertices[iv].y = (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 = cx vertices[iv].y = cy vertices[iv].s0 = 0 vertices[iv].t0 = 0 iv += 1 for i in xrange(0, self._cap_precision - 1): vertices[iv].x = (cx + cos(a1 + step * i) * w) vertices[iv].y = (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, vertices_count, indices, 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 = 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 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 = int(len(p) / 2.) if count < 2: self.batch.clear_data() return vcount = (count * 4) icount = (count - 1) * 18 vertices = malloc(vcount * sizeof(vertex_t)) if vertices == NULL: raise MemoryError("vertices") indices = 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 = 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 = 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 = x1 vertices[iv].y = y1 vertices[iv].s0 = 0.5 vertices[iv].t0 = 0.25 iv += 1 vertices[iv].x = x2 vertices[iv].y = y2 vertices[iv].s0 = 0.5 vertices[iv].t0 = 0.75 iv += 1 vertices[iv].x = ox1 vertices[iv].y = oy1 vertices[iv].s0 = 1 vertices[iv].t0 = 0 iv += 1 vertices[iv].x = ox2 vertices[iv].y = 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', tindices, indices, (tindices - indices) / sizeof(unsigned short)) self.batch.set_data(vertices, vcount, indices, 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()