Spaces:
Sleeping
Sleeping
from fontTools.varLib.models import supportScalar | |
from fontTools.misc.fixedTools import MAX_F2DOT14 | |
from functools import lru_cache | |
__all__ = ["rebaseTent"] | |
EPSILON = 1 / (1 << 14) | |
def _reverse_negate(v): | |
return (-v[2], -v[1], -v[0]) | |
def _solve(tent, axisLimit, negative=False): | |
axisMin, axisDef, axisMax, _distanceNegative, _distancePositive = axisLimit | |
lower, peak, upper = tent | |
# Mirror the problem such that axisDef <= peak | |
if axisDef > peak: | |
return [ | |
(scalar, _reverse_negate(t) if t is not None else None) | |
for scalar, t in _solve( | |
_reverse_negate(tent), | |
axisLimit.reverse_negate(), | |
not negative, | |
) | |
] | |
# axisDef <= peak | |
# case 1: The whole deltaset falls outside the new limit; we can drop it | |
# | |
# peak | |
# 1.........................................o.......... | |
# / \ | |
# / \ | |
# / \ | |
# / \ | |
# 0---|-----------|----------|-------- o o----1 | |
# axisMin axisDef axisMax lower upper | |
# | |
if axisMax <= lower and axisMax < peak: | |
return [] # No overlap | |
# case 2: Only the peak and outermost bound fall outside the new limit; | |
# we keep the deltaset, update peak and outermost bound and and scale deltas | |
# by the scalar value for the restricted axis at the new limit, and solve | |
# recursively. | |
# | |
# |peak | |
# 1...............................|.o.......... | |
# |/ \ | |
# / \ | |
# /| \ | |
# / | \ | |
# 0--------------------------- o | o----1 | |
# lower | upper | |
# | | |
# axisMax | |
# | |
# Convert to: | |
# | |
# 1............................................ | |
# | | |
# o peak | |
# /| | |
# /x| | |
# 0--------------------------- o o upper ----1 | |
# lower | | |
# | | |
# axisMax | |
if axisMax < peak: | |
mult = supportScalar({"tag": axisMax}, {"tag": tent}) | |
tent = (lower, axisMax, axisMax) | |
return [(scalar * mult, t) for scalar, t in _solve(tent, axisLimit)] | |
# lower <= axisDef <= peak <= axisMax | |
gain = supportScalar({"tag": axisDef}, {"tag": tent}) | |
out = [(gain, None)] | |
# First, the positive side | |
# outGain is the scalar of axisMax at the tent. | |
outGain = supportScalar({"tag": axisMax}, {"tag": tent}) | |
# Case 3a: Gain is more than outGain. The tent down-slope crosses | |
# the axis into negative. We have to split it into multiples. | |
# | |
# | peak | | |
# 1...................|.o.....|.............. | |
# |/x\_ | | |
# gain................+....+_.|.............. | |
# /| |y\| | |
# ................../.|....|..+_......outGain | |
# / | | | \ | |
# 0---|-----------o | | | o----------1 | |
# axisMin lower | | | upper | |
# | | | | |
# axisDef | axisMax | |
# | | |
# crossing | |
if gain >= outGain: | |
# Note that this is the branch taken if both gain and outGain are 0. | |
# Crossing point on the axis. | |
crossing = peak + (1 - gain) * (upper - peak) | |
loc = (max(lower, axisDef), peak, crossing) | |
scalar = 1 | |
# The part before the crossing point. | |
out.append((scalar - gain, loc)) | |
# The part after the crossing point may use one or two tents, | |
# depending on whether upper is before axisMax or not, in one | |
# case we need to keep it down to eternity. | |
# Case 3a1, similar to case 1neg; just one tent needed, as in | |
# the drawing above. | |
if upper >= axisMax: | |
loc = (crossing, axisMax, axisMax) | |
scalar = outGain | |
out.append((scalar - gain, loc)) | |
# Case 3a2: Similar to case 2neg; two tents needed, to keep | |
# down to eternity. | |
# | |
# | peak | | |
# 1...................|.o................|... | |
# |/ \_ | | |
# gain................+....+_............|... | |
# /| | \xxxxxxxxxxy| | |
# / | | \_xxxxxyyyy| | |
# / | | \xxyyyyyy| | |
# 0---|-----------o | | o-------|--1 | |
# axisMin lower | | upper | | |
# | | | | |
# axisDef | axisMax | |
# | | |
# crossing | |
else: | |
# A tent's peak cannot fall on axis default. Nudge it. | |
if upper == axisDef: | |
upper += EPSILON | |
# Downslope. | |
loc1 = (crossing, upper, axisMax) | |
scalar1 = 0 | |
# Eternity justify. | |
loc2 = (upper, axisMax, axisMax) | |
scalar2 = 0 | |
out.append((scalar1 - gain, loc1)) | |
out.append((scalar2 - gain, loc2)) | |
else: | |
# Special-case if peak is at axisMax. | |
if axisMax == peak: | |
upper = peak | |
# Case 3: | |
# We keep delta as is and only scale the axis upper to achieve | |
# the desired new tent if feasible. | |
# | |
# peak | |
# 1.....................o.................... | |
# / \_| | |
# ..................../....+_.........outGain | |
# / | \ | |
# gain..............+......|..+_............. | |
# /| | | \ | |
# 0---|-----------o | | | o----------1 | |
# axisMin lower| | | upper | |
# | | newUpper | |
# axisDef axisMax | |
# | |
newUpper = peak + (1 - gain) * (upper - peak) | |
assert axisMax <= newUpper # Because outGain > gain | |
# Disabled because ots doesn't like us: | |
# https://github.com/fonttools/fonttools/issues/3350 | |
if False and newUpper <= axisDef + (axisMax - axisDef) * 2: | |
upper = newUpper | |
if not negative and axisDef + (axisMax - axisDef) * MAX_F2DOT14 < upper: | |
# we clamp +2.0 to the max F2Dot14 (~1.99994) for convenience | |
upper = axisDef + (axisMax - axisDef) * MAX_F2DOT14 | |
assert peak < upper | |
loc = (max(axisDef, lower), peak, upper) | |
scalar = 1 | |
out.append((scalar - gain, loc)) | |
# Case 4: New limit doesn't fit; we need to chop into two tents, | |
# because the shape of a triangle with part of one side cut off | |
# cannot be represented as a triangle itself. | |
# | |
# | peak | | |
# 1.........|......o.|.................... | |
# ..........|...../x\|.............outGain | |
# | |xxy|\_ | |
# | /xxxy| \_ | |
# | |xxxxy| \_ | |
# | /xxxxy| \_ | |
# 0---|-----|-oxxxxxx| o----------1 | |
# axisMin | lower | upper | |
# | | | |
# axisDef axisMax | |
# | |
else: | |
loc1 = (max(axisDef, lower), peak, axisMax) | |
scalar1 = 1 | |
loc2 = (peak, axisMax, axisMax) | |
scalar2 = outGain | |
out.append((scalar1 - gain, loc1)) | |
# Don't add a dirac delta! | |
if peak < axisMax: | |
out.append((scalar2 - gain, loc2)) | |
# Now, the negative side | |
# Case 1neg: Lower extends beyond axisMin: we chop. Simple. | |
# | |
# | |peak | |
# 1..................|...|.o................. | |
# | |/ \ | |
# gain...............|...+...\............... | |
# |x_/| \ | |
# |/ | \ | |
# _/| | \ | |
# 0---------------o | | o----------1 | |
# lower | | upper | |
# | | | |
# axisMin axisDef | |
# | |
if lower <= axisMin: | |
loc = (axisMin, axisMin, axisDef) | |
scalar = supportScalar({"tag": axisMin}, {"tag": tent}) | |
out.append((scalar - gain, loc)) | |
# Case 2neg: Lower is betwen axisMin and axisDef: we add two | |
# tents to keep it down all the way to eternity. | |
# | |
# | |peak | |
# 1...|...............|.o................. | |
# | |/ \ | |
# gain|...............+...\............... | |
# |yxxxxxxxxxxxxx/| \ | |
# |yyyyyyxxxxxxx/ | \ | |
# |yyyyyyyyyyyx/ | \ | |
# 0---|-----------o | o----------1 | |
# axisMin lower | upper | |
# | | |
# axisDef | |
# | |
else: | |
# A tent's peak cannot fall on axis default. Nudge it. | |
if lower == axisDef: | |
lower -= EPSILON | |
# Downslope. | |
loc1 = (axisMin, lower, axisDef) | |
scalar1 = 0 | |
# Eternity justify. | |
loc2 = (axisMin, axisMin, lower) | |
scalar2 = 0 | |
out.append((scalar1 - gain, loc1)) | |
out.append((scalar2 - gain, loc2)) | |
return out | |
def rebaseTent(tent, axisLimit): | |
"""Given a tuple (lower,peak,upper) "tent" and new axis limits | |
(axisMin,axisDefault,axisMax), solves how to represent the tent | |
under the new axis configuration. All values are in normalized | |
-1,0,+1 coordinate system. Tent values can be outside this range. | |
Return value is a list of tuples. Each tuple is of the form | |
(scalar,tent), where scalar is a multipler to multiply any | |
delta-sets by, and tent is a new tent for that output delta-set. | |
If tent value is None, that is a special deltaset that should | |
be always-enabled (called "gain").""" | |
axisMin, axisDef, axisMax, _distanceNegative, _distancePositive = axisLimit | |
assert -1 <= axisMin <= axisDef <= axisMax <= +1 | |
lower, peak, upper = tent | |
assert -2 <= lower <= peak <= upper <= +2 | |
assert peak != 0 | |
sols = _solve(tent, axisLimit) | |
n = lambda v: axisLimit.renormalizeValue(v) | |
sols = [ | |
(scalar, (n(v[0]), n(v[1]), n(v[2])) if v is not None else None) | |
for scalar, v in sols | |
if scalar | |
] | |
return sols | |