ranger-compiler 3.5.0 → 3.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (338) hide show
  1. package/CHANGELOG.md +1160 -19
  2. package/LICENSE +3 -1
  3. package/README.md +124 -29
  4. package/dist/Lang.rgr +1188 -290
  5. package/dist/api.d.ts +2343 -879
  6. package/dist/api.js +79924 -56922
  7. package/dist/lib/JSON.rgr +102 -91
  8. package/dist/lib/Shell.rgr +4 -4
  9. package/dist/lib/apple/AppleToolchain.rgr +4 -4
  10. package/dist/lib/apple/README.md +5 -4
  11. package/dist/lib/apple/apple_test.rgr +1 -1
  12. package/dist/lib/core/README.md +1 -1
  13. package/dist/lib/evg/EVG.rgr +12 -0
  14. package/dist/lib/evg/EVGA11yFromTree.rgr +302 -0
  15. package/dist/lib/evg/EVGA11yTree.rgr +894 -0
  16. package/dist/lib/evg/EVGBox.rgr +267 -0
  17. package/dist/lib/evg/EVGBoxShorthandTest.rgr +220 -0
  18. package/dist/lib/evg/EVGCodepoint.rgr +316 -0
  19. package/dist/lib/evg/EVGColor.rgr +700 -0
  20. package/dist/lib/evg/EVGCommands.rgr +177 -0
  21. package/dist/lib/evg/EVGComponent.rgr +331 -0
  22. package/dist/lib/evg/EVGComponentTest.rgr +387 -0
  23. package/dist/lib/evg/EVGConnector.rgr +541 -0
  24. package/dist/lib/evg/EVGConnectorTest.rgr +306 -0
  25. package/dist/lib/evg/EVGDisplayList.rgr +4186 -0
  26. package/dist/lib/evg/EVGEasing.rgr +370 -0
  27. package/dist/lib/evg/EVGEffectTest.rgr +253 -0
  28. package/dist/lib/evg/EVGElement.rgr +4914 -0
  29. package/dist/lib/evg/EVGFixedTest.rgr +289 -0
  30. package/dist/lib/evg/EVGFlexRulesTest.rgr +317 -0
  31. package/dist/lib/evg/EVGFlexWrapTest.rgr +246 -0
  32. package/dist/lib/evg/EVGFling.rgr +244 -0
  33. package/dist/lib/evg/EVGFocus.rgr +400 -0
  34. package/dist/lib/evg/EVGFocusTest.rgr +399 -0
  35. package/dist/lib/evg/EVGGradient.rgr +322 -0
  36. package/dist/lib/evg/EVGGrapheme.rgr +190 -0
  37. package/dist/lib/evg/EVGGrid.rgr +975 -0
  38. package/dist/lib/evg/EVGHitTest.rgr +208 -0
  39. package/dist/lib/evg/EVGHoles.rgr +294 -0
  40. package/dist/lib/evg/EVGHostMeasurerTest.rgr +281 -0
  41. package/dist/lib/evg/EVGHostTextMeasurer.rgr +331 -0
  42. package/dist/lib/evg/EVGHostTree.rgr +834 -0
  43. package/dist/lib/evg/EVGHostTreeTest.rgr +447 -0
  44. package/dist/lib/evg/EVGImageDecode.rgr +117 -0
  45. package/dist/lib/evg/EVGImageMeasurer.rgr +86 -0
  46. package/dist/lib/evg/EVGInspect.rgr +883 -0
  47. package/dist/lib/evg/EVGInvalidateTest.rgr +432 -0
  48. package/dist/lib/evg/EVGJsonTest.rgr +535 -0
  49. package/dist/lib/evg/EVGLayout.rgr +4220 -0
  50. package/dist/lib/evg/EVGMeasure.rgr +1267 -0
  51. package/dist/lib/evg/EVGOverlayTest.rgr +626 -0
  52. package/dist/lib/evg/EVGPatch.rgr +1721 -0
  53. package/dist/lib/evg/EVGPatchTest.rgr +692 -0
  54. package/dist/lib/evg/EVGPopoverTest.rgr +395 -0
  55. package/dist/lib/evg/EVGReconcile.rgr +226 -0
  56. package/dist/lib/evg/EVGReconcileTest.rgr +421 -0
  57. package/dist/lib/evg/EVGReject.rgr +126 -0
  58. package/dist/lib/evg/EVGRelayoutTest.rgr +375 -0
  59. package/dist/lib/evg/EVGRtlLayoutTest.rgr +319 -0
  60. package/dist/lib/evg/EVGRuler.rgr +232 -0
  61. package/dist/lib/evg/EVGRulerTest.rgr +172 -0
  62. package/dist/lib/evg/EVGSelectChrome.rgr +178 -0
  63. package/dist/lib/evg/EVGStyleCacheTest.rgr +419 -0
  64. package/dist/lib/evg/EVGStyleSheet.rgr +2148 -0
  65. package/dist/lib/evg/EVGStyleStateTest.rgr +407 -0
  66. package/dist/lib/evg/EVGStyleVarTest.rgr +471 -0
  67. package/dist/lib/evg/EVGText.rgr +105 -0
  68. package/dist/lib/evg/EVGTextEngine.rgr +536 -0
  69. package/dist/lib/evg/EVGTextMeasurer.rgr +720 -0
  70. package/dist/lib/evg/EVGTimingTest.rgr +1417 -0
  71. package/dist/lib/evg/EVGToolbar.rgr +1369 -0
  72. package/dist/lib/evg/EVGTransition.rgr +728 -0
  73. package/dist/lib/evg/EVGTreeJson.rgr +721 -0
  74. package/dist/lib/evg/EVGUnit.rgr +554 -0
  75. package/dist/lib/evg/EVGViewportUnitTest.rgr +236 -0
  76. package/dist/lib/evg/EvgApp.rgr +189 -0
  77. package/dist/lib/evg/EvgBitmapTracer.rgr +4277 -0
  78. package/dist/lib/evg/EvgBitmapTracerTest.rgr +2119 -0
  79. package/dist/lib/evg/EvgHost.rgr +451 -0
  80. package/dist/lib/evg/EvgTest.rgr +91 -0
  81. package/dist/lib/evg/EvgTraceColor.rgr +236 -0
  82. package/dist/lib/evg/EvgTraceCurve.rgr +648 -0
  83. package/dist/lib/evg/EvgTraceFit.rgr +767 -0
  84. package/dist/lib/evg/EvgTracePath.rgr +435 -0
  85. package/dist/lib/evg/EvgTraceTypes.rgr +559 -0
  86. package/dist/lib/evg/EvgViewport.rgr +313 -0
  87. package/dist/lib/evg/FxDemoDoc.rgr +185 -0
  88. package/dist/lib/evg/HOSTS.md +227 -0
  89. package/dist/lib/evg/ISSUES.md +835 -0
  90. package/dist/lib/evg/PLAN_ACCESSIBILITY.md +650 -0
  91. package/dist/lib/evg/PLAN_CSS_LAYOUT_AND_FONTS.md +1124 -0
  92. package/dist/lib/evg/PLAN_EFFECTS.md +431 -0
  93. package/dist/lib/evg/PLAN_EVG.md +518 -0
  94. package/dist/lib/evg/PLAN_EVG_RENDERER.md +1674 -0
  95. package/dist/lib/evg/PLAN_INSPECTOR.md +788 -0
  96. package/dist/lib/evg/PLAN_LINKS_AND_FORMS.md +157 -0
  97. package/dist/lib/evg/PLAN_NATIVE_HOSTS.md +671 -0
  98. package/dist/lib/evg/PLAN_VECTOR_IR.md +664 -0
  99. package/dist/lib/evg/PLAN_VIEW_TRANSFORM.md +322 -0
  100. package/dist/lib/evg/PathBuilder.rgr +333 -0
  101. package/dist/lib/evg/README.md +1236 -0
  102. package/dist/lib/evg/SPEC.md +1252 -0
  103. package/dist/lib/evg/SVGPathParser.rgr +1199 -0
  104. package/dist/lib/evg/SvgParser.rgr +1736 -0
  105. package/dist/lib/evg/TODO_EVG_RENDERER.md +595 -0
  106. package/dist/lib/evg/VectorShapes.rgr +331 -0
  107. package/dist/lib/evg/VectorStroke.rgr +107 -0
  108. package/dist/lib/evg/VectorViewBox.rgr +380 -0
  109. package/dist/lib/evg/agent/README.md +290 -0
  110. package/dist/lib/evg/agent/evg_agent.rgr +899 -0
  111. package/dist/lib/evg/agent/fixtures/broken.evg.json +7 -0
  112. package/dist/lib/evg/agent/fixtures/card.evg.json +9 -0
  113. package/dist/lib/evg/agent/fixtures/connector.css +63 -0
  114. package/dist/lib/evg/agent/fixtures/connector.evg.json +14 -0
  115. package/dist/lib/evg/agent/fixtures/drawn.evg.json +129 -0
  116. package/dist/lib/evg/agent/fixtures/gradient.evg.json +4 -0
  117. package/dist/lib/evg/agent/fixtures/popover.css +99 -0
  118. package/dist/lib/evg/agent/fixtures/popover.evg.json +30 -0
  119. package/dist/lib/evg/agent/roundtrip.sh +74 -0
  120. package/dist/lib/evg/agent/smoke.sh +230 -0
  121. package/dist/lib/evg/android/README.md +97 -0
  122. package/dist/lib/evg/android/androidstubs/AndroidStubs.kt +175 -0
  123. package/dist/lib/evg/android/androidstubs/Annotation.kt +10 -0
  124. package/dist/lib/evg/android/androidstubs/App.kt +30 -0
  125. package/dist/lib/evg/android/androidstubs/Content.kt +41 -0
  126. package/dist/lib/evg/android/androidstubs/ContentRes.kt +12 -0
  127. package/dist/lib/evg/android/androidstubs/InputMethod.kt +46 -0
  128. package/dist/lib/evg/android/androidstubs/Net.kt +6 -0
  129. package/dist/lib/evg/android/androidstubs/Os.kt +30 -0
  130. package/dist/lib/evg/android/androidstubs/Util.kt +15 -0
  131. package/dist/lib/evg/android/androidstubs/View.kt +113 -0
  132. package/dist/lib/evg/android/androidstubs/Widget.kt +16 -0
  133. package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidEvgSurface.kt +323 -0
  134. package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidTextMeasurer.kt +56 -0
  135. package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/RippleEffect.kt +262 -0
  136. package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtEvgSurface.kt +238 -0
  137. package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtTextMeasurer.kt +85 -0
  138. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgEngineThread.kt +115 -0
  139. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgPainter.kt +231 -0
  140. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgSurface.kt +117 -0
  141. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/RecordingSurface.kt +94 -0
  142. package/dist/lib/evg/apple/README.md +128 -0
  143. package/dist/lib/evg/apple/Sources/CoreGraphicsEvgSurface.swift +329 -0
  144. package/dist/lib/evg/apple/Sources/CoreTextMeasurer.swift +70 -0
  145. package/dist/lib/evg/apple/Sources/EvgEngineQueue.swift +105 -0
  146. package/dist/lib/evg/apple/Sources/EvgPainter.swift +225 -0
  147. package/dist/lib/evg/apple/Sources/EvgSurface.swift +163 -0
  148. package/dist/lib/evg/apple/Sources/RecordingSurface.swift +110 -0
  149. package/dist/lib/evg/bench/EvgLayoutBench.rgr +34 -0
  150. package/dist/lib/evg/bench/README.md +64 -0
  151. package/dist/lib/evg/bench/layout-bench.mjs +316 -0
  152. package/dist/lib/evg/bench/layout-cases.mjs +510 -0
  153. package/dist/lib/evg/bench/layout-conformance.mjs +254 -0
  154. package/dist/lib/evg/bin/.gitignore +17 -0
  155. package/dist/lib/evg/evg_test.rgr +313 -0
  156. package/dist/lib/evg/gl/README.md +198 -0
  157. package/dist/lib/evg/gl/a11y-paint-check.mjs +73 -0
  158. package/dist/lib/evg/gl/blur-check.mjs +399 -0
  159. package/dist/lib/evg/gl/boxmodel.json +1 -0
  160. package/dist/lib/evg/gl/demo.html +46 -0
  161. package/dist/lib/evg/gl/effect-presets.css +285 -0
  162. package/dist/lib/evg/gl/effect-presets.js +63 -0
  163. package/dist/lib/evg/gl/effect-shots.mjs +135 -0
  164. package/dist/lib/evg/gl/evg-a11y.js +566 -0
  165. package/dist/lib/evg/gl/evg-binary.js +160 -0
  166. package/dist/lib/evg/gl/evg-engine.js +296 -0
  167. package/dist/lib/evg/gl/evg-fx.js +237 -0
  168. package/dist/lib/evg/gl/evg-gestures.js +209 -0
  169. package/dist/lib/evg/gl/evg-list.js +167 -0
  170. package/dist/lib/evg/gl/evg-measure.js +186 -0
  171. package/dist/lib/evg/gl/evg-textinput.js +303 -0
  172. package/dist/lib/evg/gl/evg-view.js +144 -0
  173. package/dist/lib/evg/gl/evg-webgl.js +3942 -0
  174. package/dist/lib/evg/gl/fx-check.mjs +998 -0
  175. package/dist/lib/evg/gl/fx-demo.html +166 -0
  176. package/dist/lib/evg/gl/fx-demo.js +2 -0
  177. package/dist/lib/evg/gl/fx-serve.mjs +47 -0
  178. package/dist/lib/evg/gl/gestures-check.mjs +189 -0
  179. package/dist/lib/evg/gl/list-binary-check.mjs +152 -0
  180. package/dist/lib/evg/gl/measure-check.mjs +135 -0
  181. package/dist/lib/evg/gl/rotation-check.mjs +207 -0
  182. package/dist/lib/evg/gl/shift-check.mjs +113 -0
  183. package/dist/lib/evg/gl/stroke-check.mjs +168 -0
  184. package/dist/lib/evg/gl/text-snap-check.mjs +139 -0
  185. package/dist/lib/evg/gl/view-check.mjs +343 -0
  186. package/dist/lib/evg/gl/view-policy-check.mjs +184 -0
  187. package/dist/lib/evg/html/evg-dom.js +318 -0
  188. package/dist/lib/evg/html/evg-html.js +600 -0
  189. package/dist/lib/evg/inspect/README.md +415 -0
  190. package/dist/lib/evg/inspect/browser-smoke.mjs +115 -0
  191. package/dist/lib/evg/inspect/evg-inspect.js +947 -0
  192. package/dist/lib/evg/inspect/shots/css.png +0 -0
  193. package/dist/lib/evg/inspect/shots/dashboard.png +0 -0
  194. package/dist/lib/evg/inspect/shots/pptx-slide.png +0 -0
  195. package/dist/lib/evg/inspect/shots/state.png +0 -0
  196. package/dist/lib/evg/inspect/shots.mjs +226 -0
  197. package/dist/lib/evg/oracle/css-blur.json +576 -0
  198. package/dist/lib/evg/oracle/css-box.json +157 -0
  199. package/dist/lib/evg/oracle/css-timing.json +709 -0
  200. package/dist/lib/evg/oracle/css_blur_oracle.mjs +529 -0
  201. package/dist/lib/evg/oracle/css_box_oracle.mjs +106 -0
  202. package/dist/lib/evg/oracle/css_timing_oracle.mjs +389 -0
  203. package/dist/lib/evg/original/EVGColor.clj +310 -0
  204. package/dist/lib/evg/original/EVGColorContext.rgr +178 -0
  205. package/dist/lib/evg/original/SVGPath.rgr +627 -0
  206. package/dist/lib/evg/original/Vec2.crgr +103 -0
  207. package/dist/lib/evg/ranger.json +9 -0
  208. package/dist/lib/evg/showcase/README.md +246 -0
  209. package/dist/lib/evg/showcase/assets/emblem.svg +30 -0
  210. package/dist/lib/evg/showcase/assets/rosette.svg +22 -0
  211. package/dist/lib/evg/showcase/build.mjs +651 -0
  212. package/dist/lib/evg/showcase/pages/album.tsx +30 -0
  213. package/dist/lib/evg/showcase/pages/boxmodel.tsx +37 -0
  214. package/dist/lib/evg/showcase/pages/cards.tsx +41 -0
  215. package/dist/lib/evg/showcase/pages/chart_api.tsx +170 -0
  216. package/dist/lib/evg/showcase/pages/charts.tsx +219 -0
  217. package/dist/lib/evg/showcase/pages/drawing.tsx +160 -0
  218. package/dist/lib/evg/showcase/pages/emoji.tsx +91 -0
  219. package/dist/lib/evg/showcase/pages/flex.tsx +46 -0
  220. package/dist/lib/evg/showcase/pages/more.tsx +332 -0
  221. package/dist/lib/evg/showcase/pages/plots.tsx +262 -0
  222. package/dist/lib/evg/showcase/pages/svg.tsx +63 -0
  223. package/dist/lib/evg/showcase/pages/tables.tsx +209 -0
  224. package/dist/lib/evg/showcase/pages/typography.tsx +43 -0
  225. package/dist/lib/evg/showcase/pages/units.tsx +35 -0
  226. package/dist/lib/evg/showcase/pages/variants.tsx +233 -0
  227. package/dist/lib/evg/showcase/pages/vector.tsx +75 -0
  228. package/dist/lib/evg/showcase/pages/views.tsx +223 -0
  229. package/dist/lib/evg/showcase/tests/chart_api_smoke.mjs +339 -0
  230. package/dist/lib/evg/showcase/tests/gl_smoke.mjs +125 -0
  231. package/dist/lib/evg/showcase/themes/chart_api-default.css +15 -0
  232. package/dist/lib/evg/showcase/themes/charts-default.css +29 -0
  233. package/dist/lib/evg/showcase/themes/drawing-default.css +35 -0
  234. package/dist/lib/evg/showcase/themes/more-default.css +128 -0
  235. package/dist/lib/evg/showcase/themes/plots-default.css +18 -0
  236. package/dist/lib/evg/showcase/themes/showcase.css +508 -0
  237. package/dist/lib/evg/showcase/themes/tables-default.css +123 -0
  238. package/dist/lib/evg/showcase/themes/variants-default.css +15 -0
  239. package/dist/lib/evg/showcase/themes/views-default.css +15 -0
  240. package/dist/lib/evg/tools/bench_vs_potrace.mjs +290 -0
  241. package/dist/lib/evg/tools/evg_image_tool.rgr +488 -0
  242. package/dist/lib/evg/tools/evg_trace_bench.rgr +114 -0
  243. package/dist/lib/evg/tools/evg_trace_cli.rgr +662 -0
  244. package/dist/lib/evg/tools/evg_trace_cpp_bench.rgr +83 -0
  245. package/dist/lib/evg/tools/run_trace_bench.sh +74 -0
  246. package/dist/lib/evg/tools/run_trace_cli_smoke.sh +128 -0
  247. package/dist/lib/evg/web/responsive/EvgResponsiveCheck.rgr +271 -0
  248. package/dist/lib/evg/web/responsive/EvgResponsiveDemo.rgr +531 -0
  249. package/dist/lib/evg/web/responsive/README.md +97 -0
  250. package/dist/lib/evg/web/responsive/build.mjs +90 -0
  251. package/dist/lib/evg/web/responsive/dom-check.mjs +169 -0
  252. package/dist/lib/evg/web/responsive/index.html +177 -0
  253. package/dist/lib/evg/web/responsive/smoke.mjs +221 -0
  254. package/dist/lib/evg/web/tools/assets-client.mjs +95 -0
  255. package/dist/lib/evg/web/tools/boot-bench.mjs +189 -0
  256. package/dist/lib/evg/web/tools/inline-assets.mjs +119 -0
  257. package/dist/lib/evg/web/tools/minify.mjs +60 -0
  258. package/dist/lib/evg/web/tracer/build.mjs +87 -0
  259. package/dist/lib/evg/web/tracer/index.html +2805 -0
  260. package/dist/lib/evg/web/tracer/sample.png +0 -0
  261. package/dist/lib/evg/web/tracer/smoke.mjs +1162 -0
  262. package/dist/lib/evgr/Cargo.lock +21 -0
  263. package/dist/lib/evgr/Cargo.toml +19 -0
  264. package/dist/lib/evgr/README.md +140 -0
  265. package/dist/lib/evgr/bench/NativeBench.rgr +246 -0
  266. package/dist/lib/evgr/bench/compare.mjs +252 -0
  267. package/dist/lib/evgr/bench/speed.mjs +277 -0
  268. package/dist/lib/evgr/bin/.gitignore +3 -0
  269. package/dist/lib/evgr/src/bin/bench.rs +106 -0
  270. package/dist/lib/evgr/src/bin/smoke.rs +13 -0
  271. package/dist/lib/evgr/src/grid.rs +730 -0
  272. package/dist/lib/evgr/src/lib.rs +1003 -0
  273. package/dist/lib/evgr/src/style.rs +468 -0
  274. package/dist/lib/evgr/src/text.rs +83 -0
  275. package/dist/lib/image/BitReader.rgr +171 -0
  276. package/dist/lib/image/Buffer.rgr +173 -0
  277. package/dist/lib/image/DCT.rgr +283 -0
  278. package/dist/lib/image/Deflate.rgr +339 -0
  279. package/dist/lib/image/HuffmanDecoder.rgr +181 -0
  280. package/dist/lib/image/ImageBuffer.rgr +706 -0
  281. package/dist/lib/image/JPEGDecoder.rgr +808 -0
  282. package/dist/lib/image/PNGDecoder.rgr +544 -0
  283. package/dist/lib/image/PNGEncoder.rgr +388 -0
  284. package/dist/lib/image/PPMImage.rgr +224 -0
  285. package/dist/lib/image/ProgressiveJPEGDecoder.rgr +1154 -0
  286. package/dist/lib/image/README.md +33 -0
  287. package/dist/lib/image/RasterBuffer.rgr +294 -0
  288. package/dist/lib/image/VP8BoolDecoder.rgr +163 -0
  289. package/dist/lib/image/WebPDecoder.rgr +433 -0
  290. package/dist/lib/image/WebPLossless.rgr +1035 -0
  291. package/dist/lib/image/WebPLossy.rgr +1929 -0
  292. package/dist/lib/image/ranger.json +9 -0
  293. package/dist/lib/image/testdata/webp/anim_2f_24x16.webp +0 -0
  294. package/dist/lib/image/testdata/webp/fixtures.txt +33 -0
  295. package/dist/lib/image/testdata/webp/gen_fixtures.py +261 -0
  296. package/dist/lib/image/testdata/webp/ll_1x1.webp +0 -0
  297. package/dist/lib/image/testdata/webp/ll_alpha_29x21.webp +0 -0
  298. package/dist/lib/image/testdata/webp/ll_gradient_64x64.webp +0 -0
  299. package/dist/lib/image/testdata/webp/ll_meta_32x32.webp +0 -0
  300. package/dist/lib/image/testdata/webp/ll_noise_17x9.webp +0 -0
  301. package/dist/lib/image/testdata/webp/ll_pal11_23x11.webp +0 -0
  302. package/dist/lib/image/testdata/webp/ll_pal2_19x7.webp +0 -0
  303. package/dist/lib/image/testdata/webp/ll_pal40_16x16.webp +0 -0
  304. package/dist/lib/image/testdata/webp/ll_pal4_13x5.webp +0 -0
  305. package/dist/lib/image/testdata/webp/ll_photo_40x30.webp +0 -0
  306. package/dist/lib/image/testdata/webp/ll_predictors_64x32.webp +0 -0
  307. package/dist/lib/image/testdata/webp/ly_1x1.webp +0 -0
  308. package/dist/lib/image/testdata/webp/ly_alph_raw_f0_21x13.webp +0 -0
  309. package/dist/lib/image/testdata/webp/ly_alph_raw_f1_21x13.webp +0 -0
  310. package/dist/lib/image/testdata/webp/ly_alph_raw_f2_21x13.webp +0 -0
  311. package/dist/lib/image/testdata/webp/ly_alph_raw_f3_21x13.webp +0 -0
  312. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f0_21x13.webp +0 -0
  313. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f1_21x13.webp +0 -0
  314. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f2_21x13.webp +0 -0
  315. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f3_21x13.webp +0 -0
  316. package/dist/lib/image/testdata/webp/ly_alpha_33x17.webp +0 -0
  317. package/dist/lib/image/testdata/webp/ly_exif_15x11.webp +0 -0
  318. package/dist/lib/image/testdata/webp/ly_i16_96x80.webp +0 -0
  319. package/dist/lib/image/testdata/webp/ly_nofilter_24x20.webp +0 -0
  320. package/dist/lib/image/testdata/webp/ly_odd_17x9.webp +0 -0
  321. package/dist/lib/image/testdata/webp/ly_photo_48x40.webp +0 -0
  322. package/dist/lib/image/testdata/webp/ly_q100_20x18.webp +0 -0
  323. package/dist/lib/image/testdata/webp/ly_sharp_40x36.webp +0 -0
  324. package/dist/lib/image/testdata/webp/ly_simple_filter_33x31.webp +0 -0
  325. package/dist/lib/image/testdata/webp/ly_vpx_parts8_37x45.webp +0 -0
  326. package/dist/lib/image/testdata/webp/ly_vpx_skip_96x80.webp +0 -0
  327. package/dist/lib/image/tests/WebPDecodeTool.rgr +81 -0
  328. package/dist/lib/image/tests/WebPDecoderTest.rgr +1162 -0
  329. package/dist/lib/image/tests/run_webp_tests.sh +39 -0
  330. package/dist/lib/rust/RsJson.rgr +468 -0
  331. package/dist/lib/rust/RsPrelude.rgr +1663 -0
  332. package/dist/lib/shell_test.rgr +7 -7
  333. package/dist/lib/stdlib.rgr +21 -5
  334. package/dist/lib/zip/ranger.json +6 -0
  335. package/dist/rgrc.js +87318 -67610
  336. package/package.json +1010 -1040
  337. package/dist/README.md +0 -117
  338. package/dist/package.json +0 -47
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: MIT
3
+ //
4
+ // A run of text is drawn on whole device pixels.
5
+ //
6
+ // node lib/evg/gl/text-snap-check.mjs
7
+ //
8
+ // The painter rasterises every run into an atlas at the page's device ratio and
9
+ // draws it as a textured quad, sampled linearly. Layout puts text at fractional
10
+ // positions all the time (a centred label, a half-leading, a flex share), and a
11
+ // quad whose corner is a fraction of a pixel off the grid samples each glyph
12
+ // between two texels: the whole run comes out resampled, a little soft next to
13
+ // the same words in the page's own DOM. That was reported by eye on the EVGUI
14
+ // pages as "a slight blurriness in the EVG rendered font".
15
+ //
16
+ // The rule, checked on the framebuffer: the same run drawn at x = 30 and at
17
+ // x = 30.37 (and the same for y, and at a device ratio of 2) differ only by a
18
+ // whole-pixel move. A resampled run matches the integer one under no shift at
19
+ // all, because every edge pixel changes value.
20
+ //
21
+ // Exit code 0 when every fractional placement is a whole-pixel copy.
22
+
23
+ import fs from "node:fs";
24
+ import path from "node:path";
25
+ import { fileURLToPath, pathToFileURL } from "node:url";
26
+ import { chromium } from "playwright-core";
27
+ import { findChromium } from "../../../gallery/ui/conformance/dom-adapter.mjs";
28
+
29
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
30
+ const W = 240;
31
+ const H = 80;
32
+
33
+ let failed = 0;
34
+ const ok = (name, cond, detail) => {
35
+ if (cond) console.log(" PASS " + name);
36
+ else {
37
+ failed += 1;
38
+ console.log(" FAIL " + name + (detail === undefined ? "" : " — " + detail));
39
+ }
40
+ };
41
+
42
+ const scene = (x, y) => [
43
+ { k: 0, x: 0, y: 0, w: W, h: H, c: [255, 255, 255, 1] },
44
+ { k: 3, x, y, w: 200, h: 20, text: "Set the dimensions 100%", font: "", size: 14, c: [9, 9, 11, 1] },
45
+ ];
46
+
47
+ const painter = fs.readFileSync(path.join(HERE, "evg-webgl.js"), "utf8").replace(/^export /gm, "");
48
+
49
+ const pageFor = (cmds, dpr) => `<!doctype html><meta charset="utf-8">
50
+ <body style="margin:0;background:#fff"><canvas id="c" width="${W * dpr}" height="${H * dpr}"></canvas>
51
+ <script type="module">
52
+ ${painter}
53
+ try {
54
+ const gl = document.getElementById("c").getContext("webgl2", { antialias: true, preserveDrawingBuffer: true });
55
+ const doc = { width: ${W}, height: ${H}, list: { cmds: ${JSON.stringify(cmds)} } };
56
+ const frame = prepareDisplayList(gl, doc, { dpr: ${dpr} });
57
+ frame.draw(null, null);
58
+ if (gl.finish) gl.finish();
59
+ window.__DONE__ = true;
60
+ } catch (e) { window.__ERR__ = String((e && e.stack) || e); window.__DONE__ = true; }
61
+ </script></body>`;
62
+
63
+ const browser = await chromium.launch({
64
+ executablePath: findChromium(),
65
+ args: ["--use-gl=swiftshader", "--enable-unsafe-swiftshader"],
66
+ });
67
+ const page = await browser.newPage({ viewport: { width: W * 2 + 40, height: H * 2 + 40 } });
68
+ const tmp = path.join(HERE, ".text-snap-check.html");
69
+ let loadSeq = 0;
70
+
71
+ /** The framebuffer as luminance, one byte a pixel, rows top-down. */
72
+ async function lumaOf(cmds, dpr) {
73
+ fs.writeFileSync(tmp, pageFor(cmds, dpr));
74
+ loadSeq += 1;
75
+ await page.goto(pathToFileURL(tmp).href + "?n=" + loadSeq, { waitUntil: "load" });
76
+ await page.waitForFunction("window.__DONE__ === true", { timeout: 30000 });
77
+ const err = await page.evaluate(() => window.__ERR__ || null);
78
+ if (err) throw new Error("painter threw: " + err);
79
+ const [ww, hh] = [W * dpr, H * dpr];
80
+ const px = await page.evaluate(([ww, hh]) => {
81
+ const gl = document.getElementById("c").getContext("webgl2");
82
+ const out = new Uint8Array(ww * hh * 4);
83
+ gl.readPixels(0, 0, ww, hh, gl.RGBA, gl.UNSIGNED_BYTE, out);
84
+ return Array.from(out);
85
+ }, [ww, hh]);
86
+ const l = new Uint8Array(ww * hh);
87
+ for (let y = 0; y < hh; y += 1) {
88
+ for (let x = 0; x < ww; x += 1) {
89
+ const i = ((hh - 1 - y) * ww + x) * 4; // GL rows are bottom-up
90
+ l[y * ww + x] = Math.round(0.299 * px[i] + 0.587 * px[i + 1] + 0.114 * px[i + 2]);
91
+ }
92
+ }
93
+ return { l, ww, hh };
94
+ }
95
+
96
+ /** The worst pixel difference between a and b moved by (dx, dy), over the inside. */
97
+ function worst(a, b, dx, dy) {
98
+ let m = 0;
99
+ for (let y = 4; y < a.hh - 4; y += 1) {
100
+ for (let x = 4; x < a.ww - 4; x += 1) {
101
+ const d = Math.abs(a.l[y * a.ww + x] - b.l[(y + dy) * b.ww + (x + dx)]);
102
+ if (d > m) m = d;
103
+ }
104
+ }
105
+ return m;
106
+ }
107
+
108
+ /** The whole-pixel move that makes b equal a, if there is one. */
109
+ function wholePixelMatch(a, b) {
110
+ let best = { d: 256, dx: 0, dy: 0 };
111
+ for (let dy = -2; dy <= 2; dy += 1) {
112
+ for (let dx = -2; dx <= 2; dx += 1) {
113
+ const d = worst(a, b, dx, dy);
114
+ if (d < best.d) best = { d, dx, dy };
115
+ }
116
+ }
117
+ return best;
118
+ }
119
+
120
+ for (const dpr of [1, 2]) {
121
+ console.log(`--- device ratio ${dpr} ---`);
122
+ const base = await lumaOf(scene(30, 30), dpr);
123
+ let ink = 0;
124
+ for (const v of base.l) if (v < 128) ink += 1;
125
+ ok("the run draws ink", ink > 50, ink + " dark pixels");
126
+ for (const [fx, fy] of [[30.37, 30], [30, 30.41], [30.5, 30.5], [29.77, 30.23]]) {
127
+ const moved = await lumaOf(scene(fx, fy), dpr);
128
+ const m = wholePixelMatch(base, moved);
129
+ // 2 levels of slack for the rasteriser's own rounding; a resampled run
130
+ // is tens of levels off along every edge.
131
+ ok(`at (${fx}, ${fy}) it is the same pixels, moved ${m.dx},${m.dy}`, m.d <= 2,
132
+ "worst difference " + m.d + " under the best whole-pixel move");
133
+ }
134
+ }
135
+
136
+ await browser.close();
137
+ fs.rmSync(tmp, { force: true });
138
+ console.log(failed ? `\nRESULT FAIL — ${failed}` : "\nALL PASS");
139
+ process.exit(failed ? 1 : 0);
@@ -0,0 +1,343 @@
1
+ /**
2
+ * The camera, against the coordinates it replaces.
3
+ *
4
+ * node lib/evg/gl/view-check.mjs [--write-png prefix]
5
+ *
6
+ * A display list has always been written in the coordinates it is drawn at,
7
+ * so a host that pans or zooms multiplies the scene and hands over a new
8
+ * list. A view does the multiplying in the shader instead, which is what lets
9
+ * a frame already built be drawn somewhere else (PLAN_VIEW_TRANSFORM.md).
10
+ *
11
+ * The claim is that the two are the SAME PICTURE, and it is a claim about
12
+ * pixels: a rounded corner whose radius did not scale, a border that stayed
13
+ * one pixel while its box doubled, a clip that scissored the unpanned
14
+ * rectangle — every one of those is a plausible drawing that is wrong, and
15
+ * none of them changes a command count. So this bakes a view into a list the
16
+ * old way, draws the same list with the view on the camera instead, and reads
17
+ * both framebuffers back.
18
+ *
19
+ * WHAT IS DELIBERATELY NOT COMPARED PIXEL FOR PIXEL. Text. A run is
20
+ * rasterised into an atlas at its size, so a baked list rasterises at the
21
+ * scaled size and a camera stretches the unscaled one — the §4 caveat, and
22
+ * the whole reason a frame may only be stretched within a band before it is
23
+ * built again. The text scene therefore compares where the ink LANDS, which
24
+ * is the thing a camera must still get right.
25
+ *
26
+ * SwiftShader, so this runs on a machine with no GPU. Exit 0 when every scene
27
+ * agrees.
28
+ */
29
+
30
+ import fs from "node:fs";
31
+ import path from "node:path";
32
+ import { fileURLToPath, pathToFileURL } from "node:url";
33
+ import { findChromium } from "../../../gallery/ui/conformance/dom-adapter.mjs";
34
+ import { chromium } from "playwright-core";
35
+
36
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
37
+ const W = 400;
38
+ const H = 240;
39
+
40
+ let passed = 0;
41
+ let failed = 0;
42
+ const ok = (name, cond, detail) => {
43
+ if (cond) {
44
+ passed += 1;
45
+ console.log(" PASS " + name + (detail ? " (" + detail + ")" : ""));
46
+ } else {
47
+ failed += 1;
48
+ console.log(" FAIL " + name + (detail ? " (" + detail + ")" : ""));
49
+ }
50
+ };
51
+
52
+ // --- the scenes -------------------------------------------------------------
53
+ //
54
+ // Every one of them is in SCENE coordinates. `bake` below is what a host does
55
+ // today; the camera is what it would do instead.
56
+
57
+ const SCENES = {
58
+ // Solid, rounded, bordered, and a gradient — the four things the rect
59
+ // program does, and the three of them whose size feeds the distance field.
60
+ boxes: [
61
+ { k: 0, x: 0, y: 0, w: W, h: H, c: [250, 250, 252, 1] },
62
+ { k: 0, x: 20, y: 20, w: 120, h: 60, c: [40, 90, 200, 1] },
63
+ { k: 0, x: 160, y: 20, w: 120, h: 60, r: 18, c: [220, 60, 80, 1] },
64
+ { k: 0, x: 20, y: 100, w: 120, h: 60, r: 10, t: 6, c: [20, 140, 90, 1] },
65
+ { k: 0, x: 160, y: 100, w: 120, h: 60, c: [255, 200, 0, 1], gd: 0, c2: [0, 120, 255, 1] },
66
+ { k: 0, x: 300, y: 20, w: 60, h: 140, rc: [30, 4, 30, 4], c: [120, 60, 180, 1] },
67
+ ],
68
+ // A fill and a stroke, both as rings. The stroke's width scales with the
69
+ // picture, which is the choice §3 names.
70
+ paths: [
71
+ { k: 0, x: 0, y: 0, w: W, h: H, c: [255, 255, 255, 1] },
72
+ {
73
+ k: 6, x: 20, y: 20, w: 160, h: 160, c: [30, 30, 40, 1],
74
+ pts: [40, 40, 160, 40, 160, 120, 90, 170, 40, 120],
75
+ ends: [10],
76
+ },
77
+ {
78
+ k: 7, x: 200, y: 20, w: 180, h: 180, t: 8, c: [200, 40, 40, 1],
79
+ pts: [220, 40, 360, 40, 360, 180, 220, 180, 220, 40],
80
+ ends: [10],
81
+ },
82
+ ],
83
+ // A clip, with content that runs past it in both directions. A scissor is
84
+ // the one rectangle the shader cannot map, so it is the one most likely to
85
+ // be left in the coordinates the camera moved away from.
86
+ clipped: [
87
+ { k: 0, x: 0, y: 0, w: W, h: H, c: [245, 245, 245, 1] },
88
+ { k: 4, x: 80, y: 60, w: 140, h: 90, c: [0, 0, 0, 1] },
89
+ { k: 0, x: 40, y: 20, w: 300, h: 180, c: [0, 140, 160, 1] },
90
+ { k: 5, x: 0, y: 0, w: 0, h: 0, c: [0, 0, 0, 1] },
91
+ { k: 0, x: 250, y: 60, w: 60, h: 90, c: [160, 160, 160, 1] },
92
+ ],
93
+ // A rotated box: the pivot is scene geometry and the turn happens before
94
+ // the camera, so a scaled view must not shear it.
95
+ rotated: [
96
+ { k: 0, x: 0, y: 0, w: W, h: H, c: [255, 255, 255, 1] },
97
+ { k: 0, x: 120, y: 70, w: 160, h: 80, r: 12, c: [60, 60, 70, 1], rot: 20, rox: 200, roy: 110 },
98
+ ],
99
+ // A softened backdrop. The radius has to grow with the picture or it is a
100
+ // different blur at a different zoom.
101
+ blurred: [
102
+ { k: 0, x: 0, y: 0, w: W, h: H, c: [255, 255, 255, 1] },
103
+ { k: 0, x: 0, y: 0, w: 200, h: H, c: [10, 10, 10, 1] },
104
+ { k: 0, x: 120, y: 60, w: 160, h: 120, r: 24, c: [0, 0, 0, 0], bb: 8 },
105
+ ],
106
+ };
107
+
108
+ const TEXT_SCENE = [
109
+ { k: 0, x: 0, y: 0, w: W, h: H, c: [255, 255, 255, 1] },
110
+ { k: 3, x: 30, y: 60, w: 300, h: 24, text: "Handgloves 123", font: "", size: 24, c: [0, 0, 0, 1] },
111
+ ];
112
+
113
+ /** What a host does today: multiply the scene and hand over a new list. */
114
+ function bake(cmds, v) {
115
+ const s = v.scale, tx = v.x, ty = v.y;
116
+ return cmds.map((c) => {
117
+ const o = { ...c };
118
+ o.x = c.x * s + tx;
119
+ o.y = c.y * s + ty;
120
+ o.w = c.w * s;
121
+ o.h = c.h * s;
122
+ if (c.r) o.r = c.r * s;
123
+ if (c.rc) o.rc = c.rc.map((r) => r * s);
124
+ if (c.t) o.t = c.t * s;
125
+ if (c.bb) o.bb = c.bb * s;
126
+ if (c.size) o.size = c.size * s;
127
+ if (c.rox !== undefined) o.rox = c.rox * s + tx;
128
+ if (c.roy !== undefined) o.roy = c.roy * s + ty;
129
+ if (c.pts) o.pts = c.pts.map((p, i) => (i % 2 === 0 ? p * s + tx : p * s + ty));
130
+ return o;
131
+ });
132
+ }
133
+
134
+ const VIEWS = [
135
+ { name: "identity", x: 0, y: 0, scale: 1 },
136
+ { name: "a pan", x: 37, y: -22, scale: 1 },
137
+ { name: "zoomed in", x: -60, y: -30, scale: 1.35 },
138
+ { name: "zoomed out", x: 40, y: 25, scale: 0.7 },
139
+ { name: "pan and zoom", x: 18, y: 44, scale: 1.15 },
140
+ ];
141
+
142
+ // --- the page ---------------------------------------------------------------
143
+
144
+ const painter = fs.readFileSync(path.join(HERE, "evg-webgl.js"), "utf8")
145
+ .replace(/^export /gm, "");
146
+
147
+ const pageFor = (cmds, view) => `<!doctype html><meta charset="utf-8">
148
+ <body style="margin:0;background:#fff"><canvas id="c" width="${W}" height="${H}"></canvas>
149
+ <script type="module">
150
+ ${painter}
151
+ try {
152
+ const gl = document.getElementById("c").getContext("webgl2", { antialias: true, preserveDrawingBuffer: true });
153
+ const doc = { width: ${W}, height: ${H}, list: { cmds: ${JSON.stringify(cmds)} } };
154
+ const frame = prepareDisplayList(gl, doc, { dpr: 1 });
155
+ frame.draw(null, ${JSON.stringify(view)});
156
+ // SwiftShader has been seen to answer readPixels before the first
157
+ // draw is in the buffer. finish() is a fence; without it the next
158
+ // navigation can capture a blank or half-drawn canvas.
159
+ if (gl.finish) gl.finish();
160
+ // For the last check below, which builds ONE frame and draws it twice.
161
+ window.__prepare = prepareDisplayList;
162
+ window.__DONE__ = true;
163
+ } catch (e) { window.__ERR__ = String((e && e.stack) || e); window.__DONE__ = true; }
164
+ </script></body>`;
165
+
166
+ // Playwright and a Chromium, and nothing else: this draws through the
167
+ // painter and reads its own framebuffer, so it needs none of the React
168
+ // reference host the conformance oracles are built on.
169
+ const browser = await chromium.launch({
170
+ executablePath: findChromium(),
171
+ args: ["--use-gl=swiftshader", "--enable-unsafe-swiftshader"],
172
+ });
173
+ const page = await browser.newPage({ viewport: { width: W + 40, height: H + 40 } });
174
+ const tmp = path.join(HERE, ".view-check.html");
175
+ let loadSeq = 0;
176
+
177
+ /** Draw one list at one view and read the whole framebuffer back. */
178
+ async function pixelsOf(cmds, view, png) {
179
+ fs.writeFileSync(tmp, pageFor(cmds, view));
180
+ // Same path every time. Chromium will sometimes answer the previous
181
+ // document for a file:// URL it already loaded, and waitForFunction
182
+ // then sees the leftover __DONE__ and we read the last framebuffer.
183
+ // A unique query is a different navigation. That flake is what made
184
+ // `evg:view:check` fail CI on a landing-only pull request: one of
185
+ // thirty-one checks, the rest pass, no FAIL line in the last 30 lines.
186
+ loadSeq += 1;
187
+ await page.goto(pathToFileURL(tmp).href + "?n=" + loadSeq, { waitUntil: "load" });
188
+ await page.waitForFunction("window.__DONE__ === true", { timeout: 30000 });
189
+ const err = await page.evaluate(() => window.__ERR__ || null);
190
+ if (err) throw new Error("painter threw: " + err);
191
+ if (png) await page.locator("#c").screenshot({ path: png });
192
+ return page.evaluate(([ww, hh]) => {
193
+ const gl = document.getElementById("c").getContext("webgl2");
194
+ if (gl.finish) gl.finish();
195
+ const px = new Uint8Array(ww * hh * 4);
196
+ gl.readPixels(0, 0, ww, hh, gl.RGBA, gl.UNSIGNED_BYTE, px);
197
+ return Array.from(px);
198
+ }, [W, H]);
199
+ }
200
+
201
+ /** The worst channel difference between two framebuffers, and where. */
202
+ function worstDiff(a, b) {
203
+ let worst = 0;
204
+ let at = -1;
205
+ let off = 0;
206
+ for (let i = 0; i < a.length; i += 1) {
207
+ const d = Math.abs(a[i] - b[i]);
208
+ if (d > worst) { worst = d; at = i; }
209
+ if (d > 2) off += 1;
210
+ }
211
+ const px = at < 0 ? null : [(at >> 2) % W, Math.floor((at >> 2) / W)];
212
+ return { worst, off, px };
213
+ }
214
+
215
+ /** The ink's bounding box: which pixels are not the paper. */
216
+ function inkBox(px) {
217
+ let x0 = W, y0 = H, x1 = -1, y1 = -1;
218
+ for (let y = 0; y < H; y += 1) {
219
+ for (let x = 0; x < W; x += 1) {
220
+ const i = (y * W + x) * 4;
221
+ if (px[i] < 200 || px[i + 1] < 200 || px[i + 2] < 200) {
222
+ if (x < x0) x0 = x;
223
+ if (x > x1) x1 = x;
224
+ if (y < y0) y0 = y;
225
+ if (y > y1) y1 = y;
226
+ }
227
+ }
228
+ }
229
+ // GL reads from the bottom; the list counts from the top. Only the height
230
+ // flips, and only the SIZE of the box is compared, so it does not matter.
231
+ return { x0, y0, x1, y1, w: x1 - x0, h: y1 - y0 };
232
+ }
233
+
234
+ const writeAt = process.argv.indexOf("--write-png");
235
+ const pngPrefix = writeAt > -1 ? process.argv[writeAt + 1] : null;
236
+
237
+ console.log("=== the camera against the coordinates it replaces ===");
238
+
239
+ // --- the geometry scenes ----------------------------------------------------
240
+ //
241
+ // The same picture, drawn twice: once with the view multiplied into the list
242
+ // and once with it on the camera. Two or three levels of slack per channel,
243
+ // because the two paths reach the same edge through different arithmetic and
244
+ // an antialiased boundary is where that shows.
245
+ for (const [name, cmds] of Object.entries(SCENES)) {
246
+ for (const v of VIEWS) {
247
+ const pngBaked = pngPrefix ? `${pngPrefix}-${name}-${v.name.replace(/ /g, "_")}-baked.png` : null;
248
+ const pngCamera = pngPrefix ? `${pngPrefix}-${name}-${v.name.replace(/ /g, "_")}-camera.png` : null;
249
+ const once = async () => {
250
+ const baked = await pixelsOf(bake(cmds, v), { x: 0, y: 0, scale: 1 }, pngBaked);
251
+ const camera = await pixelsOf(cmds, v, pngCamera);
252
+ return worstDiff(baked, camera);
253
+ };
254
+ let d = await once();
255
+ // A leftover framebuffer from the previous navigation is a one-shot
256
+ // miss, not a different picture. Draw both sides again before it
257
+ // is a failure.
258
+ if (d.worst > 3) d = await once();
259
+ ok(
260
+ `${name}, ${v.name}`,
261
+ d.worst <= 3,
262
+ `worst ${d.worst}, ${d.off} px past tolerance` + (d.px && d.worst > 3 ? ` at ${d.px[0]},${d.px[1]}` : ""),
263
+ );
264
+ }
265
+ }
266
+
267
+ // --- text lands where it should --------------------------------------------
268
+ //
269
+ // Not the pixels: the atlas was rasterised at one size and stretched to the
270
+ // other, which is the §4 caveat and the reason for the band. What must hold
271
+ // is that the run is the right size and in the right place.
272
+ for (const v of VIEWS.slice(1)) {
273
+ const once = async () => {
274
+ const baked = inkBox(await pixelsOf(bake(TEXT_SCENE, v), { x: 0, y: 0, scale: 1 }));
275
+ const camera = inkBox(await pixelsOf(TEXT_SCENE, v));
276
+ return {
277
+ dw: Math.abs(baked.w - camera.w),
278
+ dh: Math.abs(baked.h - camera.h),
279
+ dx: Math.abs(baked.x0 - camera.x0),
280
+ dy: Math.abs(baked.y0 - camera.y0),
281
+ };
282
+ };
283
+ let b = await once();
284
+ if (b.dw > 2 || b.dh > 2 || b.dx > 2 || b.dy > 2) b = await once();
285
+ ok(
286
+ `text, ${v.name}: the run is the same size in the same place`,
287
+ b.dw <= 2 && b.dh <= 2 && b.dx <= 2 && b.dy <= 2,
288
+ `box off by ${b.dx},${b.dy} and ${b.dw}x${b.dh}`,
289
+ );
290
+ }
291
+
292
+ // --- a frame drawn twice is two pictures, not one --------------------------
293
+ //
294
+ // The point of the whole thing: ONE build, drawn at two views. If `draw`
295
+ // silently kept the first view, every check above would still pass — they
296
+ // each build their own frame.
297
+ {
298
+ const two = await page.evaluate(
299
+ ([cmds, ww, hh, v1, v2]) => {
300
+ const gl = document.getElementById("c").getContext("webgl2");
301
+ const doc = { width: ww, height: hh, list: { cmds } };
302
+ const frame = window.__prepare(gl, doc, { dpr: 1 });
303
+ const read = () => {
304
+ if (gl.finish) gl.finish();
305
+ const px = new Uint8Array(ww * hh * 4);
306
+ gl.readPixels(0, 0, ww, hh, gl.RGBA, gl.UNSIGNED_BYTE, px);
307
+ return Array.from(px);
308
+ };
309
+ frame.draw(null, v1);
310
+ const a = read();
311
+ frame.draw(null, v2);
312
+ const b = read();
313
+ frame.dispose();
314
+ return { a, b };
315
+ },
316
+ [SCENES.boxes, W, H, VIEWS[0], VIEWS[1]],
317
+ ).catch((e) => ({ err: String(e && e.message) }));
318
+ if (two.err) {
319
+ ok("one frame, two views", false, two.err);
320
+ } else {
321
+ const moved = worstDiff(two.a, two.b);
322
+ ok("one frame drawn at two views is two pictures", moved.worst > 3, `worst ${moved.worst}`);
323
+ // …and the second of them is the picture a fresh build gives.
324
+ let fresh = await pixelsOf(SCENES.boxes, VIEWS[1]);
325
+ let d = worstDiff(two.b, fresh);
326
+ if (d.worst > 3) {
327
+ fresh = await pixelsOf(SCENES.boxes, VIEWS[1]);
328
+ d = worstDiff(two.b, fresh);
329
+ }
330
+ ok("and the moved one matches a frame built there", d.worst <= 3, `worst ${d.worst}, ${d.off} px past tolerance`);
331
+ }
332
+ }
333
+
334
+ fs.rmSync(tmp, { force: true });
335
+ await browser.close();
336
+
337
+ console.log("");
338
+ console.log(`passed=${passed} failed=${failed}`);
339
+ if (failed > 0) {
340
+ console.log("SOME FAILED");
341
+ process.exit(1);
342
+ }
343
+ console.log("ALL PASS");
@@ -0,0 +1,184 @@
1
+ // SPDX-License-Identifier: MIT
2
+ //
3
+ // The keep-or-build policy, on its own.
4
+ //
5
+ // node lib/evg/gl/view-policy-check.mjs
6
+ //
7
+ // `evg-view.js` is the arithmetic a host does between "the view moved" and
8
+ // "walk the scene again": is the new scale within the band the atlas and the
9
+ // flattened curves were built for, and is the new viewport still inside the
10
+ // region the walk covered. Both answers are numbers, so both can be checked
11
+ // without a browser, a GPU or a scene — which is the point of keeping them
12
+ // out of the engine and out of the painter.
13
+ //
14
+ // What each case here is guarding against is a policy that is WRONG IN THE
15
+ // CHEAP DIRECTION: one that keeps a frame it should have rebuilt shows stale
16
+ // or blurred pixels, and that is the failure nobody notices in a profile.
17
+
18
+ import {
19
+ sceneViewport,
20
+ regionFor,
21
+ rectInside,
22
+ frameFits,
23
+ createViewKeeper,
24
+ DEFAULT_BAND,
25
+ } from "./evg-view.js";
26
+
27
+ let passed = 0;
28
+ let failed = 0;
29
+ const ok = (name, cond, detail) => {
30
+ if (cond) {
31
+ passed += 1;
32
+ console.log(" PASS " + name + (detail ? " (" + detail + ")" : ""));
33
+ } else {
34
+ failed += 1;
35
+ console.log(" FAIL " + name + (detail ? " (" + detail + ")" : ""));
36
+ }
37
+ };
38
+ const near = (a, b, eps = 1e-9) => Math.abs(a - b) <= eps;
39
+
40
+ const W = 1000;
41
+ const H = 800;
42
+
43
+ console.log("=== keep or build ===");
44
+
45
+ // --- what the canvas shows ---------------------------------------------------
46
+ {
47
+ const v = sceneViewport({ x: 0, y: 0, scale: 1 }, W, H);
48
+ ok("an identity view shows the page as it is", v.x === 0 && v.y === 0 && v.w === W && v.h === H);
49
+
50
+ // A pan of +100 puts the scene's -100 at the page's 0.
51
+ const p = sceneViewport({ x: 100, y: 50, scale: 1 }, W, H);
52
+ ok("a pan moves the window the other way", p.x === -100 && p.y === -50, `${p.x},${p.y}`);
53
+
54
+ // At 2x the page covers half as much scene.
55
+ const z = sceneViewport({ x: 0, y: 0, scale: 2 }, W, H);
56
+ ok("a zoom in shows less scene", z.w === W / 2 && z.h === H / 2, `${z.w}x${z.h}`);
57
+ const o = sceneViewport({ x: 0, y: 0, scale: 0.5 }, W, H);
58
+ ok("and a zoom out shows more", o.w === W * 2 && o.h === H * 2, `${o.w}x${o.h}`);
59
+
60
+ // The round trip: the point under the page's top-left is the scene's origin.
61
+ const view = { x: -320, y: 180, scale: 1.6 };
62
+ const sv = sceneViewport(view, W, H);
63
+ ok(
64
+ "the window is the camera read backwards",
65
+ near(sv.x * view.scale + view.x, 0) && near(sv.y * view.scale + view.y, 0),
66
+ `${sv.x.toFixed(2)},${sv.y.toFixed(2)}`,
67
+ );
68
+ }
69
+
70
+ // --- the region ---------------------------------------------------------------
71
+ {
72
+ const view = { x: 0, y: 0, scale: 1 };
73
+ const r = regionFor(view, W, H, 1);
74
+ ok("one viewport of overscan is three viewports across", r.w === W * 3 && r.h === H * 3, `${r.w}x${r.h}`);
75
+ ok("centred on what is shown", r.x === -W && r.y === -H, `${r.x},${r.y}`);
76
+ ok("and it contains what is shown", rectInside(sceneViewport(view, W, H), r));
77
+
78
+ const none = regionFor(view, W, H, 0);
79
+ ok("no overscan is the viewport itself", none.w === W && none.h === H);
80
+
81
+ // In SCENE units, so the same number of viewports at any zoom — which is
82
+ // what keeps a board at 8x from building a region nobody will ever pan to.
83
+ const far = regionFor({ x: 0, y: 0, scale: 8 }, W, H, 1);
84
+ ok("the overscan is viewports, not pixels", far.w === (W / 8) * 3, `${far.w}`);
85
+ }
86
+
87
+ // --- the band -----------------------------------------------------------------
88
+ {
89
+ const built = { view: { x: 0, y: 0, scale: 1 }, region: regionFor({ x: 0, y: 0, scale: 1 }, W, H, 1) };
90
+ ok("the same view fits", frameFits(built, { x: 0, y: 0, scale: 1 }, W, H).keep);
91
+ ok("a scale just inside the band fits", frameFits(built, { x: 0, y: 0, scale: DEFAULT_BAND - 0.01 }, W, H).keep);
92
+ const out = frameFits(built, { x: 0, y: 0, scale: DEFAULT_BAND + 0.01 }, W, H);
93
+ ok("and one just outside does not", !out.keep && out.reason === "scale left the band", out.reason);
94
+ // Both ways: a frame shrunk too far carries points nobody can see, and its
95
+ // glyphs alias rather than blur.
96
+ const small = frameFits(built, { x: 0, y: 0, scale: 1 / DEFAULT_BAND - 0.01 }, W, H);
97
+ ok("the band is two-sided", !small.keep && small.reason === "scale left the band", small.reason);
98
+ ok("a tighter band is tighter", !frameFits(built, { x: 0, y: 0, scale: 1.3 }, W, H, { band: 1.15 }).keep);
99
+ }
100
+
101
+ // --- the region, as a gate ------------------------------------------------------
102
+ {
103
+ const at = (x, y, scale = 1) => ({ x, y, scale });
104
+ const built = { view: at(0, 0), region: regionFor(at(0, 0), W, H, 1) };
105
+ // One viewport of overscan means the window may travel one viewport.
106
+ ok("a pan inside the overscan keeps the frame", frameFits(built, at(-W * 0.9, 0), W, H).keep);
107
+ const gone = frameFits(built, at(-W * 1.1, 0), W, H);
108
+ ok("and one past it does not", !gone.keep && gone.reason === "viewport left the region", gone.reason);
109
+ ok("upward too", !frameFits(built, at(0, H * 1.1), W, H).keep);
110
+ // WHICH OF THE TWO BINDS, and it depends on the overscan. With one
111
+ // viewport each way the region is three viewports across and the band is
112
+ // 1.41, so a zoom OUT always hits the band first — the window can only
113
+ // grow to 1.41 viewports and the region holds three.
114
+ ok("with a generous overscan the band binds a zoom", frameFits(built, at(0, 0, 1 / 1.39), W, H).keep);
115
+ // Shrink the overscan and the region binds first: the window at 1.3x out
116
+ // is 1.3 viewports and the region is only 1.2.
117
+ const tight = { view: at(0, 0), region: regionFor(at(0, 0), W, H, 0.1) };
118
+ const wider = frameFits(tight, at(0, 0, 1 / 1.3), W, H);
119
+ ok("with a tight one the region binds it instead", !wider.keep && wider.reason === "viewport left the region", wider.reason);
120
+ }
121
+
122
+ // --- nothing built ---------------------------------------------------------------
123
+ {
124
+ const f = frameFits(null, { x: 0, y: 0, scale: 1 }, W, H);
125
+ ok("with no frame there is nothing to keep", !f.keep && f.reason === "nothing built");
126
+ ok("and half a state is no state", !frameFits({ view: { scale: 1 } }, { x: 0, y: 0, scale: 1 }, W, H).keep);
127
+ }
128
+
129
+ // --- the keeper, as a host drives it -----------------------------------------
130
+ {
131
+ const keep = createViewKeeper({ overscan: 1 });
132
+ const at = (x, y, scale = 1) => ({ x, y, scale });
133
+
134
+ ok("the first view has to be built", !keep.fits(at(0, 0), W, H).keep);
135
+ const region = keep.built(at(0, 0), W, H);
136
+ ok("and the build is told what to cover", region.w === W * 3, `${region.w}`);
137
+
138
+ // A pan across the overscan: every one of these is a uniform, not a walk.
139
+ let kept = 0;
140
+ for (let i = 0; i <= 8; i += 1) {
141
+ if (keep.fits(at(-i * 100, 0), W, H).keep) kept += 1;
142
+ }
143
+ ok("a pan across the overscan is free the whole way", kept === 9, `${kept} of 9`);
144
+
145
+ // …and one step past it is not.
146
+ ok("until it is not", !keep.fits(at(-W * 1.2, 0), W, H).keep);
147
+
148
+ // A reset is what a new document or an edit does.
149
+ keep.built(at(-W * 1.2, 0), W, H);
150
+ ok("a rebuild moves the region with it", keep.fits(at(-W * 1.2, 0), W, H).keep);
151
+ keep.reset();
152
+ ok("and a reset throws the frame away", !keep.fits(at(-W * 1.2, 0), W, H).keep);
153
+ ok("the counts are kept", keep.counts.builds === 2, JSON.stringify(keep.counts));
154
+ }
155
+
156
+ // --- a pinch, counted ----------------------------------------------------------
157
+ //
158
+ // The design's own claim, as a number: a zoom gesture costs a rebuild about
159
+ // every third frame and the pans between them cost nothing. A policy that
160
+ // rebuilt every frame would score 0 here, and one that never rebuilt would
161
+ // score 1 — both are wrong and both are easy to write.
162
+ {
163
+ const keep = createViewKeeper({ overscan: 1 });
164
+ let builds = 0;
165
+ let scale = 1;
166
+ for (let i = 0; i < 60; i += 1) {
167
+ scale *= 1.05; // a steady pinch, 5% a frame
168
+ const view = { x: 0, y: 0, scale };
169
+ if (!keep.fits(view, W, H).keep) {
170
+ keep.built(view, W, H);
171
+ builds += 1;
172
+ }
173
+ }
174
+ // √2 is about seven frames of 5%, so a sixty-frame pinch is eight or nine.
175
+ ok("a steady pinch rebuilds on the band, not on the frame", builds >= 7 && builds <= 10, `${builds} builds in 60 frames`);
176
+ }
177
+
178
+ console.log("");
179
+ console.log(`passed=${passed} failed=${failed}`);
180
+ if (failed > 0) {
181
+ console.log("SOME FAILED");
182
+ process.exit(1);
183
+ }
184
+ console.log("ALL PASS");