From 90a84d25ca2f70d395976075df114c09de46880b Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Tue, 5 Dec 2023 15:09:14 -0700 Subject: [PATCH 1/7] [interpolatableTestStartingPoint] Try to rationalize the extended case Breaks all kinds of things. Going to revert. I have no idea why the existing code works so well but any touching it to make it more reasonable regresses the results :(. --- Lib/fontTools/varLib/interpolatable.py | 23 +++++- .../varLib/interpolatableTestStartingPoint.py | 78 ++++--------------- 2 files changed, 33 insertions(+), 68 deletions(-) diff --git a/Lib/fontTools/varLib/interpolatable.py b/Lib/fontTools/varLib/interpolatable.py index ae2de2a19..81675052a 100644 --- a/Lib/fontTools/varLib/interpolatable.py +++ b/Lib/fontTools/varLib/interpolatable.py @@ -42,7 +42,9 @@ class Glyph: "controlVectors", "nodeTypes", "isomorphisms", + "isomorphismsNormalized", "points", + "pointsNormalized", "openContours", ) @@ -93,12 +95,12 @@ class Glyph: # Save a "normalized" version of the outlines try: rpen = DecomposingRecordingPen(glyphset) - tpen = TransformPen( - rpen, transform_from_stats(greenStats, inverse=True) - ) + transform = transform_from_stats(greenStats, inverse=True) + tpen = TransformPen(rpen, transform) contour.replay(tpen) self.recordingsNormalized.append(rpen) except ZeroDivisionError: + transform = Transform() self.recordingsNormalized.append(None) greenStats = StatisticsPen(glyphset=glyphset) @@ -112,6 +114,7 @@ class Glyph: assert nodeTypes[0] == "moveTo" assert nodeTypes[-1] in ("closePath", "endPath") + points = SimpleRecordingPointPen() converter = SegmentToPointPen(points, False) contour.replay(converter) @@ -120,14 +123,26 @@ class Glyph: # possible starting points. self.points.append(points.value) + pointsNormalized = SimpleRecordingPointPen() + converter = SegmentToPointPen(pointsNormalized, False) + converter = TransformPen(converter, transform) + contour.replay(converter) + self.pointsNormalized.append(pointsNormalized.value) + isomorphisms = [] self.isomorphisms.append(isomorphisms) - # Add rotations add_isomorphisms(points.value, isomorphisms, False) # Add mirrored rotations add_isomorphisms(points.value, isomorphisms, True) + isomorphisms = [] + self.isomorphismsNormalized.append(isomorphisms) + # Add rotations + add_isomorphisms(pointsNormalized.value, isomorphisms, False) + # Add mirrored rotations + add_isomorphisms(pointsNormalized.value, isomorphisms, True) + def draw(self, pen, countor_idx=None): if countor_idx is None: for contour in self.recordings: diff --git a/Lib/fontTools/varLib/interpolatableTestStartingPoint.py b/Lib/fontTools/varLib/interpolatableTestStartingPoint.py index a84a65184..409297a60 100644 --- a/Lib/fontTools/varLib/interpolatableTestStartingPoint.py +++ b/Lib/fontTools/varLib/interpolatableTestStartingPoint.py @@ -9,10 +9,6 @@ def test_starting_point(glyph0, glyph1, ix, tolerance, matching): m0Vectors = glyph0.greenVectors m1Vectors = [glyph1.greenVectors[i] for i in matching] - proposed_point = 0 - reverse = False - min_cost = first_cost = 1 - c0 = contour0[0] # Next few lines duplicated below. costs = [vdiff_hypot2_complex(c0[0], c1[0]) for c1 in contour1] @@ -21,6 +17,7 @@ def test_starting_point(glyph0, glyph1, ix, tolerance, matching): proposed_point = contour1[min_cost_idx][1] reverse = contour1[min_cost_idx][2] + this_tolerance = min_cost / first_cost if first_cost else 1 if min_cost < first_cost * tolerance: # c0 is the first isomorphism of the m0 master @@ -40,69 +37,22 @@ def test_starting_point(glyph0, glyph1, ix, tolerance, matching): # pass. num_points = len(glyph1.points[ix]) - leeway = 3 - if not reverse and ( - proposed_point <= leeway or proposed_point >= num_points - leeway - ): + leeway = num_points // 4 + if proposed_point <= leeway or proposed_point >= num_points - leeway: # Try harder + contour0 = glyph0.isomorphismsNormalized[ix] + contour1 = glyph1.isomorphismsNormalized[matching[ix]] - # Recover the covariance matrix from the GreenVectors. - # This is a 2x2 matrix. - transforms = [] - for vector in (m0Vectors[ix], m1Vectors[ix]): - meanX = vector[1] - meanY = vector[2] - stddevX = vector[3] * 0.5 - stddevY = vector[4] * 0.5 - correlation = vector[5] / abs(vector[0]) + c0 = contour0[0] + costs = [vdiff_hypot2_complex(c0[0], c1[0]) for c1 in contour1] + new_min_cost_idx, new_min_cost = min(enumerate(costs), key=lambda x: x[1]) + new_first_cost = costs[0] + new_this_tolerance = new_min_cost / new_first_cost if new_first_cost else 1 + if new_this_tolerance > this_tolerance: + proposed_point = contour1[new_min_cost_idx][1] + reverse = contour1[new_min_cost_idx][2] + this_tolerance = new_this_tolerance - # https://cookierobotics.com/007/ - a = stddevX * stddevX # VarianceX - c = stddevY * stddevY # VarianceY - b = correlation * stddevX * stddevY # Covariance - - delta = (((a - c) * 0.5) ** 2 + b * b) ** 0.5 - lambda1 = (a + c) * 0.5 + delta # Major eigenvalue - lambda2 = (a + c) * 0.5 - delta # Minor eigenvalue - theta = atan2(lambda1 - a, b) if b != 0 else (pi * 0.5 if a < c else 0) - trans = Transform() - # Don't translate here. We are working on the complex-vector - # that includes more than just the points. It's horrible what - # we are doing anyway... - # trans = trans.translate(meanX, meanY) - trans = trans.rotate(theta) - trans = trans.scale(sqrt(lambda1), sqrt(lambda2)) - transforms.append(trans) - - trans = transforms[0] - new_c0 = ( - [complex(*trans.transformPoint((pt.real, pt.imag))) for pt in c0[0]], - ) + c0[1:] - trans = transforms[1] - new_contour1 = [] - for c1 in contour1: - new_c1 = ( - [ - complex(*trans.transformPoint((pt.real, pt.imag))) - for pt in c1[0] - ], - ) + c1[1:] - new_contour1.append(new_c1) - - # Next few lines duplicate from above. - costs = [ - vdiff_hypot2_complex(new_c0[0], new_c1[0]) for new_c1 in new_contour1 - ] - min_cost_idx, min_cost = min(enumerate(costs), key=lambda x: x[1]) - first_cost = costs[0] - if min_cost < first_cost * tolerance: - # Don't report this - # min_cost = first_cost - # reverse = False - # proposed_point = 0 # new_contour1[min_cost_idx][1] - pass - - this_tolerance = min_cost / first_cost if first_cost else 1 log.debug( "test-starting-point: tolerance %g", this_tolerance, From d88292b7428f5daa118600b20553c5fb02c22485 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Tue, 5 Dec 2023 15:10:19 -0700 Subject: [PATCH 2/7] Revert "[interpolatableTestStartingPoint] Try to rationalize the extended case" This reverts commit b950447e491ddf1e1a739d06711755db01ad5e4e. --- Lib/fontTools/varLib/interpolatable.py | 23 +----- .../varLib/interpolatableTestStartingPoint.py | 78 +++++++++++++++---- 2 files changed, 68 insertions(+), 33 deletions(-) diff --git a/Lib/fontTools/varLib/interpolatable.py b/Lib/fontTools/varLib/interpolatable.py index 81675052a..ae2de2a19 100644 --- a/Lib/fontTools/varLib/interpolatable.py +++ b/Lib/fontTools/varLib/interpolatable.py @@ -42,9 +42,7 @@ class Glyph: "controlVectors", "nodeTypes", "isomorphisms", - "isomorphismsNormalized", "points", - "pointsNormalized", "openContours", ) @@ -95,12 +93,12 @@ class Glyph: # Save a "normalized" version of the outlines try: rpen = DecomposingRecordingPen(glyphset) - transform = transform_from_stats(greenStats, inverse=True) - tpen = TransformPen(rpen, transform) + tpen = TransformPen( + rpen, transform_from_stats(greenStats, inverse=True) + ) contour.replay(tpen) self.recordingsNormalized.append(rpen) except ZeroDivisionError: - transform = Transform() self.recordingsNormalized.append(None) greenStats = StatisticsPen(glyphset=glyphset) @@ -114,7 +112,6 @@ class Glyph: assert nodeTypes[0] == "moveTo" assert nodeTypes[-1] in ("closePath", "endPath") - points = SimpleRecordingPointPen() converter = SegmentToPointPen(points, False) contour.replay(converter) @@ -123,26 +120,14 @@ class Glyph: # possible starting points. self.points.append(points.value) - pointsNormalized = SimpleRecordingPointPen() - converter = SegmentToPointPen(pointsNormalized, False) - converter = TransformPen(converter, transform) - contour.replay(converter) - self.pointsNormalized.append(pointsNormalized.value) - isomorphisms = [] self.isomorphisms.append(isomorphisms) + # Add rotations add_isomorphisms(points.value, isomorphisms, False) # Add mirrored rotations add_isomorphisms(points.value, isomorphisms, True) - isomorphisms = [] - self.isomorphismsNormalized.append(isomorphisms) - # Add rotations - add_isomorphisms(pointsNormalized.value, isomorphisms, False) - # Add mirrored rotations - add_isomorphisms(pointsNormalized.value, isomorphisms, True) - def draw(self, pen, countor_idx=None): if countor_idx is None: for contour in self.recordings: diff --git a/Lib/fontTools/varLib/interpolatableTestStartingPoint.py b/Lib/fontTools/varLib/interpolatableTestStartingPoint.py index 409297a60..a84a65184 100644 --- a/Lib/fontTools/varLib/interpolatableTestStartingPoint.py +++ b/Lib/fontTools/varLib/interpolatableTestStartingPoint.py @@ -9,6 +9,10 @@ def test_starting_point(glyph0, glyph1, ix, tolerance, matching): m0Vectors = glyph0.greenVectors m1Vectors = [glyph1.greenVectors[i] for i in matching] + proposed_point = 0 + reverse = False + min_cost = first_cost = 1 + c0 = contour0[0] # Next few lines duplicated below. costs = [vdiff_hypot2_complex(c0[0], c1[0]) for c1 in contour1] @@ -17,7 +21,6 @@ def test_starting_point(glyph0, glyph1, ix, tolerance, matching): proposed_point = contour1[min_cost_idx][1] reverse = contour1[min_cost_idx][2] - this_tolerance = min_cost / first_cost if first_cost else 1 if min_cost < first_cost * tolerance: # c0 is the first isomorphism of the m0 master @@ -37,22 +40,69 @@ def test_starting_point(glyph0, glyph1, ix, tolerance, matching): # pass. num_points = len(glyph1.points[ix]) - leeway = num_points // 4 - if proposed_point <= leeway or proposed_point >= num_points - leeway: + leeway = 3 + if not reverse and ( + proposed_point <= leeway or proposed_point >= num_points - leeway + ): # Try harder - contour0 = glyph0.isomorphismsNormalized[ix] - contour1 = glyph1.isomorphismsNormalized[matching[ix]] - c0 = contour0[0] - costs = [vdiff_hypot2_complex(c0[0], c1[0]) for c1 in contour1] - new_min_cost_idx, new_min_cost = min(enumerate(costs), key=lambda x: x[1]) - new_first_cost = costs[0] - new_this_tolerance = new_min_cost / new_first_cost if new_first_cost else 1 - if new_this_tolerance > this_tolerance: - proposed_point = contour1[new_min_cost_idx][1] - reverse = contour1[new_min_cost_idx][2] - this_tolerance = new_this_tolerance + # Recover the covariance matrix from the GreenVectors. + # This is a 2x2 matrix. + transforms = [] + for vector in (m0Vectors[ix], m1Vectors[ix]): + meanX = vector[1] + meanY = vector[2] + stddevX = vector[3] * 0.5 + stddevY = vector[4] * 0.5 + correlation = vector[5] / abs(vector[0]) + # https://cookierobotics.com/007/ + a = stddevX * stddevX # VarianceX + c = stddevY * stddevY # VarianceY + b = correlation * stddevX * stddevY # Covariance + + delta = (((a - c) * 0.5) ** 2 + b * b) ** 0.5 + lambda1 = (a + c) * 0.5 + delta # Major eigenvalue + lambda2 = (a + c) * 0.5 - delta # Minor eigenvalue + theta = atan2(lambda1 - a, b) if b != 0 else (pi * 0.5 if a < c else 0) + trans = Transform() + # Don't translate here. We are working on the complex-vector + # that includes more than just the points. It's horrible what + # we are doing anyway... + # trans = trans.translate(meanX, meanY) + trans = trans.rotate(theta) + trans = trans.scale(sqrt(lambda1), sqrt(lambda2)) + transforms.append(trans) + + trans = transforms[0] + new_c0 = ( + [complex(*trans.transformPoint((pt.real, pt.imag))) for pt in c0[0]], + ) + c0[1:] + trans = transforms[1] + new_contour1 = [] + for c1 in contour1: + new_c1 = ( + [ + complex(*trans.transformPoint((pt.real, pt.imag))) + for pt in c1[0] + ], + ) + c1[1:] + new_contour1.append(new_c1) + + # Next few lines duplicate from above. + costs = [ + vdiff_hypot2_complex(new_c0[0], new_c1[0]) for new_c1 in new_contour1 + ] + min_cost_idx, min_cost = min(enumerate(costs), key=lambda x: x[1]) + first_cost = costs[0] + if min_cost < first_cost * tolerance: + # Don't report this + # min_cost = first_cost + # reverse = False + # proposed_point = 0 # new_contour1[min_cost_idx][1] + pass + + this_tolerance = min_cost / first_cost if first_cost else 1 log.debug( "test-starting-point: tolerance %g", this_tolerance, From aceaf10b78be3949c4f255e389219e449a8794fd Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Tue, 5 Dec 2023 15:10:57 -0700 Subject: [PATCH 3/7] [interpolatableTestStartingPoint] Remove unneeded code --- Lib/fontTools/varLib/interpolatableTestStartingPoint.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Lib/fontTools/varLib/interpolatableTestStartingPoint.py b/Lib/fontTools/varLib/interpolatableTestStartingPoint.py index a84a65184..e76000663 100644 --- a/Lib/fontTools/varLib/interpolatableTestStartingPoint.py +++ b/Lib/fontTools/varLib/interpolatableTestStartingPoint.py @@ -9,16 +9,11 @@ def test_starting_point(glyph0, glyph1, ix, tolerance, matching): m0Vectors = glyph0.greenVectors m1Vectors = [glyph1.greenVectors[i] for i in matching] - proposed_point = 0 - reverse = False - min_cost = first_cost = 1 - c0 = contour0[0] # Next few lines duplicated below. costs = [vdiff_hypot2_complex(c0[0], c1[0]) for c1 in contour1] min_cost_idx, min_cost = min(enumerate(costs), key=lambda x: x[1]) first_cost = costs[0] - proposed_point = contour1[min_cost_idx][1] reverse = contour1[min_cost_idx][2] From 8ad3bb53b7d9f7492420078550a6ac3529e3cb5a Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 6 Dec 2023 12:38:32 -0700 Subject: [PATCH 4/7] [interpolatable] Add InterpolatableProblem --- Lib/fontTools/varLib/interpolatable.py | 53 ++++++++------- Lib/fontTools/varLib/interpolatableHelpers.py | 15 +++++ Lib/fontTools/varLib/interpolatablePlot.py | 67 ++++++++++++------- 3 files changed, 86 insertions(+), 49 deletions(-) diff --git a/Lib/fontTools/varLib/interpolatable.py b/Lib/fontTools/varLib/interpolatable.py index ae2de2a19..4438d9afa 100644 --- a/Lib/fontTools/varLib/interpolatable.py +++ b/Lib/fontTools/varLib/interpolatable.py @@ -186,7 +186,11 @@ def test_gen( if not ignore_missing: yield ( glyph_name, - {"type": "missing", "master": name, "master_idx": master_idx}, + { + "type": InterpolatableProblem.MISSING, + "master": name, + "master_idx": master_idx, + }, ) continue @@ -198,10 +202,10 @@ def test_gen( yield ( glyph_name, { + "type": InterpolatableProblem.OPEN_PATH, "master": name, "master_idx": master_idx, "contour": ix, - "type": "open_path", }, ) if has_open: @@ -230,7 +234,7 @@ def test_gen( yield ( glyph_name, { - "type": "path_count", + "type": InterpolatableProblem.PATH_COUNT, "master_1": names[m0idx], "master_2": names[m1idx], "master_1_idx": m0idx, @@ -249,7 +253,7 @@ def test_gen( yield ( glyph_name, { - "type": "node_count", + "type": InterpolatableProblem.NODE_COUNT, "path": pathIx, "master_1": names[m0idx], "master_2": names[m1idx], @@ -265,7 +269,7 @@ def test_gen( yield ( glyph_name, { - "type": "node_incompatibility", + "type": InterpolatableProblem.NODE_INCOMPATIBILITY, "path": pathIx, "node": nodeIx, "master_1": names[m0idx], @@ -279,7 +283,7 @@ def test_gen( continue # - # "contour_order" check + # InterpolatableProblem.CONTOUR_ORDER check # this_tolerance, matching = test_contour_order(glyph0, glyph1) @@ -287,7 +291,7 @@ def test_gen( yield ( glyph_name, { - "type": "contour_order", + "type": InterpolatableProblem.CONTOUR_ORDER, "master_1": names[m0idx], "master_2": names[m1idx], "master_1_idx": m0idx, @@ -300,7 +304,7 @@ def test_gen( matchings[m1idx] = matching # - # "wrong_start_point" / weight check + # wrong-start-point / weight check # m0Isomorphisms = glyph0.isomorphisms @@ -354,7 +358,7 @@ def test_gen( yield ( glyph_name, { - "type": "wrong_start_point", + "type": InterpolatableProblem.WRONG_START_POINT, "contour": ix, "master_1": names[m0idx], "master_2": names[m1idx], @@ -400,7 +404,10 @@ def test_gen( t = tolerance**power for overweight, problem_type in enumerate( - ("underweight", "overweight") + ( + InterpolatableProblem.UNDERWEIGHT, + InterpolatableProblem.OVERWEIGHT, + ) ): if overweight: expectedSize = sqrt(size0 * size1) @@ -579,7 +586,7 @@ def test_gen( yield ( glyph_name, { - "type": "kink", + "type": InterpolatableProblem.KINK, "contour": ix, "master_1": names[m0idx], "master_2": names[m1idx], @@ -598,7 +605,7 @@ def test_gen( yield ( glyph_name, { - "type": "nothing", + "type": InterpolatableProblem.NOTHING, "master_1": names[m0idx], "master_2": names[m1idx], "master_1_idx": m0idx, @@ -947,16 +954,16 @@ def main(args=None): print(f" Masters: %s:" % ", ".join(master_names), file=f) last_master_idxs = master_idxs - if p["type"] == "missing": + if p["type"] == InterpolatableProblem.MISSING: print( " Glyph was missing in master %s" % p["master"], file=f ) - elif p["type"] == "open_path": + elif p["type"] == InterpolatableProblem.OPEN_PATH: print( " Glyph has an open path in master %s" % p["master"], file=f, ) - elif p["type"] == "path_count": + elif p["type"] == InterpolatableProblem.PATH_COUNT: print( " Path count differs: %i in %s, %i in %s" % ( @@ -967,7 +974,7 @@ def main(args=None): ), file=f, ) - elif p["type"] == "node_count": + elif p["type"] == InterpolatableProblem.NODE_COUNT: print( " Node count differs in path %i: %i in %s, %i in %s" % ( @@ -979,7 +986,7 @@ def main(args=None): ), file=f, ) - elif p["type"] == "node_incompatibility": + elif p["type"] == InterpolatableProblem.NODE_INCOMPATIBILITY: print( " Node %o incompatible in path %i: %s in %s, %s in %s" % ( @@ -992,7 +999,7 @@ def main(args=None): ), file=f, ) - elif p["type"] == "contour_order": + elif p["type"] == InterpolatableProblem.CONTOUR_ORDER: print( " Contour order differs: %s in %s, %s in %s" % ( @@ -1003,7 +1010,7 @@ def main(args=None): ), file=f, ) - elif p["type"] == "wrong_start_point": + elif p["type"] == InterpolatableProblem.WRONG_START_POINT: print( " Contour %d start point differs: %s in %s, %s in %s; reversed: %s" % ( @@ -1016,7 +1023,7 @@ def main(args=None): ), file=f, ) - elif p["type"] == "underweight": + elif p["type"] == InterpolatableProblem.UNDERWEIGHT: print( " Contour %d interpolation is underweight: %s, %s" % ( @@ -1026,7 +1033,7 @@ def main(args=None): ), file=f, ) - elif p["type"] == "overweight": + elif p["type"] == InterpolatableProblem.OVERWEIGHT: print( " Contour %d interpolation is overweight: %s, %s" % ( @@ -1036,7 +1043,7 @@ def main(args=None): ), file=f, ) - elif p["type"] == "kink": + elif p["type"] == InterpolatableProblem.KINK: print( " Contour %d has a kink at %s: %s, %s" % ( @@ -1047,7 +1054,7 @@ def main(args=None): ), file=f, ) - elif p["type"] == "nothing": + elif p["type"] == InterpolatableProblem.NOTHING: print( " Showing %s and %s" % ( diff --git a/Lib/fontTools/varLib/interpolatableHelpers.py b/Lib/fontTools/varLib/interpolatableHelpers.py index 513e5f740..8f3f4de5f 100644 --- a/Lib/fontTools/varLib/interpolatableHelpers.py +++ b/Lib/fontTools/varLib/interpolatableHelpers.py @@ -4,6 +4,7 @@ from fontTools.pens.recordingPen import RecordingPen, DecomposingRecordingPen from fontTools.misc.transform import Transform from collections import defaultdict, deque from math import sqrt, copysign, atan2, pi +from enum import Enum import itertools import logging @@ -11,6 +12,20 @@ import logging log = logging.getLogger("fontTools.varLib.interpolatable") +class InterpolatableProblem: + NOTHING = "nothing" + MISSING = "missing" + OPEN_PATH = "open_path" + PATH_COUNT = "path_count" + NODE_COUNT = "node_count" + NODE_INCOMPATIBILITY = "node_incompatibility" + CONTOUR_ORDER = "contour_order" + WRONG_START_POINT = "wrong_start_point" + UNDERWEIGHT = "underweight" + OVERWEIGHT = "overweight" + KINK = "kink" + + def rot_list(l, k): """Rotate list by k items forward. Ie. item at position 0 will be at position k in returned list. Negative k is allowed.""" diff --git a/Lib/fontTools/varLib/interpolatablePlot.py b/Lib/fontTools/varLib/interpolatablePlot.py index eef4a4716..e5c446a39 100644 --- a/Lib/fontTools/varLib/interpolatablePlot.py +++ b/Lib/fontTools/varLib/interpolatablePlot.py @@ -1,3 +1,4 @@ +from .interpolatableHelpers import * from fontTools.ttLib import TTFont from fontTools.pens.recordingPen import ( RecordingPen, @@ -397,7 +398,7 @@ class InterpolatablePlot: ) master_indices = [problems[0][k] for k in master_keys] - if problem_type == "missing": + if problem_type == InterpolatableProblem.MISSING: sample_glyph = next( i for i, m in enumerate(self.glyphsets) if m[glyphname] is not None ) @@ -457,12 +458,12 @@ class InterpolatablePlot: if any( pt in ( - "nothing", - "wrong_start_point", - "contour_order", - "kink", - "underweight", - "overweight", + InterpolatableProblem.NOTHING, + InterpolatableProblem.WRONG_START_POINT, + InterpolatableProblem.CONTOUR_ORDER, + InterpolatableProblem.KINK, + InterpolatableProblem.UNDERWEIGHT, + InterpolatableProblem.OVERWEIGHT, ) for pt in problem_types ): @@ -489,7 +490,12 @@ class InterpolatablePlot: + [ p for p in problems - if p["type"] in ("kink", "underweight", "overweight") + if p["type"] + in ( + InterpolatableProblem.KINK, + InterpolatableProblem.UNDERWEIGHT, + InterpolatableProblem.OVERWEIGHT, + ) ], None, x=x, @@ -502,9 +508,9 @@ class InterpolatablePlot: if any( pt in ( - "wrong_start_point", - "contour_order", - "kink", + InterpolatableProblem.WRONG_START_POINT, + InterpolatableProblem.CONTOUR_ORDER, + InterpolatableProblem.KINK, ) for pt in problem_types ): @@ -525,14 +531,14 @@ class InterpolatablePlot: glyphset2[glyphname].draw(perContourPen2) for problem in problems: - if problem["type"] == "contour_order": + if problem["type"] == InterpolatableProblem.CONTOUR_ORDER: fixed_contours = [ perContourPen2.value[i] for i in problems[0]["value_2"] ] perContourPen2.value = fixed_contours for problem in problems: - if problem["type"] == "wrong_start_point": + if problem["type"] == InterpolatableProblem.WRONG_START_POINT: # Save the wrong contours wrongContour1 = perContourPen1.value[problem["contour"]] wrongContour2 = perContourPen2.value[problem["contour"]] @@ -578,7 +584,7 @@ class InterpolatablePlot: for problem in problems: # If we have a kink, try to fix it. - if problem["type"] == "kink": + if problem["type"] == InterpolatableProblem.KINK: # Save the wrong contours wrongContour1 = perContourPen1.value[problem["contour"]] wrongContour2 = perContourPen2.value[problem["contour"]] @@ -673,11 +679,11 @@ class InterpolatablePlot: else: emoticon = self.shrug - if "underweight" in problem_types: + if InterpolatableProblem.UNDERWEIGHT in problem_types: emoticon = self.underweight - elif "overweight" in problem_types: + elif InterpolatableProblem.OVERWEIGHT in problem_types: emoticon = self.overweight - elif "nothing" in problem_types: + elif InterpolatableProblem.NOTHING in problem_types: emoticon = self.yay self.draw_emoticon(emoticon, x=x, y=y) @@ -793,7 +799,7 @@ class InterpolatablePlot: pen = CairoPen(glyphset, cr) decomposedRecording.replay(pen) - if self.fill_color and problem_type != "open_path": + if self.fill_color and problem_type != InterpolatableProblem.OPEN_PATH: cr.set_source_rgb(*self.fill_color) cr.fill_preserve() @@ -804,11 +810,17 @@ class InterpolatablePlot: cr.new_path() - if "underweight" in problem_types or "overweight" in problem_types: + if ( + InterpolatableProblem.UNDERWEIGHT in problem_types + or InterpolatableProblem.OVERWEIGHT in problem_types + ): perContourPen = PerContourOrComponentPen(RecordingPen, glyphset=glyphset) recording.replay(perContourPen) for problem in problems: - if problem["type"] in ("underweight", "overweight"): + if problem["type"] in ( + InterpolatableProblem.UNDERWEIGHT, + InterpolatableProblem.OVERWEIGHT, + ): contour = perContourPen.value[problem["contour"]] contour.replay(CairoPen(glyphset, cr)) cr.set_source_rgba(*self.weight_issue_contour_color) @@ -817,9 +829,9 @@ class InterpolatablePlot: if any( t in problem_types for t in { - "nothing", - "node_count", - "node_incompatibility", + InterpolatableProblem.NOTHING, + InterpolatableProblem.NODE_COUNT, + InterpolatableProblem.NODE_INCOMPATIBILITY, } ): cr.set_line_cap(cairo.LINE_CAP_ROUND) @@ -873,7 +885,7 @@ class InterpolatablePlot: matching = None for problem in problems: - if problem["type"] == "contour_order": + if problem["type"] == InterpolatableProblem.CONTOUR_ORDER: matching = problem["value_2"] colors = cycle(self.contour_colors) perContourPen = PerContourOrComponentPen( @@ -889,7 +901,10 @@ class InterpolatablePlot: cr.fill() for problem in problems: - if problem["type"] in ("nothing", "wrong_start_point"): + if problem["type"] in ( + InterpolatableProblem.NOTHING, + InterpolatableProblem.WRONG_START_POINT, + ): idx = problem.get("contour") # Draw suggested point @@ -967,7 +982,7 @@ class InterpolatablePlot: cr.restore() - if problem["type"] == "kink": + if problem["type"] == InterpolatableProblem.KINK: idx = problem.get("contour") perContourPen = PerContourOrComponentPen( RecordingPen, glyphset=glyphset From 1a361941795939777fc2876b00ff6fe43d49f8c0 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 6 Dec 2023 12:47:06 -0700 Subject: [PATCH 5/7] [interpolatable] Sort problems by severity --- Lib/fontTools/varLib/interpolatable.py | 2 ++ Lib/fontTools/varLib/interpolatableHelpers.py | 26 ++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/varLib/interpolatable.py b/Lib/fontTools/varLib/interpolatable.py index 4438d9afa..263d0f999 100644 --- a/Lib/fontTools/varLib/interpolatable.py +++ b/Lib/fontTools/varLib/interpolatable.py @@ -1067,6 +1067,8 @@ def main(args=None): for glyphname, problem in problems_gen: problems[glyphname].append(problem) + problems = sort_problems(problems) + if args.pdf: log.info("Writing PDF to %s", args.pdf) from .interpolatablePlot import InterpolatablePDF diff --git a/Lib/fontTools/varLib/interpolatableHelpers.py b/Lib/fontTools/varLib/interpolatableHelpers.py index 8f3f4de5f..b99ae063d 100644 --- a/Lib/fontTools/varLib/interpolatableHelpers.py +++ b/Lib/fontTools/varLib/interpolatableHelpers.py @@ -21,9 +21,33 @@ class InterpolatableProblem: NODE_INCOMPATIBILITY = "node_incompatibility" CONTOUR_ORDER = "contour_order" WRONG_START_POINT = "wrong_start_point" + KINK = "kink" UNDERWEIGHT = "underweight" OVERWEIGHT = "overweight" - KINK = "kink" + + severity = { + NOTHING: 0, + MISSING: 1, + OPEN_PATH: 2, + PATH_COUNT: 3, + NODE_COUNT: 4, + NODE_INCOMPATIBILITY: 5, + CONTOUR_ORDER: 6, + WRONG_START_POINT: 7, + KINK: 8, + UNDERWEIGHT: 9, + OVERWEIGHT: 10, + } + + +def sort_problems(problems): + """Sort problems by severity, then by glyph name, then by problem message.""" + return dict( + sorted( + problems.items(), + key=lambda _: max(InterpolatableProblem.severity[p["type"]] for p in _[1]), + ) + ) def rot_list(l, k): From c7d9da630a439f2a6db64188a68c21cbe089f4c9 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 6 Dec 2023 13:25:28 -0700 Subject: [PATCH 6/7] [interpolatable] Sort "nothing" at the end of the report --- Lib/fontTools/varLib/interpolatableHelpers.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/varLib/interpolatableHelpers.py b/Lib/fontTools/varLib/interpolatableHelpers.py index b99ae063d..aeac863c1 100644 --- a/Lib/fontTools/varLib/interpolatableHelpers.py +++ b/Lib/fontTools/varLib/interpolatableHelpers.py @@ -26,7 +26,6 @@ class InterpolatableProblem: OVERWEIGHT = "overweight" severity = { - NOTHING: 0, MISSING: 1, OPEN_PATH: 2, PATH_COUNT: 3, @@ -37,6 +36,7 @@ class InterpolatableProblem: KINK: 8, UNDERWEIGHT: 9, OVERWEIGHT: 10, + NOTHING: 11, } @@ -45,7 +45,15 @@ def sort_problems(problems): return dict( sorted( problems.items(), - key=lambda _: max(InterpolatableProblem.severity[p["type"]] for p in _[1]), + key=lambda _: max( + ( + InterpolatableProblem.severity[p["type"]] + for p in _[1] + if InterpolatableProblem.severity[p["type"]] + != InterpolatableProblem.severity[InterpolatableProblem.NOTHING] + ), + default=InterpolatableProblem.severity[InterpolatableProblem.NOTHING], + ), ) ) From 930047d853111b9c9d473e37d02b697af8727298 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 6 Dec 2023 13:44:06 -0700 Subject: [PATCH 7/7] [interpolatable] Tweak sort order Sort by most severe problem. --- Lib/fontTools/varLib/interpolatableHelpers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/varLib/interpolatableHelpers.py b/Lib/fontTools/varLib/interpolatableHelpers.py index aeac863c1..b54ca8a36 100644 --- a/Lib/fontTools/varLib/interpolatableHelpers.py +++ b/Lib/fontTools/varLib/interpolatableHelpers.py @@ -45,7 +45,7 @@ def sort_problems(problems): return dict( sorted( problems.items(), - key=lambda _: max( + key=lambda _: -min( ( InterpolatableProblem.severity[p["type"]] for p in _[1] @@ -54,6 +54,7 @@ def sort_problems(problems): ), default=InterpolatableProblem.severity[InterpolatableProblem.NOTHING], ), + reverse=True, ) )