msspec_python3/msspec/msspecgui/scenegraph2d/scenegraph/element/_path.py

362 lines
9.6 KiB
Python
Raw Normal View History

2019-11-14 15:16:51 +01:00
# -*- 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
<http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes>
"""
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)