| 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 |