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