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.
- package/CHANGELOG.md +1160 -19
- package/LICENSE +3 -1
- package/README.md +124 -29
- package/dist/Lang.rgr +1188 -290
- package/dist/api.d.ts +2343 -879
- package/dist/api.js +79924 -56922
- package/dist/lib/JSON.rgr +102 -91
- package/dist/lib/Shell.rgr +4 -4
- package/dist/lib/apple/AppleToolchain.rgr +4 -4
- package/dist/lib/apple/README.md +5 -4
- package/dist/lib/apple/apple_test.rgr +1 -1
- package/dist/lib/core/README.md +1 -1
- package/dist/lib/evg/EVG.rgr +12 -0
- package/dist/lib/evg/EVGA11yFromTree.rgr +302 -0
- package/dist/lib/evg/EVGA11yTree.rgr +894 -0
- package/dist/lib/evg/EVGBox.rgr +267 -0
- package/dist/lib/evg/EVGBoxShorthandTest.rgr +220 -0
- package/dist/lib/evg/EVGCodepoint.rgr +316 -0
- package/dist/lib/evg/EVGColor.rgr +700 -0
- package/dist/lib/evg/EVGCommands.rgr +177 -0
- package/dist/lib/evg/EVGComponent.rgr +331 -0
- package/dist/lib/evg/EVGComponentTest.rgr +387 -0
- package/dist/lib/evg/EVGConnector.rgr +541 -0
- package/dist/lib/evg/EVGConnectorTest.rgr +306 -0
- package/dist/lib/evg/EVGDisplayList.rgr +4186 -0
- package/dist/lib/evg/EVGEasing.rgr +370 -0
- package/dist/lib/evg/EVGEffectTest.rgr +253 -0
- package/dist/lib/evg/EVGElement.rgr +4914 -0
- package/dist/lib/evg/EVGFixedTest.rgr +289 -0
- package/dist/lib/evg/EVGFlexRulesTest.rgr +317 -0
- package/dist/lib/evg/EVGFlexWrapTest.rgr +246 -0
- package/dist/lib/evg/EVGFling.rgr +244 -0
- package/dist/lib/evg/EVGFocus.rgr +400 -0
- package/dist/lib/evg/EVGFocusTest.rgr +399 -0
- package/dist/lib/evg/EVGGradient.rgr +322 -0
- package/dist/lib/evg/EVGGrapheme.rgr +190 -0
- package/dist/lib/evg/EVGGrid.rgr +975 -0
- package/dist/lib/evg/EVGHitTest.rgr +208 -0
- package/dist/lib/evg/EVGHoles.rgr +294 -0
- package/dist/lib/evg/EVGHostMeasurerTest.rgr +281 -0
- package/dist/lib/evg/EVGHostTextMeasurer.rgr +331 -0
- package/dist/lib/evg/EVGHostTree.rgr +834 -0
- package/dist/lib/evg/EVGHostTreeTest.rgr +447 -0
- package/dist/lib/evg/EVGImageDecode.rgr +117 -0
- package/dist/lib/evg/EVGImageMeasurer.rgr +86 -0
- package/dist/lib/evg/EVGInspect.rgr +883 -0
- package/dist/lib/evg/EVGInvalidateTest.rgr +432 -0
- package/dist/lib/evg/EVGJsonTest.rgr +535 -0
- package/dist/lib/evg/EVGLayout.rgr +4220 -0
- package/dist/lib/evg/EVGMeasure.rgr +1267 -0
- package/dist/lib/evg/EVGOverlayTest.rgr +626 -0
- package/dist/lib/evg/EVGPatch.rgr +1721 -0
- package/dist/lib/evg/EVGPatchTest.rgr +692 -0
- package/dist/lib/evg/EVGPopoverTest.rgr +395 -0
- package/dist/lib/evg/EVGReconcile.rgr +226 -0
- package/dist/lib/evg/EVGReconcileTest.rgr +421 -0
- package/dist/lib/evg/EVGReject.rgr +126 -0
- package/dist/lib/evg/EVGRelayoutTest.rgr +375 -0
- package/dist/lib/evg/EVGRtlLayoutTest.rgr +319 -0
- package/dist/lib/evg/EVGRuler.rgr +232 -0
- package/dist/lib/evg/EVGRulerTest.rgr +172 -0
- package/dist/lib/evg/EVGSelectChrome.rgr +178 -0
- package/dist/lib/evg/EVGStyleCacheTest.rgr +419 -0
- package/dist/lib/evg/EVGStyleSheet.rgr +2148 -0
- package/dist/lib/evg/EVGStyleStateTest.rgr +407 -0
- package/dist/lib/evg/EVGStyleVarTest.rgr +471 -0
- package/dist/lib/evg/EVGText.rgr +105 -0
- package/dist/lib/evg/EVGTextEngine.rgr +536 -0
- package/dist/lib/evg/EVGTextMeasurer.rgr +720 -0
- package/dist/lib/evg/EVGTimingTest.rgr +1417 -0
- package/dist/lib/evg/EVGToolbar.rgr +1369 -0
- package/dist/lib/evg/EVGTransition.rgr +728 -0
- package/dist/lib/evg/EVGTreeJson.rgr +721 -0
- package/dist/lib/evg/EVGUnit.rgr +554 -0
- package/dist/lib/evg/EVGViewportUnitTest.rgr +236 -0
- package/dist/lib/evg/EvgApp.rgr +189 -0
- package/dist/lib/evg/EvgBitmapTracer.rgr +4277 -0
- package/dist/lib/evg/EvgBitmapTracerTest.rgr +2119 -0
- package/dist/lib/evg/EvgHost.rgr +451 -0
- package/dist/lib/evg/EvgTest.rgr +91 -0
- package/dist/lib/evg/EvgTraceColor.rgr +236 -0
- package/dist/lib/evg/EvgTraceCurve.rgr +648 -0
- package/dist/lib/evg/EvgTraceFit.rgr +767 -0
- package/dist/lib/evg/EvgTracePath.rgr +435 -0
- package/dist/lib/evg/EvgTraceTypes.rgr +559 -0
- package/dist/lib/evg/EvgViewport.rgr +313 -0
- package/dist/lib/evg/FxDemoDoc.rgr +185 -0
- package/dist/lib/evg/HOSTS.md +227 -0
- package/dist/lib/evg/ISSUES.md +835 -0
- package/dist/lib/evg/PLAN_ACCESSIBILITY.md +650 -0
- package/dist/lib/evg/PLAN_CSS_LAYOUT_AND_FONTS.md +1124 -0
- package/dist/lib/evg/PLAN_EFFECTS.md +431 -0
- package/dist/lib/evg/PLAN_EVG.md +518 -0
- package/dist/lib/evg/PLAN_EVG_RENDERER.md +1674 -0
- package/dist/lib/evg/PLAN_INSPECTOR.md +788 -0
- package/dist/lib/evg/PLAN_LINKS_AND_FORMS.md +157 -0
- package/dist/lib/evg/PLAN_NATIVE_HOSTS.md +671 -0
- package/dist/lib/evg/PLAN_VECTOR_IR.md +664 -0
- package/dist/lib/evg/PLAN_VIEW_TRANSFORM.md +322 -0
- package/dist/lib/evg/PathBuilder.rgr +333 -0
- package/dist/lib/evg/README.md +1236 -0
- package/dist/lib/evg/SPEC.md +1252 -0
- package/dist/lib/evg/SVGPathParser.rgr +1199 -0
- package/dist/lib/evg/SvgParser.rgr +1736 -0
- package/dist/lib/evg/TODO_EVG_RENDERER.md +595 -0
- package/dist/lib/evg/VectorShapes.rgr +331 -0
- package/dist/lib/evg/VectorStroke.rgr +107 -0
- package/dist/lib/evg/VectorViewBox.rgr +380 -0
- package/dist/lib/evg/agent/README.md +290 -0
- package/dist/lib/evg/agent/evg_agent.rgr +899 -0
- package/dist/lib/evg/agent/fixtures/broken.evg.json +7 -0
- package/dist/lib/evg/agent/fixtures/card.evg.json +9 -0
- package/dist/lib/evg/agent/fixtures/connector.css +63 -0
- package/dist/lib/evg/agent/fixtures/connector.evg.json +14 -0
- package/dist/lib/evg/agent/fixtures/drawn.evg.json +129 -0
- package/dist/lib/evg/agent/fixtures/gradient.evg.json +4 -0
- package/dist/lib/evg/agent/fixtures/popover.css +99 -0
- package/dist/lib/evg/agent/fixtures/popover.evg.json +30 -0
- package/dist/lib/evg/agent/roundtrip.sh +74 -0
- package/dist/lib/evg/agent/smoke.sh +230 -0
- package/dist/lib/evg/android/README.md +97 -0
- package/dist/lib/evg/android/androidstubs/AndroidStubs.kt +175 -0
- package/dist/lib/evg/android/androidstubs/Annotation.kt +10 -0
- package/dist/lib/evg/android/androidstubs/App.kt +30 -0
- package/dist/lib/evg/android/androidstubs/Content.kt +41 -0
- package/dist/lib/evg/android/androidstubs/ContentRes.kt +12 -0
- package/dist/lib/evg/android/androidstubs/InputMethod.kt +46 -0
- package/dist/lib/evg/android/androidstubs/Net.kt +6 -0
- package/dist/lib/evg/android/androidstubs/Os.kt +30 -0
- package/dist/lib/evg/android/androidstubs/Util.kt +15 -0
- package/dist/lib/evg/android/androidstubs/View.kt +113 -0
- package/dist/lib/evg/android/androidstubs/Widget.kt +16 -0
- package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidEvgSurface.kt +323 -0
- package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidTextMeasurer.kt +56 -0
- package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/RippleEffect.kt +262 -0
- package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtEvgSurface.kt +238 -0
- package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtTextMeasurer.kt +85 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgEngineThread.kt +115 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgPainter.kt +231 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgSurface.kt +117 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/RecordingSurface.kt +94 -0
- package/dist/lib/evg/apple/README.md +128 -0
- package/dist/lib/evg/apple/Sources/CoreGraphicsEvgSurface.swift +329 -0
- package/dist/lib/evg/apple/Sources/CoreTextMeasurer.swift +70 -0
- package/dist/lib/evg/apple/Sources/EvgEngineQueue.swift +105 -0
- package/dist/lib/evg/apple/Sources/EvgPainter.swift +225 -0
- package/dist/lib/evg/apple/Sources/EvgSurface.swift +163 -0
- package/dist/lib/evg/apple/Sources/RecordingSurface.swift +110 -0
- package/dist/lib/evg/bench/EvgLayoutBench.rgr +34 -0
- package/dist/lib/evg/bench/README.md +64 -0
- package/dist/lib/evg/bench/layout-bench.mjs +316 -0
- package/dist/lib/evg/bench/layout-cases.mjs +510 -0
- package/dist/lib/evg/bench/layout-conformance.mjs +254 -0
- package/dist/lib/evg/bin/.gitignore +17 -0
- package/dist/lib/evg/evg_test.rgr +313 -0
- package/dist/lib/evg/gl/README.md +198 -0
- package/dist/lib/evg/gl/a11y-paint-check.mjs +73 -0
- package/dist/lib/evg/gl/blur-check.mjs +399 -0
- package/dist/lib/evg/gl/boxmodel.json +1 -0
- package/dist/lib/evg/gl/demo.html +46 -0
- package/dist/lib/evg/gl/effect-presets.css +285 -0
- package/dist/lib/evg/gl/effect-presets.js +63 -0
- package/dist/lib/evg/gl/effect-shots.mjs +135 -0
- package/dist/lib/evg/gl/evg-a11y.js +566 -0
- package/dist/lib/evg/gl/evg-binary.js +160 -0
- package/dist/lib/evg/gl/evg-engine.js +296 -0
- package/dist/lib/evg/gl/evg-fx.js +237 -0
- package/dist/lib/evg/gl/evg-gestures.js +209 -0
- package/dist/lib/evg/gl/evg-list.js +167 -0
- package/dist/lib/evg/gl/evg-measure.js +186 -0
- package/dist/lib/evg/gl/evg-textinput.js +303 -0
- package/dist/lib/evg/gl/evg-view.js +144 -0
- package/dist/lib/evg/gl/evg-webgl.js +3942 -0
- package/dist/lib/evg/gl/fx-check.mjs +998 -0
- package/dist/lib/evg/gl/fx-demo.html +166 -0
- package/dist/lib/evg/gl/fx-demo.js +2 -0
- package/dist/lib/evg/gl/fx-serve.mjs +47 -0
- package/dist/lib/evg/gl/gestures-check.mjs +189 -0
- package/dist/lib/evg/gl/list-binary-check.mjs +152 -0
- package/dist/lib/evg/gl/measure-check.mjs +135 -0
- package/dist/lib/evg/gl/rotation-check.mjs +207 -0
- package/dist/lib/evg/gl/shift-check.mjs +113 -0
- package/dist/lib/evg/gl/stroke-check.mjs +168 -0
- package/dist/lib/evg/gl/text-snap-check.mjs +139 -0
- package/dist/lib/evg/gl/view-check.mjs +343 -0
- package/dist/lib/evg/gl/view-policy-check.mjs +184 -0
- package/dist/lib/evg/html/evg-dom.js +318 -0
- package/dist/lib/evg/html/evg-html.js +600 -0
- package/dist/lib/evg/inspect/README.md +415 -0
- package/dist/lib/evg/inspect/browser-smoke.mjs +115 -0
- package/dist/lib/evg/inspect/evg-inspect.js +947 -0
- package/dist/lib/evg/inspect/shots/css.png +0 -0
- package/dist/lib/evg/inspect/shots/dashboard.png +0 -0
- package/dist/lib/evg/inspect/shots/pptx-slide.png +0 -0
- package/dist/lib/evg/inspect/shots/state.png +0 -0
- package/dist/lib/evg/inspect/shots.mjs +226 -0
- package/dist/lib/evg/oracle/css-blur.json +576 -0
- package/dist/lib/evg/oracle/css-box.json +157 -0
- package/dist/lib/evg/oracle/css-timing.json +709 -0
- package/dist/lib/evg/oracle/css_blur_oracle.mjs +529 -0
- package/dist/lib/evg/oracle/css_box_oracle.mjs +106 -0
- package/dist/lib/evg/oracle/css_timing_oracle.mjs +389 -0
- package/dist/lib/evg/original/EVGColor.clj +310 -0
- package/dist/lib/evg/original/EVGColorContext.rgr +178 -0
- package/dist/lib/evg/original/SVGPath.rgr +627 -0
- package/dist/lib/evg/original/Vec2.crgr +103 -0
- package/dist/lib/evg/ranger.json +9 -0
- package/dist/lib/evg/showcase/README.md +246 -0
- package/dist/lib/evg/showcase/assets/emblem.svg +30 -0
- package/dist/lib/evg/showcase/assets/rosette.svg +22 -0
- package/dist/lib/evg/showcase/build.mjs +651 -0
- package/dist/lib/evg/showcase/pages/album.tsx +30 -0
- package/dist/lib/evg/showcase/pages/boxmodel.tsx +37 -0
- package/dist/lib/evg/showcase/pages/cards.tsx +41 -0
- package/dist/lib/evg/showcase/pages/chart_api.tsx +170 -0
- package/dist/lib/evg/showcase/pages/charts.tsx +219 -0
- package/dist/lib/evg/showcase/pages/drawing.tsx +160 -0
- package/dist/lib/evg/showcase/pages/emoji.tsx +91 -0
- package/dist/lib/evg/showcase/pages/flex.tsx +46 -0
- package/dist/lib/evg/showcase/pages/more.tsx +332 -0
- package/dist/lib/evg/showcase/pages/plots.tsx +262 -0
- package/dist/lib/evg/showcase/pages/svg.tsx +63 -0
- package/dist/lib/evg/showcase/pages/tables.tsx +209 -0
- package/dist/lib/evg/showcase/pages/typography.tsx +43 -0
- package/dist/lib/evg/showcase/pages/units.tsx +35 -0
- package/dist/lib/evg/showcase/pages/variants.tsx +233 -0
- package/dist/lib/evg/showcase/pages/vector.tsx +75 -0
- package/dist/lib/evg/showcase/pages/views.tsx +223 -0
- package/dist/lib/evg/showcase/tests/chart_api_smoke.mjs +339 -0
- package/dist/lib/evg/showcase/tests/gl_smoke.mjs +125 -0
- package/dist/lib/evg/showcase/themes/chart_api-default.css +15 -0
- package/dist/lib/evg/showcase/themes/charts-default.css +29 -0
- package/dist/lib/evg/showcase/themes/drawing-default.css +35 -0
- package/dist/lib/evg/showcase/themes/more-default.css +128 -0
- package/dist/lib/evg/showcase/themes/plots-default.css +18 -0
- package/dist/lib/evg/showcase/themes/showcase.css +508 -0
- package/dist/lib/evg/showcase/themes/tables-default.css +123 -0
- package/dist/lib/evg/showcase/themes/variants-default.css +15 -0
- package/dist/lib/evg/showcase/themes/views-default.css +15 -0
- package/dist/lib/evg/tools/bench_vs_potrace.mjs +290 -0
- package/dist/lib/evg/tools/evg_image_tool.rgr +488 -0
- package/dist/lib/evg/tools/evg_trace_bench.rgr +114 -0
- package/dist/lib/evg/tools/evg_trace_cli.rgr +662 -0
- package/dist/lib/evg/tools/evg_trace_cpp_bench.rgr +83 -0
- package/dist/lib/evg/tools/run_trace_bench.sh +74 -0
- package/dist/lib/evg/tools/run_trace_cli_smoke.sh +128 -0
- package/dist/lib/evg/web/responsive/EvgResponsiveCheck.rgr +271 -0
- package/dist/lib/evg/web/responsive/EvgResponsiveDemo.rgr +531 -0
- package/dist/lib/evg/web/responsive/README.md +97 -0
- package/dist/lib/evg/web/responsive/build.mjs +90 -0
- package/dist/lib/evg/web/responsive/dom-check.mjs +169 -0
- package/dist/lib/evg/web/responsive/index.html +177 -0
- package/dist/lib/evg/web/responsive/smoke.mjs +221 -0
- package/dist/lib/evg/web/tools/assets-client.mjs +95 -0
- package/dist/lib/evg/web/tools/boot-bench.mjs +189 -0
- package/dist/lib/evg/web/tools/inline-assets.mjs +119 -0
- package/dist/lib/evg/web/tools/minify.mjs +60 -0
- package/dist/lib/evg/web/tracer/build.mjs +87 -0
- package/dist/lib/evg/web/tracer/index.html +2805 -0
- package/dist/lib/evg/web/tracer/sample.png +0 -0
- package/dist/lib/evg/web/tracer/smoke.mjs +1162 -0
- package/dist/lib/evgr/Cargo.lock +21 -0
- package/dist/lib/evgr/Cargo.toml +19 -0
- package/dist/lib/evgr/README.md +140 -0
- package/dist/lib/evgr/bench/NativeBench.rgr +246 -0
- package/dist/lib/evgr/bench/compare.mjs +252 -0
- package/dist/lib/evgr/bench/speed.mjs +277 -0
- package/dist/lib/evgr/bin/.gitignore +3 -0
- package/dist/lib/evgr/src/bin/bench.rs +106 -0
- package/dist/lib/evgr/src/bin/smoke.rs +13 -0
- package/dist/lib/evgr/src/grid.rs +730 -0
- package/dist/lib/evgr/src/lib.rs +1003 -0
- package/dist/lib/evgr/src/style.rs +468 -0
- package/dist/lib/evgr/src/text.rs +83 -0
- package/dist/lib/image/BitReader.rgr +171 -0
- package/dist/lib/image/Buffer.rgr +173 -0
- package/dist/lib/image/DCT.rgr +283 -0
- package/dist/lib/image/Deflate.rgr +339 -0
- package/dist/lib/image/HuffmanDecoder.rgr +181 -0
- package/dist/lib/image/ImageBuffer.rgr +706 -0
- package/dist/lib/image/JPEGDecoder.rgr +808 -0
- package/dist/lib/image/PNGDecoder.rgr +544 -0
- package/dist/lib/image/PNGEncoder.rgr +388 -0
- package/dist/lib/image/PPMImage.rgr +224 -0
- package/dist/lib/image/ProgressiveJPEGDecoder.rgr +1154 -0
- package/dist/lib/image/README.md +33 -0
- package/dist/lib/image/RasterBuffer.rgr +294 -0
- package/dist/lib/image/VP8BoolDecoder.rgr +163 -0
- package/dist/lib/image/WebPDecoder.rgr +433 -0
- package/dist/lib/image/WebPLossless.rgr +1035 -0
- package/dist/lib/image/WebPLossy.rgr +1929 -0
- package/dist/lib/image/ranger.json +9 -0
- package/dist/lib/image/testdata/webp/anim_2f_24x16.webp +0 -0
- package/dist/lib/image/testdata/webp/fixtures.txt +33 -0
- package/dist/lib/image/testdata/webp/gen_fixtures.py +261 -0
- package/dist/lib/image/testdata/webp/ll_1x1.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_alpha_29x21.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_gradient_64x64.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_meta_32x32.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_noise_17x9.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal11_23x11.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal2_19x7.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal40_16x16.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal4_13x5.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_photo_40x30.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_predictors_64x32.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_1x1.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f0_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f1_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f2_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f3_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f0_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f1_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f2_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f3_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alpha_33x17.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_exif_15x11.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_i16_96x80.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_nofilter_24x20.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_odd_17x9.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_photo_48x40.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_q100_20x18.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_sharp_40x36.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_simple_filter_33x31.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_vpx_parts8_37x45.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_vpx_skip_96x80.webp +0 -0
- package/dist/lib/image/tests/WebPDecodeTool.rgr +81 -0
- package/dist/lib/image/tests/WebPDecoderTest.rgr +1162 -0
- package/dist/lib/image/tests/run_webp_tests.sh +39 -0
- package/dist/lib/rust/RsJson.rgr +468 -0
- package/dist/lib/rust/RsPrelude.rgr +1663 -0
- package/dist/lib/shell_test.rgr +7 -7
- package/dist/lib/stdlib.rgr +21 -5
- package/dist/lib/zip/ranger.json +6 -0
- package/dist/rgrc.js +87318 -67610
- package/package.json +1010 -1040
- package/dist/README.md +0 -117
- 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");
|