[qu2cu] Produce optimal mix of cubic/quadratic splines
Yay. Finally!
This commit is contained in:
parent
8427e6dd18
commit
f1086ddb65
@ -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
|
||||||
|
@ -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()",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
[
|
[
|
||||||
|
Loading…
x
Reference in New Issue
Block a user