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

362 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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