@react-x11/components 0.9.0 → 0.11.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 +10 -4
- package/dist/charts/node.d.ts +11 -1
- package/dist/charts/node.d.ts.map +1 -1
- package/dist/charts/node.js +44 -14
- 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 +27 -24
- 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/index.d.ts +1 -1
- package/dist/code-language/index.d.ts.map +1 -1
- package/dist/code-language/index.js +1 -1
- package/dist/code-language/index.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/runs.d.ts +31 -0
- package/dist/code-language/runs.d.ts.map +1 -1
- package/dist/code-language/runs.js +105 -0
- package/dist/code-language/runs.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 +26 -2
- package/dist/codeblock/index.d.ts.map +1 -1
- package/dist/codeblock/index.js +57 -4
- package/dist/codeblock/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 +262 -6
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +1373 -67
- 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 +501 -15
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +3729 -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 +65 -16
- 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 +3077 -219
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +367 -17
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +1742 -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 +154 -1
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +3598 -175
- 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 +169 -4
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +1429 -137
- 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 +3635 -107
- 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 +442 -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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/codelines.d.ts +42 -0
- package/dist/internal/codelines.d.ts.map +1 -0
- package/dist/internal/codelines.js +170 -0
- package/dist/internal/codelines.js.map +1 -0
- 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/internal/markdown/ast.d.ts +11 -0
- package/dist/internal/markdown/ast.d.ts.map +1 -1
- package/dist/internal/markdown/parse.d.ts.map +1 -1
- package/dist/internal/markdown/parse.js +109 -8
- package/dist/internal/markdown/parse.js.map +1 -1
- 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/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 +6 -1
- package/dist/maps/anchors.d.ts.map +1 -1
- package/dist/maps/anchors.js +260 -1
- package/dist/maps/anchors.js.map +1 -1
- package/dist/maps/controller.d.ts +57 -0
- package/dist/maps/controller.d.ts.map +1 -1
- package/dist/maps/controller.js +97 -18
- package/dist/maps/controller.js.map +1 -1
- package/dist/maps/gl/buckets.d.ts +5 -1
- package/dist/maps/gl/buckets.d.ts.map +1 -1
- package/dist/maps/gl/buckets.js +5 -2
- package/dist/maps/gl/buckets.js.map +1 -1
- package/dist/maps/gl/build-worker.d.ts +2 -0
- package/dist/maps/gl/build-worker.d.ts.map +1 -1
- package/dist/maps/gl/build-worker.js +1 -1
- package/dist/maps/gl/build-worker.js.map +1 -1
- package/dist/maps/gl/cover.d.ts +23 -4
- package/dist/maps/gl/cover.d.ts.map +1 -1
- package/dist/maps/gl/cover.js +61 -48
- package/dist/maps/gl/cover.js.map +1 -1
- package/dist/maps/gl/placement.d.ts +21 -8
- package/dist/maps/gl/placement.d.ts.map +1 -1
- package/dist/maps/gl/placement.js +133 -58
- package/dist/maps/gl/placement.js.map +1 -1
- package/dist/maps/gl/renderer.d.ts +36 -4
- package/dist/maps/gl/renderer.d.ts.map +1 -1
- package/dist/maps/gl/renderer.js +81 -24
- package/dist/maps/gl/renderer.js.map +1 -1
- package/dist/maps/gl/shaders.d.ts +18 -17
- package/dist/maps/gl/shaders.d.ts.map +1 -1
- package/dist/maps/gl/shaders.js +34 -32
- package/dist/maps/gl/shaders.js.map +1 -1
- package/dist/maps/gl/store.d.ts +10 -0
- package/dist/maps/gl/store.d.ts.map +1 -1
- package/dist/maps/gl/store.js +47 -4
- package/dist/maps/gl/store.js.map +1 -1
- package/dist/maps/gl/text.d.ts +153 -24
- package/dist/maps/gl/text.d.ts.map +1 -1
- package/dist/maps/gl/text.js +379 -63
- package/dist/maps/gl/text.js.map +1 -1
- package/dist/maps/gl/view.d.ts +293 -2
- package/dist/maps/gl/view.d.ts.map +1 -1
- package/dist/maps/gl/view.js +317 -82
- package/dist/maps/gl/view.js.map +1 -1
- 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 +2 -0
- package/dist/maps/index.d.ts.map +1 -1
- package/dist/maps/index.js +2 -0
- package/dist/maps/index.js.map +1 -1
- package/dist/maps/labels.d.ts +56 -4
- package/dist/maps/labels.d.ts.map +1 -1
- package/dist/maps/labels.js +213 -46
- package/dist/maps/labels.js.map +1 -1
- package/dist/maps/node.d.ts +17 -0
- package/dist/maps/node.d.ts.map +1 -1
- package/dist/maps/node.js +64 -30
- package/dist/maps/node.js.map +1 -1
- package/dist/maps/proj.d.ts +48 -0
- package/dist/maps/proj.d.ts.map +1 -1
- package/dist/maps/proj.js +73 -0
- package/dist/maps/proj.js.map +1 -1
- package/dist/maps/sources.d.ts +5 -3
- package/dist/maps/sources.d.ts.map +1 -1
- package/dist/maps/sources.js +5 -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/tiles.d.ts +31 -11
- package/dist/maps/tiles.d.ts.map +1 -1
- package/dist/maps/tiles.js +61 -27
- package/dist/maps/tiles.js.map +1 -1
- package/dist/maps/types.d.ts +28 -4
- package/dist/maps/types.d.ts.map +1 -1
- package/dist/markdown/index.d.ts.map +1 -1
- package/dist/markdown/index.js +209 -68
- package/dist/markdown/index.js.map +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/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/index.d.ts +0 -11
- package/dist/rich-text-editor/index.d.ts.map +1 -1
- package/dist/rich-text-editor/index.js +38 -9
- package/dist/rich-text-editor/index.js.map +1 -1
- package/dist/rich-text-editor/keymap.d.ts.map +1 -1
- package/dist/rich-text-editor/keymap.js +2 -1
- package/dist/rich-text-editor/keymap.js.map +1 -1
- package/dist/rich-text-editor/keys.d.ts +17 -6
- package/dist/rich-text-editor/keys.d.ts.map +1 -1
- package/dist/rich-text-editor/keys.js +135 -26
- package/dist/rich-text-editor/keys.js.map +1 -1
- package/dist/rich-text-editor/markdown.d.ts.map +1 -1
- package/dist/rich-text-editor/markdown.js +30 -1
- package/dist/rich-text-editor/markdown.js.map +1 -1
- 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/render.d.ts.map +1 -1
- package/dist/rich-text-editor/render.js +28 -10
- package/dist/rich-text-editor/render.js.map +1 -1
- package/dist/rich-text-editor/replace.d.ts +13 -0
- package/dist/rich-text-editor/replace.d.ts.map +1 -0
- package/dist/rich-text-editor/replace.js +64 -0
- package/dist/rich-text-editor/replace.js.map +1 -0
- package/dist/rich-text-editor/toolbar.d.ts.map +1 -1
- package/dist/rich-text-editor/toolbar.js +1 -1
- package/dist/rich-text-editor/toolbar.js.map +1 -1
- package/dist/rich-text-editor/virtual.d.ts.map +1 -1
- package/dist/rich-text-editor/virtual.js +17 -0
- package/dist/rich-text-editor/virtual.js.map +1 -1
- package/dist/richtext/node.d.ts +46 -2
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +187 -4
- 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/index.d.ts.map +1 -1
- package/dist/terminal/index.js +4 -0
- 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 +103 -23
- package/dist/terminal-output/index.js.map +1 -1
- package/dist/terminal-output/runs.d.ts +4 -1
- package/dist/terminal-output/runs.d.ts.map +1 -1
- package/dist/terminal-output/runs.js +5 -1
- package/dist/terminal-output/runs.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 +42 -33
- package/src/charts/node.ts +62 -26
- package/src/charts/render.ts +120 -30
- package/src/code/index.ts +36 -32
- 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/index.ts +1 -1
- 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/runs.ts +111 -1
- package/src/code-language/stream.ts +257 -17
- package/src/code-language/types.ts +5 -4
- package/src/codeblock/index.ts +68 -4
- 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 +1481 -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 +4131 -223
- package/src/html/css/ua.ts +66 -17
- 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 +3560 -208
- package/src/html/layout/boxes.ts +2141 -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 +4187 -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 +1585 -156
- package/src/html/paint.ts +4582 -267
- package/src/html/resources.ts +161 -50
- package/src/html/surfaces.ts +81 -0
- package/src/html/svg.ts +504 -0
- package/src/html/url.ts +86 -0
- package/src/index.ts +2 -0
- package/src/internal/codelines.ts +200 -0
- package/src/internal/collate.ts +12 -0
- package/src/internal/markdown/ast.ts +11 -0
- package/src/internal/markdown/parse.ts +154 -11
- package/src/internal/prewarm.ts +45 -0
- 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 +307 -1
- package/src/maps/controller.ts +116 -31
- package/src/maps/gl/buckets.ts +5 -1
- package/src/maps/gl/build-worker.ts +13 -2
- package/src/maps/gl/cover.ts +79 -44
- package/src/maps/gl/placement.ts +221 -69
- package/src/maps/gl/renderer.ts +104 -20
- package/src/maps/gl/shaders.ts +35 -32
- package/src/maps/gl/store.ts +66 -17
- package/src/maps/gl/text.ts +444 -71
- package/src/maps/gl/view.ts +361 -96
- package/src/maps/icons.ts +183 -0
- package/src/maps/index.ts +4 -0
- package/src/maps/labels.ts +269 -49
- package/src/maps/node.ts +82 -49
- package/src/maps/proj.ts +87 -0
- package/src/maps/sources.ts +5 -3
- package/src/maps/style.ts +32 -0
- package/src/maps/styles.ts +215 -4
- package/src/maps/tiles.ts +64 -31
- package/src/maps/types.ts +28 -4
- package/src/markdown/index.ts +293 -92
- package/src/markdown/spans.ts +5 -0
- 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/index.ts +37 -12
- package/src/rich-text-editor/keymap.ts +1 -1
- package/src/rich-text-editor/keys.ts +135 -26
- package/src/rich-text-editor/markdown.ts +36 -1
- package/src/rich-text-editor/marks.ts +101 -0
- package/src/rich-text-editor/render.ts +49 -12
- package/src/rich-text-editor/replace.ts +85 -0
- package/src/rich-text-editor/toolbar.ts +1 -1
- package/src/rich-text-editor/virtual.ts +25 -0
- package/src/richtext/node.ts +235 -14
- package/src/richtext/runs.ts +23 -12
- package/src/table/index.ts +29 -0
- package/src/table/rows.ts +18 -8
- package/src/terminal/index.ts +4 -0
- package/src/terminal/vt/pty.ts +16 -2
- package/src/terminal-output/index.ts +129 -31
- package/src/terminal-output/runs.ts +16 -2
- package/src/three/canvas.ts +102 -8
- package/src/three/events.ts +49 -9
- package/src/tree/index.ts +19 -0
package/dist/flow/node.js
CHANGED
|
@@ -21,11 +21,12 @@
|
|
|
21
21
|
// `handleWheel`/`handleHover` — with the same veto, checked here.
|
|
22
22
|
import * as ntk from 'react-x11/ntk';
|
|
23
23
|
import { Node } from 'react-x11/node';
|
|
24
|
-
import { tint } from 'react-x11/style';
|
|
25
24
|
import { ctrlChordLetter, keysymOf, XK_BACKSPACE, XK_DELETE, XK_DOWN, XK_ESCAPE, XK_HOME, XK_LEFT, XK_RIGHT, XK_UP, } from 'react-x11/keysyms';
|
|
26
|
-
import { createPainter, measureText, toDevice } from './draw.js';
|
|
27
|
-
import { boundsOf, canConnect, clamp,
|
|
28
|
-
import { distanceToPath,
|
|
25
|
+
import { createPainter, measureText, snapToGrid, toDevice } from './draw.js';
|
|
26
|
+
import { boundsOf, canConnect, clamp, DEFAULT_MAX_ZOOM, DEFAULT_MIN_ZOOM, EDGE_SLOP, fitViewport, HANDLE_RADIUS, HANDLE_SLOP, gripPoint, inflateRect, intersectRects, measureNode, MIN_NODE_HEIGHT, MIN_NODE_WIDTH, NODE_BODY_INSET, NODE_HEADER, normalizeBackground, orientConnection, rectContains, rectsOverlap, RENDER_ZOOM, RESIZE_DIRECTIONS, RESIZE_GRIP, RESIZE_SLOP, resizeRect, resolveHandles, resolvePalette, snapTo, unionRects, ZOOM_STEP, } from './model.js';
|
|
27
|
+
import { distanceToPath, pathBounds } from './paths.js';
|
|
28
|
+
import { paintFloat, paintGraph, paintGraphEdges, paintGraphNodes, paintGround, paintNodeItem, paintPanels, paintScene, } from './paint.js';
|
|
29
|
+
import { anchorsOf, buildNodeItems, buildScene, connectionPath, SceneCache, CULL_MARGIN, edgeCoarseBox, edgeInkBox, withLabelPlate, edgeRoute, batchesEdges, endpoint, HANDLE_ZOOM, MINIMAP_BLEED, miniMapPlace, screenRect, screenViewport, } from './scene.js';
|
|
29
30
|
/** Registration key, `kind` and JSX tag, one string — react-x11 rejects a
|
|
30
31
|
* node whose `kind` is not the name it was registered under, because `kind`
|
|
31
32
|
* is what paint order, the test queries and the DEV style assertion all
|
|
@@ -35,31 +36,55 @@ export const ELEMENT = 'flowgraph';
|
|
|
35
36
|
* global that wandered in would become an implicit `@types/node` dependency
|
|
36
37
|
* a consumer has to satisfy. */
|
|
37
38
|
const timers = globalThis;
|
|
39
|
+
/** How long the zoom has to hold still before mounted bodies come back at
|
|
40
|
+
* the new scale. Wheel notches in a flick land well inside it. */
|
|
41
|
+
const BODY_ZOOM_REST_MS = 150;
|
|
42
|
+
/** How long held bodies wait for the frame that draws a zoom's last step
|
|
43
|
+
* before they come back without it (`_restFrom`). */
|
|
44
|
+
const BODY_REST_CAP_MS = 1000;
|
|
45
|
+
/** What re-scaling the mounted bodies may add to one step of a zoom
|
|
46
|
+
* gesture before they sit the gesture out (`_holdBodies`). */
|
|
47
|
+
const BODY_BUDGET_MS = 8;
|
|
48
|
+
/** What one body costs a zoom step until the pane has measured it: 1.1–1.2
|
|
49
|
+
* ms on an M-series Mac, the 400-widget scene on Cocoa (42 bodies added
|
|
50
|
+
* 49 ms to a 13 ms step, 9 added 10). Learned from then on. */
|
|
51
|
+
const BODY_STEP_PRIOR_MS = 1.2;
|
|
52
|
+
/** Under GL, how long a zoom holds still before the world is rebuilt at it
|
|
53
|
+
* — until then each step draws the world already on the GPU, scaled
|
|
54
|
+
* (`glFrame`) — and how far that scaling may go before a step rebuilds
|
|
55
|
+
* anyway, so a long gesture never magnifies one build by more than this.
|
|
56
|
+
* The 2D renderer's zoom picture keeps to both. */
|
|
57
|
+
const GL_ZOOM_REST_MS = 120;
|
|
58
|
+
const GL_ZOOM_SPAN = 2;
|
|
59
|
+
/** How much of the pane a 2D zoom step may paint live, round the picture it
|
|
60
|
+
* composites — the ring a zoom out uncovers — before it paints a new one. */
|
|
61
|
+
const ZOOM_SHOT_LIVE = 0.5;
|
|
62
|
+
/**
|
|
63
|
+
* What painting the graph whole has to cost, ms, before a zoom paints a
|
|
64
|
+
* picture of it and composites that (`_zoomShotFor`) — and what it has to
|
|
65
|
+
* fall under before a zoom paints it live again. A picture pays for a graph
|
|
66
|
+
* too big to paint at every step and costs one that is not: on macOS at 2x
|
|
67
|
+
* its composite is ~20 ms a step whatever the graph, on X11 each new
|
|
68
|
+
* picture was a 50–150 ms step, and either way it holds a surface the size
|
|
69
|
+
* of the pane. A thirty-node graph painted live in 7 ms a step on Cocoa and
|
|
70
|
+
* zoomed at 90 frames a second where its picture held it to 45. A frame at
|
|
71
|
+
* 60 Hz to start, and a margin under it to stop, so a graph near the line
|
|
72
|
+
* does not go back and forth; judged on the median of the last
|
|
73
|
+
* `GRAPH_PAINTS_KEPT` whole paints, so a slow paint or two — a collection,
|
|
74
|
+
* the server catching up — decide nothing.
|
|
75
|
+
*/
|
|
76
|
+
const ZOOM_SHOT_WORTH_MS = 16;
|
|
77
|
+
const ZOOM_SHOT_DROP_MS = 12;
|
|
78
|
+
const GRAPH_PAINTS_KEPT = 5;
|
|
79
|
+
/** How far round a box selection's outline its step claims: the pen, the
|
|
80
|
+
* corner's radius and a pixel of antialiasing, with room to spare. */
|
|
81
|
+
const SELECT_BAND = 4;
|
|
38
82
|
/** How often the dash on an animated edge moves. Slow enough that a graph
|
|
39
83
|
* full of them is not a repaint storm, fast enough to read as motion. */
|
|
40
84
|
const ANIMATION_MS = 60;
|
|
41
85
|
const ANIMATION_SPEED = 1.4; // px of dash travel per tick, at zoom 1
|
|
42
|
-
const DEFAULT_DASH = [7, 5];
|
|
43
|
-
/** Below these zooms the pane stops drawing detail nobody could read — the
|
|
44
|
-
* cheapest optimisation there is, and the one that keeps a zoomed-out
|
|
45
|
-
* overview interactive. */
|
|
46
|
-
const LABEL_ZOOM = 0.45;
|
|
47
|
-
const HANDLE_ZOOM = 0.5;
|
|
48
|
-
const DESC_ZOOM = 0.6;
|
|
49
86
|
/** Screen pixels the pointer may travel before a press becomes a drag. */
|
|
50
87
|
const DRAG_THRESHOLD = 3;
|
|
51
|
-
/** The grid never draws denser than this on screen, whatever the zoom. */
|
|
52
|
-
const MIN_GRID_PX = 16;
|
|
53
|
-
/** See `StrokeBuckets.BATCH_MIN`. */
|
|
54
|
-
const MARKER_BATCH_MIN = 24;
|
|
55
|
-
/**
|
|
56
|
-
* How far outside its own box a node's ink can land: handles (capped at
|
|
57
|
-
* 6 × 1.4 plus their outline), resize grips, the selection border, a pixel
|
|
58
|
-
* of antialiasing. Damage rects grow by this before they are invalidated,
|
|
59
|
-
* and cull tests grow their target by the same amount — the two must agree,
|
|
60
|
-
* or a moved node leaves crumbs of its old handles behind.
|
|
61
|
-
*/
|
|
62
|
-
const CULL_MARGIN = 16;
|
|
63
88
|
/**
|
|
64
89
|
* The props whose change means different pixels. `applyProps` repaints when
|
|
65
90
|
* one of these moves and stays quiet otherwise — the event handlers are
|
|
@@ -81,7 +106,21 @@ const VISUAL_PROPS = [
|
|
|
81
106
|
'nodesConnectable',
|
|
82
107
|
'nodesResizable',
|
|
83
108
|
'disabled',
|
|
109
|
+
'renderer',
|
|
84
110
|
];
|
|
111
|
+
/** The clock, through `globalThis`: `src/` compiles with `types: []`. */
|
|
112
|
+
const clock = globalThis;
|
|
113
|
+
/**
|
|
114
|
+
* The pane's time: what a pan is stamped with, and what the dashes' march
|
|
115
|
+
* reads to decide it has held still. A test holds it (test/flow.test.ts):
|
|
116
|
+
* a runner that spends longer than the dashes' wait over one step of a pan
|
|
117
|
+
* lets them march in the middle of it — as they should — and a test of the
|
|
118
|
+
* pan cannot tell that from a march it should not have made.
|
|
119
|
+
*/
|
|
120
|
+
export const flowClock = {
|
|
121
|
+
now: () => clock.performance?.now() ?? Date.now(),
|
|
122
|
+
};
|
|
123
|
+
const now = () => flowClock.now();
|
|
85
124
|
const CONTROL_SIZE = 26;
|
|
86
125
|
const PANEL_MARGIN = 10;
|
|
87
126
|
const MINIMAP_W = 190;
|
|
@@ -124,6 +163,7 @@ export const SELF_DAMAGED_PROPS = [
|
|
|
124
163
|
'zoomOnDoubleClick',
|
|
125
164
|
'selectionOnDrag',
|
|
126
165
|
'deleteOnKey',
|
|
166
|
+
'adaptive',
|
|
127
167
|
'snapToGrid',
|
|
128
168
|
'snapGrid',
|
|
129
169
|
];
|
|
@@ -175,71 +215,13 @@ function unionMaybe(a, b) {
|
|
|
175
215
|
return a;
|
|
176
216
|
return unionRects(a, b);
|
|
177
217
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
*
|
|
181
|
-
* Everything a graph strokes is the same two or three pens — the default
|
|
182
|
-
* edge, the selected one, the animated dash — so grouping collapses a
|
|
183
|
-
* per-edge request into a per-pen one. The key has to carry the dash *and*
|
|
184
|
-
* its offset: two edges marching out of phase cannot share a path, because
|
|
185
|
-
* the offset is set on the context, not on the subpath.
|
|
186
|
-
*/
|
|
187
|
-
class StrokeBuckets {
|
|
188
|
-
byPen = new Map();
|
|
189
|
-
bucket(stroke, lineWidth, dash, phase, zoom) {
|
|
190
|
-
const scaled = dash?.map((d) => d * zoom);
|
|
191
|
-
const key = `${stroke}|${lineWidth}|${scaled?.join(',') ?? ''}|${phase}`;
|
|
192
|
-
let entry = this.byPen.get(key);
|
|
193
|
-
if (!entry) {
|
|
194
|
-
entry = {
|
|
195
|
-
options: {
|
|
196
|
-
stroke,
|
|
197
|
-
lineWidth,
|
|
198
|
-
dash: scaled,
|
|
199
|
-
dashOffset: phase ? -phase * zoom : 0,
|
|
200
|
-
},
|
|
201
|
-
runs: [],
|
|
202
|
-
};
|
|
203
|
-
this.byPen.set(key, entry);
|
|
204
|
-
}
|
|
205
|
-
return entry.runs;
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* Below this many edges in one pen, they are stroked one at a time.
|
|
209
|
-
*
|
|
210
|
-
* A path's mask is its bounding box, so batching scattered geometry trades
|
|
211
|
-
* many small masks for one the size of the pane — about three quarters of
|
|
212
|
-
* a megabyte at a normal window size. That is a large win at seven hundred
|
|
213
|
-
* edges (measured: 3.9 MB a frame down to 1.3) and a loss at twenty, where
|
|
214
|
-
* the individual masks never add up to a paneful. The threshold is where
|
|
215
|
-
* they start to.
|
|
216
|
-
*/
|
|
217
|
-
static BATCH_MIN = 24;
|
|
218
|
-
paint(painter) {
|
|
219
|
-
for (const { options, runs } of this.byPen.values()) {
|
|
220
|
-
if (runs.length >= StrokeBuckets.BATCH_MIN) {
|
|
221
|
-
painter.strokeRuns(runs, options);
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
for (const run of runs)
|
|
225
|
-
painter.polyline(run, options);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
218
|
+
function sameRect(p, q) {
|
|
219
|
+
return (p.x === q.x && p.y === q.y && p.width === q.width && p.height === q.height);
|
|
229
220
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
* handles with no id at all. Node, id, type and side together are what
|
|
235
|
-
* distinguish them.
|
|
236
|
-
*/
|
|
237
|
-
function sameHandle(a, b) {
|
|
238
|
-
return (a != null &&
|
|
239
|
-
a.nodeId === b.nodeId &&
|
|
240
|
-
(a.id ?? null) === (b.id ?? null) &&
|
|
241
|
-
a.type === b.type &&
|
|
242
|
-
a.position === b.position);
|
|
221
|
+
function sameFit(a, b) {
|
|
222
|
+
return (sameRect(a.panel, b.panel) &&
|
|
223
|
+
sameRect(a.bounds, b.bounds) &&
|
|
224
|
+
a.scale === b.scale);
|
|
243
225
|
}
|
|
244
226
|
export class FlowGraphNode extends Node {
|
|
245
227
|
// --- viewport, owned here unless the `viewport` prop takes it over ------
|
|
@@ -268,21 +250,146 @@ export class FlowGraphNode extends Node {
|
|
|
268
250
|
* when the app applies the changes it is being sent. Cleared on release,
|
|
269
251
|
* which is also how a refused drag snaps back. */
|
|
270
252
|
_dragTo = null;
|
|
253
|
+
/** Whether the pane's edges are stroked a pen at a time (`EDGE_BATCH`):
|
|
254
|
+
* chosen by a pass over the whole pane, and followed by every pass over
|
|
255
|
+
* part of it until the next. */
|
|
256
|
+
_batch = false;
|
|
271
257
|
/** The box a resize is currently making, for the same reason. */
|
|
272
258
|
_resizeTo = null;
|
|
273
|
-
/**
|
|
274
|
-
*
|
|
275
|
-
|
|
259
|
+
/** The bodies last sent, re-sent as the same array when only the origin
|
|
260
|
+
* moved, and that origin — and each one by id, reused as the same object
|
|
261
|
+
* when nothing about it changed, so the React half re-renders the card
|
|
262
|
+
* that moved and not the ones beside it. */
|
|
263
|
+
_bodies = [];
|
|
264
|
+
_bodiesById = new Map();
|
|
265
|
+
_bodiesOrigin = { x: 0, y: 0 };
|
|
266
|
+
/** Bodies held back while the zoom moves (`_holdBodies`): whether they
|
|
267
|
+
* are, the zoom last seen, the timer that brings them back and when the
|
|
268
|
+
* last held step was. */
|
|
269
|
+
_bodiesHeld = false;
|
|
270
|
+
_seenZoom = NaN;
|
|
271
|
+
_bodiesRest = null;
|
|
272
|
+
_bodiesAt = 0;
|
|
273
|
+
/** Mounted cards `<Flow>` shows, painted in the bodies' layer — which
|
|
274
|
+
* the 2D pane then leaves out (`setShownBodies`, `_cardInLayer`). */
|
|
275
|
+
_shownBodies = new Set();
|
|
276
|
+
/** The canvases `<Flow>` paints the minimap and controls on, over the
|
|
277
|
+
* bodies — while there are any, the graph leaves the panels out. */
|
|
278
|
+
_panelCanvases = [];
|
|
279
|
+
/** The fit the minimap on screen was painted with: every pass over it
|
|
280
|
+
* since its corner was last claimed whole resolved the same one. Set by
|
|
281
|
+
* the pass that paints it (`_sceneInput`), read by what a move claims
|
|
282
|
+
* there (`_miniMapMoves`). */
|
|
283
|
+
_miniMapShown = null;
|
|
284
|
+
_panelsKey = '';
|
|
285
|
+
/** The budget's model (`_holdBodies`, `_frameTick`): what a body adds to a
|
|
286
|
+
* zoom step, what a step costs with none re-scaled, the last frame's
|
|
287
|
+
* time, and what the step awaiting its frame did. */
|
|
288
|
+
_bodyStepMs = BODY_STEP_PRIOR_MS;
|
|
289
|
+
_baseStepMs = NaN;
|
|
290
|
+
_lastFrameAt = -Infinity;
|
|
291
|
+
_zoomStep = null;
|
|
276
292
|
_dashPhase = 0;
|
|
293
|
+
/** The box the bodies are laid out in, which a 2D pan carries. */
|
|
294
|
+
_bodiesLayer = null;
|
|
295
|
+
/** When a pan last asked to blit — the dash timer waits for it. */
|
|
296
|
+
_blittedPanAt = -Infinity;
|
|
297
|
+
/** When the view last moved, and when the dash timer last came round,
|
|
298
|
+
* ticking or not: a 2D pan's dashes wait for a view that held still. */
|
|
299
|
+
_viewMovedAt = -Infinity;
|
|
300
|
+
_tickAt = -Infinity;
|
|
301
|
+
/** When the dash timer last marched, and what a 2D frame has cost lately
|
|
302
|
+
* — the paint, and the server's answer to the frame before it; a running
|
|
303
|
+
* mean, ms: what a tick waits on. */
|
|
304
|
+
_marchedAt = -Infinity;
|
|
305
|
+
_tickCost = 0;
|
|
277
306
|
_animTimer = null;
|
|
278
307
|
/** Inside `paint`, where an invalidation would only schedule a redraw of
|
|
279
308
|
* the frame being drawn. */
|
|
280
309
|
_painting = false;
|
|
310
|
+
/** Routes that survived the last frame — see {@link SceneCache}. */
|
|
311
|
+
_sceneCache = new SceneCache();
|
|
312
|
+
/** Asks the GL surface for a frame, while one is drawing this pane. */
|
|
313
|
+
_glRequest = null;
|
|
314
|
+
/**
|
|
315
|
+
* Bumped by every repaint this element asks for that is *not* a pure pan
|
|
316
|
+
* — which is how the GL surface knows the graph it holds on the GPU is
|
|
317
|
+
* still the graph, and a frame is only a new offset. Errs one way on
|
|
318
|
+
* purpose: anything it cannot see is covered by the rest of the key
|
|
319
|
+
* (`glFrame`), and a rebuild too many costs a frame's packing, where one
|
|
320
|
+
* too few draws the wrong graph.
|
|
321
|
+
*/
|
|
322
|
+
_worldVersion = 0;
|
|
323
|
+
/**
|
|
324
|
+
* The same for the nodes a drag is moving under GL, which are lifted out
|
|
325
|
+
* of the world for as long as it moves them (`_lifted`): a step changes
|
|
326
|
+
* them and nothing else, so it moves this and the world stays on the GPU.
|
|
327
|
+
* Rebuilding the world was the whole of a drag step's cost — 5-7 ms of
|
|
328
|
+
* scene and packing a step at 2,000 nodes, for one card.
|
|
329
|
+
*/
|
|
330
|
+
_liftVersion = 0;
|
|
331
|
+
/** The lifted ids, for the drag gesture they were made for, and a count
|
|
332
|
+
* of every change to them — the world's key, since the world is built
|
|
333
|
+
* without them. */
|
|
334
|
+
_liftSet = null;
|
|
335
|
+
_liftFor = null;
|
|
336
|
+
_liftGen = 0;
|
|
337
|
+
/** Whether a marching edge is in the world, or in the lifted layer. */
|
|
338
|
+
_worldAnimated = false;
|
|
339
|
+
_liftedAnimated = false;
|
|
340
|
+
/** The last nodes commit changed lifted nodes and nothing else. */
|
|
341
|
+
_liftOnly = false;
|
|
342
|
+
/** The overscan the GL world was culled to, in its own pinned
|
|
343
|
+
* coordinates, and the key it was built under. */
|
|
344
|
+
_worldCull = null;
|
|
345
|
+
_worldCullId = 0;
|
|
346
|
+
_worldZoom = NaN;
|
|
347
|
+
/** When the zoom last moved, whether that move continued a stream of
|
|
348
|
+
* them — a gesture, where one alone is a jump — and the timer that asks
|
|
349
|
+
* for the frame that rebuilds the GL world once the stream stops. */
|
|
350
|
+
_zoomAt = -Infinity;
|
|
351
|
+
_zoomStream = false;
|
|
352
|
+
/** When the frame that drew the latest zoom step began, and when it last
|
|
353
|
+
* finished a pass: an animation's next step follows the frame that drew
|
|
354
|
+
* its last, however long that frame took (`_applyViewport`). */
|
|
355
|
+
_zoomFrameAt = -Infinity;
|
|
356
|
+
_zoomDrawnAt = -Infinity;
|
|
357
|
+
_zoomRest = null;
|
|
358
|
+
/** A 2D paint drew labels scaled mid-zoom, and owes them at their own
|
|
359
|
+
* sizes once it rests. */
|
|
360
|
+
_textApproximated = false;
|
|
361
|
+
/**
|
|
362
|
+
* Mid-zoom on the 2D renderer: the graph and its ground as the gesture
|
|
363
|
+
* drew them once, on a surface of their own, composited scaled for every
|
|
364
|
+
* step after — the GL renderer's world drawn scaled, in a bitmap. A step
|
|
365
|
+
* used to stroke every edge, fill every card and lay the grid again, 20 ms
|
|
366
|
+
* of Direct2D a step over the stress lattice, for a picture the next step
|
|
367
|
+
* moved off. Dropped when the zoom rests, which paints it all again
|
|
368
|
+
* exactly, and whenever the graph itself changes.
|
|
369
|
+
*/
|
|
370
|
+
_zoomShot = null;
|
|
371
|
+
/** What the last `GRAPH_PAINTS_KEPT` whole paints of the graph cost, ms,
|
|
372
|
+
* newest last: the steps of a zoom painted live and the pictures painted
|
|
373
|
+
* for zooms, the two passes that say what a step would cost live — and
|
|
374
|
+
* not a zoom's first step, which sets every label at its size once and
|
|
375
|
+
* says what the steps after it will not. Whether a zoom is worth a
|
|
376
|
+
* picture (`ZOOM_SHOT_WORTH_MS`); empty until a zoom has painted, so the
|
|
377
|
+
* first one starts live. */
|
|
378
|
+
_graphPaints = [];
|
|
379
|
+
/** A 2D drag's pictures (`_liftShotFor`), and the gesture that is not to
|
|
380
|
+
* have any: one whose view moved under it, where a picture a step would
|
|
381
|
+
* cost two full paints a step. */
|
|
382
|
+
_liftShot = null;
|
|
383
|
+
_liftShotRefused = null;
|
|
281
384
|
/** Inside a live input dispatch — what makes a body emission `sync`.
|
|
282
385
|
* Motion and the wheel run at continuous priority, whose React updates
|
|
283
386
|
* can trail the pane's own painting by frames; an emission made under
|
|
284
387
|
* this flag asks the receiver to commit before the dispatch returns. */
|
|
285
388
|
_gestureSync = false;
|
|
389
|
+
/** Inside a call through `FlowInstance` (`fromHandle`) — an application's
|
|
390
|
+
* handler, timer or animation loop moving the viewport. Its emissions ask
|
|
391
|
+
* to commit inline too. */
|
|
392
|
+
_handleSync = false;
|
|
286
393
|
/** The scene has been offered to assistive tech at least once — items
|
|
287
394
|
* only exist once layout has placed them, which no commit marks. */
|
|
288
395
|
_sceneAnnounced = false;
|
|
@@ -355,9 +462,29 @@ export class FlowGraphNode extends Node {
|
|
|
355
462
|
};
|
|
356
463
|
}
|
|
357
464
|
_measure = (text, options) => measureText(this._textOptions(), text, options).width;
|
|
465
|
+
/** The same shaping, both dimensions: what a scene's label chips are
|
|
466
|
+
* sized from. `_measure` answers a width because `measureNode` only ever
|
|
467
|
+
* wanted one. */
|
|
468
|
+
_measureBox = (text, options) => measureText(this._textOptions(), text, options);
|
|
469
|
+
/**
|
|
470
|
+
* The palette, resolved once for the theme and the `palette` prop it was
|
|
471
|
+
* resolved from. Every text measured asks for it (`_textOptions`), and
|
|
472
|
+
* resolving it tints the accent and reads the background's lightness out
|
|
473
|
+
* of a colour string: a 2D pan's passes measured their labels with a
|
|
474
|
+
* fresh one each. The theme object is replaced when it changes.
|
|
475
|
+
*/
|
|
358
476
|
_palette() {
|
|
359
|
-
|
|
477
|
+
const theme = this.theme;
|
|
478
|
+
const overrides = this._prop('palette');
|
|
479
|
+
const held = this._paletteHeld;
|
|
480
|
+
if (held && held.theme === theme && held.overrides === overrides) {
|
|
481
|
+
return held.palette;
|
|
482
|
+
}
|
|
483
|
+
const palette = resolvePalette(theme, overrides);
|
|
484
|
+
this._paletteHeld = { theme, overrides, palette };
|
|
485
|
+
return palette;
|
|
360
486
|
}
|
|
487
|
+
_paletteHeld = null;
|
|
361
488
|
// --- the derived graph ---------------------------------------------------
|
|
362
489
|
/**
|
|
363
490
|
* Rebuild the node index when — and only when — one of its inputs changed
|
|
@@ -422,12 +549,12 @@ export class FlowGraphNode extends Node {
|
|
|
422
549
|
// a fresh literal saying the same thing: keep the built entry
|
|
423
550
|
continue;
|
|
424
551
|
}
|
|
425
|
-
const wasBox = this.
|
|
552
|
+
const wasBox = this._edgeInkBox(this._edges[i]);
|
|
426
553
|
this._edges[i] = defaults
|
|
427
554
|
? { ...defaults, ...next }
|
|
428
555
|
: next;
|
|
429
556
|
this._edgesRaw[i] = next;
|
|
430
|
-
const isBox = this.
|
|
557
|
+
const isBox = this._edgeInkBox(this._edges[i]);
|
|
431
558
|
if (wasBox)
|
|
432
559
|
damage = damage ? unionRects(damage, wasBox) : wasBox;
|
|
433
560
|
if (isBox)
|
|
@@ -461,8 +588,20 @@ export class FlowGraphNode extends Node {
|
|
|
461
588
|
* and `null` when nothing visual changed at all.
|
|
462
589
|
*/
|
|
463
590
|
_applyNodes(nodes) {
|
|
591
|
+
const sameCardText = (a, b) => {
|
|
592
|
+
const da = a.data;
|
|
593
|
+
const db = b.data;
|
|
594
|
+
return ((da?.label ?? a.id) === (db?.label ?? b.id) &&
|
|
595
|
+
da?.description === db?.description);
|
|
596
|
+
};
|
|
464
597
|
const prev = this._entries;
|
|
465
598
|
this._nodesSeen = this.props.nodes;
|
|
599
|
+
// Whether every node this commit changed is one a drag has lifted out
|
|
600
|
+
// of the GL world — the commit of the drag's own step, when the app
|
|
601
|
+
// stores it — so the world on the GPU is still the world.
|
|
602
|
+
const lift = this._lifted();
|
|
603
|
+
let onlyLifted = lift != null;
|
|
604
|
+
this._liftOnly = false;
|
|
466
605
|
let structural = nodes.length !== prev.length;
|
|
467
606
|
if (!structural) {
|
|
468
607
|
for (let i = 0; i < nodes.length; i++) {
|
|
@@ -472,7 +611,6 @@ export class FlowGraphNode extends Node {
|
|
|
472
611
|
continue;
|
|
473
612
|
if (n.id !== o.id ||
|
|
474
613
|
n.type !== o.type ||
|
|
475
|
-
n.selected !== o.selected ||
|
|
476
614
|
n.hidden !== o.hidden ||
|
|
477
615
|
n.zIndex !== o.zIndex ||
|
|
478
616
|
n.handles !== o.handles ||
|
|
@@ -489,15 +627,40 @@ export class FlowGraphNode extends Node {
|
|
|
489
627
|
this._rebuildNodes();
|
|
490
628
|
return 'full';
|
|
491
629
|
}
|
|
492
|
-
|
|
630
|
+
const damage = [];
|
|
631
|
+
// A node that is selected, or no longer is, is restyled and restacked —
|
|
632
|
+
// selection lifts it over its neighbours — and both happen inside its
|
|
633
|
+
// own box: edges are drawn under every node. Treated as structural, a
|
|
634
|
+
// selection change re-measured every node and repainted the pane, which
|
|
635
|
+
// a box selection does on every step that takes a node in.
|
|
636
|
+
let reordered = false;
|
|
637
|
+
// The minimap draws every node where it is, at its size, marked when it
|
|
638
|
+
// is selected — and, to a `nodeColor` function, as anything at all.
|
|
639
|
+
const colour = this._miniMapOptions()?.nodeColor;
|
|
640
|
+
let mapped = false;
|
|
641
|
+
// …and where the pane draws it, a node that only moved or changed size
|
|
642
|
+
// changed its own places in it, while the fit holds (`_miniMapMoves`)
|
|
643
|
+
const drawsMap = this._drawsMiniMap();
|
|
644
|
+
const moves = [];
|
|
645
|
+
// round the box: its handles, and the resize grips a selected node
|
|
646
|
+
// grows at its corners, which at a deep zoom reach past the cull margin
|
|
647
|
+
const margin = Math.max(CULL_MARGIN, RESIZE_GRIP * this._viewport().zoom + 2);
|
|
493
648
|
for (let i = 0; i < nodes.length; i++) {
|
|
494
649
|
const entry = prev[i];
|
|
495
650
|
const next = nodes[i];
|
|
496
651
|
const old = entry.node;
|
|
497
652
|
if (next === old)
|
|
498
653
|
continue;
|
|
654
|
+
if (!lift?.has(next.id))
|
|
655
|
+
onlyLifted = false;
|
|
656
|
+
if (next.selected !== old.selected)
|
|
657
|
+
reordered = true;
|
|
658
|
+
if (typeof colour === 'function')
|
|
659
|
+
mapped = true;
|
|
499
660
|
const moved = next.position.x !== old.position.x ||
|
|
500
661
|
next.position.y !== old.position.y;
|
|
662
|
+
if (moved)
|
|
663
|
+
mapped = true;
|
|
501
664
|
// `data`, an explicit size or a paint style are the node's own to
|
|
502
665
|
// change: they re-measure and repaint *this* node, never the graph. A
|
|
503
666
|
// keystroke into a mounted node's textarea patches `data` on every
|
|
@@ -507,7 +670,7 @@ export class FlowGraphNode extends Node {
|
|
|
507
670
|
const reshaped = next.data !== old.data ||
|
|
508
671
|
next.width !== old.width ||
|
|
509
672
|
next.height !== old.height;
|
|
510
|
-
const restyled = next.style !== old.style;
|
|
673
|
+
const restyled = next.style !== old.style || next.selected !== old.selected;
|
|
511
674
|
if (!moved && !reshaped && !restyled) {
|
|
512
675
|
// a behavioural flag (draggable, deletable…) — nothing drawn reads it
|
|
513
676
|
entry.node = next;
|
|
@@ -515,6 +678,7 @@ export class FlowGraphNode extends Node {
|
|
|
515
678
|
}
|
|
516
679
|
const widthBefore = entry.width;
|
|
517
680
|
const heightBefore = entry.height;
|
|
681
|
+
const placeBefore = drawsMap && (moved || reshaped) ? this.rectOf(entry) : null;
|
|
518
682
|
let box = moved ? this._nodeDamage(entry) : this._screenRect(entry);
|
|
519
683
|
entry.node = next;
|
|
520
684
|
if (reshaped) {
|
|
@@ -524,14 +688,51 @@ export class FlowGraphNode extends Node {
|
|
|
524
688
|
entry.specs = resolveHandles(next, entry.type);
|
|
525
689
|
}
|
|
526
690
|
const grew = entry.width !== widthBefore || entry.height !== heightBefore;
|
|
691
|
+
if (grew)
|
|
692
|
+
mapped = true;
|
|
693
|
+
// A drag's own step, stored by the app, moves nothing on screen:
|
|
694
|
+
// the node was already where the drag had it
|
|
695
|
+
if (placeBefore && !sameRect(placeBefore, this.rectOf(entry))) {
|
|
696
|
+
moves.push({ entry, before: placeBefore });
|
|
697
|
+
}
|
|
698
|
+
// Nothing the pane draws changed, while the node holds its place and
|
|
699
|
+
// its size, when its card is the bodies' layer's on the 2D renderer —
|
|
700
|
+
// the layer repaints it, if anything it shows changed — or when what
|
|
701
|
+
// changed is data its card does not show: a card shows the label, the
|
|
702
|
+
// description and the style, and a type that paints its own may show
|
|
703
|
+
// anything. A board of live widgets, a tenth of them patching their
|
|
704
|
+
// data ten times a second, rebuilt the GL world on every patch, and on
|
|
705
|
+
// the 2D renderer repainted cards that showed none of it.
|
|
706
|
+
if (!moved && !grew) {
|
|
707
|
+
if (this._cardInLayer(next.id))
|
|
708
|
+
continue;
|
|
709
|
+
if (!restyled && !entry.type?.paint && sameCardText(old, next)) {
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
527
713
|
// The edges ride along only when an endpoint actually moved — a
|
|
528
714
|
// label edit that kept the box is the box's own business, and a
|
|
529
715
|
// keystroke that unioned its node's edges swept half the layer's
|
|
530
716
|
// neighbours into every repaint.
|
|
531
717
|
box = unionRects(box, moved || grew ? this._nodeDamage(entry) : this._screenRect(entry));
|
|
532
|
-
damage
|
|
718
|
+
damage.push(inflateRect(box, margin));
|
|
719
|
+
}
|
|
720
|
+
if (reordered)
|
|
721
|
+
this._sortOrder();
|
|
722
|
+
if (reordered || mapped) {
|
|
723
|
+
// A node that moved, grew or was picked out is a change in the
|
|
724
|
+
// minimap's corner too. Only a selection used to claim it, so a node
|
|
725
|
+
// the app moved repainted its own box and edges and the minimap went
|
|
726
|
+
// on showing it where it had been. A selection, or a colour worked
|
|
727
|
+
// out per node, is the whole corner's: it changes how the nodes there
|
|
728
|
+
// are filled together (`SceneMiniMapNode`).
|
|
729
|
+
const whole = !drawsMap || reordered || typeof colour === 'function';
|
|
730
|
+
const map = whole ? this._miniMapCorner() : this._miniMapMoves(moves);
|
|
731
|
+
if (map)
|
|
732
|
+
damage.push(map);
|
|
533
733
|
}
|
|
534
|
-
|
|
734
|
+
this._liftOnly = onlyLifted;
|
|
735
|
+
return damage;
|
|
535
736
|
}
|
|
536
737
|
/**
|
|
537
738
|
* The screen box a node's redraw touches: its own rect and every edge on
|
|
@@ -549,13 +750,17 @@ export class FlowGraphNode extends Node {
|
|
|
549
750
|
let box = this._screenRect(entry);
|
|
550
751
|
const edges = this._edgesByNode.get(entry.node.id) ?? [];
|
|
551
752
|
const tight = edges.length <= 16;
|
|
753
|
+
const sv = this._screenViewport();
|
|
552
754
|
for (const edge of edges) {
|
|
553
755
|
const geometry = tight ? this._edgeGeometry(edge) : null;
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
756
|
+
// the route's own ink, heads and pen and all — the pen only past the
|
|
757
|
+
// margin every caller grows this by — or the coarse box, whose slack
|
|
758
|
+
// holds both
|
|
759
|
+
const ink = geometry
|
|
760
|
+
? edgeInkBox(sv, edge, geometry.points, CULL_MARGIN)
|
|
761
|
+
: this._edgeInkBox(edge);
|
|
762
|
+
if (ink)
|
|
763
|
+
box = unionRects(box, ink);
|
|
559
764
|
}
|
|
560
765
|
return box;
|
|
561
766
|
}
|
|
@@ -580,7 +785,12 @@ export class FlowGraphNode extends Node {
|
|
|
580
785
|
}
|
|
581
786
|
this._entries = entries;
|
|
582
787
|
this._byId = byId;
|
|
583
|
-
this.
|
|
788
|
+
this._sortOrder();
|
|
789
|
+
}
|
|
790
|
+
/** The paint order: by `zIndex`, a selected node over the rest of its
|
|
791
|
+
* layer, and declaration order breaking ties. */
|
|
792
|
+
_sortOrder() {
|
|
793
|
+
this._order = this._entries
|
|
584
794
|
.map((entry, index) => ({ entry, index }))
|
|
585
795
|
.sort((a, b) => {
|
|
586
796
|
const az = a.entry.node.zIndex ?? 0;
|
|
@@ -625,6 +835,11 @@ export class FlowGraphNode extends Node {
|
|
|
625
835
|
_viewport() {
|
|
626
836
|
return this._prop('viewport') ?? this._vp;
|
|
627
837
|
}
|
|
838
|
+
/** The viewport with the pane's window origin folded in — what every
|
|
839
|
+
* graph-to-window conversion shared with `./scene.ts` takes. */
|
|
840
|
+
_screenViewport() {
|
|
841
|
+
return screenViewport(this._viewport(), this._pane());
|
|
842
|
+
}
|
|
628
843
|
/**
|
|
629
844
|
* Device pixels per logical pixel — the display scale this pane's window
|
|
630
845
|
* resolved to (react-x11's docs/scale.md): `1` on an ordinary display,
|
|
@@ -675,6 +890,46 @@ export class FlowGraphNode extends Node {
|
|
|
675
890
|
_claim(rect, reason) {
|
|
676
891
|
this.invalidate(false, this._device(rect), reason);
|
|
677
892
|
}
|
|
893
|
+
/** Whether the pane draws the minimap itself. On a canvas of its own —
|
|
894
|
+
* under mounted bodies — it is `invalidate`'s to repaint. */
|
|
895
|
+
_drawsMiniMap() {
|
|
896
|
+
return this._panelCanvases.length === 0 && this._miniMapOptions() != null;
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* What moving or resizing `moves` changed in the minimap's corner, where
|
|
900
|
+
* the pane draws it: each node's place there before and after, while the
|
|
901
|
+
* fit is the one on screen — the graph's bounds with the viewport's,
|
|
902
|
+
* which put every node where it is there — and the whole corner when it
|
|
903
|
+
* is not, since every node moves in it then. A drag of one node among
|
|
904
|
+
* 2,000 claimed the corner on every step, and every such pass built and
|
|
905
|
+
* filled all 2,000 again, where it builds what it reaches now
|
|
906
|
+
* (`buildMiniMap`). Asked against the fit on screen rather than the one
|
|
907
|
+
* before the change, a step works the fit out once, and not at all when
|
|
908
|
+
* nothing moved on screen.
|
|
909
|
+
*/
|
|
910
|
+
_miniMapMoves(moves) {
|
|
911
|
+
const corner = this._miniMapCorner();
|
|
912
|
+
if (!corner || moves.length === 0)
|
|
913
|
+
return null;
|
|
914
|
+
const fit = this._miniMap();
|
|
915
|
+
const shown = this._miniMapShown;
|
|
916
|
+
if (!fit && !shown)
|
|
917
|
+
return null;
|
|
918
|
+
if (!fit || !shown || !sameFit(fit, shown))
|
|
919
|
+
return corner;
|
|
920
|
+
let out = null;
|
|
921
|
+
for (const { entry, before } of moves) {
|
|
922
|
+
out = unionMaybe(out, miniMapPlace(shown, before));
|
|
923
|
+
out = unionMaybe(out, miniMapPlace(fit, this.rectOf(entry)));
|
|
924
|
+
}
|
|
925
|
+
return out && inflateRect(out, MINIMAP_BLEED);
|
|
926
|
+
}
|
|
927
|
+
/** A gesture's step, claimed in the minimap: `_miniMapMoves`. */
|
|
928
|
+
_claimMiniMapMoves(moves, reason) {
|
|
929
|
+
const map = this._miniMapMoves(moves);
|
|
930
|
+
if (map)
|
|
931
|
+
this._claim(map, reason);
|
|
932
|
+
}
|
|
678
933
|
/**
|
|
679
934
|
* The pane proper: `abs` inset by border and padding, off core's own
|
|
680
935
|
* resolved layout (react-x11#254) — so a `<Flow style={{ borderWidth: 1,
|
|
@@ -749,9 +1004,85 @@ export class FlowGraphNode extends Node {
|
|
|
749
1004
|
return !anchors.some((a) => Math.abs(a.x - at.x) <= near && Math.abs(a.y - at.y) <= near);
|
|
750
1005
|
});
|
|
751
1006
|
}
|
|
1007
|
+
/**
|
|
1008
|
+
* One entry as `./scene.ts` takes it: the live gesture state resolved
|
|
1009
|
+
* away, so everything past this point is pure. A drag's position and a
|
|
1010
|
+
* resize's box are already in `rect`, and `grips` is already filtered by
|
|
1011
|
+
* whatever the handles took.
|
|
1012
|
+
*
|
|
1013
|
+
* Built per call rather than cached: `rectOf` is what keeps a drag in step
|
|
1014
|
+
* with the commit that has not landed yet, and a cache here is a cache of
|
|
1015
|
+
* last frame's positions.
|
|
1016
|
+
*/
|
|
1017
|
+
/** Whether a React body is mounted over this node — which is what makes
|
|
1018
|
+
* its card draw a title bar rather than a centred label, and what
|
|
1019
|
+
* `_emitBodies` reports a box for. */
|
|
1020
|
+
_mounted(entry) {
|
|
1021
|
+
return entry.type?.render != null && this._viewport().zoom >= RENDER_ZOOM;
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* The frame being painted: each node's scene source, and the two lists a
|
|
1025
|
+
* 2D pass builds its scene from. A frame paints its damage as several
|
|
1026
|
+
* passes — a pan's strips, its pinned furniture, the corners of a rounded
|
|
1027
|
+
* pane — and the scene cache culls the graph to the screen once for all
|
|
1028
|
+
* of them (`SceneCache.edgesOnScreen`), keyed on these very lists, so
|
|
1029
|
+
* every pass has to be handed the same ones. The passes run one after
|
|
1030
|
+
* another in a single task, so a microtask queued by the first forgets
|
|
1031
|
+
* the frame after the last — the same reasoning as `_reportFrame`.
|
|
1032
|
+
*/
|
|
1033
|
+
_frame = null;
|
|
1034
|
+
_paintedFrame() {
|
|
1035
|
+
let frame = this._frame;
|
|
1036
|
+
if (!frame) {
|
|
1037
|
+
frame = this._frame = { sources: new Map(), lists: null };
|
|
1038
|
+
void Promise.resolve().then(() => {
|
|
1039
|
+
this._frame = null;
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
return frame;
|
|
1043
|
+
}
|
|
1044
|
+
_sourceOf(entry) {
|
|
1045
|
+
if (!this._painting)
|
|
1046
|
+
return this._source(entry);
|
|
1047
|
+
const { sources } = this._paintedFrame();
|
|
1048
|
+
let source = sources.get(entry);
|
|
1049
|
+
if (!source) {
|
|
1050
|
+
source = this._source(entry);
|
|
1051
|
+
sources.set(entry, source);
|
|
1052
|
+
}
|
|
1053
|
+
return source;
|
|
1054
|
+
}
|
|
1055
|
+
/** A 2D pass's node lists, the same arrays for every pass of a frame. */
|
|
1056
|
+
_frameLists(order) {
|
|
1057
|
+
const frame = this._paintedFrame();
|
|
1058
|
+
frame.lists ??= {
|
|
1059
|
+
all: this._entries.map((entry) => this._sourceOf(entry)),
|
|
1060
|
+
nodes: order
|
|
1061
|
+
.filter((entry) => !this._cardInLayer(entry.node.id))
|
|
1062
|
+
.map((entry) => this._sourceOf(entry)),
|
|
1063
|
+
};
|
|
1064
|
+
return frame.lists;
|
|
1065
|
+
}
|
|
1066
|
+
_source(entry) {
|
|
1067
|
+
return {
|
|
1068
|
+
node: entry.node,
|
|
1069
|
+
rect: this.rectOf(entry),
|
|
1070
|
+
specs: entry.specs,
|
|
1071
|
+
type: entry.type,
|
|
1072
|
+
header: this._headerHeight(entry),
|
|
1073
|
+
mounted: this._mounted(entry),
|
|
1074
|
+
connectable: this._connectable(entry),
|
|
1075
|
+
grips: this._grips(entry),
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
/** Not through `_source`: a source carries its grips, and `_grips` asks
|
|
1079
|
+
* where the handles are — the pair would recur. */
|
|
752
1080
|
_handlesOf(entry) {
|
|
753
|
-
|
|
754
|
-
|
|
1081
|
+
return anchorsOf({
|
|
1082
|
+
node: entry.node,
|
|
1083
|
+
rect: this.rectOf(entry),
|
|
1084
|
+
specs: entry.specs,
|
|
1085
|
+
});
|
|
755
1086
|
}
|
|
756
1087
|
/**
|
|
757
1088
|
* A node's box on screen, **on whole device pixels**.
|
|
@@ -768,22 +1099,12 @@ export class FlowGraphNode extends Node {
|
|
|
768
1099
|
* testing reads the same rect, so what is drawn and what is clicked still
|
|
769
1100
|
* agree to the pixel.
|
|
770
1101
|
*/
|
|
771
|
-
_screenRect(entry
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const x = grid(p.x);
|
|
778
|
-
const y = grid(p.y);
|
|
779
|
-
return {
|
|
780
|
-
x,
|
|
781
|
-
y,
|
|
782
|
-
// rounded as edges rather than as a size, so two nodes that share a
|
|
783
|
-
// column still share it after rounding
|
|
784
|
-
width: grid(p.x + rect.width * v.zoom) - x,
|
|
785
|
-
height: grid(p.y + rect.height * v.zoom) - y,
|
|
786
|
-
};
|
|
1102
|
+
_screenRect(entry,
|
|
1103
|
+
// A loop over the nodes passes it in: the viewport reads the pane's
|
|
1104
|
+
// content box off layout, and asked once per node it was most of what a
|
|
1105
|
+
// hit test cost on a large graph — every pointer move, 400 times.
|
|
1106
|
+
sv = this._screenViewport()) {
|
|
1107
|
+
return screenRect(sv, this.rectOf(entry), this._scale);
|
|
787
1108
|
}
|
|
788
1109
|
_visible() {
|
|
789
1110
|
const pane = this._pane();
|
|
@@ -800,12 +1121,47 @@ export class FlowGraphNode extends Node {
|
|
|
800
1121
|
const controlled = this.props.viewport !== undefined;
|
|
801
1122
|
if (!controlled)
|
|
802
1123
|
this._vp = v;
|
|
1124
|
+
if (previous.x !== v.x || previous.y !== v.y || previous.zoom !== v.zoom) {
|
|
1125
|
+
this._viewMovedAt = now();
|
|
1126
|
+
}
|
|
1127
|
+
if (previous.zoom !== v.zoom) {
|
|
1128
|
+
// A stream is steps close together — or a step that follows the frame
|
|
1129
|
+
// that drew the one before it: an animation zooms a step a frame, and
|
|
1130
|
+
// a frame whose bodies re-scale can take longer than the rest. Judged
|
|
1131
|
+
// on the steps alone, such an animation was a run of single jumps, so
|
|
1132
|
+
// its bodies were never held, every step re-scaled them all, and it
|
|
1133
|
+
// stayed at two frames a second where held it ran at twenty — on
|
|
1134
|
+
// whichever side of the line its first frames happened to fall.
|
|
1135
|
+
const t = now();
|
|
1136
|
+
this._zoomStream =
|
|
1137
|
+
t - this._zoomAt < GL_ZOOM_REST_MS ||
|
|
1138
|
+
t - this._zoomDrawnAt < GL_ZOOM_REST_MS;
|
|
1139
|
+
this._zoomAt = t;
|
|
1140
|
+
this._zoomFrameAt = -Infinity;
|
|
1141
|
+
this._zoomDrawnAt = -Infinity;
|
|
1142
|
+
}
|
|
1143
|
+
// Each claims its own box: a claim with no region is the whole window,
|
|
1144
|
+
// which made every pan step and dash tick a full frame. The minimap's
|
|
1145
|
+
// alone: its view box moves, and the controls show nothing of the
|
|
1146
|
+
// viewport.
|
|
1147
|
+
for (const canvas of this._miniMapCanvases())
|
|
1148
|
+
canvas.invalidate(false, canvas, 'props');
|
|
803
1149
|
this._prop('onViewportChange')?.(v);
|
|
804
1150
|
// The `fitView` that runs at the top of a paint has already changed what
|
|
805
1151
|
// this frame will draw, so asking for another one would only draw the
|
|
806
1152
|
// same picture twice.
|
|
807
1153
|
if (!controlled && !this._painting) {
|
|
808
|
-
if (
|
|
1154
|
+
if (this._gl) {
|
|
1155
|
+
// Under GL a pan is the surface's offset uniform and nothing else,
|
|
1156
|
+
// and a zoom is the frame's to decide — the world scaled while the
|
|
1157
|
+
// gesture moves, rebuilt once it rests (`glFrame`). Neither is a
|
|
1158
|
+
// change to the world, and the 2D pane under the surface shows none
|
|
1159
|
+
// of the graph: claiming its box every step repainted it for
|
|
1160
|
+
// nothing (~4 ms a frame) — and, with bodies mounted, reached
|
|
1161
|
+
// core's overlay too.
|
|
1162
|
+
this._glRequest?.();
|
|
1163
|
+
}
|
|
1164
|
+
else if (!this._blitPan(previous, v))
|
|
809
1165
|
this._repaint('scroll');
|
|
810
1166
|
// same-frame compositing for mounted bodies — see `_dragStep`
|
|
811
1167
|
this._emitBodies();
|
|
@@ -819,27 +1175,35 @@ export class FlowGraphNode extends Node {
|
|
|
819
1175
|
* the shift exposed — which `paintDamage()` then hands to `paint`, so the
|
|
820
1176
|
* existing culling draws the sliver and nothing else.
|
|
821
1177
|
*
|
|
822
|
-
* The furniture — minimap, zoom controls —
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
*
|
|
1178
|
+
* The furniture — minimap, zoom controls — stays put while the graph
|
|
1179
|
+
* moves, so it is handed over as `pinned` (react-x11#682, 2.22): the
|
|
1180
|
+
* whole pane is copied, and core repaints each panel and the image of
|
|
1181
|
+
* it the copy dragged along, with the strips the shift exposed.
|
|
1182
|
+
*
|
|
1183
|
+
* Mounted node bodies ride it. They are laid out in one box beside the
|
|
1184
|
+
* pane, which a pan moves by exactly the pan, so their pixels move with
|
|
1185
|
+
* the pane's: the box goes to `scrollContents` as a rider
|
|
1186
|
+
* (react-x11#671), its commit claims nothing, and what it leaves or
|
|
1187
|
+
* reaches outside the rect is all it costs. `<Flow>` clips it to the pane for that. A body entering or
|
|
1188
|
+
* leaving the pane, or changing as it goes, claims inside the rect and
|
|
1189
|
+
* declines that frame's blit; the next one blits again.
|
|
829
1190
|
*
|
|
830
1191
|
* Still a full repaint when:
|
|
831
1192
|
* - the zoom moved (scaling is not a blit) or the shift is fractional on
|
|
832
1193
|
* the device grid — every real pan gesture is whole device pixels;
|
|
833
|
-
* - mounted
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
* - the furniture bands would eat the pane (a tiny pane, or panels on
|
|
837
|
-
* both the top and the bottom of a short one).
|
|
1194
|
+
* - bodies are mounted and `<Flow>` has not handed their box over;
|
|
1195
|
+
* - the pane is too small to be worth it; and core falls back itself
|
|
1196
|
+
* when the repairs would repaint most of it.
|
|
838
1197
|
*/
|
|
839
1198
|
_blitPan(previous, next) {
|
|
1199
|
+
// There is no backing store to scroll under GL: the surface redraws the
|
|
1200
|
+
// whole scene every frame, and a pan is its cheapest frame of all.
|
|
1201
|
+
if (this._gl)
|
|
1202
|
+
return false;
|
|
840
1203
|
if (next.zoom !== previous.zoom)
|
|
841
1204
|
return false;
|
|
842
|
-
|
|
1205
|
+
const riders = this._bodies.length > 0 ? this._bodiesLayer : null;
|
|
1206
|
+
if (this._bodies.length > 0 && !riders)
|
|
843
1207
|
return false;
|
|
844
1208
|
// Device pixels: the blit copies the backing store, and its grid is the
|
|
845
1209
|
// panel's. A pointer step lands on it whatever the scale — it came off
|
|
@@ -853,59 +1217,57 @@ export class FlowGraphNode extends Node {
|
|
|
853
1217
|
return true; // sub-pixel: nothing to show yet
|
|
854
1218
|
if (shiftX !== dx || shiftY !== dy)
|
|
855
1219
|
return false;
|
|
856
|
-
|
|
857
|
-
//
|
|
858
|
-
//
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
};
|
|
869
|
-
const map = this._miniMapOptions();
|
|
870
|
-
if (map) {
|
|
871
|
-
claim(this._corner(map.position, map.width ?? MINIMAP_W, map.height ?? MINIMAP_H, 'bottom-right'), map.position);
|
|
872
|
-
}
|
|
873
|
-
const buttons = this._controlButtons();
|
|
874
|
-
if (buttons.length > 0) {
|
|
875
|
-
const first = buttons[0].rect;
|
|
876
|
-
const last = buttons[buttons.length - 1].rect;
|
|
877
|
-
claim(unionRects(first, last), this._controlsOptions()?.position ?? 'bottom-left');
|
|
878
|
-
}
|
|
879
|
-
// From here on, device pixels: `scrollContents` and the claims speak
|
|
880
|
-
// core's units. The bands round *up* and the blit is what is left, so
|
|
881
|
-
// the three sit edge to edge on whole pixels — a claim overlapping the
|
|
882
|
-
// blit rect by a pixel is a foreign claim, and declines the blit.
|
|
1220
|
+
// The furniture — minimap, zoom controls — stays put while the graph
|
|
1221
|
+
// moves under it: pinned inside the region, which core repaints after
|
|
1222
|
+
// the copy, with the stale image the copy dragged along (react-x11
|
|
1223
|
+
// #682, 2.22). It used to be carved out of the region, and a region is
|
|
1224
|
+
// one rectangle, so controls in one bottom corner and the minimap in the
|
|
1225
|
+
// other carved a band the pane's full width — every card, label and edge
|
|
1226
|
+
// in it repainted on every pan frame: 47 fps over the stress example's
|
|
1227
|
+
// widgets on XQuartz, where a pane with no furniture pans at 80.
|
|
1228
|
+
//
|
|
1229
|
+
// Device pixels, rounded out, and a pixel over: a panel's own claims —
|
|
1230
|
+
// the minimap's view box moves with every pan — must land inside its
|
|
1231
|
+
// pinned rect, or they decline the blit.
|
|
883
1232
|
const box = this.contentBox();
|
|
884
|
-
|
|
885
|
-
const bottom = Math.ceil(toDevice(bottomBand, s));
|
|
886
|
-
const blit = {
|
|
887
|
-
x: box.x,
|
|
888
|
-
y: box.y + top,
|
|
889
|
-
width: box.width,
|
|
890
|
-
height: box.height - top - bottom,
|
|
891
|
-
};
|
|
892
|
-
if (blit.width < 64 * s || blit.height < 64 * s)
|
|
1233
|
+
if (box.width < 64 * s || box.height < 64 * s)
|
|
893
1234
|
return false;
|
|
894
|
-
if (Math.abs(dx) >=
|
|
1235
|
+
if (Math.abs(dx) >= box.width || Math.abs(dy) >= box.height)
|
|
895
1236
|
return false;
|
|
1237
|
+
const pinned = [];
|
|
1238
|
+
const pin = (rect) => {
|
|
1239
|
+
const x0 = Math.max(box.x, Math.floor(rect.x) - 1);
|
|
1240
|
+
const y0 = Math.max(box.y, Math.floor(rect.y) - 1);
|
|
1241
|
+
const x1 = Math.min(box.x + box.width, Math.ceil(rect.x + rect.width) + 1);
|
|
1242
|
+
const y1 = Math.min(box.y + box.height, Math.ceil(rect.y + rect.height) + 1);
|
|
1243
|
+
if (x1 > x0 && y1 > y0) {
|
|
1244
|
+
pinned.push({ x: x0, y: y0, width: x1 - x0, height: y1 - y0 });
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
if (this._panelCanvases.length > 0) {
|
|
1248
|
+
// Painted on canvases of their own, over mounted bodies: a node laid
|
|
1249
|
+
// over the region is pinned by its paint bounds, which carry core's
|
|
1250
|
+
// slop round its box.
|
|
1251
|
+
for (const canvas of this._panelCanvases) {
|
|
1252
|
+
const reach = canvas.paintBounds?.();
|
|
1253
|
+
if (reach)
|
|
1254
|
+
pin(reach);
|
|
1255
|
+
}
|
|
896
1256
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
this.
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
}
|
|
1257
|
+
else {
|
|
1258
|
+
const map = this._miniMapOptions();
|
|
1259
|
+
if (map) {
|
|
1260
|
+
pin(this._device(this._corner(map.position, map.width ?? MINIMAP_W, map.height ?? MINIMAP_H, 'bottom-right')));
|
|
1261
|
+
}
|
|
1262
|
+
const buttons = this._controlButtons();
|
|
1263
|
+
if (buttons.length > 0) {
|
|
1264
|
+
const first = buttons[0].rect;
|
|
1265
|
+
const last = buttons[buttons.length - 1].rect;
|
|
1266
|
+
pin(this._device(unionRects(first, last)));
|
|
1267
|
+
}
|
|
908
1268
|
}
|
|
1269
|
+
this.scrollContents(box, dx, dy, riders ? [riders] : null, pinned);
|
|
1270
|
+
this._blittedPanAt = now();
|
|
909
1271
|
return true;
|
|
910
1272
|
}
|
|
911
1273
|
/** Zoom about a point that must not move — the pointer under a wheel, the
|
|
@@ -948,6 +1310,27 @@ export class FlowGraphNode extends Node {
|
|
|
948
1310
|
}));
|
|
949
1311
|
}
|
|
950
1312
|
// --- the imperative surface (`FlowInstance`) -----------------------------
|
|
1313
|
+
/**
|
|
1314
|
+
* `fn`, as a call through the handle. A programmatic pan — an animation
|
|
1315
|
+
* loop stepping `setViewport` — moves the pane at once and the bodies'
|
|
1316
|
+
* box through React, and on React's schedule the box caught up with the
|
|
1317
|
+
* pane in jumps: several steps' worth in one commit, landing in a frame
|
|
1318
|
+
* whose blit shifted by one. A rider that moves by more than the blit is
|
|
1319
|
+
* a layout move, so that frame repainted the pane whole — a fifth of the
|
|
1320
|
+
* frames of a pan over mounted bodies (`_blitPan`). Marked like a
|
|
1321
|
+
* gesture's, the emission commits before the call returns, and the box
|
|
1322
|
+
* moves by exactly the pan in the frame that blits it.
|
|
1323
|
+
*/
|
|
1324
|
+
fromHandle(fn) {
|
|
1325
|
+
const was = this._handleSync;
|
|
1326
|
+
this._handleSync = true;
|
|
1327
|
+
try {
|
|
1328
|
+
return fn();
|
|
1329
|
+
}
|
|
1330
|
+
finally {
|
|
1331
|
+
this._handleSync = was;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
951
1334
|
getViewport() {
|
|
952
1335
|
return { ...this._viewport() };
|
|
953
1336
|
}
|
|
@@ -1069,11 +1452,12 @@ export class FlowGraphNode extends Node {
|
|
|
1069
1452
|
/** The topmost node under a window point, or null. Walks paint order
|
|
1070
1453
|
* backwards, which is what "topmost" means. */
|
|
1071
1454
|
_nodeAt(x, y) {
|
|
1455
|
+
const sv = this._screenViewport();
|
|
1072
1456
|
for (let i = this._order.length - 1; i >= 0; i--) {
|
|
1073
1457
|
const entry = this._order[i];
|
|
1074
1458
|
if (entry.node.hidden)
|
|
1075
1459
|
continue;
|
|
1076
|
-
if (rectContains(this._screenRect(entry), { x, y }))
|
|
1460
|
+
if (rectContains(this._screenRect(entry, sv), { x, y }))
|
|
1077
1461
|
return entry;
|
|
1078
1462
|
}
|
|
1079
1463
|
return null;
|
|
@@ -1081,6 +1465,7 @@ export class FlowGraphNode extends Node {
|
|
|
1081
1465
|
_handleAt(x, y, accept) {
|
|
1082
1466
|
const zoom = this._viewport().zoom;
|
|
1083
1467
|
const reach = Math.max(7, (HANDLE_RADIUS + HANDLE_SLOP) * zoom);
|
|
1468
|
+
const sv = this._screenViewport();
|
|
1084
1469
|
for (let i = this._order.length - 1; i >= 0; i--) {
|
|
1085
1470
|
const entry = this._order[i];
|
|
1086
1471
|
if (entry.node.hidden)
|
|
@@ -1089,7 +1474,7 @@ export class FlowGraphNode extends Node {
|
|
|
1089
1474
|
continue;
|
|
1090
1475
|
// cheap reject: the handles are on the node's border, so nothing more
|
|
1091
1476
|
// than `reach` outside its box can be one
|
|
1092
|
-
const rect = this._screenRect(entry);
|
|
1477
|
+
const rect = this._screenRect(entry, sv);
|
|
1093
1478
|
if (x < rect.x - reach ||
|
|
1094
1479
|
x > rect.x + rect.width + reach ||
|
|
1095
1480
|
y < rect.y - reach ||
|
|
@@ -1115,6 +1500,7 @@ export class FlowGraphNode extends Node {
|
|
|
1115
1500
|
_gripAt(x, y) {
|
|
1116
1501
|
const zoom = this._viewport().zoom;
|
|
1117
1502
|
const reach = Math.max(6, (RESIZE_GRIP + RESIZE_SLOP) * zoom);
|
|
1503
|
+
const sv = this._screenViewport();
|
|
1118
1504
|
for (let i = this._order.length - 1; i >= 0; i--) {
|
|
1119
1505
|
const entry = this._order[i];
|
|
1120
1506
|
if (entry.node.hidden)
|
|
@@ -1122,7 +1508,7 @@ export class FlowGraphNode extends Node {
|
|
|
1122
1508
|
const grips = this._grips(entry);
|
|
1123
1509
|
if (grips.length === 0)
|
|
1124
1510
|
continue;
|
|
1125
|
-
const rect = this._screenRect(entry);
|
|
1511
|
+
const rect = this._screenRect(entry, sv);
|
|
1126
1512
|
if (x < rect.x - reach ||
|
|
1127
1513
|
x > rect.x + rect.width + reach ||
|
|
1128
1514
|
y < rect.y - reach ||
|
|
@@ -1168,6 +1554,13 @@ export class FlowGraphNode extends Node {
|
|
|
1168
1554
|
* the square root of the gap (see `paths.ts`) — `8` where the shoulder
|
|
1169
1555
|
* uses `6.25`, so the bound is generous rather than tight.
|
|
1170
1556
|
*/
|
|
1557
|
+
/** The coarse box with the label's plate in it: what an edge that
|
|
1558
|
+
* changes has to claim, where its route alone would leave a plate's
|
|
1559
|
+
* ends. */
|
|
1560
|
+
_edgeInkBox(edge) {
|
|
1561
|
+
const box = this._edgeCoarseBox(edge);
|
|
1562
|
+
return box && withLabelPlate(box, edge, this._viewport().zoom);
|
|
1563
|
+
}
|
|
1171
1564
|
_edgeCoarseBox(edge) {
|
|
1172
1565
|
const source = this._byId.get(edge.source);
|
|
1173
1566
|
const target = this._byId.get(edge.target);
|
|
@@ -1175,27 +1568,11 @@ export class FlowGraphNode extends Node {
|
|
|
1175
1568
|
return null;
|
|
1176
1569
|
if (source.node.hidden || target.node.hidden)
|
|
1177
1570
|
return null;
|
|
1178
|
-
|
|
1179
|
-
const a = this._screenRect(source);
|
|
1180
|
-
const b = this._screenRect(target);
|
|
1181
|
-
const span = Math.hypot(a.x + a.width / 2 - (b.x + b.width / 2), a.y + a.height / 2 - (b.y + b.height / 2));
|
|
1182
|
-
const slack = Math.max(5, EDGE_SLOP * zoom) +
|
|
1183
|
-
Math.max(EDGE_STEP_OFFSET * 3 * zoom, 8 * Math.sqrt(zoom * span));
|
|
1184
|
-
const x = Math.min(a.x, b.x) - slack;
|
|
1185
|
-
const y = Math.min(a.y, b.y) - slack;
|
|
1186
|
-
return {
|
|
1187
|
-
x,
|
|
1188
|
-
y,
|
|
1189
|
-
width: Math.max(a.x + a.width, b.x + b.width) + slack - x,
|
|
1190
|
-
height: Math.max(a.y + a.height, b.y + b.height) + slack - y,
|
|
1191
|
-
};
|
|
1571
|
+
return edgeCoarseBox(this._screenViewport(), this._source(source), this._source(target), this._scale);
|
|
1192
1572
|
}
|
|
1193
1573
|
/** The drawn handle, in screen pixels. Capped as well as floored: a dot
|
|
1194
1574
|
* that scaled all the way up would be a saucer at 2.5×, and the thing it
|
|
1195
1575
|
* marks — a point on the border — does not get bigger. */
|
|
1196
|
-
_handleRadius() {
|
|
1197
|
-
return clamp(HANDLE_RADIUS * this._viewport().zoom, 2.5, 6);
|
|
1198
|
-
}
|
|
1199
1576
|
_connectable(entry) {
|
|
1200
1577
|
return ((entry.node.connectable ?? this._bool('nodesConnectable', true)) !== false);
|
|
1201
1578
|
}
|
|
@@ -1206,59 +1583,19 @@ export class FlowGraphNode extends Node {
|
|
|
1206
1583
|
/** Pick the handle an edge attaches to: the one it names, else the only
|
|
1207
1584
|
* one of the right type, else the one facing the other end. */
|
|
1208
1585
|
_endpoint(entry, handleId, type, towards) {
|
|
1209
|
-
|
|
1210
|
-
if (handleId != null) {
|
|
1211
|
-
const named = anchors.find((a) => a.id === handleId);
|
|
1212
|
-
if (named)
|
|
1213
|
-
return named;
|
|
1214
|
-
}
|
|
1215
|
-
const typed = anchors.filter((a) => a.type === type);
|
|
1216
|
-
const candidates = typed.length > 0 ? typed : anchors;
|
|
1217
|
-
if (candidates.length === 0)
|
|
1218
|
-
return null;
|
|
1219
|
-
if (candidates.length === 1 || !towards)
|
|
1220
|
-
return candidates[0];
|
|
1221
|
-
let best = candidates[0];
|
|
1222
|
-
let bestDistance = Infinity;
|
|
1223
|
-
for (const anchor of candidates) {
|
|
1224
|
-
const d = Math.hypot(anchor.x - towards.x, anchor.y - towards.y);
|
|
1225
|
-
if (d < bestDistance) {
|
|
1226
|
-
bestDistance = d;
|
|
1227
|
-
best = anchor;
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
return best;
|
|
1586
|
+
return endpoint({ node: entry.node, rect: this.rectOf(entry), specs: entry.specs }, handleId, type, towards);
|
|
1231
1587
|
}
|
|
1232
1588
|
/** An edge's polyline, in **screen** pixels, plus the two ends it joins.
|
|
1233
1589
|
* Null when either end is missing — an edge to a node that was removed is
|
|
1234
1590
|
* a normal state during an edit, not an error. */
|
|
1235
1591
|
_edgeGeometry(edge) {
|
|
1236
|
-
const
|
|
1237
|
-
const
|
|
1238
|
-
if (!
|
|
1239
|
-
return null;
|
|
1240
|
-
if (sourceEntry.node.hidden || targetEntry.node.hidden)
|
|
1592
|
+
const source = this._byId.get(edge.source);
|
|
1593
|
+
const target = this._byId.get(edge.target);
|
|
1594
|
+
if (!source || !target)
|
|
1241
1595
|
return null;
|
|
1242
|
-
|
|
1243
|
-
const targetCentre = this._centre(targetEntry);
|
|
1244
|
-
const from = this._endpoint(sourceEntry, edge.sourceHandle, 'source', targetCentre);
|
|
1245
|
-
const to = this._endpoint(targetEntry, edge.targetHandle, 'target', sourceCentre);
|
|
1246
|
-
if (!from || !to)
|
|
1596
|
+
if (source.node.hidden || target.node.hidden)
|
|
1247
1597
|
return null;
|
|
1248
|
-
|
|
1249
|
-
const s = this._toScreen(from);
|
|
1250
|
-
const t = this._toScreen(to);
|
|
1251
|
-
const points = edgePath(edge.type, { x: s.x, y: s.y, position: from.position }, { x: t.x, y: t.y, position: to.position }, {
|
|
1252
|
-
stepOffset: EDGE_STEP_OFFSET * zoom,
|
|
1253
|
-
radius: 8 * zoom,
|
|
1254
|
-
scale: zoom,
|
|
1255
|
-
loop: edge.source === edge.target,
|
|
1256
|
-
});
|
|
1257
|
-
return { points, from, to };
|
|
1258
|
-
}
|
|
1259
|
-
_centre(entry) {
|
|
1260
|
-
const r = this.rectOf(entry);
|
|
1261
|
-
return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
|
|
1598
|
+
return edgeRoute(this._screenViewport(), edge, this._source(source), this._source(target));
|
|
1262
1599
|
}
|
|
1263
1600
|
// --- pane furniture geometry --------------------------------------------
|
|
1264
1601
|
_corner(where, width, height, fallback) {
|
|
@@ -1326,23 +1663,49 @@ export class FlowGraphNode extends Node {
|
|
|
1326
1663
|
// A minimap that covers the pane it summarises is worse than none.
|
|
1327
1664
|
return null;
|
|
1328
1665
|
}
|
|
1329
|
-
const rects = [];
|
|
1330
|
-
for (const entry of this._entries) {
|
|
1331
|
-
if (!entry.node.hidden)
|
|
1332
|
-
rects.push(this.rectOf(entry));
|
|
1333
|
-
}
|
|
1334
1666
|
// The viewport joins the bounds, so panning off the graph still shows
|
|
1335
|
-
// where you are rather than pinning the box to an edge.
|
|
1667
|
+
// where you are rather than pinning the box to an edge. Worked out with
|
|
1668
|
+
// no box made per node: a drag asks this twice a step and again for
|
|
1669
|
+
// the pass (`_miniMapMoves`), over every node of the graph.
|
|
1336
1670
|
const v = this._viewport();
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1671
|
+
let minX = -v.x / v.zoom;
|
|
1672
|
+
let minY = -v.y / v.zoom;
|
|
1673
|
+
let maxX = minX + pane.width / v.zoom;
|
|
1674
|
+
let maxY = minY + pane.height / v.zoom;
|
|
1675
|
+
const resizing = this._resizeTo;
|
|
1676
|
+
for (const entry of this._entries) {
|
|
1677
|
+
const node = entry.node;
|
|
1678
|
+
if (node.hidden)
|
|
1679
|
+
continue;
|
|
1680
|
+
let x;
|
|
1681
|
+
let y;
|
|
1682
|
+
let w;
|
|
1683
|
+
let h;
|
|
1684
|
+
if (resizing && resizing.id === node.id) {
|
|
1685
|
+
({ x, y, width: w, height: h } = resizing.rect);
|
|
1686
|
+
}
|
|
1687
|
+
else {
|
|
1688
|
+
const p = this._positionOf(node);
|
|
1689
|
+
x = p.x;
|
|
1690
|
+
y = p.y;
|
|
1691
|
+
w = entry.width;
|
|
1692
|
+
h = entry.height;
|
|
1693
|
+
}
|
|
1694
|
+
if (x < minX)
|
|
1695
|
+
minX = x;
|
|
1696
|
+
if (y < minY)
|
|
1697
|
+
minY = y;
|
|
1698
|
+
if (x + w > maxX)
|
|
1699
|
+
maxX = x + w;
|
|
1700
|
+
if (y + h > maxY)
|
|
1701
|
+
maxY = y + h;
|
|
1702
|
+
}
|
|
1703
|
+
const bounds = {
|
|
1704
|
+
x: minX,
|
|
1705
|
+
y: minY,
|
|
1706
|
+
width: maxX - minX,
|
|
1707
|
+
height: maxY - minY,
|
|
1708
|
+
};
|
|
1346
1709
|
const pad = Math.max(bounds.width, bounds.height) * 0.05 + 10;
|
|
1347
1710
|
const padded = {
|
|
1348
1711
|
x: bounds.x - pad,
|
|
@@ -1362,14 +1725,6 @@ export class FlowGraphNode extends Node {
|
|
|
1362
1725
|
y: (y - oy) / map.scale + map.bounds.y,
|
|
1363
1726
|
};
|
|
1364
1727
|
}
|
|
1365
|
-
_miniToScreen(map, p) {
|
|
1366
|
-
const ox = map.panel.x + (map.panel.width - map.bounds.width * map.scale) / 2;
|
|
1367
|
-
const oy = map.panel.y + (map.panel.height - map.bounds.height * map.scale) / 2;
|
|
1368
|
-
return {
|
|
1369
|
-
x: ox + (p.x - map.bounds.x) * map.scale,
|
|
1370
|
-
y: oy + (p.y - map.bounds.y) * map.scale,
|
|
1371
|
-
};
|
|
1372
|
-
}
|
|
1373
1728
|
// --- gestures ------------------------------------------------------------
|
|
1374
1729
|
defaultMouseDown(ev) {
|
|
1375
1730
|
if (this.props.disabled)
|
|
@@ -1561,10 +1916,10 @@ export class FlowGraphNode extends Node {
|
|
|
1561
1916
|
gesture.x = ev.x;
|
|
1562
1917
|
gesture.y = ev.y;
|
|
1563
1918
|
this._selectBox(gesture);
|
|
1564
|
-
// The box
|
|
1565
|
-
//
|
|
1566
|
-
//
|
|
1567
|
-
this.
|
|
1919
|
+
// The box is all that moves this step; a node crossing the boundary
|
|
1920
|
+
// changes `selected`, and that repaints through the controlled round
|
|
1921
|
+
// trip like any other selection change.
|
|
1922
|
+
this._claimSelectStep(oldBox, selectBoxRect(gesture));
|
|
1568
1923
|
return;
|
|
1569
1924
|
}
|
|
1570
1925
|
case 'minimap': {
|
|
@@ -1681,6 +2036,32 @@ export class FlowGraphNode extends Node {
|
|
|
1681
2036
|
}
|
|
1682
2037
|
this._prop('onNodeClick')?.(ev, entry.node);
|
|
1683
2038
|
}
|
|
2039
|
+
/**
|
|
2040
|
+
* The nodes a drag is moving, once it has moved them: lifted out of the
|
|
2041
|
+
* rest of the graph for the rest of the gesture. Under GL the world is
|
|
2042
|
+
* built without them and they are a layer of their own (`glFrame`), so
|
|
2043
|
+
* each step packs them and their edges and leaves the world on the GPU;
|
|
2044
|
+
* in 2D the rest is two pictures a step copies from (`_liftShotFor`).
|
|
2045
|
+
* Null otherwise — and under GL the change back is a change of the
|
|
2046
|
+
* world's key, which rebuilds it with them in.
|
|
2047
|
+
*/
|
|
2048
|
+
_lifted() {
|
|
2049
|
+
const g = this._gesture;
|
|
2050
|
+
if (g?.kind !== 'drag' || !g.moved || g.ids.length === 0) {
|
|
2051
|
+
if (this._liftSet) {
|
|
2052
|
+
this._liftSet = null;
|
|
2053
|
+
this._liftFor = null;
|
|
2054
|
+
this._liftGen++;
|
|
2055
|
+
}
|
|
2056
|
+
return null;
|
|
2057
|
+
}
|
|
2058
|
+
if (this._liftFor !== g) {
|
|
2059
|
+
this._liftSet = new Set(g.ids);
|
|
2060
|
+
this._liftFor = g;
|
|
2061
|
+
this._liftGen++;
|
|
2062
|
+
}
|
|
2063
|
+
return this._liftSet;
|
|
2064
|
+
}
|
|
1684
2065
|
_dragStep(gesture, ev) {
|
|
1685
2066
|
const zoom = this._viewport().zoom;
|
|
1686
2067
|
const dx = (ev.x - gesture.startX) / zoom;
|
|
@@ -1721,25 +2102,43 @@ export class FlowGraphNode extends Node {
|
|
|
1721
2102
|
}
|
|
1722
2103
|
// Old places first, new places second: the union is everything this
|
|
1723
2104
|
// step uncovers plus everything it covers, and nothing else — with the
|
|
1724
|
-
// edges that follow the moved nodes, coarse, on both sides.
|
|
2105
|
+
// edges that follow the moved nodes, coarse, on both sides. Under GL
|
|
2106
|
+
// there is nothing to size: the step is a frame of the surface's, and
|
|
2107
|
+
// routing each moved node's edges for a rect nobody reads was a third
|
|
2108
|
+
// of a millisecond a step.
|
|
2109
|
+
const sized = !this._gl;
|
|
1725
2110
|
let damage = null;
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
2111
|
+
// …and in the minimap, the boxes the step moves them from
|
|
2112
|
+
const maps = sized && this._drawsMiniMap();
|
|
2113
|
+
const moves = [];
|
|
2114
|
+
if (sized) {
|
|
2115
|
+
for (const id of gesture.ids) {
|
|
2116
|
+
const entry = this._byId.get(id);
|
|
2117
|
+
if (!entry)
|
|
2118
|
+
continue;
|
|
1729
2119
|
damage = unionMaybe(damage, this._nodeDamage(entry));
|
|
2120
|
+
if (maps)
|
|
2121
|
+
moves.push({ entry, before: this.rectOf(entry) });
|
|
2122
|
+
}
|
|
1730
2123
|
}
|
|
1731
2124
|
this._dragTo = to;
|
|
1732
|
-
|
|
1733
|
-
const
|
|
1734
|
-
|
|
1735
|
-
|
|
2125
|
+
if (sized) {
|
|
2126
|
+
for (const id of gesture.ids) {
|
|
2127
|
+
const entry = this._byId.get(id);
|
|
2128
|
+
if (entry)
|
|
2129
|
+
damage = unionMaybe(damage, this._nodeDamage(entry));
|
|
2130
|
+
}
|
|
1736
2131
|
}
|
|
1737
2132
|
this._emitNodes(changes);
|
|
2133
|
+
const reason = this._lifted() ? 'lift' : 'content';
|
|
1738
2134
|
if (damage) {
|
|
1739
|
-
this._claim(inflateRect(damage, CULL_MARGIN),
|
|
2135
|
+
this._claim(inflateRect(damage, CULL_MARGIN), reason);
|
|
2136
|
+
if (maps)
|
|
2137
|
+
this._claimMiniMapMoves(moves, reason);
|
|
1740
2138
|
}
|
|
1741
2139
|
else {
|
|
1742
|
-
|
|
2140
|
+
// under GL: a frame, and — while the nodes are lifted — of them alone
|
|
2141
|
+
this._repaint(reason);
|
|
1743
2142
|
}
|
|
1744
2143
|
// Inside the gesture dispatch, deliberately: the body-rect setState this
|
|
1745
2144
|
// triggers is a discrete-priority update, so React commits the moved
|
|
@@ -1765,6 +2164,8 @@ export class FlowGraphNode extends Node {
|
|
|
1765
2164
|
minWidth: entry.node.minWidth ?? MIN_NODE_WIDTH,
|
|
1766
2165
|
minHeight: entry.node.minHeight ?? MIN_NODE_HEIGHT,
|
|
1767
2166
|
}, snap);
|
|
2167
|
+
const maps = this._drawsMiniMap();
|
|
2168
|
+
const before = this.rectOf(entry);
|
|
1768
2169
|
let damage = this._nodeDamage(entry);
|
|
1769
2170
|
this._resizeTo = { id: gesture.id, rect };
|
|
1770
2171
|
damage = unionRects(damage, this._nodeDamage(entry));
|
|
@@ -1786,6 +2187,8 @@ export class FlowGraphNode extends Node {
|
|
|
1786
2187
|
}
|
|
1787
2188
|
this._emitNodes(changes);
|
|
1788
2189
|
this._claim(inflateRect(damage, CULL_MARGIN), 'content');
|
|
2190
|
+
if (maps)
|
|
2191
|
+
this._claimMiniMapMoves([{ entry, before }], 'content');
|
|
1789
2192
|
this._emitBodies();
|
|
1790
2193
|
}
|
|
1791
2194
|
_connectStep(gesture, ev) {
|
|
@@ -1836,28 +2239,79 @@ export class FlowGraphNode extends Node {
|
|
|
1836
2239
|
/** The pending connection's polyline — one builder for the paint and for
|
|
1837
2240
|
* the damage it has to claim, so the two cannot disagree. */
|
|
1838
2241
|
_connectionPath(gesture) {
|
|
1839
|
-
|
|
1840
|
-
const from = this._toScreen(gesture.from);
|
|
1841
|
-
const target = gesture.to ?? gesture.pointer;
|
|
1842
|
-
const to = this._toScreen(target);
|
|
1843
|
-
const toPosition = gesture.to?.position ??
|
|
1844
|
-
(gesture.from.position === 'left'
|
|
1845
|
-
? 'right'
|
|
1846
|
-
: gesture.from.position === 'right'
|
|
1847
|
-
? 'left'
|
|
1848
|
-
: gesture.from.position === 'top'
|
|
1849
|
-
? 'bottom'
|
|
1850
|
-
: 'top');
|
|
1851
|
-
return edgePath('bezier', { x: from.x, y: from.y, position: gesture.from.position }, { x: to.x, y: to.y, position: toPosition }, {
|
|
1852
|
-
stepOffset: EDGE_STEP_OFFSET * v.zoom,
|
|
1853
|
-
radius: 8 * v.zoom,
|
|
1854
|
-
scale: v.zoom,
|
|
1855
|
-
});
|
|
2242
|
+
return connectionPath(this._screenViewport(), gesture);
|
|
1856
2243
|
}
|
|
1857
2244
|
_validConnection(connection) {
|
|
1858
2245
|
const check = this._prop('isValidConnection');
|
|
1859
2246
|
return check ? check(connection) !== false : true;
|
|
1860
2247
|
}
|
|
2248
|
+
/**
|
|
2249
|
+
* What one step of a box selection changes on screen: a band round each
|
|
2250
|
+
* side of the box that moved, across the whole of both boxes. That covers
|
|
2251
|
+
* the outline where it was and where it is, and the tint where one box
|
|
2252
|
+
* lies over the graph and the other does not; a side that held still —
|
|
2253
|
+
* the two the gesture is anchored by — changed only where a moving side
|
|
2254
|
+
* shortened or lengthened it, which that side's band covers. Claiming the
|
|
2255
|
+
* box round both repainted everything under the selection, every node and
|
|
2256
|
+
* edge in it, on every step: 21 ms of a step on the stress example's
|
|
2257
|
+
* fan-out, which is what made a sweep lag the pointer.
|
|
2258
|
+
*
|
|
2259
|
+
* The bands are cut on the device grid so that none of them overlaps
|
|
2260
|
+
* another: core merges overlapping damage into the box round it, which is
|
|
2261
|
+
* the whole selection again — and a pass over a translucent tint must not
|
|
2262
|
+
* paint it twice. Claimed past this element's own `invalidate`, whose one
|
|
2263
|
+
* job here would be to repaint the minimap and the controls on their
|
|
2264
|
+
* canvases, and a box selection moves nothing on either.
|
|
2265
|
+
*
|
|
2266
|
+
* Under GL the box is the overlay's, drawn every frame: a step is a frame
|
|
2267
|
+
* and not a change to the world, which a claim would have rebuilt.
|
|
2268
|
+
*/
|
|
2269
|
+
_claimSelectStep(from, to) {
|
|
2270
|
+
if (this._gl) {
|
|
2271
|
+
this._glRequest();
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
const s = this._scale;
|
|
2275
|
+
const lo = (v) => Math.floor(toDevice(v - SELECT_BAND, s));
|
|
2276
|
+
const hi = (v) => Math.ceil(toDevice(v + SELECT_BAND, s));
|
|
2277
|
+
/** The bands round the sides that moved along one axis, merged where
|
|
2278
|
+
* they meet, in device pixels. */
|
|
2279
|
+
const bands = (a0, a1, b0, b1) => {
|
|
2280
|
+
const out = [];
|
|
2281
|
+
if (a0 !== b0)
|
|
2282
|
+
out.push([lo(Math.min(a0, b0)), hi(Math.max(a0, b0))]);
|
|
2283
|
+
if (a1 !== b1)
|
|
2284
|
+
out.push([lo(Math.min(a1, b1)), hi(Math.max(a1, b1))]);
|
|
2285
|
+
out.sort((m, n) => m[0] - n[0]);
|
|
2286
|
+
if (out.length === 2 && out[1][0] <= out[0][1]) {
|
|
2287
|
+
return [[out[0][0], Math.max(out[0][1], out[1][1])]];
|
|
2288
|
+
}
|
|
2289
|
+
return out;
|
|
2290
|
+
};
|
|
2291
|
+
const outer = unionRects(from, to);
|
|
2292
|
+
const x0 = lo(outer.x);
|
|
2293
|
+
const x1 = hi(outer.x + outer.width);
|
|
2294
|
+
const y0 = lo(outer.y);
|
|
2295
|
+
const y1 = hi(outer.y + outer.height);
|
|
2296
|
+
const columns = bands(from.x, from.x + from.width, to.x, to.x + to.width);
|
|
2297
|
+
const rows = bands(from.y, from.y + from.height, to.y, to.y + to.height);
|
|
2298
|
+
const claim = (x, y, width, height) => {
|
|
2299
|
+
if (width > 0 && height > 0) {
|
|
2300
|
+
super.invalidate(false, { x, y, width, height }, 'style-state');
|
|
2301
|
+
}
|
|
2302
|
+
};
|
|
2303
|
+
for (const [c0, c1] of columns)
|
|
2304
|
+
claim(c0, y0, c1 - c0, y1 - y0);
|
|
2305
|
+
for (const [r0, r1] of rows) {
|
|
2306
|
+
// across both boxes, less the columns already claimed
|
|
2307
|
+
let x = x0;
|
|
2308
|
+
for (const [c0, c1] of columns) {
|
|
2309
|
+
claim(x, r0, c0 - x, r1 - r0);
|
|
2310
|
+
x = Math.max(x, c1);
|
|
2311
|
+
}
|
|
2312
|
+
claim(x, r0, x1 - x, r1 - r0);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
1861
2315
|
_selectBox(gesture) {
|
|
1862
2316
|
const a = this._toGraph(gesture.startX, gesture.startY);
|
|
1863
2317
|
const b = this._toGraph(gesture.x, gesture.y);
|
|
@@ -1984,11 +2438,12 @@ export class FlowGraphNode extends Node {
|
|
|
1984
2438
|
a11yScene() {
|
|
1985
2439
|
this._sync();
|
|
1986
2440
|
const pane = this._pane();
|
|
2441
|
+
const sv = screenViewport(this._viewport(), pane);
|
|
1987
2442
|
const items = [];
|
|
1988
2443
|
for (const entry of this._order) {
|
|
1989
2444
|
if (entry.node.hidden)
|
|
1990
2445
|
continue;
|
|
1991
|
-
const rect = this._screenRect(entry);
|
|
2446
|
+
const rect = this._screenRect(entry, sv);
|
|
1992
2447
|
if (!rectsOverlap(rect, pane))
|
|
1993
2448
|
continue;
|
|
1994
2449
|
const data = entry.node.data;
|
|
@@ -2153,7 +2608,12 @@ export class FlowGraphNode extends Node {
|
|
|
2153
2608
|
// changed — a moved node repaints the box it moved through, an edit to
|
|
2154
2609
|
// anything else on the visual list repaints in full, and handler churn
|
|
2155
2610
|
// repaints nothing.
|
|
2156
|
-
|
|
2611
|
+
// Boxes rather than the box around them: the nodes a box selection
|
|
2612
|
+
// takes in one step lie along both of its moving edges, and the box
|
|
2613
|
+
// round an L of nodes is the whole selection again.
|
|
2614
|
+
let damage = [];
|
|
2615
|
+
let edgesRedrawn = false;
|
|
2616
|
+
this._liftOnly = false;
|
|
2157
2617
|
if (!shallowEqual(nextProps.nodeTypes, before.nodeTypes) ||
|
|
2158
2618
|
!shallowEqual(nextProps.defaultEdgeOptions, before.defaultEdgeOptions)) {
|
|
2159
2619
|
this._sync();
|
|
@@ -2161,16 +2621,20 @@ export class FlowGraphNode extends Node {
|
|
|
2161
2621
|
}
|
|
2162
2622
|
else {
|
|
2163
2623
|
if (nextProps.edges !== before.edges) {
|
|
2164
|
-
|
|
2624
|
+
const edgeDamage = this._applyEdges(this._rawEdges());
|
|
2625
|
+
if (edgeDamage === 'full')
|
|
2626
|
+
damage = 'full';
|
|
2627
|
+
else if (edgeDamage) {
|
|
2628
|
+
damage.push(edgeDamage);
|
|
2629
|
+
edgesRedrawn = true;
|
|
2630
|
+
}
|
|
2165
2631
|
}
|
|
2166
2632
|
if (damage !== 'full' && nextProps.nodes !== before.nodes) {
|
|
2167
2633
|
const nodeDamage = this._applyNodes(this._nodes());
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
? damage
|
|
2173
|
-
: unionRects(damage, nodeDamage);
|
|
2634
|
+
if (nodeDamage === 'full')
|
|
2635
|
+
damage = 'full';
|
|
2636
|
+
else
|
|
2637
|
+
damage.push(...nodeDamage);
|
|
2174
2638
|
}
|
|
2175
2639
|
}
|
|
2176
2640
|
// `fitView` is a one-shot: turning it on later refits, and it stays off
|
|
@@ -2187,19 +2651,38 @@ export class FlowGraphNode extends Node {
|
|
|
2187
2651
|
}
|
|
2188
2652
|
}
|
|
2189
2653
|
}
|
|
2654
|
+
// What moved, restyled or reshaped is the graph's own content: under GL
|
|
2655
|
+
// it is a frame of the surface's, where a claim billed as `props` went
|
|
2656
|
+
// on to a window pass over the 2D pane under it — every step of a drag
|
|
2657
|
+
// an app stores, 1.2 ms of each, for pixels the surface covers.
|
|
2190
2658
|
if (damage === 'full')
|
|
2191
2659
|
this._repaint('props');
|
|
2192
|
-
else
|
|
2193
|
-
|
|
2660
|
+
else {
|
|
2661
|
+
// a commit of lifted nodes alone is a frame of the lifted layer
|
|
2662
|
+
const reason = this._liftOnly && !edgesRedrawn ? 'lift' : 'content';
|
|
2663
|
+
for (const box of damage)
|
|
2664
|
+
this._claim(box, reason);
|
|
2665
|
+
}
|
|
2666
|
+
this._liftOnly = false;
|
|
2194
2667
|
// Core re-reads the scene for aria-prop commits; a `nodes`/`edges`
|
|
2195
2668
|
// change is invisible to it, so the re-read is asked for by name.
|
|
2196
2669
|
// Free when no assistive technology is listening.
|
|
2197
|
-
if (damage)
|
|
2670
|
+
if (damage === 'full' || damage.length > 0)
|
|
2198
2671
|
this.notifyA11ySceneChanged();
|
|
2199
2672
|
}
|
|
2200
2673
|
destroySubtree() {
|
|
2674
|
+
this._dropZoomShot();
|
|
2675
|
+
this._dropLiftShot();
|
|
2676
|
+
this._glRequest = null;
|
|
2677
|
+
if (this._bodiesRest != null)
|
|
2678
|
+
timers.clearTimeout?.(this._bodiesRest);
|
|
2679
|
+
this._bodiesRest = null;
|
|
2680
|
+
if (this._zoomRest != null)
|
|
2681
|
+
timers.clearTimeout?.(this._zoomRest);
|
|
2682
|
+
this._zoomRest = null;
|
|
2201
2683
|
this._stopAnimation();
|
|
2202
2684
|
this._dropGridTile();
|
|
2685
|
+
this._sceneCache.clear();
|
|
2203
2686
|
super.destroySubtree();
|
|
2204
2687
|
}
|
|
2205
2688
|
_startAnimation() {
|
|
@@ -2207,10 +2690,62 @@ export class FlowGraphNode extends Node {
|
|
|
2207
2690
|
return;
|
|
2208
2691
|
this._animTimer =
|
|
2209
2692
|
timers.setInterval?.(() => {
|
|
2210
|
-
|
|
2693
|
+
// A pan that blits copies the pane's pixels, dashes and all, and a
|
|
2694
|
+
// tick claims the dashes inside the band it copies — which declines
|
|
2695
|
+
// the copy: every frame a tick landed in repainted the whole pane,
|
|
2696
|
+
// a sixth of a pan's frames and all of its stutter. So the dashes
|
|
2697
|
+
// sit the pan out, phase and all, and what the pan copies and what
|
|
2698
|
+
// it draws agree; they march again once it has held still.
|
|
2699
|
+
//
|
|
2700
|
+
// And the view moving at all is what they wait out, not a blit that
|
|
2701
|
+
// happened: a tick declines the blit it lands beside, so a pan
|
|
2702
|
+
// whose frames ran past the wait had its ticks back, each one
|
|
2703
|
+
// cancelling the next blit and repainting the pane — over the
|
|
2704
|
+
// stress example's widgets in a large window, 2 frames a second,
|
|
2705
|
+
// ticks and pan steps taking turns. A step since the last tick, or
|
|
2706
|
+
// one within two ticks, holds the dashes still whatever a frame
|
|
2707
|
+
// costs. Under GL a tick is a uniform and blits nothing, and they
|
|
2708
|
+
// march through a pan as before.
|
|
2709
|
+
//
|
|
2710
|
+
// A 2D tick is a repaint of the box the dashes are in, which over a
|
|
2711
|
+
// dense graph in a large window is most of the pane: 75 ms a tick on
|
|
2712
|
+
// XQuartz, against a timer of 60. So the wait scales with what
|
|
2713
|
+
// frames cost: the view held still for two frames' worth, and one
|
|
2714
|
+
// tick every two frames at most — the dashes slow down before the
|
|
2715
|
+
// thread is theirs, and a pan whose frames come slowly still sees
|
|
2716
|
+
// them hold still.
|
|
2717
|
+
const t = now();
|
|
2718
|
+
const lastTick = this._tickAt;
|
|
2719
|
+
this._tickAt = t;
|
|
2720
|
+
if (t - this._blittedPanAt < ANIMATION_MS * 2)
|
|
2721
|
+
return;
|
|
2722
|
+
let steps = 1;
|
|
2723
|
+
if (!this._gl) {
|
|
2724
|
+
const cost = this._tickCost * 2;
|
|
2725
|
+
const still = Math.max(ANIMATION_MS * 2, cost);
|
|
2726
|
+
if (this._viewMovedAt > lastTick || t - this._viewMovedAt < still) {
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2729
|
+
// a tick under ANIMATION_MS apart, less a millisecond of timer
|
|
2730
|
+
// slop; further while ticks are dear — and then the dashes cover
|
|
2731
|
+
// the ground the skipped ticks would have, so they slow down in
|
|
2732
|
+
// steps and not in speed
|
|
2733
|
+
const since = t - this._marchedAt;
|
|
2734
|
+
if (since < Math.max(ANIMATION_MS, cost) - 1)
|
|
2735
|
+
return;
|
|
2736
|
+
steps = Math.min(4, Math.max(1, Math.round(since / ANIMATION_MS)));
|
|
2737
|
+
}
|
|
2738
|
+
this._marchedAt = t;
|
|
2739
|
+
this._dashPhase += ANIMATION_SPEED * steps;
|
|
2211
2740
|
// the box the last paint saw animated edges in, not the pane: a
|
|
2212
|
-
// marching dash should not cost a full grid repaint per tick
|
|
2213
|
-
|
|
2741
|
+
// marching dash should not cost a full grid repaint per tick. And
|
|
2742
|
+
// only the part of it the pane shows — an edge on its way out of
|
|
2743
|
+
// the pane takes the box past it, and claimed whole, a tick
|
|
2744
|
+
// repainted everything the window has beside the graph.
|
|
2745
|
+
const box = this._animBox ? this._device(this._animBox) : this.abs;
|
|
2746
|
+
const shown = intersectRects(box, this.contentBox());
|
|
2747
|
+
if (shown)
|
|
2748
|
+
this.invalidate(false, shown, 'animation');
|
|
2214
2749
|
}, ANIMATION_MS) ?? null;
|
|
2215
2750
|
}
|
|
2216
2751
|
_stopAnimation() {
|
|
@@ -2221,6 +2756,8 @@ export class FlowGraphNode extends Node {
|
|
|
2221
2756
|
}
|
|
2222
2757
|
// --- painting ------------------------------------------------------------
|
|
2223
2758
|
paint(ctx) {
|
|
2759
|
+
this._frameTick();
|
|
2760
|
+
const paintStart = now();
|
|
2224
2761
|
// What this pass is repainting. The renderer paints each damage rect as
|
|
2225
2762
|
// its own clipped pass; content outside it survives on the window, so
|
|
2226
2763
|
// everything we skip here is content the last frame already drew — the
|
|
@@ -2245,9 +2782,35 @@ export class FlowGraphNode extends Node {
|
|
|
2245
2782
|
super.paint(ctx);
|
|
2246
2783
|
if (!this._visible())
|
|
2247
2784
|
return;
|
|
2248
|
-
|
|
2785
|
+
if (this._gl) {
|
|
2786
|
+
// The surface over this box draws the graph. What the 2D pass still
|
|
2787
|
+
// owes is the part of a paint that is not drawing — the derived graph
|
|
2788
|
+
// brought up to date, the bodies placed — and then the frame itself,
|
|
2789
|
+
// asked of the surface. Every claim this element makes arrives here,
|
|
2790
|
+
// so every change the 2D renderer would repaint is one GL frame.
|
|
2791
|
+
this._sync();
|
|
2792
|
+
this._emitBodies();
|
|
2793
|
+
this._emitPanels();
|
|
2794
|
+
this._glRequest?.();
|
|
2795
|
+
return;
|
|
2796
|
+
}
|
|
2797
|
+
// Mid-gesture, labels come from the layouts they already have, scaled:
|
|
2798
|
+
// shaping every one again at each step of a zoom was half of what a 2D
|
|
2799
|
+
// step cost, at sizes the next step moves off. Only where the context
|
|
2800
|
+
// scales text with its transform — X11's draws glyphs at the size they
|
|
2801
|
+
// were shaped at — and set exactly once the zoom rests (`_restZoom`).
|
|
2802
|
+
const approximateText = this._zoomMoving() &&
|
|
2803
|
+
ctx.scalesText === true;
|
|
2804
|
+
const painter = createPainter(ctx, {
|
|
2805
|
+
...this._textOptions(),
|
|
2806
|
+
approximateText,
|
|
2807
|
+
});
|
|
2249
2808
|
if (!painter)
|
|
2250
2809
|
return; // a backend with no path API: geometry only
|
|
2810
|
+
if (approximateText) {
|
|
2811
|
+
this._textApproximated = true;
|
|
2812
|
+
this._restZoom();
|
|
2813
|
+
}
|
|
2251
2814
|
this._sync();
|
|
2252
2815
|
this._painting = true;
|
|
2253
2816
|
if (this._fitPending) {
|
|
@@ -2298,31 +2861,116 @@ export class FlowGraphNode extends Node {
|
|
|
2298
2861
|
{ x: x + width - radius, y: y + height - radius },
|
|
2299
2862
|
].some((corner) => rectsOverlap({ x: corner.x, y: corner.y, width: radius, height: radius }, this._frameClip)));
|
|
2300
2863
|
painter.clipRect(x, y, width, height, nearCorner ? radius : 0);
|
|
2301
|
-
|
|
2302
|
-
//
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
this.
|
|
2317
|
-
this.
|
|
2318
|
-
this.
|
|
2319
|
-
|
|
2320
|
-
|
|
2864
|
+
const started = now();
|
|
2865
|
+
// Not under a drag or a connection: the picture would hold the moving
|
|
2866
|
+
// node, or the line, where they were
|
|
2867
|
+
// …and only for a graph that costs more than a frame to paint live
|
|
2868
|
+
// (`ZOOM_SHOT_WORTH_MS`, `ZOOM_SHOT_DROP_MS`)
|
|
2869
|
+
const zooming = this._zoomMoving() && !this._gesture;
|
|
2870
|
+
const paintMs = zooming ? this._graphPaintMs() : 0;
|
|
2871
|
+
if (zooming && this._zoomShot !== null && paintMs < ZOOM_SHOT_DROP_MS) {
|
|
2872
|
+
this._dropZoomShot();
|
|
2873
|
+
}
|
|
2874
|
+
const shot = zooming && (this._zoomShot !== null || paintMs > ZOOM_SHOT_WORTH_MS)
|
|
2875
|
+
? this._zoomShotFor(palette, approximateText)
|
|
2876
|
+
: null;
|
|
2877
|
+
// A drag's nodes over pictures of the rest (`_liftShotFor`), in 2D and
|
|
2878
|
+
// not mid-zoom.
|
|
2879
|
+
const lift = shot || approximateText ? null : this._lifted();
|
|
2880
|
+
const liftShot = lift ? this._liftShotFor(palette, lift) : null;
|
|
2881
|
+
if (!lift && this._liftShot)
|
|
2882
|
+
this._dropLiftShot();
|
|
2883
|
+
let scene;
|
|
2884
|
+
if (liftShot && lift) {
|
|
2885
|
+
// the pictures, with the moved nodes' edges between them and the
|
|
2886
|
+
// nodes over both; the pane's furniture over it all
|
|
2887
|
+
const lifted = buildScene(this._sceneInput(palette, { lift: { ids: lift, only: true } }));
|
|
2888
|
+
scene = buildScene(this._sceneInput(palette, 'overlay'));
|
|
2889
|
+
const built = now();
|
|
2890
|
+
this._compositeLift(ctx, liftShot, liftShot.under, null);
|
|
2891
|
+
paintGraphEdges(painter, lifted);
|
|
2892
|
+
this._compositeLift(ctx, liftShot, liftShot.over, liftShot.cards);
|
|
2893
|
+
paintGraphNodes(painter, lifted);
|
|
2894
|
+
paintFloat(painter, scene);
|
|
2895
|
+
this._reportFrame(built - started, now() - built);
|
|
2896
|
+
}
|
|
2897
|
+
else if (shot) {
|
|
2898
|
+
// the graph and the ground under it as the gesture drew them, scaled;
|
|
2899
|
+
// both live wherever the picture does not reach; and the pane's
|
|
2900
|
+
// furniture over them at the zoom of the moment
|
|
2901
|
+
scene = buildScene(this._sceneInput(palette, 'overlay'));
|
|
2902
|
+
const bands = this._uncovered(shot);
|
|
2903
|
+
const input = bands.length > 0 ? this._sceneInput(palette) : null;
|
|
2904
|
+
const live = input
|
|
2905
|
+
? bands.map((band) => ({
|
|
2906
|
+
band,
|
|
2907
|
+
scene: buildScene({ ...input, clip: band }),
|
|
2908
|
+
}))
|
|
2909
|
+
: [];
|
|
2910
|
+
const built = now();
|
|
2911
|
+
this._compositeShot(ctx, shot);
|
|
2912
|
+
for (const { band, scene: part } of live) {
|
|
2913
|
+
painter.save();
|
|
2914
|
+
painter.clipRect(band.x, band.y, band.width, band.height, 0);
|
|
2915
|
+
paintGround(painter, part, this._grid);
|
|
2916
|
+
paintGraph(painter, part);
|
|
2917
|
+
painter.restore();
|
|
2918
|
+
}
|
|
2919
|
+
paintFloat(painter, scene);
|
|
2920
|
+
this._textApproximated = true;
|
|
2921
|
+
this._restZoom();
|
|
2922
|
+
this._reportFrame(built - started, now() - built);
|
|
2923
|
+
}
|
|
2924
|
+
else {
|
|
2925
|
+
// Only a pass over the whole pane may change how edges are stroked;
|
|
2926
|
+
// one over part of it follows what the rest was drawn with, and asks
|
|
2927
|
+
// for the whole when the edges on the pane have crossed over
|
|
2928
|
+
// (`EDGE_BATCH`) — so a pan's copy never meets a strip drawn the
|
|
2929
|
+
// other way.
|
|
2930
|
+
const input = this._sceneInput(palette);
|
|
2931
|
+
const whole = this._coversPane(clip);
|
|
2932
|
+
scene = buildScene(whole ? { ...input, batch: undefined, batchWas: this._batch } : input);
|
|
2933
|
+
if (whole)
|
|
2934
|
+
this._batch = scene.batch;
|
|
2935
|
+
else if (batchesEdges(scene.edgesOnPane, this._batch) !== this._batch) {
|
|
2936
|
+
this._repaint('content');
|
|
2937
|
+
}
|
|
2938
|
+
const built = now();
|
|
2939
|
+
paintScene(painter, scene, this._grid);
|
|
2940
|
+
const done = now();
|
|
2941
|
+
this._reportFrame(built - started, done - built);
|
|
2942
|
+
// a zoom's step, whole and live: what the next one would cost
|
|
2943
|
+
if (this._zoomMoving() && this._wholePane(clip)) {
|
|
2944
|
+
this._noteGraphPaint(done - started);
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
// The box the dash ticks invalidate comes off the *drawn* geometry; a
|
|
2948
|
+
// pass that culled every animated edge keeps the one before it, because
|
|
2949
|
+
// the endpoints did not move, or that move's own damage would have
|
|
2950
|
+
// redrawn them here.
|
|
2951
|
+
if (scene.animBox)
|
|
2952
|
+
this._animBox = scene.animBox;
|
|
2321
2953
|
painter.restore();
|
|
2322
2954
|
this._painting = false;
|
|
2323
2955
|
this._frameClip = null;
|
|
2956
|
+
{
|
|
2957
|
+
// What a frame costs, for the dash timer (`_startAnimation`): this
|
|
2958
|
+
// paint, and how long the server took to answer the frame before it
|
|
2959
|
+
// — on XQuartz most of it.
|
|
2960
|
+
const latency = this.root?.window?.frameLatency;
|
|
2961
|
+
// A reply can take seconds on a server that is waiting for a
|
|
2962
|
+
// display — a Mac's asleep — and that is when ticks most need to
|
|
2963
|
+
// hold; clamped rather than dropped, so one outlier does not stand
|
|
2964
|
+
// for long in the mean.
|
|
2965
|
+
const cost = now() -
|
|
2966
|
+
paintStart +
|
|
2967
|
+
(typeof latency === 'number' && latency > 0
|
|
2968
|
+
? Math.min(latency, 2000)
|
|
2969
|
+
: 0);
|
|
2970
|
+
this._tickCost = this._tickCost * 0.7 + cost * 0.3;
|
|
2971
|
+
}
|
|
2324
2972
|
// The timer exists only while something on screen needs it.
|
|
2325
|
-
if (animated)
|
|
2973
|
+
if (scene.animated)
|
|
2326
2974
|
this._startAnimation();
|
|
2327
2975
|
else
|
|
2328
2976
|
this._stopAnimation();
|
|
@@ -2336,147 +2984,1264 @@ export class FlowGraphNode extends Node {
|
|
|
2336
2984
|
// to put the node bodies it mounts, and it is answered from the geometry
|
|
2337
2985
|
// this frame just used — the same numbers, never a second derivation.
|
|
2338
2986
|
this._emitBodies();
|
|
2987
|
+
this._emitPanels();
|
|
2988
|
+
// a pass of the frame that drew the latest zoom step ends here
|
|
2989
|
+
if (this._zoomFrameAt === this._lastFrameAt)
|
|
2990
|
+
this._zoomDrawnAt = now();
|
|
2339
2991
|
}
|
|
2340
2992
|
/**
|
|
2341
|
-
*
|
|
2342
|
-
*
|
|
2343
|
-
*
|
|
2344
|
-
*
|
|
2993
|
+
* Everything `buildScene` reads, gathered from the element once.
|
|
2994
|
+
*
|
|
2995
|
+
* The two node lists differ on purpose: `nodes` is the paint order with a
|
|
2996
|
+
* dragged node lifted to the top, and `all` is every node, because the
|
|
2997
|
+
* minimap summarises the graph rather than the pass.
|
|
2345
2998
|
*/
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
const
|
|
2360
|
-
const
|
|
2361
|
-
|
|
2362
|
-
const height = rect.height - header - inset;
|
|
2363
|
-
if (width <= 1 || height <= 1)
|
|
2364
|
-
continue;
|
|
2365
|
-
bodies.push({
|
|
2366
|
-
id: entry.node.id,
|
|
2367
|
-
// pane-relative and logical, because that is what an absolutely
|
|
2368
|
-
// positioned box beside the pane is laid out against, and in
|
|
2369
|
-
x: Math.round(rect.x + inset - pane.x),
|
|
2370
|
-
y: Math.round(rect.y + header - pane.y),
|
|
2371
|
-
width: Math.round(width),
|
|
2372
|
-
height: Math.round(height),
|
|
2373
|
-
zoom: v.zoom,
|
|
2374
|
-
selected: entry.node.selected ?? false,
|
|
2375
|
-
});
|
|
2999
|
+
invalidate(layout, damage, reason) {
|
|
3000
|
+
// Whatever changed the graph may have changed the minimap, which
|
|
3001
|
+
// `<Flow>` may be painting on a canvas of its own; a change to this
|
|
3002
|
+
// element's props may have changed the controls too. The controls draw
|
|
3003
|
+
// nothing of the graph, and repainting them with every step of a drag
|
|
3004
|
+
// repainted the minimap with them — each canvas builds the panels its
|
|
3005
|
+
// box reaches, and theirs reached both. Each claims its own box: a
|
|
3006
|
+
// claim with no region is the whole window, which made every pan step
|
|
3007
|
+
// and dash tick a full frame. A dash tick changes neither — the minimap
|
|
3008
|
+
// draws no dashes — and repainting both canvases on every one kept an
|
|
3009
|
+
// idle pane painting them 17 times a second, and put a claim in the
|
|
3010
|
+
// middle of any pan frame it landed in.
|
|
3011
|
+
if (reason !== 'animation') {
|
|
3012
|
+
const canvases = reason === 'props' ? this._panelCanvases : this._miniMapCanvases();
|
|
3013
|
+
for (const canvas of canvases)
|
|
3014
|
+
canvas.invalidate(false, canvas, 'props');
|
|
2376
3015
|
}
|
|
2377
|
-
//
|
|
2378
|
-
//
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
.
|
|
2383
|
-
|
|
3016
|
+
// A dash tick moves the phase, a uniform on the GPU, so it is no change
|
|
3017
|
+
// to the world. A pan or a zoom never gets here under GL.
|
|
3018
|
+
if (reason === 'lift')
|
|
3019
|
+
this._liftVersion++;
|
|
3020
|
+
else if (reason !== 'animation')
|
|
3021
|
+
this._worldVersion++;
|
|
3022
|
+
// …and what a 2D zoom gesture composites is the graph as it was: a
|
|
3023
|
+
// change to it is a picture that no longer is
|
|
3024
|
+
if (reason !== 'animation' && reason !== 'scroll')
|
|
3025
|
+
this._dropZoomShot();
|
|
3026
|
+
// Under GL the surface over this box draws the graph and the 2D pane
|
|
3027
|
+
// under it shows nothing: a claim of its pixels was a window pass over
|
|
3028
|
+
// them, unseen, whose one job was to reach `paint` and ask the surface
|
|
3029
|
+
// for a frame — a BeginDraw, a walk and a commit on every step of a
|
|
3030
|
+
// drag, 2.6 ms of each. The graph's own claims ask for the frame
|
|
3031
|
+
// directly; the frame does what `paint` did (`glFrame`). A layout change
|
|
3032
|
+
// and a change of props — which may move the panels — still go through.
|
|
3033
|
+
if (!layout &&
|
|
3034
|
+
damage != null &&
|
|
3035
|
+
(reason === 'content' ||
|
|
3036
|
+
reason === 'style-state' ||
|
|
3037
|
+
reason === 'animation' ||
|
|
3038
|
+
reason === 'lift') &&
|
|
3039
|
+
this._gl) {
|
|
3040
|
+
this._glRequest();
|
|
2384
3041
|
return;
|
|
2385
|
-
|
|
2386
|
-
|
|
3042
|
+
}
|
|
3043
|
+
// a lift is this element's own bookkeeping; core knows it as content
|
|
3044
|
+
super.invalidate(layout, damage, reason === 'lift' ? 'content' : reason);
|
|
2387
3045
|
}
|
|
2388
|
-
/**
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
3046
|
+
/** A 2D frame's cost so far, while its flush is still painting passes. */
|
|
3047
|
+
_frameCost = null;
|
|
3048
|
+
/**
|
|
3049
|
+
* One `onFrame` per window flush, however many damage passes it painted.
|
|
3050
|
+
* The window paints its passes one after another in a single task, so a
|
|
3051
|
+
* microtask queued by the first runs after the last — and reports the
|
|
3052
|
+
* frame whole, which is what a frame rate counts.
|
|
3053
|
+
*/
|
|
3054
|
+
_reportFrame(sceneMs, drawMs) {
|
|
3055
|
+
if (!this.props.onFrame)
|
|
2394
3056
|
return;
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
return;
|
|
2399
|
-
// Doubling rather than clamping keeps the grid *aligned* to the graph
|
|
2400
|
-
// while zooming out: every visible line is still a real one.
|
|
2401
|
-
while (step < MIN_GRID_PX)
|
|
2402
|
-
step *= 2;
|
|
2403
|
-
const color = options.color ?? palette.grid;
|
|
2404
|
-
const pane = this._pane();
|
|
2405
|
-
const { x, y, width, height } = region;
|
|
2406
|
-
const originX = pane.x + v.x;
|
|
2407
|
-
const originY = pane.y + v.y;
|
|
2408
|
-
if (this._paintGridPattern(painter, options, step, color, region))
|
|
2409
|
-
return;
|
|
2410
|
-
const firstX = originX + Math.ceil((x - originX) / step) * step;
|
|
2411
|
-
const firstY = originY + Math.ceil((y - originY) / step) * step;
|
|
2412
|
-
if (options.variant === 'lines') {
|
|
2413
|
-
const runs = [];
|
|
2414
|
-
for (let gx = firstX; gx <= x + width; gx += step) {
|
|
2415
|
-
runs.push([
|
|
2416
|
-
{ x: gx, y },
|
|
2417
|
-
{ x: gx, y: y + height },
|
|
2418
|
-
]);
|
|
2419
|
-
}
|
|
2420
|
-
for (let gy = firstY; gy <= y + height; gy += step) {
|
|
2421
|
-
runs.push([
|
|
2422
|
-
{ x, y: gy },
|
|
2423
|
-
{ x: x + width, y: gy },
|
|
2424
|
-
]);
|
|
2425
|
-
}
|
|
2426
|
-
painter.strokeRuns(runs, { stroke: color, lineWidth: 1 });
|
|
2427
|
-
return;
|
|
2428
|
-
}
|
|
2429
|
-
if (options.variant === 'cross') {
|
|
2430
|
-
const arm = Math.max(2, options.size * 3 * v.zoom);
|
|
2431
|
-
const runs = [];
|
|
2432
|
-
for (let gx = firstX; gx <= x + width; gx += step) {
|
|
2433
|
-
for (let gy = firstY; gy <= y + height; gy += step) {
|
|
2434
|
-
runs.push([
|
|
2435
|
-
{ x: gx - arm, y: gy },
|
|
2436
|
-
{ x: gx + arm, y: gy },
|
|
2437
|
-
]);
|
|
2438
|
-
runs.push([
|
|
2439
|
-
{ x: gx, y: gy - arm },
|
|
2440
|
-
{ x: gx, y: gy + arm },
|
|
2441
|
-
]);
|
|
2442
|
-
}
|
|
2443
|
-
}
|
|
2444
|
-
painter.strokeRuns(runs, { stroke: color, lineWidth: 1 });
|
|
3057
|
+
if (this._frameCost) {
|
|
3058
|
+
this._frameCost.sceneMs += sceneMs;
|
|
3059
|
+
this._frameCost.drawMs += drawMs;
|
|
2445
3060
|
return;
|
|
2446
3061
|
}
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
3062
|
+
this._frameCost = { sceneMs, drawMs };
|
|
3063
|
+
void Promise.resolve().then(() => {
|
|
3064
|
+
const cost = this._frameCost;
|
|
3065
|
+
this._frameCost = null;
|
|
3066
|
+
if (!cost || this.destroyed)
|
|
3067
|
+
return;
|
|
3068
|
+
this._prop('onFrame')?.({
|
|
3069
|
+
renderer: 'retained',
|
|
3070
|
+
sceneMs: cost.sceneMs,
|
|
3071
|
+
packMs: 0,
|
|
3072
|
+
drawMs: cost.drawMs,
|
|
3073
|
+
drawCalls: 0,
|
|
3074
|
+
lines: 0,
|
|
3075
|
+
boxes: 0,
|
|
3076
|
+
triangles: 0,
|
|
3077
|
+
uploadBytes: 0,
|
|
3078
|
+
worldRebuilt: false,
|
|
3079
|
+
gaps: { text: 0, custom: 0 },
|
|
3080
|
+
bodies: this.bodyBudget(),
|
|
3081
|
+
});
|
|
3082
|
+
});
|
|
3083
|
+
}
|
|
3084
|
+
get _gl() {
|
|
3085
|
+
return this.props.renderer === 'gl' && this._glRequest != null;
|
|
3086
|
+
}
|
|
3087
|
+
/** What the GL renderer's label atlas sets strings with: this pane's app,
|
|
3088
|
+
* for its staging surface, and the options its 2D painter measures with
|
|
3089
|
+
* — so a label is cut and placed the same on both renderers. Null where
|
|
3090
|
+
* there is no text engine to ask. */
|
|
3091
|
+
glText() {
|
|
3092
|
+
const options = this._textOptions();
|
|
3093
|
+
return options.fonts ? { app: this.app, options } : null;
|
|
2454
3094
|
}
|
|
2455
3095
|
/**
|
|
2456
|
-
* The
|
|
2457
|
-
*
|
|
2458
|
-
*
|
|
3096
|
+
* The GL surface's hook: how it asks for frames. Set while a surface draws
|
|
3097
|
+
* this pane, cleared when it goes — and until it is set the pane draws
|
|
3098
|
+
* itself, so a surface that is still loading shows the 2D graph rather
|
|
3099
|
+
* than an empty box.
|
|
3100
|
+
*/
|
|
3101
|
+
setGlRequest(request) {
|
|
3102
|
+
if (this._glRequest === request)
|
|
3103
|
+
return;
|
|
3104
|
+
this._glRequest = request;
|
|
3105
|
+
// whichever renderer now draws, it draws the whole pane
|
|
3106
|
+
this._repaint('props');
|
|
3107
|
+
}
|
|
3108
|
+
/**
|
|
3109
|
+
* One GL frame's worth of scene, in the two layers the renderer keeps
|
|
3110
|
+
* apart (`./gl/renderer.ts`): the **world**, rebuilt only when it changed
|
|
3111
|
+
* since the key the surface last drew, and the **overlay**, every frame.
|
|
2459
3112
|
*
|
|
2460
|
-
*
|
|
3113
|
+
* The world is built at a pinned origin and culled to an *overscan* — the
|
|
3114
|
+
* view grown by a pane in every direction — so a pan inside it changes
|
|
3115
|
+
* nothing but `offset`. When the view leaves the overscan, or the zoom
|
|
3116
|
+
* moves, the overscan is re-centred and the world rebuilt; at every other
|
|
3117
|
+
* frame of a pan the world comes back `null` and the renderer draws the
|
|
3118
|
+
* buffers already on the GPU.
|
|
2461
3119
|
*
|
|
2462
|
-
*
|
|
2463
|
-
*
|
|
2464
|
-
*
|
|
2465
|
-
*
|
|
2466
|
-
*
|
|
2467
|
-
*
|
|
2468
|
-
*
|
|
2469
|
-
*
|
|
2470
|
-
*
|
|
2471
|
-
*
|
|
2472
|
-
*
|
|
2473
|
-
*
|
|
2474
|
-
*
|
|
3120
|
+
* A zoom *gesture* is drawn the same way: while the zoom keeps moving,
|
|
3121
|
+
* each step draws the world on the GPU magnified by `zoom` — the view's
|
|
3122
|
+
* zoom over the one it was built at — and the world is rebuilt at the
|
|
3123
|
+
* zoom the gesture stopped at, `GL_ZOOM_REST_MS` after its last step.
|
|
3124
|
+
* Rebuilding every step was the whole of a zoom's cost: a scene built,
|
|
3125
|
+
* packed and uploaded per step, 19 ms of it on a 300-node graph, where a
|
|
3126
|
+
* pan step is a uniform. What a magnified world gets wrong until then is
|
|
3127
|
+
* what a zoom does not scale linearly — a label's raster, the hairlines
|
|
3128
|
+
* with a floor of a pixel, the detail that appears past a zoom — which is
|
|
3129
|
+
* what every map does during a pinch. A single jump — a control's button,
|
|
3130
|
+
* `fitView`, `setViewport` — is not a stream and rebuilds at once, and so
|
|
3131
|
+
* does a step that would magnify one build by more than `GL_ZOOM_SPAN`
|
|
3132
|
+
* or show more of the graph than the overscan holds.
|
|
3133
|
+
*
|
|
3134
|
+
* The key is everything the world is a function of that this element can
|
|
3135
|
+
* name: its version (every repaint but a view change moves it), the
|
|
3136
|
+
* overscan, the zoom it was built at, and the palette and face — the two
|
|
3137
|
+
* a theme change moves, which core announces to the window rather than
|
|
3138
|
+
* to this element.
|
|
3139
|
+
*
|
|
3140
|
+
* Everything a 2D paint does around its drawing happens here too: the
|
|
3141
|
+
* pending fit, the dash timer, the first scene announcement, the bodies.
|
|
2475
3142
|
*/
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
3143
|
+
glFrame(lastKey, lastLifted = null) {
|
|
3144
|
+
if (!this._visible())
|
|
3145
|
+
return null;
|
|
3146
|
+
this._frameTick();
|
|
3147
|
+
this._sync();
|
|
3148
|
+
this._painting = true;
|
|
3149
|
+
this._frameClip = null;
|
|
3150
|
+
if (this._fitPending) {
|
|
3151
|
+
this._fitPending = false;
|
|
3152
|
+
this._fit(this._prop('fitViewOptions'));
|
|
3153
|
+
}
|
|
3154
|
+
const v = this._viewport();
|
|
3155
|
+
const pane = this._pane();
|
|
3156
|
+
const palette = this._palette();
|
|
3157
|
+
const lift = this._lifted();
|
|
3158
|
+
const keyOf = () => `${this._worldVersion}|${this._worldCullId}|${this._worldZoom}|` +
|
|
3159
|
+
`${this._fontSeen}|${JSON.stringify(palette)}|${this._liftGen}`;
|
|
3160
|
+
let key = keyOf();
|
|
3161
|
+
// The view in the world's own coordinates — graph × the zoom it was
|
|
3162
|
+
// built at, the pan left out: `offset` puts the pan back, and `zoom`
|
|
3163
|
+
// the rest of the zoom.
|
|
3164
|
+
const zoom = v.zoom / this._worldZoom;
|
|
3165
|
+
const view = {
|
|
3166
|
+
x: -v.x / zoom,
|
|
3167
|
+
y: -v.y / zoom,
|
|
3168
|
+
width: pane.width / zoom,
|
|
3169
|
+
height: pane.height / zoom,
|
|
3170
|
+
};
|
|
3171
|
+
const cull = this._worldCull;
|
|
3172
|
+
const inside = cull != null &&
|
|
3173
|
+
view.x >= cull.x &&
|
|
3174
|
+
view.y >= cull.y &&
|
|
3175
|
+
view.x + view.width <= cull.x + cull.width &&
|
|
3176
|
+
view.y + view.height <= cull.y + cull.height;
|
|
3177
|
+
const scaled = inside &&
|
|
3178
|
+
zoom !== 1 &&
|
|
3179
|
+
key === lastKey &&
|
|
3180
|
+
this._zoomStream &&
|
|
3181
|
+
now() - this._zoomAt < GL_ZOOM_REST_MS &&
|
|
3182
|
+
zoom <= GL_ZOOM_SPAN &&
|
|
3183
|
+
zoom >= 1 / GL_ZOOM_SPAN;
|
|
3184
|
+
if (scaled) {
|
|
3185
|
+
this._restZoom();
|
|
3186
|
+
}
|
|
3187
|
+
else if (!inside || zoom !== 1) {
|
|
3188
|
+
this._worldCull = {
|
|
3189
|
+
x: -v.x - pane.width,
|
|
3190
|
+
y: -v.y - pane.height,
|
|
3191
|
+
width: pane.width * 3,
|
|
3192
|
+
height: pane.height * 3,
|
|
3193
|
+
};
|
|
3194
|
+
this._worldZoom = v.zoom;
|
|
3195
|
+
this._worldCullId++;
|
|
3196
|
+
key = keyOf();
|
|
3197
|
+
}
|
|
3198
|
+
let world = null;
|
|
3199
|
+
if (key !== lastKey) {
|
|
3200
|
+
world = buildScene(this._sceneInput(palette, {
|
|
3201
|
+
world: this._worldCull,
|
|
3202
|
+
lift: lift ? { ids: lift, only: false } : undefined,
|
|
3203
|
+
}));
|
|
3204
|
+
this._worldAnimated = world.animated;
|
|
3205
|
+
}
|
|
3206
|
+
// The nodes a drag moves, and their edges, on their own: a step
|
|
3207
|
+
// rebuilds them and not the world (`_liftVersion`).
|
|
3208
|
+
let lifted;
|
|
3209
|
+
const liftedKey = lift ? `${this._liftVersion}|${key}` : null;
|
|
3210
|
+
if (lift && liftedKey !== lastLifted) {
|
|
3211
|
+
lifted = buildScene(this._sceneInput(palette, {
|
|
3212
|
+
world: this._worldCull,
|
|
3213
|
+
lift: { ids: lift, only: true },
|
|
3214
|
+
}));
|
|
3215
|
+
this._liftedAnimated = lifted.animated;
|
|
3216
|
+
}
|
|
3217
|
+
else if (!lift && lastLifted != null) {
|
|
3218
|
+
lifted = null;
|
|
3219
|
+
this._liftedAnimated = false;
|
|
3220
|
+
}
|
|
3221
|
+
if (world || lifted !== undefined) {
|
|
3222
|
+
// The dash timer lives as long as an animated edge is drawn; its
|
|
3223
|
+
// ticks move the phase, a uniform.
|
|
3224
|
+
if (this._worldAnimated || this._liftedAnimated)
|
|
3225
|
+
this._startAnimation();
|
|
3226
|
+
else
|
|
3227
|
+
this._stopAnimation();
|
|
3228
|
+
}
|
|
3229
|
+
const overlay = buildScene(this._sceneInput(palette, 'overlay'));
|
|
3230
|
+
this._painting = false;
|
|
3231
|
+
if (!this._sceneAnnounced) {
|
|
3232
|
+
this._sceneAnnounced = true;
|
|
3233
|
+
this.notifyA11ySceneChanged();
|
|
3234
|
+
}
|
|
3235
|
+
this._emitBodies();
|
|
3236
|
+
return {
|
|
3237
|
+
world,
|
|
3238
|
+
key,
|
|
3239
|
+
lifted,
|
|
3240
|
+
liftedKey,
|
|
3241
|
+
offset: { x: pane.x + v.x, y: pane.y + v.y },
|
|
3242
|
+
zoom: scaled ? zoom : 1,
|
|
3243
|
+
overlay,
|
|
3244
|
+
// the scene bakes `-phase * zoom` into a marching edge; the GPU is
|
|
3245
|
+
// handed the same number as a uniform
|
|
3246
|
+
phase: -this._dashPhase * v.zoom,
|
|
3247
|
+
moving: this._zoomMoving(),
|
|
3248
|
+
};
|
|
3249
|
+
}
|
|
3250
|
+
/** The frame that rebuilds the GL world once a zoom gesture stops — asked
|
|
3251
|
+
* for `GL_ZOOM_REST_MS` after its last step, however many steps the
|
|
3252
|
+
* timer outlived. */
|
|
3253
|
+
_restZoom() {
|
|
3254
|
+
if (this._zoomRest != null)
|
|
3255
|
+
return;
|
|
3256
|
+
const wait = this._zoomAt + GL_ZOOM_REST_MS - now();
|
|
3257
|
+
this._zoomRest = timers.setTimeout?.(() => {
|
|
3258
|
+
this._zoomRest = null;
|
|
3259
|
+
if (now() - this._zoomAt < GL_ZOOM_REST_MS)
|
|
3260
|
+
this._restZoom();
|
|
3261
|
+
else if (this._gl)
|
|
3262
|
+
this._glRequest?.();
|
|
3263
|
+
else if (this._textApproximated) {
|
|
3264
|
+
this._dropZoomShot();
|
|
3265
|
+
// …and in 2D, the labels the gesture drew scaled, set at their
|
|
3266
|
+
// own sizes
|
|
3267
|
+
this._textApproximated = false;
|
|
3268
|
+
this._repaint('content');
|
|
3269
|
+
}
|
|
3270
|
+
}, Math.max(0, wait) + 1);
|
|
3271
|
+
}
|
|
3272
|
+
/** A whole paint of the graph took `ms` (`_graphPaints`). */
|
|
3273
|
+
_noteGraphPaint(ms) {
|
|
3274
|
+
this._graphPaints.push(ms);
|
|
3275
|
+
if (this._graphPaints.length > GRAPH_PAINTS_KEPT)
|
|
3276
|
+
this._graphPaints.shift();
|
|
3277
|
+
}
|
|
3278
|
+
/** The median of the graph's last whole paints — the lower of the two
|
|
3279
|
+
* middle ones for an even count — or zero before the first. */
|
|
3280
|
+
_graphPaintMs() {
|
|
3281
|
+
const paints = this._graphPaints;
|
|
3282
|
+
if (paints.length === 0)
|
|
3283
|
+
return 0;
|
|
3284
|
+
const sorted = [...paints].sort((a, b) => a - b);
|
|
3285
|
+
return sorted[(sorted.length - 1) >> 1];
|
|
3286
|
+
}
|
|
3287
|
+
/** Whether a pass over `clip` painted the whole pane — what a zoom step
|
|
3288
|
+
* paints, and the only pass whose cost says what one would. */
|
|
3289
|
+
/** Whether a pass (`paintDamage()`, device pixels) repaints every pixel
|
|
3290
|
+
* of the pane, unlike {@link _wholePane}'s nine tenths. */
|
|
3291
|
+
_coversPane(clip) {
|
|
3292
|
+
if (clip === null)
|
|
3293
|
+
return true;
|
|
3294
|
+
const pane = this._pane();
|
|
3295
|
+
const c = this._logical(clip);
|
|
3296
|
+
const slack = 0.5 / this._scale;
|
|
3297
|
+
return (c.x <= pane.x + slack &&
|
|
3298
|
+
c.y <= pane.y + slack &&
|
|
3299
|
+
c.x + c.width >= pane.x + pane.width - slack &&
|
|
3300
|
+
c.y + c.height >= pane.y + pane.height - slack);
|
|
3301
|
+
}
|
|
3302
|
+
_wholePane(clip) {
|
|
3303
|
+
if (clip === null)
|
|
3304
|
+
return true;
|
|
3305
|
+
const pane = this._pane();
|
|
3306
|
+
const s = this._scale;
|
|
3307
|
+
return clip.width * clip.height >= 0.9 * pane.width * s * pane.height * s;
|
|
3308
|
+
}
|
|
3309
|
+
/**
|
|
3310
|
+
* The graph painted once onto a surface of its own for the zoom gesture
|
|
3311
|
+
* under way — made at the gesture's first step that finds none — or null
|
|
3312
|
+
* where there is no surface to paint on.
|
|
3313
|
+
*/
|
|
3314
|
+
_zoomShotFor(palette, approximateText) {
|
|
3315
|
+
const held = this._zoomShot;
|
|
3316
|
+
if (held) {
|
|
3317
|
+
// Kept while it is sharp enough and covers enough: magnified past
|
|
3318
|
+
// `GL_ZOOM_SPAN` it is soft, and a zoom out uncovers a ring the step
|
|
3319
|
+
// paints live, which past `ZOOM_SHOT_LIVE` of the pane costs what a
|
|
3320
|
+
// new picture does.
|
|
3321
|
+
const k = this._viewport().zoom / held.viewport.zoom;
|
|
3322
|
+
const pane = this._pane();
|
|
3323
|
+
const live = this._uncovered(held).reduce((sum, r) => sum + r.width * r.height, 0) /
|
|
3324
|
+
Math.max(1, pane.width * pane.height);
|
|
3325
|
+
if (k <= GL_ZOOM_SPAN && live <= ZOOM_SHOT_LIVE)
|
|
3326
|
+
return held;
|
|
3327
|
+
this._dropZoomShot();
|
|
3328
|
+
}
|
|
3329
|
+
const ctor = ntk.Surface;
|
|
3330
|
+
if (typeof ctor !== 'function')
|
|
3331
|
+
return null;
|
|
3332
|
+
const pane = this._pane();
|
|
3333
|
+
const scale = this._scale;
|
|
3334
|
+
const width = Math.max(1, Math.ceil(pane.width * scale));
|
|
3335
|
+
const height = Math.max(1, Math.ceil(pane.height * scale));
|
|
3336
|
+
let surface;
|
|
3337
|
+
try {
|
|
3338
|
+
surface = new ctor(this.app, { width, height, format: 'argb32' });
|
|
3339
|
+
}
|
|
3340
|
+
catch {
|
|
3341
|
+
return null;
|
|
3342
|
+
}
|
|
3343
|
+
const sctx = surface.getContext('2d');
|
|
3344
|
+
// its labels drawn as a step inside the gesture draws them — from the
|
|
3345
|
+
// sizes they have, where the window's context scales text — so the one
|
|
3346
|
+
// paint of the gesture shapes nothing the gesture would not
|
|
3347
|
+
const painter = sctx
|
|
3348
|
+
? createPainter(sctx, { ...this._textOptions(), approximateText })
|
|
3349
|
+
: null;
|
|
3350
|
+
if (!sctx || !painter || typeof sctx.translate !== 'function') {
|
|
3351
|
+
surface.destroy?.();
|
|
3352
|
+
return null;
|
|
3353
|
+
}
|
|
3354
|
+
// The surface's corner is the pane's, and the whole of the pane is in
|
|
3355
|
+
// it whatever this pass's damage, since every step after composites it.
|
|
3356
|
+
// The ground too: a grid at the fractional pitch a zoom passes through
|
|
3357
|
+
// is thousands of runs, 7 ms of Direct2D a step over the stress
|
|
3358
|
+
// lattice's pane once the graph was a picture. Drawn as runs here —
|
|
3359
|
+
// the tile is a pattern on the window's context, phased in its pixels,
|
|
3360
|
+
// and a zoom's pitch is a fraction of a pixel almost always.
|
|
3361
|
+
sctx.translate(-pane.x * scale, -pane.y * scale);
|
|
3362
|
+
const building = now();
|
|
3363
|
+
const whole = buildScene({ ...this._sceneInput(palette), clip: null });
|
|
3364
|
+
const built = now();
|
|
3365
|
+
paintGround(painter, whole);
|
|
3366
|
+
const grounded = now();
|
|
3367
|
+
paintGraph(painter, whole);
|
|
3368
|
+
// What a step would cost live: the scene and the graph. Not the ground,
|
|
3369
|
+
// which is runs here and a tile on the window — on X11 it made a
|
|
3370
|
+
// picture's paint half as dear again as a step, and kept the picture of
|
|
3371
|
+
// a graph that did not need one.
|
|
3372
|
+
this._noteGraphPaint(built - building + (now() - grounded));
|
|
3373
|
+
this._zoomShot = { surface, viewport: this._viewport(), width, height };
|
|
3374
|
+
return this._zoomShot;
|
|
3375
|
+
}
|
|
3376
|
+
/** Where the gesture's picture lands at the viewport of the moment, in
|
|
3377
|
+
* the pane's logical pixels: a world point drawn at `w·z₀ + v₀` is at
|
|
3378
|
+
* `w·z + v`, so the picture is scaled by `z / z₀` about the pane's corner
|
|
3379
|
+
* and moved by `v − v₀·z/z₀`. */
|
|
3380
|
+
_shotRect(shot) {
|
|
3381
|
+
const v = this._viewport();
|
|
3382
|
+
const v0 = shot.viewport;
|
|
3383
|
+
const k = v.zoom / v0.zoom;
|
|
3384
|
+
const pane = this._pane();
|
|
3385
|
+
const s = this._scale;
|
|
3386
|
+
return {
|
|
3387
|
+
x: pane.x + v.x - v0.x * k,
|
|
3388
|
+
y: pane.y + v.y - v0.y * k,
|
|
3389
|
+
width: (shot.width / s) * k,
|
|
3390
|
+
height: (shot.height / s) * k,
|
|
3391
|
+
};
|
|
3392
|
+
}
|
|
3393
|
+
_compositeShot(ctx, shot) {
|
|
3394
|
+
const draw = ctx.drawImage;
|
|
3395
|
+
if (typeof draw !== 'function')
|
|
3396
|
+
return;
|
|
3397
|
+
const to = this._shotRect(shot);
|
|
3398
|
+
const s = this._scale;
|
|
3399
|
+
draw.call(ctx, shot.surface, 0, 0, shot.width, shot.height, to.x * s, to.y * s, to.width * s, to.height * s);
|
|
3400
|
+
}
|
|
3401
|
+
/**
|
|
3402
|
+
* The pane this pass reaches that the gesture's picture does not: nothing
|
|
3403
|
+
* while zooming in about a point in the pane, a ring round it zooming out,
|
|
3404
|
+
* a side after a pan. Up to four bands — above, below, and either side
|
|
3405
|
+
* between them — each reaching a pixel under the picture's edge, which
|
|
3406
|
+
* its filtering leaves half there.
|
|
3407
|
+
*/
|
|
3408
|
+
_uncovered(shot) {
|
|
3409
|
+
const pane = this._frameClip
|
|
3410
|
+
? intersectRects(this._pane(), this._frameClip)
|
|
3411
|
+
: this._pane();
|
|
3412
|
+
if (!pane)
|
|
3413
|
+
return [];
|
|
3414
|
+
const inner = inflateRect(this._shotRect(shot), -1);
|
|
3415
|
+
const x0 = Math.max(pane.x, inner.x);
|
|
3416
|
+
const y0 = Math.max(pane.y, inner.y);
|
|
3417
|
+
const x1 = Math.min(pane.x + pane.width, inner.x + inner.width);
|
|
3418
|
+
const y1 = Math.min(pane.y + pane.height, inner.y + inner.height);
|
|
3419
|
+
if (x1 <= x0 || y1 <= y0)
|
|
3420
|
+
return [pane];
|
|
3421
|
+
const right = pane.x + pane.width;
|
|
3422
|
+
const bottom = pane.y + pane.height;
|
|
3423
|
+
const bands = [];
|
|
3424
|
+
if (y0 > pane.y) {
|
|
3425
|
+
bands.push({
|
|
3426
|
+
x: pane.x,
|
|
3427
|
+
y: pane.y,
|
|
3428
|
+
width: pane.width,
|
|
3429
|
+
height: y0 - pane.y,
|
|
3430
|
+
});
|
|
3431
|
+
}
|
|
3432
|
+
if (bottom > y1) {
|
|
3433
|
+
bands.push({ x: pane.x, y: y1, width: pane.width, height: bottom - y1 });
|
|
3434
|
+
}
|
|
3435
|
+
if (x0 > pane.x) {
|
|
3436
|
+
bands.push({ x: pane.x, y: y0, width: x0 - pane.x, height: y1 - y0 });
|
|
3437
|
+
}
|
|
3438
|
+
if (right > x1) {
|
|
3439
|
+
bands.push({ x: x1, y: y0, width: right - x1, height: y1 - y0 });
|
|
3440
|
+
}
|
|
3441
|
+
return bands;
|
|
3442
|
+
}
|
|
3443
|
+
_dropZoomShot() {
|
|
3444
|
+
this._zoomShot?.surface.destroy?.();
|
|
3445
|
+
this._zoomShot = null;
|
|
3446
|
+
}
|
|
3447
|
+
/**
|
|
3448
|
+
* The pictures a 2D drag copies from — made on the first pass of the
|
|
3449
|
+
* gesture that has its nodes lifted, and kept while the graph under them
|
|
3450
|
+
* and the view hold still. A drag step repainted the box round the moved
|
|
3451
|
+
* node and every edge on it, and an edge is as long as it is: the stress
|
|
3452
|
+
* lattice sends its last rows' edges back to its first nodes, so dragging
|
|
3453
|
+
* one repainted the window a step, every card and edge in it, at 46-57
|
|
3454
|
+
* fps on Cocoa. Cutting the damage finer did not help — core merges a
|
|
3455
|
+
* frame's rects down to four, and a long diagonal's pieces merge back to
|
|
3456
|
+
* most of the pane. Copying what did not move is what does.
|
|
3457
|
+
*
|
|
3458
|
+
* Null where there is no offscreen surface, and for a gesture whose view
|
|
3459
|
+
* moved while it held pictures: it paints live from then on, rather than
|
|
3460
|
+
* paint the graph twice a step.
|
|
3461
|
+
*/
|
|
3462
|
+
_liftShotFor(palette, lift) {
|
|
3463
|
+
const gesture = this._liftFor;
|
|
3464
|
+
if (!gesture || this._liftShotRefused === gesture)
|
|
3465
|
+
return null;
|
|
3466
|
+
const v = this._viewport();
|
|
3467
|
+
const pane = this._pane();
|
|
3468
|
+
const scale = this._scale;
|
|
3469
|
+
const width = Math.max(1, Math.ceil(pane.width * scale));
|
|
3470
|
+
const height = Math.max(1, Math.ceil(pane.height * scale));
|
|
3471
|
+
const held = this._liftShot;
|
|
3472
|
+
if (held) {
|
|
3473
|
+
const still = held.viewport.x === v.x &&
|
|
3474
|
+
held.viewport.y === v.y &&
|
|
3475
|
+
held.viewport.zoom === v.zoom;
|
|
3476
|
+
if (held.gesture === gesture &&
|
|
3477
|
+
still &&
|
|
3478
|
+
held.version === this._worldVersion &&
|
|
3479
|
+
held.width === width &&
|
|
3480
|
+
held.height === height) {
|
|
3481
|
+
return held;
|
|
3482
|
+
}
|
|
3483
|
+
this._dropLiftShot();
|
|
3484
|
+
if (held.gesture === gesture && !still) {
|
|
3485
|
+
this._liftShotRefused = gesture;
|
|
3486
|
+
return null;
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
const ctor = ntk.Surface;
|
|
3490
|
+
if (typeof ctor !== 'function')
|
|
3491
|
+
return null;
|
|
3492
|
+
const make = () => {
|
|
3493
|
+
let surface;
|
|
3494
|
+
try {
|
|
3495
|
+
surface = new ctor(this.app, { width, height, format: 'argb32' });
|
|
3496
|
+
}
|
|
3497
|
+
catch {
|
|
3498
|
+
return null;
|
|
3499
|
+
}
|
|
3500
|
+
const sctx = surface.getContext('2d');
|
|
3501
|
+
const painter = sctx ? createPainter(sctx, this._textOptions()) : null;
|
|
3502
|
+
if (!sctx || !painter || typeof sctx.translate !== 'function') {
|
|
3503
|
+
surface.destroy?.();
|
|
3504
|
+
return null;
|
|
3505
|
+
}
|
|
3506
|
+
// a clear ground: the cards' picture goes over the edges
|
|
3507
|
+
sctx.clearRect?.(0, 0, width, height);
|
|
3508
|
+
sctx.translate(-pane.x * scale, -pane.y * scale);
|
|
3509
|
+
return { surface, painter };
|
|
3510
|
+
};
|
|
3511
|
+
const under = make();
|
|
3512
|
+
const over = under ? make() : null;
|
|
3513
|
+
if (!under || !over) {
|
|
3514
|
+
under?.surface.destroy?.();
|
|
3515
|
+
return null;
|
|
3516
|
+
}
|
|
3517
|
+
const rest = buildScene({
|
|
3518
|
+
...this._sceneInput(palette, { lift: { ids: lift, only: false } }),
|
|
3519
|
+
clip: null,
|
|
3520
|
+
});
|
|
3521
|
+
paintGround(under.painter, rest);
|
|
3522
|
+
paintGraphEdges(under.painter, rest);
|
|
3523
|
+
paintGraphNodes(over.painter, rest);
|
|
3524
|
+
this._liftShot = {
|
|
3525
|
+
under: under.surface,
|
|
3526
|
+
over: over.surface,
|
|
3527
|
+
cards: rest.nodes.map((item) => inflateRect(item.rect, CULL_MARGIN)),
|
|
3528
|
+
gesture,
|
|
3529
|
+
version: this._worldVersion,
|
|
3530
|
+
viewport: { ...v },
|
|
3531
|
+
width,
|
|
3532
|
+
height,
|
|
3533
|
+
};
|
|
3534
|
+
return this._liftShot;
|
|
3535
|
+
}
|
|
3536
|
+
/**
|
|
3537
|
+
* One of a drag's pictures, copied into this pass's rect of the pane —
|
|
3538
|
+
* the whole pane on a full pass. The ground's picture is opaque and is
|
|
3539
|
+
* *copied*, which the server does as a copy rather than a blend (`copy`
|
|
3540
|
+
* is XRender's `Src`, and a memcpy on Cocoa); the cards' picture is
|
|
3541
|
+
* blended in only where a card is (`only`). On XQuartz the two blends of
|
|
3542
|
+
* the step's box — as big as the moved node's longest edge — were most
|
|
3543
|
+
* of what a drag cost the server.
|
|
3544
|
+
*/
|
|
3545
|
+
_compositeLift(ctx, shot, surface, only) {
|
|
3546
|
+
const c = ctx;
|
|
3547
|
+
if (typeof c.drawImage !== 'function')
|
|
3548
|
+
return;
|
|
3549
|
+
const pane = this._pane();
|
|
3550
|
+
const rect = this._frameClip ? intersectRects(pane, this._frameClip) : pane;
|
|
3551
|
+
if (!rect)
|
|
3552
|
+
return;
|
|
3553
|
+
if (only) {
|
|
3554
|
+
for (const card of only) {
|
|
3555
|
+
const part = intersectRects(rect, card);
|
|
3556
|
+
if (part)
|
|
3557
|
+
this._copyShotRect(c, shot, surface, part);
|
|
3558
|
+
}
|
|
3559
|
+
return;
|
|
3560
|
+
}
|
|
3561
|
+
const before = c.globalCompositeOperation;
|
|
3562
|
+
c.globalCompositeOperation = 'copy';
|
|
3563
|
+
try {
|
|
3564
|
+
this._copyShotRect(c, shot, surface, rect);
|
|
3565
|
+
}
|
|
3566
|
+
finally {
|
|
3567
|
+
c.globalCompositeOperation = before ?? 'source-over';
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
/** A rect of the pane, logical pixels, drawn from the same rect of a
|
|
3571
|
+
* picture of it — on whole device pixels, so nothing is resampled. */
|
|
3572
|
+
_copyShotRect(ctx, shot, surface, rect) {
|
|
3573
|
+
const pane = this._pane();
|
|
3574
|
+
const s = this._scale;
|
|
3575
|
+
const ox = pane.x * s;
|
|
3576
|
+
const oy = pane.y * s;
|
|
3577
|
+
const x0 = Math.max(0, Math.floor(rect.x * s - ox));
|
|
3578
|
+
const y0 = Math.max(0, Math.floor(rect.y * s - oy));
|
|
3579
|
+
const x1 = Math.min(shot.width, Math.ceil((rect.x + rect.width) * s - ox));
|
|
3580
|
+
const y1 = Math.min(shot.height, Math.ceil((rect.y + rect.height) * s - oy));
|
|
3581
|
+
if (x1 <= x0 || y1 <= y0)
|
|
3582
|
+
return;
|
|
3583
|
+
const w = x1 - x0;
|
|
3584
|
+
const h = y1 - y0;
|
|
3585
|
+
ctx.drawImage(surface, x0, y0, w, h, ox + x0, oy + y0, w, h);
|
|
3586
|
+
}
|
|
3587
|
+
_dropLiftShot() {
|
|
3588
|
+
this._liftShot?.under.destroy?.();
|
|
3589
|
+
this._liftShot?.over.destroy?.();
|
|
3590
|
+
this._liftShot = null;
|
|
3591
|
+
}
|
|
3592
|
+
/** Whether a zoom gesture is moving: a stream of zoom steps, the last of
|
|
3593
|
+
* them under `GL_ZOOM_REST_MS` ago. A single step — a button, `fitView`,
|
|
3594
|
+
* an app's `setViewport` — is not one. */
|
|
3595
|
+
_zoomMoving() {
|
|
3596
|
+
return this._zoomStream && now() - this._zoomAt < GL_ZOOM_REST_MS;
|
|
3597
|
+
}
|
|
3598
|
+
/** Whether this pass's damage reaches the minimap's corner at all —
|
|
3599
|
+
* answered from the panel's box alone, without resolving the map. */
|
|
3600
|
+
_miniMapReached() {
|
|
3601
|
+
const corner = this._miniMapCorner();
|
|
3602
|
+
if (!corner)
|
|
3603
|
+
return false;
|
|
3604
|
+
const clip = this._frameClip;
|
|
3605
|
+
return !clip || rectsOverlap(corner, clip);
|
|
3606
|
+
}
|
|
3607
|
+
/** The minimap's panel, in logical window pixels, or null without one. */
|
|
3608
|
+
_miniMapCorner() {
|
|
3609
|
+
const options = this._miniMapOptions();
|
|
3610
|
+
if (!options)
|
|
3611
|
+
return null;
|
|
3612
|
+
return this._corner(options.position, options.width ?? MINIMAP_W, options.height ?? MINIMAP_H, 'bottom-right');
|
|
3613
|
+
}
|
|
3614
|
+
/**
|
|
3615
|
+
* Everything `buildScene` reads, for one of three uses:
|
|
3616
|
+
*
|
|
3617
|
+
* - `'all'` — the 2D renderer's pass: the pane as it is, culled to this
|
|
3618
|
+
* pass's damage.
|
|
3619
|
+
* - `{ world }` — the GL graph: built at a **pinned origin** (the viewport's
|
|
3620
|
+
* translation and the pane's origin both left out, so a pan does not
|
|
3621
|
+
* change a vertex) and culled to the overscan `world` rather than the
|
|
3622
|
+
* pane, with none of the pane's furniture.
|
|
3623
|
+
* - `'overlay'` — the GL furniture: the pane's background, grid, selection,
|
|
3624
|
+
* minimap and controls, and no graph at all.
|
|
3625
|
+
*/
|
|
3626
|
+
/**
|
|
3627
|
+
* The order nodes are drawn in, bottom to top: `zIndex`, then selection,
|
|
3628
|
+
* then declaration (`_order`), with a node being dragged lifted over all
|
|
3629
|
+
* of them. The one order both halves of a node follow — its card in the
|
|
3630
|
+
* scene, its mounted body in `_emitBodies` — so a body is stacked over
|
|
3631
|
+
* the bodies of the cards under its own, and under the ones above.
|
|
3632
|
+
*/
|
|
3633
|
+
_paintOrder() {
|
|
3634
|
+
const dragging = this._dragTo;
|
|
3635
|
+
if (!dragging || dragging.size === 0)
|
|
3636
|
+
return this._order;
|
|
3637
|
+
const order = [];
|
|
3638
|
+
const lifted = [];
|
|
3639
|
+
for (const entry of this._order) {
|
|
3640
|
+
if (dragging.has(entry.node.id))
|
|
3641
|
+
lifted.push(entry);
|
|
3642
|
+
else
|
|
3643
|
+
order.push(entry);
|
|
3644
|
+
}
|
|
3645
|
+
return order.concat(lifted);
|
|
3646
|
+
}
|
|
3647
|
+
_sceneInput(palette, layer = 'all',
|
|
3648
|
+
/** The panels even while `<Flow>` paints them itself — for that paint. */
|
|
3649
|
+
panels = false) {
|
|
3650
|
+
// One card, for `paintCard`, which hands in the node itself: nothing
|
|
3651
|
+
// about the rest of the graph is read, where the whole input — every
|
|
3652
|
+
// node's source twice over, the minimap's walk for its bounds — was most
|
|
3653
|
+
// of what a card cost to paint, and a zoom repaints every one of them.
|
|
3654
|
+
if (layer === 'card') {
|
|
3655
|
+
const gesture = this._gesture;
|
|
3656
|
+
return {
|
|
3657
|
+
viewport: this._viewport(),
|
|
3658
|
+
pane: this._pane(),
|
|
3659
|
+
clip: null,
|
|
3660
|
+
palette,
|
|
3661
|
+
background: normalizeBackground(undefined),
|
|
3662
|
+
nodes: [],
|
|
3663
|
+
all: [],
|
|
3664
|
+
edges: [],
|
|
3665
|
+
dashPhase: this._dashPhase,
|
|
3666
|
+
hover: this._hover,
|
|
3667
|
+
connection: gesture?.kind === 'connect' ? gesture : null,
|
|
3668
|
+
selection: null,
|
|
3669
|
+
miniMap: null,
|
|
3670
|
+
controls: [],
|
|
3671
|
+
scale: this._scale,
|
|
3672
|
+
measure: this._measureBox,
|
|
3673
|
+
cache: this._sceneCache,
|
|
3674
|
+
batch: this._batch,
|
|
3675
|
+
};
|
|
3676
|
+
}
|
|
3677
|
+
const order = this._paintOrder();
|
|
3678
|
+
const gesture = this._gesture;
|
|
3679
|
+
// The panel cull comes before `_miniMap()`, which walks every node to
|
|
3680
|
+
// find the graph's bounds — during a drag that walk per pass would cost
|
|
3681
|
+
// more than the panel it skips. The dot for a mid-drag node goes stale
|
|
3682
|
+
// until the release repaints in full; that is the trade, and it is
|
|
3683
|
+
// deliberate.
|
|
3684
|
+
const world = typeof layer === 'object' ? (layer.world ?? null) : null;
|
|
3685
|
+
const lift = typeof layer === 'object' ? layer.lift : undefined;
|
|
3686
|
+
const overlay = layer === 'overlay';
|
|
3687
|
+
// the furniture is the overlay's: a lifted layer or the graph under it
|
|
3688
|
+
// has none of its own
|
|
3689
|
+
const map = !world && !lift && this._miniMapReached() ? this._miniMap() : null;
|
|
3690
|
+
// the fit the minimap on screen is painted with, from this pass on
|
|
3691
|
+
if (map && !panels && this._painting && this._panelCanvases.length === 0) {
|
|
3692
|
+
this._miniMapShown = map;
|
|
3693
|
+
}
|
|
3694
|
+
const viewport = this._viewport();
|
|
3695
|
+
const pane = this._pane();
|
|
3696
|
+
// An edge is the lifted layer's when either end is lifted.
|
|
3697
|
+
const edges = lift
|
|
3698
|
+
? this._edges.filter((e) => (lift.ids.has(e.source) || lift.ids.has(e.target)) === lift.only)
|
|
3699
|
+
: this._edges;
|
|
3700
|
+
let all;
|
|
3701
|
+
if (lift?.only) {
|
|
3702
|
+
// the lifted nodes and the far ends of their edges: all the lifted
|
|
3703
|
+
// layer routes to, where every node's source was most of a step
|
|
3704
|
+
const ids = new Set(lift.ids);
|
|
3705
|
+
for (const e of edges) {
|
|
3706
|
+
ids.add(e.source);
|
|
3707
|
+
ids.add(e.target);
|
|
3708
|
+
}
|
|
3709
|
+
all = [];
|
|
3710
|
+
for (const id of ids) {
|
|
3711
|
+
const entry = this._byId.get(id);
|
|
3712
|
+
if (entry)
|
|
3713
|
+
all.push(this._sourceOf(entry));
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
else if (overlay && !map) {
|
|
3717
|
+
all = [];
|
|
3718
|
+
}
|
|
3719
|
+
else if (layer === 'all' && this._painting) {
|
|
3720
|
+
all = this._frameLists(order).all;
|
|
3721
|
+
}
|
|
3722
|
+
else {
|
|
3723
|
+
all = this._entries.map((entry) => this._sourceOf(entry));
|
|
3724
|
+
}
|
|
3725
|
+
return {
|
|
3726
|
+
viewport: world ? { x: 0, y: 0, zoom: viewport.zoom } : viewport,
|
|
3727
|
+
pane: world
|
|
3728
|
+
? { x: 0, y: 0, width: pane.width, height: pane.height }
|
|
3729
|
+
: pane,
|
|
3730
|
+
clip: world || overlay ? null : this._frameClip,
|
|
3731
|
+
cull: world,
|
|
3732
|
+
palette,
|
|
3733
|
+
paneBackground: this.style.backgroundColor,
|
|
3734
|
+
background: normalizeBackground(this.props.background),
|
|
3735
|
+
// a card `<Flow>` paints in the bodies' layer is not painted twice
|
|
3736
|
+
nodes: overlay
|
|
3737
|
+
? []
|
|
3738
|
+
: layer === 'all' && this._painting
|
|
3739
|
+
? this._frameLists(order).nodes
|
|
3740
|
+
: order
|
|
3741
|
+
.filter((entry) => !this._cardInLayer(entry.node.id) &&
|
|
3742
|
+
(!lift || lift.ids.has(entry.node.id) === lift.only))
|
|
3743
|
+
.map((entry) => this._sourceOf(entry)),
|
|
3744
|
+
all,
|
|
3745
|
+
edges: overlay ? [] : edges,
|
|
3746
|
+
dashPhase: this._dashPhase,
|
|
3747
|
+
hover: this._hover,
|
|
3748
|
+
connection: !overlay && gesture?.kind === 'connect' ? gesture : null,
|
|
3749
|
+
selection: !world && gesture?.kind === 'select' ? selectBoxRect(gesture) : null,
|
|
3750
|
+
miniMap: map && (panels || this._panelCanvases.length === 0)
|
|
3751
|
+
? {
|
|
3752
|
+
panel: map.panel,
|
|
3753
|
+
bounds: map.bounds,
|
|
3754
|
+
scale: map.scale,
|
|
3755
|
+
nodeColor: map.options.nodeColor,
|
|
3756
|
+
maskColor: map.options.maskColor,
|
|
3757
|
+
// the pane's own pass: the part of the minimap it repaints
|
|
3758
|
+
reach: panels ? null : this._frameClip,
|
|
3759
|
+
}
|
|
3760
|
+
: null,
|
|
3761
|
+
controls: world || lift || (!panels && this._panelCanvases.length > 0)
|
|
3762
|
+
? []
|
|
3763
|
+
: this._controlButtons(),
|
|
3764
|
+
scale: this._scale,
|
|
3765
|
+
measure: this._measureBox,
|
|
3766
|
+
cache: this._sceneCache,
|
|
3767
|
+
batch: this._batch,
|
|
3768
|
+
};
|
|
3769
|
+
}
|
|
3770
|
+
/**
|
|
3771
|
+
* Where every mounted node body belongs, and where the graph's origin is.
|
|
3772
|
+
*
|
|
3773
|
+
* The two are sent apart because they change apart. A body's rect is
|
|
3774
|
+
* relative to the **graph's origin on screen** — its position times the
|
|
3775
|
+
* zoom — which a pan does not move; the origin is the viewport's
|
|
3776
|
+
* translation, which is all a pan moves. So a pan re-sends the *same*
|
|
3777
|
+
* bodies array with a new origin, `<Flow>` moves the one box they are
|
|
3778
|
+
* laid out in, and React re-renders none of them: the per-step commit of
|
|
3779
|
+
* every mounted body's position is what held a pan over 48 of them to 57
|
|
3780
|
+
* frames a second. A drag, a zoom, a selection or a body entering the pane
|
|
3781
|
+
* sends a new array, as before.
|
|
3782
|
+
*/
|
|
3783
|
+
_emitBodies() {
|
|
3784
|
+
const notify = this._prop('onNodeBodies');
|
|
3785
|
+
if (!notify)
|
|
3786
|
+
return;
|
|
3787
|
+
const v = this._viewport();
|
|
3788
|
+
if (this._holdBodies(v.zoom)) {
|
|
3789
|
+
if (!this._bodiesHeld) {
|
|
3790
|
+
// The 2D pane draws the cards again from this frame, not from the
|
|
3791
|
+
// commit that hides the bodies. The GL world never stopped drawing
|
|
3792
|
+
// them (`_cardInLayer`).
|
|
3793
|
+
this._bodiesHeld = true;
|
|
3794
|
+
notify(this._bodies, this._gestureSync || this._handleSync, this._bodiesOrigin, true);
|
|
3795
|
+
}
|
|
3796
|
+
return;
|
|
3797
|
+
}
|
|
3798
|
+
const pane = this._pane();
|
|
3799
|
+
// Exact, and every rect below relative to it and free of the pan: a
|
|
3800
|
+
// card's offset from the graph's origin is its graph position times the
|
|
3801
|
+
// zoom, so a pan re-sends the same array and moves only the origin.
|
|
3802
|
+
// Nothing here is rounded — `<Flow>` puts the boxes on the device-pixel
|
|
3803
|
+
// grid, once. Rounded here as well, to logical pixels against a rounded
|
|
3804
|
+
// origin, a card's offset flipped by a pixel as the pan's fraction
|
|
3805
|
+
// cycled against the device grid `_screenRect` snaps to: the array was
|
|
3806
|
+
// new on most steps of a pan, and at 1.25× the layer changed size with
|
|
3807
|
+
// it — every body repainted, where core would have moved them.
|
|
3808
|
+
const origin = { x: v.x, y: v.y };
|
|
3809
|
+
const sv = screenViewport(v, pane);
|
|
3810
|
+
const was = this._bodies;
|
|
3811
|
+
const bodies = [];
|
|
3812
|
+
let changed = false;
|
|
3813
|
+
for (const entry of this._paintOrder()) {
|
|
3814
|
+
if (entry.node.hidden || !this._mounted(entry))
|
|
3815
|
+
continue;
|
|
3816
|
+
if (!rectsOverlap(this._screenRect(entry, sv), pane))
|
|
3817
|
+
continue;
|
|
3818
|
+
const graph = this.rectOf(entry);
|
|
3819
|
+
const x = graph.x * v.zoom;
|
|
3820
|
+
const y = graph.y * v.zoom;
|
|
3821
|
+
const cardWidth = graph.width * v.zoom;
|
|
3822
|
+
const cardHeight = graph.height * v.zoom;
|
|
3823
|
+
const header = this._headerHeight(entry) * v.zoom;
|
|
3824
|
+
const inset = NODE_BODY_INSET * v.zoom;
|
|
3825
|
+
const width = cardWidth - inset * 2;
|
|
3826
|
+
const height = cardHeight - header - inset;
|
|
3827
|
+
if (width <= 1 || height <= 1)
|
|
3828
|
+
continue;
|
|
3829
|
+
const id = entry.node.id;
|
|
3830
|
+
const hovered = this._hover.nodeId === id;
|
|
3831
|
+
const selected = entry.node.selected ?? false;
|
|
3832
|
+
// The zoom counts as well as the rect it produced: it is the body's
|
|
3833
|
+
// subtree scale, and two zooms a hair apart can leave a box where it
|
|
3834
|
+
// was while the text inside it should have moved.
|
|
3835
|
+
const old = this._bodiesById.get(id);
|
|
3836
|
+
const body = old &&
|
|
3837
|
+
old.zoom === v.zoom &&
|
|
3838
|
+
old.selected === selected &&
|
|
3839
|
+
old.hovered === hovered &&
|
|
3840
|
+
old.card.x === x &&
|
|
3841
|
+
old.card.y === y &&
|
|
3842
|
+
old.card.width === cardWidth &&
|
|
3843
|
+
old.card.height === cardHeight &&
|
|
3844
|
+
old.height === height
|
|
3845
|
+
? old
|
|
3846
|
+
: {
|
|
3847
|
+
id,
|
|
3848
|
+
card: { x, y, width: cardWidth, height: cardHeight },
|
|
3849
|
+
hovered,
|
|
3850
|
+
// logical, and relative to the graph's origin on screen — the
|
|
3851
|
+
// box they are laid out in sits at `origin` in the pane
|
|
3852
|
+
x: x + inset,
|
|
3853
|
+
y: y + header,
|
|
3854
|
+
width,
|
|
3855
|
+
height,
|
|
3856
|
+
zoom: v.zoom,
|
|
3857
|
+
selected,
|
|
3858
|
+
};
|
|
3859
|
+
if (body !== was[bodies.length])
|
|
3860
|
+
changed = true;
|
|
3861
|
+
bodies.push(body);
|
|
3862
|
+
}
|
|
3863
|
+
if (bodies.length !== was.length)
|
|
3864
|
+
changed = true;
|
|
3865
|
+
// An origin with no bodies laid out at it is nobody's news: a pan over a
|
|
3866
|
+
// graph whose node types mount bodies, with none on screen, sent one per
|
|
3867
|
+
// step, and `<Flow>` re-rendered for it — its panels' canvases with it.
|
|
3868
|
+
// The origin is still kept, and goes out with the first body to arrive.
|
|
3869
|
+
const moved = was.length > 0 &&
|
|
3870
|
+
(origin.x !== this._bodiesOrigin.x || origin.y !== this._bodiesOrigin.y);
|
|
3871
|
+
const released = this._bodiesHeld;
|
|
3872
|
+
if (!changed && !moved && !released)
|
|
3873
|
+
return;
|
|
3874
|
+
if (changed) {
|
|
3875
|
+
this._bodies = bodies;
|
|
3876
|
+
this._bodiesById = new Map(bodies.map((body) => [body.id, body]));
|
|
3877
|
+
}
|
|
3878
|
+
this._bodiesOrigin = origin;
|
|
3879
|
+
this._bodiesHeld = false;
|
|
3880
|
+
notify(this._bodies, this._gestureSync || this._handleSync, origin, false);
|
|
3881
|
+
}
|
|
3882
|
+
/**
|
|
3883
|
+
* Whether mounted bodies sit this zoom out.
|
|
3884
|
+
*
|
|
3885
|
+
* A zoom changes every body's scale, and a scale is not a transform: each
|
|
3886
|
+
* body is styled, laid out, its text shaped and its pixels painted again,
|
|
3887
|
+
* at the new size, per step. That is about a millisecond a body on this
|
|
3888
|
+
* class of machine — 42 bodies held a zoom to 15 frames a second where
|
|
3889
|
+
* the graph alone costs 13 ms a step. So each step of a zoom *gesture*
|
|
3890
|
+
* predicts what re-scaling the bodies on screen would add, and when that
|
|
3891
|
+
* is over `BODY_BUDGET_MS` they sit the gesture out: still mounted — no
|
|
3892
|
+
* state is lost — but hidden (`display: 'none'`) and untouched, while the
|
|
3893
|
+
* cards, their labels and the edges carry it. Once the zoom has held still
|
|
3894
|
+
* for
|
|
3895
|
+
* `BODY_ZOOM_REST_MS` they come back, laid out once at the new scale. A
|
|
3896
|
+
* handful of bodies fits the budget and zooms live.
|
|
3897
|
+
*
|
|
3898
|
+
* The prediction is bodies × what one costs, measured from the pane's own
|
|
3899
|
+
* frames (`_frameTick`) — so a slower machine holds sooner. Once held, a
|
|
3900
|
+
* gesture stays held until it rests: bodies blinking in and out as the
|
|
3901
|
+
* count crosses the line would be worse than either.
|
|
3902
|
+
*
|
|
3903
|
+
* Only a stream counts: a gesture's steps — the wheel, a pinch, the keys
|
|
3904
|
+
* (`_gestureSync`) — or an animation's, `setViewport` a frame at a time,
|
|
3905
|
+
* each zoom under `GL_ZOOM_REST_MS` after the one before (`_zoomStream`,
|
|
3906
|
+
* the rule the zoom's picture and its scaled labels already follow). A
|
|
3907
|
+
* single programmatic jump — `fitView`, `setViewport`, a control's button
|
|
3908
|
+
* — applies at once, so an app that sets the viewport and reads the result
|
|
3909
|
+
* sees it; the pane never animates a viewport of its own. An animation
|
|
3910
|
+
* that stepped every body at every frame held a zoom over the stress
|
|
3911
|
+
* example's charts to 10 frames a second.
|
|
3912
|
+
*/
|
|
3913
|
+
_holdBodies(zoom) {
|
|
3914
|
+
if (zoom !== this._seenZoom) {
|
|
3915
|
+
const first = Number.isNaN(this._seenZoom);
|
|
3916
|
+
this._seenZoom = zoom;
|
|
3917
|
+
if (first ||
|
|
3918
|
+
(!this._gestureSync && !this._zoomStream && !this._bodiesHeld)) {
|
|
3919
|
+
return false;
|
|
3920
|
+
}
|
|
3921
|
+
const bodies = this._bodies.length;
|
|
3922
|
+
if (!this._bodiesHeld && bodies * this._bodyStepMs <= this._budgetMs()) {
|
|
3923
|
+
this._zoomStep = { live: true, bodies };
|
|
3924
|
+
return false;
|
|
3925
|
+
}
|
|
3926
|
+
this._zoomStep = { live: false, bodies: 0 };
|
|
3927
|
+
this._bodiesAt = now();
|
|
3928
|
+
this._restBodies();
|
|
3929
|
+
return true;
|
|
3930
|
+
}
|
|
3931
|
+
return this._bodiesHeld && this._bodiesRest != null;
|
|
3932
|
+
}
|
|
3933
|
+
/** Bring the held bodies back once the zoom has held still for
|
|
3934
|
+
* `BODY_ZOOM_REST_MS` — judged on the pane's clock, like the zoom's own
|
|
3935
|
+
* rest (`_restZoom`): a timer that comes due while steps are still
|
|
3936
|
+
* arriving waits out what is left of the rest instead. */
|
|
3937
|
+
_restBodies() {
|
|
3938
|
+
if (this._bodiesRest != null)
|
|
3939
|
+
return;
|
|
3940
|
+
const wait = this._restFrom() + BODY_ZOOM_REST_MS - now();
|
|
3941
|
+
this._bodiesRest = timers.setTimeout?.(() => {
|
|
3942
|
+
this._bodiesRest = null;
|
|
3943
|
+
if (now() - this._restFrom() < BODY_ZOOM_REST_MS)
|
|
3944
|
+
this._restBodies();
|
|
3945
|
+
else
|
|
3946
|
+
this._emitBodies();
|
|
3947
|
+
}, Math.max(0, wait) + 1);
|
|
3948
|
+
}
|
|
3949
|
+
/**
|
|
3950
|
+
* What the bodies' rest is counted from: the last step of the zoom, or
|
|
3951
|
+
* the frame that drew it where that came later. A step the window has not
|
|
3952
|
+
* drawn yet is a zoom still moving — the frame is late, not the gesture
|
|
3953
|
+
* over — and counted from the step alone, a frame a server took half a
|
|
3954
|
+
* second to answer brought the bodies back between two steps of one
|
|
3955
|
+
* animation, whose next frame re-scaled all of them and was late in turn.
|
|
3956
|
+
* Past `BODY_REST_CAP_MS` without that frame they come back anyway: a
|
|
3957
|
+
* surface that stopped drawing must not keep them out for good.
|
|
3958
|
+
*/
|
|
3959
|
+
_restFrom() {
|
|
3960
|
+
const drawn = this._zoomDrawnAt;
|
|
3961
|
+
if (drawn !== -Infinity)
|
|
3962
|
+
return Math.max(this._bodiesAt, drawn);
|
|
3963
|
+
return now() - this._bodiesAt < BODY_REST_CAP_MS ? now() : this._bodiesAt;
|
|
3964
|
+
}
|
|
3965
|
+
/**
|
|
3966
|
+
* Paint one mounted node's card — fill, border, header, handles, grips —
|
|
3967
|
+
* into `ctx`, a `<canvas>` in the bodies' layer (`<Flow>`), exactly as the
|
|
3968
|
+
* scene paints it. `ctx` is at the canvas's origin, `abs` its place in
|
|
3969
|
+
* the window in device pixels.
|
|
3970
|
+
*
|
|
3971
|
+
* Why the card is painted twice: the bodies share one layer over every
|
|
3972
|
+
* card — over the GL surface, or beside the 2D pane — so a body could not
|
|
3973
|
+
* go under a card that was over its own. It painted over that card's
|
|
3974
|
+
* header, border and handles: a selected node's outline hidden behind the
|
|
3975
|
+
* body of the node under it. A card painted in the bodies' layer, just
|
|
3976
|
+
* under its own body, is stacked with it in the cards' paint order, and
|
|
3977
|
+
* covers the card the graph drew for it.
|
|
3978
|
+
*/
|
|
3979
|
+
paintCard(id, ctx, abs) {
|
|
3980
|
+
const entry = this._byId.get(id);
|
|
3981
|
+
if (!entry)
|
|
3982
|
+
return;
|
|
3983
|
+
const painter = createPainter(ctx, this._textOptions());
|
|
3984
|
+
if (!painter)
|
|
3985
|
+
return;
|
|
3986
|
+
const palette = this._palette();
|
|
3987
|
+
const input = this._sceneInput(palette, 'card');
|
|
3988
|
+
const [item] = buildNodeItems({
|
|
3989
|
+
...input,
|
|
3990
|
+
nodes: [this._source(entry)],
|
|
3991
|
+
clip: null,
|
|
3992
|
+
cull: undefined,
|
|
3993
|
+
});
|
|
3994
|
+
if (!item)
|
|
3995
|
+
return;
|
|
3996
|
+
const c = ctx;
|
|
3997
|
+
c.save();
|
|
3998
|
+
try {
|
|
3999
|
+
c.translate(-abs.x, -abs.y);
|
|
4000
|
+
paintNodeItem(painter, item, { viewport: input.viewport, palette });
|
|
4001
|
+
}
|
|
4002
|
+
finally {
|
|
4003
|
+
c.restore();
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
/**
|
|
4007
|
+
* Whether a card is the bodies' layer's to draw and not the pane's: on
|
|
4008
|
+
* the 2D renderer, shown there and the bodies not held out of a zoom.
|
|
4009
|
+
*
|
|
4010
|
+
* **Never under GL.** The layer and the GL frame reach the screen by two
|
|
4011
|
+
* paths — the layer is the surface's overlay, painted and presented with
|
|
4012
|
+
* the window's frame, and a GL frame is swapped on the surface's own
|
|
4013
|
+
* turn — and this element cannot order them. So a card left to the layer
|
|
4014
|
+
* was, on some path, a card nobody drew: edges crossing the empty place
|
|
4015
|
+
* where it should be. The first paint of a scene of charts did that until
|
|
4016
|
+
* the layer had painted; a GL frame landing between a zoom's hold and the
|
|
4017
|
+
* commit that hid the bodies did it; and on macOS a wheel notch did it,
|
|
4018
|
+
* painting the overlay inside its own dispatch 20–25 ms before the GL
|
|
4019
|
+
* frame it asked for — a frame of bare edges at the start of every zoom
|
|
4020
|
+
* that held the bodies. Each was fixed by ordering one more path.
|
|
4021
|
+
*
|
|
4022
|
+
* The world draws every card instead, mounted or not, and the layer only
|
|
4023
|
+
* ever adds to it: its card and body land on the world's copy of that
|
|
4024
|
+
* card. Whatever order the two paths take, the worst the screen can show
|
|
4025
|
+
* is a body a frame early or late, over a card that is there. What it
|
|
4026
|
+
* costs is a few dozen cards more in a world build, drawn under their
|
|
4027
|
+
* copies. The 2D pane is painted in the same pass as the layer, in one
|
|
4028
|
+
* frame, and it leaves them out.
|
|
4029
|
+
*/
|
|
4030
|
+
_cardInLayer(id) {
|
|
4031
|
+
// held: hidden, or on the way to it, whatever `<Flow>` last reported
|
|
4032
|
+
return !this._gl && !this._bodiesHeld && this._shownBodies.has(id);
|
|
4033
|
+
}
|
|
4034
|
+
/**
|
|
4035
|
+
* The cards `<Flow>` has on screen in the bodies' layer (`paintCard`),
|
|
4036
|
+
* which the 2D pane then leaves out. Told after the commit that shows
|
|
4037
|
+
* them, so no 2D frame lacks both; told the empty set while bodies are
|
|
4038
|
+
* held out of a zoom, when their cards are the pane's again. Under GL it
|
|
4039
|
+
* changes nothing drawn: the world draws every card (`_cardInLayer`).
|
|
4040
|
+
*/
|
|
4041
|
+
setShownBodies(ids) {
|
|
4042
|
+
const was = this._shownBodies;
|
|
4043
|
+
if (was.size === ids.size && [...ids].every((id) => was.has(id)))
|
|
4044
|
+
return;
|
|
4045
|
+
this._shownBodies = ids;
|
|
4046
|
+
// the pane, not the window
|
|
4047
|
+
if (!this._gl)
|
|
4048
|
+
this._repaint('content');
|
|
4049
|
+
}
|
|
4050
|
+
/**
|
|
4051
|
+
* The canvases `<Flow>` paints the minimap and controls on, over its
|
|
4052
|
+
* bodies (`paintPanels`): the bodies' layer is over the graph, so panels
|
|
4053
|
+
* the graph drew went under any card that reached them. While there are
|
|
4054
|
+
* any the graph leaves the panels out, and every change to the pane asks
|
|
4055
|
+
* them to repaint. Presses still land on the pane: the canvases take
|
|
4056
|
+
* none.
|
|
4057
|
+
*/
|
|
4058
|
+
/** The panel canvases the minimap is painted on — the ones whose box
|
|
4059
|
+
* reaches its corner. */
|
|
4060
|
+
_miniMapCanvases() {
|
|
4061
|
+
const canvases = this._panelCanvases;
|
|
4062
|
+
if (canvases.length === 0)
|
|
4063
|
+
return canvases;
|
|
4064
|
+
const corner = this._miniMapCorner();
|
|
4065
|
+
if (!corner)
|
|
4066
|
+
return [];
|
|
4067
|
+
const box = this._device(corner);
|
|
4068
|
+
return canvases.filter((c) => !c.abs || rectsOverlap(c.abs, box));
|
|
4069
|
+
}
|
|
4070
|
+
/**
|
|
4071
|
+
* The box `<Flow>` lays the node bodies out in, beside this pane, under
|
|
4072
|
+
* the 2D renderer — null under GL, and with no bodies to lay out. A pan
|
|
4073
|
+
* hands it to `scrollContents` as a rider: its pixels move with the
|
|
4074
|
+
* pane's.
|
|
4075
|
+
*/
|
|
4076
|
+
setBodiesLayer(layer) {
|
|
4077
|
+
this._bodiesLayer = layer ?? null;
|
|
4078
|
+
}
|
|
4079
|
+
setPanelCanvases(canvases) {
|
|
4080
|
+
const had = this._panelCanvases.length > 0;
|
|
4081
|
+
this._panelCanvases = canvases;
|
|
4082
|
+
if (had !== canvases.length > 0)
|
|
4083
|
+
this._repaint('props');
|
|
4084
|
+
}
|
|
4085
|
+
/** Where the minimap and the controls are, relative to the pane's
|
|
4086
|
+
* top-left, logical — with a pixel round for their borders. */
|
|
4087
|
+
panelRects() {
|
|
4088
|
+
const pane = this._pane();
|
|
4089
|
+
const rects = [];
|
|
4090
|
+
const map = this._miniMapOptions();
|
|
4091
|
+
if (map) {
|
|
4092
|
+
const panel = this._corner(map.position, map.width ?? MINIMAP_W, map.height ?? MINIMAP_H, 'bottom-right');
|
|
4093
|
+
if (pane.width >= panel.width * 1.6 && pane.height >= panel.height * 1.6)
|
|
4094
|
+
rects.push(panel);
|
|
4095
|
+
}
|
|
4096
|
+
const buttons = this._controlButtons();
|
|
4097
|
+
if (buttons.length > 0) {
|
|
4098
|
+
rects.push(unionRects(buttons[0].rect, buttons[buttons.length - 1].rect));
|
|
4099
|
+
}
|
|
4100
|
+
return rects.map((r) => ({
|
|
4101
|
+
x: Math.floor(r.x - pane.x) - 1,
|
|
4102
|
+
y: Math.floor(r.y - pane.y) - 1,
|
|
4103
|
+
width: Math.ceil(r.width) + 2,
|
|
4104
|
+
height: Math.ceil(r.height) + 2,
|
|
4105
|
+
}));
|
|
4106
|
+
}
|
|
4107
|
+
/** Paint the minimap and the controls into `ctx`, a canvas whose place in
|
|
4108
|
+
* the window, in device pixels, is `abs` — as the scene paints them. */
|
|
4109
|
+
paintPanels(ctx, abs) {
|
|
4110
|
+
const painter = createPainter(ctx, this._textOptions());
|
|
4111
|
+
if (!painter)
|
|
4112
|
+
return;
|
|
4113
|
+
// Only the panels this canvas's box reaches: the minimap's canvas and
|
|
4114
|
+
// the controls' each built and painted both, which for the controls
|
|
4115
|
+
// was every node in the graph, clipped away. The frame clip is set to
|
|
4116
|
+
// the box too, for the minimap's own reach test — a canvas paints
|
|
4117
|
+
// outside this element's paint, where the clip is the last pass's.
|
|
4118
|
+
const bounds = abs.width > 0 && abs.height > 0 ? this._logical(abs) : null;
|
|
4119
|
+
const clip = this._frameClip;
|
|
4120
|
+
this._frameClip = bounds;
|
|
4121
|
+
let scene;
|
|
4122
|
+
try {
|
|
4123
|
+
scene = buildScene({
|
|
4124
|
+
...this._sceneInput(this._palette(), 'overlay', true),
|
|
4125
|
+
clip: bounds,
|
|
4126
|
+
});
|
|
4127
|
+
}
|
|
4128
|
+
finally {
|
|
4129
|
+
this._frameClip = clip;
|
|
4130
|
+
}
|
|
4131
|
+
const c = ctx;
|
|
4132
|
+
c.save();
|
|
4133
|
+
try {
|
|
4134
|
+
c.translate(-abs.x, -abs.y);
|
|
4135
|
+
paintPanels(painter, scene);
|
|
4136
|
+
}
|
|
4137
|
+
finally {
|
|
4138
|
+
c.restore();
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
/** Tell `<Flow>` where the panels are when that changed (`onPanels`). */
|
|
4142
|
+
_emitPanels() {
|
|
4143
|
+
const notify = this._prop('onPanels');
|
|
4144
|
+
if (!notify)
|
|
4145
|
+
return;
|
|
4146
|
+
const rects = this.panelRects();
|
|
4147
|
+
const key = rects
|
|
4148
|
+
.map((r) => `${r.x},${r.y},${r.width},${r.height}`)
|
|
4149
|
+
.join('|');
|
|
4150
|
+
if (key === this._panelsKey)
|
|
4151
|
+
return;
|
|
4152
|
+
this._panelsKey = key;
|
|
4153
|
+
notify(rects);
|
|
4154
|
+
}
|
|
4155
|
+
/** `adaptive` as a number of milliseconds: `Infinity` never holds. */
|
|
4156
|
+
_budgetMs() {
|
|
4157
|
+
const adaptive = this._prop('adaptive');
|
|
4158
|
+
if (adaptive === false)
|
|
4159
|
+
return Infinity;
|
|
4160
|
+
if (typeof adaptive === 'object' && adaptive?.budgetMs != null) {
|
|
4161
|
+
return Math.max(0, adaptive.budgetMs);
|
|
4162
|
+
}
|
|
4163
|
+
return BODY_BUDGET_MS;
|
|
4164
|
+
}
|
|
4165
|
+
/** Where the mounted bodies stand against the budget — reported with
|
|
4166
|
+
* every frame (`FlowFrameStats.bodies`). */
|
|
4167
|
+
bodyBudget() {
|
|
4168
|
+
const count = this._bodies.length;
|
|
4169
|
+
return {
|
|
4170
|
+
count,
|
|
4171
|
+
held: this._bodiesHeld,
|
|
4172
|
+
perBodyMs: this._bodyStepMs,
|
|
4173
|
+
predictedMs: count * this._bodyStepMs,
|
|
4174
|
+
budgetMs: this._budgetMs(),
|
|
4175
|
+
};
|
|
4176
|
+
}
|
|
4177
|
+
/**
|
|
4178
|
+
* A frame was drawn: time the zoom step it carried, into the model
|
|
4179
|
+
* `_holdBodies` predicts from. A step with no body re-scaled is the
|
|
4180
|
+
* baseline; one that re-scaled some says what each cost over it. Passes
|
|
4181
|
+
* a millisecond or two apart are one frame (the 2D renderer paints a
|
|
4182
|
+
* frame's damage as several), and a gap long enough to be the user
|
|
4183
|
+
* pausing is not a step at all.
|
|
4184
|
+
*/
|
|
4185
|
+
_frameTick() {
|
|
4186
|
+
const t = now();
|
|
4187
|
+
// the first frame to draw the latest zoom step, and when: the GL
|
|
4188
|
+
// renderer's is over as it starts — the bodies' cost is the window's
|
|
4189
|
+
// flush before it — and the 2D one's ends with its last pass (`paint`)
|
|
4190
|
+
if (this._zoomFrameAt === -Infinity && this._zoomAt !== -Infinity) {
|
|
4191
|
+
this._zoomFrameAt = t;
|
|
4192
|
+
this._zoomDrawnAt = t;
|
|
4193
|
+
}
|
|
4194
|
+
const dt = t - this._lastFrameAt;
|
|
4195
|
+
if (dt < 2)
|
|
4196
|
+
return;
|
|
4197
|
+
this._lastFrameAt = t;
|
|
4198
|
+
const step = this._zoomStep;
|
|
4199
|
+
this._zoomStep = null;
|
|
4200
|
+
if (!step || dt > 250)
|
|
4201
|
+
return;
|
|
4202
|
+
if (!step.live || step.bodies === 0) {
|
|
4203
|
+
this._baseStepMs = Number.isNaN(this._baseStepMs)
|
|
4204
|
+
? dt
|
|
4205
|
+
: this._baseStepMs * 0.7 + dt * 0.3;
|
|
4206
|
+
}
|
|
4207
|
+
else if (!Number.isNaN(this._baseStepMs)) {
|
|
4208
|
+
const each = Math.max(0, dt - this._baseStepMs) / step.bodies;
|
|
4209
|
+
this._bodyStepMs = this._bodyStepMs * 0.7 + each * 0.3;
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
/**
|
|
4213
|
+
* The grid as one repeating fill (ntk#263): a step-sized tile drawn once,
|
|
4214
|
+
* a `createPattern('repeat')` over it, and one composite for the whole
|
|
4215
|
+
* region — where the runs path in `./paint.ts` pays a region-sized
|
|
4216
|
+
* coverage mask.
|
|
4217
|
+
*
|
|
4218
|
+
* Handed to `paintScene` as its grid painter, and answering **false** when
|
|
4219
|
+
* it does not apply, so the universal path takes over. Two decisions with
|
|
4220
|
+
* reasons:
|
|
4221
|
+
*
|
|
4222
|
+
* - **Integral device steps only.** The tile is a pixmap, so its size is
|
|
4223
|
+
* whole pixels; at a fractional step the pattern would drift against the
|
|
4224
|
+
* graph's own coordinates — against `snapToGrid`, against node positions
|
|
4225
|
+
* — by a fraction per tile, and a grid that slides under the content it
|
|
4226
|
+
* grids is worse than a slower exact one.
|
|
4227
|
+
* - **The phase lives in the tile, not in a picture transform.** An
|
|
4228
|
+
* untransformed repeat is anchored to the window origin, so the grid's
|
|
4229
|
+
* alignment is baked in by drawing the mark at `origin mod tile` — and
|
|
4230
|
+
* re-baking when the origin moves. A `setTransform` translate says the
|
|
4231
|
+
* same thing in one request, but a *transformed* repeat forfeits the
|
|
4232
|
+
* server's untransformed fast path — on the in-process test server that
|
|
4233
|
+
* was ~180 ms a frame of per-pixel arithmetic, and re-rendering a 24px
|
|
4234
|
+
* tile is a handful of requests on any server.
|
|
4235
|
+
*
|
|
4236
|
+
* An arrow property rather than a method: it is passed as a value, and a
|
|
4237
|
+
* method would arrive with no `this`.
|
|
4238
|
+
*/
|
|
4239
|
+
_grid = (painter, grid) => {
|
|
4240
|
+
const options = { variant: grid.variant, size: grid.size };
|
|
4241
|
+
const { step, color, region } = grid;
|
|
4242
|
+
// The tile is a pixmap on the panel's grid, so the pitch has to be a
|
|
4243
|
+
// whole number of *device* pixels, and the phase is where the viewport
|
|
4244
|
+
// origin lands on that grid.
|
|
2480
4245
|
const s = this._scale;
|
|
2481
4246
|
const pitch = toDevice(step, s);
|
|
2482
4247
|
const tileSize = Math.round(pitch);
|
|
@@ -2488,8 +4253,8 @@ export class FlowGraphNode extends Node {
|
|
|
2488
4253
|
const v = this._viewport();
|
|
2489
4254
|
const pane = this._pane();
|
|
2490
4255
|
const mod = (a, m) => ((a % m) + m) % m;
|
|
2491
|
-
const px =
|
|
2492
|
-
const py =
|
|
4256
|
+
const px = snapToGrid(mod(toDevice(pane.x + v.x, s), tileSize)) % tileSize;
|
|
4257
|
+
const py = snapToGrid(mod(toDevice(pane.y + v.y, s), tileSize)) % tileSize;
|
|
2493
4258
|
// device pixels per graph unit — what a mark's size is drawn in
|
|
2494
4259
|
const unit = v.zoom * s;
|
|
2495
4260
|
const key = `${options.variant}|${Math.round(options.size * unit * 4)}|${color}|${px},${py}`;
|
|
@@ -2515,7 +4280,7 @@ export class FlowGraphNode extends Node {
|
|
|
2515
4280
|
const fill = this._device(region);
|
|
2516
4281
|
raw.fillRect?.(fill.x, fill.y, fill.width, fill.height);
|
|
2517
4282
|
return true;
|
|
2518
|
-
}
|
|
4283
|
+
};
|
|
2519
4284
|
_makeGridSurface(tileSize) {
|
|
2520
4285
|
const ctor = ntk.Surface;
|
|
2521
4286
|
if (typeof ctor !== 'function')
|
|
@@ -2536,6 +4301,16 @@ export class FlowGraphNode extends Node {
|
|
|
2536
4301
|
* Device pixels throughout: `unit` is how many of them a graph unit is. */
|
|
2537
4302
|
_renderGridTile(tile, options, unit, color, px, py) {
|
|
2538
4303
|
const t = tile.size;
|
|
4304
|
+
// Every mark on whole pixels. A backend cuts a fractional edge its own
|
|
4305
|
+
// way, and X11 cut a mark's position and its size separately, so a mark
|
|
4306
|
+
// that straddled the tile's edge lost a column: at phase 0 a 3px dot
|
|
4307
|
+
// came out as two pixels a column apart and a 1px line not at all. The
|
|
4308
|
+
// grid changed shape as the view panned, and a pan's blit kept the old
|
|
4309
|
+
// shape where a repaint drew the new one. The phase point `(px, py)` is
|
|
4310
|
+
// a pixel edge; a line and a cross are centred on the pixel before it,
|
|
4311
|
+
// and a dot is `size` pixels ending on it, where the cut had put them.
|
|
4312
|
+
const arm = Math.max(1, Math.floor(Math.max(2, options.size * 3 * unit)));
|
|
4313
|
+
const dot = Math.max(1, Math.round(options.size * 2 * unit));
|
|
2539
4314
|
tile.surface.render((ctx) => {
|
|
2540
4315
|
ctx.clearRect?.(0, 0, t, t);
|
|
2541
4316
|
ctx.fillStyle = color;
|
|
@@ -2545,18 +4320,18 @@ export class FlowGraphNode extends Node {
|
|
|
2545
4320
|
const cy = py + j * t;
|
|
2546
4321
|
if (options.variant === 'lines') {
|
|
2547
4322
|
if (j === 0)
|
|
2548
|
-
ctx.fillRect(cx -
|
|
4323
|
+
ctx.fillRect(cx - 1, 0, 1, t);
|
|
2549
4324
|
if (i === 0)
|
|
2550
|
-
ctx.fillRect(0, cy -
|
|
4325
|
+
ctx.fillRect(0, cy - 1, t, 1);
|
|
2551
4326
|
}
|
|
2552
4327
|
else if (options.variant === 'cross') {
|
|
2553
|
-
|
|
2554
|
-
ctx.fillRect(cx -
|
|
2555
|
-
ctx.fillRect(cx - 0.5, cy - arm, 1, arm * 2);
|
|
4328
|
+
ctx.fillRect(cx - 1 - arm, cy - 1, arm * 2 + 1, 1);
|
|
4329
|
+
ctx.fillRect(cx - 1, cy - 1 - arm, 1, arm * 2 + 1);
|
|
2556
4330
|
}
|
|
2557
4331
|
else {
|
|
2558
|
-
const
|
|
2559
|
-
|
|
4332
|
+
const x0 = Math.floor(cx - dot / 2);
|
|
4333
|
+
const y0 = Math.floor(cy - dot / 2);
|
|
4334
|
+
ctx.fillRect(x0, y0, dot, dot);
|
|
2560
4335
|
}
|
|
2561
4336
|
}
|
|
2562
4337
|
}
|
|
@@ -2570,545 +4345,5 @@ export class FlowGraphNode extends Node {
|
|
|
2570
4345
|
tile.surface.destroy?.();
|
|
2571
4346
|
this._gridTile = null;
|
|
2572
4347
|
}
|
|
2573
|
-
/** Draws every visible edge; answers whether any of them is animated, so
|
|
2574
|
-
* the caller knows whether to keep a timer alive. */
|
|
2575
|
-
_paintEdges(painter, palette) {
|
|
2576
|
-
const v = this._viewport();
|
|
2577
|
-
const pane = this._pane();
|
|
2578
|
-
const labels = v.zoom >= LABEL_ZOOM;
|
|
2579
|
-
let animated = false;
|
|
2580
|
-
// Collect, then draw. A stroke has no fast path in ntk — every one is a
|
|
2581
|
-
// mask rasterized in JS, uploaded with `PutImage` and composited — so a
|
|
2582
|
-
// graph of seven hundred edges was seven hundred round trips and about
|
|
2583
|
-
// four megabytes a frame. Bucketed by the style they will be stroked
|
|
2584
|
-
// with, the same graph is one path per distinct pen, which for almost
|
|
2585
|
-
// every graph is one.
|
|
2586
|
-
const strokes = new StrokeBuckets();
|
|
2587
|
-
const markers = new Map();
|
|
2588
|
-
const labelChips = [];
|
|
2589
|
-
const pendingLabels = [];
|
|
2590
|
-
const clip = this._frameClip;
|
|
2591
|
-
let animBox = null;
|
|
2592
|
-
for (const edge of this._edges) {
|
|
2593
|
-
if (edge.hidden)
|
|
2594
|
-
continue;
|
|
2595
|
-
// two rejects: one from the nodes alone, one from the route it took
|
|
2596
|
-
const coarse = this._edgeCoarseBox(edge);
|
|
2597
|
-
if (!coarse || !rectsOverlap(coarse, pane))
|
|
2598
|
-
continue;
|
|
2599
|
-
// Tracked before the damage skip, deliberately: whether the dash
|
|
2600
|
-
// timer runs is a question about the viewport, not about what this
|
|
2601
|
-
// particular pass repaints — deciding it after the skip is how a drag
|
|
2602
|
-
// in one corner would stop the dash marching in the other. The box
|
|
2603
|
-
// the ticks invalidate is collected below from the *drawn* geometry:
|
|
2604
|
-
// the coarse box carries the bezier slack, and a tick that repaints
|
|
2605
|
-
// slack repaints a card-sized halo of neighbours sixteen times a
|
|
2606
|
-
// second.
|
|
2607
|
-
if (edge.animated)
|
|
2608
|
-
animated = true;
|
|
2609
|
-
if (clip && !rectsOverlap(coarse, clip))
|
|
2610
|
-
continue;
|
|
2611
|
-
const geometry = this._edgeGeometry(edge);
|
|
2612
|
-
if (!geometry)
|
|
2613
|
-
continue;
|
|
2614
|
-
if (!rectsOverlap(pathBounds(geometry.points), pane))
|
|
2615
|
-
continue;
|
|
2616
|
-
const selected = edge.selected ?? false;
|
|
2617
|
-
const hovered = this._hover.edgeId === edge.id;
|
|
2618
|
-
const stroke = edge.style?.stroke ??
|
|
2619
|
-
(selected
|
|
2620
|
-
? palette.edgeSelected
|
|
2621
|
-
: hovered
|
|
2622
|
-
? palette.text
|
|
2623
|
-
: palette.edge);
|
|
2624
|
-
const lineWidth = Math.max(1, (edge.style?.strokeWidth ?? (selected ? 2 : 1.5)) * v.zoom);
|
|
2625
|
-
// `markerEnd` left out means an arrow: a directed graph whose edges do
|
|
2626
|
-
// not say which way they point is a set of lines. `null` opts out.
|
|
2627
|
-
const markerEnd = normalizeMarker(edge.markerEnd === undefined ? 'arrowclosed' : edge.markerEnd);
|
|
2628
|
-
const markerStart = normalizeMarker(edge.markerStart);
|
|
2629
|
-
// The tip stops short of the handle rather than at it: the handle dot
|
|
2630
|
-
// is drawn *over* the edges, with the nodes, so an arrow aimed at the
|
|
2631
|
-
// handle's centre is an arrow mostly hidden under a white circle.
|
|
2632
|
-
const inset = v.zoom >= HANDLE_ZOOM ? this._handleRadius() + 1 : 1;
|
|
2633
|
-
const last = geometry.points[geometry.points.length - 1];
|
|
2634
|
-
const outAngle = endAngle(geometry.points);
|
|
2635
|
-
const endTip = {
|
|
2636
|
-
x: last.x - Math.cos(outAngle) * inset,
|
|
2637
|
-
y: last.y - Math.sin(outAngle) * inset,
|
|
2638
|
-
};
|
|
2639
|
-
let points = geometry.points;
|
|
2640
|
-
if (markerEnd) {
|
|
2641
|
-
// and the stroke stops behind the head, so a filled triangle is a
|
|
2642
|
-
// triangle rather than a triangle with a line through it
|
|
2643
|
-
points = trimEnd(points, (markerEnd.size ?? DEFAULT_MARKER_SIZE) * v.zoom * 0.8 + inset);
|
|
2644
|
-
}
|
|
2645
|
-
const dash = edge.style?.dash ?? (edge.animated ? DEFAULT_DASH : undefined);
|
|
2646
|
-
if (edge.animated) {
|
|
2647
|
-
const tight = inflateRect(pathBounds(geometry.points), CULL_MARGIN);
|
|
2648
|
-
animBox = animBox ? unionRects(animBox, tight) : tight;
|
|
2649
|
-
}
|
|
2650
|
-
strokes
|
|
2651
|
-
.bucket(stroke, lineWidth, dash, edge.animated ? this._dashPhase : 0, v.zoom)
|
|
2652
|
-
.push(points);
|
|
2653
|
-
if (markerEnd) {
|
|
2654
|
-
this._collectMarker(markers, endTip, outAngle, markerEnd.type, markerEnd.color ?? stroke, (markerEnd.size ?? DEFAULT_MARKER_SIZE) * v.zoom, lineWidth);
|
|
2655
|
-
}
|
|
2656
|
-
if (markerStart) {
|
|
2657
|
-
const inAngle = startAngle(geometry.points);
|
|
2658
|
-
this._collectMarker(markers, {
|
|
2659
|
-
x: geometry.points[0].x - Math.cos(inAngle) * inset,
|
|
2660
|
-
y: geometry.points[0].y - Math.sin(inAngle) * inset,
|
|
2661
|
-
}, inAngle, markerStart.type, markerStart.color ?? stroke, (markerStart.size ?? DEFAULT_MARKER_SIZE) * v.zoom, lineWidth);
|
|
2662
|
-
}
|
|
2663
|
-
if (labels && edge.label) {
|
|
2664
|
-
const at = pointAtFraction(geometry.points, 0.5);
|
|
2665
|
-
const size = Math.max(8, 11 * v.zoom);
|
|
2666
|
-
const metrics = painter.measureText(edge.label, { size });
|
|
2667
|
-
const padX = 5 * v.zoom;
|
|
2668
|
-
const padY = 2 * v.zoom;
|
|
2669
|
-
// The chip is collected with the rest; the text is not, because a
|
|
2670
|
-
// glyph run is already one request and nothing is gained by holding
|
|
2671
|
-
// it. Both still land above every edge, which is the point of the
|
|
2672
|
-
// chip.
|
|
2673
|
-
labelChips.push({
|
|
2674
|
-
rect: {
|
|
2675
|
-
x: Math.round(at.x - metrics.width / 2 - padX),
|
|
2676
|
-
y: Math.round(at.y - metrics.height / 2 - padY),
|
|
2677
|
-
width: Math.round(metrics.width + padX * 2),
|
|
2678
|
-
height: Math.round(metrics.height + padY * 2),
|
|
2679
|
-
},
|
|
2680
|
-
radius: Math.max(2, Math.round(3 * v.zoom)),
|
|
2681
|
-
fill: edge.style?.labelBackground ?? tint(palette.background, 0.92),
|
|
2682
|
-
});
|
|
2683
|
-
pendingLabels.push({
|
|
2684
|
-
text: edge.label,
|
|
2685
|
-
x: at.x,
|
|
2686
|
-
y: at.y,
|
|
2687
|
-
size,
|
|
2688
|
-
color: edge.style?.labelColor ?? palette.text,
|
|
2689
|
-
});
|
|
2690
|
-
}
|
|
2691
|
-
}
|
|
2692
|
-
strokes.paint(painter);
|
|
2693
|
-
for (const bucket of markers.values()) {
|
|
2694
|
-
// the same threshold, for the same reason: a handful of arrowheads
|
|
2695
|
-
// scattered over the pane is cheaper drawn as a handful
|
|
2696
|
-
if (bucket.filled.length >= MARKER_BATCH_MIN) {
|
|
2697
|
-
painter.polygons(bucket.filled, { fill: bucket.color });
|
|
2698
|
-
}
|
|
2699
|
-
else {
|
|
2700
|
-
for (const head of bucket.filled) {
|
|
2701
|
-
painter.polygon(head, { fill: bucket.color });
|
|
2702
|
-
}
|
|
2703
|
-
}
|
|
2704
|
-
if (bucket.open.length >= MARKER_BATCH_MIN) {
|
|
2705
|
-
painter.strokeRuns(bucket.open, {
|
|
2706
|
-
stroke: bucket.color,
|
|
2707
|
-
lineWidth: bucket.lineWidth,
|
|
2708
|
-
});
|
|
2709
|
-
}
|
|
2710
|
-
else {
|
|
2711
|
-
for (const head of bucket.open) {
|
|
2712
|
-
painter.strokeRuns([head], {
|
|
2713
|
-
stroke: bucket.color,
|
|
2714
|
-
lineWidth: bucket.lineWidth,
|
|
2715
|
-
});
|
|
2716
|
-
}
|
|
2717
|
-
}
|
|
2718
|
-
}
|
|
2719
|
-
for (const chip of labelChips) {
|
|
2720
|
-
painter.rect(chip.rect.x, chip.rect.y, chip.rect.width, chip.rect.height, chip.radius, { fill: chip.fill });
|
|
2721
|
-
}
|
|
2722
|
-
for (const l of pendingLabels) {
|
|
2723
|
-
painter.text(l.text, l.x, l.y, {
|
|
2724
|
-
size: l.size,
|
|
2725
|
-
color: l.color,
|
|
2726
|
-
align: 'center',
|
|
2727
|
-
baseline: 'middle',
|
|
2728
|
-
});
|
|
2729
|
-
}
|
|
2730
|
-
// A pass that drew an animated edge knows exactly where its dash is; a
|
|
2731
|
-
// pass that culled them all keeps the previous box — the endpoints did
|
|
2732
|
-
// not move, or the move's own damage would have redrawn them here.
|
|
2733
|
-
if (animBox)
|
|
2734
|
-
this._animBox = animBox;
|
|
2735
|
-
return animated;
|
|
2736
|
-
}
|
|
2737
|
-
/** An arrowhead's three or four points, added to the pile for its colour
|
|
2738
|
-
* rather than drawn — see {@link StrokeBuckets} for why. */
|
|
2739
|
-
_collectMarker(markers, at, angle, type, color, size, lineWidth) {
|
|
2740
|
-
const spread = 0.42; // radians off the shaft — a ~24° half-angle
|
|
2741
|
-
const back = {
|
|
2742
|
-
x: at.x - Math.cos(angle) * size,
|
|
2743
|
-
y: at.y - Math.sin(angle) * size,
|
|
2744
|
-
};
|
|
2745
|
-
const left = {
|
|
2746
|
-
x: at.x - Math.cos(angle - spread) * size,
|
|
2747
|
-
y: at.y - Math.sin(angle - spread) * size,
|
|
2748
|
-
};
|
|
2749
|
-
const right = {
|
|
2750
|
-
x: at.x - Math.cos(angle + spread) * size,
|
|
2751
|
-
y: at.y - Math.sin(angle + spread) * size,
|
|
2752
|
-
};
|
|
2753
|
-
const key = `${color}|${lineWidth}`;
|
|
2754
|
-
let bucket = markers.get(key);
|
|
2755
|
-
if (!bucket) {
|
|
2756
|
-
bucket = { color, lineWidth, filled: [], open: [] };
|
|
2757
|
-
markers.set(key, bucket);
|
|
2758
|
-
}
|
|
2759
|
-
if (type === 'arrowclosed')
|
|
2760
|
-
bucket.filled.push([at, left, back, right]);
|
|
2761
|
-
else
|
|
2762
|
-
bucket.open.push([left, at, right]);
|
|
2763
|
-
}
|
|
2764
|
-
_paintNodes(painter, palette) {
|
|
2765
|
-
const dragging = this._dragTo;
|
|
2766
|
-
const order = [];
|
|
2767
|
-
const deferred = [];
|
|
2768
|
-
for (const entry of this._order) {
|
|
2769
|
-
// a dragged node comes to the top
|
|
2770
|
-
if (dragging?.has(entry.node.id))
|
|
2771
|
-
deferred.push(entry);
|
|
2772
|
-
else
|
|
2773
|
-
order.push(entry);
|
|
2774
|
-
}
|
|
2775
|
-
order.push(...deferred);
|
|
2776
|
-
// Nodes are drawn one at a time, in z-order, and both halves of that
|
|
2777
|
-
// were measured rather than assumed — see `_paintHandles` for why
|
|
2778
|
-
// batching a card or a handle costs more than it saves.
|
|
2779
|
-
for (const entry of order)
|
|
2780
|
-
this._paintNode(painter, palette, entry);
|
|
2781
|
-
}
|
|
2782
|
-
_paintNode(painter, palette, entry) {
|
|
2783
|
-
if (entry.node.hidden)
|
|
2784
|
-
return;
|
|
2785
|
-
const rect = this._screenRect(entry);
|
|
2786
|
-
if (!rectsOverlap(rect, this._pane()))
|
|
2787
|
-
return;
|
|
2788
|
-
// inflated by the margin its handles and grips can ink outside the box —
|
|
2789
|
-
// the same margin every invalidate grew by, so the two agree
|
|
2790
|
-
const clip = this._frameClip;
|
|
2791
|
-
if (clip && !rectsOverlap(inflateRect(rect, CULL_MARGIN), clip))
|
|
2792
|
-
return;
|
|
2793
|
-
const v = this._viewport();
|
|
2794
|
-
const selected = entry.node.selected ?? false;
|
|
2795
|
-
const hovered = this._hover.nodeId === entry.node.id;
|
|
2796
|
-
const handles = this._handlesOf(entry).map((anchor) => {
|
|
2797
|
-
const s = this._toScreen(anchor);
|
|
2798
|
-
return { ...anchor, x: s.x, y: s.y };
|
|
2799
|
-
});
|
|
2800
|
-
if (entry.type?.paint) {
|
|
2801
|
-
// Never batched: a type that draws its own body is drawing whatever it
|
|
2802
|
-
// likes, in an order only it knows.
|
|
2803
|
-
entry.type.paint({
|
|
2804
|
-
node: entry.node,
|
|
2805
|
-
rect,
|
|
2806
|
-
zoom: v.zoom,
|
|
2807
|
-
selected,
|
|
2808
|
-
hovered,
|
|
2809
|
-
palette,
|
|
2810
|
-
painter,
|
|
2811
|
-
handles,
|
|
2812
|
-
});
|
|
2813
|
-
}
|
|
2814
|
-
else {
|
|
2815
|
-
this._paintCardShape(painter, palette, entry, rect, selected, hovered);
|
|
2816
|
-
this._paintCardInk(painter, palette, entry, rect);
|
|
2817
|
-
}
|
|
2818
|
-
if (this._connectable(entry) && (v.zoom >= HANDLE_ZOOM || hovered)) {
|
|
2819
|
-
this._paintHandles(painter, palette, handles);
|
|
2820
|
-
}
|
|
2821
|
-
this._paintGrips(painter, palette, entry, rect);
|
|
2822
|
-
}
|
|
2823
|
-
_mounted(entry) {
|
|
2824
|
-
return entry.type?.render != null && this._viewport().zoom >= RENDER_ZOOM;
|
|
2825
|
-
}
|
|
2826
|
-
_paintGrips(painter, palette, entry, rect) {
|
|
2827
|
-
const grips = this._grips(entry);
|
|
2828
|
-
if (grips.length === 0)
|
|
2829
|
-
return;
|
|
2830
|
-
const size = Math.max(4, RESIZE_GRIP * 2 * this._viewport().zoom);
|
|
2831
|
-
for (const dir of grips) {
|
|
2832
|
-
const at = gripPoint(rect, dir);
|
|
2833
|
-
painter.rect(at.x - size / 2, at.y - size / 2, size, size, 1, {
|
|
2834
|
-
fill: palette.nodeBackground,
|
|
2835
|
-
stroke: palette.accent,
|
|
2836
|
-
lineWidth: 1.5,
|
|
2837
|
-
});
|
|
2838
|
-
}
|
|
2839
|
-
}
|
|
2840
|
-
/** The built-in node: a card with a label, an optional second line and an
|
|
2841
|
-
* optional accent stripe down its leading edge. */
|
|
2842
|
-
/** The card itself: one rounded box, and the accent stripe if it has one.
|
|
2843
|
-
* With a `batch` the box joins the pile for its appearance instead of
|
|
2844
|
-
* being drawn on its own. */
|
|
2845
|
-
_paintCardShape(painter, palette, entry, rect, selected, hovered) {
|
|
2846
|
-
const v = this._viewport();
|
|
2847
|
-
const style = entry.node.style;
|
|
2848
|
-
const radius = Math.round((style?.borderRadius ?? 6) * v.zoom);
|
|
2849
|
-
const border = selected
|
|
2850
|
-
? palette.accent
|
|
2851
|
-
: hovered
|
|
2852
|
-
? tint(palette.accent, 0.55)
|
|
2853
|
-
: (style?.borderColor ?? palette.nodeBorder);
|
|
2854
|
-
const options = {
|
|
2855
|
-
fill: style?.background ?? palette.nodeBackground,
|
|
2856
|
-
stroke: border,
|
|
2857
|
-
lineWidth: Math.max(1, Math.round((style?.borderWidth ?? (selected ? 2 : 1)) * v.zoom)),
|
|
2858
|
-
};
|
|
2859
|
-
painter.rect(rect.x, rect.y, rect.width, rect.height, radius, options);
|
|
2860
|
-
if (style?.accent) {
|
|
2861
|
-
// Inset past the rounded corners instead of clipped to them: a
|
|
2862
|
-
// non-rectangular clip forfeits ntk's rounded-box fast path for every
|
|
2863
|
-
// fill under it — measured as a pixmap create/free and a trapezoid
|
|
2864
|
-
// pass per card per repaint.
|
|
2865
|
-
painter.rect(rect.x + Math.max(1, v.zoom), rect.y + radius, Math.max(2, 3 * v.zoom), rect.height - radius * 2, 0, { fill: style.accent });
|
|
2866
|
-
}
|
|
2867
|
-
}
|
|
2868
|
-
/** The label and its second line — the half of a card that has to come
|
|
2869
|
-
* after every card when they are batched. */
|
|
2870
|
-
_paintCardInk(painter, palette, entry, rect) {
|
|
2871
|
-
const v = this._viewport();
|
|
2872
|
-
if (v.zoom < LABEL_ZOOM)
|
|
2873
|
-
return;
|
|
2874
|
-
const style = entry.node.style;
|
|
2875
|
-
const header = this._mounted(entry) ? this._headerHeight(entry) : 0;
|
|
2876
|
-
const data = entry.node.data;
|
|
2877
|
-
const label = data?.label ?? entry.node.id;
|
|
2878
|
-
const description = data?.description;
|
|
2879
|
-
const color = style?.color ?? palette.text;
|
|
2880
|
-
const padX = NODE_PAD_X * v.zoom;
|
|
2881
|
-
const centre = rect.x + rect.width / 2;
|
|
2882
|
-
const maxWidth = Math.max(8, rect.width - padX * 2);
|
|
2883
|
-
const showDescription = Boolean(description) && v.zoom >= DESC_ZOOM;
|
|
2884
|
-
if (header > 0) {
|
|
2885
|
-
// The title bar of a node whose body is somebody else's: left-aligned,
|
|
2886
|
-
// with a hairline under it so the strip reads as the thing to grab.
|
|
2887
|
-
const band = header * v.zoom;
|
|
2888
|
-
painter.text(label, rect.x + padX, rect.y + band / 2, {
|
|
2889
|
-
size: NODE_LABEL_SIZE * v.zoom,
|
|
2890
|
-
weight: 'bold',
|
|
2891
|
-
color,
|
|
2892
|
-
baseline: 'middle',
|
|
2893
|
-
maxWidth,
|
|
2894
|
-
});
|
|
2895
|
-
painter.strokeRuns([
|
|
2896
|
-
[
|
|
2897
|
-
{ x: rect.x + 1, y: rect.y + band },
|
|
2898
|
-
{ x: rect.x + rect.width - 1, y: rect.y + band },
|
|
2899
|
-
],
|
|
2900
|
-
], { stroke: palette.nodeBorder, lineWidth: 1 });
|
|
2901
|
-
return;
|
|
2902
|
-
}
|
|
2903
|
-
if (!showDescription) {
|
|
2904
|
-
painter.text(label, centre, rect.y + rect.height / 2, {
|
|
2905
|
-
size: NODE_LABEL_SIZE * v.zoom,
|
|
2906
|
-
weight: 'bold',
|
|
2907
|
-
color,
|
|
2908
|
-
align: 'center',
|
|
2909
|
-
baseline: 'middle',
|
|
2910
|
-
maxWidth,
|
|
2911
|
-
});
|
|
2912
|
-
return;
|
|
2913
|
-
}
|
|
2914
|
-
const top = rect.y + NODE_PAD_Y * v.zoom;
|
|
2915
|
-
painter.text(label, centre, top, {
|
|
2916
|
-
size: NODE_LABEL_SIZE * v.zoom,
|
|
2917
|
-
weight: 'bold',
|
|
2918
|
-
color,
|
|
2919
|
-
align: 'center',
|
|
2920
|
-
maxWidth,
|
|
2921
|
-
});
|
|
2922
|
-
painter.text(description, centre, top + NODE_LABEL_SIZE * 1.35 * v.zoom, {
|
|
2923
|
-
size: NODE_DESC_SIZE * v.zoom,
|
|
2924
|
-
color: palette.dim,
|
|
2925
|
-
align: 'center',
|
|
2926
|
-
maxWidth,
|
|
2927
|
-
});
|
|
2928
|
-
}
|
|
2929
|
-
/**
|
|
2930
|
-
* Handles are drawn one disc at a time, and that is the measured answer
|
|
2931
|
-
* rather than the obvious one.
|
|
2932
|
-
*
|
|
2933
|
-
* Batching them into a single path halved the requests and made the frame
|
|
2934
|
-
* *slower*: a path's mask is its bounding box, so forty dots scattered
|
|
2935
|
-
* across the pane rasterize to a paneful of mask — three quarters of a
|
|
2936
|
-
* megabyte — where forty small ones cost a kilobyte each. Batching pays
|
|
2937
|
-
* for the edges because an edge already spans that box; it does not pay
|
|
2938
|
-
* for anything small and scattered.
|
|
2939
|
-
*/
|
|
2940
|
-
_paintHandles(painter, palette, handles) {
|
|
2941
|
-
const v = this._viewport();
|
|
2942
|
-
const radius = this._handleRadius();
|
|
2943
|
-
const gesture = this._gesture;
|
|
2944
|
-
const connecting = gesture?.kind === 'connect' ? gesture : null;
|
|
2945
|
-
const lineWidth = Math.max(1, 1.5 * v.zoom);
|
|
2946
|
-
for (const anchor of handles) {
|
|
2947
|
-
const active = sameHandle(this._hover.handle, anchor) ||
|
|
2948
|
-
sameHandle(connecting?.to ?? null, anchor) ||
|
|
2949
|
-
sameHandle(connecting?.from ?? null, anchor);
|
|
2950
|
-
const options = {
|
|
2951
|
-
fill: active ? palette.accent : palette.nodeBackground,
|
|
2952
|
-
stroke: active ? palette.accent : palette.handle,
|
|
2953
|
-
lineWidth,
|
|
2954
|
-
};
|
|
2955
|
-
painter.circle(anchor.x, anchor.y, active ? radius * 1.4 : radius, options);
|
|
2956
|
-
if (anchor.label && v.zoom >= DESC_ZOOM) {
|
|
2957
|
-
const outside = anchor.position === 'left' || anchor.position === 'top';
|
|
2958
|
-
painter.text(anchor.label, anchor.x +
|
|
2959
|
-
(anchor.position === 'left'
|
|
2960
|
-
? -radius - 4 * v.zoom
|
|
2961
|
-
: radius + 4 * v.zoom), anchor.y, {
|
|
2962
|
-
size: 10 * v.zoom,
|
|
2963
|
-
color: palette.dim,
|
|
2964
|
-
align: outside ? 'right' : 'left',
|
|
2965
|
-
baseline: 'middle',
|
|
2966
|
-
});
|
|
2967
|
-
}
|
|
2968
|
-
}
|
|
2969
|
-
}
|
|
2970
|
-
_paintConnection(painter, palette) {
|
|
2971
|
-
const gesture = this._gesture;
|
|
2972
|
-
if (gesture?.kind !== 'connect')
|
|
2973
|
-
return;
|
|
2974
|
-
const v = this._viewport();
|
|
2975
|
-
const target = gesture.to ?? gesture.pointer;
|
|
2976
|
-
const to = this._toScreen(target);
|
|
2977
|
-
const points = this._connectionPath(gesture);
|
|
2978
|
-
const invalid = gesture.to != null && !gesture.valid;
|
|
2979
|
-
painter.polyline(points, {
|
|
2980
|
-
stroke: invalid ? palette.dim : palette.accent,
|
|
2981
|
-
lineWidth: Math.max(1.5, 2 * v.zoom),
|
|
2982
|
-
dash: gesture.to && gesture.valid ? undefined : [5 * v.zoom, 4 * v.zoom],
|
|
2983
|
-
});
|
|
2984
|
-
painter.circle(to.x, to.y, this._handleRadius(), {
|
|
2985
|
-
fill: invalid ? palette.dim : palette.accent,
|
|
2986
|
-
});
|
|
2987
|
-
}
|
|
2988
|
-
_paintSelectionBox(painter, palette) {
|
|
2989
|
-
const gesture = this._gesture;
|
|
2990
|
-
if (gesture?.kind !== 'select')
|
|
2991
|
-
return;
|
|
2992
|
-
const x = Math.min(gesture.startX, gesture.x);
|
|
2993
|
-
const y = Math.min(gesture.startY, gesture.y);
|
|
2994
|
-
painter.rect(x, y, Math.abs(gesture.x - gesture.startX), Math.abs(gesture.y - gesture.startY), 2, { fill: palette.selection, stroke: palette.accent, lineWidth: 1 });
|
|
2995
|
-
}
|
|
2996
|
-
_paintMiniMap(painter, palette) {
|
|
2997
|
-
const opts = this._miniMapOptions();
|
|
2998
|
-
if (!opts)
|
|
2999
|
-
return;
|
|
3000
|
-
// The panel cull comes before `_miniMap()`, which walks every node to
|
|
3001
|
-
// find the graph's bounds — during a drag that walk per pass would cost
|
|
3002
|
-
// more than the panel it skips. The dot for a mid-drag node goes stale
|
|
3003
|
-
// until the release repaints in full; that is the trade, and it is
|
|
3004
|
-
// deliberate.
|
|
3005
|
-
const clip = this._frameClip;
|
|
3006
|
-
if (clip) {
|
|
3007
|
-
const quick = this._corner(opts.position, opts.width ?? MINIMAP_W, opts.height ?? MINIMAP_H, 'bottom-right');
|
|
3008
|
-
if (!rectsOverlap(quick, clip))
|
|
3009
|
-
return;
|
|
3010
|
-
}
|
|
3011
|
-
const map = this._miniMap();
|
|
3012
|
-
if (!map)
|
|
3013
|
-
return;
|
|
3014
|
-
const { panel, options } = map;
|
|
3015
|
-
painter.rect(panel.x, panel.y, panel.width, panel.height, 4, {
|
|
3016
|
-
fill: tint(palette.surface, 0.92),
|
|
3017
|
-
stroke: palette.surfaceBorder,
|
|
3018
|
-
lineWidth: 1,
|
|
3019
|
-
});
|
|
3020
|
-
painter.save();
|
|
3021
|
-
painter.clipRect(panel.x, panel.y, panel.width, panel.height, 4);
|
|
3022
|
-
const nodeColor = options.nodeColor;
|
|
3023
|
-
for (const entry of this._entries) {
|
|
3024
|
-
if (entry.node.hidden)
|
|
3025
|
-
continue;
|
|
3026
|
-
const rect = this.rectOf(entry);
|
|
3027
|
-
const at = this._miniToScreen(map, rect);
|
|
3028
|
-
const fill = typeof nodeColor === 'function'
|
|
3029
|
-
? nodeColor(entry.node)
|
|
3030
|
-
: (nodeColor ??
|
|
3031
|
-
(entry.node.selected ? palette.accent : palette.nodeBorder));
|
|
3032
|
-
painter.rect(at.x, at.y, Math.max(1, rect.width * map.scale), Math.max(1, rect.height * map.scale), 0, { fill });
|
|
3033
|
-
}
|
|
3034
|
-
// The viewport, as an outline over a wash on everything outside it —
|
|
3035
|
-
// cheaper than four mask rectangles and reads the same.
|
|
3036
|
-
const v = this._viewport();
|
|
3037
|
-
const paneRect = this._pane();
|
|
3038
|
-
const view = this._miniToScreen(map, {
|
|
3039
|
-
x: -v.x / v.zoom,
|
|
3040
|
-
y: -v.y / v.zoom,
|
|
3041
|
-
});
|
|
3042
|
-
painter.rect(view.x, view.y, (paneRect.width / v.zoom) * map.scale, (paneRect.height / v.zoom) * map.scale, 2, {
|
|
3043
|
-
fill: options.maskColor ?? tint(palette.accent, 0.1),
|
|
3044
|
-
stroke: palette.accent,
|
|
3045
|
-
lineWidth: 1,
|
|
3046
|
-
});
|
|
3047
|
-
painter.restore();
|
|
3048
|
-
}
|
|
3049
|
-
_paintControls(painter, palette) {
|
|
3050
|
-
const buttons = this._controlButtons();
|
|
3051
|
-
if (buttons.length === 0)
|
|
3052
|
-
return;
|
|
3053
|
-
const clip = this._frameClip;
|
|
3054
|
-
if (clip && !buttons.some((b) => rectsOverlap(b.rect, clip)))
|
|
3055
|
-
return;
|
|
3056
|
-
const first = buttons[0].rect;
|
|
3057
|
-
const last = buttons[buttons.length - 1].rect;
|
|
3058
|
-
painter.rect(first.x, first.y, first.width, last.y + last.height - first.y, 4, {
|
|
3059
|
-
fill: tint(palette.surface, 0.94),
|
|
3060
|
-
stroke: palette.surfaceBorder,
|
|
3061
|
-
lineWidth: 1,
|
|
3062
|
-
});
|
|
3063
|
-
for (let i = 0; i < buttons.length; i++) {
|
|
3064
|
-
const { rect, action } = buttons[i];
|
|
3065
|
-
if (i > 0) {
|
|
3066
|
-
painter.strokeRuns([
|
|
3067
|
-
[
|
|
3068
|
-
{ x: rect.x + 4, y: rect.y },
|
|
3069
|
-
{ x: rect.x + rect.width - 4, y: rect.y },
|
|
3070
|
-
],
|
|
3071
|
-
], { stroke: palette.surfaceBorder, lineWidth: 1 });
|
|
3072
|
-
}
|
|
3073
|
-
const cx = rect.x + rect.width / 2;
|
|
3074
|
-
const cy = rect.y + rect.height / 2;
|
|
3075
|
-
const arm = 5;
|
|
3076
|
-
const runs = [];
|
|
3077
|
-
if (action === 'in' || action === 'out') {
|
|
3078
|
-
runs.push([
|
|
3079
|
-
{ x: cx - arm, y: cy },
|
|
3080
|
-
{ x: cx + arm, y: cy },
|
|
3081
|
-
]);
|
|
3082
|
-
if (action === 'in') {
|
|
3083
|
-
runs.push([
|
|
3084
|
-
{ x: cx, y: cy - arm },
|
|
3085
|
-
{ x: cx, y: cy + arm },
|
|
3086
|
-
]);
|
|
3087
|
-
}
|
|
3088
|
-
}
|
|
3089
|
-
else {
|
|
3090
|
-
// a frame with its sides opened out: "fit what is there into this"
|
|
3091
|
-
const a = 5;
|
|
3092
|
-
runs.push([
|
|
3093
|
-
{ x: cx - a, y: cy - a + 3 },
|
|
3094
|
-
{ x: cx - a, y: cy - a },
|
|
3095
|
-
{ x: cx - a + 3, y: cy - a },
|
|
3096
|
-
], [
|
|
3097
|
-
{ x: cx + a - 3, y: cy - a },
|
|
3098
|
-
{ x: cx + a, y: cy - a },
|
|
3099
|
-
{ x: cx + a, y: cy - a + 3 },
|
|
3100
|
-
], [
|
|
3101
|
-
{ x: cx + a, y: cy + a - 3 },
|
|
3102
|
-
{ x: cx + a, y: cy + a },
|
|
3103
|
-
{ x: cx + a - 3, y: cy + a },
|
|
3104
|
-
], [
|
|
3105
|
-
{ x: cx - a + 3, y: cy + a },
|
|
3106
|
-
{ x: cx - a, y: cy + a },
|
|
3107
|
-
{ x: cx - a, y: cy + a - 3 },
|
|
3108
|
-
]);
|
|
3109
|
-
}
|
|
3110
|
-
painter.strokeRuns(runs, { stroke: palette.text, lineWidth: 1.5 });
|
|
3111
|
-
}
|
|
3112
|
-
}
|
|
3113
4348
|
}
|
|
3114
4349
|
//# sourceMappingURL=node.js.map
|