[varLib.interpolatable] Support discrete axes in .designspace

Fixes https://github.com/fonttools/fonttools/issues/3597
This commit is contained in:
Behdad Esfahbod 2024-08-03 17:34:01 -06:00
parent 16dbe3f5e2
commit ead2a18d4b
2 changed files with 39 additions and 10 deletions

View File

@ -135,6 +135,7 @@ def test_gen(
kinkiness=DEFAULT_KINKINESS, kinkiness=DEFAULT_KINKINESS,
upem=DEFAULT_UPEM, upem=DEFAULT_UPEM,
show_all=False, show_all=False,
discrete_axes=[],
): ):
if tolerance >= 10: if tolerance >= 10:
tolerance *= 0.01 tolerance *= 0.01
@ -150,7 +151,9 @@ def test_gen(
# ... risks the sparse master being the first one, and only processing a subset of the glyphs # ... risks the sparse master being the first one, and only processing a subset of the glyphs
glyphs = {g for glyphset in glyphsets for g in glyphset.keys()} glyphs = {g for glyphset in glyphsets for g in glyphset.keys()}
parents, order = find_parents_and_order(glyphsets, locations) parents, order = find_parents_and_order(
glyphsets, locations, discrete_axes=discrete_axes
)
def grand_parent(i, glyphname): def grand_parent(i, glyphname):
if i is None: if i is None:
@ -701,6 +704,7 @@ def main(args=None):
fonts = [] fonts = []
names = [] names = []
locations = [] locations = []
discrete_axes = set()
upem = DEFAULT_UPEM upem = DEFAULT_UPEM
original_args_inputs = tuple(args.inputs) original_args_inputs = tuple(args.inputs)
@ -713,8 +717,13 @@ def main(args=None):
designspace = DesignSpaceDocument.fromfile(args.inputs[0]) designspace = DesignSpaceDocument.fromfile(args.inputs[0])
args.inputs = [master.path for master in designspace.sources] args.inputs = [master.path for master in designspace.sources]
locations = [master.location for master in designspace.sources] locations = [master.location for master in designspace.sources]
discrete_axes = {
a.name for a in designspace.axes if not hasattr(a, "minimum")
}
axis_triples = { axis_triples = {
a.name: (a.minimum, a.default, a.maximum) for a in designspace.axes a.name: (a.minimum, a.default, a.maximum)
for a in designspace.axes
if a.name not in discrete_axes
} }
axis_mappings = {a.name: a.map for a in designspace.axes} axis_mappings = {a.name: a.map for a in designspace.axes}
axis_triples = { axis_triples = {
@ -879,7 +888,13 @@ def main(args=None):
glyphset[gn] = None glyphset[gn] = None
# Normalize locations # Normalize locations
locations = [normalizeLocation(loc, axis_triples) for loc in locations] locations = [
{
**normalizeLocation(loc, axis_triples),
**{k: v for k, v in loc.items() if k in discrete_axes},
}
for loc in locations
]
tolerance = args.tolerance or DEFAULT_TOLERANCE tolerance = args.tolerance or DEFAULT_TOLERANCE
kinkiness = args.kinkiness if args.kinkiness is not None else DEFAULT_KINKINESS kinkiness = args.kinkiness if args.kinkiness is not None else DEFAULT_KINKINESS
@ -896,6 +911,7 @@ def main(args=None):
tolerance=tolerance, tolerance=tolerance,
kinkiness=kinkiness, kinkiness=kinkiness,
show_all=args.show_all, show_all=args.show_all,
discrete_axes=discrete_axes,
) )
problems = defaultdict(list) problems = defaultdict(list)

View File

@ -293,17 +293,19 @@ def add_isomorphisms(points, isomorphisms, reverse):
) )
def find_parents_and_order(glyphsets, locations): def find_parents_and_order(glyphsets, locations, *, discrete_axes=set()):
parents = [None] + list(range(len(glyphsets) - 1)) parents = [None] + list(range(len(glyphsets) - 1))
order = list(range(len(glyphsets))) order = list(range(len(glyphsets)))
if locations: if locations:
# Order base master first # Order base master first
bases = (i for i, l in enumerate(locations) if all(v == 0 for v in l.values())) bases = [
i
for i, l in enumerate(locations)
if all(v == 0 for k, v in l.items() if k not in discrete_axes)
]
if bases: if bases:
base = next(bases) logging.info("Found %s base masters: %s", len(bases), bases)
logging.info("Base master index %s, location %s", base, locations[base])
else: else:
base = 0
logging.warning("No base master location found") logging.warning("No base master location found")
# Form a minimum spanning tree of the locations # Form a minimum spanning tree of the locations
@ -317,9 +319,17 @@ def find_parents_and_order(glyphsets, locations):
axes = sorted(axes) axes = sorted(axes)
vectors = [tuple(l.get(k, 0) for k in axes) for l in locations] vectors = [tuple(l.get(k, 0) for k in axes) for l in locations]
for i, j in itertools.combinations(range(len(locations)), 2): for i, j in itertools.combinations(range(len(locations)), 2):
i_discrete_location = {
k: v for k, v in zip(axes, vectors[i]) if k in discrete_axes
}
j_discrete_location = {
k: v for k, v in zip(axes, vectors[j]) if k in discrete_axes
}
if i_discrete_location != j_discrete_location:
continue
graph[i][j] = vdiff_hypot2(vectors[i], vectors[j]) graph[i][j] = vdiff_hypot2(vectors[i], vectors[j])
tree = minimum_spanning_tree(graph) tree = minimum_spanning_tree(graph, overwrite=True)
rows, cols = tree.nonzero() rows, cols = tree.nonzero()
graph = defaultdict(set) graph = defaultdict(set)
for row, col in zip(rows, cols): for row, col in zip(rows, cols):
@ -330,7 +340,7 @@ def find_parents_and_order(glyphsets, locations):
parents = [None] * len(locations) parents = [None] * len(locations)
order = [] order = []
visited = set() visited = set()
queue = deque([base]) queue = deque(bases)
while queue: while queue:
i = queue.popleft() i = queue.popleft()
visited.add(i) visited.add(i)
@ -339,6 +349,9 @@ def find_parents_and_order(glyphsets, locations):
if j not in visited: if j not in visited:
parents[j] = i parents[j] = i
queue.append(j) queue.append(j)
assert len(order) == len(
parents
), "Not all masters are reachable; report an issue"
except ImportError: except ImportError:
pass pass