summaryrefslogtreecommitdiff
path: root/third_party/spiro/curves/tocubic.py
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/spiro/curves/tocubic.py')
-rw-r--r--third_party/spiro/curves/tocubic.py461
1 files changed, 461 insertions, 0 deletions
diff --git a/third_party/spiro/curves/tocubic.py b/third_party/spiro/curves/tocubic.py
new file mode 100644
index 0000000..d70b9a2
--- /dev/null
+++ b/third_party/spiro/curves/tocubic.py
@@ -0,0 +1,461 @@
+# Some code to convert arbitrary curves to high quality cubics.
+
+# Some conventions: points are (x, y) pairs. Cubic Bezier segments are
+# lists of four points.
+
+import sys
+
+from math import *
+
+import pcorn
+
+def pt_wsum(points, wts):
+ x, y = 0, 0
+ for i in range(len(points)):
+ x += points[i][0] * wts[i]
+ y += points[i][1] * wts[i]
+ return x, y
+
+# Very basic spline primitives
+def bz_eval(bz, t):
+ degree = len(bz) - 1
+ mt = 1 - t
+ if degree == 3:
+ return pt_wsum(bz, [mt * mt * mt, 3 * mt * mt * t, 3 * mt * t * t, t * t * t])
+ elif degree == 2:
+ return pt_wsum(bz, [mt * mt, 2 * mt * t, t * t])
+ elif degree == 1:
+ return pt_wsum(bz, [mt, t])
+
+def bz_deriv(bz):
+ degree = len(bz) - 1
+ return [(degree * (bz[i + 1][0] - bz[i][0]), degree * (bz[i + 1][1] - bz[i][1])) for i in range(degree)]
+
+def bz_arclength(bz, n = 10):
+ # We're just going to integrate |z'| over the parameter [0..1].
+ # The integration algorithm here is eqn 4.1.14 from NRC2, and is
+ # chosen for simplicity. Likely adaptive and/or higher-order
+ # algorithms would be better, but this should be good enough.
+ # Convergence should be quartic in n.
+ wtarr = (3./8, 7./6, 23./24)
+ dt = 1./n
+ s = 0
+ dbz = bz_deriv(bz)
+ for i in range(0, n + 1):
+ if i < 3:
+ wt = wtarr[i]
+ elif i > n - 3:
+ wt = wtarr[n - i]
+ else:
+ wt = 1.
+ dx, dy = bz_eval(dbz, i * dt)
+ ds = hypot(dx, dy)
+ s += wt * ds
+ return s * dt
+
+# One step of 4th-order Runge-Kutta numerical integration - update y in place
+def rk4(y, dydx, x, h, derivs):
+ hh = h * .5
+ h6 = h * (1./6)
+ xh = x + hh
+ yt = []
+ for i in range(len(y)):
+ yt.append(y[i] + hh * dydx[i])
+ dyt = derivs(xh, yt)
+ for i in range(len(y)):
+ yt[i] = y[i] + hh * dyt[i]
+ dym = derivs(xh, yt)
+ for i in range(len(y)):
+ yt[i] = y[i] + h * dym[i]
+ dym[i] += dyt[i]
+ dyt = derivs(x + h, yt)
+ for i in range(len(y)):
+ y[i] += h6 * (dydx[i] + dyt[i] + 2 * dym[i])
+
+def bz_arclength_rk4(bz, n = 10):
+ dbz = bz_deriv(bz)
+ def arclength_deriv(x, ys):
+ dx, dy = bz_eval(dbz, x)
+ return [hypot(dx, dy)]
+ dt = 1./n
+ t = 0
+ ys = [0]
+ for i in range(n):
+ dydx = arclength_deriv(t, ys)
+ rk4(ys, dydx, t, dt, arclength_deriv)
+ t += dt
+ return ys[0]
+
+# z0 and z1 are start and end points, resp.
+# th0 and th1 are the initial and final tangents, measured in the
+# direction of the curve.
+# aab is a/(a + b), where a and b are the lengths of the bezier "arms"
+def fit_cubic_arclen(z0, z1, arclen, th0, th1, aab):
+ chord = hypot(z1[0] - z0[0], z1[1] - z0[1])
+ cth0, sth0 = cos(th0), sin(th0)
+ cth1, sth1 = -cos(th1), -sin(th1)
+ armlen = .66667 * chord
+ darmlen = 1e-6 * armlen
+ for i in range(10):
+ a = armlen * aab
+ b = armlen - a
+ bz = [z0, (z0[0] + cth0 * a, z0[1] + sth0 * a),
+ (z1[0] + cth1 * b, z1[1] + sth1 * b), z1]
+ actual_s = bz_arclength_rk4(bz)
+ if (abs(arclen - actual_s) < 1e-12):
+ break
+ a = (armlen + darmlen) * aab
+ b = (armlen + darmlen) - a
+ bz = [z0, (z0[0] + cth0 * a, z0[1] + sth0 * a),
+ (z1[0] + cth1 * b, z1[1] + sth1 * b), z1]
+ actual_s2 = bz_arclength_rk4(bz)
+ ds = (actual_s2 - actual_s) / darmlen
+ #print '% armlen = ', armlen
+ if ds == 0:
+ break
+ armlen += (arclen - actual_s) / ds
+ a = armlen * aab
+ b = armlen - a
+ bz = [z0, (z0[0] + cth0 * a, z0[1] + sth0 * a),
+ (z1[0] + cth1 * b, z1[1] + sth1 * b), z1]
+ return bz
+
+def mod_2pi(th):
+ u = th / (2 * pi)
+ return 2 * pi * (u - floor(u + 0.5))
+
+def measure_bz(bz, arclen, th_fn, n = 1000):
+ dt = 1./n
+ dbz = bz_deriv(bz)
+ s = 0
+ score = 0
+ for i in range(n):
+ dx, dy = bz_eval(dbz, (i + .5) * dt)
+ ds = dt * hypot(dx, dy)
+ s += ds
+ score += ds * (mod_2pi(atan2(dy, dx) - th_fn(s)) ** 2)
+ return score
+
+def measure_bz_rk4(bz, arclen, th_fn, n = 10):
+ dbz = bz_deriv(bz)
+ def measure_derivs(x, ys):
+ dx, dy = bz_eval(dbz, x)
+ ds = hypot(dx, dy)
+ s = ys[0]
+ dscore = ds * (mod_2pi(atan2(dy, dx) - th_fn(s)) ** 2)
+ return [ds, dscore]
+ dt = 1./n
+ t = 0
+ ys = [0, 0]
+ for i in range(n):
+ dydx = measure_derivs(t, ys)
+ rk4(ys, dydx, t, dt, measure_derivs)
+ t += dt
+ return ys[1]
+
+# th_fn() is a function that takes an arclength from the start point, and
+# returns an angle - thus th_fn(0) and th_fn(arclen) are the initial and
+# final tangents.
+# z0, z1, and arclen are as fit_cubic_arclen
+def fit_cubic(z0, z1, arclen, th_fn, fast = 1):
+ chord = hypot(z1[0] - z0[0], z1[1] - z0[1])
+ if (arclen < 1.000001 * chord):
+ return [z0, z1], 0
+ th0 = th_fn(0)
+ th1 = th_fn(arclen)
+ imax = 4
+ jmax = 10
+ aabmin = 0
+ aabmax = 1.
+ if fast:
+ imax = 1
+ jmax = 0
+ for i in range(imax):
+ for j in range(jmax + 1):
+ if jmax == 0:
+ aab = 0.5 * (aabmin + aabmax)
+ else:
+ aab = aabmin + (aabmax - aabmin) * j / jmax
+ bz = fit_cubic_arclen(z0, z1, arclen, th0, th1, aab)
+ score = measure_bz_rk4(bz, arclen, th_fn)
+ print '% aab =', aab, 'score =', score
+ sys.stdout.flush()
+ if j == 0 or score < best_score:
+ best_score = score
+ best_aab = aab
+ best_bz = bz
+ daab = .06 * (aabmax - aabmin)
+ aabmin = max(0, best_aab - daab)
+ aabmax = min(1, best_aab + daab)
+ print '%--- best_aab =', best_aab
+ return best_bz, best_score
+
+def plot_prolog():
+ print '%!PS'
+ print '/m { moveto } bind def'
+ print '/l { lineto } bind def'
+ print '/c { curveto } bind def'
+ print '/z { closepath } bind def'
+
+def plot_bz(bz, z0, scale, do_moveto = True):
+ x0, y0 = z0
+ if do_moveto:
+ print bz[0][0] * scale + x0, bz[0][1] * scale + y0, 'm'
+ if len(bz) == 4:
+ x1, y1 = bz[1][0] * scale + x0, bz[1][1] * scale + y0
+ x2, y2 = bz[2][0] * scale + x0, bz[2][1] * scale + y0
+ x3, y3 = bz[3][0] * scale + x0, bz[3][1] * scale + y0
+ print x1, y1, x2, y2, x3, y3, 'c'
+ elif len(bz) == 2:
+ print bz[1][0] * scale + x0, bz[1][1] * scale + y0, 'l'
+
+def test_bz_arclength():
+ bz = [(0, 0), (.5, 0), (1, 0.5), (1, 1)]
+ ans = bz_arclength_rk4(bz, 2048)
+ last = 1
+ lastrk = 1
+ for i in range(3, 11):
+ n = 1 << i
+ err = bz_arclength(bz, n) - ans
+ err_rk = bz_arclength_rk4(bz, n) - ans
+ print n, err, last / err, err_rk, lastrk / err_rk
+ last = err
+ lastrk = err_rk
+
+def test_fit_cubic_arclen():
+ th = pi / 4
+ arclen = th / sin(th)
+ bz = fit_cubic_arclen((0, 0), (1, 0), arclen, th, th, .5)
+ print '%', bz
+ plot_bz(bz, (100, 400), 500)
+ print 'stroke'
+ print 'showpage'
+
+# -- cornu fitting
+
+import cornu
+
+def cornu_to_cubic(t0, t1):
+ def th_fn(s):
+ return (s + t0) ** 2
+ y0, x0 = cornu.eval_cornu(t0)
+ y1, x1 = cornu.eval_cornu(t1)
+ bz, score = fit_cubic((x0, y0), (x1, y1), t1 - t0, th_fn, 0)
+ return bz, score
+
+def test_draw_cornu():
+ plot_prolog()
+ thresh = 1e-6
+ print '/ss 1.5 def'
+ print '/circle { ss 0 moveto currentpoint exch ss sub exch ss 0 360 arc } bind def'
+ s0 = 0
+ imax = 200
+ x0, y0, scale = 36, 100, 500
+ bzs = []
+ for i in range(1, imax):
+ s = sqrt(i * .1)
+ bz, score = cornu_to_cubic(s0, s)
+ if score > (s - s0) * thresh or i == imax - 1:
+ plot_bz(bz, (x0, y0), scale, s0 == 0)
+ bzs.append(bz)
+ s0 = s
+ print 'stroke'
+ for i in range(len(bzs)):
+ bz = bzs[i]
+ bx0, by0 = x0 + bz[0][0] * scale, y0 + bz[0][1] * scale
+ bx1, by1 = x0 + bz[1][0] * scale, y0 + bz[1][1] * scale
+ bx2, by2 = x0 + bz[2][0] * scale, y0 + bz[2][1] * scale
+ bx3, by3 = x0 + bz[3][0] * scale, y0 + bz[3][1] * scale
+ print 'gsave 0 0 1 setrgbcolor .5 setlinewidth'
+ print bx0, by0, 'moveto', bx1, by1, 'lineto stroke'
+ print bx2, by2, 'moveto', bx3, by3, 'lineto stroke'
+ print 'grestore'
+ print 'gsave', bx0, by0, 'translate circle fill grestore'
+ print 'gsave', bx1, by1, 'translate .5 dup scale circle fill grestore'
+ print 'gsave', bx2, by2, 'translate .5 dup scale circle fill grestore'
+ print 'gsave', bx3, by3, 'translate circle fill grestore'
+
+# -- fitting of piecewise cornu curves
+
+def pcorn_segment_to_bzs_optim_inner(curve, s0, s1, thresh, nmax = None):
+ result = []
+ if s0 == s1: return [], 0
+ while s0 < s1:
+ def th_fn_inner(s):
+ if s > s1: s = s1
+ return curve.th(s0 + s, s == 0)
+ z0 = curve.xy(s0)
+ z1 = curve.xy(s1)
+ bz, score = fit_cubic(z0, z1, s1 - s0, th_fn_inner, 0)
+ if score < thresh or nmax != None and len(result) == nmax - 1:
+ result.append(bz)
+ break
+ r = s1
+ l = s0 + .001 * (s1 - s0)
+ for i in range(10):
+ smid = 0.5 * (l + r)
+ zmid = curve.xy(smid)
+ bz, score = fit_cubic(z0, zmid, smid - s0, th_fn_inner, 0)
+ if score > thresh:
+ r = smid
+ else:
+ l = smid
+ print '% s0=', s0, 'smid=', smid, 'actual score =', score
+ result.append(bz)
+ s0 = smid
+ print '% last actual score=', score
+ return result, score
+
+def pcorn_segment_to_bzs_optim(curve, s0, s1, thresh, optim):
+ result, score = pcorn_segment_to_bzs_optim_inner(curve, s0, s1, thresh)
+ bresult, bscore = result, score
+ if len(result) > 1 and optim > 2:
+ nmax = len(result)
+ gamma = 1./6
+ l = score
+ r = thresh
+ for i in range(5):
+ tmid = (0.5 * (l ** gamma + r ** gamma)) ** (1/gamma)
+ result, score = pcorn_segment_to_bzs_optim_inner(curve, s0, s1, tmid, nmax)
+ if score < tmid:
+ l = max(l, score)
+ r = tmid
+ else:
+ l = tmid
+ r = min(r, score)
+ if max(score, tmid) < bscore:
+ bresult, bscore = result, max(score, tmid)
+ return result
+
+def pcorn_segment_to_bzs(curve, s0, s1, optim = 0, thresh = 1e-3):
+ if optim >= 2:
+ return pcorn_segment_to_bzs_optim(curve, s0, s1, thresh, optim)
+ z0 = curve.xy(s0)
+ z1 = curve.xy(s1)
+ fast = (optim == 0)
+ def th_fn(s):
+ return curve.th(s0 + s, s == 0)
+ bz, score = fit_cubic(z0, z1, s1 - s0, th_fn, fast)
+ if score < thresh:
+ return [bz]
+ else:
+ smid = 0.5 * (s0 + s1)
+ result = pcorn_segment_to_bzs(curve, s0, smid, optim, thresh)
+ result.extend(pcorn_segment_to_bzs(curve, smid, s1, optim, thresh))
+ return result
+
+def pcorn_curve_to_bzs(curve, optim = 3, thresh = 1e-3):
+ result = []
+ extrema = curve.find_extrema()
+ extrema.extend(curve.find_breaks())
+ extrema.sort()
+ print '%', extrema
+ for i in range(len(extrema)):
+ s0 = extrema[i]
+ if i == len(extrema) - 1:
+ s1 = extrema[0] + curve.arclen
+ else:
+ s1 = extrema[i + 1]
+ result.extend(pcorn_segment_to_bzs(curve, s0, s1, optim, thresh))
+ return result
+
+import struct
+
+def fit_cubic_arclen_forplot(z0, z1, arclen, th0, th1, aab):
+ chord = hypot(z1[0] - z0[0], z1[1] - z0[1])
+ cth0, sth0 = cos(th0), sin(th0)
+ cth1, sth1 = -cos(th1), -sin(th1)
+ armlen = .66667 * chord
+ darmlen = 1e-6 * armlen
+ for i in range(10):
+ a = armlen * aab
+ b = armlen - a
+ bz = [z0, (z0[0] + cth0 * a, z0[1] + sth0 * a),
+ (z1[0] + cth1 * b, z1[1] + sth1 * b), z1]
+ actual_s = bz_arclength_rk4(bz)
+ if (abs(arclen - actual_s) < 1e-12):
+ break
+ a = (armlen + darmlen) * aab
+ b = (armlen + darmlen) - a
+ bz = [z0, (z0[0] + cth0 * a, z0[1] + sth0 * a),
+ (z1[0] + cth1 * b, z1[1] + sth1 * b), z1]
+ actual_s2 = bz_arclength_rk4(bz)
+ ds = (actual_s2 - actual_s) / darmlen
+ #print '% armlen = ', armlen
+ armlen += (arclen - actual_s) / ds
+ a = armlen * aab
+ b = armlen - a
+ bz = [z0, (z0[0] + cth0 * a, z0[1] + sth0 * a),
+ (z1[0] + cth1 * b, z1[1] + sth1 * b), z1]
+ return bz, a, b
+
+def plot_errors_2d(t0, t1, as_ppm):
+ xs = 1024
+ ys = 1024
+ if as_ppm:
+ print 'P6'
+ print xs, ys
+ print 255
+ def th_fn(s):
+ return (s + t0) ** 2
+ y0, x0 = cornu.eval_cornu(t0)
+ y1, x1 = cornu.eval_cornu(t1)
+ z0 = (x0, y0)
+ z1 = (x1, y1)
+ chord = hypot(y1 - y0, x1 - x0)
+ arclen = t1 - t0
+ th0 = th_fn(0)
+ th1 = th_fn(arclen)
+ cth0, sth0 = cos(th0), sin(th0)
+ cth1, sth1 = -cos(th1), -sin(th1)
+
+ for y in range(ys):
+ b = .8 * chord * (ys - y - 1) / ys
+ for x in range(xs):
+ a = .8 * chord * x / xs
+ bz = [z0, (z0[0] + cth0 * a, z0[1] + sth0 * a),
+ (z1[0] + cth1 * b, z1[1] + sth1 * b), z1]
+ s_bz = bz_arclength(bz, 10)
+ def th_fn_scaled(s):
+ return (s * arclen / s_bz + t0) ** 2
+ score = measure_bz_rk4(bz, arclen, th_fn_scaled, 10)
+ if as_ppm:
+ ls = -log(score)
+ color_th = ls
+ darkstep = 0
+ if s_bz > arclen:
+ g0 = 128 - darkstep
+ else:
+ g0 = 128 + darkstep
+ sc = 127 - darkstep
+ rr = g0 + sc * cos(color_th)
+ gg = g0 + sc * cos(color_th + 2 * pi / 3)
+ bb = g0 + sc * cos(color_th - 2 * pi / 3)
+ sys.stdout.write(struct.pack('3B', rr, gg, bb))
+ else:
+ print a, b, score
+ if not as_ppm:
+ print
+
+def plot_arclen(t0, t1):
+ def th_fn(s):
+ return (s + t0) ** 2
+ y0, x0 = cornu.eval_cornu(t0)
+ y1, x1 = cornu.eval_cornu(t1)
+ z0 = (x0, y0)
+ z1 = (x1, y1)
+ chord = hypot(y1 - y0, x1 - x0)
+ arclen = t1 - t0
+ th0 = th_fn(0)
+ th1 = th_fn(arclen)
+ for i in range(101):
+ aab = i * .01
+ bz, a, b = fit_cubic_arclen_forplot(z0, z1, arclen, th0, th1, aab)
+ print a, b
+
+if __name__ == '__main__':
+ #test_bz_arclength()
+ test_draw_cornu()
+ #run_one_cornu_seg()
+ #plot_errors_2d(.5, 1.0, False)
+ #plot_arclen(.5, 1.0)