# -*- coding: utf-8 -*- """ scenegraph.element._path Low level path utility functions suitable for optimizations based on typing. """ # imports #################################################################### from math import hypot, sqrt, pi, cos, sin, atan2, radians # constants ################################################################## INF = float("inf") # geometry ################################################################### def _line(p0, p1): """equation of (p0, p1) in the ax+by+c=0 form.""" (x0, y0), (x1, y1) = p0, p1 dx, dy = x1 - x0, y1 - y0 return dy, -dx, y0 * dx - x0 * dy def _intersection(l0, l1, e=1e-6): """intersection of lines.""" a0, b0, c0 = l0 a1, b1, c1 = l1 w = a0 * b1 - a1 * b0 x = b0 * c1 - b1 * c0 y = c0 * a1 - c1 * a0 if abs(w) < e: raise ZeroDivisionError return x / w, y / w def _parallel(l, p): """parallel to l passing by p.""" a, b, c = l # @UnusedVariable x, y = p return a, b, -(a * x + b * y) def _h(p0, p1): """distance between two points.""" (x0, y0), (x1, y1) = p0, p1 return hypot(x1 - x0, y1 - y0) def _lerp(p0, p1, t=.5): (x0, y0), (x1, y1) = p0, p1 return x0 + t * (x1 - x0), y0 + t * (y1 - y0) # flattening ################################################################# # Bézier splines _L2_RATIO = 4 # trade-off precision for polygons def _casteljau(p0, p1, p2, p3, t=.5): """de Casteljau subdivision of cubic Bézier curve.""" p01, p12, p23 = _lerp(p0, p1, t), _lerp(p1, p2, t), _lerp(p2, p3, t) p012, p123 = _lerp(p01, p12, t), _lerp(p12, p23, t) p0123 = _lerp(p012, p123, t) return p01, p12, p23, p012, p123, p0123 def _cubic(p0, p1, p2, p3, du2): """cubic Bézier spline flattenization.""" if (p0, p2) == (p1, p3): return [p3] (x0, y0), (x1, y1), (x2, y2), (x3, y3) = p0, p1, p2, p3 d1 = (x3 - x0) * (y1 - y0) - (y3 - y0) * (x1 - x0) d2 = (x3 - x0) * (y2 - y0) - (y3 - y0) * (x2 - x0) dd03 = (x3 - x0) * (x3 - x0) + (y3 - y0) * (y3 - y0) if (d1 * d1 + d2 * d2) * du2 < dd03 * _L2_RATIO: return [_lerp(p1, p2), p3] else: p01, p12, p23, p012, p123, p0123 = _casteljau(p0, p1, p2, p3) # @UnusedVariable return _cubic(p0, p01, p012, p0123, du2) + \ _cubic(p0123, p123, p23, p3, du2) def _quadric(p0, p1, p2, du2): """quadric Bézier spline flattenization by transforming it to cubic.""" return _cubic(p0, _lerp(p0, p1, 2 / 3.), _lerp(p1, p2, 1 / 3.), p2, du2) # arc def _arc(p0, rs, phi, flags, p1, du2): """arc flatenization. implementation derived from """ if p0 == p1: return [] rx, ry = rs if rx == 0 or ry == 0: return [p1] rx, ry = abs(rx), abs(ry) phi = radians(phi) % pi c, s = cos(phi), sin(phi) large_arc, sweep = map(bool, flags) (x0, y0), (x1, y1) = p0, p1 ux, uy = .5 * (x0 - x1), .5 * (y0 - y1) X, Y = c * ux + s * uy, -s * ux + c * uy X2, Y2, r2x, r2y = X * X, Y * Y, rx * rx, ry * ry L2 = X2 / r2x + Y2 / r2y if L2 > 1.: L = sqrt(L2) rx, ry = L * rx, L * ry r2x, r2y = L2 * r2x, L2 * r2y K = sqrt(max(0., (r2x * r2y - r2x * Y2 - r2y * X2) / (r2x * Y2 + r2y * X2))) if large_arc == sweep: K = -K Xc, Yc = K * Y * rx / ry, -K * X * ry / rx a0 = atan2(-(Yc - Y) / ry, -(Xc - X) / rx) da = atan2(-(Yc + Y) / ry, -(Xc + X) / rx) - a0 if sweep: if da < 0: da += 2 * pi else: if da > 0: da -= 2 * pi path = [] xc, yc = c * Xc - s * Yc + ux + x1, s * Xc + c * Yc + uy + y1 N = int((((r2x + r2y) * du2) ** .25) * abs(da)) for i in range(N - 1): a = a0 + da * (i + 1) / N X, Y = rx * cos(a), ry * sin(a) path.append((c * X - s * Y + xc, s * X + c * Y + yc)) path.append(p1) # i in range(N) introduce numerical errors for p1 return path # stroking ################################################################### # caps def _offset(p0, p1, hw): if p0 == p1: return 0., hw (x0, y0), (x1, y1) = p0, p1 dx, dy = x1 - x0, y1 - y0 w = hw / hypot(dx, dy) return dy * w, -dx * w def _caps_butt(p0, p1, hw, du=1, start=True): """compute butt cap of width 2*hw for [p0,p1].""" aw, bw = _offset(p0, p1, hw) if start: x, y = p0 else: x, y = p1 return [(x + aw, y + bw), (x - aw, y - bw)] def _caps_square(p0, p1, hw, du=1, start=True): """compute square cap of width 2*hw for [p0,p1].""" aw, bw = _offset(p0, p1, hw) if start: x, y = p0 return [(x + aw + bw, y + bw - aw), (x - aw + bw, y - bw - aw), (x + aw, y + bw), (x - aw, y - bw)] else: x, y = p1 return [(x + aw, y + bw), (x - aw, y - bw), (x + aw - bw, y + bw + aw), (x - aw - bw, y - bw + aw)] def _caps_round(p0, p1, hw, du=1, start=True): """compute round cap of width 2*hw for [p0,p1].""" aw, bw = _offset(p0, p1, hw) n = int(sqrt(hw * du)) + 1 # 1/(du*hw) ~ 1 - cos(da/2) ~ daˆ2/8 at first order da = pi / (2 * n + 1) if start: x, y = p0 n0 = n else: x, y = p1 n0 = 0 r = [] for i in range(n + 1): a = (n0 - i) * da c, s = cos(a), sin(a) r += [(x + c * aw + s * bw, y + c * bw - s * aw), (x - c * aw + s * bw, y - c * bw - s * aw)] return r # join def _join_miter(p0, p1, p2, hw, du, miterlimit): p0a, p0b, p1a, p1b = _join_bevel(p0, p1, p2, hw, du, miterlimit) l0, l1 = _line(p0, p1), _line(p1, p2) try: pa = _intersection(_parallel(l0, p0a), _parallel(l1, p1a)) pb = _intersection(_parallel(l0, p0b), _parallel(l1, p1b)) except ZeroDivisionError: return [p1a, p1b] r = miterlimit * hw / _h(p1a, pa) if r < 1.: return [_lerp(p0a, pa, r), _lerp(p0b, pb, r), _lerp(p1a, pa, r), _lerp(p1b, pb, r)] # return [p0a, p0b, p1a, p1b] else: return [pa, pb] def _join_bevel(p0, p1, p2, hw, du, miterlimit): return _caps_butt(p0, p1, hw, start=False) + \ _caps_butt(p1, p2, hw) def _join_round(p0, p1, p2, hw, du, miterlimit): return _caps_butt(p0, p1, hw, du, start=False) + \ _caps_round(p1, p2, hw, du) # stroke _caps = { 'butt': _caps_butt, 'square': _caps_square, 'round': _caps_round, } _joins = { 'miter': _join_miter, 'bevel': _join_bevel, 'round': _join_round, } def _enumerate_unique(path): previous = None for i, p in enumerate(path): if p != previous: yield i, p previous = p def _stroke(path, closed, joins, width, du=1., cap='butt', join='miter', miterlimit=4.): """compute a stroke from discretized path.""" hw = width / 2. _cap = _caps[cap] _join = _joins[join] stroke = [] path_points = _enumerate_unique(path) (i0, p0) = next(path_points) (i1, p1) = next(path_points, (i0, p0)) p0i, p1i = p0, p1 join_indices = iter(joins) next_join = next(join_indices) while next_join < i1: next_join = next(join_indices) for i2, p2 in path_points: if i1 == next_join: j = _join(p0, p1, p2, hw, du, miterlimit) next_join = next(join_indices, 0) else: j = _join_miter(p0, p1, p2, hw, du, 1.) stroke += j i1 = i2 p0, p1 = p1, p2 if closed: b = e = _join(p0, p1, p1i, hw, du, miterlimit) else: b = _cap(p0i, p1i, hw, du) e = _cap(p0, p1, hw, du, start=False) return b + stroke + e # filling #################################################################### def _triangle_strip_hits(strip, x, y): """yields hits and signs in triangles stored as strip.""" strip_iter = iter(strip) p0, p1 = next(strip_iter), next(strip_iter) a0, b0, c0 = _line(p0, p1) s0, s = a0 * x + b0 * y + c0, 1 for p2 in strip_iter: a1, b1, c1 = _line(p1, p2) s1 = a1 * x + b1 * y + c1 a2, b2, c2 = _line(p2, p0) s2 = a2 * x + b2 * y + c2 yield (s0 * s1 > 0) and (s1 * s2 > 0), s0 * s > 0 p0, p1 = p1, p2 s0, s = s1, -s def _evenodd_hit(x, y, fills): """even/odd hit test on interior of a path.""" # print( '_evenodd_hit : coucou') in_count = 0 for hit, _ in _triangle_strip_hits(fills, x, y): if hit: in_count += 1 # print( '_evenodd_hit : in_count=%d' % in_count ) return (in_count % 2) == 1 def _nonzero_hit(x, y, fills): """non-zero hit test on interior of a path.""" # print( '_nonzero_hit : coucou') in_count = 0 for hit, positive in _triangle_strip_hits(fills, x, y): if hit: if positive: in_count += 1 # print( '_nonzero_hit : positive hit : in_count=%d' % in_count ) else: in_count -= 1 # print( '_nonzero_hit : negative hit : in_count=%d' % in_count ) # print( '_nonzero_hit : in_count=%d' % in_count ) return in_count != 0 def _stroke_hit(x, y, strokes): """hit test on stroke of a path.""" for hit, _ in _triangle_strip_hits(strokes, x, y): if hit: return True return False def _bbox(paths): """bounding box of a path.""" x_min = y_min = +INF x_max = y_max = -INF for path in paths: xs, ys = zip(*path) x_min, x_max = min(x_min, min(xs)), max(x_max, max(xs)) y_min, y_max = min(y_min, min(ys)), max(y_max, max(ys)) return (x_min, y_min), (x_max, y_max)