solver.py 11 KB

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