Skip to content

Commit

Permalink
Fiddling with reuse wiring
Browse files Browse the repository at this point in the history
  • Loading branch information
rsheeter committed May 13, 2022
1 parent e2b24b5 commit e64d91e
Show file tree
Hide file tree
Showing 24 changed files with 471 additions and 671 deletions.
2 changes: 1 addition & 1 deletion src/nanoemoji/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ class FontConfig(NamedTuple):
transform: Affine2D = Affine2D.identity()
version_major: int = 1
version_minor: int = 0
reuse_tolerance: float = 0.1
reuse_tolerance: float = 0.05
ignore_reuse_error: bool = True
keep_glyph_names: bool = False
clip_to_viewbox: bool = True
Expand Down
72 changes: 43 additions & 29 deletions src/nanoemoji/glyph_reuse.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,21 @@


from absl import logging
import dataclasses
from nanoemoji import parts
from nanoemoji.parts import ReuseInstruction, ReusableParts
from picosvg.svg_transform import Affine2D
from picosvg.svg_types import SVGPath
from typing import (
MutableMapping,
NamedTuple,
Optional,
Set,
)
from .fixed import fixed_safe


# TODO delete me?
class GlyphInstruction(NamedTuple):
glyph_name: str
path: str
Expand All @@ -38,13 +42,17 @@ def is_reuse(self) -> bool:
return not self.transform.almost_equals(Affine2D.identity())


@dataclasses.dataclass
class GlyphReuseCache:
def __init__(self, reusable_parts: ReusableParts):
assert isinstance(reusable_parts, ReusableParts)
self._reusable_parts = reusable_parts
self._path_to_glyph = {}
self._glyph_to_path = {}
_reusable_parts: ReusableParts
_shape_to_glyph: MutableMapping[parts.Shape, str] = dataclasses.field(
default_factory=dict
)
_glyph_to_shape: MutableMapping[str, parts.Shape] = dataclasses.field(
default_factory=dict
)

# TODO rename try_reuse, ReuseInstruction
def try_reuse(self, path: str) -> ReuseInstruction:
assert path[0].upper() == "M", path
maybe_reuse = self._reusable_parts.try_reuse(SVGPath(d=path))
Expand All @@ -59,16 +67,21 @@ def try_reuse(self, path: str) -> ReuseInstruction:
)
maybe_reuse = None
if maybe_reuse is None:
maybe_reuse = ReuseInstruction(Affine2D.identity(), path)
maybe_reuse = ReuseInstruction(
Affine2D.identity(), parts.as_shape(SVGPath(d=path))
)
return maybe_reuse

# TODO delete me?
def for_path(self, path: str) -> GlyphInstruction:
# TODO we are doing an awful lot of str <> SVGPath
assert path[0].upper() == "M", path
maybe_reuse = self.try_reuse(path)
if maybe_reuse.shape not in self._path_to_glyph:
raise ValueError("Must associate glyph names with paths")
glyph_name = self._path_to_glyph[maybe_reuse.shape]
if maybe_reuse.shape not in self._shape_to_glyph:
raise ValueError(
f"Must associate glyph names with paths, what is {maybe_reuse.shape}"
)
glyph_name = self._shape_to_glyph[maybe_reuse.shape]
return GlyphInstruction(
glyph_name,
maybe_reuse.shape,
Expand All @@ -77,33 +90,34 @@ def for_path(self, path: str) -> GlyphInstruction:
)

def set_glyph_for_path(self, glyph_name: str, path: str):
self._reusable_parts.add(SVGPath(d=path))
self._path_to_glyph[path] = glyph_name
self._glyph_to_path[glyph_name] = path

def set_glyph_name(
self, glyph_name: str, glyph_instruction: GlyphInstruction
) -> GlyphInstruction:
assert (
not glyph_instruction.glyph_name
), f"Name {glyph_instruction.glyph_name} already assigned, cannot update to {glyph_name}"
glyph_instruction = glyph_instruction._replace(glyph_name=glyph_name)
norm = self._reusable_parts.normalize(path)
assert norm in self._reusable_parts.shape_sets, f"No shape set for {path}"
shape = parts.as_shape(SVGPath(d=path))
assert (
glyph_instruction.path not in self._path_to_glyph
), f"Multiple addition of {glyph_instruction.path}"
self._path_to_glyph[glyph_instruction.path] = glyph_name
self._glyph_to_instruction[glyph_name] = glyph_instruction
return glyph_instruction
shape in self._reusable_parts.shape_sets[norm]
), f"Not present in shape set: {path}"
self._shape_to_glyph[shape] = glyph_name
self._glyph_to_shape[glyph_name] = shape

def get_glyph_for_path(self, path: str) -> str:
return self._shape_to_glyph[parts.as_shape(SVGPath(d=path))]

def forget_glyph_path_associations(self):
self._shape_to_glyph.clear()
self._glyph_to_shape.clear()

