ranger-compiler 3.5.1 → 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 +1064 -20
- package/LICENSE +3 -1
- package/README.md +123 -28
- package/dist/Lang.rgr +1188 -290
- package/dist/api.d.ts +2343 -879
- package/dist/api.js +79923 -56921
- 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,1162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tracer/smoke.mjs — does the bitmap tracer page run in a browser?
|
|
3
|
+
*
|
|
4
|
+
* npm run evg:trace:web && npm run evg:trace:web:smoke
|
|
5
|
+
*
|
|
6
|
+
* Checks that the compiled bundle loads, the sample image vectorizes, and the
|
|
7
|
+
* SVG stage ends up with a real <svg>/<path>.
|
|
8
|
+
*/
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import http from "node:http";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import zlib from "node:zlib";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
16
|
+
|
|
17
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const ROOT = path.resolve(HERE, "../../../..");
|
|
19
|
+
const DIST = path.resolve(process.argv[2] || path.join(HERE, "dist"));
|
|
20
|
+
|
|
21
|
+
function loadPlaywright() {
|
|
22
|
+
const anchors = [
|
|
23
|
+
"/opt/node22/lib/node_modules/x",
|
|
24
|
+
path.join(ROOT, "package.json"),
|
|
25
|
+
path.join(process.cwd(), "package.json"),
|
|
26
|
+
import.meta.url,
|
|
27
|
+
];
|
|
28
|
+
for (const anchor of anchors) {
|
|
29
|
+
for (const name of ["playwright", "playwright-core"]) {
|
|
30
|
+
try {
|
|
31
|
+
return createRequire(anchor)(name);
|
|
32
|
+
} catch { /* next */ }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function findChrome() {
|
|
39
|
+
const fixed = [process.env.CHROME_PATH, "/usr/bin/chromium", "/usr/bin/google-chrome",
|
|
40
|
+
"/usr/local/bin/google-chrome"].filter(Boolean);
|
|
41
|
+
for (const c of fixed) if (fs.existsSync(c)) return c;
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const pw = loadPlaywright();
|
|
46
|
+
if (!pw) {
|
|
47
|
+
console.log("Playwright is not available — tracer page was not checked.");
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
if (!fs.existsSync(path.join(DIST, "index.html")) ||
|
|
51
|
+
!fs.existsSync(path.join(DIST, "evg_bitmap_tracer.js"))) {
|
|
52
|
+
console.error(`no tracer page in ${path.relative(ROOT, DIST)} — run: npm run evg:trace:web`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// A PNG writer, twenty lines of it, so this file can build a picture whose
|
|
57
|
+
// true answer it already knows. Everything else here checks that the tool did
|
|
58
|
+
// something; the composite below checks that what it did was right.
|
|
59
|
+
const CRC = (() => {
|
|
60
|
+
const t = new Int32Array(256);
|
|
61
|
+
for (let n = 0; n < 256; n++) { let c = n;
|
|
62
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
63
|
+
t[n] = c; }
|
|
64
|
+
return t;
|
|
65
|
+
})();
|
|
66
|
+
function crc32(buf) {
|
|
67
|
+
let c = -1;
|
|
68
|
+
for (let i = 0; i < buf.length; i++) c = CRC[(c ^ buf[i]) & 255] ^ (c >>> 8);
|
|
69
|
+
return (c ^ -1) >>> 0;
|
|
70
|
+
}
|
|
71
|
+
function writePng(w, h, rgba) {
|
|
72
|
+
const stride = w * 4 + 1;
|
|
73
|
+
const raw = Buffer.alloc(stride * h);
|
|
74
|
+
for (let y = 0; y < h; y++) rgba.copy(raw, y * stride + 1, y * w * 4, (y + 1) * w * 4);
|
|
75
|
+
const chunk = (type, data) => {
|
|
76
|
+
const len = Buffer.alloc(4); len.writeUInt32BE(data.length, 0);
|
|
77
|
+
const td = Buffer.concat([Buffer.from(type, "ascii"), data]);
|
|
78
|
+
const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(td), 0);
|
|
79
|
+
return Buffer.concat([len, td, crc]);
|
|
80
|
+
};
|
|
81
|
+
const ihdr = Buffer.alloc(13);
|
|
82
|
+
ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4);
|
|
83
|
+
ihdr[8] = 8; ihdr[9] = 6;
|
|
84
|
+
return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
85
|
+
chunk("IHDR", ihdr), chunk("IDAT", zlib.deflateSync(raw)),
|
|
86
|
+
chunk("IEND", Buffer.alloc(0))]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const TYPES = {
|
|
90
|
+
".html": "text/html",
|
|
91
|
+
".js": "text/javascript",
|
|
92
|
+
".png": "image/png",
|
|
93
|
+
".svg": "image/svg+xml",
|
|
94
|
+
};
|
|
95
|
+
const server = http.createServer((req, res) => {
|
|
96
|
+
let file = path.join(DIST, decodeURIComponent(req.url.split("?")[0]));
|
|
97
|
+
if (fs.existsSync(file) && fs.statSync(file).isDirectory()) file = path.join(file, "index.html");
|
|
98
|
+
if (!file.startsWith(DIST) || !fs.existsSync(file)) {
|
|
99
|
+
res.writeHead(404); res.end("missing"); return;
|
|
100
|
+
}
|
|
101
|
+
const ext = path.extname(file);
|
|
102
|
+
res.writeHead(200, { "Content-Type": TYPES[ext] || "application/octet-stream" });
|
|
103
|
+
fs.createReadStream(file).pipe(res);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
107
|
+
const { port } = server.address();
|
|
108
|
+
const url = `http://127.0.0.1:${port}/`;
|
|
109
|
+
|
|
110
|
+
const { chromium } = pw;
|
|
111
|
+
const browser = await chromium.launch({
|
|
112
|
+
executablePath: findChrome() || undefined,
|
|
113
|
+
headless: true,
|
|
114
|
+
});
|
|
115
|
+
const page = await browser.newPage();
|
|
116
|
+
const errors = [];
|
|
117
|
+
page.on("pageerror", (e) => errors.push(String(e)));
|
|
118
|
+
page.on("console", (msg) => {
|
|
119
|
+
if (msg.type() !== "error") return;
|
|
120
|
+
const t = msg.text();
|
|
121
|
+
if (/favicon\.ico|Failed to load resource.*404/i.test(t)) return;
|
|
122
|
+
errors.push(t);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await page.goto(url, { waitUntil: "networkidle" });
|
|
126
|
+
const ready = await page.evaluate(() =>
|
|
127
|
+
typeof EvgBitmapTracer === "function" && typeof ImageBuffer === "function"
|
|
128
|
+
);
|
|
129
|
+
if (!ready) {
|
|
130
|
+
console.error("bundle did not publish EvgBitmapTracer / ImageBuffer");
|
|
131
|
+
process.exitCode = 1;
|
|
132
|
+
} else {
|
|
133
|
+
await page.click("#sample");
|
|
134
|
+
await page.waitForFunction(() => {
|
|
135
|
+
const st = document.getElementById("status");
|
|
136
|
+
return st && /OK/.test(st.textContent || "");
|
|
137
|
+
}, { timeout: 60000 });
|
|
138
|
+
const info = await page.evaluate(() => {
|
|
139
|
+
const out = document.getElementById("outStage");
|
|
140
|
+
const svg = out && out.querySelector("svg");
|
|
141
|
+
const paths = svg ? svg.querySelectorAll("path").length : 0;
|
|
142
|
+
return {
|
|
143
|
+
status: document.getElementById("status").textContent,
|
|
144
|
+
hasSvg: !!svg,
|
|
145
|
+
paths,
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
console.log(JSON.stringify(info, null, 2));
|
|
149
|
+
if (!info.hasSvg || info.paths < 1) {
|
|
150
|
+
console.error("expected an SVG with at least one path");
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Color mode: the posterize path is the one with the moving parts.
|
|
155
|
+
await page.evaluate(() => {
|
|
156
|
+
document.getElementById("status").textContent = "…";
|
|
157
|
+
const cc = document.getElementById("colorCount");
|
|
158
|
+
cc.value = "8";
|
|
159
|
+
cc.dispatchEvent(new Event("input", { bubbles: true }));
|
|
160
|
+
});
|
|
161
|
+
await page.waitForFunction(() => {
|
|
162
|
+
const st = document.getElementById("status");
|
|
163
|
+
return st && /OK/.test(st.textContent || "");
|
|
164
|
+
}, { timeout: 60000 });
|
|
165
|
+
const color = await page.evaluate(() => {
|
|
166
|
+
const out = document.getElementById("outStage");
|
|
167
|
+
const svg = out && out.querySelector("svg");
|
|
168
|
+
const fills = svg
|
|
169
|
+
? Array.from(svg.querySelectorAll("path")).map((p) => p.getAttribute("fill"))
|
|
170
|
+
: [];
|
|
171
|
+
return {
|
|
172
|
+
status: document.getElementById("status").textContent,
|
|
173
|
+
paths: fills.length,
|
|
174
|
+
distinctFills: new Set(fills).size,
|
|
175
|
+
swatches: document.querySelectorAll("#palette .sw").length,
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
console.log(JSON.stringify(color, null, 2));
|
|
179
|
+
if (color.paths < 2 || color.distinctFills < 2) {
|
|
180
|
+
console.error("expected several colored paths in posterize mode");
|
|
181
|
+
process.exitCode = 1;
|
|
182
|
+
}
|
|
183
|
+
if (color.swatches !== color.paths) {
|
|
184
|
+
console.error("palette swatches should match the emitted layers");
|
|
185
|
+
process.exitCode = 1;
|
|
186
|
+
}
|
|
187
|
+
// The color slider's range is a promise the engine has to keep: it is worth
|
|
188
|
+
// asking for more than sixteen colors only if asking produces more of them.
|
|
189
|
+
// Measured on a portrait the palette keeps growing to 64 (16→15 layers,
|
|
190
|
+
// 32→24, 64→38) and the trace does not get slower for it, so 64 is where the
|
|
191
|
+
// slider stops.
|
|
192
|
+
const wide = await (async () => {
|
|
193
|
+
const top = await page.evaluate(() => +document.getElementById("colorCount").max);
|
|
194
|
+
const layersAt = async (n) => {
|
|
195
|
+
await page.evaluate((n) => {
|
|
196
|
+
document.getElementById("status").textContent = "…";
|
|
197
|
+
const cc = document.getElementById("colorCount");
|
|
198
|
+
cc.value = String(n);
|
|
199
|
+
cc.dispatchEvent(new Event("input", { bubbles: true }));
|
|
200
|
+
}, n);
|
|
201
|
+
await page.waitForFunction(() => /OK/.test(document.getElementById("status").textContent || ""),
|
|
202
|
+
{ timeout: 120000 });
|
|
203
|
+
return page.evaluate(() => new Set([...document.querySelectorAll("#outStage svg path")]
|
|
204
|
+
.map((p) => p.getAttribute("fill"))).size);
|
|
205
|
+
};
|
|
206
|
+
return { top, at16: await layersAt(16), atTop: await layersAt(top) };
|
|
207
|
+
})();
|
|
208
|
+
console.log(JSON.stringify(wide));
|
|
209
|
+
if (wide.top < 64) {
|
|
210
|
+
console.error("the color slider should reach 64, stops at " + wide.top);
|
|
211
|
+
process.exitCode = 1;
|
|
212
|
+
}
|
|
213
|
+
if (!(wide.atTop > wide.at16)) {
|
|
214
|
+
console.error("asking for more colors than 16 should produce more of them, got "
|
|
215
|
+
+ wide.at16 + " → " + wide.atTop);
|
|
216
|
+
process.exitCode = 1;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// The palette controls: a control that is on screen must be one that does
|
|
220
|
+
// something, and none of them may be clipped out of the panel.
|
|
221
|
+
const controls = await page.evaluate(() => {
|
|
222
|
+
const $ = (id) => document.getElementById(id);
|
|
223
|
+
const panel = document.querySelector(".body").getBoundingClientRect();
|
|
224
|
+
const overflow = (id) =>
|
|
225
|
+
Math.round($(id).getBoundingClientRect().right - panel.right);
|
|
226
|
+
const row = $("row-palEdit");
|
|
227
|
+
return {
|
|
228
|
+
autoShowsEditor: getComputedStyle(row).display !== "none",
|
|
229
|
+
autoMarksItIdle: row.classList.contains("pal-inactive"),
|
|
230
|
+
noteShown: !$("palNote").hidden,
|
|
231
|
+
biasOverflow: overflow("paletteBias"),
|
|
232
|
+
modeOverflow: overflow("paletteMode"),
|
|
233
|
+
bgOverflow: overflow("bgMode"),
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
console.log(JSON.stringify(controls, null, 2));
|
|
237
|
+
// Hiding the editor on "auto" put "Poimi tuloksesta" out of reach and made
|
|
238
|
+
// the whole group look inert; it stays visible and says it is idle instead.
|
|
239
|
+
if (!controls.autoShowsEditor || !controls.autoMarksItIdle || !controls.noteShown) {
|
|
240
|
+
console.error("the palette editor should be visible and marked idle on auto");
|
|
241
|
+
process.exitCode = 1;
|
|
242
|
+
}
|
|
243
|
+
if (controls.biasOverflow > 0 || controls.modeOverflow > 0 || controls.bgOverflow > 0) {
|
|
244
|
+
console.error("a select is clipped outside the parameter panel");
|
|
245
|
+
process.exitCode = 1;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Editing a color while the mode is "auto" must take effect, not vanish.
|
|
249
|
+
const before = color.paths;
|
|
250
|
+
await page.evaluate(() => {
|
|
251
|
+
document.getElementById("status").textContent = "…";
|
|
252
|
+
const inp = document.querySelector("#palEdit input");
|
|
253
|
+
inp.value = "#ff0055";
|
|
254
|
+
inp.dispatchEvent(new Event("input", { bubbles: true }));
|
|
255
|
+
});
|
|
256
|
+
await page.waitForFunction(() => {
|
|
257
|
+
const st = document.getElementById("status");
|
|
258
|
+
return st && /OK/.test(st.textContent || "");
|
|
259
|
+
}, { timeout: 60000 });
|
|
260
|
+
const edited = await page.evaluate(() => ({
|
|
261
|
+
mode: document.getElementById("paletteMode").value,
|
|
262
|
+
fills: Array.from(document.querySelectorAll("#outStage path"))
|
|
263
|
+
.map((p) => p.getAttribute("fill")),
|
|
264
|
+
}));
|
|
265
|
+
console.log(JSON.stringify(edited));
|
|
266
|
+
if (edited.mode !== "fixed") {
|
|
267
|
+
console.error("editing a color on auto should switch the palette to it");
|
|
268
|
+
process.exitCode = 1;
|
|
269
|
+
}
|
|
270
|
+
if (!edited.fills.includes("#FF0055")) {
|
|
271
|
+
console.error("the edited color should appear in the traced output");
|
|
272
|
+
process.exitCode = 1;
|
|
273
|
+
}
|
|
274
|
+
if (edited.fills.length === before) {
|
|
275
|
+
console.error("the palette change did not reach the output");
|
|
276
|
+
process.exitCode = 1;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Pasting an image loads and traces it; a paste with no image, or one aimed
|
|
280
|
+
// at a text field, is left alone.
|
|
281
|
+
const pasted = await page.evaluate(async () => {
|
|
282
|
+
const cv = document.createElement("canvas");
|
|
283
|
+
cv.width = 40; cv.height = 30;
|
|
284
|
+
const ctx = cv.getContext("2d");
|
|
285
|
+
ctx.fillStyle = "#ffffff"; ctx.fillRect(0, 0, 40, 30);
|
|
286
|
+
ctx.fillStyle = "#101010"; ctx.fillRect(8, 6, 24, 18);
|
|
287
|
+
const blob = await new Promise((r) => cv.toBlob(r, "image/png"));
|
|
288
|
+
const fire = (build) => {
|
|
289
|
+
const dt = new DataTransfer();
|
|
290
|
+
build(dt);
|
|
291
|
+
document.dispatchEvent(new ClipboardEvent("paste", {
|
|
292
|
+
clipboardData: dt, bubbles: true, cancelable: true,
|
|
293
|
+
}));
|
|
294
|
+
};
|
|
295
|
+
document.getElementById("status").textContent = "…";
|
|
296
|
+
fire((dt) => dt.items.add(new File([blob], "pasted.png", { type: "image/png" })));
|
|
297
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
298
|
+
const afterImage = document.getElementById("status").textContent;
|
|
299
|
+
|
|
300
|
+
// A paste carrying no image must not disturb anything.
|
|
301
|
+
document.getElementById("status").textContent = "UNTOUCHED";
|
|
302
|
+
fire((dt) => dt.setData("text/plain", "just words"));
|
|
303
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
304
|
+
const afterText = document.getElementById("status").textContent;
|
|
305
|
+
|
|
306
|
+
// Nor may it steal a paste meant for a text field.
|
|
307
|
+
const tf = document.createElement("input");
|
|
308
|
+
tf.type = "text";
|
|
309
|
+
document.body.appendChild(tf);
|
|
310
|
+
tf.focus();
|
|
311
|
+
document.getElementById("status").textContent = "UNTOUCHED";
|
|
312
|
+
fire((dt) => dt.items.add(new File([blob], "pasted.png", { type: "image/png" })));
|
|
313
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
314
|
+
const afterField = document.getElementById("status").textContent;
|
|
315
|
+
tf.remove();
|
|
316
|
+
|
|
317
|
+
return { afterImage, afterText, afterField, size: "40x30" };
|
|
318
|
+
});
|
|
319
|
+
console.log(JSON.stringify(pasted));
|
|
320
|
+
if (!/40×30/.test(pasted.afterImage)) {
|
|
321
|
+
console.error("pasting an image should load and trace it");
|
|
322
|
+
process.exitCode = 1;
|
|
323
|
+
}
|
|
324
|
+
if (pasted.afterText !== "UNTOUCHED" || pasted.afterField !== "UNTOUCHED") {
|
|
325
|
+
console.error("paste should be ignored without an image, and inside a text field");
|
|
326
|
+
process.exitCode = 1;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// The checkerboard is the only way to tell transparent from a painted
|
|
330
|
+
// background, so it must actually be behind both stages.
|
|
331
|
+
const view = await page.evaluate(() => {
|
|
332
|
+
const src = document.getElementById("srcStage");
|
|
333
|
+
const out = document.getElementById("outStage");
|
|
334
|
+
const on = src.classList.contains("checker") && out.classList.contains("checker");
|
|
335
|
+
const box = document.getElementById("checker");
|
|
336
|
+
box.checked = false;
|
|
337
|
+
box.dispatchEvent(new Event("change", { bubbles: true }));
|
|
338
|
+
const off = src.classList.contains("checker") || out.classList.contains("checker");
|
|
339
|
+
box.checked = true;
|
|
340
|
+
box.dispatchEvent(new Event("change", { bubbles: true }));
|
|
341
|
+
return { on, off, backAgain: out.classList.contains("checker") };
|
|
342
|
+
});
|
|
343
|
+
console.log(JSON.stringify(view));
|
|
344
|
+
if (!view.on || view.off || !view.backAgain) {
|
|
345
|
+
console.error("the checkerboard should be on by default and follow its checkbox");
|
|
346
|
+
process.exitCode = 1;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// --- edit mode ----------------------------------------------------------
|
|
350
|
+
// A traced layer is one path per color, so the thing that makes edit mode
|
|
351
|
+
// possible at all is splitting those into one path per shape. Check that,
|
|
352
|
+
// then each of the three tools and undo.
|
|
353
|
+
await page.evaluate(() => {
|
|
354
|
+
// Back to a palette taken from the picture: the test above left the mode on
|
|
355
|
+
// "fixed" with two colors, and a two-color drawing cannot show whether a
|
|
356
|
+
// tool that works on color is doing anything.
|
|
357
|
+
const m = document.getElementById("paletteMode");
|
|
358
|
+
m.value = "auto";
|
|
359
|
+
m.dispatchEvent(new Event("change", { bubbles: true }));
|
|
360
|
+
const c = document.getElementById("colorCount");
|
|
361
|
+
c.value = 8;
|
|
362
|
+
c.dispatchEvent(new Event("input", { bubbles: true }));
|
|
363
|
+
});
|
|
364
|
+
await page.click("#sample");
|
|
365
|
+
await page.waitForFunction(() => !document.getElementById("dl").disabled, { timeout: 120000 });
|
|
366
|
+
// A queued re-trace replaces the drawing and ends the edit session with it,
|
|
367
|
+
// so wait for the tracer to go idle before opening one.
|
|
368
|
+
await page.waitForFunction(() => !document.getElementById("run").disabled, { timeout: 120000 });
|
|
369
|
+
await page.waitForTimeout(300);
|
|
370
|
+
|
|
371
|
+
const edit = await (async () => {
|
|
372
|
+
const before = await page.evaluate(() => document.querySelectorAll("#outStage svg path").length);
|
|
373
|
+
await page.click("#editToggle");
|
|
374
|
+
const exploded = await page.evaluate(() => document.querySelectorAll("#outStage svg path").length);
|
|
375
|
+
|
|
376
|
+
// The status line quotes a shape count and a byte count, and editing
|
|
377
|
+
// changes both — it used to go on quoting the file the tracer produced,
|
|
378
|
+
// which after a few edits describes something that no longer exists.
|
|
379
|
+
await page.waitForTimeout(250);
|
|
380
|
+
const statusLive = await page.evaluate(() => {
|
|
381
|
+
const m = document.getElementById("status").textContent.match(/layers=(\d+).*svg=(\d+)/);
|
|
382
|
+
const live = document.querySelector("#outStage svg");
|
|
383
|
+
return {
|
|
384
|
+
saysLayers: +m[1], saysBytes: +m[2],
|
|
385
|
+
realLayers: live.querySelectorAll("path").length,
|
|
386
|
+
realBytes: new XMLSerializer().serializeToString(live).length
|
|
387
|
+
};
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Tarkennin is the default tool, so nothing below can assume which tool is
|
|
391
|
+
// live — every one of them selects itself first.
|
|
392
|
+
const defaultTool = await page.evaluate(() => ({
|
|
393
|
+
refine: document.getElementById("toolRefine").classList.contains("active"),
|
|
394
|
+
merge: document.getElementById("toolMerge").classList.contains("active"),
|
|
395
|
+
stage: document.getElementById("outStage").classList.contains("refining"),
|
|
396
|
+
}));
|
|
397
|
+
|
|
398
|
+
// merge: set the picker, click the biggest shape, count what took the color
|
|
399
|
+
await page.click("#toolMerge");
|
|
400
|
+
await page.evaluate(() => {
|
|
401
|
+
const c = document.getElementById("editColor");
|
|
402
|
+
c.value = "#00ff88";
|
|
403
|
+
c.dispatchEvent(new Event("input", { bubbles: true }));
|
|
404
|
+
const ps = [...document.querySelectorAll("#outStage svg path")];
|
|
405
|
+
ps.sort((a, b) => {
|
|
406
|
+
const x = a.getBBox(), y = b.getBBox();
|
|
407
|
+
return y.width * y.height - x.width * x.height;
|
|
408
|
+
});
|
|
409
|
+
ps[1].dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
410
|
+
});
|
|
411
|
+
const green = () => page.evaluate(() =>
|
|
412
|
+
[...document.querySelectorAll("#outStage svg path")]
|
|
413
|
+
.filter(e => (e.getAttribute("fill") || "").toLowerCase() === "#00ff88").length);
|
|
414
|
+
const merged = await green();
|
|
415
|
+
await page.click("#editUndo");
|
|
416
|
+
const undone = await green();
|
|
417
|
+
|
|
418
|
+
// pick: take a shape's own fill into the picker
|
|
419
|
+
await page.click("#toolPick");
|
|
420
|
+
const want = await page.evaluate(() => {
|
|
421
|
+
const el = document.querySelectorAll("#outStage svg path")[2];
|
|
422
|
+
el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
423
|
+
return (el.getAttribute("fill") || "").toLowerCase();
|
|
424
|
+
});
|
|
425
|
+
const picked = await page.evaluate(() => document.getElementById("editColor").value.toLowerCase());
|
|
426
|
+
|
|
427
|
+
// refine: drag over an area, which re-traces it with a palette of its own
|
|
428
|
+
// and lays the result on top, masked to the stroke. The mask is the part
|
|
429
|
+
// worth guarding — resolved in the wrong coordinate system it hides the
|
|
430
|
+
// whole result, and the tool then reports success while changing nothing.
|
|
431
|
+
await page.click("#toolRefine");
|
|
432
|
+
// Force the patch to differ from what is already there. With the palette
|
|
433
|
+
// seeded from the border and the mask feathered, a refine stroke can quite
|
|
434
|
+
// correctly change nothing — it had nothing to add — and then this test
|
|
435
|
+
// would pass a broken mask as a no-op. Given its own colors to invent and
|
|
436
|
+
// no edge snapping, it must change something, and that is what makes the
|
|
437
|
+
// "is the mask hiding everything" check mean anything.
|
|
438
|
+
await page.evaluate(() => {
|
|
439
|
+
const x = document.getElementById("refineExtra");
|
|
440
|
+
x.value = 12; x.dispatchEvent(new Event("input", { bubbles: true }));
|
|
441
|
+
const e = document.getElementById("edgeBlend");
|
|
442
|
+
e.value = 0; e.dispatchEvent(new Event("input", { bubbles: true }));
|
|
443
|
+
});
|
|
444
|
+
const refined = await page.evaluate(async () => {
|
|
445
|
+
const svg = document.querySelector("#outStage svg");
|
|
446
|
+
const rect = svg.getBoundingClientRect();
|
|
447
|
+
// Rasterize a probe inside the stroke: "one group was added" is not the
|
|
448
|
+
// same claim as "the picture changed", and the mask bug satisfied the
|
|
449
|
+
// first while failing the second.
|
|
450
|
+
const shot = async () => {
|
|
451
|
+
const str = new XMLSerializer().serializeToString(svg);
|
|
452
|
+
const im = new Image();
|
|
453
|
+
im.src = "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(str)));
|
|
454
|
+
await im.decode();
|
|
455
|
+
await new Promise(r => setTimeout(r, 150));
|
|
456
|
+
const vb = svg.getAttribute("viewBox").split(/[\s,]+/).map(Number);
|
|
457
|
+
const c = document.createElement("canvas");
|
|
458
|
+
c.width = vb[2]; c.height = vb[3];
|
|
459
|
+
const g = c.getContext("2d");
|
|
460
|
+
g.fillStyle = "#fff"; g.fillRect(0, 0, c.width, c.height);
|
|
461
|
+
g.drawImage(im, 0, 0, c.width, c.height);
|
|
462
|
+
return g.getImageData(0, 0, c.width, c.height).data;
|
|
463
|
+
};
|
|
464
|
+
const vb = svg.getAttribute("viewBox").split(/[\s,]+/).map(Number);
|
|
465
|
+
const before = await shot();
|
|
466
|
+
const stage = document.getElementById("outStage");
|
|
467
|
+
const at = (fx, fy) => ({ x: rect.left + rect.width * fx, y: rect.top + rect.height * fy });
|
|
468
|
+
const send = (type, pt) => stage.dispatchEvent(new PointerEvent(type, {
|
|
469
|
+
bubbles: true, clientX: pt.x, clientY: pt.y
|
|
470
|
+
}));
|
|
471
|
+
send("pointerdown", at(0.3, 0.45));
|
|
472
|
+
for (let i = 1; i <= 12; i++) send("pointermove", at(0.3 + 0.4 * i / 12, 0.45));
|
|
473
|
+
const preview = svg.querySelectorAll('path[stroke="#ff2ec4"]').length;
|
|
474
|
+
send("pointerup", at(0.7, 0.45));
|
|
475
|
+
await new Promise(r => setTimeout(r, 400));
|
|
476
|
+
const after = await shot();
|
|
477
|
+
let changed = 0;
|
|
478
|
+
const y0 = Math.round(vb[3] * 0.40), y1 = Math.round(vb[3] * 0.50);
|
|
479
|
+
const x0 = Math.round(vb[2] * 0.32), x1 = Math.round(vb[2] * 0.68);
|
|
480
|
+
for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++) {
|
|
481
|
+
const o = (y * vb[2] + x) * 4;
|
|
482
|
+
if (Math.abs(before[o] - after[o]) + Math.abs(before[o+1] - after[o+1])
|
|
483
|
+
+ Math.abs(before[o+2] - after[o+2]) > 24) changed++;
|
|
484
|
+
}
|
|
485
|
+
return { preview, previewLeft: svg.querySelectorAll('path[stroke="#ff2ec4"]').length, changed };
|
|
486
|
+
});
|
|
487
|
+
const groups = await page.evaluate(() => document.querySelectorAll("#outStage svg g[mask]").length);
|
|
488
|
+
const masks = await page.evaluate(() => document.querySelectorAll("#outStage svg mask").length);
|
|
489
|
+
await page.click("#editUndo");
|
|
490
|
+
const groupsUndone = await page.evaluate(() => document.querySelectorAll("#outStage svg g[mask]").length);
|
|
491
|
+
const masksUndone = await page.evaluate(() => document.querySelectorAll("#outStage svg mask").length);
|
|
492
|
+
|
|
493
|
+
// smooth: brush over the biggest shape's own colour and check the outline
|
|
494
|
+
// gets simpler rather than heavier, and that undo restores it exactly.
|
|
495
|
+
await page.click("#toolSmooth");
|
|
496
|
+
const smooth = await page.evaluate(async () => {
|
|
497
|
+
const svg = document.querySelector("#outStage svg");
|
|
498
|
+
const ps = [...svg.querySelectorAll("path")];
|
|
499
|
+
ps.sort((a, b) => {
|
|
500
|
+
const x = a.getBBox(), y = b.getBBox();
|
|
501
|
+
return y.width * y.height - x.width * x.height;
|
|
502
|
+
});
|
|
503
|
+
// The biggest shape is usually the background frame; take the biggest one
|
|
504
|
+
// that actually has an outline worth smoothing.
|
|
505
|
+
const el = ps.find(e => (e.getAttribute("d") || "").length > 400) || ps[1];
|
|
506
|
+
el.id = "__smoothTarget";
|
|
507
|
+
const fill = el.getAttribute("fill");
|
|
508
|
+
const c = document.getElementById("editColor");
|
|
509
|
+
c.value = fill; c.dispatchEvent(new Event("input", { bubbles: true }));
|
|
510
|
+
const bb = el.getBBox();
|
|
511
|
+
const rect = svg.getBoundingClientRect();
|
|
512
|
+
const vb = svg.getAttribute("viewBox").split(/[\s,]+/).map(Number);
|
|
513
|
+
const toScreen = (ux, uy) => ({
|
|
514
|
+
x: rect.left + ux / vb[2] * rect.width,
|
|
515
|
+
y: rect.top + uy / vb[3] * rect.height
|
|
516
|
+
});
|
|
517
|
+
const before = el.getAttribute("d").length;
|
|
518
|
+
const stage = document.getElementById("outStage");
|
|
519
|
+
const send = (type, pt) => stage.dispatchEvent(new PointerEvent(type, {
|
|
520
|
+
bubbles: true, clientX: pt.x, clientY: pt.y
|
|
521
|
+
}));
|
|
522
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
523
|
+
send("pointerdown", toScreen(bb.x + bb.width * 0.2, bb.y + bb.height * 0.5));
|
|
524
|
+
for (let i = 1; i <= 12; i++) {
|
|
525
|
+
send("pointermove", toScreen(bb.x + bb.width * (0.2 + 0.6 * i / 12), bb.y + bb.height * 0.5));
|
|
526
|
+
}
|
|
527
|
+
send("pointerup", toScreen(bb.x + bb.width * 0.8, bb.y + bb.height * 0.5));
|
|
528
|
+
}
|
|
529
|
+
return { before, after: el.getAttribute("d").length };
|
|
530
|
+
});
|
|
531
|
+
// A stroke is one undo step however many shapes it crossed, so getting the
|
|
532
|
+
// outline back takes as many clicks as there were strokes that changed
|
|
533
|
+
// something — never one per shape they ran over, which is what recording a
|
|
534
|
+
// shape at a time cost, and which is what this counts.
|
|
535
|
+
let smoothSteps = 0;
|
|
536
|
+
let smoothUndone = -1;
|
|
537
|
+
for (let i = 0; i < 12; i++) {
|
|
538
|
+
const state = await page.evaluate(() => {
|
|
539
|
+
const el = document.getElementById("__smoothTarget");
|
|
540
|
+
return { d: el ? el.getAttribute("d").length : -1,
|
|
541
|
+
empty: document.getElementById("editNote").textContent === "Ei kumottavaa." };
|
|
542
|
+
});
|
|
543
|
+
smoothUndone = state.d;
|
|
544
|
+
if (state.d === smooth.before || state.empty) break;
|
|
545
|
+
await page.click("#editUndo");
|
|
546
|
+
smoothSteps++;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// wand: pick an object out and cut the rest away. Four things have to
|
|
550
|
+
// hold. The selection grows with the tolerance — a wand that only ever
|
|
551
|
+
// takes the shape under the cursor is the merge tool with extra steps. A
|
|
552
|
+
// rough outline drawn by hand selects too, and selects something else. The
|
|
553
|
+
// cut must not *uncover* anything: a traced picture is stacked, so a lower
|
|
554
|
+
// layer's shape is the union of itself and everything painted over it, and
|
|
555
|
+
// deleting what is on top used to reveal geometry that was never visible.
|
|
556
|
+
// And all of it comes back in one click.
|
|
557
|
+
await page.click("#toolWand");
|
|
558
|
+
const wand = await (async () => {
|
|
559
|
+
// Seed from a real click point, the way a person does: half the shapes
|
|
560
|
+
// in a stacked drawing are buried under later layers and can neither be
|
|
561
|
+
// clicked nor seen.
|
|
562
|
+
const grow = (tol) => page.evaluate((tol) => {
|
|
563
|
+
const t = document.getElementById("wandTol");
|
|
564
|
+
t.value = String(tol);
|
|
565
|
+
t.dispatchEvent(new Event("input", { bubbles: true }));
|
|
566
|
+
const svg = document.querySelector("#outStage svg");
|
|
567
|
+
const r = svg.getBoundingClientRect();
|
|
568
|
+
const el = document.elementFromPoint(r.left + r.width * 0.5, r.top + r.height * 0.55);
|
|
569
|
+
if (!el || el.tagName !== "path") return -1;
|
|
570
|
+
el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
571
|
+
return [...svg.querySelectorAll("path:not(.wand-off)")]
|
|
572
|
+
.filter((p) => !p.closest("mask") && !p.closest("clipPath")).length;
|
|
573
|
+
}, tol);
|
|
574
|
+
const tight = await grow(0);
|
|
575
|
+
const loose = await grow(140);
|
|
576
|
+
|
|
577
|
+
// The hand-drawn outline: a wobbling box over the middle of the picture.
|
|
578
|
+
// Nothing is cut along the line — shapes decide, by how much of what you
|
|
579
|
+
// can see of them falls inside it — so the result lands on the picture's
|
|
580
|
+
// own edges.
|
|
581
|
+
const lasso = await page.evaluate(() => {
|
|
582
|
+
const svg = document.querySelector("#outStage svg");
|
|
583
|
+
const r = svg.getBoundingClientRect();
|
|
584
|
+
const stage = document.getElementById("outStage");
|
|
585
|
+
const at = (fx, fy) => ({ x: r.left + r.width * fx, y: r.top + r.height * fy });
|
|
586
|
+
const send = (t, p) => stage.dispatchEvent(new PointerEvent(t, {
|
|
587
|
+
bubbles: true, clientX: p.x, clientY: p.y }));
|
|
588
|
+
const box = [[0.25, 0.2], [0.75, 0.22], [0.78, 0.75], [0.22, 0.72]];
|
|
589
|
+
const pts = [];
|
|
590
|
+
for (let i = 0; i < box.length; i++) {
|
|
591
|
+
const a = box[i], b = box[(i + 1) % box.length];
|
|
592
|
+
for (let k = 0; k < 8; k++) {
|
|
593
|
+
pts.push(at(a[0] + (b[0] - a[0]) * k / 8, a[1] + (b[1] - a[1]) * k / 8));
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
send("pointerdown", pts[0]);
|
|
597
|
+
pts.slice(1).forEach((p) => send("pointermove", p));
|
|
598
|
+
send("pointerup", pts[pts.length - 1]);
|
|
599
|
+
window.__smokeLasso = box;
|
|
600
|
+
return {
|
|
601
|
+
sel: [...svg.querySelectorAll("path:not(.wand-off)")]
|
|
602
|
+
.filter((p) => !p.closest("mask") && !p.closest("clipPath")).length,
|
|
603
|
+
left: svg.querySelectorAll('path[stroke="#ff2ec4"]').length,
|
|
604
|
+
// A line drawn through shapes has to clip the ones it cut, or the
|
|
605
|
+
// background reaching across it comes along whole.
|
|
606
|
+
clips: svg.querySelectorAll("clipPath[id^=wandclip]").length,
|
|
607
|
+
clipped: svg.querySelectorAll("path[clip-path]").length,
|
|
608
|
+
};
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// Rasterize before and after. Every pixel the cut leaves painted has to
|
|
612
|
+
// carry the colour it already had; a pixel that changes colour is
|
|
613
|
+
// geometry that was hidden and is not any more.
|
|
614
|
+
// Both shots are taken in one fixed frame. The cut pulls the frame in,
|
|
615
|
+
// and comparing two rasters of different frames compares pixels that do
|
|
616
|
+
// not stand for the same place — which reads as 9% of the picture having
|
|
617
|
+
// moved when nothing has.
|
|
618
|
+
const shot = (frame) => page.evaluate(async (frame) => {
|
|
619
|
+
const svg = document.querySelector("#outStage svg");
|
|
620
|
+
const vb = frame.split(/[\s,]+/).map(Number);
|
|
621
|
+
const str = new XMLSerializer().serializeToString(svg)
|
|
622
|
+
.replace(/viewBox="[^"]*"/, 'viewBox="' + frame + '"')
|
|
623
|
+
.replace(/width="[^"]*"/, 'width="' + vb[2] + '"')
|
|
624
|
+
.replace(/height="[^"]*"/, 'height="' + vb[3] + '"');
|
|
625
|
+
const im = new Image();
|
|
626
|
+
im.src = "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(str)));
|
|
627
|
+
await im.decode();
|
|
628
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
629
|
+
// The <img> renders at the SVG's own width/height, which is its
|
|
630
|
+
// viewBox, so drawing it to fill the canvas maps user space linearly.
|
|
631
|
+
const c = document.createElement("canvas");
|
|
632
|
+
c.width = 240; c.height = Math.max(1, Math.round(240 * vb[3] / vb[2]));
|
|
633
|
+
const g = c.getContext("2d");
|
|
634
|
+
g.drawImage(im, 0, 0, c.width, c.height);
|
|
635
|
+
return { w: c.width, h: c.height, vb,
|
|
636
|
+
px: Array.from(g.getImageData(0, 0, c.width, c.height).data) };
|
|
637
|
+
}, frame);
|
|
638
|
+
const frameBefore = await page.evaluate(() =>
|
|
639
|
+
document.querySelector("#outStage svg").getAttribute("viewBox"));
|
|
640
|
+
const before = await shot(frameBefore);
|
|
641
|
+
// Shapes of the drawing: not the ones the tool parks inside a <mask> or
|
|
642
|
+
// a <clipPath>, which are machinery rather than picture.
|
|
643
|
+
const drawn = () => page.evaluate(() =>
|
|
644
|
+
[...document.querySelectorAll("#outStage svg path")]
|
|
645
|
+
.filter((p) => !p.closest("mask") && !p.closest("clipPath")).length);
|
|
646
|
+
const shapesBefore = await drawn();
|
|
647
|
+
await page.click("#wandCut");
|
|
648
|
+
const cut = await page.evaluate(() => {
|
|
649
|
+
const svg = document.querySelector("#outStage svg");
|
|
650
|
+
return {
|
|
651
|
+
// Paths inside the mask are not shapes of the drawing: the cut moves
|
|
652
|
+
// what it removes in there rather than cloning it.
|
|
653
|
+
shapes: [...svg.querySelectorAll("path")]
|
|
654
|
+
.filter((p) => !p.closest("mask") && !p.closest("clipPath")).length,
|
|
655
|
+
frame: svg.getAttribute("viewBox"),
|
|
656
|
+
marked: svg.querySelectorAll(".wand-off").length,
|
|
657
|
+
masks: svg.querySelectorAll("mask[id^=wandmask]").length,
|
|
658
|
+
};
|
|
659
|
+
});
|
|
660
|
+
const after = await shot(frameBefore);
|
|
661
|
+
// Interior pixels only. A kept shape's edge used to be composited
|
|
662
|
+
// against the neighbour that has just been removed and is now composited
|
|
663
|
+
// against nothing, so the one-pixel antialiased fringe legitimately
|
|
664
|
+
// changes colour all over the drawing — 7% of it, which is noise, not
|
|
665
|
+
// uncovering. Uncovered geometry is area, and area has an interior.
|
|
666
|
+
let painted = 0, moved = 0;
|
|
667
|
+
for (let y = 1; y + 1 < after.h; y++) {
|
|
668
|
+
for (let x = 1; x + 1 < after.w; x++) {
|
|
669
|
+
const o = (y * after.w + x) * 4;
|
|
670
|
+
if (after.px[o + 3] < 255) continue;
|
|
671
|
+
if (after.px[o - 4 + 3] < 255 || after.px[o + 4 + 3] < 255
|
|
672
|
+
|| after.px[o - after.w * 4 + 3] < 255
|
|
673
|
+
|| after.px[o + after.w * 4 + 3] < 255) continue;
|
|
674
|
+
painted++;
|
|
675
|
+
if (Math.abs(before.px[o] - after.px[o])
|
|
676
|
+
+ Math.abs(before.px[o + 1] - after.px[o + 1])
|
|
677
|
+
+ Math.abs(before.px[o + 2] - after.px[o + 2]) > 60) moved++;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
// How far past the drawn line the result reaches. Taking every admitted
|
|
681
|
+
// shape whole let one background shape crossing the line drag its whole
|
|
682
|
+
// visible area in, and the enclosure rule then closed over everything
|
|
683
|
+
// that shape surrounded — the flag, the curtain and the desk came along
|
|
684
|
+
// with the person. A shape the line cuts through is clipped to it now,
|
|
685
|
+
// so what sticks out is only the overhang of shapes that were almost
|
|
686
|
+
// entirely inside anyway.
|
|
687
|
+
const leak = await page.evaluate((box) => {
|
|
688
|
+
const svg = document.querySelector("#outStage svg");
|
|
689
|
+
const vb = svg.getAttribute("viewBox").split(/[\s,]+/).map(Number);
|
|
690
|
+
const c = document.createElement("canvas");
|
|
691
|
+
c.width = 200; c.height = Math.max(1, Math.round(200 * vb[3] / vb[2]));
|
|
692
|
+
const g = c.getContext("2d");
|
|
693
|
+
// The lasso, in the frame's own coordinates. It was drawn as fractions
|
|
694
|
+
// of the original 320×221 stage.
|
|
695
|
+
const poly = box.map((p) => [p[0] * 320, p[1] * 221]);
|
|
696
|
+
const inPoly = (x, y) => { let inside = false;
|
|
697
|
+
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
|
698
|
+
const [xi, yi] = poly[i], [xj, yj] = poly[j];
|
|
699
|
+
if (((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) {
|
|
700
|
+
inside = !inside;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return inside; };
|
|
704
|
+
return new Promise((done) => {
|
|
705
|
+
const im = new Image();
|
|
706
|
+
im.onload = () => {
|
|
707
|
+
g.drawImage(im, 0, 0, c.width, c.height);
|
|
708
|
+
const d = g.getImageData(0, 0, c.width, c.height).data;
|
|
709
|
+
let painted = 0, outside = 0;
|
|
710
|
+
for (let y = 0; y < c.height; y++) {
|
|
711
|
+
for (let x = 0; x < c.width; x++) {
|
|
712
|
+
if (d[(y * c.width + x) * 4 + 3] < 200) continue;
|
|
713
|
+
painted++;
|
|
714
|
+
const ux = vb[0] + (x + 0.5) * vb[2] / c.width;
|
|
715
|
+
const uy = vb[1] + (y + 0.5) * vb[3] / c.height;
|
|
716
|
+
if (!inPoly(ux, uy)) outside++;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
done({ painted, outside });
|
|
720
|
+
};
|
|
721
|
+
im.src = "data:image/svg+xml;base64,"
|
|
722
|
+
+ btoa(unescape(encodeURIComponent(new XMLSerializer().serializeToString(svg))));
|
|
723
|
+
});
|
|
724
|
+
}, await page.evaluate(() => window.__smokeLasso));
|
|
725
|
+
|
|
726
|
+
await page.click("#editUndo");
|
|
727
|
+
const back = await page.evaluate(() => {
|
|
728
|
+
const svg = document.querySelector("#outStage svg");
|
|
729
|
+
return { shapes: [...svg.querySelectorAll("path")]
|
|
730
|
+
.filter((p) => !p.closest("mask") && !p.closest("clipPath")).length,
|
|
731
|
+
frame: svg.getAttribute("viewBox"),
|
|
732
|
+
masks: svg.querySelectorAll("mask[id^=wandmask]").length,
|
|
733
|
+
clips: svg.querySelectorAll("clipPath[id^=wandclip]").length };
|
|
734
|
+
});
|
|
735
|
+
return { tight, loose, lasso, shapesBefore, frameBefore, cut, back, painted, moved, leak };
|
|
736
|
+
})();
|
|
737
|
+
await page.click("#toolRefine");
|
|
738
|
+
|
|
739
|
+
// The refine-size slider must not re-trace: a re-trace replaces the
|
|
740
|
+
// drawing and takes the edit session down with it, which is a strange way
|
|
741
|
+
// to lose your work while sizing the next click.
|
|
742
|
+
await page.evaluate(() => {
|
|
743
|
+
const r = document.getElementById("refineSize");
|
|
744
|
+
r.value = 80;
|
|
745
|
+
r.dispatchEvent(new Event("input", { bubbles: true }));
|
|
746
|
+
});
|
|
747
|
+
await page.waitForTimeout(400);
|
|
748
|
+
const stillEditing = await page.evaluate(() =>
|
|
749
|
+
document.getElementById("editbar").classList.contains("on"));
|
|
750
|
+
return { before, exploded, statusLive, defaultTool, merged, undone, picked, want, wand, smoothSteps,
|
|
751
|
+
groups, groupsUndone, stillEditing,
|
|
752
|
+
masks, masksUndone, preview: refined.preview, previewLeft: refined.previewLeft,
|
|
753
|
+
refineChangedPixels: refined.changed,
|
|
754
|
+
smoothBefore: smooth.before, smoothAfter: smooth.after, smoothUndone };
|
|
755
|
+
})();
|
|
756
|
+
console.log(JSON.stringify(edit));
|
|
757
|
+
if (!(edit.exploded > edit.before)) {
|
|
758
|
+
console.error("edit mode must split color layers into individual shapes");
|
|
759
|
+
process.exitCode = 1;
|
|
760
|
+
}
|
|
761
|
+
if (edit.statusLive.saysLayers !== edit.statusLive.realLayers
|
|
762
|
+
|| edit.statusLive.saysBytes !== edit.statusLive.realBytes) {
|
|
763
|
+
console.error("the status line must describe the drawing on screen, got "
|
|
764
|
+
+ JSON.stringify(edit.statusLive));
|
|
765
|
+
process.exitCode = 1;
|
|
766
|
+
}
|
|
767
|
+
if (edit.merged !== 1 || edit.undone !== 0) {
|
|
768
|
+
console.error("merge should recolor exactly one shape, and undo should put it back");
|
|
769
|
+
process.exitCode = 1;
|
|
770
|
+
}
|
|
771
|
+
if (edit.picked !== edit.want) {
|
|
772
|
+
console.error("the picker should take the clicked shape's fill, got " + edit.picked + " want " + edit.want);
|
|
773
|
+
process.exitCode = 1;
|
|
774
|
+
}
|
|
775
|
+
if (edit.groups !== 1 || edit.groupsUndone !== 0 || edit.masks !== 1 || edit.masksUndone !== 0) {
|
|
776
|
+
console.error("a refine stroke should add one masked group and its mask, and undo should take both away");
|
|
777
|
+
process.exitCode = 1;
|
|
778
|
+
}
|
|
779
|
+
if (!(edit.refineChangedPixels > 0)) {
|
|
780
|
+
console.error("a refine stroke must change the picture under it, not just add a hidden group");
|
|
781
|
+
process.exitCode = 1;
|
|
782
|
+
}
|
|
783
|
+
if (edit.preview !== 1 || edit.previewLeft !== 0) {
|
|
784
|
+
console.error("the stroke preview should show while dragging and be gone afterwards");
|
|
785
|
+
process.exitCode = 1;
|
|
786
|
+
}
|
|
787
|
+
if (!(edit.smoothAfter > 0) || edit.smoothAfter === edit.smoothBefore) {
|
|
788
|
+
console.error("the smooth brush should change the outline it is dragged over");
|
|
789
|
+
process.exitCode = 1;
|
|
790
|
+
}
|
|
791
|
+
if (edit.smoothUndone !== edit.smoothBefore) {
|
|
792
|
+
console.error("undo should restore the smoothed outline exactly, got "
|
|
793
|
+
+ edit.smoothUndone + " want " + edit.smoothBefore);
|
|
794
|
+
process.exitCode = 1;
|
|
795
|
+
}
|
|
796
|
+
if (edit.smoothSteps !== 3) {
|
|
797
|
+
console.error("three smooth strokes should take exactly three clicks back, took "
|
|
798
|
+
+ edit.smoothSteps);
|
|
799
|
+
process.exitCode = 1;
|
|
800
|
+
}
|
|
801
|
+
if (!edit.stillEditing) {
|
|
802
|
+
console.error("sizing the refine box must not re-trace and drop the edit session");
|
|
803
|
+
process.exitCode = 1;
|
|
804
|
+
}
|
|
805
|
+
if (!edit.defaultTool.refine || edit.defaultTool.merge || !edit.defaultTool.stage) {
|
|
806
|
+
console.error("edit mode should open on Tarkennin, got " + JSON.stringify(edit.defaultTool));
|
|
807
|
+
process.exitCode = 1;
|
|
808
|
+
}
|
|
809
|
+
if (!(edit.wand.loose > edit.wand.tight) || edit.wand.tight < 1) {
|
|
810
|
+
console.error("the wand selection should grow with its tolerance, got "
|
|
811
|
+
+ edit.wand.tight + " → " + edit.wand.loose);
|
|
812
|
+
process.exitCode = 1;
|
|
813
|
+
}
|
|
814
|
+
if (edit.wand.lasso.sel < 2 || edit.wand.lasso.sel === edit.wand.loose
|
|
815
|
+
|| edit.wand.lasso.left !== 0 || edit.wand.lasso.clips !== 1
|
|
816
|
+
|| edit.wand.lasso.clipped < 1) {
|
|
817
|
+
console.error("a hand-drawn outline should select an area of its own and clean up after itself, got "
|
|
818
|
+
+ JSON.stringify(edit.wand.lasso));
|
|
819
|
+
process.exitCode = 1;
|
|
820
|
+
}
|
|
821
|
+
if (edit.wand.cut.shapes !== edit.wand.lasso.sel || edit.wand.cut.marked !== 0) {
|
|
822
|
+
console.error("the cut should leave exactly the selection behind, got "
|
|
823
|
+
+ JSON.stringify(edit.wand.cut) + " want " + edit.wand.lasso.sel);
|
|
824
|
+
process.exitCode = 1;
|
|
825
|
+
}
|
|
826
|
+
if (!(edit.wand.cut.shapes < edit.wand.shapesBefore)
|
|
827
|
+
|| edit.wand.cut.frame === edit.wand.frameBefore) {
|
|
828
|
+
console.error("the cut should drop the rest of the drawing and pull the frame in, got "
|
|
829
|
+
+ JSON.stringify(edit.wand.cut));
|
|
830
|
+
process.exitCode = 1;
|
|
831
|
+
}
|
|
832
|
+
// The one that catches the stacking bug: cutting a box out of a picture with
|
|
833
|
+
// a ball over it returned the box with the ball's bulge painted blue, and
|
|
834
|
+
// the tool reported success either way.
|
|
835
|
+
if (!(edit.wand.painted > 500) || edit.wand.moved > edit.wand.painted * 0.005) {
|
|
836
|
+
console.error("the cut must not uncover hidden geometry: " + edit.wand.moved
|
|
837
|
+
+ " of " + edit.wand.painted + " painted pixels changed colour");
|
|
838
|
+
process.exitCode = 1;
|
|
839
|
+
}
|
|
840
|
+
if (!(edit.wand.leak.painted > 500)
|
|
841
|
+
|| edit.wand.leak.outside > edit.wand.leak.painted * 0.05) {
|
|
842
|
+
console.error("a hand-drawn outline should bound the cut: " + edit.wand.leak.outside
|
|
843
|
+
+ " of " + edit.wand.leak.painted + " painted pixels landed outside the line");
|
|
844
|
+
process.exitCode = 1;
|
|
845
|
+
}
|
|
846
|
+
if (edit.wand.back.shapes !== edit.wand.shapesBefore
|
|
847
|
+
|| edit.wand.back.frame !== edit.wand.frameBefore || edit.wand.back.masks !== 0
|
|
848
|
+
|| edit.wand.back.clips !== 0) {
|
|
849
|
+
console.error("undo should put every cut shape back and take the mask, the clip and the frame with it, got "
|
|
850
|
+
+ JSON.stringify(edit.wand.back) + " want " + edit.wand.shapesBefore
|
|
851
|
+
+ " / " + edit.wand.frameBefore);
|
|
852
|
+
process.exitCode = 1;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// --- pre-processing ------------------------------------------------------
|
|
856
|
+
// Filters that run on the bitmap before anything is quantized. The one that
|
|
857
|
+
// has to hold is that they reach the tracer at all: the chain is built in the
|
|
858
|
+
// page and the engine only ever sees its output, so a wiring mistake is
|
|
859
|
+
// invisible except by tracing the same image twice.
|
|
860
|
+
const pre = await (async () => {
|
|
861
|
+
// Leave edit mode and re-trace first: the status quotes the live drawing,
|
|
862
|
+
// and the edit tests above left a modified one, so "restores exactly" would
|
|
863
|
+
// be comparing an edited picture against a fresh trace.
|
|
864
|
+
if (await page.evaluate(() => document.getElementById("editbar").classList.contains("on"))) {
|
|
865
|
+
await page.click("#editToggle");
|
|
866
|
+
}
|
|
867
|
+
await page.click("#run");
|
|
868
|
+
await page.waitForFunction(() => !document.getElementById("run").disabled, { timeout: 120000 });
|
|
869
|
+
await page.waitForTimeout(350);
|
|
870
|
+
|
|
871
|
+
const hiddenAtFirst = await page.evaluate(() =>
|
|
872
|
+
!document.getElementById("prebar").classList.contains("on"));
|
|
873
|
+
await page.click("#preToggle");
|
|
874
|
+
const opens = await page.evaluate(() =>
|
|
875
|
+
document.getElementById("prebar").classList.contains("on"));
|
|
876
|
+
|
|
877
|
+
const bytes = () => page.evaluate(() => {
|
|
878
|
+
const m = document.getElementById("status").textContent.match(/svg=(\d+)/);
|
|
879
|
+
return m ? +m[1] : -1;
|
|
880
|
+
});
|
|
881
|
+
const settle = async () => {
|
|
882
|
+
await page.waitForFunction(() => !document.getElementById("run").disabled, { timeout: 120000 });
|
|
883
|
+
await page.waitForTimeout(350);
|
|
884
|
+
};
|
|
885
|
+
await settle();
|
|
886
|
+
const plain = await bytes();
|
|
887
|
+
|
|
888
|
+
// Blur is the one that must visibly simplify: it decides how much detail
|
|
889
|
+
// reaches the tracer at all.
|
|
890
|
+
// input redraws, change is what a released slider sends and what the undo
|
|
891
|
+
// stack records — a real drag sends both.
|
|
892
|
+
await page.evaluate(() => {
|
|
893
|
+
const e = document.getElementById("preBlur");
|
|
894
|
+
e.value = 4;
|
|
895
|
+
e.dispatchEvent(new Event("input", { bubbles: true }));
|
|
896
|
+
e.dispatchEvent(new Event("change", { bubbles: true }));
|
|
897
|
+
});
|
|
898
|
+
await settle();
|
|
899
|
+
const blurred = await bytes();
|
|
900
|
+
const undoLive = await page.evaluate(() => !document.getElementById("preUndo").disabled);
|
|
901
|
+
|
|
902
|
+
// A second change, then step back over it: undo works on the settings, so
|
|
903
|
+
// one step has to land exactly on the previous set of them.
|
|
904
|
+
await page.evaluate(() => {
|
|
905
|
+
const e = document.getElementById("preContrast");
|
|
906
|
+
e.value = 50;
|
|
907
|
+
e.dispatchEvent(new Event("input", { bubbles: true }));
|
|
908
|
+
e.dispatchEvent(new Event("change", { bubbles: true }));
|
|
909
|
+
});
|
|
910
|
+
await settle();
|
|
911
|
+
await page.click("#preUndo");
|
|
912
|
+
await settle();
|
|
913
|
+
const undone = await bytes();
|
|
914
|
+
|
|
915
|
+
// And restoring must put the original picture back exactly.
|
|
916
|
+
await page.click("#preReset");
|
|
917
|
+
await settle();
|
|
918
|
+
const restored = await bytes();
|
|
919
|
+
// One picture, not an original and a copy of it: two panes, never three.
|
|
920
|
+
const panes = await page.evaluate(() =>
|
|
921
|
+
document.querySelectorAll(".split > .pane").length);
|
|
922
|
+
return { hiddenAtFirst, opens, plain, blurred, undoLive, undone, restored, panes };
|
|
923
|
+
})();
|
|
924
|
+
console.log(JSON.stringify(pre));
|
|
925
|
+
if (!pre.hiddenAtFirst || !pre.opens) {
|
|
926
|
+
console.error("pre-processing should be hidden until asked for, then open");
|
|
927
|
+
process.exitCode = 1;
|
|
928
|
+
}
|
|
929
|
+
if (!(pre.blurred < pre.plain)) {
|
|
930
|
+
console.error("blurring the bitmap should reach the tracer and simplify it, got "
|
|
931
|
+
+ pre.plain + " -> " + pre.blurred);
|
|
932
|
+
process.exitCode = 1;
|
|
933
|
+
}
|
|
934
|
+
if (pre.restored !== pre.plain) {
|
|
935
|
+
console.error("restoring should bring the original trace back exactly, got "
|
|
936
|
+
+ pre.restored + " want " + pre.plain);
|
|
937
|
+
process.exitCode = 1;
|
|
938
|
+
}
|
|
939
|
+
if (!pre.undoLive || pre.undone !== pre.blurred) {
|
|
940
|
+
console.error("undo should step back onto the previous settings exactly, got "
|
|
941
|
+
+ pre.undone + " want " + pre.blurred);
|
|
942
|
+
process.exitCode = 1;
|
|
943
|
+
}
|
|
944
|
+
if (pre.panes !== 2) {
|
|
945
|
+
console.error("the picture is one picture: source and SVG, never a third pane, got "
|
|
946
|
+
+ pre.panes);
|
|
947
|
+
process.exitCode = 1;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// --- the refiner must read the edited picture, not the original -----------
|
|
951
|
+
// Pre-processing edits the bitmap; everything that samples pixels afterwards
|
|
952
|
+
// has to sample the edited one. The refiner did not, so on a blurred picture
|
|
953
|
+
// it cut its patch from the unblurred original and put sharp detail back
|
|
954
|
+
// exactly where the filters had been asked to take it away.
|
|
955
|
+
const refPre = await (async () => {
|
|
956
|
+
const settle = async () => {
|
|
957
|
+
await page.waitForFunction(() => !document.getElementById("run").disabled, { timeout: 120000 });
|
|
958
|
+
await page.waitForTimeout(350);
|
|
959
|
+
};
|
|
960
|
+
if (await page.evaluate(() => document.getElementById("editbar").classList.contains("on"))) {
|
|
961
|
+
await page.click("#editToggle");
|
|
962
|
+
}
|
|
963
|
+
await page.evaluate(() => {
|
|
964
|
+
const e = document.getElementById("preBlur");
|
|
965
|
+
e.value = 6;
|
|
966
|
+
e.dispatchEvent(new Event("input", { bubbles: true }));
|
|
967
|
+
e.dispatchEvent(new Event("change", { bubbles: true }));
|
|
968
|
+
});
|
|
969
|
+
await settle();
|
|
970
|
+
|
|
971
|
+
const energy = () => page.evaluate(async () => {
|
|
972
|
+
const svg = document.querySelector("#outStage svg");
|
|
973
|
+
const str = new XMLSerializer().serializeToString(svg);
|
|
974
|
+
const im = new Image();
|
|
975
|
+
im.src = "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(str)));
|
|
976
|
+
await im.decode();
|
|
977
|
+
await new Promise(r => setTimeout(r, 200));
|
|
978
|
+
const vb = svg.getAttribute("viewBox").split(/[\s,]+/).map(Number);
|
|
979
|
+
const c = document.createElement("canvas");
|
|
980
|
+
c.width = vb[2]; c.height = vb[3];
|
|
981
|
+
const g = c.getContext("2d");
|
|
982
|
+
g.fillStyle = "#fff"; g.fillRect(0, 0, c.width, c.height);
|
|
983
|
+
g.drawImage(im, 0, 0, c.width, c.height);
|
|
984
|
+
const d = g.getImageData(0, 0, c.width, c.height).data;
|
|
985
|
+
const y0 = Math.round(vb[3] * 0.40), y1 = Math.round(vb[3] * 0.50);
|
|
986
|
+
const x0 = Math.round(vb[2] * 0.32), x1 = Math.round(vb[2] * 0.66);
|
|
987
|
+
let sum = 0, n = 0;
|
|
988
|
+
for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++) {
|
|
989
|
+
const o = (y * vb[2] + x) * 4, q = (y * vb[2] + x + 1) * 4;
|
|
990
|
+
sum += Math.abs(d[o] - d[q]) + Math.abs(d[o+1] - d[q+1]) + Math.abs(d[o+2] - d[q+2]);
|
|
991
|
+
n++;
|
|
992
|
+
}
|
|
993
|
+
return sum / n / 3;
|
|
994
|
+
});
|
|
995
|
+
const flat = await energy();
|
|
996
|
+
|
|
997
|
+
await page.click("#editToggle");
|
|
998
|
+
await page.click("#toolRefine");
|
|
999
|
+
await page.evaluate(async () => {
|
|
1000
|
+
const svg = document.querySelector("#outStage svg");
|
|
1001
|
+
const rect = svg.getBoundingClientRect();
|
|
1002
|
+
const stage = document.getElementById("outStage");
|
|
1003
|
+
const at = (fx, fy) => ({ x: rect.left + rect.width * fx, y: rect.top + rect.height * fy });
|
|
1004
|
+
const send = (t, pt) => stage.dispatchEvent(new PointerEvent(t, {
|
|
1005
|
+
bubbles: true, clientX: pt.x, clientY: pt.y
|
|
1006
|
+
}));
|
|
1007
|
+
send("pointerdown", at(0.34, 0.45));
|
|
1008
|
+
for (let i = 1; i <= 12; i++) send("pointermove", at(0.34 + 0.3 * i / 12, 0.45));
|
|
1009
|
+
send("pointerup", at(0.64, 0.45));
|
|
1010
|
+
await new Promise(r => setTimeout(r, 400));
|
|
1011
|
+
});
|
|
1012
|
+
const refined = await energy();
|
|
1013
|
+
return { flat: +flat.toFixed(2), refined: +refined.toFixed(2) };
|
|
1014
|
+
})();
|
|
1015
|
+
console.log(JSON.stringify(refPre));
|
|
1016
|
+
// Reading the original instead measured 3.4x on a portrait; reading the
|
|
1017
|
+
// edited picture measured 1.4x. Anything past double is the old bug back.
|
|
1018
|
+
if (refPre.refined > refPre.flat * 2) {
|
|
1019
|
+
console.error("the refiner is sampling the unprocessed bitmap: edge energy went "
|
|
1020
|
+
+ refPre.flat + " -> " + refPre.refined);
|
|
1021
|
+
process.exitCode = 1;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// --- can it tell two pictures apart? --------------------------------------
|
|
1025
|
+
// Every check above asks whether the tool did something. This one asks
|
|
1026
|
+
// whether what it did was right, and it can, because the picture was built
|
|
1027
|
+
// here: one background, one figure, composited through a silhouette this
|
|
1028
|
+
// file knows by heart. The wand is then given a deliberately sloppy version
|
|
1029
|
+
// of that silhouette — every point pushed out or in by up to 18 px — and
|
|
1030
|
+
// what it cuts is compared against the truth, pixel by pixel.
|
|
1031
|
+
const cut = await (async () => {
|
|
1032
|
+
const W = 240, H = 300;
|
|
1033
|
+
const px = Buffer.alloc(W * H * 4);
|
|
1034
|
+
// Ground truth: a head on a body, as a function rather than a bitmap.
|
|
1035
|
+
const inFigure = (x, y) => {
|
|
1036
|
+
const hx = x - 120, hy = y - 78;
|
|
1037
|
+
if (hx * hx / (34 * 34) + hy * hy / (40 * 40) <= 1) return true;
|
|
1038
|
+
return y >= 108 && y <= 285 && Math.abs(x - 120) <= 62 - Math.max(0, (140 - y) * 0.4);
|
|
1039
|
+
};
|
|
1040
|
+
for (let y = 0; y < H; y++) {
|
|
1041
|
+
for (let x = 0; x < W; x++) {
|
|
1042
|
+
const o = (y * W + x) * 4;
|
|
1043
|
+
let r, g, b;
|
|
1044
|
+
if (inFigure(x, y)) {
|
|
1045
|
+
// The figure: warm, banded, with one cool patch so it is not a
|
|
1046
|
+
// single flat tone the color flood could have had for free.
|
|
1047
|
+
const band = ((x * 0.7 + y * 1.3) | 0) % 60;
|
|
1048
|
+
r = 150 + band; g = 90 + (band >> 1); b = 60 + (band >> 2);
|
|
1049
|
+
if (x > 96 && x < 144 && y > 150 && y < 200) { r = 70; g = 90; b = 140; }
|
|
1050
|
+
} else {
|
|
1051
|
+
// The background: cool, banded, with warm streaks, so neither side
|
|
1052
|
+
// owns a colour outright.
|
|
1053
|
+
const band = ((y * 1.1 + x * 0.3) | 0) % 50;
|
|
1054
|
+
r = 60 + band; g = 80 + band; b = 120 + band;
|
|
1055
|
+
if ((x * 3 + y) % 47 < 3) { r = 150; g = 100; b = 70; }
|
|
1056
|
+
}
|
|
1057
|
+
px[o] = r; px[o + 1] = g; px[o + 2] = b; px[o + 3] = 255;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
const file = path.join(os.tmpdir(), "tracer-smoke-composite.png");
|
|
1061
|
+
fs.writeFileSync(file, writePng(W, H, px));
|
|
1062
|
+
|
|
1063
|
+
await page.setInputFiles("#file", file);
|
|
1064
|
+
await page.waitForFunction(() =>
|
|
1065
|
+
/OK/.test(document.getElementById("status").textContent || ""), { timeout: 120000 });
|
|
1066
|
+
await page.evaluate(() => {
|
|
1067
|
+
document.getElementById("status").textContent = "…";
|
|
1068
|
+
const c = document.getElementById("colorCount");
|
|
1069
|
+
c.value = 12; c.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1070
|
+
});
|
|
1071
|
+
await page.waitForFunction(() =>
|
|
1072
|
+
/OK/.test(document.getElementById("status").textContent || ""), { timeout: 120000 });
|
|
1073
|
+
await page.click("#editToggle");
|
|
1074
|
+
await page.click("#toolWand");
|
|
1075
|
+
const r = await page.evaluate(async ({ W, H }) => {
|
|
1076
|
+
const inFigure = (x, y) => {
|
|
1077
|
+
const hx = x - 120, hy = y - 78;
|
|
1078
|
+
if (hx * hx / (34 * 34) + hy * hy / (40 * 40) <= 1) return true;
|
|
1079
|
+
return y >= 108 && y <= 285 && Math.abs(x - 120) <= 62 - Math.max(0, (140 - y) * 0.4);
|
|
1080
|
+
};
|
|
1081
|
+
const svg = document.querySelector("#outStage svg");
|
|
1082
|
+
const vb0 = svg.getAttribute("viewBox");
|
|
1083
|
+
const rect = svg.getBoundingClientRect();
|
|
1084
|
+
const stage = document.getElementById("outStage");
|
|
1085
|
+
const at = (ux, uy) => ({ x: rect.left + ux / W * rect.width,
|
|
1086
|
+
y: rect.top + uy / H * rect.height });
|
|
1087
|
+
const send = (t, p) => stage.dispatchEvent(new PointerEvent(t, {
|
|
1088
|
+
bubbles: true, clientX: p.x, clientY: p.y }));
|
|
1089
|
+
// Walk the silhouette and push every point away from the middle by a
|
|
1090
|
+
// wobble: this is the hand, and it is not steady.
|
|
1091
|
+
const pts = [];
|
|
1092
|
+
for (let i = 0; i < 72; i++) {
|
|
1093
|
+
const th = i / 72 * Math.PI * 2;
|
|
1094
|
+
const dx = Math.cos(th), dy = Math.sin(th);
|
|
1095
|
+
let hit = 0;
|
|
1096
|
+
for (let t = 4; t < 260; t += 1.5) {
|
|
1097
|
+
if (inFigure(120 + dx * t, 165 + dy * t)) hit = t;
|
|
1098
|
+
}
|
|
1099
|
+
if (!hit) continue;
|
|
1100
|
+
const wobble = 18 * Math.sin(i * 0.8) + 6;
|
|
1101
|
+
pts.push(at(120 + dx * (hit + wobble), 165 + dy * (hit + wobble)));
|
|
1102
|
+
}
|
|
1103
|
+
send("pointerdown", pts[0]);
|
|
1104
|
+
pts.slice(1).forEach((p) => send("pointermove", p));
|
|
1105
|
+
send("pointerup", pts[0]);
|
|
1106
|
+
const note = document.getElementById("editNote").textContent;
|
|
1107
|
+
document.getElementById("wandCut").click();
|
|
1108
|
+
const str = new XMLSerializer().serializeToString(svg)
|
|
1109
|
+
.replace(/viewBox="[^"]*"/, 'viewBox="' + vb0 + '"')
|
|
1110
|
+
.replace(/width="[^"]*"/, 'width="' + W + '"')
|
|
1111
|
+
.replace(/height="[^"]*"/, 'height="' + H + '"');
|
|
1112
|
+
const im = new Image();
|
|
1113
|
+
im.src = "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(str)));
|
|
1114
|
+
await im.decode();
|
|
1115
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
1116
|
+
const c = document.createElement("canvas");
|
|
1117
|
+
c.width = W; c.height = H;
|
|
1118
|
+
const g = c.getContext("2d");
|
|
1119
|
+
g.drawImage(im, 0, 0, W, H);
|
|
1120
|
+
const d = g.getImageData(0, 0, W, H).data;
|
|
1121
|
+
let tp = 0, fp = 0, fn = 0;
|
|
1122
|
+
for (let y = 0; y < H; y++) {
|
|
1123
|
+
for (let x = 0; x < W; x++) {
|
|
1124
|
+
const got = d[(y * W + x) * 4 + 3] > 128, want = inFigure(x, y);
|
|
1125
|
+
if (got && want) tp++; else if (got) fp++; else if (want) fn++;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return { note, tp, fp, fn,
|
|
1129
|
+
iou: +(tp / (tp + fp + fn)).toFixed(3),
|
|
1130
|
+
precision: +(tp / (tp + fp)).toFixed(3),
|
|
1131
|
+
recall: +(tp / (tp + fn)).toFixed(3) };
|
|
1132
|
+
}, { W, H });
|
|
1133
|
+
return r;
|
|
1134
|
+
})();
|
|
1135
|
+
console.log(JSON.stringify(cut));
|
|
1136
|
+
// Measured on three composites of two real photographs, a rough outline
|
|
1137
|
+
// scores 0.86 to 0.98 against the truth; this generated one sits in the same
|
|
1138
|
+
// range. The bar is set below the worst of them, so it fails on a change
|
|
1139
|
+
// that makes the cut worse rather than on the noise of a wobbling line.
|
|
1140
|
+
if (!(cut.iou >= 0.8)) {
|
|
1141
|
+
console.error("a rough outline should cut the figure out of the background, IoU "
|
|
1142
|
+
+ cut.iou + " (precision " + cut.precision + ", recall " + cut.recall + ")");
|
|
1143
|
+
process.exitCode = 1;
|
|
1144
|
+
}
|
|
1145
|
+
if (!(cut.recall >= 0.85)) {
|
|
1146
|
+
console.error("the cut should keep the figure, recall " + cut.recall);
|
|
1147
|
+
process.exitCode = 1;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
if (!process.exitCode) {
|
|
1151
|
+
console.log("tracer smoke OK");
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
if (errors.length) {
|
|
1156
|
+
console.error("page errors:\n" + errors.join("\n"));
|
|
1157
|
+
process.exitCode = 1;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
await browser.close();
|
|
1161
|
+
server.close();
|
|
1162
|
+
process.exit(process.exitCode || 0);
|