@react-x11/components 0.8.0 → 0.10.0
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/README.md +105 -38
- package/dist/charts/node.d.ts +12 -2
- package/dist/charts/node.d.ts.map +1 -1
- package/dist/charts/node.js +50 -16
- package/dist/charts/node.js.map +1 -1
- package/dist/charts/render.d.ts +12 -3
- package/dist/charts/render.d.ts.map +1 -1
- package/dist/charts/render.js +94 -25
- package/dist/charts/render.js.map +1 -1
- package/dist/code/index.d.ts.map +1 -1
- package/dist/code/index.js +3 -1
- package/dist/code/index.js.map +1 -1
- package/dist/code-editor/completion.d.ts.map +1 -1
- package/dist/code-editor/completion.js +3 -2
- package/dist/code-editor/completion.js.map +1 -1
- package/dist/code-editor/index.d.ts.map +1 -1
- package/dist/code-editor/index.js +19 -0
- package/dist/code-editor/index.js.map +1 -1
- package/dist/code-editor/node.d.ts +177 -7
- package/dist/code-editor/node.d.ts.map +1 -1
- package/dist/code-editor/node.js +1329 -164
- package/dist/code-editor/node.js.map +1 -1
- package/dist/code-language/languages/glsl.d.ts.map +1 -1
- package/dist/code-language/languages/glsl.js +10 -2
- package/dist/code-language/languages/glsl.js.map +1 -1
- package/dist/code-language/languages/javascript.d.ts.map +1 -1
- package/dist/code-language/languages/javascript.js +18 -3
- package/dist/code-language/languages/javascript.js.map +1 -1
- package/dist/code-language/languages/json.d.ts.map +1 -1
- package/dist/code-language/languages/json.js +10 -2
- package/dist/code-language/languages/json.js.map +1 -1
- package/dist/code-language/languages/shell.d.ts.map +1 -1
- package/dist/code-language/languages/shell.js +10 -2
- package/dist/code-language/languages/shell.js.map +1 -1
- package/dist/code-language/languages/sql.d.ts.map +1 -1
- package/dist/code-language/languages/sql.js +20 -2
- package/dist/code-language/languages/sql.js.map +1 -1
- package/dist/code-language/stream.d.ts +22 -0
- package/dist/code-language/stream.d.ts.map +1 -1
- package/dist/code-language/stream.js +259 -9
- package/dist/code-language/stream.js.map +1 -1
- package/dist/code-language/types.d.ts +5 -4
- package/dist/code-language/types.d.ts.map +1 -1
- package/dist/codeblock/index.d.ts +13 -2
- package/dist/codeblock/index.d.ts.map +1 -1
- package/dist/codeblock/index.js +20 -3
- package/dist/codeblock/index.js.map +1 -1
- package/dist/embed/client.d.ts +48 -3
- package/dist/embed/client.d.ts.map +1 -1
- package/dist/embed/client.js +79 -5
- package/dist/embed/client.js.map +1 -1
- package/dist/embed/index.d.ts +1 -1
- package/dist/embed/index.d.ts.map +1 -1
- package/dist/embed/index.js +1 -1
- package/dist/embed/index.js.map +1 -1
- package/dist/flow/draw.d.ts +50 -0
- package/dist/flow/draw.d.ts.map +1 -1
- package/dist/flow/draw.js +132 -33
- package/dist/flow/draw.js.map +1 -1
- package/dist/flow/gl/color.d.ts +13 -0
- package/dist/flow/gl/color.d.ts.map +1 -0
- package/dist/flow/gl/color.js +82 -0
- package/dist/flow/gl/color.js.map +1 -0
- package/dist/flow/gl/index.d.ts +60 -0
- package/dist/flow/gl/index.d.ts.map +1 -0
- package/dist/flow/gl/index.js +283 -0
- package/dist/flow/gl/index.js.map +1 -0
- package/dist/flow/gl/pack.d.ts +133 -0
- package/dist/flow/gl/pack.d.ts.map +1 -0
- package/dist/flow/gl/pack.js +408 -0
- package/dist/flow/gl/pack.js.map +1 -0
- package/dist/flow/gl/renderer.d.ts +95 -0
- package/dist/flow/gl/renderer.d.ts.map +1 -0
- package/dist/flow/gl/renderer.js +459 -0
- package/dist/flow/gl/renderer.js.map +1 -0
- package/dist/flow/gl/shaders.d.ts +79 -0
- package/dist/flow/gl/shaders.d.ts.map +1 -0
- package/dist/flow/gl/shaders.js +346 -0
- package/dist/flow/gl/shaders.js.map +1 -0
- package/dist/flow/gl/text.d.ts +318 -0
- package/dist/flow/gl/text.d.ts.map +1 -0
- package/dist/flow/gl/text.js +1103 -0
- package/dist/flow/gl/text.js.map +1 -0
- package/dist/flow/index.d.ts +1 -1
- package/dist/flow/index.d.ts.map +1 -1
- package/dist/flow/index.js +619 -64
- package/dist/flow/index.js.map +1 -1
- package/dist/flow/node.d.ts +621 -72
- package/dist/flow/node.d.ts.map +1 -1
- package/dist/flow/node.js +2253 -1018
- package/dist/flow/node.js.map +1 -1
- package/dist/flow/paint.d.ts +35 -0
- package/dist/flow/paint.d.ts.map +1 -0
- package/dist/flow/paint.js +393 -0
- package/dist/flow/paint.js.map +1 -0
- package/dist/flow/paths.d.ts +14 -0
- package/dist/flow/paths.d.ts.map +1 -1
- package/dist/flow/paths.js +45 -19
- package/dist/flow/paths.js.map +1 -1
- package/dist/flow/scene.d.ts +584 -0
- package/dist/flow/scene.d.ts.map +1 -0
- package/dist/flow/scene.js +1453 -0
- package/dist/flow/scene.js.map +1 -0
- package/dist/flow/types.d.ts +111 -1
- package/dist/flow/types.d.ts.map +1 -1
- package/dist/html/controls.d.ts +28 -0
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +52 -2
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/calc.d.ts +14 -0
- package/dist/html/css/calc.d.ts.map +1 -0
- package/dist/html/css/calc.js +344 -0
- package/dist/html/css/calc.js.map +1 -0
- package/dist/html/css/cascade.d.ts +240 -6
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +1327 -66
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/color.d.ts +23 -0
- package/dist/html/css/color.d.ts.map +1 -0
- package/dist/html/css/color.js +740 -0
- package/dist/html/css/color.js.map +1 -0
- package/dist/html/css/content.d.ts +92 -0
- package/dist/html/css/content.d.ts.map +1 -0
- package/dist/html/css/content.js +388 -0
- package/dist/html/css/content.js.map +1 -0
- package/dist/html/css/counter-styles.d.ts +76 -0
- package/dist/html/css/counter-styles.d.ts.map +1 -0
- package/dist/html/css/counter-styles.js +893 -0
- package/dist/html/css/counter-styles.js.map +1 -0
- package/dist/html/css/decode.d.ts +12 -0
- package/dist/html/css/decode.d.ts.map +1 -0
- package/dist/html/css/decode.js +116 -0
- package/dist/html/css/decode.js.map +1 -0
- package/dist/html/css/parse.d.ts +110 -10
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +1473 -180
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +498 -15
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +3698 -206
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/ua.d.ts.map +1 -1
- package/dist/html/css/ua.js +57 -11
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/css/values.d.ts +37 -3
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js +122 -48
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/css/vars.d.ts +52 -0
- package/dist/html/css/vars.d.ts.map +1 -0
- package/dist/html/css/vars.js +358 -0
- package/dist/html/css/vars.js.map +1 -0
- package/dist/html/dom.d.ts +30 -4
- package/dist/html/dom.d.ts.map +1 -1
- package/dist/html/dom.js +278 -17
- package/dist/html/dom.js.map +1 -1
- package/dist/html/fonts.d.ts +106 -0
- package/dist/html/fonts.d.ts.map +1 -0
- package/dist/html/fonts.js +565 -0
- package/dist/html/fonts.js.map +1 -0
- package/dist/html/gif.d.ts +15 -0
- package/dist/html/gif.d.ts.map +1 -0
- package/dist/html/gif.js +229 -0
- package/dist/html/gif.js.map +1 -0
- package/dist/html/index.d.ts +50 -11
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +115 -9
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/block.d.ts +226 -3
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +2937 -197
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +340 -17
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +1714 -182
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/cache.d.ts +58 -0
- package/dist/html/layout/cache.d.ts.map +1 -0
- package/dist/html/layout/cache.js +224 -0
- package/dist/html/layout/cache.js.map +1 -0
- package/dist/html/layout/collapse.d.ts +45 -0
- package/dist/html/layout/collapse.d.ts.map +1 -0
- package/dist/html/layout/collapse.js +246 -0
- package/dist/html/layout/collapse.js.map +1 -0
- package/dist/html/layout/css-grid.d.ts +7 -0
- package/dist/html/layout/css-grid.d.ts.map +1 -0
- package/dist/html/layout/css-grid.js +900 -0
- package/dist/html/layout/css-grid.js.map +1 -0
- package/dist/html/layout/flex.d.ts.map +1 -1
- package/dist/html/layout/flex.js +1025 -89
- package/dist/html/layout/flex.js.map +1 -1
- package/dist/html/layout/floats.d.ts +45 -6
- package/dist/html/layout/floats.d.ts.map +1 -1
- package/dist/html/layout/floats.js +113 -14
- package/dist/html/layout/floats.js.map +1 -1
- package/dist/html/layout/grid-lines.d.ts +39 -0
- package/dist/html/layout/grid-lines.d.ts.map +1 -0
- package/dist/html/layout/grid-lines.js +119 -0
- package/dist/html/layout/grid-lines.js.map +1 -0
- package/dist/html/layout/grid.d.ts +53 -0
- package/dist/html/layout/grid.d.ts.map +1 -0
- package/dist/html/layout/grid.js +153 -0
- package/dist/html/layout/grid.js.map +1 -0
- package/dist/html/layout/inline.d.ts +151 -1
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +3375 -174
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/layout/shaping.d.ts +6 -0
- package/dist/html/layout/shaping.d.ts.map +1 -0
- package/dist/html/layout/shaping.js +100 -0
- package/dist/html/layout/shaping.js.map +1 -0
- package/dist/html/layout/table.d.ts +3 -0
- package/dist/html/layout/table.d.ts.map +1 -1
- package/dist/html/layout/table.js +681 -175
- package/dist/html/layout/table.js.map +1 -1
- package/dist/html/layout/tracks.d.ts +44 -0
- package/dist/html/layout/tracks.d.ts.map +1 -0
- package/dist/html/layout/tracks.js +349 -0
- package/dist/html/layout/tracks.js.map +1 -0
- package/dist/html/node.d.ts +172 -5
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +1336 -103
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +92 -3
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +3503 -141
- package/dist/html/paint.js.map +1 -1
- package/dist/html/resources.d.ts +60 -14
- package/dist/html/resources.d.ts.map +1 -1
- package/dist/html/resources.js +122 -36
- package/dist/html/resources.js.map +1 -1
- package/dist/html/surfaces.d.ts +23 -0
- package/dist/html/surfaces.d.ts.map +1 -0
- package/dist/html/surfaces.js +68 -0
- package/dist/html/surfaces.js.map +1 -0
- package/dist/html/svg.d.ts +80 -0
- package/dist/html/svg.d.ts.map +1 -0
- package/dist/html/svg.js +435 -0
- package/dist/html/svg.js.map +1 -0
- package/dist/html/url.d.ts +23 -0
- package/dist/html/url.d.ts.map +1 -0
- package/dist/html/url.js +80 -0
- package/dist/html/url.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/collate.d.ts +3 -0
- package/dist/internal/collate.d.ts.map +1 -0
- package/dist/internal/collate.js +11 -0
- package/dist/internal/collate.js.map +1 -0
- package/dist/{markdown → internal/markdown}/ast.d.ts +23 -0
- package/dist/internal/markdown/ast.d.ts.map +1 -0
- package/dist/internal/markdown/ast.js +22 -0
- package/dist/internal/markdown/ast.js.map +1 -0
- package/dist/internal/markdown/parse.d.ts.map +1 -0
- package/dist/{markdown → internal/markdown}/parse.js +221 -30
- package/dist/internal/markdown/parse.js.map +1 -0
- package/dist/internal/markdown/stringify.d.ts +4 -0
- package/dist/internal/markdown/stringify.d.ts.map +1 -0
- package/dist/internal/markdown/stringify.js +684 -0
- package/dist/internal/markdown/stringify.js.map +1 -0
- package/dist/internal/markdown/tags.d.ts.map +1 -0
- package/dist/internal/markdown/tags.js.map +1 -0
- package/dist/internal/prewarm.d.ts +18 -0
- package/dist/internal/prewarm.d.ts.map +1 -0
- package/dist/internal/prewarm.js +42 -0
- package/dist/internal/prewarm.js.map +1 -0
- package/dist/internal/scroll.d.ts +3 -2
- package/dist/internal/scroll.d.ts.map +1 -1
- package/dist/internal/scroll.js +1 -0
- package/dist/internal/scroll.js.map +1 -1
- package/dist/internal/sdf.d.ts +20 -0
- package/dist/internal/sdf.d.ts.map +1 -0
- package/dist/internal/sdf.js +216 -0
- package/dist/internal/sdf.js.map +1 -0
- package/dist/internal/splice.d.ts +12 -0
- package/dist/internal/splice.d.ts.map +1 -0
- package/dist/internal/splice.js +23 -0
- package/dist/internal/splice.js.map +1 -0
- package/dist/internal/window.d.ts +12 -0
- package/dist/internal/window.d.ts.map +1 -1
- package/dist/internal/window.js +23 -6
- package/dist/internal/window.js.map +1 -1
- package/dist/maps/anchors.d.ts +103 -0
- package/dist/maps/anchors.d.ts.map +1 -0
- package/dist/maps/anchors.js +790 -0
- package/dist/maps/anchors.js.map +1 -0
- package/dist/maps/controller.d.ts +340 -0
- package/dist/maps/controller.d.ts.map +1 -0
- package/dist/maps/controller.js +822 -0
- package/dist/maps/controller.js.map +1 -0
- package/dist/maps/gl/buckets.d.ts +99 -0
- package/dist/maps/gl/buckets.d.ts.map +1 -0
- package/dist/maps/gl/buckets.js +365 -0
- package/dist/maps/gl/buckets.js.map +1 -0
- package/dist/maps/gl/build-worker.d.ts +15 -0
- package/dist/maps/gl/build-worker.d.ts.map +1 -0
- package/dist/maps/gl/build-worker.js +44 -0
- package/dist/maps/gl/build-worker.js.map +1 -0
- package/dist/maps/gl/color.d.ts +8 -0
- package/dist/maps/gl/color.d.ts.map +1 -0
- package/dist/maps/gl/color.js +57 -0
- package/dist/maps/gl/color.js.map +1 -0
- package/dist/maps/gl/cover.d.ts +70 -0
- package/dist/maps/gl/cover.d.ts.map +1 -0
- package/dist/maps/gl/cover.js +137 -0
- package/dist/maps/gl/cover.js.map +1 -0
- package/dist/maps/gl/index.d.ts +3 -0
- package/dist/maps/gl/index.d.ts.map +1 -0
- package/dist/maps/gl/index.js +20 -0
- package/dist/maps/gl/index.js.map +1 -0
- package/dist/maps/gl/markers.d.ts +28 -0
- package/dist/maps/gl/markers.d.ts.map +1 -0
- package/dist/maps/gl/markers.js +60 -0
- package/dist/maps/gl/markers.js.map +1 -0
- package/dist/maps/gl/overlays.d.ts +68 -0
- package/dist/maps/gl/overlays.d.ts.map +1 -0
- package/dist/maps/gl/overlays.js +300 -0
- package/dist/maps/gl/overlays.js.map +1 -0
- package/dist/maps/gl/pane.d.ts +12 -0
- package/dist/maps/gl/pane.d.ts.map +1 -0
- package/dist/maps/gl/pane.js +37 -0
- package/dist/maps/gl/pane.js.map +1 -0
- package/dist/maps/gl/placement.d.ts +124 -0
- package/dist/maps/gl/placement.d.ts.map +1 -0
- package/dist/maps/gl/placement.js +642 -0
- package/dist/maps/gl/placement.js.map +1 -0
- package/dist/maps/gl/renderer.d.ts +384 -0
- package/dist/maps/gl/renderer.d.ts.map +1 -0
- package/dist/maps/gl/renderer.js +1184 -0
- package/dist/maps/gl/renderer.js.map +1 -0
- package/dist/maps/gl/shaders.d.ts +121 -0
- package/dist/maps/gl/shaders.d.ts.map +1 -0
- package/dist/maps/gl/shaders.js +453 -0
- package/dist/maps/gl/shaders.js.map +1 -0
- package/dist/maps/gl/store.d.ts +125 -0
- package/dist/maps/gl/store.d.ts.map +1 -0
- package/dist/maps/gl/store.js +512 -0
- package/dist/maps/gl/store.js.map +1 -0
- package/dist/maps/gl/text.d.ts +283 -0
- package/dist/maps/gl/text.d.ts.map +1 -0
- package/dist/maps/gl/text.js +698 -0
- package/dist/maps/gl/text.js.map +1 -0
- package/dist/maps/gl/view.d.ts +345 -0
- package/dist/maps/gl/view.d.ts.map +1 -0
- package/dist/maps/gl/view.js +1209 -0
- package/dist/maps/gl/view.js.map +1 -0
- package/dist/maps/icons.d.ts +32 -0
- package/dist/maps/icons.d.ts.map +1 -0
- package/dist/maps/icons.js +151 -0
- package/dist/maps/icons.js.map +1 -0
- package/dist/maps/index.d.ts +3 -1
- package/dist/maps/index.d.ts.map +1 -1
- package/dist/maps/index.js +218 -52
- package/dist/maps/index.js.map +1 -1
- package/dist/maps/labels.d.ts +91 -18
- package/dist/maps/labels.d.ts.map +1 -1
- package/dist/maps/labels.js +376 -140
- package/dist/maps/labels.js.map +1 -1
- package/dist/maps/node.d.ts +179 -56
- package/dist/maps/node.d.ts.map +1 -1
- package/dist/maps/node.js +663 -537
- package/dist/maps/node.js.map +1 -1
- package/dist/maps/overlay.d.ts +72 -9
- package/dist/maps/overlay.d.ts.map +1 -1
- package/dist/maps/overlay.js +108 -28
- package/dist/maps/overlay.js.map +1 -1
- package/dist/maps/paint.d.ts +3 -0
- package/dist/maps/paint.d.ts.map +1 -1
- package/dist/maps/paint.js.map +1 -1
- package/dist/maps/proj.d.ts +62 -0
- package/dist/maps/proj.d.ts.map +1 -1
- package/dist/maps/proj.js +101 -0
- package/dist/maps/proj.js.map +1 -1
- package/dist/maps/renderer.d.ts +69 -0
- package/dist/maps/renderer.d.ts.map +1 -0
- package/dist/maps/renderer.js +66 -0
- package/dist/maps/renderer.js.map +1 -0
- package/dist/maps/sources.d.ts +30 -6
- package/dist/maps/sources.d.ts.map +1 -1
- package/dist/maps/sources.js +22 -3
- package/dist/maps/sources.js.map +1 -1
- package/dist/maps/style.d.ts +35 -0
- package/dist/maps/style.d.ts.map +1 -1
- package/dist/maps/style.js.map +1 -1
- package/dist/maps/styles.d.ts +12 -0
- package/dist/maps/styles.d.ts.map +1 -1
- package/dist/maps/styles.js +120 -6
- package/dist/maps/styles.js.map +1 -1
- package/dist/maps/theme.d.ts +19 -0
- package/dist/maps/theme.d.ts.map +1 -0
- package/dist/maps/theme.js +44 -0
- package/dist/maps/theme.js.map +1 -0
- package/dist/maps/tiles.d.ts +147 -27
- package/dist/maps/tiles.d.ts.map +1 -1
- package/dist/maps/tiles.js +221 -54
- package/dist/maps/tiles.js.map +1 -1
- package/dist/maps/types.d.ts +213 -21
- package/dist/maps/types.d.ts.map +1 -1
- package/dist/markdown/expressions.d.ts +1 -1
- package/dist/markdown/expressions.d.ts.map +1 -1
- package/dist/markdown/expressions.js.map +1 -1
- package/dist/markdown/index.d.ts +7 -6
- package/dist/markdown/index.d.ts.map +1 -1
- package/dist/markdown/index.js +121 -50
- package/dist/markdown/index.js.map +1 -1
- package/dist/markdown/spans.d.ts +1 -1
- package/dist/markdown/spans.d.ts.map +1 -1
- package/dist/markdown/spans.js +6 -0
- package/dist/markdown/spans.js.map +1 -1
- package/dist/media-player/index.d.ts +11 -3
- package/dist/media-player/index.d.ts.map +1 -1
- package/dist/media-player/index.js +13 -2
- package/dist/media-player/index.js.map +1 -1
- package/dist/qml/containers.d.ts.map +1 -1
- package/dist/qml/containers.js +6 -1
- package/dist/qml/containers.js.map +1 -1
- package/dist/qml/interaction.d.ts.map +1 -1
- package/dist/qml/interaction.js +38 -5
- package/dist/qml/interaction.js.map +1 -1
- package/dist/qml/layouts.d.ts.map +1 -1
- package/dist/qml/layouts.js +21 -6
- package/dist/qml/layouts.js.map +1 -1
- package/dist/qml/qtquick.d.ts.map +1 -1
- package/dist/qml/qtquick.js +9 -2
- package/dist/qml/qtquick.js.map +1 -1
- package/dist/qml/react.d.ts.map +1 -1
- package/dist/qml/react.js +9 -7
- package/dist/qml/react.js.map +1 -1
- package/dist/reorder/clock.d.ts +7 -0
- package/dist/reorder/clock.d.ts.map +1 -0
- package/dist/reorder/clock.js +26 -0
- package/dist/reorder/clock.js.map +1 -0
- package/dist/reorder/index.d.ts +25 -3
- package/dist/reorder/index.d.ts.map +1 -1
- package/dist/reorder/index.js +64 -19
- package/dist/reorder/index.js.map +1 -1
- package/dist/rich-text-editor/clipboard.d.ts +23 -0
- package/dist/rich-text-editor/clipboard.d.ts.map +1 -0
- package/dist/rich-text-editor/clipboard.js +307 -0
- package/dist/rich-text-editor/clipboard.js.map +1 -0
- package/dist/rich-text-editor/collab.d.ts +32 -0
- package/dist/rich-text-editor/collab.d.ts.map +1 -0
- package/dist/rich-text-editor/collab.js +55 -0
- package/dist/rich-text-editor/collab.js.map +1 -0
- package/dist/rich-text-editor/commands.d.ts +50 -0
- package/dist/rich-text-editor/commands.d.ts.map +1 -0
- package/dist/rich-text-editor/commands.js +376 -0
- package/dist/rich-text-editor/commands.js.map +1 -0
- package/dist/rich-text-editor/drag.d.ts +68 -0
- package/dist/rich-text-editor/drag.d.ts.map +1 -0
- package/dist/rich-text-editor/drag.js +148 -0
- package/dist/rich-text-editor/drag.js.map +1 -0
- package/dist/rich-text-editor/html.d.ts +145 -0
- package/dist/rich-text-editor/html.d.ts.map +1 -0
- package/dist/rich-text-editor/html.js +516 -0
- package/dist/rich-text-editor/html.js.map +1 -0
- package/dist/rich-text-editor/index.d.ts +252 -0
- package/dist/rich-text-editor/index.d.ts.map +1 -0
- package/dist/rich-text-editor/index.js +856 -0
- package/dist/rich-text-editor/index.js.map +1 -0
- package/dist/rich-text-editor/inline.d.ts +94 -0
- package/dist/rich-text-editor/inline.d.ts.map +1 -0
- package/dist/rich-text-editor/inline.js +252 -0
- package/dist/rich-text-editor/inline.js.map +1 -0
- package/dist/rich-text-editor/keymap.d.ts +71 -0
- package/dist/rich-text-editor/keymap.d.ts.map +1 -0
- package/dist/rich-text-editor/keymap.js +394 -0
- package/dist/rich-text-editor/keymap.js.map +1 -0
- package/dist/rich-text-editor/keys.d.ts +40 -0
- package/dist/rich-text-editor/keys.d.ts.map +1 -0
- package/dist/rich-text-editor/keys.js +202 -0
- package/dist/rich-text-editor/keys.js.map +1 -0
- package/dist/rich-text-editor/look.d.ts +71 -0
- package/dist/rich-text-editor/look.d.ts.map +1 -0
- package/dist/rich-text-editor/look.js +342 -0
- package/dist/rich-text-editor/look.js.map +1 -0
- package/dist/rich-text-editor/markdown.d.ts +18 -0
- package/dist/rich-text-editor/markdown.d.ts.map +1 -0
- package/dist/rich-text-editor/markdown.js +496 -0
- package/dist/rich-text-editor/markdown.js.map +1 -0
- package/dist/rich-text-editor/marks.d.ts +9 -0
- package/dist/rich-text-editor/marks.d.ts.map +1 -0
- package/dist/rich-text-editor/marks.js +83 -0
- package/dist/rich-text-editor/marks.js.map +1 -0
- package/dist/rich-text-editor/nodes.d.ts +135 -0
- package/dist/rich-text-editor/nodes.d.ts.map +1 -0
- package/dist/rich-text-editor/nodes.js +397 -0
- package/dist/rich-text-editor/nodes.js.map +1 -0
- package/dist/rich-text-editor/render.d.ts +68 -0
- package/dist/rich-text-editor/render.d.ts.map +1 -0
- package/dist/rich-text-editor/render.js +518 -0
- package/dist/rich-text-editor/render.js.map +1 -0
- package/dist/rich-text-editor/schema.d.ts +16 -0
- package/dist/rich-text-editor/schema.d.ts.map +1 -0
- package/dist/rich-text-editor/schema.js +386 -0
- package/dist/rich-text-editor/schema.js.map +1 -0
- package/dist/rich-text-editor/store.d.ts +11 -0
- package/dist/rich-text-editor/store.d.ts.map +1 -0
- package/dist/rich-text-editor/store.js +43 -0
- package/dist/rich-text-editor/store.js.map +1 -0
- package/dist/rich-text-editor/suggest.d.ts +127 -0
- package/dist/rich-text-editor/suggest.d.ts.map +1 -0
- package/dist/rich-text-editor/suggest.js +0 -0
- package/dist/rich-text-editor/suggest.js.map +1 -0
- package/dist/rich-text-editor/tables.d.ts +41 -0
- package/dist/rich-text-editor/tables.d.ts.map +1 -0
- package/dist/rich-text-editor/tables.js +204 -0
- package/dist/rich-text-editor/tables.js.map +1 -0
- package/dist/rich-text-editor/toolbar.d.ts +58 -0
- package/dist/rich-text-editor/toolbar.d.ts.map +1 -0
- package/dist/rich-text-editor/toolbar.js +548 -0
- package/dist/rich-text-editor/toolbar.js.map +1 -0
- package/dist/rich-text-editor/view.d.ts +368 -0
- package/dist/rich-text-editor/view.d.ts.map +1 -0
- package/dist/rich-text-editor/view.js +1968 -0
- package/dist/rich-text-editor/view.js.map +1 -0
- package/dist/rich-text-editor/virtual.d.ts +54 -0
- package/dist/rich-text-editor/virtual.d.ts.map +1 -0
- package/dist/rich-text-editor/virtual.js +279 -0
- package/dist/rich-text-editor/virtual.js.map +1 -0
- package/dist/richtext/node.d.ts +71 -4
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +233 -28
- package/dist/richtext/node.js.map +1 -1
- package/dist/richtext/runs.d.ts +13 -8
- package/dist/richtext/runs.d.ts.map +1 -1
- package/dist/richtext/runs.js +18 -8
- package/dist/richtext/runs.js.map +1 -1
- package/dist/table/index.d.ts.map +1 -1
- package/dist/table/index.js +23 -0
- package/dist/table/index.js.map +1 -1
- package/dist/table/rows.d.ts +3 -2
- package/dist/table/rows.d.ts.map +1 -1
- package/dist/table/rows.js +20 -6
- package/dist/table/rows.js.map +1 -1
- package/dist/terminal/backends.d.ts +4 -1
- package/dist/terminal/backends.d.ts.map +1 -1
- package/dist/terminal/backends.js.map +1 -1
- package/dist/terminal/index.d.ts +18 -7
- package/dist/terminal/index.d.ts.map +1 -1
- package/dist/terminal/index.js +32 -11
- package/dist/terminal/index.js.map +1 -1
- package/dist/terminal/vt/pty.d.ts +13 -2
- package/dist/terminal/vt/pty.d.ts.map +1 -1
- package/dist/terminal/vt/pty.js +18 -3
- package/dist/terminal/vt/pty.js.map +1 -1
- package/dist/terminal-output/index.d.ts.map +1 -1
- package/dist/terminal-output/index.js +6 -1
- package/dist/terminal-output/index.js.map +1 -1
- package/dist/three/canvas.d.ts.map +1 -1
- package/dist/three/canvas.js +85 -7
- package/dist/three/canvas.js.map +1 -1
- package/dist/three/events.d.ts +22 -4
- package/dist/three/events.d.ts.map +1 -1
- package/dist/three/events.js +34 -4
- package/dist/three/events.js.map +1 -1
- package/dist/tree/index.d.ts.map +1 -1
- package/dist/tree/index.js +17 -0
- package/dist/tree/index.js.map +1 -1
- package/package.json +59 -31
- package/src/charts/node.ts +68 -28
- package/src/charts/render.ts +120 -30
- package/src/code/index.ts +3 -1
- package/src/code-editor/completion.ts +3 -2
- package/src/code-editor/index.ts +19 -0
- package/src/code-editor/node.ts +1574 -184
- package/src/code-language/languages/glsl.ts +12 -2
- package/src/code-language/languages/javascript.ts +21 -4
- package/src/code-language/languages/json.ts +12 -2
- package/src/code-language/languages/shell.ts +12 -2
- package/src/code-language/languages/sql.ts +22 -2
- package/src/code-language/stream.ts +257 -17
- package/src/code-language/types.ts +5 -4
- package/src/codeblock/index.ts +20 -3
- package/src/embed/client.ts +102 -8
- package/src/embed/index.ts +2 -0
- package/src/flow/draw.ts +167 -41
- package/src/flow/gl/color.ts +85 -0
- package/src/flow/gl/index.ts +342 -0
- package/src/flow/gl/pack.ts +585 -0
- package/src/flow/gl/renderer.ts +679 -0
- package/src/flow/gl/shaders.ts +371 -0
- package/src/flow/gl/text.ts +1295 -0
- package/src/flow/index.ts +794 -67
- package/src/flow/node.ts +2496 -1256
- package/src/flow/paint.ts +511 -0
- package/src/flow/paths.ts +80 -15
- package/src/flow/scene.ts +2199 -0
- package/src/flow/types.ts +113 -1
- package/src/html/controls.ts +73 -2
- package/src/html/css/calc.ts +385 -0
- package/src/html/css/cascade.ts +1403 -62
- package/src/html/css/color.ts +755 -0
- package/src/html/css/content.ts +417 -0
- package/src/html/css/counter-styles.ts +973 -0
- package/src/html/css/decode.ts +130 -0
- package/src/html/css/parse.ts +1514 -160
- package/src/html/css/style.ts +4102 -222
- package/src/html/css/ua.ts +58 -12
- package/src/html/css/values.ts +150 -50
- package/src/html/css/vars.ts +348 -0
- package/src/html/dom.ts +284 -17
- package/src/html/fonts.ts +641 -0
- package/src/html/gif.ts +233 -0
- package/src/html/index.ts +180 -20
- package/src/html/layout/block.ts +3456 -230
- package/src/html/layout/boxes.ts +2100 -190
- package/src/html/layout/cache.ts +289 -0
- package/src/html/layout/collapse.ts +317 -0
- package/src/html/layout/css-grid.ts +1063 -0
- package/src/html/layout/flex.ts +1115 -106
- package/src/html/layout/floats.ts +128 -14
- package/src/html/layout/grid-lines.ts +141 -0
- package/src/html/layout/grid.ts +181 -0
- package/src/html/layout/inline.ts +3943 -192
- package/src/html/layout/shaping.ts +140 -0
- package/src/html/layout/table.ts +696 -186
- package/src/html/layout/tracks.ts +437 -0
- package/src/html/node.ts +1486 -121
- package/src/html/paint.ts +4293 -169
- package/src/html/resources.ts +161 -50
- package/src/html/surfaces.ts +81 -0
- package/src/html/svg.ts +493 -0
- package/src/html/url.ts +86 -0
- package/src/index.ts +4 -0
- package/src/internal/collate.ts +12 -0
- package/src/{markdown → internal/markdown}/ast.ts +35 -7
- package/src/{markdown → internal/markdown}/parse.ts +288 -35
- package/src/internal/markdown/stringify.ts +772 -0
- package/src/internal/prewarm.ts +45 -0
- package/src/internal/scroll.ts +5 -3
- package/src/internal/sdf.ts +227 -0
- package/src/internal/splice.ts +25 -0
- package/src/internal/window.ts +36 -5
- package/src/maps/anchors.ts +913 -0
- package/src/maps/controller.ts +1084 -0
- package/src/maps/gl/buckets.ts +461 -0
- package/src/maps/gl/build-worker.ts +66 -0
- package/src/maps/gl/color.ts +62 -0
- package/src/maps/gl/cover.ts +205 -0
- package/src/maps/gl/index.ts +23 -0
- package/src/maps/gl/markers.ts +83 -0
- package/src/maps/gl/overlays.ts +379 -0
- package/src/maps/gl/pane.ts +42 -0
- package/src/maps/gl/placement.ts +968 -0
- package/src/maps/gl/renderer.ts +1660 -0
- package/src/maps/gl/shaders.ts +489 -0
- package/src/maps/gl/store.ts +615 -0
- package/src/maps/gl/text.ts +868 -0
- package/src/maps/gl/view.ts +1489 -0
- package/src/maps/icons.ts +183 -0
- package/src/maps/index.ts +274 -81
- package/src/maps/labels.ts +457 -144
- package/src/maps/node.ts +832 -665
- package/src/maps/overlay.ts +148 -43
- package/src/maps/paint.ts +3 -0
- package/src/maps/proj.ts +118 -0
- package/src/maps/renderer.ts +155 -0
- package/src/maps/sources.ts +43 -6
- package/src/maps/style.ts +32 -0
- package/src/maps/styles.ts +215 -4
- package/src/maps/theme.ts +51 -0
- package/src/maps/tiles.ts +253 -62
- package/src/maps/types.ts +213 -21
- package/src/markdown/expressions.ts +5 -1
- package/src/markdown/index.ts +163 -78
- package/src/markdown/spans.ts +6 -1
- package/src/media-player/index.ts +18 -4
- package/src/qml/containers.tsx +6 -1
- package/src/qml/interaction.tsx +42 -5
- package/src/qml/layouts.tsx +21 -6
- package/src/qml/qtquick.tsx +9 -2
- package/src/qml/react.tsx +7 -5
- package/src/reorder/clock.ts +27 -0
- package/src/reorder/index.ts +100 -27
- package/src/rich-text-editor/clipboard.ts +439 -0
- package/src/rich-text-editor/collab.ts +91 -0
- package/src/rich-text-editor/commands.ts +448 -0
- package/src/rich-text-editor/dom-globals.d.ts +23 -0
- package/src/rich-text-editor/drag.ts +211 -0
- package/src/rich-text-editor/html.ts +619 -0
- package/src/rich-text-editor/index.ts +1364 -0
- package/src/rich-text-editor/inline.ts +345 -0
- package/src/rich-text-editor/keymap.ts +510 -0
- package/src/rich-text-editor/keys.ts +228 -0
- package/src/rich-text-editor/look.ts +427 -0
- package/src/rich-text-editor/markdown.ts +653 -0
- package/src/rich-text-editor/marks.ts +101 -0
- package/src/rich-text-editor/nodes.ts +497 -0
- package/src/rich-text-editor/render.ts +837 -0
- package/src/rich-text-editor/schema.ts +431 -0
- package/src/rich-text-editor/store.ts +39 -0
- package/src/rich-text-editor/suggest.ts +597 -0
- package/src/rich-text-editor/tables.ts +272 -0
- package/src/rich-text-editor/toolbar.ts +724 -0
- package/src/rich-text-editor/view.ts +2287 -0
- package/src/rich-text-editor/virtual.ts +372 -0
- package/src/richtext/node.ts +294 -44
- package/src/richtext/runs.ts +23 -12
- package/src/table/index.ts +29 -0
- package/src/table/rows.ts +18 -8
- package/src/terminal/backends.ts +4 -1
- package/src/terminal/index.ts +47 -15
- package/src/terminal/vt/pty.ts +16 -2
- package/src/terminal-output/index.ts +6 -1
- package/src/three/canvas.ts +102 -8
- package/src/three/events.ts +49 -9
- package/src/tree/index.ts +19 -0
- package/dist/markdown/ast.d.ts.map +0 -1
- package/dist/markdown/ast.js +0 -17
- package/dist/markdown/ast.js.map +0 -1
- package/dist/markdown/parse.d.ts.map +0 -1
- package/dist/markdown/parse.js.map +0 -1
- package/dist/markdown/tags.d.ts.map +0 -1
- package/dist/markdown/tags.js.map +0 -1
- /package/dist/{markdown → internal/markdown}/parse.d.ts +0 -0
- /package/dist/{markdown → internal/markdown}/tags.d.ts +0 -0
- /package/dist/{markdown → internal/markdown}/tags.js +0 -0
- /package/src/{markdown → internal/markdown}/tags.ts +0 -0
package/src/flow/node.ts
CHANGED
|
@@ -23,7 +23,6 @@ import * as ntk from 'react-x11/ntk';
|
|
|
23
23
|
import { Node } from 'react-x11/node';
|
|
24
24
|
import type { A11ySceneAction, A11ySceneItem } from 'react-x11/node';
|
|
25
25
|
import type { Context2D } from 'react-x11/node';
|
|
26
|
-
import { tint } from 'react-x11/style';
|
|
27
26
|
import type { KeyboardEvent, MouseEvent, WheelEvent } from 'react-x11';
|
|
28
27
|
import {
|
|
29
28
|
ctrlChordLetter,
|
|
@@ -38,19 +37,16 @@ import {
|
|
|
38
37
|
XK_UP,
|
|
39
38
|
} from 'react-x11/keysyms';
|
|
40
39
|
|
|
41
|
-
import { createPainter, measureText, toDevice } from './draw.js';
|
|
40
|
+
import { createPainter, measureText, snapToGrid, toDevice } from './draw.js';
|
|
42
41
|
import type { CachedText, FontsLike, PainterOptions } from './draw.js';
|
|
43
42
|
import {
|
|
44
43
|
boundsOf,
|
|
45
44
|
canConnect,
|
|
46
45
|
clamp,
|
|
47
|
-
DEFAULT_MARKER_SIZE,
|
|
48
46
|
DEFAULT_MAX_ZOOM,
|
|
49
47
|
DEFAULT_MIN_ZOOM,
|
|
50
48
|
EDGE_SLOP,
|
|
51
|
-
EDGE_STEP_OFFSET,
|
|
52
49
|
fitViewport,
|
|
53
|
-
handleAnchor,
|
|
54
50
|
HANDLE_RADIUS,
|
|
55
51
|
HANDLE_SLOP,
|
|
56
52
|
gripPoint,
|
|
@@ -60,13 +56,8 @@ import {
|
|
|
60
56
|
MIN_NODE_HEIGHT,
|
|
61
57
|
MIN_NODE_WIDTH,
|
|
62
58
|
NODE_BODY_INSET,
|
|
63
|
-
NODE_DESC_SIZE,
|
|
64
59
|
NODE_HEADER,
|
|
65
|
-
NODE_LABEL_SIZE,
|
|
66
|
-
NODE_PAD_X,
|
|
67
|
-
NODE_PAD_Y,
|
|
68
60
|
normalizeBackground,
|
|
69
|
-
normalizeMarker,
|
|
70
61
|
orientConnection,
|
|
71
62
|
rectContains,
|
|
72
63
|
rectsOverlap,
|
|
@@ -81,15 +72,42 @@ import {
|
|
|
81
72
|
unionRects,
|
|
82
73
|
ZOOM_STEP,
|
|
83
74
|
} from './model.js';
|
|
75
|
+
import { distanceToPath, pathBounds } from './paths.js';
|
|
84
76
|
import {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
77
|
+
paintFloat,
|
|
78
|
+
paintGraph,
|
|
79
|
+
paintGraphEdges,
|
|
80
|
+
paintGraphNodes,
|
|
81
|
+
paintGround,
|
|
82
|
+
paintNodeItem,
|
|
83
|
+
paintPanels,
|
|
84
|
+
paintScene,
|
|
85
|
+
} from './paint.js';
|
|
86
|
+
import {
|
|
87
|
+
anchorsOf,
|
|
88
|
+
buildNodeItems,
|
|
89
|
+
buildScene,
|
|
90
|
+
connectionPath,
|
|
91
|
+
SceneCache,
|
|
92
|
+
CULL_MARGIN,
|
|
93
|
+
edgeCoarseBox,
|
|
94
|
+
edgeInkBox,
|
|
95
|
+
withLabelPlate,
|
|
96
|
+
edgeRoute,
|
|
97
|
+
batchesEdges,
|
|
98
|
+
endpoint,
|
|
99
|
+
HANDLE_ZOOM,
|
|
100
|
+
MINIMAP_BLEED,
|
|
101
|
+
miniMapPlace,
|
|
102
|
+
screenRect,
|
|
103
|
+
screenViewport,
|
|
104
|
+
} from './scene.js';
|
|
105
|
+
import type {
|
|
106
|
+
FlowScene,
|
|
107
|
+
SceneGrid,
|
|
108
|
+
SceneInput,
|
|
109
|
+
SceneNodeSource,
|
|
110
|
+
} from './scene.js';
|
|
93
111
|
import type {
|
|
94
112
|
BackgroundOptions,
|
|
95
113
|
BackgroundVariant,
|
|
@@ -97,6 +115,8 @@ import type {
|
|
|
97
115
|
EdgeChange,
|
|
98
116
|
FitViewOptions,
|
|
99
117
|
FlowEdge,
|
|
118
|
+
FlowBodyBudget,
|
|
119
|
+
FlowFrameStats,
|
|
100
120
|
FlowNode,
|
|
101
121
|
FlowNodeData,
|
|
102
122
|
FlowNodeType,
|
|
@@ -109,8 +129,6 @@ import type {
|
|
|
109
129
|
MiniMapOptions,
|
|
110
130
|
NodeBodyRect,
|
|
111
131
|
NodeChange,
|
|
112
|
-
ShapeOptions,
|
|
113
|
-
StrokeOptions,
|
|
114
132
|
TextOptions,
|
|
115
133
|
Viewport,
|
|
116
134
|
XYPosition,
|
|
@@ -141,44 +159,120 @@ interface PatternLike {
|
|
|
141
159
|
_picture?: { destroy?(): void };
|
|
142
160
|
}
|
|
143
161
|
|
|
162
|
+
/** The slice of ntk's Surface a 2D zoom gesture paints the graph onto. */
|
|
163
|
+
interface ShotSurface {
|
|
164
|
+
getContext(name: '2d'): unknown;
|
|
165
|
+
destroy?(): void;
|
|
166
|
+
}
|
|
167
|
+
type ShotCtor = new (
|
|
168
|
+
app: unknown,
|
|
169
|
+
options: { width: number; height: number; format: string },
|
|
170
|
+
) => ShotSurface;
|
|
171
|
+
|
|
172
|
+
/** The graph as a 2D zoom gesture's first step drew it, and the viewport
|
|
173
|
+
* it drew at. */
|
|
174
|
+
interface ZoomShot {
|
|
175
|
+
surface: ShotSurface;
|
|
176
|
+
viewport: Viewport;
|
|
177
|
+
width: number;
|
|
178
|
+
height: number;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* What a 2D drag copies from instead of repainting: the graph without the
|
|
183
|
+
* nodes it moves, in two pictures of the pane — the ground and the edges,
|
|
184
|
+
* and the cards on a clear ground over them — so the moved nodes' edges go
|
|
185
|
+
* between the two and the nodes over both, as the painter orders them.
|
|
186
|
+
* Good for the gesture it was made in, at the viewport and the version of
|
|
187
|
+
* the graph it was painted at.
|
|
188
|
+
*/
|
|
189
|
+
interface LiftShot {
|
|
190
|
+
under: ShotSurface;
|
|
191
|
+
over: ShotSurface;
|
|
192
|
+
/** Where the cards' picture has anything, in the pane's logical pixels:
|
|
193
|
+
* each card and the reach of its handles. Blended in there and nowhere
|
|
194
|
+
* else — a step's box between the cards has none to blend. */
|
|
195
|
+
cards: FlowRect[];
|
|
196
|
+
gesture: object;
|
|
197
|
+
version: number;
|
|
198
|
+
viewport: Viewport;
|
|
199
|
+
width: number;
|
|
200
|
+
height: number;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** A canvas `<Flow>` paints a panel on: a core node, asked to repaint its
|
|
204
|
+
* own box. */
|
|
205
|
+
interface PanelCanvas {
|
|
206
|
+
invalidate(layout: boolean, damage: unknown, reason: string): void;
|
|
207
|
+
/** Where a claim of the canvas lands: its box and core's damage slop,
|
|
208
|
+
* device pixels. */
|
|
209
|
+
paintBounds?(): FlowRect;
|
|
210
|
+
/** Its box, device pixels. */
|
|
211
|
+
readonly abs?: FlowRect;
|
|
212
|
+
}
|
|
213
|
+
|
|
144
214
|
/** Timers, through `globalThis`: `src/` compiles with `types: []` so a Node
|
|
145
215
|
* global that wandered in would become an implicit `@types/node` dependency
|
|
146
216
|
* a consumer has to satisfy. */
|
|
147
217
|
const timers = globalThis as {
|
|
148
218
|
setInterval?(fn: () => void, ms: number): unknown;
|
|
149
219
|
clearInterval?(id: unknown): void;
|
|
220
|
+
setTimeout?(fn: () => void, ms: number): unknown;
|
|
221
|
+
clearTimeout?(id: unknown): void;
|
|
150
222
|
};
|
|
151
223
|
|
|
224
|
+
/** How long the zoom has to hold still before mounted bodies come back at
|
|
225
|
+
* the new scale. Wheel notches in a flick land well inside it. */
|
|
226
|
+
const BODY_ZOOM_REST_MS = 150;
|
|
227
|
+
/** How long held bodies wait for the frame that draws a zoom's last step
|
|
228
|
+
* before they come back without it (`_restFrom`). */
|
|
229
|
+
const BODY_REST_CAP_MS = 1000;
|
|
230
|
+
/** What re-scaling the mounted bodies may add to one step of a zoom
|
|
231
|
+
* gesture before they sit the gesture out (`_holdBodies`). */
|
|
232
|
+
const BODY_BUDGET_MS = 8;
|
|
233
|
+
/** What one body costs a zoom step until the pane has measured it: 1.1–1.2
|
|
234
|
+
* ms on an M-series Mac, the 400-widget scene on Cocoa (42 bodies added
|
|
235
|
+
* 49 ms to a 13 ms step, 9 added 10). Learned from then on. */
|
|
236
|
+
const BODY_STEP_PRIOR_MS = 1.2;
|
|
237
|
+
|
|
238
|
+
/** Under GL, how long a zoom holds still before the world is rebuilt at it
|
|
239
|
+
* — until then each step draws the world already on the GPU, scaled
|
|
240
|
+
* (`glFrame`) — and how far that scaling may go before a step rebuilds
|
|
241
|
+
* anyway, so a long gesture never magnifies one build by more than this.
|
|
242
|
+
* The 2D renderer's zoom picture keeps to both. */
|
|
243
|
+
const GL_ZOOM_REST_MS = 120;
|
|
244
|
+
const GL_ZOOM_SPAN = 2;
|
|
245
|
+
/** How much of the pane a 2D zoom step may paint live, round the picture it
|
|
246
|
+
* composites — the ring a zoom out uncovers — before it paints a new one. */
|
|
247
|
+
const ZOOM_SHOT_LIVE = 0.5;
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* What painting the graph whole has to cost, ms, before a zoom paints a
|
|
251
|
+
* picture of it and composites that (`_zoomShotFor`) — and what it has to
|
|
252
|
+
* fall under before a zoom paints it live again. A picture pays for a graph
|
|
253
|
+
* too big to paint at every step and costs one that is not: on macOS at 2x
|
|
254
|
+
* its composite is ~20 ms a step whatever the graph, on X11 each new
|
|
255
|
+
* picture was a 50–150 ms step, and either way it holds a surface the size
|
|
256
|
+
* of the pane. A thirty-node graph painted live in 7 ms a step on Cocoa and
|
|
257
|
+
* zoomed at 90 frames a second where its picture held it to 45. A frame at
|
|
258
|
+
* 60 Hz to start, and a margin under it to stop, so a graph near the line
|
|
259
|
+
* does not go back and forth; judged on the median of the last
|
|
260
|
+
* `GRAPH_PAINTS_KEPT` whole paints, so a slow paint or two — a collection,
|
|
261
|
+
* the server catching up — decide nothing.
|
|
262
|
+
*/
|
|
263
|
+
const ZOOM_SHOT_WORTH_MS = 16;
|
|
264
|
+
const ZOOM_SHOT_DROP_MS = 12;
|
|
265
|
+
const GRAPH_PAINTS_KEPT = 5;
|
|
266
|
+
/** How far round a box selection's outline its step claims: the pen, the
|
|
267
|
+
* corner's radius and a pixel of antialiasing, with room to spare. */
|
|
268
|
+
const SELECT_BAND = 4;
|
|
152
269
|
/** How often the dash on an animated edge moves. Slow enough that a graph
|
|
153
270
|
* full of them is not a repaint storm, fast enough to read as motion. */
|
|
154
271
|
const ANIMATION_MS = 60;
|
|
155
272
|
const ANIMATION_SPEED = 1.4; // px of dash travel per tick, at zoom 1
|
|
156
|
-
const DEFAULT_DASH = [7, 5];
|
|
157
|
-
|
|
158
|
-
/** Below these zooms the pane stops drawing detail nobody could read — the
|
|
159
|
-
* cheapest optimisation there is, and the one that keeps a zoomed-out
|
|
160
|
-
* overview interactive. */
|
|
161
|
-
const LABEL_ZOOM = 0.45;
|
|
162
|
-
const HANDLE_ZOOM = 0.5;
|
|
163
|
-
const DESC_ZOOM = 0.6;
|
|
164
273
|
|
|
165
274
|
/** Screen pixels the pointer may travel before a press becomes a drag. */
|
|
166
275
|
const DRAG_THRESHOLD = 3;
|
|
167
|
-
/** The grid never draws denser than this on screen, whatever the zoom. */
|
|
168
|
-
const MIN_GRID_PX = 16;
|
|
169
|
-
|
|
170
|
-
/** See `StrokeBuckets.BATCH_MIN`. */
|
|
171
|
-
const MARKER_BATCH_MIN = 24;
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* How far outside its own box a node's ink can land: handles (capped at
|
|
175
|
-
* 6 × 1.4 plus their outline), resize grips, the selection border, a pixel
|
|
176
|
-
* of antialiasing. Damage rects grow by this before they are invalidated,
|
|
177
|
-
* and cull tests grow their target by the same amount — the two must agree,
|
|
178
|
-
* or a moved node leaves crumbs of its old handles behind.
|
|
179
|
-
*/
|
|
180
|
-
const CULL_MARGIN = 16;
|
|
181
|
-
|
|
182
276
|
/**
|
|
183
277
|
* The props whose change means different pixels. `applyProps` repaints when
|
|
184
278
|
* one of these moves and stays quiet otherwise — the event handlers are
|
|
@@ -200,8 +294,24 @@ const VISUAL_PROPS = [
|
|
|
200
294
|
'nodesConnectable',
|
|
201
295
|
'nodesResizable',
|
|
202
296
|
'disabled',
|
|
297
|
+
'renderer',
|
|
203
298
|
] as const;
|
|
204
299
|
|
|
300
|
+
/** The clock, through `globalThis`: `src/` compiles with `types: []`. */
|
|
301
|
+
const clock = globalThis as { performance?: { now(): number } };
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* The pane's time: what a pan is stamped with, and what the dashes' march
|
|
305
|
+
* reads to decide it has held still. A test holds it (test/flow.test.ts):
|
|
306
|
+
* a runner that spends longer than the dashes' wait over one step of a pan
|
|
307
|
+
* lets them march in the middle of it — as they should — and a test of the
|
|
308
|
+
* pan cannot tell that from a march it should not have made.
|
|
309
|
+
*/
|
|
310
|
+
export const flowClock = {
|
|
311
|
+
now: (): number => clock.performance?.now() ?? Date.now(),
|
|
312
|
+
};
|
|
313
|
+
const now = (): number => flowClock.now();
|
|
314
|
+
|
|
205
315
|
const CONTROL_SIZE = 26;
|
|
206
316
|
const PANEL_MARGIN = 10;
|
|
207
317
|
const MINIMAP_W = 190;
|
|
@@ -327,6 +437,7 @@ export const SELF_DAMAGED_PROPS: readonly string[] = [
|
|
|
327
437
|
'zoomOnDoubleClick',
|
|
328
438
|
'selectionOnDrag',
|
|
329
439
|
'deleteOnKey',
|
|
440
|
+
'adaptive',
|
|
330
441
|
'snapToGrid',
|
|
331
442
|
'snapGrid',
|
|
332
443
|
];
|
|
@@ -383,95 +494,27 @@ function unionMaybe(a: FlowRect | null, b: FlowRect | null): FlowRect | null {
|
|
|
383
494
|
return unionRects(a, b);
|
|
384
495
|
}
|
|
385
496
|
|
|
386
|
-
/**
|
|
387
|
-
*
|
|
388
|
-
|
|
389
|
-
color: string;
|
|
390
|
-
lineWidth: number;
|
|
391
|
-
filled: XYPosition[][];
|
|
392
|
-
open: XYPosition[][];
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Edge strokes, grouped by the pen that will draw them.
|
|
397
|
-
*
|
|
398
|
-
* Everything a graph strokes is the same two or three pens — the default
|
|
399
|
-
* edge, the selected one, the animated dash — so grouping collapses a
|
|
400
|
-
* per-edge request into a per-pen one. The key has to carry the dash *and*
|
|
401
|
-
* its offset: two edges marching out of phase cannot share a path, because
|
|
402
|
-
* the offset is set on the context, not on the subpath.
|
|
403
|
-
*/
|
|
404
|
-
class StrokeBuckets {
|
|
405
|
-
private readonly byPen = new Map<
|
|
406
|
-
string,
|
|
407
|
-
{ options: StrokeOptions; runs: XYPosition[][] }
|
|
408
|
-
>();
|
|
409
|
-
|
|
410
|
-
bucket(
|
|
411
|
-
stroke: string,
|
|
412
|
-
lineWidth: number,
|
|
413
|
-
dash: readonly number[] | undefined,
|
|
414
|
-
phase: number,
|
|
415
|
-
zoom: number,
|
|
416
|
-
): XYPosition[][] {
|
|
417
|
-
const scaled = dash?.map((d) => d * zoom);
|
|
418
|
-
const key = `${stroke}|${lineWidth}|${scaled?.join(',') ?? ''}|${phase}`;
|
|
419
|
-
let entry = this.byPen.get(key);
|
|
420
|
-
if (!entry) {
|
|
421
|
-
entry = {
|
|
422
|
-
options: {
|
|
423
|
-
stroke,
|
|
424
|
-
lineWidth,
|
|
425
|
-
dash: scaled,
|
|
426
|
-
dashOffset: phase ? -phase * zoom : 0,
|
|
427
|
-
},
|
|
428
|
-
runs: [],
|
|
429
|
-
};
|
|
430
|
-
this.byPen.set(key, entry);
|
|
431
|
-
}
|
|
432
|
-
return entry.runs;
|
|
433
|
-
}
|
|
497
|
+
/** The minimap's fit: where its panel is, and the part of the graph it
|
|
498
|
+
* shows at what scale — all that puts a node where it is in it. */
|
|
499
|
+
type MiniMapFit = { panel: FlowRect; bounds: FlowRect; scale: number };
|
|
434
500
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
* a megabyte at a normal window size. That is a large win at seven hundred
|
|
441
|
-
* edges (measured: 3.9 MB a frame down to 1.3) and a loss at twenty, where
|
|
442
|
-
* the individual masks never add up to a paneful. The threshold is where
|
|
443
|
-
* they start to.
|
|
444
|
-
*/
|
|
445
|
-
private static readonly BATCH_MIN = 24;
|
|
501
|
+
/** A node a change moved or resized, and its box in the graph before. */
|
|
502
|
+
interface MiniMapMove {
|
|
503
|
+
entry: NodeEntry;
|
|
504
|
+
before: FlowRect;
|
|
505
|
+
}
|
|
446
506
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
} else {
|
|
452
|
-
for (const run of runs) painter.polyline(run, options);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
507
|
+
function sameRect(p: FlowRect, q: FlowRect): boolean {
|
|
508
|
+
return (
|
|
509
|
+
p.x === q.x && p.y === q.y && p.width === q.width && p.height === q.height
|
|
510
|
+
);
|
|
456
511
|
}
|
|
457
512
|
|
|
458
|
-
|
|
459
|
-
* Are these the same handle? Anchors are rebuilt from the node on every read
|
|
460
|
-
* — a drag moves them between commits — so identity says nothing, and the
|
|
461
|
-
* `id` alone says too little: the node every graph starts with has *two*
|
|
462
|
-
* handles with no id at all. Node, id, type and side together are what
|
|
463
|
-
* distinguish them.
|
|
464
|
-
*/
|
|
465
|
-
function sameHandle(
|
|
466
|
-
a: HandleAnchor | null | undefined,
|
|
467
|
-
b: HandleAnchor,
|
|
468
|
-
): boolean {
|
|
513
|
+
function sameFit(a: MiniMapFit, b: MiniMapFit): boolean {
|
|
469
514
|
return (
|
|
470
|
-
a
|
|
471
|
-
a.
|
|
472
|
-
|
|
473
|
-
a.type === b.type &&
|
|
474
|
-
a.position === b.position
|
|
515
|
+
sameRect(a.panel, b.panel) &&
|
|
516
|
+
sameRect(a.bounds, b.bounds) &&
|
|
517
|
+
a.scale === b.scale
|
|
475
518
|
);
|
|
476
519
|
}
|
|
477
520
|
|
|
@@ -504,21 +547,146 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
504
547
|
* when the app applies the changes it is being sent. Cleared on release,
|
|
505
548
|
* which is also how a refused drag snaps back. */
|
|
506
549
|
private _dragTo: Map<string, XYPosition> | null = null;
|
|
550
|
+
/** Whether the pane's edges are stroked a pen at a time (`EDGE_BATCH`):
|
|
551
|
+
* chosen by a pass over the whole pane, and followed by every pass over
|
|
552
|
+
* part of it until the next. */
|
|
553
|
+
private _batch = false;
|
|
507
554
|
/** The box a resize is currently making, for the same reason. */
|
|
508
555
|
private _resizeTo: { id: string; rect: FlowRect } | null = null;
|
|
509
|
-
/**
|
|
510
|
-
*
|
|
511
|
-
|
|
556
|
+
/** The bodies last sent, re-sent as the same array when only the origin
|
|
557
|
+
* moved, and that origin — and each one by id, reused as the same object
|
|
558
|
+
* when nothing about it changed, so the React half re-renders the card
|
|
559
|
+
* that moved and not the ones beside it. */
|
|
560
|
+
private _bodies: readonly NodeBodyRect[] = [];
|
|
561
|
+
private _bodiesById = new Map<string, NodeBodyRect>();
|
|
562
|
+
private _bodiesOrigin: XYPosition = { x: 0, y: 0 };
|
|
563
|
+
/** Bodies held back while the zoom moves (`_holdBodies`): whether they
|
|
564
|
+
* are, the zoom last seen, the timer that brings them back and when the
|
|
565
|
+
* last held step was. */
|
|
566
|
+
private _bodiesHeld = false;
|
|
567
|
+
private _seenZoom = NaN;
|
|
568
|
+
private _bodiesRest: unknown = null;
|
|
569
|
+
private _bodiesAt = 0;
|
|
570
|
+
/** Mounted cards `<Flow>` shows, painted in the bodies' layer — which
|
|
571
|
+
* the 2D pane then leaves out (`setShownBodies`, `_cardInLayer`). */
|
|
572
|
+
private _shownBodies: ReadonlySet<string> = new Set();
|
|
573
|
+
/** The canvases `<Flow>` paints the minimap and controls on, over the
|
|
574
|
+
* bodies — while there are any, the graph leaves the panels out. */
|
|
575
|
+
private _panelCanvases: readonly PanelCanvas[] = [];
|
|
576
|
+
/** The fit the minimap on screen was painted with: every pass over it
|
|
577
|
+
* since its corner was last claimed whole resolved the same one. Set by
|
|
578
|
+
* the pass that paints it (`_sceneInput`), read by what a move claims
|
|
579
|
+
* there (`_miniMapMoves`). */
|
|
580
|
+
private _miniMapShown: MiniMapFit | null = null;
|
|
581
|
+
private _panelsKey = '';
|
|
582
|
+
/** The budget's model (`_holdBodies`, `_frameTick`): what a body adds to a
|
|
583
|
+
* zoom step, what a step costs with none re-scaled, the last frame's
|
|
584
|
+
* time, and what the step awaiting its frame did. */
|
|
585
|
+
private _bodyStepMs = BODY_STEP_PRIOR_MS;
|
|
586
|
+
private _baseStepMs = NaN;
|
|
587
|
+
private _lastFrameAt = -Infinity;
|
|
588
|
+
private _zoomStep: { live: boolean; bodies: number } | null = null;
|
|
512
589
|
private _dashPhase = 0;
|
|
590
|
+
/** The box the bodies are laid out in, which a 2D pan carries. */
|
|
591
|
+
private _bodiesLayer: Node | null = null;
|
|
592
|
+
/** When a pan last asked to blit — the dash timer waits for it. */
|
|
593
|
+
private _blittedPanAt = -Infinity;
|
|
594
|
+
/** When the view last moved, and when the dash timer last came round,
|
|
595
|
+
* ticking or not: a 2D pan's dashes wait for a view that held still. */
|
|
596
|
+
private _viewMovedAt = -Infinity;
|
|
597
|
+
private _tickAt = -Infinity;
|
|
598
|
+
/** When the dash timer last marched, and what a 2D frame has cost lately
|
|
599
|
+
* — the paint, and the server's answer to the frame before it; a running
|
|
600
|
+
* mean, ms: what a tick waits on. */
|
|
601
|
+
private _marchedAt = -Infinity;
|
|
602
|
+
private _tickCost = 0;
|
|
513
603
|
private _animTimer: unknown = null;
|
|
514
604
|
/** Inside `paint`, where an invalidation would only schedule a redraw of
|
|
515
605
|
* the frame being drawn. */
|
|
516
606
|
private _painting = false;
|
|
607
|
+
/** Routes that survived the last frame — see {@link SceneCache}. */
|
|
608
|
+
private readonly _sceneCache = new SceneCache();
|
|
609
|
+
/** Asks the GL surface for a frame, while one is drawing this pane. */
|
|
610
|
+
private _glRequest: (() => void) | null = null;
|
|
611
|
+
/**
|
|
612
|
+
* Bumped by every repaint this element asks for that is *not* a pure pan
|
|
613
|
+
* — which is how the GL surface knows the graph it holds on the GPU is
|
|
614
|
+
* still the graph, and a frame is only a new offset. Errs one way on
|
|
615
|
+
* purpose: anything it cannot see is covered by the rest of the key
|
|
616
|
+
* (`glFrame`), and a rebuild too many costs a frame's packing, where one
|
|
617
|
+
* too few draws the wrong graph.
|
|
618
|
+
*/
|
|
619
|
+
private _worldVersion = 0;
|
|
620
|
+
/**
|
|
621
|
+
* The same for the nodes a drag is moving under GL, which are lifted out
|
|
622
|
+
* of the world for as long as it moves them (`_lifted`): a step changes
|
|
623
|
+
* them and nothing else, so it moves this and the world stays on the GPU.
|
|
624
|
+
* Rebuilding the world was the whole of a drag step's cost — 5-7 ms of
|
|
625
|
+
* scene and packing a step at 2,000 nodes, for one card.
|
|
626
|
+
*/
|
|
627
|
+
private _liftVersion = 0;
|
|
628
|
+
/** The lifted ids, for the drag gesture they were made for, and a count
|
|
629
|
+
* of every change to them — the world's key, since the world is built
|
|
630
|
+
* without them. */
|
|
631
|
+
private _liftSet: Set<string> | null = null;
|
|
632
|
+
private _liftFor: object | null = null;
|
|
633
|
+
private _liftGen = 0;
|
|
634
|
+
/** Whether a marching edge is in the world, or in the lifted layer. */
|
|
635
|
+
private _worldAnimated = false;
|
|
636
|
+
private _liftedAnimated = false;
|
|
637
|
+
/** The last nodes commit changed lifted nodes and nothing else. */
|
|
638
|
+
private _liftOnly = false;
|
|
639
|
+
/** The overscan the GL world was culled to, in its own pinned
|
|
640
|
+
* coordinates, and the key it was built under. */
|
|
641
|
+
private _worldCull: FlowRect | null = null;
|
|
642
|
+
private _worldCullId = 0;
|
|
643
|
+
private _worldZoom = NaN;
|
|
644
|
+
/** When the zoom last moved, whether that move continued a stream of
|
|
645
|
+
* them — a gesture, where one alone is a jump — and the timer that asks
|
|
646
|
+
* for the frame that rebuilds the GL world once the stream stops. */
|
|
647
|
+
private _zoomAt = -Infinity;
|
|
648
|
+
private _zoomStream = false;
|
|
649
|
+
/** When the frame that drew the latest zoom step began, and when it last
|
|
650
|
+
* finished a pass: an animation's next step follows the frame that drew
|
|
651
|
+
* its last, however long that frame took (`_applyViewport`). */
|
|
652
|
+
private _zoomFrameAt = -Infinity;
|
|
653
|
+
private _zoomDrawnAt = -Infinity;
|
|
654
|
+
private _zoomRest: unknown = null;
|
|
655
|
+
/** A 2D paint drew labels scaled mid-zoom, and owes them at their own
|
|
656
|
+
* sizes once it rests. */
|
|
657
|
+
private _textApproximated = false;
|
|
658
|
+
/**
|
|
659
|
+
* Mid-zoom on the 2D renderer: the graph and its ground as the gesture
|
|
660
|
+
* drew them once, on a surface of their own, composited scaled for every
|
|
661
|
+
* step after — the GL renderer's world drawn scaled, in a bitmap. A step
|
|
662
|
+
* used to stroke every edge, fill every card and lay the grid again, 20 ms
|
|
663
|
+
* of Direct2D a step over the stress lattice, for a picture the next step
|
|
664
|
+
* moved off. Dropped when the zoom rests, which paints it all again
|
|
665
|
+
* exactly, and whenever the graph itself changes.
|
|
666
|
+
*/
|
|
667
|
+
private _zoomShot: ZoomShot | null = null;
|
|
668
|
+
/** What the last `GRAPH_PAINTS_KEPT` whole paints of the graph cost, ms,
|
|
669
|
+
* newest last: the steps of a zoom painted live and the pictures painted
|
|
670
|
+
* for zooms, the two passes that say what a step would cost live — and
|
|
671
|
+
* not a zoom's first step, which sets every label at its size once and
|
|
672
|
+
* says what the steps after it will not. Whether a zoom is worth a
|
|
673
|
+
* picture (`ZOOM_SHOT_WORTH_MS`); empty until a zoom has painted, so the
|
|
674
|
+
* first one starts live. */
|
|
675
|
+
private _graphPaints: number[] = [];
|
|
676
|
+
/** A 2D drag's pictures (`_liftShotFor`), and the gesture that is not to
|
|
677
|
+
* have any: one whose view moved under it, where a picture a step would
|
|
678
|
+
* cost two full paints a step. */
|
|
679
|
+
private _liftShot: LiftShot | null = null;
|
|
680
|
+
private _liftShotRefused: object | null = null;
|
|
517
681
|
/** Inside a live input dispatch — what makes a body emission `sync`.
|
|
518
682
|
* Motion and the wheel run at continuous priority, whose React updates
|
|
519
683
|
* can trail the pane's own painting by frames; an emission made under
|
|
520
684
|
* this flag asks the receiver to commit before the dispatch returns. */
|
|
521
685
|
private _gestureSync = false;
|
|
686
|
+
/** Inside a call through `FlowInstance` (`fromHandle`) — an application's
|
|
687
|
+
* handler, timer or animation loop moving the viewport. Its emissions ask
|
|
688
|
+
* to commit inline too. */
|
|
689
|
+
private _handleSync = false;
|
|
522
690
|
/** The scene has been offered to assistive tech at least once — items
|
|
523
691
|
* only exist once layout has placed them, which no commit marks. */
|
|
524
692
|
private _sceneAnnounced = false;
|
|
@@ -616,13 +784,40 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
616
784
|
private _measure = (text: string, options?: TextOptions): number =>
|
|
617
785
|
measureText(this._textOptions(), text, options).width;
|
|
618
786
|
|
|
787
|
+
/** The same shaping, both dimensions: what a scene's label chips are
|
|
788
|
+
* sized from. `_measure` answers a width because `measureNode` only ever
|
|
789
|
+
* wanted one. */
|
|
790
|
+
private _measureBox = (
|
|
791
|
+
text: string,
|
|
792
|
+
options?: TextOptions,
|
|
793
|
+
): { width: number; height: number } =>
|
|
794
|
+
measureText(this._textOptions(), text, options);
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* The palette, resolved once for the theme and the `palette` prop it was
|
|
798
|
+
* resolved from. Every text measured asks for it (`_textOptions`), and
|
|
799
|
+
* resolving it tints the accent and reads the background's lightness out
|
|
800
|
+
* of a colour string: a 2D pan's passes measured their labels with a
|
|
801
|
+
* fresh one each. The theme object is replaced when it changes.
|
|
802
|
+
*/
|
|
619
803
|
private _palette(): FlowPalette {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
)
|
|
804
|
+
const theme = this.theme as Record<string, unknown> | null;
|
|
805
|
+
const overrides = this._prop<Partial<FlowPalette>>('palette');
|
|
806
|
+
const held = this._paletteHeld;
|
|
807
|
+
if (held && held.theme === theme && held.overrides === overrides) {
|
|
808
|
+
return held.palette;
|
|
809
|
+
}
|
|
810
|
+
const palette = resolvePalette(theme, overrides);
|
|
811
|
+
this._paletteHeld = { theme, overrides, palette };
|
|
812
|
+
return palette;
|
|
624
813
|
}
|
|
625
814
|
|
|
815
|
+
private _paletteHeld: {
|
|
816
|
+
theme: Record<string, unknown> | null;
|
|
817
|
+
overrides: Partial<FlowPalette> | undefined;
|
|
818
|
+
palette: FlowPalette;
|
|
819
|
+
} | null = null;
|
|
820
|
+
|
|
626
821
|
// --- the derived graph ---------------------------------------------------
|
|
627
822
|
|
|
628
823
|
/**
|
|
@@ -686,12 +881,12 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
686
881
|
// a fresh literal saying the same thing: keep the built entry
|
|
687
882
|
continue;
|
|
688
883
|
}
|
|
689
|
-
const wasBox = this.
|
|
884
|
+
const wasBox = this._edgeInkBox(this._edges[i]);
|
|
690
885
|
this._edges[i] = defaults
|
|
691
886
|
? ({ ...defaults, ...next } as AnyEdge)
|
|
692
887
|
: (next as AnyEdge);
|
|
693
888
|
this._edgesRaw[i] = next;
|
|
694
|
-
const isBox = this.
|
|
889
|
+
const isBox = this._edgeInkBox(this._edges[i]);
|
|
695
890
|
if (wasBox) damage = damage ? unionRects(damage, wasBox) : wasBox;
|
|
696
891
|
if (isBox) damage = damage ? unionRects(damage, isBox) : isBox;
|
|
697
892
|
if (next.source !== old.source || next.target !== old.target) {
|
|
@@ -722,9 +917,23 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
722
917
|
* Returns `'full'` after a rebuild, a damage rect after an in-place fold,
|
|
723
918
|
* and `null` when nothing visual changed at all.
|
|
724
919
|
*/
|
|
725
|
-
private _applyNodes(nodes: readonly AnyNode[]): 'full' | FlowRect
|
|
920
|
+
private _applyNodes(nodes: readonly AnyNode[]): 'full' | FlowRect[] {
|
|
921
|
+
const sameCardText = (a: AnyNode, b: AnyNode): boolean => {
|
|
922
|
+
const da = a.data as FlowNodeData | undefined;
|
|
923
|
+
const db = b.data as FlowNodeData | undefined;
|
|
924
|
+
return (
|
|
925
|
+
(da?.label ?? a.id) === (db?.label ?? b.id) &&
|
|
926
|
+
da?.description === db?.description
|
|
927
|
+
);
|
|
928
|
+
};
|
|
726
929
|
const prev = this._entries;
|
|
727
930
|
this._nodesSeen = this.props.nodes;
|
|
931
|
+
// Whether every node this commit changed is one a drag has lifted out
|
|
932
|
+
// of the GL world — the commit of the drag's own step, when the app
|
|
933
|
+
// stores it — so the world on the GPU is still the world.
|
|
934
|
+
const lift = this._lifted();
|
|
935
|
+
let onlyLifted = lift != null;
|
|
936
|
+
this._liftOnly = false;
|
|
728
937
|
let structural = nodes.length !== prev.length;
|
|
729
938
|
if (!structural) {
|
|
730
939
|
for (let i = 0; i < nodes.length; i++) {
|
|
@@ -734,7 +943,6 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
734
943
|
if (
|
|
735
944
|
n.id !== o.id ||
|
|
736
945
|
n.type !== o.type ||
|
|
737
|
-
n.selected !== o.selected ||
|
|
738
946
|
n.hidden !== o.hidden ||
|
|
739
947
|
n.zIndex !== o.zIndex ||
|
|
740
948
|
n.handles !== o.handles ||
|
|
@@ -752,15 +960,39 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
752
960
|
this._rebuildNodes();
|
|
753
961
|
return 'full';
|
|
754
962
|
}
|
|
755
|
-
|
|
963
|
+
const damage: FlowRect[] = [];
|
|
964
|
+
// A node that is selected, or no longer is, is restyled and restacked —
|
|
965
|
+
// selection lifts it over its neighbours — and both happen inside its
|
|
966
|
+
// own box: edges are drawn under every node. Treated as structural, a
|
|
967
|
+
// selection change re-measured every node and repainted the pane, which
|
|
968
|
+
// a box selection does on every step that takes a node in.
|
|
969
|
+
let reordered = false;
|
|
970
|
+
// The minimap draws every node where it is, at its size, marked when it
|
|
971
|
+
// is selected — and, to a `nodeColor` function, as anything at all.
|
|
972
|
+
const colour = this._miniMapOptions()?.nodeColor;
|
|
973
|
+
let mapped = false;
|
|
974
|
+
// …and where the pane draws it, a node that only moved or changed size
|
|
975
|
+
// changed its own places in it, while the fit holds (`_miniMapMoves`)
|
|
976
|
+
const drawsMap = this._drawsMiniMap();
|
|
977
|
+
const moves: MiniMapMove[] = [];
|
|
978
|
+
// round the box: its handles, and the resize grips a selected node
|
|
979
|
+
// grows at its corners, which at a deep zoom reach past the cull margin
|
|
980
|
+
const margin = Math.max(
|
|
981
|
+
CULL_MARGIN,
|
|
982
|
+
RESIZE_GRIP * this._viewport().zoom + 2,
|
|
983
|
+
);
|
|
756
984
|
for (let i = 0; i < nodes.length; i++) {
|
|
757
985
|
const entry = prev[i];
|
|
758
986
|
const next = nodes[i];
|
|
759
987
|
const old = entry.node;
|
|
760
988
|
if (next === old) continue;
|
|
989
|
+
if (!lift?.has(next.id)) onlyLifted = false;
|
|
990
|
+
if (next.selected !== old.selected) reordered = true;
|
|
991
|
+
if (typeof colour === 'function') mapped = true;
|
|
761
992
|
const moved =
|
|
762
993
|
next.position.x !== old.position.x ||
|
|
763
994
|
next.position.y !== old.position.y;
|
|
995
|
+
if (moved) mapped = true;
|
|
764
996
|
// `data`, an explicit size or a paint style are the node's own to
|
|
765
997
|
// change: they re-measure and repaint *this* node, never the graph. A
|
|
766
998
|
// keystroke into a mounted node's textarea patches `data` on every
|
|
@@ -771,7 +1003,8 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
771
1003
|
next.data !== old.data ||
|
|
772
1004
|
next.width !== old.width ||
|
|
773
1005
|
next.height !== old.height;
|
|
774
|
-
const restyled =
|
|
1006
|
+
const restyled =
|
|
1007
|
+
next.style !== old.style || next.selected !== old.selected;
|
|
775
1008
|
if (!moved && !reshaped && !restyled) {
|
|
776
1009
|
// a behavioural flag (draggable, deletable…) — nothing drawn reads it
|
|
777
1010
|
entry.node = next;
|
|
@@ -779,6 +1012,8 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
779
1012
|
}
|
|
780
1013
|
const widthBefore = entry.width;
|
|
781
1014
|
const heightBefore = entry.height;
|
|
1015
|
+
const placeBefore =
|
|
1016
|
+
drawsMap && (moved || reshaped) ? this.rectOf(entry) : null;
|
|
782
1017
|
let box = moved ? this._nodeDamage(entry) : this._screenRect(entry);
|
|
783
1018
|
entry.node = next;
|
|
784
1019
|
if (reshaped) {
|
|
@@ -788,6 +1023,26 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
788
1023
|
entry.specs = resolveHandles(next, entry.type);
|
|
789
1024
|
}
|
|
790
1025
|
const grew = entry.width !== widthBefore || entry.height !== heightBefore;
|
|
1026
|
+
if (grew) mapped = true;
|
|
1027
|
+
// A drag's own step, stored by the app, moves nothing on screen:
|
|
1028
|
+
// the node was already where the drag had it
|
|
1029
|
+
if (placeBefore && !sameRect(placeBefore, this.rectOf(entry))) {
|
|
1030
|
+
moves.push({ entry, before: placeBefore });
|
|
1031
|
+
}
|
|
1032
|
+
// Nothing the pane draws changed, while the node holds its place and
|
|
1033
|
+
// its size, when its card is the bodies' layer's on the 2D renderer —
|
|
1034
|
+
// the layer repaints it, if anything it shows changed — or when what
|
|
1035
|
+
// changed is data its card does not show: a card shows the label, the
|
|
1036
|
+
// description and the style, and a type that paints its own may show
|
|
1037
|
+
// anything. A board of live widgets, a tenth of them patching their
|
|
1038
|
+
// data ten times a second, rebuilt the GL world on every patch, and on
|
|
1039
|
+
// the 2D renderer repainted cards that showed none of it.
|
|
1040
|
+
if (!moved && !grew) {
|
|
1041
|
+
if (this._cardInLayer(next.id)) continue;
|
|
1042
|
+
if (!restyled && !entry.type?.paint && sameCardText(old, next)) {
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
791
1046
|
// The edges ride along only when an endpoint actually moved — a
|
|
792
1047
|
// label edit that kept the box is the box's own business, and a
|
|
793
1048
|
// keystroke that unioned its node's edges swept half the layer's
|
|
@@ -796,9 +1051,22 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
796
1051
|
box,
|
|
797
1052
|
moved || grew ? this._nodeDamage(entry) : this._screenRect(entry),
|
|
798
1053
|
);
|
|
799
|
-
damage
|
|
1054
|
+
damage.push(inflateRect(box, margin));
|
|
800
1055
|
}
|
|
801
|
-
|
|
1056
|
+
if (reordered) this._sortOrder();
|
|
1057
|
+
if (reordered || mapped) {
|
|
1058
|
+
// A node that moved, grew or was picked out is a change in the
|
|
1059
|
+
// minimap's corner too. Only a selection used to claim it, so a node
|
|
1060
|
+
// the app moved repainted its own box and edges and the minimap went
|
|
1061
|
+
// on showing it where it had been. A selection, or a colour worked
|
|
1062
|
+
// out per node, is the whole corner's: it changes how the nodes there
|
|
1063
|
+
// are filled together (`SceneMiniMapNode`).
|
|
1064
|
+
const whole = !drawsMap || reordered || typeof colour === 'function';
|
|
1065
|
+
const map = whole ? this._miniMapCorner() : this._miniMapMoves(moves);
|
|
1066
|
+
if (map) damage.push(map);
|
|
1067
|
+
}
|
|
1068
|
+
this._liftOnly = onlyLifted;
|
|
1069
|
+
return damage;
|
|
802
1070
|
}
|
|
803
1071
|
|
|
804
1072
|
/**
|
|
@@ -817,12 +1085,16 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
817
1085
|
let box = this._screenRect(entry);
|
|
818
1086
|
const edges = this._edgesByNode.get(entry.node.id) ?? [];
|
|
819
1087
|
const tight = edges.length <= 16;
|
|
1088
|
+
const sv = this._screenViewport();
|
|
820
1089
|
for (const edge of edges) {
|
|
821
1090
|
const geometry = tight ? this._edgeGeometry(edge) : null;
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1091
|
+
// the route's own ink, heads and pen and all — the pen only past the
|
|
1092
|
+
// margin every caller grows this by — or the coarse box, whose slack
|
|
1093
|
+
// holds both
|
|
1094
|
+
const ink = geometry
|
|
1095
|
+
? edgeInkBox(sv, edge, geometry.points, CULL_MARGIN)
|
|
1096
|
+
: this._edgeInkBox(edge);
|
|
1097
|
+
if (ink) box = unionRects(box, ink);
|
|
826
1098
|
}
|
|
827
1099
|
return box;
|
|
828
1100
|
}
|
|
@@ -847,7 +1119,13 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
847
1119
|
}
|
|
848
1120
|
this._entries = entries;
|
|
849
1121
|
this._byId = byId;
|
|
850
|
-
this.
|
|
1122
|
+
this._sortOrder();
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/** The paint order: by `zIndex`, a selected node over the rest of its
|
|
1126
|
+
* layer, and declaration order breaking ties. */
|
|
1127
|
+
private _sortOrder(): void {
|
|
1128
|
+
this._order = this._entries
|
|
851
1129
|
.map((entry, index) => ({ entry, index }))
|
|
852
1130
|
.sort((a, b) => {
|
|
853
1131
|
const az = a.entry.node.zIndex ?? 0;
|
|
@@ -892,6 +1170,12 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
892
1170
|
return this._prop<Viewport>('viewport') ?? this._vp;
|
|
893
1171
|
}
|
|
894
1172
|
|
|
1173
|
+
/** The viewport with the pane's window origin folded in — what every
|
|
1174
|
+
* graph-to-window conversion shared with `./scene.ts` takes. */
|
|
1175
|
+
private _screenViewport(): Viewport {
|
|
1176
|
+
return screenViewport(this._viewport(), this._pane());
|
|
1177
|
+
}
|
|
1178
|
+
|
|
895
1179
|
/**
|
|
896
1180
|
* Device pixels per logical pixel — the display scale this pane's window
|
|
897
1181
|
* resolved to (react-x11's docs/scale.md): `1` on an ordinary display,
|
|
@@ -946,6 +1230,48 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
946
1230
|
this.invalidate(false, this._device(rect), reason);
|
|
947
1231
|
}
|
|
948
1232
|
|
|
1233
|
+
/** Whether the pane draws the minimap itself. On a canvas of its own —
|
|
1234
|
+
* under mounted bodies — it is `invalidate`'s to repaint. */
|
|
1235
|
+
private _drawsMiniMap(): boolean {
|
|
1236
|
+
return this._panelCanvases.length === 0 && this._miniMapOptions() != null;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* What moving or resizing `moves` changed in the minimap's corner, where
|
|
1241
|
+
* the pane draws it: each node's place there before and after, while the
|
|
1242
|
+
* fit is the one on screen — the graph's bounds with the viewport's,
|
|
1243
|
+
* which put every node where it is there — and the whole corner when it
|
|
1244
|
+
* is not, since every node moves in it then. A drag of one node among
|
|
1245
|
+
* 2,000 claimed the corner on every step, and every such pass built and
|
|
1246
|
+
* filled all 2,000 again, where it builds what it reaches now
|
|
1247
|
+
* (`buildMiniMap`). Asked against the fit on screen rather than the one
|
|
1248
|
+
* before the change, a step works the fit out once, and not at all when
|
|
1249
|
+
* nothing moved on screen.
|
|
1250
|
+
*/
|
|
1251
|
+
private _miniMapMoves(moves: readonly MiniMapMove[]): FlowRect | null {
|
|
1252
|
+
const corner = this._miniMapCorner();
|
|
1253
|
+
if (!corner || moves.length === 0) return null;
|
|
1254
|
+
const fit = this._miniMap();
|
|
1255
|
+
const shown = this._miniMapShown;
|
|
1256
|
+
if (!fit && !shown) return null;
|
|
1257
|
+
if (!fit || !shown || !sameFit(fit, shown)) return corner;
|
|
1258
|
+
let out: FlowRect | null = null;
|
|
1259
|
+
for (const { entry, before } of moves) {
|
|
1260
|
+
out = unionMaybe(out, miniMapPlace(shown, before));
|
|
1261
|
+
out = unionMaybe(out, miniMapPlace(fit, this.rectOf(entry)));
|
|
1262
|
+
}
|
|
1263
|
+
return out && inflateRect(out, MINIMAP_BLEED);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/** A gesture's step, claimed in the minimap: `_miniMapMoves`. */
|
|
1267
|
+
private _claimMiniMapMoves(
|
|
1268
|
+
moves: readonly MiniMapMove[],
|
|
1269
|
+
reason: string,
|
|
1270
|
+
): void {
|
|
1271
|
+
const map = this._miniMapMoves(moves);
|
|
1272
|
+
if (map) this._claim(map, reason);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
949
1275
|
/**
|
|
950
1276
|
* The pane proper: `abs` inset by border and padding, off core's own
|
|
951
1277
|
* resolved layout (react-x11#254) — so a `<Flow style={{ borderWidth: 1,
|
|
@@ -1027,9 +1353,96 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
1027
1353
|
});
|
|
1028
1354
|
}
|
|
1029
1355
|
|
|
1356
|
+
/**
|
|
1357
|
+
* One entry as `./scene.ts` takes it: the live gesture state resolved
|
|
1358
|
+
* away, so everything past this point is pure. A drag's position and a
|
|
1359
|
+
* resize's box are already in `rect`, and `grips` is already filtered by
|
|
1360
|
+
* whatever the handles took.
|
|
1361
|
+
*
|
|
1362
|
+
* Built per call rather than cached: `rectOf` is what keeps a drag in step
|
|
1363
|
+
* with the commit that has not landed yet, and a cache here is a cache of
|
|
1364
|
+
* last frame's positions.
|
|
1365
|
+
*/
|
|
1366
|
+
/** Whether a React body is mounted over this node — which is what makes
|
|
1367
|
+
* its card draw a title bar rather than a centred label, and what
|
|
1368
|
+
* `_emitBodies` reports a box for. */
|
|
1369
|
+
private _mounted(entry: NodeEntry): boolean {
|
|
1370
|
+
return entry.type?.render != null && this._viewport().zoom >= RENDER_ZOOM;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
/**
|
|
1374
|
+
* The frame being painted: each node's scene source, and the two lists a
|
|
1375
|
+
* 2D pass builds its scene from. A frame paints its damage as several
|
|
1376
|
+
* passes — a pan's strips, its pinned furniture, the corners of a rounded
|
|
1377
|
+
* pane — and the scene cache culls the graph to the screen once for all
|
|
1378
|
+
* of them (`SceneCache.edgesOnScreen`), keyed on these very lists, so
|
|
1379
|
+
* every pass has to be handed the same ones. The passes run one after
|
|
1380
|
+
* another in a single task, so a microtask queued by the first forgets
|
|
1381
|
+
* the frame after the last — the same reasoning as `_reportFrame`.
|
|
1382
|
+
*/
|
|
1383
|
+
private _frame: {
|
|
1384
|
+
sources: Map<NodeEntry, SceneNodeSource>;
|
|
1385
|
+
lists: { all: SceneNodeSource[]; nodes: SceneNodeSource[] } | null;
|
|
1386
|
+
} | null = null;
|
|
1387
|
+
|
|
1388
|
+
private _paintedFrame(): NonNullable<FlowGraphNode['_frame']> {
|
|
1389
|
+
let frame = this._frame;
|
|
1390
|
+
if (!frame) {
|
|
1391
|
+
frame = this._frame = { sources: new Map(), lists: null };
|
|
1392
|
+
void Promise.resolve().then(() => {
|
|
1393
|
+
this._frame = null;
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
return frame;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
private _sourceOf(entry: NodeEntry): SceneNodeSource {
|
|
1400
|
+
if (!this._painting) return this._source(entry);
|
|
1401
|
+
const { sources } = this._paintedFrame();
|
|
1402
|
+
let source = sources.get(entry);
|
|
1403
|
+
if (!source) {
|
|
1404
|
+
source = this._source(entry);
|
|
1405
|
+
sources.set(entry, source);
|
|
1406
|
+
}
|
|
1407
|
+
return source;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/** A 2D pass's node lists, the same arrays for every pass of a frame. */
|
|
1411
|
+
private _frameLists(order: readonly NodeEntry[]): {
|
|
1412
|
+
all: SceneNodeSource[];
|
|
1413
|
+
nodes: SceneNodeSource[];
|
|
1414
|
+
} {
|
|
1415
|
+
const frame = this._paintedFrame();
|
|
1416
|
+
frame.lists ??= {
|
|
1417
|
+
all: this._entries.map((entry) => this._sourceOf(entry)),
|
|
1418
|
+
nodes: order
|
|
1419
|
+
.filter((entry) => !this._cardInLayer(entry.node.id))
|
|
1420
|
+
.map((entry) => this._sourceOf(entry)),
|
|
1421
|
+
};
|
|
1422
|
+
return frame.lists;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
private _source(entry: NodeEntry): SceneNodeSource {
|
|
1426
|
+
return {
|
|
1427
|
+
node: entry.node,
|
|
1428
|
+
rect: this.rectOf(entry),
|
|
1429
|
+
specs: entry.specs,
|
|
1430
|
+
type: entry.type,
|
|
1431
|
+
header: this._headerHeight(entry),
|
|
1432
|
+
mounted: this._mounted(entry),
|
|
1433
|
+
connectable: this._connectable(entry),
|
|
1434
|
+
grips: this._grips(entry),
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/** Not through `_source`: a source carries its grips, and `_grips` asks
|
|
1439
|
+
* where the handles are — the pair would recur. */
|
|
1030
1440
|
private _handlesOf(entry: NodeEntry): HandleAnchor[] {
|
|
1031
|
-
|
|
1032
|
-
|
|
1441
|
+
return anchorsOf({
|
|
1442
|
+
node: entry.node,
|
|
1443
|
+
rect: this.rectOf(entry),
|
|
1444
|
+
specs: entry.specs,
|
|
1445
|
+
});
|
|
1033
1446
|
}
|
|
1034
1447
|
|
|
1035
1448
|
/**
|
|
@@ -1047,22 +1460,14 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
1047
1460
|
* testing reads the same rect, so what is drawn and what is clicked still
|
|
1048
1461
|
* agree to the pixel.
|
|
1049
1462
|
*/
|
|
1050
|
-
private _screenRect(
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
return {
|
|
1059
|
-
x,
|
|
1060
|
-
y,
|
|
1061
|
-
// rounded as edges rather than as a size, so two nodes that share a
|
|
1062
|
-
// column still share it after rounding
|
|
1063
|
-
width: grid(p.x + rect.width * v.zoom) - x,
|
|
1064
|
-
height: grid(p.y + rect.height * v.zoom) - y,
|
|
1065
|
-
};
|
|
1463
|
+
private _screenRect(
|
|
1464
|
+
entry: NodeEntry,
|
|
1465
|
+
// A loop over the nodes passes it in: the viewport reads the pane's
|
|
1466
|
+
// content box off layout, and asked once per node it was most of what a
|
|
1467
|
+
// hit test cost on a large graph — every pointer move, 400 times.
|
|
1468
|
+
sv: Viewport = this._screenViewport(),
|
|
1469
|
+
): FlowRect {
|
|
1470
|
+
return screenRect(sv, this.rectOf(entry), this._scale);
|
|
1066
1471
|
}
|
|
1067
1472
|
|
|
1068
1473
|
private _visible(): boolean {
|
|
@@ -1082,12 +1487,46 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
1082
1487
|
const v = { x: next.x, y: next.y, zoom };
|
|
1083
1488
|
const controlled = this.props.viewport !== undefined;
|
|
1084
1489
|
if (!controlled) this._vp = v;
|
|
1490
|
+
if (previous.x !== v.x || previous.y !== v.y || previous.zoom !== v.zoom) {
|
|
1491
|
+
this._viewMovedAt = now();
|
|
1492
|
+
}
|
|
1493
|
+
if (previous.zoom !== v.zoom) {
|
|
1494
|
+
// A stream is steps close together — or a step that follows the frame
|
|
1495
|
+
// that drew the one before it: an animation zooms a step a frame, and
|
|
1496
|
+
// a frame whose bodies re-scale can take longer than the rest. Judged
|
|
1497
|
+
// on the steps alone, such an animation was a run of single jumps, so
|
|
1498
|
+
// its bodies were never held, every step re-scaled them all, and it
|
|
1499
|
+
// stayed at two frames a second where held it ran at twenty — on
|
|
1500
|
+
// whichever side of the line its first frames happened to fall.
|
|
1501
|
+
const t = now();
|
|
1502
|
+
this._zoomStream =
|
|
1503
|
+
t - this._zoomAt < GL_ZOOM_REST_MS ||
|
|
1504
|
+
t - this._zoomDrawnAt < GL_ZOOM_REST_MS;
|
|
1505
|
+
this._zoomAt = t;
|
|
1506
|
+
this._zoomFrameAt = -Infinity;
|
|
1507
|
+
this._zoomDrawnAt = -Infinity;
|
|
1508
|
+
}
|
|
1509
|
+
// Each claims its own box: a claim with no region is the whole window,
|
|
1510
|
+
// which made every pan step and dash tick a full frame. The minimap's
|
|
1511
|
+
// alone: its view box moves, and the controls show nothing of the
|
|
1512
|
+
// viewport.
|
|
1513
|
+
for (const canvas of this._miniMapCanvases())
|
|
1514
|
+
canvas.invalidate(false, canvas, 'props');
|
|
1085
1515
|
this._prop<(vp: Viewport) => void>('onViewportChange')?.(v);
|
|
1086
1516
|
// The `fitView` that runs at the top of a paint has already changed what
|
|
1087
1517
|
// this frame will draw, so asking for another one would only draw the
|
|
1088
1518
|
// same picture twice.
|
|
1089
1519
|
if (!controlled && !this._painting) {
|
|
1090
|
-
if (
|
|
1520
|
+
if (this._gl) {
|
|
1521
|
+
// Under GL a pan is the surface's offset uniform and nothing else,
|
|
1522
|
+
// and a zoom is the frame's to decide — the world scaled while the
|
|
1523
|
+
// gesture moves, rebuilt once it rests (`glFrame`). Neither is a
|
|
1524
|
+
// change to the world, and the 2D pane under the surface shows none
|
|
1525
|
+
// of the graph: claiming its box every step repainted it for
|
|
1526
|
+
// nothing (~4 ms a frame) — and, with bodies mounted, reached
|
|
1527
|
+
// core's overlay too.
|
|
1528
|
+
this._glRequest?.();
|
|
1529
|
+
} else if (!this._blitPan(previous, v)) this._repaint('scroll');
|
|
1091
1530
|
// same-frame compositing for mounted bodies — see `_dragStep`
|
|
1092
1531
|
this._emitBodies();
|
|
1093
1532
|
}
|
|
@@ -1101,26 +1540,33 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
1101
1540
|
* the shift exposed — which `paintDamage()` then hands to `paint`, so the
|
|
1102
1541
|
* existing culling draws the sliver and nothing else.
|
|
1103
1542
|
*
|
|
1104
|
-
* The furniture — minimap, zoom controls —
|
|
1105
|
-
*
|
|
1106
|
-
*
|
|
1107
|
-
*
|
|
1108
|
-
*
|
|
1109
|
-
*
|
|
1110
|
-
*
|
|
1543
|
+
* The furniture — minimap, zoom controls — stays put while the graph
|
|
1544
|
+
* moves, so it is handed over as `pinned` (react-x11#682, 2.22): the
|
|
1545
|
+
* whole pane is copied, and core repaints each panel and the image of
|
|
1546
|
+
* it the copy dragged along, with the strips the shift exposed.
|
|
1547
|
+
*
|
|
1548
|
+
* Mounted node bodies ride it. They are laid out in one box beside the
|
|
1549
|
+
* pane, which a pan moves by exactly the pan, so their pixels move with
|
|
1550
|
+
* the pane's: the box goes to `scrollContents` as a rider
|
|
1551
|
+
* (react-x11#671), its commit claims nothing, and what it leaves or
|
|
1552
|
+
* reaches outside the rect is all it costs. `<Flow>` clips it to the pane for that. A body entering or
|
|
1553
|
+
* leaving the pane, or changing as it goes, claims inside the rect and
|
|
1554
|
+
* declines that frame's blit; the next one blits again.
|
|
1111
1555
|
*
|
|
1112
1556
|
* Still a full repaint when:
|
|
1113
1557
|
* - the zoom moved (scaling is not a blit) or the shift is fractional on
|
|
1114
1558
|
* the device grid — every real pan gesture is whole device pixels;
|
|
1115
|
-
* - mounted
|
|
1116
|
-
*
|
|
1117
|
-
*
|
|
1118
|
-
* - the furniture bands would eat the pane (a tiny pane, or panels on
|
|
1119
|
-
* both the top and the bottom of a short one).
|
|
1559
|
+
* - bodies are mounted and `<Flow>` has not handed their box over;
|
|
1560
|
+
* - the pane is too small to be worth it; and core falls back itself
|
|
1561
|
+
* when the repairs would repaint most of it.
|
|
1120
1562
|
*/
|
|
1121
1563
|
private _blitPan(previous: Viewport, next: Viewport): boolean {
|
|
1564
|
+
// There is no backing store to scroll under GL: the surface redraws the
|
|
1565
|
+
// whole scene every frame, and a pan is its cheapest frame of all.
|
|
1566
|
+
if (this._gl) return false;
|
|
1122
1567
|
if (next.zoom !== previous.zoom) return false;
|
|
1123
|
-
|
|
1568
|
+
const riders = this._bodies.length > 0 ? this._bodiesLayer : null;
|
|
1569
|
+
if (this._bodies.length > 0 && !riders) return false;
|
|
1124
1570
|
// Device pixels: the blit copies the backing store, and its grid is the
|
|
1125
1571
|
// panel's. A pointer step lands on it whatever the scale — it came off
|
|
1126
1572
|
// the wire as whole device pixels — so every real pan gesture blits.
|
|
@@ -1131,77 +1577,68 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
1131
1577
|
const dy = Math.round(shiftY);
|
|
1132
1578
|
if (dx === 0 && dy === 0) return true; // sub-pixel: nothing to show yet
|
|
1133
1579
|
if (shiftX !== dx || shiftY !== dy) return false;
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
//
|
|
1137
|
-
//
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
};
|
|
1147
|
-
const map = this._miniMapOptions();
|
|
1148
|
-
if (map) {
|
|
1149
|
-
claim(
|
|
1150
|
-
this._corner(
|
|
1151
|
-
map.position,
|
|
1152
|
-
map.width ?? MINIMAP_W,
|
|
1153
|
-
map.height ?? MINIMAP_H,
|
|
1154
|
-
'bottom-right',
|
|
1155
|
-
),
|
|
1156
|
-
map.position,
|
|
1157
|
-
);
|
|
1158
|
-
}
|
|
1159
|
-
const buttons = this._controlButtons();
|
|
1160
|
-
if (buttons.length > 0) {
|
|
1161
|
-
const first = buttons[0].rect;
|
|
1162
|
-
const last = buttons[buttons.length - 1].rect;
|
|
1163
|
-
claim(
|
|
1164
|
-
unionRects(first, last),
|
|
1165
|
-
this._controlsOptions()?.position ?? 'bottom-left',
|
|
1166
|
-
);
|
|
1167
|
-
}
|
|
1168
|
-
// From here on, device pixels: `scrollContents` and the claims speak
|
|
1169
|
-
// core's units. The bands round *up* and the blit is what is left, so
|
|
1170
|
-
// the three sit edge to edge on whole pixels — a claim overlapping the
|
|
1171
|
-
// blit rect by a pixel is a foreign claim, and declines the blit.
|
|
1580
|
+
// The furniture — minimap, zoom controls — stays put while the graph
|
|
1581
|
+
// moves under it: pinned inside the region, which core repaints after
|
|
1582
|
+
// the copy, with the stale image the copy dragged along (react-x11
|
|
1583
|
+
// #682, 2.22). It used to be carved out of the region, and a region is
|
|
1584
|
+
// one rectangle, so controls in one bottom corner and the minimap in the
|
|
1585
|
+
// other carved a band the pane's full width — every card, label and edge
|
|
1586
|
+
// in it repainted on every pan frame: 47 fps over the stress example's
|
|
1587
|
+
// widgets on XQuartz, where a pane with no furniture pans at 80.
|
|
1588
|
+
//
|
|
1589
|
+
// Device pixels, rounded out, and a pixel over: a panel's own claims —
|
|
1590
|
+
// the minimap's view box moves with every pan — must land inside its
|
|
1591
|
+
// pinned rect, or they decline the blit.
|
|
1172
1592
|
const box = this.contentBox();
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
const
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
if (Math.abs(dx) >= blit.width || Math.abs(dy) >= blit.height) {
|
|
1183
|
-
return false;
|
|
1184
|
-
}
|
|
1185
|
-
this.scrollContents(blit, dx, dy);
|
|
1186
|
-
if (top > 0) {
|
|
1187
|
-
this.invalidate(
|
|
1188
|
-
false,
|
|
1189
|
-
{ x: box.x, y: box.y, width: box.width, height: top },
|
|
1190
|
-
'scroll',
|
|
1593
|
+
if (box.width < 64 * s || box.height < 64 * s) return false;
|
|
1594
|
+
if (Math.abs(dx) >= box.width || Math.abs(dy) >= box.height) return false;
|
|
1595
|
+
const pinned: FlowRect[] = [];
|
|
1596
|
+
const pin = (rect: FlowRect): void => {
|
|
1597
|
+
const x0 = Math.max(box.x, Math.floor(rect.x) - 1);
|
|
1598
|
+
const y0 = Math.max(box.y, Math.floor(rect.y) - 1);
|
|
1599
|
+
const x1 = Math.min(
|
|
1600
|
+
box.x + box.width,
|
|
1601
|
+
Math.ceil(rect.x + rect.width) + 1,
|
|
1191
1602
|
);
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
false,
|
|
1196
|
-
{
|
|
1197
|
-
x: box.x,
|
|
1198
|
-
y: blit.y + blit.height,
|
|
1199
|
-
width: box.width,
|
|
1200
|
-
height: bottom,
|
|
1201
|
-
},
|
|
1202
|
-
'scroll',
|
|
1603
|
+
const y1 = Math.min(
|
|
1604
|
+
box.y + box.height,
|
|
1605
|
+
Math.ceil(rect.y + rect.height) + 1,
|
|
1203
1606
|
);
|
|
1607
|
+
if (x1 > x0 && y1 > y0) {
|
|
1608
|
+
pinned.push({ x: x0, y: y0, width: x1 - x0, height: y1 - y0 });
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
if (this._panelCanvases.length > 0) {
|
|
1612
|
+
// Painted on canvases of their own, over mounted bodies: a node laid
|
|
1613
|
+
// over the region is pinned by its paint bounds, which carry core's
|
|
1614
|
+
// slop round its box.
|
|
1615
|
+
for (const canvas of this._panelCanvases) {
|
|
1616
|
+
const reach = canvas.paintBounds?.();
|
|
1617
|
+
if (reach) pin(reach);
|
|
1618
|
+
}
|
|
1619
|
+
} else {
|
|
1620
|
+
const map = this._miniMapOptions();
|
|
1621
|
+
if (map) {
|
|
1622
|
+
pin(
|
|
1623
|
+
this._device(
|
|
1624
|
+
this._corner(
|
|
1625
|
+
map.position,
|
|
1626
|
+
map.width ?? MINIMAP_W,
|
|
1627
|
+
map.height ?? MINIMAP_H,
|
|
1628
|
+
'bottom-right',
|
|
1629
|
+
),
|
|
1630
|
+
),
|
|
1631
|
+
);
|
|
1632
|
+
}
|
|
1633
|
+
const buttons = this._controlButtons();
|
|
1634
|
+
if (buttons.length > 0) {
|
|
1635
|
+
const first = buttons[0].rect;
|
|
1636
|
+
const last = buttons[buttons.length - 1].rect;
|
|
1637
|
+
pin(this._device(unionRects(first, last)));
|
|
1638
|
+
}
|
|
1204
1639
|
}
|
|
1640
|
+
this.scrollContents(box, dx, dy, riders ? [riders] : null, pinned);
|
|
1641
|
+
this._blittedPanAt = now();
|
|
1205
1642
|
return true;
|
|
1206
1643
|
}
|
|
1207
1644
|
|
|
@@ -1245,6 +1682,27 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
1245
1682
|
|
|
1246
1683
|
// --- the imperative surface (`FlowInstance`) -----------------------------
|
|
1247
1684
|
|
|
1685
|
+
/**
|
|
1686
|
+
* `fn`, as a call through the handle. A programmatic pan — an animation
|
|
1687
|
+
* loop stepping `setViewport` — moves the pane at once and the bodies'
|
|
1688
|
+
* box through React, and on React's schedule the box caught up with the
|
|
1689
|
+
* pane in jumps: several steps' worth in one commit, landing in a frame
|
|
1690
|
+
* whose blit shifted by one. A rider that moves by more than the blit is
|
|
1691
|
+
* a layout move, so that frame repainted the pane whole — a fifth of the
|
|
1692
|
+
* frames of a pan over mounted bodies (`_blitPan`). Marked like a
|
|
1693
|
+
* gesture's, the emission commits before the call returns, and the box
|
|
1694
|
+
* moves by exactly the pan in the frame that blits it.
|
|
1695
|
+
*/
|
|
1696
|
+
fromHandle<T>(fn: () => T): T {
|
|
1697
|
+
const was = this._handleSync;
|
|
1698
|
+
this._handleSync = true;
|
|
1699
|
+
try {
|
|
1700
|
+
return fn();
|
|
1701
|
+
} finally {
|
|
1702
|
+
this._handleSync = was;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1248
1706
|
getViewport(): Viewport {
|
|
1249
1707
|
return { ...this._viewport() };
|
|
1250
1708
|
}
|
|
@@ -1386,10 +1844,11 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
1386
1844
|
/** The topmost node under a window point, or null. Walks paint order
|
|
1387
1845
|
* backwards, which is what "topmost" means. */
|
|
1388
1846
|
private _nodeAt(x: number, y: number): NodeEntry | null {
|
|
1847
|
+
const sv = this._screenViewport();
|
|
1389
1848
|
for (let i = this._order.length - 1; i >= 0; i--) {
|
|
1390
1849
|
const entry = this._order[i];
|
|
1391
1850
|
if (entry.node.hidden) continue;
|
|
1392
|
-
if (rectContains(this._screenRect(entry), { x, y })) return entry;
|
|
1851
|
+
if (rectContains(this._screenRect(entry, sv), { x, y })) return entry;
|
|
1393
1852
|
}
|
|
1394
1853
|
return null;
|
|
1395
1854
|
}
|
|
@@ -1401,13 +1860,14 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
1401
1860
|
): HandleAnchor | null {
|
|
1402
1861
|
const zoom = this._viewport().zoom;
|
|
1403
1862
|
const reach = Math.max(7, (HANDLE_RADIUS + HANDLE_SLOP) * zoom);
|
|
1863
|
+
const sv = this._screenViewport();
|
|
1404
1864
|
for (let i = this._order.length - 1; i >= 0; i--) {
|
|
1405
1865
|
const entry = this._order[i];
|
|
1406
1866
|
if (entry.node.hidden) continue;
|
|
1407
1867
|
if (!this._connectable(entry)) continue;
|
|
1408
1868
|
// cheap reject: the handles are on the node's border, so nothing more
|
|
1409
1869
|
// than `reach` outside its box can be one
|
|
1410
|
-
const rect = this._screenRect(entry);
|
|
1870
|
+
const rect = this._screenRect(entry, sv);
|
|
1411
1871
|
if (
|
|
1412
1872
|
x < rect.x - reach ||
|
|
1413
1873
|
x > rect.x + rect.width + reach ||
|
|
@@ -1437,12 +1897,13 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
1437
1897
|
): { entry: NodeEntry; dir: XYPosition } | null {
|
|
1438
1898
|
const zoom = this._viewport().zoom;
|
|
1439
1899
|
const reach = Math.max(6, (RESIZE_GRIP + RESIZE_SLOP) * zoom);
|
|
1900
|
+
const sv = this._screenViewport();
|
|
1440
1901
|
for (let i = this._order.length - 1; i >= 0; i--) {
|
|
1441
1902
|
const entry = this._order[i];
|
|
1442
1903
|
if (entry.node.hidden) continue;
|
|
1443
1904
|
const grips = this._grips(entry);
|
|
1444
1905
|
if (grips.length === 0) continue;
|
|
1445
|
-
const rect = this._screenRect(entry);
|
|
1906
|
+
const rect = this._screenRect(entry, sv);
|
|
1446
1907
|
if (
|
|
1447
1908
|
x < rect.x - reach ||
|
|
1448
1909
|
x > rect.x + rect.width + reach ||
|
|
@@ -1487,38 +1948,30 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
1487
1948
|
* the square root of the gap (see `paths.ts`) — `8` where the shoulder
|
|
1488
1949
|
* uses `6.25`, so the bound is generous rather than tight.
|
|
1489
1950
|
*/
|
|
1951
|
+
/** The coarse box with the label's plate in it: what an edge that
|
|
1952
|
+
* changes has to claim, where its route alone would leave a plate's
|
|
1953
|
+
* ends. */
|
|
1954
|
+
private _edgeInkBox(edge: AnyEdge): FlowRect | null {
|
|
1955
|
+
const box = this._edgeCoarseBox(edge);
|
|
1956
|
+
return box && withLabelPlate(box, edge, this._viewport().zoom);
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1490
1959
|
private _edgeCoarseBox(edge: AnyEdge): FlowRect | null {
|
|
1491
1960
|
const source = this._byId.get(edge.source);
|
|
1492
1961
|
const target = this._byId.get(edge.target);
|
|
1493
1962
|
if (!source || !target) return null;
|
|
1494
1963
|
if (source.node.hidden || target.node.hidden) return null;
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
a.y + a.height / 2 - (b.y + b.height / 2),
|
|
1964
|
+
return edgeCoarseBox(
|
|
1965
|
+
this._screenViewport(),
|
|
1966
|
+
this._source(source),
|
|
1967
|
+
this._source(target),
|
|
1968
|
+
this._scale,
|
|
1501
1969
|
);
|
|
1502
|
-
const slack =
|
|
1503
|
-
Math.max(5, EDGE_SLOP * zoom) +
|
|
1504
|
-
Math.max(EDGE_STEP_OFFSET * 3 * zoom, 8 * Math.sqrt(zoom * span));
|
|
1505
|
-
const x = Math.min(a.x, b.x) - slack;
|
|
1506
|
-
const y = Math.min(a.y, b.y) - slack;
|
|
1507
|
-
return {
|
|
1508
|
-
x,
|
|
1509
|
-
y,
|
|
1510
|
-
width: Math.max(a.x + a.width, b.x + b.width) + slack - x,
|
|
1511
|
-
height: Math.max(a.y + a.height, b.y + b.height) + slack - y,
|
|
1512
|
-
};
|
|
1513
1970
|
}
|
|
1514
1971
|
|
|
1515
1972
|
/** The drawn handle, in screen pixels. Capped as well as floored: a dot
|
|
1516
1973
|
* that scaled all the way up would be a saucer at 2.5×, and the thing it
|
|
1517
1974
|
* marks — a point on the border — does not get bigger. */
|
|
1518
|
-
private _handleRadius(): number {
|
|
1519
|
-
return clamp(HANDLE_RADIUS * this._viewport().zoom, 2.5, 6);
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
1975
|
private _connectable(entry: NodeEntry): boolean {
|
|
1523
1976
|
return (
|
|
1524
1977
|
(entry.node.connectable ?? this._bool('nodesConnectable', true)) !== false
|
|
@@ -1541,25 +1994,12 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
1541
1994
|
type: 'source' | 'target',
|
|
1542
1995
|
towards: XYPosition | null,
|
|
1543
1996
|
): HandleAnchor | null {
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
const candidates = typed.length > 0 ? typed : anchors;
|
|
1551
|
-
if (candidates.length === 0) return null;
|
|
1552
|
-
if (candidates.length === 1 || !towards) return candidates[0];
|
|
1553
|
-
let best = candidates[0];
|
|
1554
|
-
let bestDistance = Infinity;
|
|
1555
|
-
for (const anchor of candidates) {
|
|
1556
|
-
const d = Math.hypot(anchor.x - towards.x, anchor.y - towards.y);
|
|
1557
|
-
if (d < bestDistance) {
|
|
1558
|
-
bestDistance = d;
|
|
1559
|
-
best = anchor;
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1562
|
-
return best;
|
|
1997
|
+
return endpoint(
|
|
1998
|
+
{ node: entry.node, rect: this.rectOf(entry), specs: entry.specs },
|
|
1999
|
+
handleId,
|
|
2000
|
+
type,
|
|
2001
|
+
towards,
|
|
2002
|
+
);
|
|
1563
2003
|
}
|
|
1564
2004
|
|
|
1565
2005
|
/** An edge's polyline, in **screen** pixels, plus the two ends it joins.
|
|
@@ -1568,46 +2008,16 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
1568
2008
|
private _edgeGeometry(
|
|
1569
2009
|
edge: AnyEdge,
|
|
1570
2010
|
): { points: XYPosition[]; from: HandleAnchor; to: HandleAnchor } | null {
|
|
1571
|
-
const
|
|
1572
|
-
const
|
|
1573
|
-
if (!
|
|
1574
|
-
if (
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
'source',
|
|
1581
|
-
targetCentre,
|
|
1582
|
-
);
|
|
1583
|
-
const to = this._endpoint(
|
|
1584
|
-
targetEntry,
|
|
1585
|
-
edge.targetHandle,
|
|
1586
|
-
'target',
|
|
1587
|
-
sourceCentre,
|
|
1588
|
-
);
|
|
1589
|
-
if (!from || !to) return null;
|
|
1590
|
-
|
|
1591
|
-
const zoom = this._viewport().zoom;
|
|
1592
|
-
const s = this._toScreen(from);
|
|
1593
|
-
const t = this._toScreen(to);
|
|
1594
|
-
const points = edgePath(
|
|
1595
|
-
edge.type,
|
|
1596
|
-
{ x: s.x, y: s.y, position: from.position },
|
|
1597
|
-
{ x: t.x, y: t.y, position: to.position },
|
|
1598
|
-
{
|
|
1599
|
-
stepOffset: EDGE_STEP_OFFSET * zoom,
|
|
1600
|
-
radius: 8 * zoom,
|
|
1601
|
-
scale: zoom,
|
|
1602
|
-
loop: edge.source === edge.target,
|
|
1603
|
-
},
|
|
2011
|
+
const source = this._byId.get(edge.source);
|
|
2012
|
+
const target = this._byId.get(edge.target);
|
|
2013
|
+
if (!source || !target) return null;
|
|
2014
|
+
if (source.node.hidden || target.node.hidden) return null;
|
|
2015
|
+
return edgeRoute(
|
|
2016
|
+
this._screenViewport(),
|
|
2017
|
+
edge,
|
|
2018
|
+
this._source(source),
|
|
2019
|
+
this._source(target),
|
|
1604
2020
|
);
|
|
1605
|
-
return { points, from, to };
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
private _centre(entry: NodeEntry): XYPosition {
|
|
1609
|
-
const r = this.rectOf(entry);
|
|
1610
|
-
return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
|
|
1611
2021
|
}
|
|
1612
2022
|
|
|
1613
2023
|
// --- pane furniture geometry --------------------------------------------
|
|
@@ -1692,21 +2102,43 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
1692
2102
|
// A minimap that covers the pane it summarises is worse than none.
|
|
1693
2103
|
return null;
|
|
1694
2104
|
}
|
|
1695
|
-
const rects: FlowRect[] = [];
|
|
1696
|
-
for (const entry of this._entries) {
|
|
1697
|
-
if (!entry.node.hidden) rects.push(this.rectOf(entry));
|
|
1698
|
-
}
|
|
1699
2105
|
// The viewport joins the bounds, so panning off the graph still shows
|
|
1700
|
-
// where you are rather than pinning the box to an edge.
|
|
2106
|
+
// where you are rather than pinning the box to an edge. Worked out with
|
|
2107
|
+
// no box made per node: a drag asks this twice a step and again for
|
|
2108
|
+
// the pass (`_miniMapMoves`), over every node of the graph.
|
|
1701
2109
|
const v = this._viewport();
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
2110
|
+
let minX = -v.x / v.zoom;
|
|
2111
|
+
let minY = -v.y / v.zoom;
|
|
2112
|
+
let maxX = minX + pane.width / v.zoom;
|
|
2113
|
+
let maxY = minY + pane.height / v.zoom;
|
|
2114
|
+
const resizing = this._resizeTo;
|
|
2115
|
+
for (const entry of this._entries) {
|
|
2116
|
+
const node = entry.node;
|
|
2117
|
+
if (node.hidden) continue;
|
|
2118
|
+
let x: number;
|
|
2119
|
+
let y: number;
|
|
2120
|
+
let w: number;
|
|
2121
|
+
let h: number;
|
|
2122
|
+
if (resizing && resizing.id === node.id) {
|
|
2123
|
+
({ x, y, width: w, height: h } = resizing.rect);
|
|
2124
|
+
} else {
|
|
2125
|
+
const p = this._positionOf(node);
|
|
2126
|
+
x = p.x;
|
|
2127
|
+
y = p.y;
|
|
2128
|
+
w = entry.width;
|
|
2129
|
+
h = entry.height;
|
|
2130
|
+
}
|
|
2131
|
+
if (x < minX) minX = x;
|
|
2132
|
+
if (y < minY) minY = y;
|
|
2133
|
+
if (x + w > maxX) maxX = x + w;
|
|
2134
|
+
if (y + h > maxY) maxY = y + h;
|
|
2135
|
+
}
|
|
2136
|
+
const bounds = {
|
|
2137
|
+
x: minX,
|
|
2138
|
+
y: minY,
|
|
2139
|
+
width: maxX - minX,
|
|
2140
|
+
height: maxY - minY,
|
|
2141
|
+
};
|
|
1710
2142
|
const pad = Math.max(bounds.width, bounds.height) * 0.05 + 10;
|
|
1711
2143
|
const padded = {
|
|
1712
2144
|
x: bounds.x - pad,
|
|
@@ -1737,20 +2169,6 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
1737
2169
|
};
|
|
1738
2170
|
}
|
|
1739
2171
|
|
|
1740
|
-
private _miniToScreen(
|
|
1741
|
-
map: { panel: FlowRect; bounds: FlowRect; scale: number },
|
|
1742
|
-
p: XYPosition,
|
|
1743
|
-
): XYPosition {
|
|
1744
|
-
const ox =
|
|
1745
|
-
map.panel.x + (map.panel.width - map.bounds.width * map.scale) / 2;
|
|
1746
|
-
const oy =
|
|
1747
|
-
map.panel.y + (map.panel.height - map.bounds.height * map.scale) / 2;
|
|
1748
|
-
return {
|
|
1749
|
-
x: ox + (p.x - map.bounds.x) * map.scale,
|
|
1750
|
-
y: oy + (p.y - map.bounds.y) * map.scale,
|
|
1751
|
-
};
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
2172
|
// --- gestures ------------------------------------------------------------
|
|
1755
2173
|
|
|
1756
2174
|
override defaultMouseDown(ev: MouseEvent): void {
|
|
@@ -1950,13 +2368,10 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
1950
2368
|
gesture.x = ev.x;
|
|
1951
2369
|
gesture.y = ev.y;
|
|
1952
2370
|
this._selectBox(gesture);
|
|
1953
|
-
// The box
|
|
1954
|
-
//
|
|
1955
|
-
//
|
|
1956
|
-
this.
|
|
1957
|
-
inflateRect(unionRects(oldBox, selectBoxRect(gesture)), CULL_MARGIN),
|
|
1958
|
-
'style-state',
|
|
1959
|
-
);
|
|
2371
|
+
// The box is all that moves this step; a node crossing the boundary
|
|
2372
|
+
// changes `selected`, and that repaints through the controlled round
|
|
2373
|
+
// trip like any other selection change.
|
|
2374
|
+
this._claimSelectStep(oldBox, selectBoxRect(gesture));
|
|
1960
2375
|
return;
|
|
1961
2376
|
}
|
|
1962
2377
|
case 'minimap': {
|
|
@@ -2104,6 +2519,33 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
2104
2519
|
);
|
|
2105
2520
|
}
|
|
2106
2521
|
|
|
2522
|
+
/**
|
|
2523
|
+
* The nodes a drag is moving, once it has moved them: lifted out of the
|
|
2524
|
+
* rest of the graph for the rest of the gesture. Under GL the world is
|
|
2525
|
+
* built without them and they are a layer of their own (`glFrame`), so
|
|
2526
|
+
* each step packs them and their edges and leaves the world on the GPU;
|
|
2527
|
+
* in 2D the rest is two pictures a step copies from (`_liftShotFor`).
|
|
2528
|
+
* Null otherwise — and under GL the change back is a change of the
|
|
2529
|
+
* world's key, which rebuilds it with them in.
|
|
2530
|
+
*/
|
|
2531
|
+
private _lifted(): ReadonlySet<string> | null {
|
|
2532
|
+
const g = this._gesture;
|
|
2533
|
+
if (g?.kind !== 'drag' || !g.moved || g.ids.length === 0) {
|
|
2534
|
+
if (this._liftSet) {
|
|
2535
|
+
this._liftSet = null;
|
|
2536
|
+
this._liftFor = null;
|
|
2537
|
+
this._liftGen++;
|
|
2538
|
+
}
|
|
2539
|
+
return null;
|
|
2540
|
+
}
|
|
2541
|
+
if (this._liftFor !== g) {
|
|
2542
|
+
this._liftSet = new Set(g.ids);
|
|
2543
|
+
this._liftFor = g;
|
|
2544
|
+
this._liftGen++;
|
|
2545
|
+
}
|
|
2546
|
+
return this._liftSet;
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2107
2549
|
private _dragStep(
|
|
2108
2550
|
gesture: Extract<Gesture, { kind: 'drag' }>,
|
|
2109
2551
|
ev: MouseEvent,
|
|
@@ -2151,22 +2593,38 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
2151
2593
|
}
|
|
2152
2594
|
// Old places first, new places second: the union is everything this
|
|
2153
2595
|
// step uncovers plus everything it covers, and nothing else — with the
|
|
2154
|
-
// edges that follow the moved nodes, coarse, on both sides.
|
|
2596
|
+
// edges that follow the moved nodes, coarse, on both sides. Under GL
|
|
2597
|
+
// there is nothing to size: the step is a frame of the surface's, and
|
|
2598
|
+
// routing each moved node's edges for a rect nobody reads was a third
|
|
2599
|
+
// of a millisecond a step.
|
|
2600
|
+
const sized = !this._gl;
|
|
2155
2601
|
let damage: FlowRect | null = null;
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2602
|
+
// …and in the minimap, the boxes the step moves them from
|
|
2603
|
+
const maps = sized && this._drawsMiniMap();
|
|
2604
|
+
const moves: MiniMapMove[] = [];
|
|
2605
|
+
if (sized) {
|
|
2606
|
+
for (const id of gesture.ids) {
|
|
2607
|
+
const entry = this._byId.get(id);
|
|
2608
|
+
if (!entry) continue;
|
|
2609
|
+
damage = unionMaybe(damage, this._nodeDamage(entry));
|
|
2610
|
+
if (maps) moves.push({ entry, before: this.rectOf(entry) });
|
|
2611
|
+
}
|
|
2159
2612
|
}
|
|
2160
2613
|
this._dragTo = to;
|
|
2161
|
-
|
|
2162
|
-
const
|
|
2163
|
-
|
|
2614
|
+
if (sized) {
|
|
2615
|
+
for (const id of gesture.ids) {
|
|
2616
|
+
const entry = this._byId.get(id);
|
|
2617
|
+
if (entry) damage = unionMaybe(damage, this._nodeDamage(entry));
|
|
2618
|
+
}
|
|
2164
2619
|
}
|
|
2165
2620
|
this._emitNodes(changes);
|
|
2621
|
+
const reason = this._lifted() ? 'lift' : 'content';
|
|
2166
2622
|
if (damage) {
|
|
2167
|
-
this._claim(inflateRect(damage, CULL_MARGIN),
|
|
2623
|
+
this._claim(inflateRect(damage, CULL_MARGIN), reason);
|
|
2624
|
+
if (maps) this._claimMiniMapMoves(moves, reason);
|
|
2168
2625
|
} else {
|
|
2169
|
-
|
|
2626
|
+
// under GL: a frame, and — while the nodes are lifted — of them alone
|
|
2627
|
+
this._repaint(reason);
|
|
2170
2628
|
}
|
|
2171
2629
|
// Inside the gesture dispatch, deliberately: the body-rect setState this
|
|
2172
2630
|
// triggers is a discrete-priority update, so React commits the moved
|
|
@@ -2203,6 +2661,8 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
2203
2661
|
},
|
|
2204
2662
|
snap,
|
|
2205
2663
|
);
|
|
2664
|
+
const maps = this._drawsMiniMap();
|
|
2665
|
+
const before = this.rectOf(entry);
|
|
2206
2666
|
let damage = this._nodeDamage(entry);
|
|
2207
2667
|
this._resizeTo = { id: gesture.id, rect };
|
|
2208
2668
|
damage = unionRects(damage, this._nodeDamage(entry));
|
|
@@ -2224,6 +2684,7 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
2224
2684
|
}
|
|
2225
2685
|
this._emitNodes(changes);
|
|
2226
2686
|
this._claim(inflateRect(damage, CULL_MARGIN), 'content');
|
|
2687
|
+
if (maps) this._claimMiniMapMoves([{ entry, before }], 'content');
|
|
2227
2688
|
this._emitBodies();
|
|
2228
2689
|
}
|
|
2229
2690
|
|
|
@@ -2295,29 +2756,7 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
2295
2756
|
private _connectionPath(
|
|
2296
2757
|
gesture: Extract<Gesture, { kind: 'connect' }>,
|
|
2297
2758
|
): XYPosition[] {
|
|
2298
|
-
|
|
2299
|
-
const from = this._toScreen(gesture.from);
|
|
2300
|
-
const target = gesture.to ?? gesture.pointer;
|
|
2301
|
-
const to = this._toScreen(target);
|
|
2302
|
-
const toPosition =
|
|
2303
|
-
gesture.to?.position ??
|
|
2304
|
-
(gesture.from.position === 'left'
|
|
2305
|
-
? 'right'
|
|
2306
|
-
: gesture.from.position === 'right'
|
|
2307
|
-
? 'left'
|
|
2308
|
-
: gesture.from.position === 'top'
|
|
2309
|
-
? 'bottom'
|
|
2310
|
-
: 'top');
|
|
2311
|
-
return edgePath(
|
|
2312
|
-
'bezier',
|
|
2313
|
-
{ x: from.x, y: from.y, position: gesture.from.position },
|
|
2314
|
-
{ x: to.x, y: to.y, position: toPosition },
|
|
2315
|
-
{
|
|
2316
|
-
stepOffset: EDGE_STEP_OFFSET * v.zoom,
|
|
2317
|
-
radius: 8 * v.zoom,
|
|
2318
|
-
scale: v.zoom,
|
|
2319
|
-
},
|
|
2320
|
-
);
|
|
2759
|
+
return connectionPath(this._screenViewport(), gesture);
|
|
2321
2760
|
}
|
|
2322
2761
|
|
|
2323
2762
|
private _validConnection(connection: {
|
|
@@ -2330,6 +2769,76 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
2330
2769
|
return check ? check(connection) !== false : true;
|
|
2331
2770
|
}
|
|
2332
2771
|
|
|
2772
|
+
/**
|
|
2773
|
+
* What one step of a box selection changes on screen: a band round each
|
|
2774
|
+
* side of the box that moved, across the whole of both boxes. That covers
|
|
2775
|
+
* the outline where it was and where it is, and the tint where one box
|
|
2776
|
+
* lies over the graph and the other does not; a side that held still —
|
|
2777
|
+
* the two the gesture is anchored by — changed only where a moving side
|
|
2778
|
+
* shortened or lengthened it, which that side's band covers. Claiming the
|
|
2779
|
+
* box round both repainted everything under the selection, every node and
|
|
2780
|
+
* edge in it, on every step: 21 ms of a step on the stress example's
|
|
2781
|
+
* fan-out, which is what made a sweep lag the pointer.
|
|
2782
|
+
*
|
|
2783
|
+
* The bands are cut on the device grid so that none of them overlaps
|
|
2784
|
+
* another: core merges overlapping damage into the box round it, which is
|
|
2785
|
+
* the whole selection again — and a pass over a translucent tint must not
|
|
2786
|
+
* paint it twice. Claimed past this element's own `invalidate`, whose one
|
|
2787
|
+
* job here would be to repaint the minimap and the controls on their
|
|
2788
|
+
* canvases, and a box selection moves nothing on either.
|
|
2789
|
+
*
|
|
2790
|
+
* Under GL the box is the overlay's, drawn every frame: a step is a frame
|
|
2791
|
+
* and not a change to the world, which a claim would have rebuilt.
|
|
2792
|
+
*/
|
|
2793
|
+
private _claimSelectStep(from: FlowRect, to: FlowRect): void {
|
|
2794
|
+
if (this._gl) {
|
|
2795
|
+
this._glRequest!();
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2798
|
+
const s = this._scale;
|
|
2799
|
+
const lo = (v: number): number => Math.floor(toDevice(v - SELECT_BAND, s));
|
|
2800
|
+
const hi = (v: number): number => Math.ceil(toDevice(v + SELECT_BAND, s));
|
|
2801
|
+
/** The bands round the sides that moved along one axis, merged where
|
|
2802
|
+
* they meet, in device pixels. */
|
|
2803
|
+
const bands = (
|
|
2804
|
+
a0: number,
|
|
2805
|
+
a1: number,
|
|
2806
|
+
b0: number,
|
|
2807
|
+
b1: number,
|
|
2808
|
+
): [number, number][] => {
|
|
2809
|
+
const out: [number, number][] = [];
|
|
2810
|
+
if (a0 !== b0) out.push([lo(Math.min(a0, b0)), hi(Math.max(a0, b0))]);
|
|
2811
|
+
if (a1 !== b1) out.push([lo(Math.min(a1, b1)), hi(Math.max(a1, b1))]);
|
|
2812
|
+
out.sort((m, n) => m[0] - n[0]);
|
|
2813
|
+
if (out.length === 2 && out[1][0] <= out[0][1]) {
|
|
2814
|
+
return [[out[0][0], Math.max(out[0][1], out[1][1])]];
|
|
2815
|
+
}
|
|
2816
|
+
return out;
|
|
2817
|
+
};
|
|
2818
|
+
const outer = unionRects(from, to);
|
|
2819
|
+
const x0 = lo(outer.x);
|
|
2820
|
+
const x1 = hi(outer.x + outer.width);
|
|
2821
|
+
const y0 = lo(outer.y);
|
|
2822
|
+
const y1 = hi(outer.y + outer.height);
|
|
2823
|
+
const columns = bands(from.x, from.x + from.width, to.x, to.x + to.width);
|
|
2824
|
+
const rows = bands(from.y, from.y + from.height, to.y, to.y + to.height);
|
|
2825
|
+
const claim = (x: number, y: number, width: number, height: number) => {
|
|
2826
|
+
if (width > 0 && height > 0) {
|
|
2827
|
+
super.invalidate(false, { x, y, width, height }, 'style-state');
|
|
2828
|
+
}
|
|
2829
|
+
};
|
|
2830
|
+
for (const [c0, c1] of columns) claim(c0, y0, c1 - c0, y1 - y0);
|
|
2831
|
+
for (const [r0, r1] of rows) {
|
|
2832
|
+
// across both boxes, less the columns already claimed
|
|
2833
|
+
let x = x0;
|
|
2834
|
+
for (const [c0, c1] of columns) {
|
|
2835
|
+
claim(x, r0, c0 - x, r1 - r0);
|
|
2836
|
+
x = Math.max(x, c1);
|
|
2837
|
+
}
|
|
2838
|
+
claim(x, r0, x1 - x, r1 - r0);
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2333
2842
|
private _selectBox(gesture: Extract<Gesture, { kind: 'select' }>): void {
|
|
2334
2843
|
const a = this._toGraph(gesture.startX, gesture.startY);
|
|
2335
2844
|
const b = this._toGraph(gesture.x, gesture.y);
|
|
@@ -2463,10 +2972,11 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
2463
2972
|
override a11yScene(): A11ySceneItem[] {
|
|
2464
2973
|
this._sync();
|
|
2465
2974
|
const pane = this._pane();
|
|
2975
|
+
const sv = screenViewport(this._viewport(), pane);
|
|
2466
2976
|
const items: A11ySceneItem[] = [];
|
|
2467
2977
|
for (const entry of this._order) {
|
|
2468
2978
|
if (entry.node.hidden) continue;
|
|
2469
|
-
const rect = this._screenRect(entry);
|
|
2979
|
+
const rect = this._screenRect(entry, sv);
|
|
2470
2980
|
if (!rectsOverlap(rect, pane)) continue;
|
|
2471
2981
|
const data = entry.node.data as FlowNodeData | undefined;
|
|
2472
2982
|
const out = this._edgesByNode.get(entry.node.id) ?? [];
|
|
@@ -2646,7 +3156,12 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
2646
3156
|
// changed — a moved node repaints the box it moved through, an edit to
|
|
2647
3157
|
// anything else on the visual list repaints in full, and handler churn
|
|
2648
3158
|
// repaints nothing.
|
|
2649
|
-
|
|
3159
|
+
// Boxes rather than the box around them: the nodes a box selection
|
|
3160
|
+
// takes in one step lie along both of its moving edges, and the box
|
|
3161
|
+
// round an L of nodes is the whole selection again.
|
|
3162
|
+
let damage: 'full' | FlowRect[] = [];
|
|
3163
|
+
let edgesRedrawn = false;
|
|
3164
|
+
this._liftOnly = false;
|
|
2650
3165
|
if (
|
|
2651
3166
|
!shallowEqual(nextProps.nodeTypes, before.nodeTypes) ||
|
|
2652
3167
|
!shallowEqual(nextProps.defaultEdgeOptions, before.defaultEdgeOptions)
|
|
@@ -2655,16 +3170,17 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
2655
3170
|
damage = 'full';
|
|
2656
3171
|
} else {
|
|
2657
3172
|
if (nextProps.edges !== before.edges) {
|
|
2658
|
-
|
|
3173
|
+
const edgeDamage = this._applyEdges(this._rawEdges());
|
|
3174
|
+
if (edgeDamage === 'full') damage = 'full';
|
|
3175
|
+
else if (edgeDamage) {
|
|
3176
|
+
damage.push(edgeDamage);
|
|
3177
|
+
edgesRedrawn = true;
|
|
3178
|
+
}
|
|
2659
3179
|
}
|
|
2660
3180
|
if (damage !== 'full' && nextProps.nodes !== before.nodes) {
|
|
2661
3181
|
const nodeDamage = this._applyNodes(this._nodes());
|
|
2662
|
-
damage =
|
|
2663
|
-
|
|
2664
|
-
? nodeDamage
|
|
2665
|
-
: nodeDamage === null
|
|
2666
|
-
? damage
|
|
2667
|
-
: unionRects(damage, nodeDamage);
|
|
3182
|
+
if (nodeDamage === 'full') damage = 'full';
|
|
3183
|
+
else damage.push(...nodeDamage);
|
|
2668
3184
|
}
|
|
2669
3185
|
}
|
|
2670
3186
|
// `fitView` is a one-shot: turning it on later refits, and it stays off
|
|
@@ -2681,17 +3197,34 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
2681
3197
|
}
|
|
2682
3198
|
}
|
|
2683
3199
|
}
|
|
3200
|
+
// What moved, restyled or reshaped is the graph's own content: under GL
|
|
3201
|
+
// it is a frame of the surface's, where a claim billed as `props` went
|
|
3202
|
+
// on to a window pass over the 2D pane under it — every step of a drag
|
|
3203
|
+
// an app stores, 1.2 ms of each, for pixels the surface covers.
|
|
2684
3204
|
if (damage === 'full') this._repaint('props');
|
|
2685
|
-
else
|
|
3205
|
+
else {
|
|
3206
|
+
// a commit of lifted nodes alone is a frame of the lifted layer
|
|
3207
|
+
const reason = this._liftOnly && !edgesRedrawn ? 'lift' : 'content';
|
|
3208
|
+
for (const box of damage) this._claim(box, reason);
|
|
3209
|
+
}
|
|
3210
|
+
this._liftOnly = false;
|
|
2686
3211
|
// Core re-reads the scene for aria-prop commits; a `nodes`/`edges`
|
|
2687
3212
|
// change is invisible to it, so the re-read is asked for by name.
|
|
2688
3213
|
// Free when no assistive technology is listening.
|
|
2689
|
-
if (damage) this.notifyA11ySceneChanged();
|
|
3214
|
+
if (damage === 'full' || damage.length > 0) this.notifyA11ySceneChanged();
|
|
2690
3215
|
}
|
|
2691
3216
|
|
|
2692
3217
|
override destroySubtree(): void {
|
|
3218
|
+
this._dropZoomShot();
|
|
3219
|
+
this._dropLiftShot();
|
|
3220
|
+
this._glRequest = null;
|
|
3221
|
+
if (this._bodiesRest != null) timers.clearTimeout?.(this._bodiesRest);
|
|
3222
|
+
this._bodiesRest = null;
|
|
3223
|
+
if (this._zoomRest != null) timers.clearTimeout?.(this._zoomRest);
|
|
3224
|
+
this._zoomRest = null;
|
|
2693
3225
|
this._stopAnimation();
|
|
2694
3226
|
this._dropGridTile();
|
|
3227
|
+
this._sceneCache.clear();
|
|
2695
3228
|
super.destroySubtree();
|
|
2696
3229
|
}
|
|
2697
3230
|
|
|
@@ -2699,14 +3232,59 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
2699
3232
|
if (this._animTimer != null) return;
|
|
2700
3233
|
this._animTimer =
|
|
2701
3234
|
timers.setInterval?.(() => {
|
|
2702
|
-
|
|
3235
|
+
// A pan that blits copies the pane's pixels, dashes and all, and a
|
|
3236
|
+
// tick claims the dashes inside the band it copies — which declines
|
|
3237
|
+
// the copy: every frame a tick landed in repainted the whole pane,
|
|
3238
|
+
// a sixth of a pan's frames and all of its stutter. So the dashes
|
|
3239
|
+
// sit the pan out, phase and all, and what the pan copies and what
|
|
3240
|
+
// it draws agree; they march again once it has held still.
|
|
3241
|
+
//
|
|
3242
|
+
// And the view moving at all is what they wait out, not a blit that
|
|
3243
|
+
// happened: a tick declines the blit it lands beside, so a pan
|
|
3244
|
+
// whose frames ran past the wait had its ticks back, each one
|
|
3245
|
+
// cancelling the next blit and repainting the pane — over the
|
|
3246
|
+
// stress example's widgets in a large window, 2 frames a second,
|
|
3247
|
+
// ticks and pan steps taking turns. A step since the last tick, or
|
|
3248
|
+
// one within two ticks, holds the dashes still whatever a frame
|
|
3249
|
+
// costs. Under GL a tick is a uniform and blits nothing, and they
|
|
3250
|
+
// march through a pan as before.
|
|
3251
|
+
//
|
|
3252
|
+
// A 2D tick is a repaint of the box the dashes are in, which over a
|
|
3253
|
+
// dense graph in a large window is most of the pane: 75 ms a tick on
|
|
3254
|
+
// XQuartz, against a timer of 60. So the wait scales with what
|
|
3255
|
+
// frames cost: the view held still for two frames' worth, and one
|
|
3256
|
+
// tick every two frames at most — the dashes slow down before the
|
|
3257
|
+
// thread is theirs, and a pan whose frames come slowly still sees
|
|
3258
|
+
// them hold still.
|
|
3259
|
+
const t = now();
|
|
3260
|
+
const lastTick = this._tickAt;
|
|
3261
|
+
this._tickAt = t;
|
|
3262
|
+
if (t - this._blittedPanAt < ANIMATION_MS * 2) return;
|
|
3263
|
+
let steps = 1;
|
|
3264
|
+
if (!this._gl) {
|
|
3265
|
+
const cost = this._tickCost * 2;
|
|
3266
|
+
const still = Math.max(ANIMATION_MS * 2, cost);
|
|
3267
|
+
if (this._viewMovedAt > lastTick || t - this._viewMovedAt < still) {
|
|
3268
|
+
return;
|
|
3269
|
+
}
|
|
3270
|
+
// a tick under ANIMATION_MS apart, less a millisecond of timer
|
|
3271
|
+
// slop; further while ticks are dear — and then the dashes cover
|
|
3272
|
+
// the ground the skipped ticks would have, so they slow down in
|
|
3273
|
+
// steps and not in speed
|
|
3274
|
+
const since = t - this._marchedAt;
|
|
3275
|
+
if (since < Math.max(ANIMATION_MS, cost) - 1) return;
|
|
3276
|
+
steps = Math.min(4, Math.max(1, Math.round(since / ANIMATION_MS)));
|
|
3277
|
+
}
|
|
3278
|
+
this._marchedAt = t;
|
|
3279
|
+
this._dashPhase += ANIMATION_SPEED * steps;
|
|
2703
3280
|
// the box the last paint saw animated edges in, not the pane: a
|
|
2704
|
-
// marching dash should not cost a full grid repaint per tick
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
);
|
|
3281
|
+
// marching dash should not cost a full grid repaint per tick. And
|
|
3282
|
+
// only the part of it the pane shows — an edge on its way out of
|
|
3283
|
+
// the pane takes the box past it, and claimed whole, a tick
|
|
3284
|
+
// repainted everything the window has beside the graph.
|
|
3285
|
+
const box = this._animBox ? this._device(this._animBox) : this.abs;
|
|
3286
|
+
const shown = intersectRects(box, this.contentBox());
|
|
3287
|
+
if (shown) this.invalidate(false, shown, 'animation');
|
|
2710
3288
|
}, ANIMATION_MS) ?? null;
|
|
2711
3289
|
}
|
|
2712
3290
|
|
|
@@ -2719,6 +3297,9 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
2719
3297
|
// --- painting ------------------------------------------------------------
|
|
2720
3298
|
|
|
2721
3299
|
override paint(ctx: Context2D): void {
|
|
3300
|
+
this._frameTick();
|
|
3301
|
+
const paintStart = now();
|
|
3302
|
+
|
|
2722
3303
|
// What this pass is repainting. The renderer paints each damage rect as
|
|
2723
3304
|
// its own clipped pass; content outside it survives on the window, so
|
|
2724
3305
|
// everything we skip here is content the last frame already drew — the
|
|
@@ -2745,8 +3326,35 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
2745
3326
|
if (!insideInner) super.paint(ctx);
|
|
2746
3327
|
|
|
2747
3328
|
if (!this._visible()) return;
|
|
2748
|
-
|
|
3329
|
+
if (this._gl) {
|
|
3330
|
+
// The surface over this box draws the graph. What the 2D pass still
|
|
3331
|
+
// owes is the part of a paint that is not drawing — the derived graph
|
|
3332
|
+
// brought up to date, the bodies placed — and then the frame itself,
|
|
3333
|
+
// asked of the surface. Every claim this element makes arrives here,
|
|
3334
|
+
// so every change the 2D renderer would repaint is one GL frame.
|
|
3335
|
+
this._sync();
|
|
3336
|
+
this._emitBodies();
|
|
3337
|
+
this._emitPanels();
|
|
3338
|
+
this._glRequest?.();
|
|
3339
|
+
return;
|
|
3340
|
+
}
|
|
3341
|
+
// Mid-gesture, labels come from the layouts they already have, scaled:
|
|
3342
|
+
// shaping every one again at each step of a zoom was half of what a 2D
|
|
3343
|
+
// step cost, at sizes the next step moves off. Only where the context
|
|
3344
|
+
// scales text with its transform — X11's draws glyphs at the size they
|
|
3345
|
+
// were shaped at — and set exactly once the zoom rests (`_restZoom`).
|
|
3346
|
+
const approximateText =
|
|
3347
|
+
this._zoomMoving() &&
|
|
3348
|
+
(ctx as { scalesText?: boolean }).scalesText === true;
|
|
3349
|
+
const painter = createPainter(ctx, {
|
|
3350
|
+
...this._textOptions(),
|
|
3351
|
+
approximateText,
|
|
3352
|
+
});
|
|
2749
3353
|
if (!painter) return; // a backend with no path API: geometry only
|
|
3354
|
+
if (approximateText) {
|
|
3355
|
+
this._textApproximated = true;
|
|
3356
|
+
this._restZoom();
|
|
3357
|
+
}
|
|
2750
3358
|
|
|
2751
3359
|
this._sync();
|
|
2752
3360
|
this._painting = true;
|
|
@@ -2808,34 +3416,120 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
2808
3416
|
),
|
|
2809
3417
|
));
|
|
2810
3418
|
painter.clipRect(x, y, width, height, nearCorner ? radius : 0);
|
|
2811
|
-
|
|
2812
|
-
//
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
fill:
|
|
2821
|
-
(this.style.backgroundColor as string | undefined) ??
|
|
2822
|
-
palette.background,
|
|
2823
|
-
});
|
|
2824
|
-
this._paintGrid(painter, palette, region);
|
|
3419
|
+
const started = now();
|
|
3420
|
+
// Not under a drag or a connection: the picture would hold the moving
|
|
3421
|
+
// node, or the line, where they were
|
|
3422
|
+
// …and only for a graph that costs more than a frame to paint live
|
|
3423
|
+
// (`ZOOM_SHOT_WORTH_MS`, `ZOOM_SHOT_DROP_MS`)
|
|
3424
|
+
const zooming = this._zoomMoving() && !this._gesture;
|
|
3425
|
+
const paintMs = zooming ? this._graphPaintMs() : 0;
|
|
3426
|
+
if (zooming && this._zoomShot !== null && paintMs < ZOOM_SHOT_DROP_MS) {
|
|
3427
|
+
this._dropZoomShot();
|
|
2825
3428
|
}
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
this.
|
|
3429
|
+
const shot =
|
|
3430
|
+
zooming && (this._zoomShot !== null || paintMs > ZOOM_SHOT_WORTH_MS)
|
|
3431
|
+
? this._zoomShotFor(palette, approximateText)
|
|
3432
|
+
: null;
|
|
3433
|
+
// A drag's nodes over pictures of the rest (`_liftShotFor`), in 2D and
|
|
3434
|
+
// not mid-zoom.
|
|
3435
|
+
const lift = shot || approximateText ? null : this._lifted();
|
|
3436
|
+
const liftShot = lift ? this._liftShotFor(palette, lift) : null;
|
|
3437
|
+
if (!lift && this._liftShot) this._dropLiftShot();
|
|
3438
|
+
let scene: FlowScene;
|
|
3439
|
+
if (liftShot && lift) {
|
|
3440
|
+
// the pictures, with the moved nodes' edges between them and the
|
|
3441
|
+
// nodes over both; the pane's furniture over it all
|
|
3442
|
+
const lifted = buildScene(
|
|
3443
|
+
this._sceneInput(palette, { lift: { ids: lift, only: true } }),
|
|
3444
|
+
);
|
|
3445
|
+
scene = buildScene(this._sceneInput(palette, 'overlay'));
|
|
3446
|
+
const built = now();
|
|
3447
|
+
this._compositeLift(ctx, liftShot, liftShot.under, null);
|
|
3448
|
+
paintGraphEdges(painter, lifted);
|
|
3449
|
+
this._compositeLift(ctx, liftShot, liftShot.over, liftShot.cards);
|
|
3450
|
+
paintGraphNodes(painter, lifted);
|
|
3451
|
+
paintFloat(painter, scene);
|
|
3452
|
+
this._reportFrame(built - started, now() - built);
|
|
3453
|
+
} else if (shot) {
|
|
3454
|
+
// the graph and the ground under it as the gesture drew them, scaled;
|
|
3455
|
+
// both live wherever the picture does not reach; and the pane's
|
|
3456
|
+
// furniture over them at the zoom of the moment
|
|
3457
|
+
scene = buildScene(this._sceneInput(palette, 'overlay'));
|
|
3458
|
+
const bands = this._uncovered(shot);
|
|
3459
|
+
const input = bands.length > 0 ? this._sceneInput(palette) : null;
|
|
3460
|
+
const live = input
|
|
3461
|
+
? bands.map((band) => ({
|
|
3462
|
+
band,
|
|
3463
|
+
scene: buildScene({ ...input, clip: band }),
|
|
3464
|
+
}))
|
|
3465
|
+
: [];
|
|
3466
|
+
const built = now();
|
|
3467
|
+
this._compositeShot(ctx, shot);
|
|
3468
|
+
for (const { band, scene: part } of live) {
|
|
3469
|
+
painter.save();
|
|
3470
|
+
painter.clipRect(band.x, band.y, band.width, band.height, 0);
|
|
3471
|
+
paintGround(painter, part, this._grid);
|
|
3472
|
+
paintGraph(painter, part);
|
|
3473
|
+
painter.restore();
|
|
3474
|
+
}
|
|
3475
|
+
paintFloat(painter, scene);
|
|
3476
|
+
this._textApproximated = true;
|
|
3477
|
+
this._restZoom();
|
|
3478
|
+
this._reportFrame(built - started, now() - built);
|
|
3479
|
+
} else {
|
|
3480
|
+
// Only a pass over the whole pane may change how edges are stroked;
|
|
3481
|
+
// one over part of it follows what the rest was drawn with, and asks
|
|
3482
|
+
// for the whole when the edges on the pane have crossed over
|
|
3483
|
+
// (`EDGE_BATCH`) — so a pan's copy never meets a strip drawn the
|
|
3484
|
+
// other way.
|
|
3485
|
+
const input = this._sceneInput(palette);
|
|
3486
|
+
const whole = this._coversPane(clip);
|
|
3487
|
+
scene = buildScene(
|
|
3488
|
+
whole ? { ...input, batch: undefined, batchWas: this._batch } : input,
|
|
3489
|
+
);
|
|
3490
|
+
if (whole) this._batch = scene.batch;
|
|
3491
|
+
else if (batchesEdges(scene.edgesOnPane, this._batch) !== this._batch) {
|
|
3492
|
+
this._repaint('content');
|
|
3493
|
+
}
|
|
3494
|
+
const built = now();
|
|
3495
|
+
paintScene(painter, scene, this._grid);
|
|
3496
|
+
const done = now();
|
|
3497
|
+
this._reportFrame(built - started, done - built);
|
|
3498
|
+
// a zoom's step, whole and live: what the next one would cost
|
|
3499
|
+
if (this._zoomMoving() && this._wholePane(clip)) {
|
|
3500
|
+
this._noteGraphPaint(done - started);
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
// The box the dash ticks invalidate comes off the *drawn* geometry; a
|
|
3504
|
+
// pass that culled every animated edge keeps the one before it, because
|
|
3505
|
+
// the endpoints did not move, or that move's own damage would have
|
|
3506
|
+
// redrawn them here.
|
|
3507
|
+
if (scene.animBox) this._animBox = scene.animBox;
|
|
2833
3508
|
painter.restore();
|
|
2834
3509
|
this._painting = false;
|
|
2835
3510
|
this._frameClip = null;
|
|
3511
|
+
{
|
|
3512
|
+
// What a frame costs, for the dash timer (`_startAnimation`): this
|
|
3513
|
+
// paint, and how long the server took to answer the frame before it
|
|
3514
|
+
// — on XQuartz most of it.
|
|
3515
|
+
const latency = (
|
|
3516
|
+
this.root as { window?: { frameLatency?: number } } | null
|
|
3517
|
+
)?.window?.frameLatency;
|
|
3518
|
+
// A reply can take seconds on a server that is waiting for a
|
|
3519
|
+
// display — a Mac's asleep — and that is when ticks most need to
|
|
3520
|
+
// hold; clamped rather than dropped, so one outlier does not stand
|
|
3521
|
+
// for long in the mean.
|
|
3522
|
+
const cost =
|
|
3523
|
+
now() -
|
|
3524
|
+
paintStart +
|
|
3525
|
+
(typeof latency === 'number' && latency > 0
|
|
3526
|
+
? Math.min(latency, 2000)
|
|
3527
|
+
: 0);
|
|
3528
|
+
this._tickCost = this._tickCost * 0.7 + cost * 0.3;
|
|
3529
|
+
}
|
|
2836
3530
|
|
|
2837
3531
|
// The timer exists only while something on screen needs it.
|
|
2838
|
-
if (animated) this._startAnimation();
|
|
3532
|
+
if (scene.animated) this._startAnimation();
|
|
2839
3533
|
else this._stopAnimation();
|
|
2840
3534
|
|
|
2841
3535
|
if (!this._sceneAnnounced) {
|
|
@@ -2849,163 +3543,1381 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
2849
3543
|
// to put the node bodies it mounts, and it is answered from the geometry
|
|
2850
3544
|
// this frame just used — the same numbers, never a second derivation.
|
|
2851
3545
|
this._emitBodies();
|
|
3546
|
+
this._emitPanels();
|
|
3547
|
+
// a pass of the frame that drew the latest zoom step ends here
|
|
3548
|
+
if (this._zoomFrameAt === this._lastFrameAt) this._zoomDrawnAt = now();
|
|
3549
|
+
}
|
|
3550
|
+
|
|
3551
|
+
/**
|
|
3552
|
+
* Everything `buildScene` reads, gathered from the element once.
|
|
3553
|
+
*
|
|
3554
|
+
* The two node lists differ on purpose: `nodes` is the paint order with a
|
|
3555
|
+
* dragged node lifted to the top, and `all` is every node, because the
|
|
3556
|
+
* minimap summarises the graph rather than the pass.
|
|
3557
|
+
*/
|
|
3558
|
+
override invalidate(
|
|
3559
|
+
layout?: boolean,
|
|
3560
|
+
damage?: Parameters<Node['invalidate']>[1],
|
|
3561
|
+
reason?: string,
|
|
3562
|
+
): void {
|
|
3563
|
+
// Whatever changed the graph may have changed the minimap, which
|
|
3564
|
+
// `<Flow>` may be painting on a canvas of its own; a change to this
|
|
3565
|
+
// element's props may have changed the controls too. The controls draw
|
|
3566
|
+
// nothing of the graph, and repainting them with every step of a drag
|
|
3567
|
+
// repainted the minimap with them — each canvas builds the panels its
|
|
3568
|
+
// box reaches, and theirs reached both. Each claims its own box: a
|
|
3569
|
+
// claim with no region is the whole window, which made every pan step
|
|
3570
|
+
// and dash tick a full frame. A dash tick changes neither — the minimap
|
|
3571
|
+
// draws no dashes — and repainting both canvases on every one kept an
|
|
3572
|
+
// idle pane painting them 17 times a second, and put a claim in the
|
|
3573
|
+
// middle of any pan frame it landed in.
|
|
3574
|
+
if (reason !== 'animation') {
|
|
3575
|
+
const canvases =
|
|
3576
|
+
reason === 'props' ? this._panelCanvases : this._miniMapCanvases();
|
|
3577
|
+
for (const canvas of canvases) canvas.invalidate(false, canvas, 'props');
|
|
3578
|
+
}
|
|
3579
|
+
// A dash tick moves the phase, a uniform on the GPU, so it is no change
|
|
3580
|
+
// to the world. A pan or a zoom never gets here under GL.
|
|
3581
|
+
if (reason === 'lift') this._liftVersion++;
|
|
3582
|
+
else if (reason !== 'animation') this._worldVersion++;
|
|
3583
|
+
// …and what a 2D zoom gesture composites is the graph as it was: a
|
|
3584
|
+
// change to it is a picture that no longer is
|
|
3585
|
+
if (reason !== 'animation' && reason !== 'scroll') this._dropZoomShot();
|
|
3586
|
+
// Under GL the surface over this box draws the graph and the 2D pane
|
|
3587
|
+
// under it shows nothing: a claim of its pixels was a window pass over
|
|
3588
|
+
// them, unseen, whose one job was to reach `paint` and ask the surface
|
|
3589
|
+
// for a frame — a BeginDraw, a walk and a commit on every step of a
|
|
3590
|
+
// drag, 2.6 ms of each. The graph's own claims ask for the frame
|
|
3591
|
+
// directly; the frame does what `paint` did (`glFrame`). A layout change
|
|
3592
|
+
// and a change of props — which may move the panels — still go through.
|
|
3593
|
+
if (
|
|
3594
|
+
!layout &&
|
|
3595
|
+
damage != null &&
|
|
3596
|
+
(reason === 'content' ||
|
|
3597
|
+
reason === 'style-state' ||
|
|
3598
|
+
reason === 'animation' ||
|
|
3599
|
+
reason === 'lift') &&
|
|
3600
|
+
this._gl
|
|
3601
|
+
) {
|
|
3602
|
+
this._glRequest!();
|
|
3603
|
+
return;
|
|
3604
|
+
}
|
|
3605
|
+
// a lift is this element's own bookkeeping; core knows it as content
|
|
3606
|
+
super.invalidate(layout, damage, reason === 'lift' ? 'content' : reason);
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
/** A 2D frame's cost so far, while its flush is still painting passes. */
|
|
3610
|
+
private _frameCost: { sceneMs: number; drawMs: number } | null = null;
|
|
3611
|
+
|
|
3612
|
+
/**
|
|
3613
|
+
* One `onFrame` per window flush, however many damage passes it painted.
|
|
3614
|
+
* The window paints its passes one after another in a single task, so a
|
|
3615
|
+
* microtask queued by the first runs after the last — and reports the
|
|
3616
|
+
* frame whole, which is what a frame rate counts.
|
|
3617
|
+
*/
|
|
3618
|
+
private _reportFrame(sceneMs: number, drawMs: number): void {
|
|
3619
|
+
if (!this.props.onFrame) return;
|
|
3620
|
+
if (this._frameCost) {
|
|
3621
|
+
this._frameCost.sceneMs += sceneMs;
|
|
3622
|
+
this._frameCost.drawMs += drawMs;
|
|
3623
|
+
return;
|
|
3624
|
+
}
|
|
3625
|
+
this._frameCost = { sceneMs, drawMs };
|
|
3626
|
+
void Promise.resolve().then(() => {
|
|
3627
|
+
const cost = this._frameCost;
|
|
3628
|
+
this._frameCost = null;
|
|
3629
|
+
if (!cost || this.destroyed) return;
|
|
3630
|
+
this._prop<(stats: FlowFrameStats) => void>('onFrame')?.({
|
|
3631
|
+
renderer: 'retained',
|
|
3632
|
+
sceneMs: cost.sceneMs,
|
|
3633
|
+
packMs: 0,
|
|
3634
|
+
drawMs: cost.drawMs,
|
|
3635
|
+
drawCalls: 0,
|
|
3636
|
+
lines: 0,
|
|
3637
|
+
boxes: 0,
|
|
3638
|
+
triangles: 0,
|
|
3639
|
+
uploadBytes: 0,
|
|
3640
|
+
worldRebuilt: false,
|
|
3641
|
+
gaps: { text: 0, custom: 0 },
|
|
3642
|
+
bodies: this.bodyBudget(),
|
|
3643
|
+
});
|
|
3644
|
+
});
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
private get _gl(): boolean {
|
|
3648
|
+
return this.props.renderer === 'gl' && this._glRequest != null;
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
/** What the GL renderer's label atlas sets strings with: this pane's app,
|
|
3652
|
+
* for its staging surface, and the options its 2D painter measures with
|
|
3653
|
+
* — so a label is cut and placed the same on both renderers. Null where
|
|
3654
|
+
* there is no text engine to ask. */
|
|
3655
|
+
glText(): { app: unknown; options: PainterOptions } | null {
|
|
3656
|
+
const options = this._textOptions();
|
|
3657
|
+
return options.fonts ? { app: this.app, options } : null;
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
/**
|
|
3661
|
+
* The GL surface's hook: how it asks for frames. Set while a surface draws
|
|
3662
|
+
* this pane, cleared when it goes — and until it is set the pane draws
|
|
3663
|
+
* itself, so a surface that is still loading shows the 2D graph rather
|
|
3664
|
+
* than an empty box.
|
|
3665
|
+
*/
|
|
3666
|
+
setGlRequest(request: (() => void) | null): void {
|
|
3667
|
+
if (this._glRequest === request) return;
|
|
3668
|
+
this._glRequest = request;
|
|
3669
|
+
// whichever renderer now draws, it draws the whole pane
|
|
3670
|
+
this._repaint('props');
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
/**
|
|
3674
|
+
* One GL frame's worth of scene, in the two layers the renderer keeps
|
|
3675
|
+
* apart (`./gl/renderer.ts`): the **world**, rebuilt only when it changed
|
|
3676
|
+
* since the key the surface last drew, and the **overlay**, every frame.
|
|
3677
|
+
*
|
|
3678
|
+
* The world is built at a pinned origin and culled to an *overscan* — the
|
|
3679
|
+
* view grown by a pane in every direction — so a pan inside it changes
|
|
3680
|
+
* nothing but `offset`. When the view leaves the overscan, or the zoom
|
|
3681
|
+
* moves, the overscan is re-centred and the world rebuilt; at every other
|
|
3682
|
+
* frame of a pan the world comes back `null` and the renderer draws the
|
|
3683
|
+
* buffers already on the GPU.
|
|
3684
|
+
*
|
|
3685
|
+
* A zoom *gesture* is drawn the same way: while the zoom keeps moving,
|
|
3686
|
+
* each step draws the world on the GPU magnified by `zoom` — the view's
|
|
3687
|
+
* zoom over the one it was built at — and the world is rebuilt at the
|
|
3688
|
+
* zoom the gesture stopped at, `GL_ZOOM_REST_MS` after its last step.
|
|
3689
|
+
* Rebuilding every step was the whole of a zoom's cost: a scene built,
|
|
3690
|
+
* packed and uploaded per step, 19 ms of it on a 300-node graph, where a
|
|
3691
|
+
* pan step is a uniform. What a magnified world gets wrong until then is
|
|
3692
|
+
* what a zoom does not scale linearly — a label's raster, the hairlines
|
|
3693
|
+
* with a floor of a pixel, the detail that appears past a zoom — which is
|
|
3694
|
+
* what every map does during a pinch. A single jump — a control's button,
|
|
3695
|
+
* `fitView`, `setViewport` — is not a stream and rebuilds at once, and so
|
|
3696
|
+
* does a step that would magnify one build by more than `GL_ZOOM_SPAN`
|
|
3697
|
+
* or show more of the graph than the overscan holds.
|
|
3698
|
+
*
|
|
3699
|
+
* The key is everything the world is a function of that this element can
|
|
3700
|
+
* name: its version (every repaint but a view change moves it), the
|
|
3701
|
+
* overscan, the zoom it was built at, and the palette and face — the two
|
|
3702
|
+
* a theme change moves, which core announces to the window rather than
|
|
3703
|
+
* to this element.
|
|
3704
|
+
*
|
|
3705
|
+
* Everything a 2D paint does around its drawing happens here too: the
|
|
3706
|
+
* pending fit, the dash timer, the first scene announcement, the bodies.
|
|
3707
|
+
*/
|
|
3708
|
+
glFrame(
|
|
3709
|
+
lastKey: string | null,
|
|
3710
|
+
lastLifted: string | null = null,
|
|
3711
|
+
): {
|
|
3712
|
+
world: FlowScene | null;
|
|
3713
|
+
key: string;
|
|
3714
|
+
lifted: FlowScene | null | undefined;
|
|
3715
|
+
liftedKey: string | null;
|
|
3716
|
+
offset: XYPosition;
|
|
3717
|
+
zoom: number;
|
|
3718
|
+
overlay: FlowScene;
|
|
3719
|
+
phase: number;
|
|
3720
|
+
moving: boolean;
|
|
3721
|
+
} | null {
|
|
3722
|
+
if (!this._visible()) return null;
|
|
3723
|
+
this._frameTick();
|
|
3724
|
+
this._sync();
|
|
3725
|
+
this._painting = true;
|
|
3726
|
+
this._frameClip = null;
|
|
3727
|
+
if (this._fitPending) {
|
|
3728
|
+
this._fitPending = false;
|
|
3729
|
+
this._fit(this._prop<FitViewOptions>('fitViewOptions'));
|
|
3730
|
+
}
|
|
3731
|
+
const v = this._viewport();
|
|
3732
|
+
const pane = this._pane();
|
|
3733
|
+
const palette = this._palette();
|
|
3734
|
+
|
|
3735
|
+
const lift = this._lifted();
|
|
3736
|
+
const keyOf = (): string =>
|
|
3737
|
+
`${this._worldVersion}|${this._worldCullId}|${this._worldZoom}|` +
|
|
3738
|
+
`${this._fontSeen}|${JSON.stringify(palette)}|${this._liftGen}`;
|
|
3739
|
+
let key = keyOf();
|
|
3740
|
+
// The view in the world's own coordinates — graph × the zoom it was
|
|
3741
|
+
// built at, the pan left out: `offset` puts the pan back, and `zoom`
|
|
3742
|
+
// the rest of the zoom.
|
|
3743
|
+
const zoom = v.zoom / this._worldZoom;
|
|
3744
|
+
const view = {
|
|
3745
|
+
x: -v.x / zoom,
|
|
3746
|
+
y: -v.y / zoom,
|
|
3747
|
+
width: pane.width / zoom,
|
|
3748
|
+
height: pane.height / zoom,
|
|
3749
|
+
};
|
|
3750
|
+
const cull = this._worldCull;
|
|
3751
|
+
const inside =
|
|
3752
|
+
cull != null &&
|
|
3753
|
+
view.x >= cull.x &&
|
|
3754
|
+
view.y >= cull.y &&
|
|
3755
|
+
view.x + view.width <= cull.x + cull.width &&
|
|
3756
|
+
view.y + view.height <= cull.y + cull.height;
|
|
3757
|
+
const scaled =
|
|
3758
|
+
inside &&
|
|
3759
|
+
zoom !== 1 &&
|
|
3760
|
+
key === lastKey &&
|
|
3761
|
+
this._zoomStream &&
|
|
3762
|
+
now() - this._zoomAt < GL_ZOOM_REST_MS &&
|
|
3763
|
+
zoom <= GL_ZOOM_SPAN &&
|
|
3764
|
+
zoom >= 1 / GL_ZOOM_SPAN;
|
|
3765
|
+
if (scaled) {
|
|
3766
|
+
this._restZoom();
|
|
3767
|
+
} else if (!inside || zoom !== 1) {
|
|
3768
|
+
this._worldCull = {
|
|
3769
|
+
x: -v.x - pane.width,
|
|
3770
|
+
y: -v.y - pane.height,
|
|
3771
|
+
width: pane.width * 3,
|
|
3772
|
+
height: pane.height * 3,
|
|
3773
|
+
};
|
|
3774
|
+
this._worldZoom = v.zoom;
|
|
3775
|
+
this._worldCullId++;
|
|
3776
|
+
key = keyOf();
|
|
3777
|
+
}
|
|
3778
|
+
|
|
3779
|
+
let world: FlowScene | null = null;
|
|
3780
|
+
if (key !== lastKey) {
|
|
3781
|
+
world = buildScene(
|
|
3782
|
+
this._sceneInput(palette, {
|
|
3783
|
+
world: this._worldCull!,
|
|
3784
|
+
lift: lift ? { ids: lift, only: false } : undefined,
|
|
3785
|
+
}),
|
|
3786
|
+
);
|
|
3787
|
+
this._worldAnimated = world.animated;
|
|
3788
|
+
}
|
|
3789
|
+
// The nodes a drag moves, and their edges, on their own: a step
|
|
3790
|
+
// rebuilds them and not the world (`_liftVersion`).
|
|
3791
|
+
let lifted: FlowScene | null | undefined;
|
|
3792
|
+
const liftedKey = lift ? `${this._liftVersion}|${key}` : null;
|
|
3793
|
+
if (lift && liftedKey !== lastLifted) {
|
|
3794
|
+
lifted = buildScene(
|
|
3795
|
+
this._sceneInput(palette, {
|
|
3796
|
+
world: this._worldCull!,
|
|
3797
|
+
lift: { ids: lift, only: true },
|
|
3798
|
+
}),
|
|
3799
|
+
);
|
|
3800
|
+
this._liftedAnimated = lifted.animated;
|
|
3801
|
+
} else if (!lift && lastLifted != null) {
|
|
3802
|
+
lifted = null;
|
|
3803
|
+
this._liftedAnimated = false;
|
|
3804
|
+
}
|
|
3805
|
+
if (world || lifted !== undefined) {
|
|
3806
|
+
// The dash timer lives as long as an animated edge is drawn; its
|
|
3807
|
+
// ticks move the phase, a uniform.
|
|
3808
|
+
if (this._worldAnimated || this._liftedAnimated) this._startAnimation();
|
|
3809
|
+
else this._stopAnimation();
|
|
3810
|
+
}
|
|
3811
|
+
const overlay = buildScene(this._sceneInput(palette, 'overlay'));
|
|
3812
|
+
this._painting = false;
|
|
3813
|
+
|
|
3814
|
+
if (!this._sceneAnnounced) {
|
|
3815
|
+
this._sceneAnnounced = true;
|
|
3816
|
+
this.notifyA11ySceneChanged();
|
|
3817
|
+
}
|
|
3818
|
+
this._emitBodies();
|
|
3819
|
+
return {
|
|
3820
|
+
world,
|
|
3821
|
+
key,
|
|
3822
|
+
lifted,
|
|
3823
|
+
liftedKey,
|
|
3824
|
+
offset: { x: pane.x + v.x, y: pane.y + v.y },
|
|
3825
|
+
zoom: scaled ? zoom : 1,
|
|
3826
|
+
overlay,
|
|
3827
|
+
// the scene bakes `-phase * zoom` into a marching edge; the GPU is
|
|
3828
|
+
// handed the same number as a uniform
|
|
3829
|
+
phase: -this._dashPhase * v.zoom,
|
|
3830
|
+
moving: this._zoomMoving(),
|
|
3831
|
+
};
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
/** The frame that rebuilds the GL world once a zoom gesture stops — asked
|
|
3835
|
+
* for `GL_ZOOM_REST_MS` after its last step, however many steps the
|
|
3836
|
+
* timer outlived. */
|
|
3837
|
+
private _restZoom(): void {
|
|
3838
|
+
if (this._zoomRest != null) return;
|
|
3839
|
+
const wait = this._zoomAt + GL_ZOOM_REST_MS - now();
|
|
3840
|
+
this._zoomRest = timers.setTimeout?.(
|
|
3841
|
+
() => {
|
|
3842
|
+
this._zoomRest = null;
|
|
3843
|
+
if (now() - this._zoomAt < GL_ZOOM_REST_MS) this._restZoom();
|
|
3844
|
+
else if (this._gl) this._glRequest?.();
|
|
3845
|
+
else if (this._textApproximated) {
|
|
3846
|
+
this._dropZoomShot();
|
|
3847
|
+
// …and in 2D, the labels the gesture drew scaled, set at their
|
|
3848
|
+
// own sizes
|
|
3849
|
+
this._textApproximated = false;
|
|
3850
|
+
this._repaint('content');
|
|
3851
|
+
}
|
|
3852
|
+
},
|
|
3853
|
+
Math.max(0, wait) + 1,
|
|
3854
|
+
);
|
|
3855
|
+
}
|
|
3856
|
+
|
|
3857
|
+
/** A whole paint of the graph took `ms` (`_graphPaints`). */
|
|
3858
|
+
private _noteGraphPaint(ms: number): void {
|
|
3859
|
+
this._graphPaints.push(ms);
|
|
3860
|
+
if (this._graphPaints.length > GRAPH_PAINTS_KEPT) this._graphPaints.shift();
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3863
|
+
/** The median of the graph's last whole paints — the lower of the two
|
|
3864
|
+
* middle ones for an even count — or zero before the first. */
|
|
3865
|
+
private _graphPaintMs(): number {
|
|
3866
|
+
const paints = this._graphPaints;
|
|
3867
|
+
if (paints.length === 0) return 0;
|
|
3868
|
+
const sorted = [...paints].sort((a, b) => a - b);
|
|
3869
|
+
return sorted[(sorted.length - 1) >> 1];
|
|
3870
|
+
}
|
|
3871
|
+
|
|
3872
|
+
/** Whether a pass over `clip` painted the whole pane — what a zoom step
|
|
3873
|
+
* paints, and the only pass whose cost says what one would. */
|
|
3874
|
+
/** Whether a pass (`paintDamage()`, device pixels) repaints every pixel
|
|
3875
|
+
* of the pane, unlike {@link _wholePane}'s nine tenths. */
|
|
3876
|
+
private _coversPane(clip: FlowRect | null): boolean {
|
|
3877
|
+
if (clip === null) return true;
|
|
3878
|
+
const pane = this._pane();
|
|
3879
|
+
const c = this._logical(clip);
|
|
3880
|
+
const slack = 0.5 / this._scale;
|
|
3881
|
+
return (
|
|
3882
|
+
c.x <= pane.x + slack &&
|
|
3883
|
+
c.y <= pane.y + slack &&
|
|
3884
|
+
c.x + c.width >= pane.x + pane.width - slack &&
|
|
3885
|
+
c.y + c.height >= pane.y + pane.height - slack
|
|
3886
|
+
);
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3889
|
+
private _wholePane(clip: FlowRect | null): boolean {
|
|
3890
|
+
if (clip === null) return true;
|
|
3891
|
+
const pane = this._pane();
|
|
3892
|
+
const s = this._scale;
|
|
3893
|
+
return clip.width * clip.height >= 0.9 * pane.width * s * pane.height * s;
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
/**
|
|
3897
|
+
* The graph painted once onto a surface of its own for the zoom gesture
|
|
3898
|
+
* under way — made at the gesture's first step that finds none — or null
|
|
3899
|
+
* where there is no surface to paint on.
|
|
3900
|
+
*/
|
|
3901
|
+
private _zoomShotFor(
|
|
3902
|
+
palette: FlowPalette,
|
|
3903
|
+
approximateText: boolean,
|
|
3904
|
+
): ZoomShot | null {
|
|
3905
|
+
const held = this._zoomShot;
|
|
3906
|
+
if (held) {
|
|
3907
|
+
// Kept while it is sharp enough and covers enough: magnified past
|
|
3908
|
+
// `GL_ZOOM_SPAN` it is soft, and a zoom out uncovers a ring the step
|
|
3909
|
+
// paints live, which past `ZOOM_SHOT_LIVE` of the pane costs what a
|
|
3910
|
+
// new picture does.
|
|
3911
|
+
const k = this._viewport().zoom / held.viewport.zoom;
|
|
3912
|
+
const pane = this._pane();
|
|
3913
|
+
const live =
|
|
3914
|
+
this._uncovered(held).reduce((sum, r) => sum + r.width * r.height, 0) /
|
|
3915
|
+
Math.max(1, pane.width * pane.height);
|
|
3916
|
+
if (k <= GL_ZOOM_SPAN && live <= ZOOM_SHOT_LIVE) return held;
|
|
3917
|
+
this._dropZoomShot();
|
|
3918
|
+
}
|
|
3919
|
+
const ctor = (ntk as unknown as { Surface?: ShotCtor }).Surface;
|
|
3920
|
+
if (typeof ctor !== 'function') return null;
|
|
3921
|
+
const pane = this._pane();
|
|
3922
|
+
const scale = this._scale;
|
|
3923
|
+
const width = Math.max(1, Math.ceil(pane.width * scale));
|
|
3924
|
+
const height = Math.max(1, Math.ceil(pane.height * scale));
|
|
3925
|
+
let surface: ShotSurface;
|
|
3926
|
+
try {
|
|
3927
|
+
surface = new ctor(this.app, { width, height, format: 'argb32' });
|
|
3928
|
+
} catch {
|
|
3929
|
+
return null;
|
|
3930
|
+
}
|
|
3931
|
+
const sctx = surface.getContext('2d') as {
|
|
3932
|
+
translate?(x: number, y: number): void;
|
|
3933
|
+
} | null;
|
|
3934
|
+
// its labels drawn as a step inside the gesture draws them — from the
|
|
3935
|
+
// sizes they have, where the window's context scales text — so the one
|
|
3936
|
+
// paint of the gesture shapes nothing the gesture would not
|
|
3937
|
+
const painter = sctx
|
|
3938
|
+
? createPainter(sctx, { ...this._textOptions(), approximateText })
|
|
3939
|
+
: null;
|
|
3940
|
+
if (!sctx || !painter || typeof sctx.translate !== 'function') {
|
|
3941
|
+
surface.destroy?.();
|
|
3942
|
+
return null;
|
|
3943
|
+
}
|
|
3944
|
+
// The surface's corner is the pane's, and the whole of the pane is in
|
|
3945
|
+
// it whatever this pass's damage, since every step after composites it.
|
|
3946
|
+
// The ground too: a grid at the fractional pitch a zoom passes through
|
|
3947
|
+
// is thousands of runs, 7 ms of Direct2D a step over the stress
|
|
3948
|
+
// lattice's pane once the graph was a picture. Drawn as runs here —
|
|
3949
|
+
// the tile is a pattern on the window's context, phased in its pixels,
|
|
3950
|
+
// and a zoom's pitch is a fraction of a pixel almost always.
|
|
3951
|
+
sctx.translate(-pane.x * scale, -pane.y * scale);
|
|
3952
|
+
const building = now();
|
|
3953
|
+
const whole = buildScene({ ...this._sceneInput(palette), clip: null });
|
|
3954
|
+
const built = now();
|
|
3955
|
+
paintGround(painter, whole);
|
|
3956
|
+
const grounded = now();
|
|
3957
|
+
paintGraph(painter, whole);
|
|
3958
|
+
// What a step would cost live: the scene and the graph. Not the ground,
|
|
3959
|
+
// which is runs here and a tile on the window — on X11 it made a
|
|
3960
|
+
// picture's paint half as dear again as a step, and kept the picture of
|
|
3961
|
+
// a graph that did not need one.
|
|
3962
|
+
this._noteGraphPaint(built - building + (now() - grounded));
|
|
3963
|
+
this._zoomShot = { surface, viewport: this._viewport(), width, height };
|
|
3964
|
+
return this._zoomShot;
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
/** Where the gesture's picture lands at the viewport of the moment, in
|
|
3968
|
+
* the pane's logical pixels: a world point drawn at `w·z₀ + v₀` is at
|
|
3969
|
+
* `w·z + v`, so the picture is scaled by `z / z₀` about the pane's corner
|
|
3970
|
+
* and moved by `v − v₀·z/z₀`. */
|
|
3971
|
+
private _shotRect(shot: ZoomShot): FlowRect {
|
|
3972
|
+
const v = this._viewport();
|
|
3973
|
+
const v0 = shot.viewport;
|
|
3974
|
+
const k = v.zoom / v0.zoom;
|
|
3975
|
+
const pane = this._pane();
|
|
3976
|
+
const s = this._scale;
|
|
3977
|
+
return {
|
|
3978
|
+
x: pane.x + v.x - v0.x * k,
|
|
3979
|
+
y: pane.y + v.y - v0.y * k,
|
|
3980
|
+
width: (shot.width / s) * k,
|
|
3981
|
+
height: (shot.height / s) * k,
|
|
3982
|
+
};
|
|
3983
|
+
}
|
|
3984
|
+
|
|
3985
|
+
private _compositeShot(ctx: Context2D, shot: ZoomShot): void {
|
|
3986
|
+
const draw = (
|
|
3987
|
+
ctx as unknown as {
|
|
3988
|
+
drawImage?(image: unknown, ...args: number[]): void;
|
|
3989
|
+
}
|
|
3990
|
+
).drawImage;
|
|
3991
|
+
if (typeof draw !== 'function') return;
|
|
3992
|
+
const to = this._shotRect(shot);
|
|
3993
|
+
const s = this._scale;
|
|
3994
|
+
draw.call(
|
|
3995
|
+
ctx,
|
|
3996
|
+
shot.surface,
|
|
3997
|
+
0,
|
|
3998
|
+
0,
|
|
3999
|
+
shot.width,
|
|
4000
|
+
shot.height,
|
|
4001
|
+
to.x * s,
|
|
4002
|
+
to.y * s,
|
|
4003
|
+
to.width * s,
|
|
4004
|
+
to.height * s,
|
|
4005
|
+
);
|
|
4006
|
+
}
|
|
4007
|
+
|
|
4008
|
+
/**
|
|
4009
|
+
* The pane this pass reaches that the gesture's picture does not: nothing
|
|
4010
|
+
* while zooming in about a point in the pane, a ring round it zooming out,
|
|
4011
|
+
* a side after a pan. Up to four bands — above, below, and either side
|
|
4012
|
+
* between them — each reaching a pixel under the picture's edge, which
|
|
4013
|
+
* its filtering leaves half there.
|
|
4014
|
+
*/
|
|
4015
|
+
private _uncovered(shot: ZoomShot): FlowRect[] {
|
|
4016
|
+
const pane = this._frameClip
|
|
4017
|
+
? intersectRects(this._pane(), this._frameClip)
|
|
4018
|
+
: this._pane();
|
|
4019
|
+
if (!pane) return [];
|
|
4020
|
+
const inner = inflateRect(this._shotRect(shot), -1);
|
|
4021
|
+
const x0 = Math.max(pane.x, inner.x);
|
|
4022
|
+
const y0 = Math.max(pane.y, inner.y);
|
|
4023
|
+
const x1 = Math.min(pane.x + pane.width, inner.x + inner.width);
|
|
4024
|
+
const y1 = Math.min(pane.y + pane.height, inner.y + inner.height);
|
|
4025
|
+
if (x1 <= x0 || y1 <= y0) return [pane];
|
|
4026
|
+
const right = pane.x + pane.width;
|
|
4027
|
+
const bottom = pane.y + pane.height;
|
|
4028
|
+
const bands: FlowRect[] = [];
|
|
4029
|
+
if (y0 > pane.y) {
|
|
4030
|
+
bands.push({
|
|
4031
|
+
x: pane.x,
|
|
4032
|
+
y: pane.y,
|
|
4033
|
+
width: pane.width,
|
|
4034
|
+
height: y0 - pane.y,
|
|
4035
|
+
});
|
|
4036
|
+
}
|
|
4037
|
+
if (bottom > y1) {
|
|
4038
|
+
bands.push({ x: pane.x, y: y1, width: pane.width, height: bottom - y1 });
|
|
4039
|
+
}
|
|
4040
|
+
if (x0 > pane.x) {
|
|
4041
|
+
bands.push({ x: pane.x, y: y0, width: x0 - pane.x, height: y1 - y0 });
|
|
4042
|
+
}
|
|
4043
|
+
if (right > x1) {
|
|
4044
|
+
bands.push({ x: x1, y: y0, width: right - x1, height: y1 - y0 });
|
|
4045
|
+
}
|
|
4046
|
+
return bands;
|
|
4047
|
+
}
|
|
4048
|
+
|
|
4049
|
+
private _dropZoomShot(): void {
|
|
4050
|
+
this._zoomShot?.surface.destroy?.();
|
|
4051
|
+
this._zoomShot = null;
|
|
4052
|
+
}
|
|
4053
|
+
|
|
4054
|
+
/**
|
|
4055
|
+
* The pictures a 2D drag copies from — made on the first pass of the
|
|
4056
|
+
* gesture that has its nodes lifted, and kept while the graph under them
|
|
4057
|
+
* and the view hold still. A drag step repainted the box round the moved
|
|
4058
|
+
* node and every edge on it, and an edge is as long as it is: the stress
|
|
4059
|
+
* lattice sends its last rows' edges back to its first nodes, so dragging
|
|
4060
|
+
* one repainted the window a step, every card and edge in it, at 46-57
|
|
4061
|
+
* fps on Cocoa. Cutting the damage finer did not help — core merges a
|
|
4062
|
+
* frame's rects down to four, and a long diagonal's pieces merge back to
|
|
4063
|
+
* most of the pane. Copying what did not move is what does.
|
|
4064
|
+
*
|
|
4065
|
+
* Null where there is no offscreen surface, and for a gesture whose view
|
|
4066
|
+
* moved while it held pictures: it paints live from then on, rather than
|
|
4067
|
+
* paint the graph twice a step.
|
|
4068
|
+
*/
|
|
4069
|
+
private _liftShotFor(
|
|
4070
|
+
palette: FlowPalette,
|
|
4071
|
+
lift: ReadonlySet<string>,
|
|
4072
|
+
): LiftShot | null {
|
|
4073
|
+
const gesture = this._liftFor;
|
|
4074
|
+
if (!gesture || this._liftShotRefused === gesture) return null;
|
|
4075
|
+
const v = this._viewport();
|
|
4076
|
+
const pane = this._pane();
|
|
4077
|
+
const scale = this._scale;
|
|
4078
|
+
const width = Math.max(1, Math.ceil(pane.width * scale));
|
|
4079
|
+
const height = Math.max(1, Math.ceil(pane.height * scale));
|
|
4080
|
+
const held = this._liftShot;
|
|
4081
|
+
if (held) {
|
|
4082
|
+
const still =
|
|
4083
|
+
held.viewport.x === v.x &&
|
|
4084
|
+
held.viewport.y === v.y &&
|
|
4085
|
+
held.viewport.zoom === v.zoom;
|
|
4086
|
+
if (
|
|
4087
|
+
held.gesture === gesture &&
|
|
4088
|
+
still &&
|
|
4089
|
+
held.version === this._worldVersion &&
|
|
4090
|
+
held.width === width &&
|
|
4091
|
+
held.height === height
|
|
4092
|
+
) {
|
|
4093
|
+
return held;
|
|
4094
|
+
}
|
|
4095
|
+
this._dropLiftShot();
|
|
4096
|
+
if (held.gesture === gesture && !still) {
|
|
4097
|
+
this._liftShotRefused = gesture;
|
|
4098
|
+
return null;
|
|
4099
|
+
}
|
|
4100
|
+
}
|
|
4101
|
+
const ctor = (ntk as unknown as { Surface?: ShotCtor }).Surface;
|
|
4102
|
+
if (typeof ctor !== 'function') return null;
|
|
4103
|
+
const make = (): {
|
|
4104
|
+
surface: ShotSurface;
|
|
4105
|
+
painter: FlowPainter;
|
|
4106
|
+
} | null => {
|
|
4107
|
+
let surface: ShotSurface;
|
|
4108
|
+
try {
|
|
4109
|
+
surface = new ctor(this.app, { width, height, format: 'argb32' });
|
|
4110
|
+
} catch {
|
|
4111
|
+
return null;
|
|
4112
|
+
}
|
|
4113
|
+
const sctx = surface.getContext('2d') as {
|
|
4114
|
+
translate?(x: number, y: number): void;
|
|
4115
|
+
clearRect?(x: number, y: number, w: number, h: number): void;
|
|
4116
|
+
} | null;
|
|
4117
|
+
const painter = sctx ? createPainter(sctx, this._textOptions()) : null;
|
|
4118
|
+
if (!sctx || !painter || typeof sctx.translate !== 'function') {
|
|
4119
|
+
surface.destroy?.();
|
|
4120
|
+
return null;
|
|
4121
|
+
}
|
|
4122
|
+
// a clear ground: the cards' picture goes over the edges
|
|
4123
|
+
sctx.clearRect?.(0, 0, width, height);
|
|
4124
|
+
sctx.translate(-pane.x * scale, -pane.y * scale);
|
|
4125
|
+
return { surface, painter };
|
|
4126
|
+
};
|
|
4127
|
+
const under = make();
|
|
4128
|
+
const over = under ? make() : null;
|
|
4129
|
+
if (!under || !over) {
|
|
4130
|
+
under?.surface.destroy?.();
|
|
4131
|
+
return null;
|
|
4132
|
+
}
|
|
4133
|
+
const rest = buildScene({
|
|
4134
|
+
...this._sceneInput(palette, { lift: { ids: lift, only: false } }),
|
|
4135
|
+
clip: null,
|
|
4136
|
+
});
|
|
4137
|
+
paintGround(under.painter, rest);
|
|
4138
|
+
paintGraphEdges(under.painter, rest);
|
|
4139
|
+
paintGraphNodes(over.painter, rest);
|
|
4140
|
+
this._liftShot = {
|
|
4141
|
+
under: under.surface,
|
|
4142
|
+
over: over.surface,
|
|
4143
|
+
cards: rest.nodes.map((item) => inflateRect(item.rect, CULL_MARGIN)),
|
|
4144
|
+
gesture,
|
|
4145
|
+
version: this._worldVersion,
|
|
4146
|
+
viewport: { ...v },
|
|
4147
|
+
width,
|
|
4148
|
+
height,
|
|
4149
|
+
};
|
|
4150
|
+
return this._liftShot;
|
|
4151
|
+
}
|
|
4152
|
+
|
|
4153
|
+
/**
|
|
4154
|
+
* One of a drag's pictures, copied into this pass's rect of the pane —
|
|
4155
|
+
* the whole pane on a full pass. The ground's picture is opaque and is
|
|
4156
|
+
* *copied*, which the server does as a copy rather than a blend (`copy`
|
|
4157
|
+
* is XRender's `Src`, and a memcpy on Cocoa); the cards' picture is
|
|
4158
|
+
* blended in only where a card is (`only`). On XQuartz the two blends of
|
|
4159
|
+
* the step's box — as big as the moved node's longest edge — were most
|
|
4160
|
+
* of what a drag cost the server.
|
|
4161
|
+
*/
|
|
4162
|
+
private _compositeLift(
|
|
4163
|
+
ctx: Context2D,
|
|
4164
|
+
shot: LiftShot,
|
|
4165
|
+
surface: ShotSurface,
|
|
4166
|
+
only: FlowRect[] | null,
|
|
4167
|
+
): void {
|
|
4168
|
+
const c = ctx as unknown as {
|
|
4169
|
+
drawImage?(image: unknown, ...args: number[]): void;
|
|
4170
|
+
globalCompositeOperation?: string;
|
|
4171
|
+
};
|
|
4172
|
+
if (typeof c.drawImage !== 'function') return;
|
|
4173
|
+
const pane = this._pane();
|
|
4174
|
+
const rect = this._frameClip ? intersectRects(pane, this._frameClip) : pane;
|
|
4175
|
+
if (!rect) return;
|
|
4176
|
+
if (only) {
|
|
4177
|
+
for (const card of only) {
|
|
4178
|
+
const part = intersectRects(rect, card);
|
|
4179
|
+
if (part) this._copyShotRect(c, shot, surface, part);
|
|
4180
|
+
}
|
|
4181
|
+
return;
|
|
4182
|
+
}
|
|
4183
|
+
const before = c.globalCompositeOperation;
|
|
4184
|
+
c.globalCompositeOperation = 'copy';
|
|
4185
|
+
try {
|
|
4186
|
+
this._copyShotRect(c, shot, surface, rect);
|
|
4187
|
+
} finally {
|
|
4188
|
+
c.globalCompositeOperation = before ?? 'source-over';
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
|
|
4192
|
+
/** A rect of the pane, logical pixels, drawn from the same rect of a
|
|
4193
|
+
* picture of it — on whole device pixels, so nothing is resampled. */
|
|
4194
|
+
private _copyShotRect(
|
|
4195
|
+
ctx: { drawImage?(image: unknown, ...args: number[]): void },
|
|
4196
|
+
shot: LiftShot,
|
|
4197
|
+
surface: ShotSurface,
|
|
4198
|
+
rect: FlowRect,
|
|
4199
|
+
): void {
|
|
4200
|
+
const pane = this._pane();
|
|
4201
|
+
const s = this._scale;
|
|
4202
|
+
const ox = pane.x * s;
|
|
4203
|
+
const oy = pane.y * s;
|
|
4204
|
+
const x0 = Math.max(0, Math.floor(rect.x * s - ox));
|
|
4205
|
+
const y0 = Math.max(0, Math.floor(rect.y * s - oy));
|
|
4206
|
+
const x1 = Math.min(shot.width, Math.ceil((rect.x + rect.width) * s - ox));
|
|
4207
|
+
const y1 = Math.min(
|
|
4208
|
+
shot.height,
|
|
4209
|
+
Math.ceil((rect.y + rect.height) * s - oy),
|
|
4210
|
+
);
|
|
4211
|
+
if (x1 <= x0 || y1 <= y0) return;
|
|
4212
|
+
const w = x1 - x0;
|
|
4213
|
+
const h = y1 - y0;
|
|
4214
|
+
ctx.drawImage!(surface, x0, y0, w, h, ox + x0, oy + y0, w, h);
|
|
4215
|
+
}
|
|
4216
|
+
|
|
4217
|
+
private _dropLiftShot(): void {
|
|
4218
|
+
this._liftShot?.under.destroy?.();
|
|
4219
|
+
this._liftShot?.over.destroy?.();
|
|
4220
|
+
this._liftShot = null;
|
|
4221
|
+
}
|
|
4222
|
+
|
|
4223
|
+
/** Whether a zoom gesture is moving: a stream of zoom steps, the last of
|
|
4224
|
+
* them under `GL_ZOOM_REST_MS` ago. A single step — a button, `fitView`,
|
|
4225
|
+
* an app's `setViewport` — is not one. */
|
|
4226
|
+
private _zoomMoving(): boolean {
|
|
4227
|
+
return this._zoomStream && now() - this._zoomAt < GL_ZOOM_REST_MS;
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
/** Whether this pass's damage reaches the minimap's corner at all —
|
|
4231
|
+
* answered from the panel's box alone, without resolving the map. */
|
|
4232
|
+
private _miniMapReached(): boolean {
|
|
4233
|
+
const corner = this._miniMapCorner();
|
|
4234
|
+
if (!corner) return false;
|
|
4235
|
+
const clip = this._frameClip;
|
|
4236
|
+
return !clip || rectsOverlap(corner, clip);
|
|
4237
|
+
}
|
|
4238
|
+
|
|
4239
|
+
/** The minimap's panel, in logical window pixels, or null without one. */
|
|
4240
|
+
private _miniMapCorner(): FlowRect | null {
|
|
4241
|
+
const options = this._miniMapOptions();
|
|
4242
|
+
if (!options) return null;
|
|
4243
|
+
return this._corner(
|
|
4244
|
+
options.position,
|
|
4245
|
+
options.width ?? MINIMAP_W,
|
|
4246
|
+
options.height ?? MINIMAP_H,
|
|
4247
|
+
'bottom-right',
|
|
4248
|
+
);
|
|
4249
|
+
}
|
|
4250
|
+
|
|
4251
|
+
/**
|
|
4252
|
+
* Everything `buildScene` reads, for one of three uses:
|
|
4253
|
+
*
|
|
4254
|
+
* - `'all'` — the 2D renderer's pass: the pane as it is, culled to this
|
|
4255
|
+
* pass's damage.
|
|
4256
|
+
* - `{ world }` — the GL graph: built at a **pinned origin** (the viewport's
|
|
4257
|
+
* translation and the pane's origin both left out, so a pan does not
|
|
4258
|
+
* change a vertex) and culled to the overscan `world` rather than the
|
|
4259
|
+
* pane, with none of the pane's furniture.
|
|
4260
|
+
* - `'overlay'` — the GL furniture: the pane's background, grid, selection,
|
|
4261
|
+
* minimap and controls, and no graph at all.
|
|
4262
|
+
*/
|
|
4263
|
+
/**
|
|
4264
|
+
* The order nodes are drawn in, bottom to top: `zIndex`, then selection,
|
|
4265
|
+
* then declaration (`_order`), with a node being dragged lifted over all
|
|
4266
|
+
* of them. The one order both halves of a node follow — its card in the
|
|
4267
|
+
* scene, its mounted body in `_emitBodies` — so a body is stacked over
|
|
4268
|
+
* the bodies of the cards under its own, and under the ones above.
|
|
4269
|
+
*/
|
|
4270
|
+
private _paintOrder(): NodeEntry[] {
|
|
4271
|
+
const dragging = this._dragTo;
|
|
4272
|
+
if (!dragging || dragging.size === 0) return this._order;
|
|
4273
|
+
const order: NodeEntry[] = [];
|
|
4274
|
+
const lifted: NodeEntry[] = [];
|
|
4275
|
+
for (const entry of this._order) {
|
|
4276
|
+
if (dragging.has(entry.node.id)) lifted.push(entry);
|
|
4277
|
+
else order.push(entry);
|
|
4278
|
+
}
|
|
4279
|
+
return order.concat(lifted);
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
private _sceneInput(
|
|
4283
|
+
palette: FlowPalette,
|
|
4284
|
+
layer:
|
|
4285
|
+
| 'all'
|
|
4286
|
+
| 'overlay'
|
|
4287
|
+
| 'card'
|
|
4288
|
+
| {
|
|
4289
|
+
/** The GL world's overscan; left out, the pane as a 2D pass sees
|
|
4290
|
+
* it. */
|
|
4291
|
+
world?: FlowRect;
|
|
4292
|
+
/** Without the lifted nodes and their edges, or those alone
|
|
4293
|
+
* (`only`). */
|
|
4294
|
+
lift?: { ids: ReadonlySet<string>; only: boolean };
|
|
4295
|
+
} = 'all',
|
|
4296
|
+
/** The panels even while `<Flow>` paints them itself — for that paint. */
|
|
4297
|
+
panels = false,
|
|
4298
|
+
): SceneInput {
|
|
4299
|
+
// One card, for `paintCard`, which hands in the node itself: nothing
|
|
4300
|
+
// about the rest of the graph is read, where the whole input — every
|
|
4301
|
+
// node's source twice over, the minimap's walk for its bounds — was most
|
|
4302
|
+
// of what a card cost to paint, and a zoom repaints every one of them.
|
|
4303
|
+
if (layer === 'card') {
|
|
4304
|
+
const gesture = this._gesture;
|
|
4305
|
+
return {
|
|
4306
|
+
viewport: this._viewport(),
|
|
4307
|
+
pane: this._pane(),
|
|
4308
|
+
clip: null,
|
|
4309
|
+
palette,
|
|
4310
|
+
background: normalizeBackground(undefined),
|
|
4311
|
+
nodes: [],
|
|
4312
|
+
all: [],
|
|
4313
|
+
edges: [],
|
|
4314
|
+
dashPhase: this._dashPhase,
|
|
4315
|
+
hover: this._hover,
|
|
4316
|
+
connection: gesture?.kind === 'connect' ? gesture : null,
|
|
4317
|
+
selection: null,
|
|
4318
|
+
miniMap: null,
|
|
4319
|
+
controls: [],
|
|
4320
|
+
scale: this._scale,
|
|
4321
|
+
measure: this._measureBox,
|
|
4322
|
+
cache: this._sceneCache,
|
|
4323
|
+
batch: this._batch,
|
|
4324
|
+
};
|
|
4325
|
+
}
|
|
4326
|
+
const order = this._paintOrder();
|
|
4327
|
+
const gesture = this._gesture;
|
|
4328
|
+
// The panel cull comes before `_miniMap()`, which walks every node to
|
|
4329
|
+
// find the graph's bounds — during a drag that walk per pass would cost
|
|
4330
|
+
// more than the panel it skips. The dot for a mid-drag node goes stale
|
|
4331
|
+
// until the release repaints in full; that is the trade, and it is
|
|
4332
|
+
// deliberate.
|
|
4333
|
+
const world = typeof layer === 'object' ? (layer.world ?? null) : null;
|
|
4334
|
+
const lift = typeof layer === 'object' ? layer.lift : undefined;
|
|
4335
|
+
const overlay = layer === 'overlay';
|
|
4336
|
+
// the furniture is the overlay's: a lifted layer or the graph under it
|
|
4337
|
+
// has none of its own
|
|
4338
|
+
const map =
|
|
4339
|
+
!world && !lift && this._miniMapReached() ? this._miniMap() : null;
|
|
4340
|
+
// the fit the minimap on screen is painted with, from this pass on
|
|
4341
|
+
if (map && !panels && this._painting && this._panelCanvases.length === 0) {
|
|
4342
|
+
this._miniMapShown = map;
|
|
4343
|
+
}
|
|
4344
|
+
const viewport = this._viewport();
|
|
4345
|
+
const pane = this._pane();
|
|
4346
|
+
// An edge is the lifted layer's when either end is lifted.
|
|
4347
|
+
const edges = lift
|
|
4348
|
+
? this._edges.filter(
|
|
4349
|
+
(e) =>
|
|
4350
|
+
(lift.ids.has(e.source) || lift.ids.has(e.target)) === lift.only,
|
|
4351
|
+
)
|
|
4352
|
+
: this._edges;
|
|
4353
|
+
let all: SceneNodeSource[];
|
|
4354
|
+
if (lift?.only) {
|
|
4355
|
+
// the lifted nodes and the far ends of their edges: all the lifted
|
|
4356
|
+
// layer routes to, where every node's source was most of a step
|
|
4357
|
+
const ids = new Set(lift.ids);
|
|
4358
|
+
for (const e of edges) {
|
|
4359
|
+
ids.add(e.source);
|
|
4360
|
+
ids.add(e.target);
|
|
4361
|
+
}
|
|
4362
|
+
all = [];
|
|
4363
|
+
for (const id of ids) {
|
|
4364
|
+
const entry = this._byId.get(id);
|
|
4365
|
+
if (entry) all.push(this._sourceOf(entry));
|
|
4366
|
+
}
|
|
4367
|
+
} else if (overlay && !map) {
|
|
4368
|
+
all = [];
|
|
4369
|
+
} else if (layer === 'all' && this._painting) {
|
|
4370
|
+
all = this._frameLists(order).all;
|
|
4371
|
+
} else {
|
|
4372
|
+
all = this._entries.map((entry) => this._sourceOf(entry));
|
|
4373
|
+
}
|
|
4374
|
+
return {
|
|
4375
|
+
viewport: world ? { x: 0, y: 0, zoom: viewport.zoom } : viewport,
|
|
4376
|
+
pane: world
|
|
4377
|
+
? { x: 0, y: 0, width: pane.width, height: pane.height }
|
|
4378
|
+
: pane,
|
|
4379
|
+
clip: world || overlay ? null : this._frameClip,
|
|
4380
|
+
cull: world,
|
|
4381
|
+
palette,
|
|
4382
|
+
paneBackground: this.style.backgroundColor as string | undefined,
|
|
4383
|
+
background: normalizeBackground(
|
|
4384
|
+
this.props.background as
|
|
4385
|
+
BackgroundOptions | string | boolean | undefined,
|
|
4386
|
+
),
|
|
4387
|
+
// a card `<Flow>` paints in the bodies' layer is not painted twice
|
|
4388
|
+
nodes: overlay
|
|
4389
|
+
? []
|
|
4390
|
+
: layer === 'all' && this._painting
|
|
4391
|
+
? this._frameLists(order).nodes
|
|
4392
|
+
: order
|
|
4393
|
+
.filter(
|
|
4394
|
+
(entry) =>
|
|
4395
|
+
!this._cardInLayer(entry.node.id) &&
|
|
4396
|
+
(!lift || lift.ids.has(entry.node.id) === lift.only),
|
|
4397
|
+
)
|
|
4398
|
+
.map((entry) => this._sourceOf(entry)),
|
|
4399
|
+
all,
|
|
4400
|
+
edges: overlay ? [] : edges,
|
|
4401
|
+
dashPhase: this._dashPhase,
|
|
4402
|
+
hover: this._hover,
|
|
4403
|
+
connection: !overlay && gesture?.kind === 'connect' ? gesture : null,
|
|
4404
|
+
selection:
|
|
4405
|
+
!world && gesture?.kind === 'select' ? selectBoxRect(gesture) : null,
|
|
4406
|
+
miniMap:
|
|
4407
|
+
map && (panels || this._panelCanvases.length === 0)
|
|
4408
|
+
? {
|
|
4409
|
+
panel: map.panel,
|
|
4410
|
+
bounds: map.bounds,
|
|
4411
|
+
scale: map.scale,
|
|
4412
|
+
nodeColor: map.options.nodeColor,
|
|
4413
|
+
maskColor: map.options.maskColor,
|
|
4414
|
+
// the pane's own pass: the part of the minimap it repaints
|
|
4415
|
+
reach: panels ? null : this._frameClip,
|
|
4416
|
+
}
|
|
4417
|
+
: null,
|
|
4418
|
+
controls:
|
|
4419
|
+
world || lift || (!panels && this._panelCanvases.length > 0)
|
|
4420
|
+
? []
|
|
4421
|
+
: this._controlButtons(),
|
|
4422
|
+
scale: this._scale,
|
|
4423
|
+
measure: this._measureBox,
|
|
4424
|
+
cache: this._sceneCache,
|
|
4425
|
+
batch: this._batch,
|
|
4426
|
+
};
|
|
4427
|
+
}
|
|
4428
|
+
|
|
4429
|
+
/**
|
|
4430
|
+
* Where every mounted node body belongs, and where the graph's origin is.
|
|
4431
|
+
*
|
|
4432
|
+
* The two are sent apart because they change apart. A body's rect is
|
|
4433
|
+
* relative to the **graph's origin on screen** — its position times the
|
|
4434
|
+
* zoom — which a pan does not move; the origin is the viewport's
|
|
4435
|
+
* translation, which is all a pan moves. So a pan re-sends the *same*
|
|
4436
|
+
* bodies array with a new origin, `<Flow>` moves the one box they are
|
|
4437
|
+
* laid out in, and React re-renders none of them: the per-step commit of
|
|
4438
|
+
* every mounted body's position is what held a pan over 48 of them to 57
|
|
4439
|
+
* frames a second. A drag, a zoom, a selection or a body entering the pane
|
|
4440
|
+
* sends a new array, as before.
|
|
4441
|
+
*/
|
|
4442
|
+
private _emitBodies(): void {
|
|
4443
|
+
const notify =
|
|
4444
|
+
this._prop<
|
|
4445
|
+
(
|
|
4446
|
+
bodies: readonly NodeBodyRect[],
|
|
4447
|
+
sync: boolean,
|
|
4448
|
+
origin: XYPosition,
|
|
4449
|
+
held: boolean,
|
|
4450
|
+
) => void
|
|
4451
|
+
>('onNodeBodies');
|
|
4452
|
+
if (!notify) return;
|
|
4453
|
+
const v = this._viewport();
|
|
4454
|
+
if (this._holdBodies(v.zoom)) {
|
|
4455
|
+
if (!this._bodiesHeld) {
|
|
4456
|
+
// The 2D pane draws the cards again from this frame, not from the
|
|
4457
|
+
// commit that hides the bodies. The GL world never stopped drawing
|
|
4458
|
+
// them (`_cardInLayer`).
|
|
4459
|
+
this._bodiesHeld = true;
|
|
4460
|
+
notify(
|
|
4461
|
+
this._bodies,
|
|
4462
|
+
this._gestureSync || this._handleSync,
|
|
4463
|
+
this._bodiesOrigin,
|
|
4464
|
+
true,
|
|
4465
|
+
);
|
|
4466
|
+
}
|
|
4467
|
+
return;
|
|
4468
|
+
}
|
|
4469
|
+
const pane = this._pane();
|
|
4470
|
+
// Exact, and every rect below relative to it and free of the pan: a
|
|
4471
|
+
// card's offset from the graph's origin is its graph position times the
|
|
4472
|
+
// zoom, so a pan re-sends the same array and moves only the origin.
|
|
4473
|
+
// Nothing here is rounded — `<Flow>` puts the boxes on the device-pixel
|
|
4474
|
+
// grid, once. Rounded here as well, to logical pixels against a rounded
|
|
4475
|
+
// origin, a card's offset flipped by a pixel as the pan's fraction
|
|
4476
|
+
// cycled against the device grid `_screenRect` snaps to: the array was
|
|
4477
|
+
// new on most steps of a pan, and at 1.25× the layer changed size with
|
|
4478
|
+
// it — every body repainted, where core would have moved them.
|
|
4479
|
+
const origin = { x: v.x, y: v.y };
|
|
4480
|
+
const sv = screenViewport(v, pane);
|
|
4481
|
+
const was = this._bodies;
|
|
4482
|
+
const bodies: NodeBodyRect[] = [];
|
|
4483
|
+
let changed = false;
|
|
4484
|
+
for (const entry of this._paintOrder()) {
|
|
4485
|
+
if (entry.node.hidden || !this._mounted(entry)) continue;
|
|
4486
|
+
if (!rectsOverlap(this._screenRect(entry, sv), pane)) continue;
|
|
4487
|
+
const graph = this.rectOf(entry);
|
|
4488
|
+
const x = graph.x * v.zoom;
|
|
4489
|
+
const y = graph.y * v.zoom;
|
|
4490
|
+
const cardWidth = graph.width * v.zoom;
|
|
4491
|
+
const cardHeight = graph.height * v.zoom;
|
|
4492
|
+
const header = this._headerHeight(entry) * v.zoom;
|
|
4493
|
+
const inset = NODE_BODY_INSET * v.zoom;
|
|
4494
|
+
const width = cardWidth - inset * 2;
|
|
4495
|
+
const height = cardHeight - header - inset;
|
|
4496
|
+
if (width <= 1 || height <= 1) continue;
|
|
4497
|
+
const id = entry.node.id;
|
|
4498
|
+
const hovered = this._hover.nodeId === id;
|
|
4499
|
+
const selected = entry.node.selected ?? false;
|
|
4500
|
+
// The zoom counts as well as the rect it produced: it is the body's
|
|
4501
|
+
// subtree scale, and two zooms a hair apart can leave a box where it
|
|
4502
|
+
// was while the text inside it should have moved.
|
|
4503
|
+
const old = this._bodiesById.get(id);
|
|
4504
|
+
const body =
|
|
4505
|
+
old &&
|
|
4506
|
+
old.zoom === v.zoom &&
|
|
4507
|
+
old.selected === selected &&
|
|
4508
|
+
old.hovered === hovered &&
|
|
4509
|
+
old.card.x === x &&
|
|
4510
|
+
old.card.y === y &&
|
|
4511
|
+
old.card.width === cardWidth &&
|
|
4512
|
+
old.card.height === cardHeight &&
|
|
4513
|
+
old.height === height
|
|
4514
|
+
? old
|
|
4515
|
+
: {
|
|
4516
|
+
id,
|
|
4517
|
+
card: { x, y, width: cardWidth, height: cardHeight },
|
|
4518
|
+
hovered,
|
|
4519
|
+
// logical, and relative to the graph's origin on screen — the
|
|
4520
|
+
// box they are laid out in sits at `origin` in the pane
|
|
4521
|
+
x: x + inset,
|
|
4522
|
+
y: y + header,
|
|
4523
|
+
width,
|
|
4524
|
+
height,
|
|
4525
|
+
zoom: v.zoom,
|
|
4526
|
+
selected,
|
|
4527
|
+
};
|
|
4528
|
+
if (body !== was[bodies.length]) changed = true;
|
|
4529
|
+
bodies.push(body);
|
|
4530
|
+
}
|
|
4531
|
+
if (bodies.length !== was.length) changed = true;
|
|
4532
|
+
// An origin with no bodies laid out at it is nobody's news: a pan over a
|
|
4533
|
+
// graph whose node types mount bodies, with none on screen, sent one per
|
|
4534
|
+
// step, and `<Flow>` re-rendered for it — its panels' canvases with it.
|
|
4535
|
+
// The origin is still kept, and goes out with the first body to arrive.
|
|
4536
|
+
const moved =
|
|
4537
|
+
was.length > 0 &&
|
|
4538
|
+
(origin.x !== this._bodiesOrigin.x || origin.y !== this._bodiesOrigin.y);
|
|
4539
|
+
const released = this._bodiesHeld;
|
|
4540
|
+
if (!changed && !moved && !released) return;
|
|
4541
|
+
if (changed) {
|
|
4542
|
+
this._bodies = bodies;
|
|
4543
|
+
this._bodiesById = new Map(bodies.map((body) => [body.id, body]));
|
|
4544
|
+
}
|
|
4545
|
+
this._bodiesOrigin = origin;
|
|
4546
|
+
this._bodiesHeld = false;
|
|
4547
|
+
notify(this._bodies, this._gestureSync || this._handleSync, origin, false);
|
|
4548
|
+
}
|
|
4549
|
+
|
|
4550
|
+
/**
|
|
4551
|
+
* Whether mounted bodies sit this zoom out.
|
|
4552
|
+
*
|
|
4553
|
+
* A zoom changes every body's scale, and a scale is not a transform: each
|
|
4554
|
+
* body is styled, laid out, its text shaped and its pixels painted again,
|
|
4555
|
+
* at the new size, per step. That is about a millisecond a body on this
|
|
4556
|
+
* class of machine — 42 bodies held a zoom to 15 frames a second where
|
|
4557
|
+
* the graph alone costs 13 ms a step. So each step of a zoom *gesture*
|
|
4558
|
+
* predicts what re-scaling the bodies on screen would add, and when that
|
|
4559
|
+
* is over `BODY_BUDGET_MS` they sit the gesture out: still mounted — no
|
|
4560
|
+
* state is lost — but hidden (`display: 'none'`) and untouched, while the
|
|
4561
|
+
* cards, their labels and the edges carry it. Once the zoom has held still
|
|
4562
|
+
* for
|
|
4563
|
+
* `BODY_ZOOM_REST_MS` they come back, laid out once at the new scale. A
|
|
4564
|
+
* handful of bodies fits the budget and zooms live.
|
|
4565
|
+
*
|
|
4566
|
+
* The prediction is bodies × what one costs, measured from the pane's own
|
|
4567
|
+
* frames (`_frameTick`) — so a slower machine holds sooner. Once held, a
|
|
4568
|
+
* gesture stays held until it rests: bodies blinking in and out as the
|
|
4569
|
+
* count crosses the line would be worse than either.
|
|
4570
|
+
*
|
|
4571
|
+
* Only a stream counts: a gesture's steps — the wheel, a pinch, the keys
|
|
4572
|
+
* (`_gestureSync`) — or an animation's, `setViewport` a frame at a time,
|
|
4573
|
+
* each zoom under `GL_ZOOM_REST_MS` after the one before (`_zoomStream`,
|
|
4574
|
+
* the rule the zoom's picture and its scaled labels already follow). A
|
|
4575
|
+
* single programmatic jump — `fitView`, `setViewport`, a control's button
|
|
4576
|
+
* — applies at once, so an app that sets the viewport and reads the result
|
|
4577
|
+
* sees it; the pane never animates a viewport of its own. An animation
|
|
4578
|
+
* that stepped every body at every frame held a zoom over the stress
|
|
4579
|
+
* example's charts to 10 frames a second.
|
|
4580
|
+
*/
|
|
4581
|
+
private _holdBodies(zoom: number): boolean {
|
|
4582
|
+
if (zoom !== this._seenZoom) {
|
|
4583
|
+
const first = Number.isNaN(this._seenZoom);
|
|
4584
|
+
this._seenZoom = zoom;
|
|
4585
|
+
if (
|
|
4586
|
+
first ||
|
|
4587
|
+
(!this._gestureSync && !this._zoomStream && !this._bodiesHeld)
|
|
4588
|
+
) {
|
|
4589
|
+
return false;
|
|
4590
|
+
}
|
|
4591
|
+
const bodies = this._bodies.length;
|
|
4592
|
+
if (!this._bodiesHeld && bodies * this._bodyStepMs <= this._budgetMs()) {
|
|
4593
|
+
this._zoomStep = { live: true, bodies };
|
|
4594
|
+
return false;
|
|
4595
|
+
}
|
|
4596
|
+
this._zoomStep = { live: false, bodies: 0 };
|
|
4597
|
+
this._bodiesAt = now();
|
|
4598
|
+
this._restBodies();
|
|
4599
|
+
return true;
|
|
4600
|
+
}
|
|
4601
|
+
return this._bodiesHeld && this._bodiesRest != null;
|
|
4602
|
+
}
|
|
4603
|
+
|
|
4604
|
+
/** Bring the held bodies back once the zoom has held still for
|
|
4605
|
+
* `BODY_ZOOM_REST_MS` — judged on the pane's clock, like the zoom's own
|
|
4606
|
+
* rest (`_restZoom`): a timer that comes due while steps are still
|
|
4607
|
+
* arriving waits out what is left of the rest instead. */
|
|
4608
|
+
private _restBodies(): void {
|
|
4609
|
+
if (this._bodiesRest != null) return;
|
|
4610
|
+
const wait = this._restFrom() + BODY_ZOOM_REST_MS - now();
|
|
4611
|
+
this._bodiesRest = timers.setTimeout?.(
|
|
4612
|
+
() => {
|
|
4613
|
+
this._bodiesRest = null;
|
|
4614
|
+
if (now() - this._restFrom() < BODY_ZOOM_REST_MS) this._restBodies();
|
|
4615
|
+
else this._emitBodies();
|
|
4616
|
+
},
|
|
4617
|
+
Math.max(0, wait) + 1,
|
|
4618
|
+
);
|
|
4619
|
+
}
|
|
4620
|
+
|
|
4621
|
+
/**
|
|
4622
|
+
* What the bodies' rest is counted from: the last step of the zoom, or
|
|
4623
|
+
* the frame that drew it where that came later. A step the window has not
|
|
4624
|
+
* drawn yet is a zoom still moving — the frame is late, not the gesture
|
|
4625
|
+
* over — and counted from the step alone, a frame a server took half a
|
|
4626
|
+
* second to answer brought the bodies back between two steps of one
|
|
4627
|
+
* animation, whose next frame re-scaled all of them and was late in turn.
|
|
4628
|
+
* Past `BODY_REST_CAP_MS` without that frame they come back anyway: a
|
|
4629
|
+
* surface that stopped drawing must not keep them out for good.
|
|
4630
|
+
*/
|
|
4631
|
+
private _restFrom(): number {
|
|
4632
|
+
const drawn = this._zoomDrawnAt;
|
|
4633
|
+
if (drawn !== -Infinity) return Math.max(this._bodiesAt, drawn);
|
|
4634
|
+
return now() - this._bodiesAt < BODY_REST_CAP_MS ? now() : this._bodiesAt;
|
|
4635
|
+
}
|
|
4636
|
+
|
|
4637
|
+
/**
|
|
4638
|
+
* Paint one mounted node's card — fill, border, header, handles, grips —
|
|
4639
|
+
* into `ctx`, a `<canvas>` in the bodies' layer (`<Flow>`), exactly as the
|
|
4640
|
+
* scene paints it. `ctx` is at the canvas's origin, `abs` its place in
|
|
4641
|
+
* the window in device pixels.
|
|
4642
|
+
*
|
|
4643
|
+
* Why the card is painted twice: the bodies share one layer over every
|
|
4644
|
+
* card — over the GL surface, or beside the 2D pane — so a body could not
|
|
4645
|
+
* go under a card that was over its own. It painted over that card's
|
|
4646
|
+
* header, border and handles: a selected node's outline hidden behind the
|
|
4647
|
+
* body of the node under it. A card painted in the bodies' layer, just
|
|
4648
|
+
* under its own body, is stacked with it in the cards' paint order, and
|
|
4649
|
+
* covers the card the graph drew for it.
|
|
4650
|
+
*/
|
|
4651
|
+
paintCard(id: string, ctx: unknown, abs: XYPosition): void {
|
|
4652
|
+
const entry = this._byId.get(id);
|
|
4653
|
+
if (!entry) return;
|
|
4654
|
+
const painter = createPainter(ctx, this._textOptions());
|
|
4655
|
+
if (!painter) return;
|
|
4656
|
+
const palette = this._palette();
|
|
4657
|
+
const input = this._sceneInput(palette, 'card');
|
|
4658
|
+
const [item] = buildNodeItems({
|
|
4659
|
+
...input,
|
|
4660
|
+
nodes: [this._source(entry)],
|
|
4661
|
+
clip: null,
|
|
4662
|
+
cull: undefined,
|
|
4663
|
+
});
|
|
4664
|
+
if (!item) return;
|
|
4665
|
+
const c = ctx as {
|
|
4666
|
+
save(): void;
|
|
4667
|
+
restore(): void;
|
|
4668
|
+
translate(x: number, y: number): void;
|
|
4669
|
+
};
|
|
4670
|
+
c.save();
|
|
4671
|
+
try {
|
|
4672
|
+
c.translate(-abs.x, -abs.y);
|
|
4673
|
+
paintNodeItem(painter, item, { viewport: input.viewport, palette });
|
|
4674
|
+
} finally {
|
|
4675
|
+
c.restore();
|
|
4676
|
+
}
|
|
4677
|
+
}
|
|
4678
|
+
|
|
4679
|
+
/**
|
|
4680
|
+
* Whether a card is the bodies' layer's to draw and not the pane's: on
|
|
4681
|
+
* the 2D renderer, shown there and the bodies not held out of a zoom.
|
|
4682
|
+
*
|
|
4683
|
+
* **Never under GL.** The layer and the GL frame reach the screen by two
|
|
4684
|
+
* paths — the layer is the surface's overlay, painted and presented with
|
|
4685
|
+
* the window's frame, and a GL frame is swapped on the surface's own
|
|
4686
|
+
* turn — and this element cannot order them. So a card left to the layer
|
|
4687
|
+
* was, on some path, a card nobody drew: edges crossing the empty place
|
|
4688
|
+
* where it should be. The first paint of a scene of charts did that until
|
|
4689
|
+
* the layer had painted; a GL frame landing between a zoom's hold and the
|
|
4690
|
+
* commit that hid the bodies did it; and on macOS a wheel notch did it,
|
|
4691
|
+
* painting the overlay inside its own dispatch 20–25 ms before the GL
|
|
4692
|
+
* frame it asked for — a frame of bare edges at the start of every zoom
|
|
4693
|
+
* that held the bodies. Each was fixed by ordering one more path.
|
|
4694
|
+
*
|
|
4695
|
+
* The world draws every card instead, mounted or not, and the layer only
|
|
4696
|
+
* ever adds to it: its card and body land on the world's copy of that
|
|
4697
|
+
* card. Whatever order the two paths take, the worst the screen can show
|
|
4698
|
+
* is a body a frame early or late, over a card that is there. What it
|
|
4699
|
+
* costs is a few dozen cards more in a world build, drawn under their
|
|
4700
|
+
* copies. The 2D pane is painted in the same pass as the layer, in one
|
|
4701
|
+
* frame, and it leaves them out.
|
|
4702
|
+
*/
|
|
4703
|
+
private _cardInLayer(id: string): boolean {
|
|
4704
|
+
// held: hidden, or on the way to it, whatever `<Flow>` last reported
|
|
4705
|
+
return !this._gl && !this._bodiesHeld && this._shownBodies.has(id);
|
|
4706
|
+
}
|
|
4707
|
+
|
|
4708
|
+
/**
|
|
4709
|
+
* The cards `<Flow>` has on screen in the bodies' layer (`paintCard`),
|
|
4710
|
+
* which the 2D pane then leaves out. Told after the commit that shows
|
|
4711
|
+
* them, so no 2D frame lacks both; told the empty set while bodies are
|
|
4712
|
+
* held out of a zoom, when their cards are the pane's again. Under GL it
|
|
4713
|
+
* changes nothing drawn: the world draws every card (`_cardInLayer`).
|
|
4714
|
+
*/
|
|
4715
|
+
setShownBodies(ids: ReadonlySet<string>): void {
|
|
4716
|
+
const was = this._shownBodies;
|
|
4717
|
+
if (was.size === ids.size && [...ids].every((id) => was.has(id))) return;
|
|
4718
|
+
this._shownBodies = ids;
|
|
4719
|
+
// the pane, not the window
|
|
4720
|
+
if (!this._gl) this._repaint('content');
|
|
2852
4721
|
}
|
|
2853
4722
|
|
|
2854
4723
|
/**
|
|
2855
|
-
*
|
|
2856
|
-
*
|
|
2857
|
-
*
|
|
2858
|
-
*
|
|
4724
|
+
* The canvases `<Flow>` paints the minimap and controls on, over its
|
|
4725
|
+
* bodies (`paintPanels`): the bodies' layer is over the graph, so panels
|
|
4726
|
+
* the graph drew went under any card that reached them. While there are
|
|
4727
|
+
* any the graph leaves the panels out, and every change to the pane asks
|
|
4728
|
+
* them to repaint. Presses still land on the pane: the canvases take
|
|
4729
|
+
* none.
|
|
2859
4730
|
*/
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
4731
|
+
/** The panel canvases the minimap is painted on — the ones whose box
|
|
4732
|
+
* reaches its corner. */
|
|
4733
|
+
private _miniMapCanvases(): readonly PanelCanvas[] {
|
|
4734
|
+
const canvases = this._panelCanvases;
|
|
4735
|
+
if (canvases.length === 0) return canvases;
|
|
4736
|
+
const corner = this._miniMapCorner();
|
|
4737
|
+
if (!corner) return [];
|
|
4738
|
+
const box = this._device(corner);
|
|
4739
|
+
return canvases.filter((c) => !c.abs || rectsOverlap(c.abs, box));
|
|
4740
|
+
}
|
|
4741
|
+
|
|
4742
|
+
/**
|
|
4743
|
+
* The box `<Flow>` lays the node bodies out in, beside this pane, under
|
|
4744
|
+
* the 2D renderer — null under GL, and with no bodies to lay out. A pan
|
|
4745
|
+
* hands it to `scrollContents` as a rider: its pixels move with the
|
|
4746
|
+
* pane's.
|
|
4747
|
+
*/
|
|
4748
|
+
setBodiesLayer(layer: unknown): void {
|
|
4749
|
+
this._bodiesLayer = (layer as Node | null) ?? null;
|
|
4750
|
+
}
|
|
4751
|
+
|
|
4752
|
+
setPanelCanvases(canvases: readonly PanelCanvas[]): void {
|
|
4753
|
+
const had = this._panelCanvases.length > 0;
|
|
4754
|
+
this._panelCanvases = canvases;
|
|
4755
|
+
if (had !== canvases.length > 0) this._repaint('props');
|
|
4756
|
+
}
|
|
4757
|
+
|
|
4758
|
+
/** Where the minimap and the controls are, relative to the pane's
|
|
4759
|
+
* top-left, logical — with a pixel round for their borders. */
|
|
4760
|
+
panelRects(): FlowRect[] {
|
|
2867
4761
|
const pane = this._pane();
|
|
2868
|
-
const
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
const
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
if (width
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
4762
|
+
const rects: FlowRect[] = [];
|
|
4763
|
+
const map = this._miniMapOptions();
|
|
4764
|
+
if (map) {
|
|
4765
|
+
const panel = this._corner(
|
|
4766
|
+
map.position,
|
|
4767
|
+
map.width ?? MINIMAP_W,
|
|
4768
|
+
map.height ?? MINIMAP_H,
|
|
4769
|
+
'bottom-right',
|
|
4770
|
+
);
|
|
4771
|
+
if (pane.width >= panel.width * 1.6 && pane.height >= panel.height * 1.6)
|
|
4772
|
+
rects.push(panel);
|
|
4773
|
+
}
|
|
4774
|
+
const buttons = this._controlButtons();
|
|
4775
|
+
if (buttons.length > 0) {
|
|
4776
|
+
rects.push(unionRects(buttons[0].rect, buttons[buttons.length - 1].rect));
|
|
4777
|
+
}
|
|
4778
|
+
return rects.map((r) => ({
|
|
4779
|
+
x: Math.floor(r.x - pane.x) - 1,
|
|
4780
|
+
y: Math.floor(r.y - pane.y) - 1,
|
|
4781
|
+
width: Math.ceil(r.width) + 2,
|
|
4782
|
+
height: Math.ceil(r.height) + 2,
|
|
4783
|
+
}));
|
|
4784
|
+
}
|
|
4785
|
+
|
|
4786
|
+
/** Paint the minimap and the controls into `ctx`, a canvas whose place in
|
|
4787
|
+
* the window, in device pixels, is `abs` — as the scene paints them. */
|
|
4788
|
+
paintPanels(ctx: unknown, abs: FlowRect): void {
|
|
4789
|
+
const painter = createPainter(ctx, this._textOptions());
|
|
4790
|
+
if (!painter) return;
|
|
4791
|
+
// Only the panels this canvas's box reaches: the minimap's canvas and
|
|
4792
|
+
// the controls' each built and painted both, which for the controls
|
|
4793
|
+
// was every node in the graph, clipped away. The frame clip is set to
|
|
4794
|
+
// the box too, for the minimap's own reach test — a canvas paints
|
|
4795
|
+
// outside this element's paint, where the clip is the last pass's.
|
|
4796
|
+
const bounds = abs.width > 0 && abs.height > 0 ? this._logical(abs) : null;
|
|
4797
|
+
const clip = this._frameClip;
|
|
4798
|
+
this._frameClip = bounds;
|
|
4799
|
+
let scene: FlowScene;
|
|
4800
|
+
try {
|
|
4801
|
+
scene = buildScene({
|
|
4802
|
+
...this._sceneInput(this._palette(), 'overlay', true),
|
|
4803
|
+
clip: bounds,
|
|
2888
4804
|
});
|
|
4805
|
+
} finally {
|
|
4806
|
+
this._frameClip = clip;
|
|
2889
4807
|
}
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
)
|
|
4808
|
+
const c = ctx as {
|
|
4809
|
+
save(): void;
|
|
4810
|
+
restore(): void;
|
|
4811
|
+
translate(x: number, y: number): void;
|
|
4812
|
+
};
|
|
4813
|
+
c.save();
|
|
4814
|
+
try {
|
|
4815
|
+
c.translate(-abs.x, -abs.y);
|
|
4816
|
+
paintPanels(painter, scene);
|
|
4817
|
+
} finally {
|
|
4818
|
+
c.restore();
|
|
4819
|
+
}
|
|
4820
|
+
}
|
|
4821
|
+
|
|
4822
|
+
/** Tell `<Flow>` where the panels are when that changed (`onPanels`). */
|
|
4823
|
+
private _emitPanels(): void {
|
|
4824
|
+
const notify = this._prop<(rects: readonly FlowRect[]) => void>('onPanels');
|
|
4825
|
+
if (!notify) return;
|
|
4826
|
+
const rects = this.panelRects();
|
|
4827
|
+
const key = rects
|
|
4828
|
+
.map((r) => `${r.x},${r.y},${r.width},${r.height}`)
|
|
2898
4829
|
.join('|');
|
|
2899
|
-
if (key === this.
|
|
2900
|
-
this.
|
|
2901
|
-
notify(
|
|
4830
|
+
if (key === this._panelsKey) return;
|
|
4831
|
+
this._panelsKey = key;
|
|
4832
|
+
notify(rects);
|
|
2902
4833
|
}
|
|
2903
4834
|
|
|
2904
|
-
/**
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
region: FlowRect,
|
|
2911
|
-
): void {
|
|
2912
|
-
const options = normalizeBackground(
|
|
2913
|
-
this.props.background as BackgroundOptions | string | boolean | undefined,
|
|
2914
|
-
);
|
|
2915
|
-
if (options.variant === 'none') return;
|
|
2916
|
-
const v = this._viewport();
|
|
2917
|
-
let step = options.gap * v.zoom;
|
|
2918
|
-
if (!(step > 0)) return;
|
|
2919
|
-
// Doubling rather than clamping keeps the grid *aligned* to the graph
|
|
2920
|
-
// while zooming out: every visible line is still a real one.
|
|
2921
|
-
while (step < MIN_GRID_PX) step *= 2;
|
|
2922
|
-
const color = options.color ?? palette.grid;
|
|
2923
|
-
const pane = this._pane();
|
|
2924
|
-
const { x, y, width, height } = region;
|
|
2925
|
-
const originX = pane.x + v.x;
|
|
2926
|
-
const originY = pane.y + v.y;
|
|
2927
|
-
if (this._paintGridPattern(painter, options, step, color, region)) return;
|
|
2928
|
-
const firstX = originX + Math.ceil((x - originX) / step) * step;
|
|
2929
|
-
const firstY = originY + Math.ceil((y - originY) / step) * step;
|
|
2930
|
-
|
|
2931
|
-
if (options.variant === 'lines') {
|
|
2932
|
-
const runs: XYPosition[][] = [];
|
|
2933
|
-
for (let gx = firstX; gx <= x + width; gx += step) {
|
|
2934
|
-
runs.push([
|
|
2935
|
-
{ x: gx, y },
|
|
2936
|
-
{ x: gx, y: y + height },
|
|
2937
|
-
]);
|
|
2938
|
-
}
|
|
2939
|
-
for (let gy = firstY; gy <= y + height; gy += step) {
|
|
2940
|
-
runs.push([
|
|
2941
|
-
{ x, y: gy },
|
|
2942
|
-
{ x: x + width, y: gy },
|
|
2943
|
-
]);
|
|
2944
|
-
}
|
|
2945
|
-
painter.strokeRuns(runs, { stroke: color, lineWidth: 1 });
|
|
2946
|
-
return;
|
|
4835
|
+
/** `adaptive` as a number of milliseconds: `Infinity` never holds. */
|
|
4836
|
+
private _budgetMs(): number {
|
|
4837
|
+
const adaptive = this._prop<boolean | { budgetMs?: number }>('adaptive');
|
|
4838
|
+
if (adaptive === false) return Infinity;
|
|
4839
|
+
if (typeof adaptive === 'object' && adaptive?.budgetMs != null) {
|
|
4840
|
+
return Math.max(0, adaptive.budgetMs);
|
|
2947
4841
|
}
|
|
4842
|
+
return BODY_BUDGET_MS;
|
|
4843
|
+
}
|
|
2948
4844
|
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
]);
|
|
2962
|
-
}
|
|
2963
|
-
}
|
|
2964
|
-
painter.strokeRuns(runs, { stroke: color, lineWidth: 1 });
|
|
2965
|
-
return;
|
|
2966
|
-
}
|
|
4845
|
+
/** Where the mounted bodies stand against the budget — reported with
|
|
4846
|
+
* every frame (`FlowFrameStats.bodies`). */
|
|
4847
|
+
bodyBudget(): FlowBodyBudget {
|
|
4848
|
+
const count = this._bodies.length;
|
|
4849
|
+
return {
|
|
4850
|
+
count,
|
|
4851
|
+
held: this._bodiesHeld,
|
|
4852
|
+
perBodyMs: this._bodyStepMs,
|
|
4853
|
+
predictedMs: count * this._bodyStepMs,
|
|
4854
|
+
budgetMs: this._budgetMs(),
|
|
4855
|
+
};
|
|
4856
|
+
}
|
|
2967
4857
|
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
4858
|
+
/**
|
|
4859
|
+
* A frame was drawn: time the zoom step it carried, into the model
|
|
4860
|
+
* `_holdBodies` predicts from. A step with no body re-scaled is the
|
|
4861
|
+
* baseline; one that re-scaled some says what each cost over it. Passes
|
|
4862
|
+
* a millisecond or two apart are one frame (the 2D renderer paints a
|
|
4863
|
+
* frame's damage as several), and a gap long enough to be the user
|
|
4864
|
+
* pausing is not a step at all.
|
|
4865
|
+
*/
|
|
4866
|
+
private _frameTick(): void {
|
|
4867
|
+
const t = now();
|
|
4868
|
+
// the first frame to draw the latest zoom step, and when: the GL
|
|
4869
|
+
// renderer's is over as it starts — the bodies' cost is the window's
|
|
4870
|
+
// flush before it — and the 2D one's ends with its last pass (`paint`)
|
|
4871
|
+
if (this._zoomFrameAt === -Infinity && this._zoomAt !== -Infinity) {
|
|
4872
|
+
this._zoomFrameAt = t;
|
|
4873
|
+
this._zoomDrawnAt = t;
|
|
4874
|
+
}
|
|
4875
|
+
const dt = t - this._lastFrameAt;
|
|
4876
|
+
if (dt < 2) return;
|
|
4877
|
+
this._lastFrameAt = t;
|
|
4878
|
+
const step = this._zoomStep;
|
|
4879
|
+
this._zoomStep = null;
|
|
4880
|
+
if (!step || dt > 250) return;
|
|
4881
|
+
if (!step.live || step.bodies === 0) {
|
|
4882
|
+
this._baseStepMs = Number.isNaN(this._baseStepMs)
|
|
4883
|
+
? dt
|
|
4884
|
+
: this._baseStepMs * 0.7 + dt * 0.3;
|
|
4885
|
+
} else if (!Number.isNaN(this._baseStepMs)) {
|
|
4886
|
+
const each = Math.max(0, dt - this._baseStepMs) / step.bodies;
|
|
4887
|
+
this._bodyStepMs = this._bodyStepMs * 0.7 + each * 0.3;
|
|
2973
4888
|
}
|
|
2974
|
-
painter.dots(
|
|
2975
|
-
centres,
|
|
2976
|
-
Math.max(1, Math.round(options.size * 2 * v.zoom)),
|
|
2977
|
-
color,
|
|
2978
|
-
);
|
|
2979
4889
|
}
|
|
2980
4890
|
|
|
2981
4891
|
/**
|
|
2982
4892
|
* The grid as one repeating fill (ntk#263): a step-sized tile drawn once,
|
|
2983
4893
|
* a `createPattern('repeat')` over it, and one composite for the whole
|
|
2984
|
-
* region — where the runs path pays a region-sized
|
|
4894
|
+
* region — where the runs path in `./paint.ts` pays a region-sized
|
|
4895
|
+
* coverage mask.
|
|
2985
4896
|
*
|
|
2986
|
-
*
|
|
4897
|
+
* Handed to `paintScene` as its grid painter, and answering **false** when
|
|
4898
|
+
* it does not apply, so the universal path takes over. Two decisions with
|
|
4899
|
+
* reasons:
|
|
2987
4900
|
*
|
|
2988
4901
|
* - **Integral device steps only.** The tile is a pixmap, so its size is
|
|
2989
|
-
* whole pixels; at a fractional step the pattern would drift against
|
|
2990
|
-
*
|
|
2991
|
-
*
|
|
2992
|
-
*
|
|
4902
|
+
* whole pixels; at a fractional step the pattern would drift against the
|
|
4903
|
+
* graph's own coordinates — against `snapToGrid`, against node positions
|
|
4904
|
+
* — by a fraction per tile, and a grid that slides under the content it
|
|
4905
|
+
* grids is worse than a slower exact one.
|
|
2993
4906
|
* - **The phase lives in the tile, not in a picture transform.** An
|
|
2994
4907
|
* untransformed repeat is anchored to the window origin, so the grid's
|
|
2995
4908
|
* alignment is baked in by drawing the mark at `origin mod tile` — and
|
|
2996
4909
|
* re-baking when the origin moves. A `setTransform` translate says the
|
|
2997
4910
|
* same thing in one request, but a *transformed* repeat forfeits the
|
|
2998
|
-
* server's untransformed fast path — on the in-process test server
|
|
2999
|
-
*
|
|
3000
|
-
*
|
|
4911
|
+
* server's untransformed fast path — on the in-process test server that
|
|
4912
|
+
* was ~180 ms a frame of per-pixel arithmetic, and re-rendering a 24px
|
|
4913
|
+
* tile is a handful of requests on any server.
|
|
4914
|
+
*
|
|
4915
|
+
* An arrow property rather than a method: it is passed as a value, and a
|
|
4916
|
+
* method would arrive with no `this`.
|
|
3001
4917
|
*/
|
|
3002
|
-
private
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
step: number,
|
|
3006
|
-
color: string,
|
|
3007
|
-
region: FlowRect,
|
|
3008
|
-
): boolean {
|
|
4918
|
+
private _grid = (painter: FlowPainter, grid: SceneGrid): boolean => {
|
|
4919
|
+
const options = { variant: grid.variant, size: grid.size };
|
|
4920
|
+
const { step, color, region } = grid;
|
|
3009
4921
|
// The tile is a pixmap on the panel's grid, so the pitch has to be a
|
|
3010
4922
|
// whole number of *device* pixels, and the phase is where the viewport
|
|
3011
4923
|
// origin lands on that grid.
|
|
@@ -3023,8 +4935,8 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
3023
4935
|
const v = this._viewport();
|
|
3024
4936
|
const pane = this._pane();
|
|
3025
4937
|
const mod = (a: number, m: number): number => ((a % m) + m) % m;
|
|
3026
|
-
const px =
|
|
3027
|
-
const py =
|
|
4938
|
+
const px = snapToGrid(mod(toDevice(pane.x + v.x, s), tileSize)) % tileSize;
|
|
4939
|
+
const py = snapToGrid(mod(toDevice(pane.y + v.y, s), tileSize)) % tileSize;
|
|
3028
4940
|
// device pixels per graph unit — what a mark's size is drawn in
|
|
3029
4941
|
const unit = v.zoom * s;
|
|
3030
4942
|
const key = `${options.variant}|${Math.round(options.size * unit * 4)}|${color}|${px},${py}`;
|
|
@@ -3050,7 +4962,7 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
3050
4962
|
const fill = this._device(region);
|
|
3051
4963
|
raw.fillRect?.(fill.x, fill.y, fill.width, fill.height);
|
|
3052
4964
|
return true;
|
|
3053
|
-
}
|
|
4965
|
+
};
|
|
3054
4966
|
|
|
3055
4967
|
private _makeGridSurface(tileSize: number): SurfaceLike | null {
|
|
3056
4968
|
const ctor = (ntk as unknown as { Surface?: SurfaceCtor }).Surface;
|
|
@@ -3078,6 +4990,16 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
3078
4990
|
py: number,
|
|
3079
4991
|
): void {
|
|
3080
4992
|
const t = tile.size;
|
|
4993
|
+
// Every mark on whole pixels. A backend cuts a fractional edge its own
|
|
4994
|
+
// way, and X11 cut a mark's position and its size separately, so a mark
|
|
4995
|
+
// that straddled the tile's edge lost a column: at phase 0 a 3px dot
|
|
4996
|
+
// came out as two pixels a column apart and a 1px line not at all. The
|
|
4997
|
+
// grid changed shape as the view panned, and a pan's blit kept the old
|
|
4998
|
+
// shape where a repaint drew the new one. The phase point `(px, py)` is
|
|
4999
|
+
// a pixel edge; a line and a cross are centred on the pixel before it,
|
|
5000
|
+
// and a dot is `size` pixels ending on it, where the cut had put them.
|
|
5001
|
+
const arm = Math.max(1, Math.floor(Math.max(2, options.size * 3 * unit)));
|
|
5002
|
+
const dot = Math.max(1, Math.round(options.size * 2 * unit));
|
|
3081
5003
|
tile.surface.render((ctx) => {
|
|
3082
5004
|
ctx.clearRect?.(0, 0, t, t);
|
|
3083
5005
|
ctx.fillStyle = color;
|
|
@@ -3086,15 +5008,15 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
3086
5008
|
const cx = px + i * t;
|
|
3087
5009
|
const cy = py + j * t;
|
|
3088
5010
|
if (options.variant === 'lines') {
|
|
3089
|
-
if (j === 0) ctx.fillRect(cx -
|
|
3090
|
-
if (i === 0) ctx.fillRect(0, cy -
|
|
5011
|
+
if (j === 0) ctx.fillRect(cx - 1, 0, 1, t);
|
|
5012
|
+
if (i === 0) ctx.fillRect(0, cy - 1, t, 1);
|
|
3091
5013
|
} else if (options.variant === 'cross') {
|
|
3092
|
-
|
|
3093
|
-
ctx.fillRect(cx -
|
|
3094
|
-
ctx.fillRect(cx - 0.5, cy - arm, 1, arm * 2);
|
|
5014
|
+
ctx.fillRect(cx - 1 - arm, cy - 1, arm * 2 + 1, 1);
|
|
5015
|
+
ctx.fillRect(cx - 1, cy - 1 - arm, 1, arm * 2 + 1);
|
|
3095
5016
|
} else {
|
|
3096
|
-
const
|
|
3097
|
-
|
|
5017
|
+
const x0 = Math.floor(cx - dot / 2);
|
|
5018
|
+
const y0 = Math.floor(cy - dot / 2);
|
|
5019
|
+
ctx.fillRect(x0, y0, dot, dot);
|
|
3098
5020
|
}
|
|
3099
5021
|
}
|
|
3100
5022
|
}
|
|
@@ -3108,686 +5030,4 @@ export class FlowGraphNode extends Node implements FlowInstance {
|
|
|
3108
5030
|
tile.surface.destroy?.();
|
|
3109
5031
|
this._gridTile = null;
|
|
3110
5032
|
}
|
|
3111
|
-
|
|
3112
|
-
/** Draws every visible edge; answers whether any of them is animated, so
|
|
3113
|
-
* the caller knows whether to keep a timer alive. */
|
|
3114
|
-
private _paintEdges(painter: FlowPainter, palette: FlowPalette): boolean {
|
|
3115
|
-
const v = this._viewport();
|
|
3116
|
-
const pane = this._pane();
|
|
3117
|
-
const labels = v.zoom >= LABEL_ZOOM;
|
|
3118
|
-
let animated = false;
|
|
3119
|
-
// Collect, then draw. A stroke has no fast path in ntk — every one is a
|
|
3120
|
-
// mask rasterized in JS, uploaded with `PutImage` and composited — so a
|
|
3121
|
-
// graph of seven hundred edges was seven hundred round trips and about
|
|
3122
|
-
// four megabytes a frame. Bucketed by the style they will be stroked
|
|
3123
|
-
// with, the same graph is one path per distinct pen, which for almost
|
|
3124
|
-
// every graph is one.
|
|
3125
|
-
const strokes = new StrokeBuckets();
|
|
3126
|
-
const markers = new Map<string, MarkerBucket>();
|
|
3127
|
-
const labelChips: { rect: FlowRect; radius: number; fill: string }[] = [];
|
|
3128
|
-
const pendingLabels: {
|
|
3129
|
-
text: string;
|
|
3130
|
-
x: number;
|
|
3131
|
-
y: number;
|
|
3132
|
-
size: number;
|
|
3133
|
-
color: string;
|
|
3134
|
-
}[] = [];
|
|
3135
|
-
|
|
3136
|
-
const clip = this._frameClip;
|
|
3137
|
-
let animBox: FlowRect | null = null;
|
|
3138
|
-
for (const edge of this._edges) {
|
|
3139
|
-
if (edge.hidden) continue;
|
|
3140
|
-
// two rejects: one from the nodes alone, one from the route it took
|
|
3141
|
-
const coarse = this._edgeCoarseBox(edge);
|
|
3142
|
-
if (!coarse || !rectsOverlap(coarse, pane)) continue;
|
|
3143
|
-
// Tracked before the damage skip, deliberately: whether the dash
|
|
3144
|
-
// timer runs is a question about the viewport, not about what this
|
|
3145
|
-
// particular pass repaints — deciding it after the skip is how a drag
|
|
3146
|
-
// in one corner would stop the dash marching in the other. The box
|
|
3147
|
-
// the ticks invalidate is collected below from the *drawn* geometry:
|
|
3148
|
-
// the coarse box carries the bezier slack, and a tick that repaints
|
|
3149
|
-
// slack repaints a card-sized halo of neighbours sixteen times a
|
|
3150
|
-
// second.
|
|
3151
|
-
if (edge.animated) animated = true;
|
|
3152
|
-
if (clip && !rectsOverlap(coarse, clip)) continue;
|
|
3153
|
-
const geometry = this._edgeGeometry(edge);
|
|
3154
|
-
if (!geometry) continue;
|
|
3155
|
-
if (!rectsOverlap(pathBounds(geometry.points), pane)) continue;
|
|
3156
|
-
|
|
3157
|
-
const selected = edge.selected ?? false;
|
|
3158
|
-
const hovered = this._hover.edgeId === edge.id;
|
|
3159
|
-
const stroke =
|
|
3160
|
-
edge.style?.stroke ??
|
|
3161
|
-
(selected
|
|
3162
|
-
? palette.edgeSelected
|
|
3163
|
-
: hovered
|
|
3164
|
-
? palette.text
|
|
3165
|
-
: palette.edge);
|
|
3166
|
-
const lineWidth = Math.max(
|
|
3167
|
-
1,
|
|
3168
|
-
(edge.style?.strokeWidth ?? (selected ? 2 : 1.5)) * v.zoom,
|
|
3169
|
-
);
|
|
3170
|
-
// `markerEnd` left out means an arrow: a directed graph whose edges do
|
|
3171
|
-
// not say which way they point is a set of lines. `null` opts out.
|
|
3172
|
-
const markerEnd = normalizeMarker(
|
|
3173
|
-
edge.markerEnd === undefined ? 'arrowclosed' : edge.markerEnd,
|
|
3174
|
-
);
|
|
3175
|
-
const markerStart = normalizeMarker(edge.markerStart);
|
|
3176
|
-
|
|
3177
|
-
// The tip stops short of the handle rather than at it: the handle dot
|
|
3178
|
-
// is drawn *over* the edges, with the nodes, so an arrow aimed at the
|
|
3179
|
-
// handle's centre is an arrow mostly hidden under a white circle.
|
|
3180
|
-
const inset = v.zoom >= HANDLE_ZOOM ? this._handleRadius() + 1 : 1;
|
|
3181
|
-
const last = geometry.points[geometry.points.length - 1];
|
|
3182
|
-
const outAngle = endAngle(geometry.points);
|
|
3183
|
-
const endTip = {
|
|
3184
|
-
x: last.x - Math.cos(outAngle) * inset,
|
|
3185
|
-
y: last.y - Math.sin(outAngle) * inset,
|
|
3186
|
-
};
|
|
3187
|
-
let points = geometry.points;
|
|
3188
|
-
if (markerEnd) {
|
|
3189
|
-
// and the stroke stops behind the head, so a filled triangle is a
|
|
3190
|
-
// triangle rather than a triangle with a line through it
|
|
3191
|
-
points = trimEnd(
|
|
3192
|
-
points,
|
|
3193
|
-
(markerEnd.size ?? DEFAULT_MARKER_SIZE) * v.zoom * 0.8 + inset,
|
|
3194
|
-
);
|
|
3195
|
-
}
|
|
3196
|
-
const dash =
|
|
3197
|
-
edge.style?.dash ?? (edge.animated ? DEFAULT_DASH : undefined);
|
|
3198
|
-
if (edge.animated) {
|
|
3199
|
-
const tight = inflateRect(pathBounds(geometry.points), CULL_MARGIN);
|
|
3200
|
-
animBox = animBox ? unionRects(animBox, tight) : tight;
|
|
3201
|
-
}
|
|
3202
|
-
strokes
|
|
3203
|
-
.bucket(
|
|
3204
|
-
stroke,
|
|
3205
|
-
lineWidth,
|
|
3206
|
-
dash,
|
|
3207
|
-
edge.animated ? this._dashPhase : 0,
|
|
3208
|
-
v.zoom,
|
|
3209
|
-
)
|
|
3210
|
-
.push(points);
|
|
3211
|
-
|
|
3212
|
-
if (markerEnd) {
|
|
3213
|
-
this._collectMarker(
|
|
3214
|
-
markers,
|
|
3215
|
-
endTip,
|
|
3216
|
-
outAngle,
|
|
3217
|
-
markerEnd.type,
|
|
3218
|
-
markerEnd.color ?? stroke,
|
|
3219
|
-
(markerEnd.size ?? DEFAULT_MARKER_SIZE) * v.zoom,
|
|
3220
|
-
lineWidth,
|
|
3221
|
-
);
|
|
3222
|
-
}
|
|
3223
|
-
if (markerStart) {
|
|
3224
|
-
const inAngle = startAngle(geometry.points);
|
|
3225
|
-
this._collectMarker(
|
|
3226
|
-
markers,
|
|
3227
|
-
{
|
|
3228
|
-
x: geometry.points[0].x - Math.cos(inAngle) * inset,
|
|
3229
|
-
y: geometry.points[0].y - Math.sin(inAngle) * inset,
|
|
3230
|
-
},
|
|
3231
|
-
inAngle,
|
|
3232
|
-
markerStart.type,
|
|
3233
|
-
markerStart.color ?? stroke,
|
|
3234
|
-
(markerStart.size ?? DEFAULT_MARKER_SIZE) * v.zoom,
|
|
3235
|
-
lineWidth,
|
|
3236
|
-
);
|
|
3237
|
-
}
|
|
3238
|
-
|
|
3239
|
-
if (labels && edge.label) {
|
|
3240
|
-
const at = pointAtFraction(geometry.points, 0.5);
|
|
3241
|
-
const size = Math.max(8, 11 * v.zoom);
|
|
3242
|
-
const metrics = painter.measureText(edge.label, { size });
|
|
3243
|
-
const padX = 5 * v.zoom;
|
|
3244
|
-
const padY = 2 * v.zoom;
|
|
3245
|
-
// The chip is collected with the rest; the text is not, because a
|
|
3246
|
-
// glyph run is already one request and nothing is gained by holding
|
|
3247
|
-
// it. Both still land above every edge, which is the point of the
|
|
3248
|
-
// chip.
|
|
3249
|
-
labelChips.push({
|
|
3250
|
-
rect: {
|
|
3251
|
-
x: Math.round(at.x - metrics.width / 2 - padX),
|
|
3252
|
-
y: Math.round(at.y - metrics.height / 2 - padY),
|
|
3253
|
-
width: Math.round(metrics.width + padX * 2),
|
|
3254
|
-
height: Math.round(metrics.height + padY * 2),
|
|
3255
|
-
},
|
|
3256
|
-
radius: Math.max(2, Math.round(3 * v.zoom)),
|
|
3257
|
-
fill: edge.style?.labelBackground ?? tint(palette.background, 0.92),
|
|
3258
|
-
});
|
|
3259
|
-
pendingLabels.push({
|
|
3260
|
-
text: edge.label,
|
|
3261
|
-
x: at.x,
|
|
3262
|
-
y: at.y,
|
|
3263
|
-
size,
|
|
3264
|
-
color: edge.style?.labelColor ?? palette.text,
|
|
3265
|
-
});
|
|
3266
|
-
}
|
|
3267
|
-
}
|
|
3268
|
-
|
|
3269
|
-
strokes.paint(painter);
|
|
3270
|
-
for (const bucket of markers.values()) {
|
|
3271
|
-
// the same threshold, for the same reason: a handful of arrowheads
|
|
3272
|
-
// scattered over the pane is cheaper drawn as a handful
|
|
3273
|
-
if (bucket.filled.length >= MARKER_BATCH_MIN) {
|
|
3274
|
-
painter.polygons(bucket.filled, { fill: bucket.color });
|
|
3275
|
-
} else {
|
|
3276
|
-
for (const head of bucket.filled) {
|
|
3277
|
-
painter.polygon(head, { fill: bucket.color });
|
|
3278
|
-
}
|
|
3279
|
-
}
|
|
3280
|
-
if (bucket.open.length >= MARKER_BATCH_MIN) {
|
|
3281
|
-
painter.strokeRuns(bucket.open, {
|
|
3282
|
-
stroke: bucket.color,
|
|
3283
|
-
lineWidth: bucket.lineWidth,
|
|
3284
|
-
});
|
|
3285
|
-
} else {
|
|
3286
|
-
for (const head of bucket.open) {
|
|
3287
|
-
painter.strokeRuns([head], {
|
|
3288
|
-
stroke: bucket.color,
|
|
3289
|
-
lineWidth: bucket.lineWidth,
|
|
3290
|
-
});
|
|
3291
|
-
}
|
|
3292
|
-
}
|
|
3293
|
-
}
|
|
3294
|
-
for (const chip of labelChips) {
|
|
3295
|
-
painter.rect(
|
|
3296
|
-
chip.rect.x,
|
|
3297
|
-
chip.rect.y,
|
|
3298
|
-
chip.rect.width,
|
|
3299
|
-
chip.rect.height,
|
|
3300
|
-
chip.radius,
|
|
3301
|
-
{ fill: chip.fill },
|
|
3302
|
-
);
|
|
3303
|
-
}
|
|
3304
|
-
for (const l of pendingLabels) {
|
|
3305
|
-
painter.text(l.text, l.x, l.y, {
|
|
3306
|
-
size: l.size,
|
|
3307
|
-
color: l.color,
|
|
3308
|
-
align: 'center',
|
|
3309
|
-
baseline: 'middle',
|
|
3310
|
-
});
|
|
3311
|
-
}
|
|
3312
|
-
// A pass that drew an animated edge knows exactly where its dash is; a
|
|
3313
|
-
// pass that culled them all keeps the previous box — the endpoints did
|
|
3314
|
-
// not move, or the move's own damage would have redrawn them here.
|
|
3315
|
-
if (animBox) this._animBox = animBox;
|
|
3316
|
-
return animated;
|
|
3317
|
-
}
|
|
3318
|
-
|
|
3319
|
-
/** An arrowhead's three or four points, added to the pile for its colour
|
|
3320
|
-
* rather than drawn — see {@link StrokeBuckets} for why. */
|
|
3321
|
-
private _collectMarker(
|
|
3322
|
-
markers: Map<string, MarkerBucket>,
|
|
3323
|
-
at: XYPosition,
|
|
3324
|
-
angle: number,
|
|
3325
|
-
type: 'arrow' | 'arrowclosed',
|
|
3326
|
-
color: string,
|
|
3327
|
-
size: number,
|
|
3328
|
-
lineWidth: number,
|
|
3329
|
-
): void {
|
|
3330
|
-
const spread = 0.42; // radians off the shaft — a ~24° half-angle
|
|
3331
|
-
const back = {
|
|
3332
|
-
x: at.x - Math.cos(angle) * size,
|
|
3333
|
-
y: at.y - Math.sin(angle) * size,
|
|
3334
|
-
};
|
|
3335
|
-
const left = {
|
|
3336
|
-
x: at.x - Math.cos(angle - spread) * size,
|
|
3337
|
-
y: at.y - Math.sin(angle - spread) * size,
|
|
3338
|
-
};
|
|
3339
|
-
const right = {
|
|
3340
|
-
x: at.x - Math.cos(angle + spread) * size,
|
|
3341
|
-
y: at.y - Math.sin(angle + spread) * size,
|
|
3342
|
-
};
|
|
3343
|
-
const key = `${color}|${lineWidth}`;
|
|
3344
|
-
let bucket = markers.get(key);
|
|
3345
|
-
if (!bucket) {
|
|
3346
|
-
bucket = { color, lineWidth, filled: [], open: [] };
|
|
3347
|
-
markers.set(key, bucket);
|
|
3348
|
-
}
|
|
3349
|
-
if (type === 'arrowclosed') bucket.filled.push([at, left, back, right]);
|
|
3350
|
-
else bucket.open.push([left, at, right]);
|
|
3351
|
-
}
|
|
3352
|
-
|
|
3353
|
-
private _paintNodes(painter: FlowPainter, palette: FlowPalette): void {
|
|
3354
|
-
const dragging = this._dragTo;
|
|
3355
|
-
const order: NodeEntry[] = [];
|
|
3356
|
-
const deferred: NodeEntry[] = [];
|
|
3357
|
-
for (const entry of this._order) {
|
|
3358
|
-
// a dragged node comes to the top
|
|
3359
|
-
if (dragging?.has(entry.node.id)) deferred.push(entry);
|
|
3360
|
-
else order.push(entry);
|
|
3361
|
-
}
|
|
3362
|
-
order.push(...deferred);
|
|
3363
|
-
|
|
3364
|
-
// Nodes are drawn one at a time, in z-order, and both halves of that
|
|
3365
|
-
// were measured rather than assumed — see `_paintHandles` for why
|
|
3366
|
-
// batching a card or a handle costs more than it saves.
|
|
3367
|
-
for (const entry of order) this._paintNode(painter, palette, entry);
|
|
3368
|
-
}
|
|
3369
|
-
|
|
3370
|
-
private _paintNode(
|
|
3371
|
-
painter: FlowPainter,
|
|
3372
|
-
palette: FlowPalette,
|
|
3373
|
-
entry: NodeEntry,
|
|
3374
|
-
): void {
|
|
3375
|
-
if (entry.node.hidden) return;
|
|
3376
|
-
const rect = this._screenRect(entry);
|
|
3377
|
-
if (!rectsOverlap(rect, this._pane())) return;
|
|
3378
|
-
// inflated by the margin its handles and grips can ink outside the box —
|
|
3379
|
-
// the same margin every invalidate grew by, so the two agree
|
|
3380
|
-
const clip = this._frameClip;
|
|
3381
|
-
if (clip && !rectsOverlap(inflateRect(rect, CULL_MARGIN), clip)) return;
|
|
3382
|
-
const v = this._viewport();
|
|
3383
|
-
const selected = entry.node.selected ?? false;
|
|
3384
|
-
const hovered = this._hover.nodeId === entry.node.id;
|
|
3385
|
-
const handles = this._handlesOf(entry).map((anchor) => {
|
|
3386
|
-
const s = this._toScreen(anchor);
|
|
3387
|
-
return { ...anchor, x: s.x, y: s.y };
|
|
3388
|
-
});
|
|
3389
|
-
|
|
3390
|
-
if (entry.type?.paint) {
|
|
3391
|
-
// Never batched: a type that draws its own body is drawing whatever it
|
|
3392
|
-
// likes, in an order only it knows.
|
|
3393
|
-
entry.type.paint({
|
|
3394
|
-
node: entry.node,
|
|
3395
|
-
rect,
|
|
3396
|
-
zoom: v.zoom,
|
|
3397
|
-
selected,
|
|
3398
|
-
hovered,
|
|
3399
|
-
palette,
|
|
3400
|
-
painter,
|
|
3401
|
-
handles,
|
|
3402
|
-
});
|
|
3403
|
-
} else {
|
|
3404
|
-
this._paintCardShape(painter, palette, entry, rect, selected, hovered);
|
|
3405
|
-
this._paintCardInk(painter, palette, entry, rect);
|
|
3406
|
-
}
|
|
3407
|
-
|
|
3408
|
-
if (this._connectable(entry) && (v.zoom >= HANDLE_ZOOM || hovered)) {
|
|
3409
|
-
this._paintHandles(painter, palette, handles);
|
|
3410
|
-
}
|
|
3411
|
-
this._paintGrips(painter, palette, entry, rect);
|
|
3412
|
-
}
|
|
3413
|
-
|
|
3414
|
-
private _mounted(entry: NodeEntry): boolean {
|
|
3415
|
-
return entry.type?.render != null && this._viewport().zoom >= RENDER_ZOOM;
|
|
3416
|
-
}
|
|
3417
|
-
|
|
3418
|
-
private _paintGrips(
|
|
3419
|
-
painter: FlowPainter,
|
|
3420
|
-
palette: FlowPalette,
|
|
3421
|
-
entry: NodeEntry,
|
|
3422
|
-
rect: FlowRect,
|
|
3423
|
-
): void {
|
|
3424
|
-
const grips = this._grips(entry);
|
|
3425
|
-
if (grips.length === 0) return;
|
|
3426
|
-
const size = Math.max(4, RESIZE_GRIP * 2 * this._viewport().zoom);
|
|
3427
|
-
for (const dir of grips) {
|
|
3428
|
-
const at = gripPoint(rect, dir);
|
|
3429
|
-
painter.rect(at.x - size / 2, at.y - size / 2, size, size, 1, {
|
|
3430
|
-
fill: palette.nodeBackground,
|
|
3431
|
-
stroke: palette.accent,
|
|
3432
|
-
lineWidth: 1.5,
|
|
3433
|
-
});
|
|
3434
|
-
}
|
|
3435
|
-
}
|
|
3436
|
-
|
|
3437
|
-
/** The built-in node: a card with a label, an optional second line and an
|
|
3438
|
-
* optional accent stripe down its leading edge. */
|
|
3439
|
-
/** The card itself: one rounded box, and the accent stripe if it has one.
|
|
3440
|
-
* With a `batch` the box joins the pile for its appearance instead of
|
|
3441
|
-
* being drawn on its own. */
|
|
3442
|
-
private _paintCardShape(
|
|
3443
|
-
painter: FlowPainter,
|
|
3444
|
-
palette: FlowPalette,
|
|
3445
|
-
entry: NodeEntry,
|
|
3446
|
-
rect: FlowRect,
|
|
3447
|
-
selected: boolean,
|
|
3448
|
-
hovered: boolean,
|
|
3449
|
-
): void {
|
|
3450
|
-
const v = this._viewport();
|
|
3451
|
-
const style = entry.node.style;
|
|
3452
|
-
const radius = Math.round((style?.borderRadius ?? 6) * v.zoom);
|
|
3453
|
-
const border = selected
|
|
3454
|
-
? palette.accent
|
|
3455
|
-
: hovered
|
|
3456
|
-
? tint(palette.accent, 0.55)
|
|
3457
|
-
: (style?.borderColor ?? palette.nodeBorder);
|
|
3458
|
-
const options: ShapeOptions = {
|
|
3459
|
-
fill: style?.background ?? palette.nodeBackground,
|
|
3460
|
-
stroke: border,
|
|
3461
|
-
lineWidth: Math.max(
|
|
3462
|
-
1,
|
|
3463
|
-
Math.round((style?.borderWidth ?? (selected ? 2 : 1)) * v.zoom),
|
|
3464
|
-
),
|
|
3465
|
-
};
|
|
3466
|
-
painter.rect(rect.x, rect.y, rect.width, rect.height, radius, options);
|
|
3467
|
-
|
|
3468
|
-
if (style?.accent) {
|
|
3469
|
-
// Inset past the rounded corners instead of clipped to them: a
|
|
3470
|
-
// non-rectangular clip forfeits ntk's rounded-box fast path for every
|
|
3471
|
-
// fill under it — measured as a pixmap create/free and a trapezoid
|
|
3472
|
-
// pass per card per repaint.
|
|
3473
|
-
painter.rect(
|
|
3474
|
-
rect.x + Math.max(1, v.zoom),
|
|
3475
|
-
rect.y + radius,
|
|
3476
|
-
Math.max(2, 3 * v.zoom),
|
|
3477
|
-
rect.height - radius * 2,
|
|
3478
|
-
0,
|
|
3479
|
-
{ fill: style.accent },
|
|
3480
|
-
);
|
|
3481
|
-
}
|
|
3482
|
-
}
|
|
3483
|
-
|
|
3484
|
-
/** The label and its second line — the half of a card that has to come
|
|
3485
|
-
* after every card when they are batched. */
|
|
3486
|
-
private _paintCardInk(
|
|
3487
|
-
painter: FlowPainter,
|
|
3488
|
-
palette: FlowPalette,
|
|
3489
|
-
entry: NodeEntry,
|
|
3490
|
-
rect: FlowRect,
|
|
3491
|
-
): void {
|
|
3492
|
-
const v = this._viewport();
|
|
3493
|
-
if (v.zoom < LABEL_ZOOM) return;
|
|
3494
|
-
const style = entry.node.style;
|
|
3495
|
-
const header = this._mounted(entry) ? this._headerHeight(entry) : 0;
|
|
3496
|
-
const data = entry.node.data as FlowNodeData | undefined;
|
|
3497
|
-
const label = data?.label ?? entry.node.id;
|
|
3498
|
-
const description = data?.description;
|
|
3499
|
-
const color = style?.color ?? palette.text;
|
|
3500
|
-
const padX = NODE_PAD_X * v.zoom;
|
|
3501
|
-
const centre = rect.x + rect.width / 2;
|
|
3502
|
-
const maxWidth = Math.max(8, rect.width - padX * 2);
|
|
3503
|
-
const showDescription = Boolean(description) && v.zoom >= DESC_ZOOM;
|
|
3504
|
-
|
|
3505
|
-
if (header > 0) {
|
|
3506
|
-
// The title bar of a node whose body is somebody else's: left-aligned,
|
|
3507
|
-
// with a hairline under it so the strip reads as the thing to grab.
|
|
3508
|
-
const band = header * v.zoom;
|
|
3509
|
-
painter.text(label, rect.x + padX, rect.y + band / 2, {
|
|
3510
|
-
size: NODE_LABEL_SIZE * v.zoom,
|
|
3511
|
-
weight: 'bold',
|
|
3512
|
-
color,
|
|
3513
|
-
baseline: 'middle',
|
|
3514
|
-
maxWidth,
|
|
3515
|
-
});
|
|
3516
|
-
painter.strokeRuns(
|
|
3517
|
-
[
|
|
3518
|
-
[
|
|
3519
|
-
{ x: rect.x + 1, y: rect.y + band },
|
|
3520
|
-
{ x: rect.x + rect.width - 1, y: rect.y + band },
|
|
3521
|
-
],
|
|
3522
|
-
],
|
|
3523
|
-
{ stroke: palette.nodeBorder, lineWidth: 1 },
|
|
3524
|
-
);
|
|
3525
|
-
return;
|
|
3526
|
-
}
|
|
3527
|
-
|
|
3528
|
-
if (!showDescription) {
|
|
3529
|
-
painter.text(label, centre, rect.y + rect.height / 2, {
|
|
3530
|
-
size: NODE_LABEL_SIZE * v.zoom,
|
|
3531
|
-
weight: 'bold',
|
|
3532
|
-
color,
|
|
3533
|
-
align: 'center',
|
|
3534
|
-
baseline: 'middle',
|
|
3535
|
-
maxWidth,
|
|
3536
|
-
});
|
|
3537
|
-
return;
|
|
3538
|
-
}
|
|
3539
|
-
const top = rect.y + NODE_PAD_Y * v.zoom;
|
|
3540
|
-
painter.text(label, centre, top, {
|
|
3541
|
-
size: NODE_LABEL_SIZE * v.zoom,
|
|
3542
|
-
weight: 'bold',
|
|
3543
|
-
color,
|
|
3544
|
-
align: 'center',
|
|
3545
|
-
maxWidth,
|
|
3546
|
-
});
|
|
3547
|
-
painter.text(description!, centre, top + NODE_LABEL_SIZE * 1.35 * v.zoom, {
|
|
3548
|
-
size: NODE_DESC_SIZE * v.zoom,
|
|
3549
|
-
color: palette.dim,
|
|
3550
|
-
align: 'center',
|
|
3551
|
-
maxWidth,
|
|
3552
|
-
});
|
|
3553
|
-
}
|
|
3554
|
-
|
|
3555
|
-
/**
|
|
3556
|
-
* Handles are drawn one disc at a time, and that is the measured answer
|
|
3557
|
-
* rather than the obvious one.
|
|
3558
|
-
*
|
|
3559
|
-
* Batching them into a single path halved the requests and made the frame
|
|
3560
|
-
* *slower*: a path's mask is its bounding box, so forty dots scattered
|
|
3561
|
-
* across the pane rasterize to a paneful of mask — three quarters of a
|
|
3562
|
-
* megabyte — where forty small ones cost a kilobyte each. Batching pays
|
|
3563
|
-
* for the edges because an edge already spans that box; it does not pay
|
|
3564
|
-
* for anything small and scattered.
|
|
3565
|
-
*/
|
|
3566
|
-
private _paintHandles(
|
|
3567
|
-
painter: FlowPainter,
|
|
3568
|
-
palette: FlowPalette,
|
|
3569
|
-
handles: readonly HandleAnchor[],
|
|
3570
|
-
): void {
|
|
3571
|
-
const v = this._viewport();
|
|
3572
|
-
const radius = this._handleRadius();
|
|
3573
|
-
const gesture = this._gesture;
|
|
3574
|
-
const connecting = gesture?.kind === 'connect' ? gesture : null;
|
|
3575
|
-
const lineWidth = Math.max(1, 1.5 * v.zoom);
|
|
3576
|
-
for (const anchor of handles) {
|
|
3577
|
-
const active =
|
|
3578
|
-
sameHandle(this._hover.handle, anchor) ||
|
|
3579
|
-
sameHandle(connecting?.to ?? null, anchor) ||
|
|
3580
|
-
sameHandle(connecting?.from ?? null, anchor);
|
|
3581
|
-
const options: ShapeOptions = {
|
|
3582
|
-
fill: active ? palette.accent : palette.nodeBackground,
|
|
3583
|
-
stroke: active ? palette.accent : palette.handle,
|
|
3584
|
-
lineWidth,
|
|
3585
|
-
};
|
|
3586
|
-
painter.circle(
|
|
3587
|
-
anchor.x,
|
|
3588
|
-
anchor.y,
|
|
3589
|
-
active ? radius * 1.4 : radius,
|
|
3590
|
-
options,
|
|
3591
|
-
);
|
|
3592
|
-
if (anchor.label && v.zoom >= DESC_ZOOM) {
|
|
3593
|
-
const outside = anchor.position === 'left' || anchor.position === 'top';
|
|
3594
|
-
painter.text(
|
|
3595
|
-
anchor.label,
|
|
3596
|
-
anchor.x +
|
|
3597
|
-
(anchor.position === 'left'
|
|
3598
|
-
? -radius - 4 * v.zoom
|
|
3599
|
-
: radius + 4 * v.zoom),
|
|
3600
|
-
anchor.y,
|
|
3601
|
-
{
|
|
3602
|
-
size: 10 * v.zoom,
|
|
3603
|
-
color: palette.dim,
|
|
3604
|
-
align: outside ? 'right' : 'left',
|
|
3605
|
-
baseline: 'middle',
|
|
3606
|
-
},
|
|
3607
|
-
);
|
|
3608
|
-
}
|
|
3609
|
-
}
|
|
3610
|
-
}
|
|
3611
|
-
|
|
3612
|
-
private _paintConnection(painter: FlowPainter, palette: FlowPalette): void {
|
|
3613
|
-
const gesture = this._gesture;
|
|
3614
|
-
if (gesture?.kind !== 'connect') return;
|
|
3615
|
-
const v = this._viewport();
|
|
3616
|
-
const target = gesture.to ?? gesture.pointer;
|
|
3617
|
-
const to = this._toScreen(target);
|
|
3618
|
-
const points = this._connectionPath(gesture);
|
|
3619
|
-
const invalid = gesture.to != null && !gesture.valid;
|
|
3620
|
-
painter.polyline(points, {
|
|
3621
|
-
stroke: invalid ? palette.dim : palette.accent,
|
|
3622
|
-
lineWidth: Math.max(1.5, 2 * v.zoom),
|
|
3623
|
-
dash: gesture.to && gesture.valid ? undefined : [5 * v.zoom, 4 * v.zoom],
|
|
3624
|
-
});
|
|
3625
|
-
painter.circle(to.x, to.y, this._handleRadius(), {
|
|
3626
|
-
fill: invalid ? palette.dim : palette.accent,
|
|
3627
|
-
});
|
|
3628
|
-
}
|
|
3629
|
-
|
|
3630
|
-
private _paintSelectionBox(painter: FlowPainter, palette: FlowPalette): void {
|
|
3631
|
-
const gesture = this._gesture;
|
|
3632
|
-
if (gesture?.kind !== 'select') return;
|
|
3633
|
-
const x = Math.min(gesture.startX, gesture.x);
|
|
3634
|
-
const y = Math.min(gesture.startY, gesture.y);
|
|
3635
|
-
painter.rect(
|
|
3636
|
-
x,
|
|
3637
|
-
y,
|
|
3638
|
-
Math.abs(gesture.x - gesture.startX),
|
|
3639
|
-
Math.abs(gesture.y - gesture.startY),
|
|
3640
|
-
2,
|
|
3641
|
-
{ fill: palette.selection, stroke: palette.accent, lineWidth: 1 },
|
|
3642
|
-
);
|
|
3643
|
-
}
|
|
3644
|
-
|
|
3645
|
-
private _paintMiniMap(painter: FlowPainter, palette: FlowPalette): void {
|
|
3646
|
-
const opts = this._miniMapOptions();
|
|
3647
|
-
if (!opts) return;
|
|
3648
|
-
// The panel cull comes before `_miniMap()`, which walks every node to
|
|
3649
|
-
// find the graph's bounds — during a drag that walk per pass would cost
|
|
3650
|
-
// more than the panel it skips. The dot for a mid-drag node goes stale
|
|
3651
|
-
// until the release repaints in full; that is the trade, and it is
|
|
3652
|
-
// deliberate.
|
|
3653
|
-
const clip = this._frameClip;
|
|
3654
|
-
if (clip) {
|
|
3655
|
-
const quick = this._corner(
|
|
3656
|
-
opts.position,
|
|
3657
|
-
opts.width ?? MINIMAP_W,
|
|
3658
|
-
opts.height ?? MINIMAP_H,
|
|
3659
|
-
'bottom-right',
|
|
3660
|
-
);
|
|
3661
|
-
if (!rectsOverlap(quick, clip)) return;
|
|
3662
|
-
}
|
|
3663
|
-
const map = this._miniMap();
|
|
3664
|
-
if (!map) return;
|
|
3665
|
-
const { panel, options } = map;
|
|
3666
|
-
painter.rect(panel.x, panel.y, panel.width, panel.height, 4, {
|
|
3667
|
-
fill: tint(palette.surface, 0.92),
|
|
3668
|
-
stroke: palette.surfaceBorder,
|
|
3669
|
-
lineWidth: 1,
|
|
3670
|
-
});
|
|
3671
|
-
painter.save();
|
|
3672
|
-
painter.clipRect(panel.x, panel.y, panel.width, panel.height, 4);
|
|
3673
|
-
|
|
3674
|
-
const nodeColor = options.nodeColor;
|
|
3675
|
-
for (const entry of this._entries) {
|
|
3676
|
-
if (entry.node.hidden) continue;
|
|
3677
|
-
const rect = this.rectOf(entry);
|
|
3678
|
-
const at = this._miniToScreen(map, rect);
|
|
3679
|
-
const fill =
|
|
3680
|
-
typeof nodeColor === 'function'
|
|
3681
|
-
? nodeColor(entry.node as FlowNode<never>)
|
|
3682
|
-
: (nodeColor ??
|
|
3683
|
-
(entry.node.selected ? palette.accent : palette.nodeBorder));
|
|
3684
|
-
painter.rect(
|
|
3685
|
-
at.x,
|
|
3686
|
-
at.y,
|
|
3687
|
-
Math.max(1, rect.width * map.scale),
|
|
3688
|
-
Math.max(1, rect.height * map.scale),
|
|
3689
|
-
0,
|
|
3690
|
-
{ fill },
|
|
3691
|
-
);
|
|
3692
|
-
}
|
|
3693
|
-
|
|
3694
|
-
// The viewport, as an outline over a wash on everything outside it —
|
|
3695
|
-
// cheaper than four mask rectangles and reads the same.
|
|
3696
|
-
const v = this._viewport();
|
|
3697
|
-
const paneRect = this._pane();
|
|
3698
|
-
const view = this._miniToScreen(map, {
|
|
3699
|
-
x: -v.x / v.zoom,
|
|
3700
|
-
y: -v.y / v.zoom,
|
|
3701
|
-
});
|
|
3702
|
-
painter.rect(
|
|
3703
|
-
view.x,
|
|
3704
|
-
view.y,
|
|
3705
|
-
(paneRect.width / v.zoom) * map.scale,
|
|
3706
|
-
(paneRect.height / v.zoom) * map.scale,
|
|
3707
|
-
2,
|
|
3708
|
-
{
|
|
3709
|
-
fill: options.maskColor ?? tint(palette.accent, 0.1),
|
|
3710
|
-
stroke: palette.accent,
|
|
3711
|
-
lineWidth: 1,
|
|
3712
|
-
},
|
|
3713
|
-
);
|
|
3714
|
-
painter.restore();
|
|
3715
|
-
}
|
|
3716
|
-
|
|
3717
|
-
private _paintControls(painter: FlowPainter, palette: FlowPalette): void {
|
|
3718
|
-
const buttons = this._controlButtons();
|
|
3719
|
-
if (buttons.length === 0) return;
|
|
3720
|
-
const clip = this._frameClip;
|
|
3721
|
-
if (clip && !buttons.some((b) => rectsOverlap(b.rect, clip))) return;
|
|
3722
|
-
const first = buttons[0].rect;
|
|
3723
|
-
const last = buttons[buttons.length - 1].rect;
|
|
3724
|
-
painter.rect(
|
|
3725
|
-
first.x,
|
|
3726
|
-
first.y,
|
|
3727
|
-
first.width,
|
|
3728
|
-
last.y + last.height - first.y,
|
|
3729
|
-
4,
|
|
3730
|
-
{
|
|
3731
|
-
fill: tint(palette.surface, 0.94),
|
|
3732
|
-
stroke: palette.surfaceBorder,
|
|
3733
|
-
lineWidth: 1,
|
|
3734
|
-
},
|
|
3735
|
-
);
|
|
3736
|
-
for (let i = 0; i < buttons.length; i++) {
|
|
3737
|
-
const { rect, action } = buttons[i];
|
|
3738
|
-
if (i > 0) {
|
|
3739
|
-
painter.strokeRuns(
|
|
3740
|
-
[
|
|
3741
|
-
[
|
|
3742
|
-
{ x: rect.x + 4, y: rect.y },
|
|
3743
|
-
{ x: rect.x + rect.width - 4, y: rect.y },
|
|
3744
|
-
],
|
|
3745
|
-
],
|
|
3746
|
-
{ stroke: palette.surfaceBorder, lineWidth: 1 },
|
|
3747
|
-
);
|
|
3748
|
-
}
|
|
3749
|
-
const cx = rect.x + rect.width / 2;
|
|
3750
|
-
const cy = rect.y + rect.height / 2;
|
|
3751
|
-
const arm = 5;
|
|
3752
|
-
const runs: XYPosition[][] = [];
|
|
3753
|
-
if (action === 'in' || action === 'out') {
|
|
3754
|
-
runs.push([
|
|
3755
|
-
{ x: cx - arm, y: cy },
|
|
3756
|
-
{ x: cx + arm, y: cy },
|
|
3757
|
-
]);
|
|
3758
|
-
if (action === 'in') {
|
|
3759
|
-
runs.push([
|
|
3760
|
-
{ x: cx, y: cy - arm },
|
|
3761
|
-
{ x: cx, y: cy + arm },
|
|
3762
|
-
]);
|
|
3763
|
-
}
|
|
3764
|
-
} else {
|
|
3765
|
-
// a frame with its sides opened out: "fit what is there into this"
|
|
3766
|
-
const a = 5;
|
|
3767
|
-
runs.push(
|
|
3768
|
-
[
|
|
3769
|
-
{ x: cx - a, y: cy - a + 3 },
|
|
3770
|
-
{ x: cx - a, y: cy - a },
|
|
3771
|
-
{ x: cx - a + 3, y: cy - a },
|
|
3772
|
-
],
|
|
3773
|
-
[
|
|
3774
|
-
{ x: cx + a - 3, y: cy - a },
|
|
3775
|
-
{ x: cx + a, y: cy - a },
|
|
3776
|
-
{ x: cx + a, y: cy - a + 3 },
|
|
3777
|
-
],
|
|
3778
|
-
[
|
|
3779
|
-
{ x: cx + a, y: cy + a - 3 },
|
|
3780
|
-
{ x: cx + a, y: cy + a },
|
|
3781
|
-
{ x: cx + a - 3, y: cy + a },
|
|
3782
|
-
],
|
|
3783
|
-
[
|
|
3784
|
-
{ x: cx - a + 3, y: cy + a },
|
|
3785
|
-
{ x: cx - a, y: cy + a },
|
|
3786
|
-
{ x: cx - a, y: cy + a - 3 },
|
|
3787
|
-
],
|
|
3788
|
-
);
|
|
3789
|
-
}
|
|
3790
|
-
painter.strokeRuns(runs, { stroke: palette.text, lineWidth: 1.5 });
|
|
3791
|
-
}
|
|
3792
|
-
}
|
|
3793
5033
|
}
|