def consuming_glyphs(self, path: str) -> Set[str]:
norm = self._reusable_parts.normalize(path)
assert (
norm in self._reusable_parts.shape_sets
), f"{glyph_name} not associated with any parts!"
), f"{path} not associated with any parts!"
return {
self._path_to_glyph[shape]
self._shape_to_glyph[shape]
for shape in self._reusable_parts.shape_sets[norm]
}

def is_known_glyph(self, glyph_name):
return glyph_name in self._glyph_to_path
def is_known_glyph(self, glyph_name: str):
return glyph_name in self._glyph_to_shape

def is_known_path(self, path: str):
return parts.as_shape(SVGPath(d=path)) in self._shape_to_glyph
1 change: 0 additions & 1 deletion src/nanoemoji/paint.py
Original file line number Diff line number Diff line change
Expand Up @@ -772,7 +772,6 @@ def transformed(transform: Affine2D, target: Paint) -> Paint:
transform
):
if int16_safe(dx, dy):
print("int16 tx", dx, dy)
return PaintTranslate(paint=target, dx=dx, dy=dy)

# Scale?
Expand Down
35 changes: 26 additions & 9 deletions src/nanoemoji/parts.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@
)


PathSource = Union[SVGShape, Iterable[SVGShape], "ReuseableParts"]
PathSource = Union[SVGShape, Iterable[SVGShape], "ReusableParts"]


_DEFAULT_ROUND_NDIGITS = 3


@lru_cache(maxsize=1)
Expand Down Expand Up @@ -86,6 +89,14 @@ def _bbox_area(shape: Shape) -> float:
return bbox.w * bbox.h


def _round(path: SVGShape) -> SVGPath:
return path.as_path().round_floats(_DEFAULT_ROUND_NDIGITS)


def as_shape(shape: SVGPath) -> Shape:
return Shape(_round(shape).d)


# TODO: create a parts builder and a frozen parts to more explicitly model the add/use cycle


Expand All @@ -100,10 +111,12 @@ class ReusableParts:
default_factory=dict
)

def normalize(self, shape: Shape) -> NormalizedShape:
norm = NormalizedShape(shape)
def normalize(self, path: str) -> NormalizedShape:
if self.reuse_tolerance != -1:
norm = NormalizedShape(normalize(SVGPath(d=shape), self.reuse_tolerance).d)
# normalize handles it's own rounding
norm = NormalizedShape(normalize(SVGPath(d=path), self.reuse_tolerance).d)
else:
norm = NormalizedShape(path)
return norm

def _add_norm_path(self, norm: NormalizedShape, shape: Shape):
Expand All @@ -128,7 +141,7 @@ def add(self, source: PathSource):
if not isinstance(a_source, SVGShape):
raise ValueError(f"Illegal source {type(a_source)}")
svg_shape: SVGShape = a_source # pytype: disable=attribute-error
self._add(Shape(svg_shape.as_path().d))
self._add(as_shape(svg_shape))

def _compute_donor(self, norm: NormalizedShape):
self._donor_cache[norm] = None # no solution
Expand All @@ -155,6 +168,7 @@ def _compute_donor(self, norm: NormalizedShape):
affine_between(svg_path, svg_path2, self.reuse_tolerance) is not None
for svg_path2 in svg_paths
):
# Do NOT use as_shape; these paths already passed through it
self._donor_cache[norm] = Shape(svg_path.d)
break

Expand All @@ -164,7 +178,7 @@ def compute_donors(self):
self._compute_donor(norm)

def is_reused(self, shape: SVGPath) -> bool:
shape = Shape(shape.d)
shape = as_shape(shape)
norm = self.normalize(shape)
if norm not in self.shape_sets:
return False
Expand All @@ -176,7 +190,10 @@ def is_reused(self, shape: SVGPath) -> bool:

def try_reuse(self, shape: SVGPath) -> Optional[ReuseInstruction]:
"""Returns the shape and transform to use to build the input shape."""
shape = Shape(shape.d)
shape = as_shape(shape)
if self.reuse_tolerance == -1:
return ReuseInstruction(Affine2D.identity(), shape)

norm = self.normalize(shape)

# The whole point is to pre-add, doing it on the fly reduces reuse
Expand Down Expand Up @@ -220,7 +237,7 @@ def to_json(self):
return json.dumps(json_dict, indent=2)

@classmethod
def fromstring(cls, string) -> "ReuseableParts":
def fromstring(cls, string) -> "ReusableParts":
first = string.strip()[0]
parts = cls()
if first == "<":
Expand Down Expand Up @@ -251,7 +268,7 @@ def fromstring(cls, string) -> "ReuseableParts":
return parts

@classmethod
def load(cls, input_file: Path) -> "ReuseableParts":
def load(cls, input_file: Path) -> "ReusableParts":
ext = input_file.suffix.lower()
if ext not in {".svg", ".json"}:
raise ValueError(f"Unknown format {input_file}")
Expand Down
Loading

0 comments on commit e64d91e

Please sign in to comment.