[qu2cu] Produce optimal mix of cubic/quadratic splines

Yay. Finally!
This commit is contained in:
Behdad Esfahbod 2023-02-18 16:21:39 -07:00
parent 8427e6dd18
commit f1086ddb65
3 changed files with 48 additions and 20 deletions

View File

@ -169,11 +169,21 @@ def quadratics_to_curves(pp, tolerance=0.5, all_cubic=False):
pp = [[complex(x, y) for (x, y) in p] for p in pp] pp = [[complex(x, y) for (x, y) in p] for p in pp]
q = [pp[0][0]] q = [pp[0][0]]
cost = 0
costs = [0]
for p in pp: for p in pp:
assert q[-1] == p[0] assert q[-1] == p[0]
q.extend(add_implicit_on_curves(p)[1:]) for i in range(len(p) - 2):
cost += 1
costs.append(cost)
costs.append(cost + 1)
qq = add_implicit_on_curves(p)[1:]
q.extend(qq)
cost += 1
costs.append(cost)
costs.append(cost + 1)
curves = spline_to_curves(q, tolerance, all_cubic) curves = spline_to_curves(q, costs, tolerance, all_cubic)
if not is_complex: if not is_complex:
curves = [tuple((c.real, c.imag) for c in curve) for curve in curves] curves = [tuple((c.real, c.imag) for c in curve) for curve in curves]
@ -185,16 +195,22 @@ def quadratic_to_curves(q, tolerance=0.5, all_cubic=False):
if not is_complex: if not is_complex:
q = [complex(x, y) for (x, y) in q] q = [complex(x, y) for (x, y) in q]
costs = [0]
for i in range(len(q) - 2):
costs.append(i + 1)
costs.append(i + 2)
costs.append(len(q) - 1)
costs.append(len(q))
q = add_implicit_on_curves(q) q = add_implicit_on_curves(q)
curves = spline_to_curves(q, tolerance, all_cubic) curves = spline_to_curves(q, costs, tolerance, all_cubic)
if not is_complex: if not is_complex:
curves = [tuple((c.real, c.imag) for c in curve) for curve in curves] curves = [tuple((c.real, c.imag) for c in curve) for curve in curves]
return curves return curves
def spline_to_curves(q, tolerance=0.5, all_cubic=False): def spline_to_curves(q, costs, tolerance=0.5, all_cubic=False):
assert len(q) >= 3, "quadratic spline requires at least 3 points" assert len(q) >= 3, "quadratic spline requires at least 3 points"
# Elevate quadratic segments to cubic # Elevate quadratic segments to cubic
@ -204,11 +220,21 @@ def spline_to_curves(q, tolerance=0.5, all_cubic=False):
# Dynamic-Programming to find the solution with fewest number of # Dynamic-Programming to find the solution with fewest number of
# cubic curves, and within those the one with smallest error. # cubic curves, and within those the one with smallest error.
sols = [(0, 0, 0)] # (best_num_segments, best_error, start_index) sols = [(0, 0, 0, False)] # (best_num_points, best_error, start_index, cubic)
for i in range(1, len(elevated_quadratics) + 1): for i in range(1, len(elevated_quadratics) + 1):
best_sol = (len(q) + 1, 0, 1) best_sol = (len(q) + 2, 0, 1, False)
for j in range(0, i): for j in range(0, i):
j_sol_count, j_sol_error, _, _ = sols[j]
if not all_cubic:
# Solution with quadratics between j:i
i_sol_count = j_sol_count + costs[2 * i] - costs[2 * j]
i_sol_error = j_sol_error
i_sol = (i_sol_count, i_sol_error, i - j, False)
if i_sol < best_sol:
best_sol = i_sol
# Fit elevated_quadratics[j:i] into one cubic # Fit elevated_quadratics[j:i] into one cubic
try: try:
curve, ts = merge_curves(elevated_quadratics[j:i]) curve, ts = merge_curves(elevated_quadratics[j:i])
@ -245,14 +271,13 @@ def spline_to_curves(q, tolerance=0.5, all_cubic=False):
continue continue
# Save best solution # Save best solution
j_sol_count, j_sol_error, _ = sols[j] i_sol_count = j_sol_count + 3
i_sol_count = j_sol_count + 1
i_sol_error = max(j_sol_error, error) i_sol_error = max(j_sol_error, error)
i_sol = (i_sol_count, i_sol_error, i - j) i_sol = (i_sol_count, i_sol_error, i - j, True)
if i_sol < best_sol: if i_sol < best_sol:
best_sol = i_sol best_sol = i_sol
if i_sol_count == 1: if i_sol_count == 4:
# Can't get any better than this # Can't get any better than this
break break
@ -260,18 +285,21 @@ def spline_to_curves(q, tolerance=0.5, all_cubic=False):
# Reconstruct solution # Reconstruct solution
splits = [] splits = []
cubic = []
i = len(sols) - 1 i = len(sols) - 1
while i: while i:
_, _, count, is_cubic = sols[i]
splits.append(i) splits.append(i)
_, _, count = sols[i] cubic.append(is_cubic)
i -= count i -= count
curves = [] curves = []
j = 0 j = 0
for i in reversed(splits): for i, is_cubic in reversed(list(zip(splits, cubic))):
if not all_cubic and j + 1 == i: if is_cubic:
curves.append(q[j * 2 : j * 2 + 3])
else:
curves.append(merge_curves(elevated_quadratics[j:i])[0]) curves.append(merge_curves(elevated_quadratics[j:i])[0])
else:
for k in range(j, i):
curves.append(q[k * 2 : k * 2 + 3])
j = i j = i
return curves return curves

