|
from __future__ import annotations |
|
|
|
from dataclasses import dataclass |
|
from typing import Dict, List, Optional, Union, cast |
|
|
|
from fontTools.designspaceLib import ( |
|
AxisDescriptor, |
|
DesignSpaceDocument, |
|
DesignSpaceDocumentError, |
|
RangeAxisSubsetDescriptor, |
|
SimpleLocationDict, |
|
ValueAxisSubsetDescriptor, |
|
VariableFontDescriptor, |
|
) |
|
|
|
|
|
def clamp(value, minimum, maximum): |
|
return min(max(value, minimum), maximum) |
|
|
|
|
|
@dataclass |
|
class Range: |
|
minimum: float |
|
"""Inclusive minimum of the range.""" |
|
maximum: float |
|
"""Inclusive maximum of the range.""" |
|
default: float = 0 |
|
"""Default value""" |
|
|
|
def __post_init__(self): |
|
self.minimum, self.maximum = sorted((self.minimum, self.maximum)) |
|
self.default = clamp(self.default, self.minimum, self.maximum) |
|
|
|
def __contains__(self, value: Union[float, Range]) -> bool: |
|
if isinstance(value, Range): |
|
return self.minimum <= value.minimum and value.maximum <= self.maximum |
|
return self.minimum <= value <= self.maximum |
|
|
|
def intersection(self, other: Range) -> Optional[Range]: |
|
if self.maximum < other.minimum or self.minimum > other.maximum: |
|
return None |
|
else: |
|
return Range( |
|
max(self.minimum, other.minimum), |
|
min(self.maximum, other.maximum), |
|
self.default, |
|
) |
|
|
|
|
|
|
|
|
|
|
|
Region = Dict[str, Union[Range, float]] |
|
|
|
|
|
ConditionSet = Dict[str, Range] |
|
|
|
|
|
Rule = List[ConditionSet] |
|
Rules = Dict[str, Rule] |
|
|
|
|
|
def locationInRegion(location: SimpleLocationDict, region: Region) -> bool: |
|
for name, value in location.items(): |
|
if name not in region: |
|
return False |
|
regionValue = region[name] |
|
if isinstance(regionValue, (float, int)): |
|
if value != regionValue: |
|
return False |
|
else: |
|
if value not in regionValue: |
|
return False |
|
return True |
|
|
|
|
|
def regionInRegion(region: Region, superRegion: Region) -> bool: |
|
for name, value in region.items(): |
|
if not name in superRegion: |
|
return False |
|
superValue = superRegion[name] |
|
if isinstance(superValue, (float, int)): |
|
if value != superValue: |
|
return False |
|
else: |
|
if value not in superValue: |
|
return False |
|
return True |
|
|
|
|
|
def userRegionToDesignRegion(doc: DesignSpaceDocument, userRegion: Region) -> Region: |
|
designRegion = {} |
|
for name, value in userRegion.items(): |
|
axis = doc.getAxis(name) |
|
if axis is None: |
|
raise DesignSpaceDocumentError( |
|
f"Cannot find axis named '{name}' for region." |
|
) |
|
if isinstance(value, (float, int)): |
|
designRegion[name] = axis.map_forward(value) |
|
else: |
|
designRegion[name] = Range( |
|
axis.map_forward(value.minimum), |
|
axis.map_forward(value.maximum), |
|
axis.map_forward(value.default), |
|
) |
|
return designRegion |
|
|
|
|
|
def getVFUserRegion(doc: DesignSpaceDocument, vf: VariableFontDescriptor) -> Region: |
|
vfUserRegion: Region = {} |
|
|
|
|
|
|
|
for axisSubset in vf.axisSubsets: |
|
axis = doc.getAxis(axisSubset.name) |
|
if axis is None: |
|
raise DesignSpaceDocumentError( |
|
f"Cannot find axis named '{axisSubset.name}' for variable font '{vf.name}'." |
|
) |
|
if hasattr(axisSubset, "userMinimum"): |
|
|
|
|
|
|
|
axisSubset = cast(RangeAxisSubsetDescriptor, axisSubset) |
|
if not hasattr(axis, "minimum"): |
|
raise DesignSpaceDocumentError( |
|
f"Cannot select a range over '{axis.name}' for variable font '{vf.name}' " |
|
"because it's a discrete axis, use only 'userValue' instead." |
|
) |
|
axis = cast(AxisDescriptor, axis) |
|
vfUserRegion[axis.name] = Range( |
|
max(axisSubset.userMinimum, axis.minimum), |
|
min(axisSubset.userMaximum, axis.maximum), |
|
axisSubset.userDefault or axis.default, |
|
) |
|
else: |
|
axisSubset = cast(ValueAxisSubsetDescriptor, axisSubset) |
|
vfUserRegion[axis.name] = axisSubset.userValue |
|
|
|
for axis in doc.axes: |
|
if axis.name not in vfUserRegion: |
|
assert isinstance( |
|
axis.default, (int, float) |
|
), f"Axis '{axis.name}' has no valid default value." |
|
vfUserRegion[axis.name] = axis.default |
|
return vfUserRegion |
|
|