blob: ba5231b796795259f9b7a471242e142d48623b0e [file] [log] [blame]
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
@lru_cache(128)
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