View File

@ -48,7 +48,7 @@ class _TestPenMixin(object):
# draw source glyph onto a new glyph using a Cu2Qu pen and return it # draw source glyph onto a new glyph using a Cu2Qu pen and return it
converted = self.Glyph() converted = self.Glyph()
pen = getattr(converted, self.pen_getter_name)() pen = getattr(converted, self.pen_getter_name)()
cubicpen = self.Qu2CuPen(pen, MAX_ERR, **kwargs) cubicpen = self.Qu2CuPen(pen, MAX_ERR, all_cubic=True, **kwargs)
getattr(glyph, self.draw_method_name)(cubicpen) getattr(glyph, self.draw_method_name)(cubicpen)
return converted return converted
@ -144,7 +144,7 @@ class TestQu2CuPen(unittest.TestCase, _TestPenMixin):
], ],
) )
def test_qCurveTo_3_points(self): def test_qCurveTo_3_points_no_conversion(self):
pen = DummyPen() pen = DummyPen()
cubicpen = Qu2CuPen(pen, MAX_ERR) cubicpen = Qu2CuPen(pen, MAX_ERR)
cubicpen.moveTo((0, 0)) cubicpen.moveTo((0, 0))
@ -155,7 +155,7 @@ class TestQu2CuPen(unittest.TestCase, _TestPenMixin):
str(pen).splitlines(), str(pen).splitlines(),
[ [
"pen.moveTo((0, 0))", "pen.moveTo((0, 0))",
"pen.curveTo((0, 4.0), (1, 4.0), (1, 0))", "pen.qCurveTo((0, 3), (1, 3), (1, 0))",
"pen.closePath()", "pen.closePath()",
], ],
) )

View File

@ -36,7 +36,7 @@ class Qu2CuTest:
((0, 0), (0, 4 / 3), (2, 4 / 3), (2, 0)), ((0, 0), (0, 4 / 3), (2, 4 / 3), (2, 0)),
], ],
0.1, 0.1,
False, True,
), ),
( (
[ [
@ -46,7 +46,7 @@ class Qu2CuTest:
((0, 0), (0, 4 / 3), (2, 2 / 3), (2, 2)), ((0, 0), (0, 4 / 3), (2, 2 / 3), (2, 2)),
], ],
0.2, 0.2,
False, True,
), ),
( (
[ [