xref: /aosp_15_r20/external/fonttools/Lib/fontTools/varLib/instancer/solver.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1from fontTools.varLib.models import supportScalar
2from fontTools.misc.fixedTools import MAX_F2DOT14
3from functools import lru_cache
4
5__all__ = ["rebaseTent"]
6
7EPSILON = 1 / (1 << 14)
8
9
10def _reverse_negate(v):
11    return (-v[2], -v[1], -v[0])
12
13
14def _solve(tent, axisLimit, negative=False):
15    axisMin, axisDef, axisMax, _distanceNegative, _distancePositive = axisLimit
16    lower, peak, upper = tent
17
18    # Mirror the problem such that axisDef <= peak
19    if axisDef > peak:
20        return [
21            (scalar, _reverse_negate(t) if t is not None else None)
22            for scalar, t in _solve(
23                _reverse_negate(tent),
24                axisLimit.reverse_negate(),
25                not negative,
26            )
27        ]
28    # axisDef <= peak
29
30    # case 1: The whole deltaset falls outside the new limit; we can drop it
31    #
32    #                                          peak
33    #  1.........................................o..........
34    #                                           / \
35    #                                          /   \
36    #                                         /     \
37    #                                        /       \
38    #  0---|-----------|----------|-------- o         o----1
39    #    axisMin     axisDef    axisMax   lower     upper
40    #
41    if axisMax <= lower and axisMax < peak:
42        return []  # No overlap
43
44    # case 2: Only the peak and outermost bound fall outside the new limit;
45    # we keep the deltaset, update peak and outermost bound and and scale deltas
46    # by the scalar value for the restricted axis at the new limit, and solve
47    # recursively.
48    #
49    #                                  |peak
50    #  1...............................|.o..........
51    #                                  |/ \
52    #                                  /   \
53    #                                 /|    \
54    #                                / |     \
55    #  0--------------------------- o  |      o----1
56    #                           lower  |      upper
57    #                                  |
58    #                                axisMax
59    #
60    # Convert to:
61    #
62    #  1............................................
63    #                                  |
64    #                                  o peak
65    #                                 /|
66    #                                /x|
67    #  0--------------------------- o  o upper ----1
68    #                           lower  |
69    #                                  |
70    #                                axisMax
71    if axisMax < peak:
72        mult = supportScalar({"tag": axisMax}, {"tag": tent})
73        tent = (lower, axisMax, axisMax)
74        return [(scalar * mult, t) for scalar, t in _solve(tent, axisLimit)]
75
76    # lower <= axisDef <= peak <= axisMax
77
78    gain = supportScalar({"tag": axisDef}, {"tag": tent})
79    out = [(gain, None)]
80
81    # First, the positive side
82
83    # outGain is the scalar of axisMax at the tent.
84    outGain = supportScalar({"tag": axisMax}, {"tag": tent})
85
86    # Case 3a: Gain is more than outGain. The tent down-slope crosses
87    # the axis into negative. We have to split it into multiples.
88    #
89    #                      | peak  |
90    #  1...................|.o.....|..............
91    #                      |/x\_   |
92    #  gain................+....+_.|..............
93    #                     /|    |y\|
94    #  ................../.|....|..+_......outGain
95    #                   /  |    |  | \
96    #  0---|-----------o   |    |  |  o----------1
97    #    axisMin    lower  |    |  |   upper
98    #                      |    |  |
99    #                axisDef    |  axisMax
100    #                           |
101    #                      crossing
102    if gain >= outGain:
103        # Note that this is the branch taken if both gain and outGain are 0.
104
105        # Crossing point on the axis.
106        crossing = peak + (1 - gain) * (upper - peak)
107
108        loc = (max(lower, axisDef), peak, crossing)
109        scalar = 1
110
111        # The part before the crossing point.
112        out.append((scalar - gain, loc))
113
114        # The part after the crossing point may use one or two tents,
115        # depending on whether upper is before axisMax or not, in one
116        # case we need to keep it down to eternity.
117
118        # Case 3a1, similar to case 1neg; just one tent needed, as in
119        # the drawing above.
120        if upper >= axisMax:
121            loc = (crossing, axisMax, axisMax)
122            scalar = outGain
123
124            out.append((scalar - gain, loc))
125
126        # Case 3a2: Similar to case 2neg; two tents needed, to keep
127        # down to eternity.
128        #
129        #                      | peak             |
130        #  1...................|.o................|...
131        #                      |/ \_              |
132        #  gain................+....+_............|...
133        #                     /|    | \xxxxxxxxxxy|
134        #                    / |    |  \_xxxxxyyyy|
135        #                   /  |    |    \xxyyyyyy|
136        #  0---|-----------o   |    |     o-------|--1
137        #    axisMin    lower  |    |      upper  |
138        #                      |    |             |
139        #                axisDef    |             axisMax
140        #                           |
141        #                      crossing
142        else:
143            # A tent's peak cannot fall on axis default. Nudge it.
144            if upper == axisDef:
145                upper += EPSILON
146
147            # Downslope.
148            loc1 = (crossing, upper, axisMax)
149            scalar1 = 0
150
151            # Eternity justify.
152            loc2 = (upper, axisMax, axisMax)
153            scalar2 = 0
154
155            out.append((scalar1 - gain, loc1))
156            out.append((scalar2 - gain, loc2))
157
158    else:
159        # Special-case if peak is at axisMax.
160        if axisMax == peak:
161            upper = peak
162
163        # Case 3:
164        # We keep delta as is and only scale the axis upper to achieve
165        # the desired new tent if feasible.
166        #
167        #                        peak
168        #  1.....................o....................
169        #                       / \_|
170        #  ..................../....+_.........outGain
171        #                     /     | \
172        #  gain..............+......|..+_.............
173        #                   /|      |  | \
174        #  0---|-----------o |      |  |  o----------1
175        #    axisMin    lower|      |  |   upper
176        #                    |      |  newUpper
177        #              axisDef      axisMax
178        #
179        newUpper = peak + (1 - gain) * (upper - peak)
180        assert axisMax <= newUpper  # Because outGain > gain
181        # Disabled because ots doesn't like us:
182        # https://github.com/fonttools/fonttools/issues/3350
183        if False and newUpper <= axisDef + (axisMax - axisDef) * 2:
184            upper = newUpper
185            if not negative and axisDef + (axisMax - axisDef) * MAX_F2DOT14 < upper:
186                # we clamp +2.0 to the max F2Dot14 (~1.99994) for convenience
187                upper = axisDef + (axisMax - axisDef) * MAX_F2DOT14
188                assert peak < upper
189
190            loc = (max(axisDef, lower), peak, upper)
191            scalar = 1
192
193            out.append((scalar - gain, loc))
194
195        # Case 4: New limit doesn't fit; we need to chop into two tents,
196        # because the shape of a triangle with part of one side cut off
197        # cannot be represented as a triangle itself.
198        #
199        #            |   peak |
200        #  1.........|......o.|....................
201        #  ..........|...../x\|.............outGain
202        #            |    |xxy|\_
203        #            |   /xxxy|  \_
204        #            |  |xxxxy|    \_
205        #            |  /xxxxy|      \_
206        #  0---|-----|-oxxxxxx|        o----------1
207        #    axisMin | lower  |        upper
208        #            |        |
209        #          axisDef  axisMax
210        #
211        else:
212            loc1 = (max(axisDef, lower), peak, axisMax)
213            scalar1 = 1
214
215            loc2 = (peak, axisMax, axisMax)
216            scalar2 = outGain
217
218            out.append((scalar1 - gain, loc1))
219            # Don't add a dirac delta!
220            if peak < axisMax:
221                out.append((scalar2 - gain, loc2))
222
223    # Now, the negative side
224
225    # Case 1neg: Lower extends beyond axisMin: we chop. Simple.
226    #
227    #                     |   |peak
228    #  1..................|...|.o.................
229    #                     |   |/ \
230    #  gain...............|...+...\...............
231    #                     |x_/|    \
232    #                     |/  |     \
233    #                   _/|   |      \
234    #  0---------------o  |   |       o----------1
235    #              lower  |   |       upper
236    #                     |   |
237    #               axisMin   axisDef
238    #
239    if lower <= axisMin:
240        loc = (axisMin, axisMin, axisDef)
241        scalar = supportScalar({"tag": axisMin}, {"tag": tent})
242
243        out.append((scalar - gain, loc))
244
245    # Case 2neg: Lower is betwen axisMin and axisDef: we add two
246    # tents to keep it down all the way to eternity.
247    #
248    #      |               |peak
249    #  1...|...............|.o.................
250    #      |               |/ \
251    #  gain|...............+...\...............
252    #      |yxxxxxxxxxxxxx/|    \
253    #      |yyyyyyxxxxxxx/ |     \
254    #      |yyyyyyyyyyyx/  |      \
255    #  0---|-----------o   |       o----------1
256    #    axisMin    lower  |       upper
257    #                      |
258    #                    axisDef
259    #
260    else:
261        # A tent's peak cannot fall on axis default. Nudge it.
262        if lower == axisDef:
263            lower -= EPSILON
264
265        # Downslope.
266        loc1 = (axisMin, lower, axisDef)
267        scalar1 = 0
268
269        # Eternity justify.
270        loc2 = (axisMin, axisMin, lower)
271        scalar2 = 0
272
273        out.append((scalar1 - gain, loc1))
274        out.append((scalar2 - gain, loc2))
275
276    return out
277
278
279@lru_cache(128)
280def rebaseTent(tent, axisLimit):
281    """Given a tuple (lower,peak,upper) "tent" and new axis limits
282    (axisMin,axisDefault,axisMax), solves how to represent the tent
283    under the new axis configuration.  All values are in normalized
284    -1,0,+1 coordinate system. Tent values can be outside this range.
285
286    Return value is a list of tuples. Each tuple is of the form
287    (scalar,tent), where scalar is a multipler to multiply any
288    delta-sets by, and tent is a new tent for that output delta-set.
289    If tent value is None, that is a special deltaset that should
290    be always-enabled (called "gain")."""
291
292    axisMin, axisDef, axisMax, _distanceNegative, _distancePositive = axisLimit
293    assert -1 <= axisMin <= axisDef <= axisMax <= +1
294
295    lower, peak, upper = tent
296    assert -2 <= lower <= peak <= upper <= +2
297
298    assert peak != 0
299
300    sols = _solve(tent, axisLimit)
301
302    n = lambda v: axisLimit.renormalizeValue(v)
303    sols = [
304        (scalar, (n(v[0]), n(v[1]), n(v[2])) if v is not None else None)
305        for scalar, v in sols
306        if scalar
307    ]
308
309    return sols
310