diff --git a/.github/workflows/ci-pygfx-release.yml b/.github/workflows/ci-pygfx-release.yml index e93f82fd5..87ed1a113 100644 --- a/.github/workflows/ci-pygfx-release.yml +++ b/.github/workflows/ci-pygfx-release.yml @@ -59,12 +59,12 @@ jobs: python -c "from examples.tests.testutils import wgpu_backend; print(wgpu_backend)" - name: Test components env: - WGPU_FORCE_OFFSCREEN: 1 + RENDERCANVAS_FORCE_OFFSCREEN: 1 run: | pytest -v tests/ - name: Test examples env: - WGPU_FORCE_OFFSCREEN: 1 + RENDERCANVAS_FORCE_OFFSCREEN: 1 run: | pytest -v examples/ - name: Test examples notebooks, exclude ImageWidget notebook @@ -82,7 +82,7 @@ jobs: - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: - name: screenshot-diffs-${{ matrix.pyversion }}-${{ matrix.imgui_dep }}-${{ matrix.notebook_dep }} + name: screenshot-diffs-${{ matrix.os }}-${{ matrix.pyversion }}-${{ matrix.imgui_dep }}-${{ matrix.notebook_dep }} path: | examples/diffs examples/notebooks/diffs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f50b9623..0274add7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,12 +65,12 @@ jobs: python -c "from examples.tests.testutils import wgpu_backend; print(wgpu_backend)" - name: Test components env: - WGPU_FORCE_OFFSCREEN: 1 + RENDERCANVAS_FORCE_OFFSCREEN: 1 run: | pytest -v tests/ - name: Test examples env: - WGPU_FORCE_OFFSCREEN: 1 + RENDERCANVAS_FORCE_OFFSCREEN: 1 run: | pytest -v examples/ - name: Test examples notebooks, exclude ImageWidget notebook @@ -88,7 +88,7 @@ jobs: - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: - name: screenshot-diffs-${{ matrix.pyversion }}-${{ matrix.imgui_dep }}-${{ matrix.notebook_dep }} + name: screenshot-diffs-${{ matrix.os }}-${{ matrix.pyversion }}-${{ matrix.imgui_dep }}-${{ matrix.notebook_dep }} path: | examples/diffs examples/notebooks/diffs diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index fe267291a..a0cb54357 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -68,18 +68,20 @@ jobs: if: ${{ github.ref == 'refs/heads/main' }} # any push to main goes to fastplotlib.org/ver/dev run: echo "DOCS_VERSION_DIR=dev" >> "$GITHUB_ENV" - - # upload docs via FTP + + # upload docs via SCP - name: Deploy docs - uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + uses: appleboy/scp-action@v0.1.7 with: - server: ${{ secrets.DOCS_SERVER }} + host: ${{ secrets.DOCS_SERVER }} username: ${{ secrets.DOCS_USERNAME }} - password: ${{ secrets.DOCS_PASSWORD }} - # built docs - local-dir: docs/build/html/ - # output subdir based on the previous if statements - server-dir: ./ver/${{ env.DOCS_VERSION_DIR }}/ + port: ${{ secrets.DOCS_PORT }} + key: ${{ secrets.DOCS_KEY }} + passphrase: ${{ secrets.DOCS_PASS }} + source: "docs/build/html/*" + # without strip_components it creates dirs docs/build/html within /ver on the server + strip_components: 3 + target: /home/${{ secrets.DOCS_USERNAME }}/public_html/ver/${{ env.DOCS_VERSION_DIR }}/ # comment on PR to provide link to built docs - name: Add PR link in comment @@ -88,17 +90,18 @@ jobs: with: message: | 📚 Docs preview built and uploaded! https://www.fastplotlib.org/ver/${{ env.DOCS_VERSION_DIR }} - - # also deploy to root if this is a new release - # i.e., fastplotlib.org/ points to docs for the latest release - - name: Deploy docs + + # upload docs via SCP + - name: Deploy docs release if: ${{ github.ref_type == 'tag' }} - uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + uses: appleboy/scp-action@v0.1.7 with: - server: ${{ secrets.DOCS_SERVER }} + host: ${{ secrets.DOCS_SERVER }} username: ${{ secrets.DOCS_USERNAME }} - password: ${{ secrets.DOCS_PASSWORD }} - local-dir: docs/build/html/ - server-dir: ./ # deploy to the root dir - exclude: | # don't delete the /ver/ dir - **/ver/** + port: ${{ secrets.DOCS_PORT }} + key: ${{ secrets.DOCS_KEY }} + passphrase: ${{ secrets.DOCS_PASS }} + source: "docs/build/html/*" + # without strip_components it creates dirs docs/build/html within /ver on the server + strip_components: 3 + target: /home/${{ secrets.DOCS_USERNAME }}/public_html/ diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index c7f3add5e..0985fc179 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -52,7 +52,7 @@ jobs: PYGFX_EXPECT_LAVAPIPE: true run: | # regenerate screenshots - WGPU_FORCE_OFFSCREEN=1 REGENERATE_SCREENSHOTS=1 pytest -v examples + RENDERCANVAS_FORCE_OFFSCREEN=1 REGENERATE_SCREENSHOTS=1 pytest -v examples - name: Generate screenshots notebook, exclude image widget env: PYGFX_EXPECT_LAVAPIPE: true diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 65efc3352..0ae81f6f0 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -63,6 +63,7 @@ We strive to: - Excessive profanity. Please avoid swearwords; people differ greatly in their sensitivity to swearing. - Repeated harassment of others. In general, if someone asks you to stop, then stop. - Advocating for, or encouraging, any of the above behavior. + - LLM spam or inauthentic interaction that is completely generated by an LLM is discouraged. We welcome the use of LLMs as tools, but unsolicited LLM bot accounts for example are not encouraged. # Diversity statement diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 347275b6a..be9e175e6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -100,11 +100,11 @@ git checkout -b my_feature_branch After you have made changes on this branch, add and commit them when you are ready: ```bash -# lint your code -black . +# black format only the source code +black fastplotlib/ # run tests from the repo root dir -WGPU_FORCE_OFFSCREEN=1 pytest tests/ +RENDERCANVAS_FORCE_OFFSCREEN=1 pytest tests/ # desktop examples pytest -v examples @@ -195,6 +195,13 @@ The tests will produce slightly different imperceptible (to a human) results on ground-truth. A small RMSE tolerance has been chosen, `0.025` for most examples. If the output image and ground-truth image are within that tolerance the test will pass. +If the test image and ground-truth image are above the threshold, the test will fail and a difference image will be located in the follow directory: + +``` +examples/desktop/diffs +examples/notebooks/diffs +``` + Some feature development may require the ground-truth screenshots to be updated. In the event that your changes require this, please do the following: @@ -288,12 +295,12 @@ pip install -e ".[imgui, tests, docs, notebook]" 4) Lint codebase and make sure tests pass ```bash -# lint codebase -black . +# black format only the source code +black fastplotlib/ # run tests # backend tests -WGPU_FORCE_OFFSCREEN=1 pytest tests/ +RENDERCANVAS_FORCE_OFFSCREEN=1 pytest tests/ # desktop examples pytest -v examples diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 59b844621..876757d40 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -103,6 +103,8 @@ Anyone (absolutely anyone, not just the leadership team members) who feels that ### Process +#### Usual process + 1. Contact the neutral moderator with a description of the conflict, max of 250 words. 2. Neutral moderator must schedule a vote within 15 days. If that is not possible then within the next 45 days. 3. The individual who has invoked the conflict vote can choose to present their case, or they may choose to let the neutral moderator represent them. @@ -110,12 +112,17 @@ Anyone (absolutely anyone, not just the leadership team members) who feels that 4. The maintainers vote on one of the actions from “Enforcement Guidelines”: https://www.contributor-covenant.org/version/2/1/code_of_conduct/. It is advised that the first offense leads to action (1) “Correction”. Repeated or serious offenses from the same individual/organization may lead to escalating levels of actions. Very bad behavior, as determined by the leadership team, can justify a first offense resulting in (3) “Temporary Ban” or (4) “Permanent Ban”. 5. The advisory committee members may advise on the actions, but the ultimate decision is voted on by the maintainers. +#### Bot accounts, LLM accounts, and spam + +Unsolicited bot accounts, inauthentic interaction that is completetely generated by an LLM, and LLM spam are against our Code of Conduct. Bot accounts with fully LLM generated comments, issues, pull requests, discussion posts, or any other unsolicited LLM generated content will be deleted by the maintainers without notice and the account will not be allowed to interact with the fastplotlib organization. + ## Transparency Governance decisions, meeting minutes, and voting outcomes are publicly documented and accessible. We aim for transparency to allow the broader community to understand and trust the governance process. ## Changes to this governance document -### Until February 28, 2025 +**Effective until February 5, 2026** -During early stages of fastplotlib development, changes to the governance document may be made directly through unanimous approval by the original maintainers, Kushal Kolar & Caitlin Lewis. They (Kushal & Caitlin) may also add new members to the advisory committee through unanimous approval. +Moving forward, `fastplotlib` will maintain the governance model as outlined above. The core maintainers (Kushal Kolar & Caitlin Lewis) will revisit in +one year to propose any necessary changes to the governance structure. diff --git a/docs/source/api/layouts/figure.rst b/docs/source/api/layouts/figure.rst index 17ee965b6..b5cbbd2bb 100644 --- a/docs/source/api/layouts/figure.rst +++ b/docs/source/api/layouts/figure.rst @@ -23,6 +23,7 @@ Properties Figure.cameras Figure.canvas Figure.controllers + Figure.layout Figure.names Figure.renderer Figure.shape @@ -33,13 +34,14 @@ Methods :toctree: Figure_api Figure.add_animations + Figure.add_subplot Figure.clear Figure.close Figure.export + Figure.export_numpy Figure.get_pygfx_render_area Figure.open_popup Figure.remove_animation - Figure.render + Figure.remove_subplot Figure.show - Figure.start_render diff --git a/docs/source/api/layouts/imgui_figure.rst b/docs/source/api/layouts/imgui_figure.rst index 38a546ae9..a338afe96 100644 --- a/docs/source/api/layouts/imgui_figure.rst +++ b/docs/source/api/layouts/imgui_figure.rst @@ -25,6 +25,7 @@ Properties ImguiFigure.controllers ImguiFigure.guis ImguiFigure.imgui_renderer + ImguiFigure.layout ImguiFigure.names ImguiFigure.renderer ImguiFigure.shape @@ -36,14 +37,15 @@ Methods ImguiFigure.add_animations ImguiFigure.add_gui + ImguiFigure.add_subplot ImguiFigure.clear ImguiFigure.close ImguiFigure.export + ImguiFigure.export_numpy ImguiFigure.get_pygfx_render_area ImguiFigure.open_popup ImguiFigure.register_popup ImguiFigure.remove_animation - ImguiFigure.render + ImguiFigure.remove_subplot ImguiFigure.show - ImguiFigure.start_render diff --git a/docs/source/api/layouts/subplot.rst b/docs/source/api/layouts/subplot.rst index 3de44222d..e1c55514d 100644 --- a/docs/source/api/layouts/subplot.rst +++ b/docs/source/api/layouts/subplot.rst @@ -26,15 +26,16 @@ Properties Subplot.canvas Subplot.controller Subplot.docks + Subplot.frame Subplot.graphics Subplot.legends Subplot.name Subplot.objects Subplot.parent - Subplot.position Subplot.renderer Subplot.scene Subplot.selectors + Subplot.title Subplot.toolbar Subplot.viewport @@ -54,16 +55,11 @@ Methods Subplot.auto_scale Subplot.center_graphic Subplot.center_scene - Subplot.center_title Subplot.clear Subplot.delete_graphic Subplot.get_figure - Subplot.get_rect Subplot.insert_graphic Subplot.map_screen_to_world Subplot.remove_animation Subplot.remove_graphic - Subplot.render - Subplot.set_title - Subplot.set_viewport_rect diff --git a/docs/source/conf.py b/docs/source/conf.py index 66b3c9317..865c462a6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -59,6 +59,7 @@ "../../examples/heatmap", "../../examples/image_widget", "../../examples/gridplot", + "../../examples/window_layouts", "../../examples/line", "../../examples/line_collection", "../../examples/scatter", @@ -88,8 +89,6 @@ templates_path = ["_templates"] exclude_patterns = [] -napoleon_custom_sections = ["Features"] - # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output diff --git a/docs/source/user_guide/faq.rst b/docs/source/user_guide/faq.rst index 029daabab..0061a04d4 100644 --- a/docs/source/user_guide/faq.rst +++ b/docs/source/user_guide/faq.rst @@ -44,8 +44,8 @@ How does ``fastplotlib`` relate to ``matplotlib``? How can I learn to use ``fastplotlib``? --------------------------------------- - We want `fastplotlib` to be easy to learn and use. To get started with the library we recommend taking a look at our `guide `_ and - `examples gallery `_. + We want `fastplotlib` to be easy to learn and use. To get started with the library we recommend taking a look at our `guide `_ and + `examples gallery `_. In general, if you are familiar with numpy and array notation you will already have a intuitive understanding of interacting with your data in `fastplotlib`. If you have any questions, please do not hesitate to post an issue or discussion forum post. diff --git a/examples/gridplot/gridplot_viewports_check.py b/examples/gridplot/gridplot_viewports_check.py new file mode 100644 index 000000000..99584b411 --- /dev/null +++ b/examples/gridplot/gridplot_viewports_check.py @@ -0,0 +1,37 @@ +""" +GridPlot test viewport rects +============================ + +Test figure to test that viewport rects are positioned correctly +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'hidden' + +import fastplotlib as fpl +import numpy as np + + +figure = fpl.Figure( + shape=(2, 3), + size=(700, 560), + names=list(map(str, range(6))) +) + +np.random.seed(0) +a = np.random.rand(6, 10, 10) + +for data, subplot in zip(a, figure): + subplot.add_image(data) + subplot.docks["left"].size = 20 + subplot.docks["right"].size = 30 + subplot.docks["bottom"].size = 40 + +figure.show() + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/image_widget/image_widget_videos.py b/examples/image_widget/image_widget_videos.py index 1e367f0ad..7de4a9c04 100644 --- a/examples/image_widget/image_widget_videos.py +++ b/examples/image_widget/image_widget_videos.py @@ -29,7 +29,7 @@ [random_data, cockatoo_sub], rgb=[False, True], figure_shape=(2, 1), # 2 rows, 1 column - figure_kwargs={"size": (700, 560)} + figure_kwargs={"size": (700, 940)} ) iw.show() diff --git a/examples/image_widget/image_widget_viewports_check.py b/examples/image_widget/image_widget_viewports_check.py new file mode 100644 index 000000000..057134341 --- /dev/null +++ b/examples/image_widget/image_widget_viewports_check.py @@ -0,0 +1,35 @@ +""" +ImageWidget test viewport rects +=============================== + +Test Figure to test that viewport rects are positioned correctly in an image widget +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'hidden' + +import fastplotlib as fpl +import numpy as np + +np.random.seed(0) +a = np.random.rand(6, 15, 10, 10) + +iw = fpl.ImageWidget( + data=[img for img in a], + names=list(map(str, range(6))), + figure_kwargs={"size": (700, 560)}, +) + +for subplot in iw.figure: + subplot.docks["left"].size = 10 + subplot.docks["bottom"].size = 40 + +iw.show() + +figure = iw.figure + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/machine_learning/kmeans.py b/examples/machine_learning/kmeans.py new file mode 100644 index 000000000..0aae8fdae --- /dev/null +++ b/examples/machine_learning/kmeans.py @@ -0,0 +1,125 @@ +""" +K-Means Clustering of MNIST Dataset +=================================== + +Example showing how you can perform K-Means clustering on the MNIST dataset. + +Use WASD keys on your keyboard to fly through the data in PCA space. +Use the mouse pointer to select points. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +import fastplotlib as fpl +import numpy as np +from sklearn.datasets import load_digits +from sklearn.cluster import KMeans +from sklearn.decomposition import PCA + +# load the data +mnist = load_digits() + +# get the data and labels +data = mnist['data'] # (1797, 64) +labels = mnist['target'] # (1797,) + +# visualize the first 5 digits +# NOTE: this is just to give a sense of the dataset if you are unfamiliar, +# the more interesting visualization is below :D +fig_data = fpl.Figure(shape=(1, 5), size=(900, 300)) + +# iterate through each subplot +for i, subplot in enumerate(fig_data): + # reshape each image to (8, 8) + subplot.add_image(data[i].reshape(8, 8), cmap="gray", interpolation="linear") + # add the label as a title + subplot.title = f"Label: {labels[i]}" + # turn off the axes and toolbar + subplot.axes.visible = False + subplot.toolbar = False + +fig_data.show() + +# project the data from 64 dimensions down to the number of unique digits +n_digits = len(np.unique(labels)) # 10 + +reduced_data = PCA(n_components=n_digits).fit_transform(data) # (1797, 10) + +# performs K-Means clustering, take the best of 4 runs +kmeans = KMeans(n_clusters=n_digits, n_init=4) +# fit the lower-dimension data +kmeans.fit(reduced_data) + +# get the centroids (center of the clusters) +centroids = kmeans.cluster_centers_ + +# plot the kmeans result and corresponding original image +figure = fpl.Figure( + shape=(1, 2), + size=(700, 560), + cameras=["3d", "2d"], + controller_types=["fly", "panzoom"] +) + +# set the axes to False in the image subplot +figure[0, 1].axes.visible = False + +figure[0, 0].title = "k-means clustering of PCA-reduced data" +figure[0, 1].title = "handwritten digit" + +# plot the centroids +figure[0, 0].add_scatter( + data=np.vstack([centroids[:, 0], centroids[:, 1], centroids[:, 2]]).T, + colors="white", + sizes=15 +) +# plot the down-projected data +digit_scatter = figure[0,0].add_scatter( + data=np.vstack([reduced_data[:, 0], reduced_data[:, 1], reduced_data[:, 2]]).T, + sizes=5, + cmap="tab10", # use a qualitative cmap + cmap_transform=kmeans.labels_, # color by the predicted cluster +) + +# initial index +ix = 0 + +# plot the initial image +digit_img = figure[0, 1].add_image( + data=data[ix].reshape(8,8), + cmap="gray", + name="digit", + interpolation="linear" +) + +# change the color and size of the initial selected data point +digit_scatter.colors[ix] = "magenta" +digit_scatter.sizes[ix] = 10 + + +# define event handler to update the selected data point +@digit_scatter.add_event_handler("pointer_enter") +def update(ev): + # reset colors and sizes + digit_scatter.cmap = "tab10" + digit_scatter.sizes = 5 + + # update with new seleciton + ix = ev.pick_info["vertex_index"] + + digit_scatter.colors[ix] = "magenta" + digit_scatter.sizes[ix] = 10 + + # update digit fig + figure[0, 1]["digit"].data = data[ix].reshape(8, 8) + + +figure.show() + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() \ No newline at end of file diff --git a/examples/notebooks/nb_test_utils.py b/examples/notebooks/nb_test_utils.py index e1c32e0a0..9d99e3be3 100644 --- a/examples/notebooks/nb_test_utils.py +++ b/examples/notebooks/nb_test_utils.py @@ -94,6 +94,26 @@ def plot_test(name, fig: fpl.Figure): if not TESTING: return + # otherwise the first render is wrong + if fpl.IMGUI: + # there doesn't seem to be a resize event for the manual offscreen canvas + fig.imgui_renderer._backend.io.display_size = fig.canvas.get_logical_size() + # run this once so any edge widgets set their sizes and therefore the subplots get the correct rect + # hacky but it works for now + fig.imgui_renderer.render() + + fig._fpl_reset_layout() + # render each subplot + for subplot in fig: + subplot.viewport.render(subplot.scene, subplot.camera) + + # flush pygfx renderer + fig.renderer.flush() + + if fpl.IMGUI: + # render imgui + fig.imgui_renderer.render() + snapshot = fig.canvas.snapshot() rgb_img = rgba_to_rgb(snapshot.data) diff --git a/examples/notebooks/quickstart.ipynb b/examples/notebooks/quickstart.ipynb index 09317110d..737aee3e7 100644 --- a/examples/notebooks/quickstart.ipynb +++ b/examples/notebooks/quickstart.ipynb @@ -1695,22 +1695,6 @@ "figure_grid[\"top-right-plot\"]" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "cb7566a5", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "# view its position\n", - "figure_grid[\"top-right-plot\"].position" - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/examples/notebooks/screenshots/nb-astronaut.png b/examples/notebooks/screenshots/nb-astronaut.png index 32b09caf9..2370c5988 100644 --- a/examples/notebooks/screenshots/nb-astronaut.png +++ b/examples/notebooks/screenshots/nb-astronaut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d9e2b0479d3de1c12764b984679dba83a1876ea6a88c072789a0e06f957ca2a -size 70655 +oid sha256:0a6e8bb3c72f1be6915e8e78c9a4f269419cfb4faded16e39b5cb11d70bec247 +size 64185 diff --git a/examples/notebooks/screenshots/nb-astronaut_RGB.png b/examples/notebooks/screenshots/nb-astronaut_RGB.png index be498bb6d..2a7eac585 100644 --- a/examples/notebooks/screenshots/nb-astronaut_RGB.png +++ b/examples/notebooks/screenshots/nb-astronaut_RGB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2d02877510191e951d38d03a6fe9d31f5c0c335913876c65b175c4bb1a9c0e1 -size 69942 +oid sha256:9f9f32e86018f87057435f7121b02bbe98823444babb330645bab618e1d586b7 +size 63838 diff --git a/examples/notebooks/screenshots/nb-camera.png b/examples/notebooks/screenshots/nb-camera.png index 3e9a518f9..bfe226ca4 100644 --- a/examples/notebooks/screenshots/nb-camera.png +++ b/examples/notebooks/screenshots/nb-camera.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5271c2204a928185b287c73c852ffa06b708d8d6a33de09acda8d2ea734e78c5 -size 51445 +oid sha256:2964d0150b38f990a7b804e9057f99505e8c99bb04538a13137989d540704593 +size 47456 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png index 8c353442a..2578ad028 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d8563587c4f642d5e4edb34f41b569673d7ea71bcbafdb734369272776baeef -size 62316 +oid sha256:78e7e99fafc15cc6edf53cfb2e5b679623ad14e0d594e0ad615088e623be22e1 +size 60988 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png index 22c7ad73a..bb2e1ee37 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b122f0ba9bfff0b0868778f09744870238bf7b4945e57410b6aa36341eaaf4a -size 116781 +oid sha256:6c9b898259fc965452ef0b6ff53ac7fa41196826c6e27b6b5d417d33fb352051 +size 112399 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png index 22c7ad73a..bb2e1ee37 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b122f0ba9bfff0b0868778f09744870238bf7b4945e57410b6aa36341eaaf4a -size 116781 +oid sha256:6c9b898259fc965452ef0b6ff53ac7fa41196826c6e27b6b5d417d33fb352051 +size 112399 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png index 84e2514d0..1841cd237 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fcc5092f35c881da4a9b9f3c216fb608b8dfc27a791b83e0d5184ef3973746cf -size 139375 +oid sha256:b9cbc2a6916c7518d40812a13276270eb1acfc596f3e6e02e98a6a5185da03a4 +size 132971 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png index 075116ff4..6cc1821fa 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fabd9d52ae2815ae883a4c8c8a8b1385c0824e0212347896a09eb3600c29430 -size 124238 +oid sha256:070748e90bd230a01d3ae7c6d6487815926b0158888a52db272356dc8b0a89d7 +size 119453 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png index 216ae2b9e..3865aef93 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86ad31cab3aefa24a1c4c0adc2033cbc9fa594e9cf8ab8e4a6ff0a3630bb7896 -size 109041 +oid sha256:b24450ccf1f8cf902b8e37e73907186f37a6495f227dcbd5ec53f75c52125f56 +size 105213 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png index 99302d4e6..025086930 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ebf4e875199c7e682dc15aa03a36ea9f111618853a94076064b055bf6ce788e -size 101209 +oid sha256:3dfc8e978eddf08d1ed32e16fbf93c037ccdf5f7349180dcda54578a8c9e1a18 +size 97359 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png index 3bb5081f0..5ff5052b0 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8dbf6b76818315e40d9d4cc97807c4276c27e7a9a09d2643f74adf701ef1cdc -size 123136 +oid sha256:00130242d3f199926604df16dda70a062071f002566a8056e4794805f29adfde +size 118044 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png index 3bb5081f0..5ff5052b0 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8dbf6b76818315e40d9d4cc97807c4276c27e7a9a09d2643f74adf701ef1cdc -size 123136 +oid sha256:00130242d3f199926604df16dda70a062071f002566a8056e4794805f29adfde +size 118044 diff --git a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png index 48ab5d6fe..e8c02adfe 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png +++ b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c65e2dc4276393278ab769706f22172fd71e38eeb3c9f4d70fa51de31820f1d1 -size 234012 +oid sha256:8c8562f8e1178cf21af98af635006c64010f3c5fc615533d1df8c49479232843 +size 217693 diff --git a/examples/notebooks/screenshots/nb-image-widget-single.png b/examples/notebooks/screenshots/nb-image-widget-single.png index 5e1cb8cc1..8de4099fb 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single.png +++ b/examples/notebooks/screenshots/nb-image-widget-single.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d4e4edf1429a135bafb7c1c927ea87f78a93fb5f3e0cbee2fb5c156af88d5a0 -size 220490 +oid sha256:5c9bae3c9c5521a4054288be7ae548204fc7b0eafbc3e99cb6b649e0be797169 +size 207176 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png index ec2911374..13297e09f 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39adce1898e5b00ccf9d8792bd4e76f2da2591a8c3f6e201a5c2af1f814d37cb -size 58692 +oid sha256:70c7738ed303f5a3e19271e8dfc12ab857a6f3aff767bdbecb485b763a09913e +size 55584 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png index ae72c8175..b8307bc44 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d50b960c66acc6672aaeb6a97f6a69aad14f9e54060c3702679d6a5bf2b70e78 -size 70582 +oid sha256:66a435e45dc4643135633115af2eeaf70761e408a94d70d94d80c14141574528 +size 69343 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png index 66f9136dc..d6237dc9f 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d244a8a91d04380f2ebe255b2b56b3be5249c0a382821877710cae6bdaa2d414 -size 128643 +oid sha256:731f225fa2de3457956b2095d1cc539734983d041b13d6ad1a1f9d8e7ebfa4bc +size 115239 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png index 230e71c0f..ecf63a369 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24c991457b8b081ee271cbdb03821ea1024c8340db92340e0e445bf3c70aba40 -size 97903 +oid sha256:7e2d70159ac47c004acb022b3a669e7bd307299ddd590b83c08854b0dba27b70 +size 93885 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png index a355670a0..e7106fae9 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bdd62a9bd1ca4f1ff110a30fb4064d778f02120295a3e3d30552e06892146e40 -size 93658 +oid sha256:1756783ab90435b46ded650033cf29ac36d2b4380744bf312caa2813267f7f38 +size 89813 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png index c47545ccb..ddd4f85ca 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db7e2cf15ff3ce61b169b114c630e2339c1c6b5c687c580e1ee6964785df6790 -size 74844 +oid sha256:a35e2e4b892b55f5d2500f895951f6a0289a2df3b69cf12f59409bbc091d1caf +size 72810 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png index 69ef49149..d9971c3fd 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64d2d3fd91ac8e10736add5a82a312927ae6f976119cfa2aaa1fc1e008bcf6f1 -size 66038 +oid sha256:3bdb0ed864c8a6f2118cfe0d29476f61c54576f7b8e041f3c3a895ba0a440c05 +size 65039 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png index bb04d1800..6736e108c 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d2a805c85e1cdf5bd2d995600b033019680ac645d7634efeaf1db7d0d00d4aa -size 79403 +oid sha256:7ae7c86bee3a30bde6cfa44e1e583e6dfd8de6bb29e7c86cea9141ae30637b4a +size 80627 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png index 5b1a4a8da..dce99223b 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:440623bb4588994c4f52f17e027af29d1f2d5d330c5691630fd2acb9e38f8a25 -size 99033 +oid sha256:b51a5d26f2408748e59e3ee481735694f8f376539b50deb2b5c5a864b7de1079 +size 105581 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png index bd72160dd..cdea3673d 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ee56adf8f2a516ef74a799e9e503f980c36c4dfb41f9ff6d8168cfcf65ad092 -size 132745 +oid sha256:e854f7f2fdaeeac6c8358f94a33698b5794c0f6c55b240d384e8c6d51fbfb0ff +size 143301 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png index 438d1e2d4..25a2fa53e 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de4733b82c2e77baa659582401eff0c70ec583c50462b33bcbfd2bb00ceaa517 -size 102959 +oid sha256:c8c8d3c59c145a4096deceabc71775a03e5e121e82509590787c768944d155bd +size 110744 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png index ee081c6df..00a4a1fd2 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6107f108b5a86ba376c53f5e207841c01a85b686100efb46e5df3565127201d2 -size 106765 +oid sha256:c4b4af7b99cad95ea3f688af8633de24b6602bd700cb244f93c28718af2e1e85 +size 114982 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png index c2071c850..3b5594c64 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:caa15f6bc21a3be0f480b442414ec4b96b21cc1de2cdcb891b366692962d4ef8 -size 100753 +oid sha256:6d28a4be4c76d5c0da5f5767b169acf7048a268b010f33f96829a5de7f06fd7d +size 107477 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png index 3d90fd77a..239237b45 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e23288d695a5a91188b285f6a0a2c9f0643dd19f3d6dedb56f4389f44ed1f44 -size 98621 +oid sha256:30dba982c9a605a7a3c0f2fa6d8cdf0df4160b2913a95b26ffdb6b04ead12add +size 104603 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png index 3fd5688d9..0745a4d4a 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b4e1bb60466d7553b4d1afc14015b7c4edc6e79c724c0afb5acd123a10093d0 -size 105541 +oid sha256:e431229806ee32a78fb9313a09af20829c27799798232193feab1723b66b1bca +size 112646 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png index 048078520..498b19cb7 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33ce1260b4715b3d28ba28d0ad4c5eb94c9997bdc1676ff6208121e789e168a5 -size 99287 +oid sha256:a8e899b48881e3eb9200cc4e776db1f865b0911c340c06d4009b3ae12aa1fc85 +size 105421 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png index ade8fb483..369168141 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e08f4e4cb3330fbbbf827af56c02039af3b293036c7676f2a87c309ad07f2f6 -size 99759 +oid sha256:93933e7ba5f791072df2934c94a782e39ed97f7db5b55c5d71c8c5bbfc69d800 +size 106360 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png index 14d9e8448..b62721be2 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3aad82db14f8100f669d2ad36b5bc3973b7c12457adfdd73adbc81c759338f7b -size 80964 +oid sha256:bf38b2af1ceb372cd0949d42c027acb5fcc4c6b9a8f38c5aacdce1cd14e290fe +size 78533 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png index af04a6f73..76ed01a7c 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e40559eea03790315718c55b4ec4976aacb97a2f07bcdc49d917c044745687c2 -size 117144 +oid sha256:ff462d24820f0bdd509e58267071fa956b5c863b8b8d66fea061c5253b7557cf +size 113926 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png index 7f530e554..d9a593ee7 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:414ebe9a0b2bc4eb1caa4b4aeef070955c662bb691899c4b2046be3e2ca821e3 -size 113649 +oid sha256:2b8fd14f8e8a90c3cd3fbb84a00d50b1b826b596d64dfae4a5ea1bab0687d906 +size 110829 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png index e2f6b8318..cf10c6d42 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea6d0c4756db434af6e257b7cd809f1d49089eca6b9eae9e347801e20b175686 -size 113631 +oid sha256:d88c64b716d19a3978bd60f8d75ffe09e022183381898fa1c48b77598be8fb7c +size 111193 diff --git a/examples/notebooks/screenshots/nb-lines-3d.png b/examples/notebooks/screenshots/nb-lines-3d.png index 2e26a8cd7..fb84ef21a 100644 --- a/examples/notebooks/screenshots/nb-lines-3d.png +++ b/examples/notebooks/screenshots/nb-lines-3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:857eb528b02fd7dd4b9f46ce1e65942066736f1bdf5271db141d73a0abab82b0 -size 19457 +oid sha256:c70c01b3ade199864df227a44fb28a53626df3beecee722a7b782c9a9f4658d8 +size 19907 diff --git a/examples/notebooks/screenshots/nb-lines-colors.png b/examples/notebooks/screenshots/nb-lines-colors.png index 1e13983f3..ab221d83f 100644 --- a/examples/notebooks/screenshots/nb-lines-colors.png +++ b/examples/notebooks/screenshots/nb-lines-colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6681a1e5658c1f2214217dcb7321cad89c7a0a3fd7919296a1069f27f1a7ee92 -size 35381 +oid sha256:3b238b085eddb664ff56bd265423d85b35fc70769ebec050b27fefa8fe6380de +size 35055 diff --git a/examples/notebooks/screenshots/nb-lines-data.png b/examples/notebooks/screenshots/nb-lines-data.png index a7e8287ef..44b142f55 100644 --- a/examples/notebooks/screenshots/nb-lines-data.png +++ b/examples/notebooks/screenshots/nb-lines-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:043d8d9cd6dfc7627a6ccdb5810efd4b1a15e8880a4e30c0f558ae4d67c2f470 -size 42410 +oid sha256:4df736ec3ea90478930a77437949977f8e30f7d9272f65ef9f4908f2103dd11e +size 40679 diff --git a/examples/notebooks/screenshots/nb-lines-underlay.png b/examples/notebooks/screenshots/nb-lines-underlay.png index c2908d479..f4a5b4e76 100644 --- a/examples/notebooks/screenshots/nb-lines-underlay.png +++ b/examples/notebooks/screenshots/nb-lines-underlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c52ac60ffc08005d1f1fcad1b29339a89a0f31b58c9ca692f9d93400e7c8ac9e -size 48540 +oid sha256:3a8b59386015b4c1eaa85c33c7b041d566ac1ac76fbba829075e9a3af021bedf +size 46228 diff --git a/examples/notebooks/screenshots/nb-lines.png b/examples/notebooks/screenshots/nb-lines.png index f4a4d58b1..8c86b48d0 100644 --- a/examples/notebooks/screenshots/nb-lines.png +++ b/examples/notebooks/screenshots/nb-lines.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2cef0e2fb84e985f8d9c18f77817fb3eba31bd30b8fa4c54bb71432587909458 -size 30075 +oid sha256:823558e877830b816cc87df0776a92d5316d98a4f40e475cbf997b597c5eb8de +size 30338 diff --git a/examples/notebooks/screenshots/no-imgui-nb-astronaut.png b/examples/notebooks/screenshots/no-imgui-nb-astronaut.png index a1e524e2a..9f9e2013a 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-astronaut.png +++ b/examples/notebooks/screenshots/no-imgui-nb-astronaut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:915f6c4695c932dc2aa467be750e58a0435fe86fe0e0fa5a52c6065e05ec3193 -size 85456 +oid sha256:4758a94e6c066d95569515c0bff8e4c9ec383c65c5928a827550c142214df085 +size 72372 diff --git a/examples/notebooks/screenshots/no-imgui-nb-astronaut_RGB.png b/examples/notebooks/screenshots/no-imgui-nb-astronaut_RGB.png index ec3208e01..23d1bd906 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-astronaut_RGB.png +++ b/examples/notebooks/screenshots/no-imgui-nb-astronaut_RGB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31cfa60229a4e297be507a8888e08d6950c2a7d4b323d34774c9462419272ada -size 84284 +oid sha256:fb3c72edc6f41d6f77e44bc68e7f5277525d2548d369925827c14d855dc33bbd +size 71588 diff --git a/examples/notebooks/screenshots/no-imgui-nb-camera.png b/examples/notebooks/screenshots/no-imgui-nb-camera.png index 31b60d9c0..22c70a760 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-camera.png +++ b/examples/notebooks/screenshots/no-imgui-nb-camera.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:800845fae18093945ed921237c8756b1afa31ee391fe679b03c57a67929e4ba9 -size 60087 +oid sha256:6de3880cc22a8f6cdb77305e4d5be520fe92fd54a9a107bdbddf1e6f72c19262 +size 52157 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png b/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png index 35c777e6a..1a5a7b548 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4253362c0908e0d983542be3691a3d94f27a0319fb9e7183315c77891dac140 -size 23232 +oid sha256:f0e63c918aac713af2015cb85289c9451be181400834b0f60bcbb50564551f08 +size 20546 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png b/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png index b8e34aab3..cdce4bf46 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc95d6291d06ab64d142ba0048318caefa28b404bb4b31635df075dc651eaa08 -size 37276 +oid sha256:2bd481f558907ac1af97bd7ee08d58951bada758cc32467c73483fa66e4602f8 +size 36206 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-data.png b/examples/notebooks/screenshots/no-imgui-nb-lines-data.png index 8f58dbc6d..8923be766 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-data.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8aa0b8303f0a69609198ea312800fc0eb98007c18d0ebc37672a9cf4f1cbaff -size 46780 +oid sha256:ea39e2651408431ad5e49af378828a41b7b377f7f0098adc8ce2c7b5e10d0234 +size 43681 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png b/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png index b33cde5a6..b6b4cf340 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:822410f43d48d12e70930b5b581bafe624ea72475d53ca0d98cdaa5649338c63 -size 51849 +oid sha256:6a8d4aba2411598ecae1b7f202fbb1a1fa7416a814b7b4c5fdd1e0e584cdb06a +size 49343 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines.png b/examples/notebooks/screenshots/no-imgui-nb-lines.png index 5d7e704ca..5d03421a4 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e3ba744fcfa43df839fddce88f79fb8d7c5eafdd22f271e6b885e09b8891072 -size 31222 +oid sha256:b2fdaf79703c475521184ab9dc948d3e817160b0162e9d88fcb20207225d0233 +size 31153 diff --git a/examples/scatter/spinning_spiral.py b/examples/scatter/spinning_spiral.py new file mode 100644 index 000000000..56cdcb906 --- /dev/null +++ b/examples/scatter/spinning_spiral.py @@ -0,0 +1,86 @@ +""" +Spinning spiral scatter +======================= + +Example of a spinning spiral scatter. + +This example with 1 million points runs at 125 fps on an AMD RX 570. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'animate 15s' + +import numpy as np +import fastplotlib as fpl + +# number of points +n = 100_000 + +# create data in the shape of a spiral +phi = np.linspace(0, 30, n) + +xs = phi * np.cos(phi) + np.random.normal(scale=1.5, size=n) +ys = np.random.normal(scale=1, size=n) +zs = phi * np.sin(phi) + np.random.normal(scale=1.5, size=n) + +data = np.column_stack([xs, ys, zs]) + +# generate some random sizes for the points +sizes = np.abs(np.random.normal(loc=0, scale=1, size=n)) + +figure = fpl.Figure( + cameras="3d", + size=(700, 560), + canvas_kwargs={"max_fps": 500, "vsync": False} +) + +spiral = figure[0, 0].add_scatter(data, cmap="viridis_r", alpha=0.5, sizes=sizes) + +# pre-generate normally distributed data to jitter the points before each render +jitter = np.random.normal(scale=0.001, size=n * 3).reshape((n, 3)) + + +def update(): + # rotate around y axis + spiral.rotate(0.005, axis="y") + + # add small jitter + spiral.data[:] += jitter + # shift array to provide a random-sampling effect + # without re-running a random generator on each iteration + # generating 1 million normally distributed points takes ~50ms even with SFC64 + jitter[1000:] = jitter[:-1000] + jitter[:1000] = jitter[-1000:] + + +figure.add_animations(update) +figure.show() + +# pre-saved camera state +camera_state = { + 'position': np.array([-0.13046005, 20.09142224, 29.03347696]), + 'rotation': np.array([-0.44485092, 0.05335406, 0.11586037, 0.88647469]), + 'scale': np.array([1., 1., 1.]), + 'reference_up': np.array([0., 1., 0.]), + 'fov': 50.0, + 'width': 62.725074768066406, + 'height': 8.856056690216064, + 'zoom': 0.75, + 'maintain_aspect': True, + 'depth_range': None +} + +figure[0, 0].camera.set_state(camera_state) +figure[0, 0].axes.visible = False + + +if fpl.IMGUI: + # show fps with imgui overlay + figure.imgui_show_fps = True + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/screenshots/extent_frac_layout.png b/examples/screenshots/extent_frac_layout.png new file mode 100644 index 000000000..7fe6d3d37 --- /dev/null +++ b/examples/screenshots/extent_frac_layout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5991b755432318310cfc2b4826bd9639cc234883aa06f1895817f710714cb58f +size 156297 diff --git a/examples/screenshots/extent_layout.png b/examples/screenshots/extent_layout.png new file mode 100644 index 000000000..dec391ac2 --- /dev/null +++ b/examples/screenshots/extent_layout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0cf23f845932023789e0823a105910e9f701d0f03c04e3c18488f0da62420921 +size 123409 diff --git a/examples/screenshots/gridplot.png b/examples/screenshots/gridplot.png index 1a222affd..08e6d6b78 100644 --- a/examples/screenshots/gridplot.png +++ b/examples/screenshots/gridplot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8de769538bb435b71b33e038998b2bafa340c635211c0dfc388c7a5bf55fd36d -size 286794 +oid sha256:6f424ec68dbc0761566cd147f3bf5b8f15e4126c3b30b2ff47b6fb48f04d512a +size 252269 diff --git a/examples/screenshots/gridplot_non_square.png b/examples/screenshots/gridplot_non_square.png index 45d71abb2..781de8749 100644 --- a/examples/screenshots/gridplot_non_square.png +++ b/examples/screenshots/gridplot_non_square.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92f55da7e2912a68e69e212b31df760f27e72253ec234fe1dd5b5463b60061b3 -size 212647 +oid sha256:9ac9ee6fd1118a06a1f0de4eee73e7b6bee188c533da872c5cbaf7119114414f +size 194385 diff --git a/examples/screenshots/gridplot_viewports_check.png b/examples/screenshots/gridplot_viewports_check.png new file mode 100644 index 000000000..b1faf9b69 --- /dev/null +++ b/examples/screenshots/gridplot_viewports_check.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67dd50d61a0caaf563d95110f99fa24c567ddd778a697715247d697a1b5bb1ac +size 46667 diff --git a/examples/screenshots/heatmap.png b/examples/screenshots/heatmap.png index a63eb5ec8..defcca301 100644 --- a/examples/screenshots/heatmap.png +++ b/examples/screenshots/heatmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f2f0699e01eb12c44a2dbefd1d8371b86b3b3456b28cb5f1850aed44c13f412 -size 94505 +oid sha256:0789d249cb4cfad21c9f1629721ade26ed734e05b1b13c3a5871793f6271362b +size 91831 diff --git a/examples/screenshots/image_cmap.png b/examples/screenshots/image_cmap.png index 6f7081b03..0301d2ed4 100644 --- a/examples/screenshots/image_cmap.png +++ b/examples/screenshots/image_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1482ce72511bc4f815825c29fabac5dd0f2586ac4c827a220a5cecb1162be4d -size 210019 +oid sha256:d2bbb79716fecce08479fbe7977565daccadf4688c8a99e155db297ecce4c484 +size 199979 diff --git a/examples/screenshots/image_rgb.png b/examples/screenshots/image_rgb.png index 88beb7df3..11129ceaa 100644 --- a/examples/screenshots/image_rgb.png +++ b/examples/screenshots/image_rgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8210ad8d1755f7819814bdaaf236738cdf1e9a0c4f77120aca4968fcd8aa8a7a -size 239431 +oid sha256:23024936931651cdf4761f2cafcd8002bb12ab86e9efb13ddc99a9bf659c3935 +size 226879 diff --git a/examples/screenshots/image_rgbvminvmax.png b/examples/screenshots/image_rgbvminvmax.png index f3ef59d84..afe4de6f7 100644 --- a/examples/screenshots/image_rgbvminvmax.png +++ b/examples/screenshots/image_rgbvminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ebbcc4a2e83e9733eb438fe2341f77c86579421f3fa96b6a49e94073c0ffd32 -size 48270 +oid sha256:2fb9cd6d32813df6a9e3bf183f73cb69fdb61d290d7f2a4cc223ab34301351a1 +size 50231 diff --git a/examples/screenshots/image_simple.png b/examples/screenshots/image_simple.png index 0c7e011f4..702a1ac5c 100644 --- a/examples/screenshots/image_simple.png +++ b/examples/screenshots/image_simple.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44bc2d1fd97921fef0be45424f21513d5d978b807db8cf148dfc59c07f6e292f -size 211333 +oid sha256:b3eb6f03364226e9f1aae72f6414ad05b0239a15c2a0fbcd71d3718fee477e2c +size 199468 diff --git a/examples/screenshots/image_small.png b/examples/screenshots/image_small.png index 41a4a240e..d17cb7ab2 100644 --- a/examples/screenshots/image_small.png +++ b/examples/screenshots/image_small.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:079ee6254dc995cc5fc8c20ff1c00cb0899f21ba2d5d1a4dc0d020c3a71902c4 -size 13022 +oid sha256:2dcfc7b8a964db9a950bf4d3217fb171d081251b107977f9acd612fcd5fb0be1 +size 14453 diff --git a/examples/screenshots/image_vminvmax.png b/examples/screenshots/image_vminvmax.png index f3ef59d84..afe4de6f7 100644 --- a/examples/screenshots/image_vminvmax.png +++ b/examples/screenshots/image_vminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ebbcc4a2e83e9733eb438fe2341f77c86579421f3fa96b6a49e94073c0ffd32 -size 48270 +oid sha256:2fb9cd6d32813df6a9e3bf183f73cb69fdb61d290d7f2a4cc223ab34301351a1 +size 50231 diff --git a/examples/screenshots/image_widget.png b/examples/screenshots/image_widget.png index af248dd3e..23d34ae50 100644 --- a/examples/screenshots/image_widget.png +++ b/examples/screenshots/image_widget.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2ae1938c5e7b742fb2dac0336877028f6ece26cd80e84f309195a55601025cb -size 197495 +oid sha256:220ebb5286b48426f9457b62d6e7f9fe61b5a62b8874c7e010e07e146ae205a5 +size 184633 diff --git a/examples/screenshots/image_widget_grid.png b/examples/screenshots/image_widget_grid.png index e0f0ff5c8..45bc70726 100644 --- a/examples/screenshots/image_widget_grid.png +++ b/examples/screenshots/image_widget_grid.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eeb5b86e7c15dfe2e71267453426930200223026f72156f34ff1ccc2f9389b6e -size 253769 +oid sha256:306977f7eebdb652828ba425d73b6018e97c100f3cf8f3cbaa0244ffb6c040a3 +size 249103 diff --git a/examples/screenshots/image_widget_imgui.png b/examples/screenshots/image_widget_imgui.png index 135a0d4c4..cb165cc86 100644 --- a/examples/screenshots/image_widget_imgui.png +++ b/examples/screenshots/image_widget_imgui.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e2cd0e3892377e6e2d552199391fc64aac6a02413168a5b4c5c4848f3390dec -size 166265 +oid sha256:7522a35768d013a257e3cf3b00cce626b023b169484e035f46c635efc553b0bf +size 165747 diff --git a/examples/screenshots/image_widget_single_video.png b/examples/screenshots/image_widget_single_video.png index 5d10d91a6..aa757a950 100644 --- a/examples/screenshots/image_widget_single_video.png +++ b/examples/screenshots/image_widget_single_video.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de1750c9c1c3cd28c356fb51687f4a8f00afb3cc7e365502342168fce8459d3a -size 90307 +oid sha256:5f0843f4693460ae985c1f33d84936fbcc943d0405e0893186cbee7a5765dbc0 +size 90283 diff --git a/examples/screenshots/image_widget_videos.png b/examples/screenshots/image_widget_videos.png index f0e262e24..2e289ae3c 100644 --- a/examples/screenshots/image_widget_videos.png +++ b/examples/screenshots/image_widget_videos.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23d993e0b5b6bcfe67da7aa4ceab3f06e99358b00f287b9703c4c3bff19648ba -size 169541 +oid sha256:eec22392f85db1fd375d7ffa995a2719cf86821fe3fe85913f4ab66084eccbf9 +size 290587 diff --git a/examples/screenshots/image_widget_viewports_check.png b/examples/screenshots/image_widget_viewports_check.png new file mode 100644 index 000000000..662432e59 --- /dev/null +++ b/examples/screenshots/image_widget_viewports_check.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c4449f7e97375aa9d7fe1d00364945fc86b568303022157621de21a20d1d13e +size 93914 diff --git a/examples/screenshots/imgui_basic.png b/examples/screenshots/imgui_basic.png index 27288e38f..1ff9952a9 100644 --- a/examples/screenshots/imgui_basic.png +++ b/examples/screenshots/imgui_basic.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3391b7cf02fc7bd2c73dc57214b21ceaca9a1513556b3a4725639f21588824e4 -size 36261 +oid sha256:09cc7b0680e53ae1a2689b63f9b0ed641535fcffc99443cd455cc8d9b6923229 +size 36218 diff --git a/examples/screenshots/line.png b/examples/screenshots/line.png index 492ea2ada..02603b692 100644 --- a/examples/screenshots/line.png +++ b/examples/screenshots/line.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1458d472362f8d5bcef599fd64f931997a246f9e7649c80cc95f465cbd858850 -size 170243 +oid sha256:9bfaa54bde0967463413ecd2defa8ca18169d534163cc8b297879900e812fee8 +size 167012 diff --git a/examples/screenshots/line_cmap.png b/examples/screenshots/line_cmap.png index 10779fcd5..1ecc930e4 100644 --- a/examples/screenshots/line_cmap.png +++ b/examples/screenshots/line_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66e64835f824d80dd7606d90530517dbc320bcc11a68393ab92c08fef3d23f5a -size 48828 +oid sha256:d0503c008f8869dcf83793c21b15169a93558988c1a5c4edfd2aa93c549d25e1 +size 49343 diff --git a/examples/screenshots/line_cmap_more.png b/examples/screenshots/line_cmap_more.png index 56e3fe8cc..4bf597e8b 100644 --- a/examples/screenshots/line_cmap_more.png +++ b/examples/screenshots/line_cmap_more.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de08452e47799d9afcadfc583e63da1c02513cf73000bd5c2649236e61ed6b34 -size 126725 +oid sha256:ab4d759dd679a2959c0fda724e7b7a1b7593d6f67ce797f08a5292dd0eb74fb1 +size 125023 diff --git a/examples/screenshots/line_collection.png b/examples/screenshots/line_collection.png index d9124daf1..382132770 100644 --- a/examples/screenshots/line_collection.png +++ b/examples/screenshots/line_collection.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50920f4bc21bb5beffe317777a20d8d09f90f3631a14df51c219814d3507c602 -size 100758 +oid sha256:b3b6b973a52f7088536a4f437be2a7f6ebb2787756f9170145a945c53e90093c +size 98950 diff --git a/examples/screenshots/line_collection_cmap_values.png b/examples/screenshots/line_collection_cmap_values.png index e04289699..c00bffdb6 100644 --- a/examples/screenshots/line_collection_cmap_values.png +++ b/examples/screenshots/line_collection_cmap_values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:850e3deb2220d44f01e6366ee7cffb83085cad933a137b9838ce8c2231e7786a -size 64152 +oid sha256:45bb6652f477ab0165bf59e504c1935e5781bceea9a891fcfa9975dec92eef4b +size 64720 diff --git a/examples/screenshots/line_collection_cmap_values_qualitative.png b/examples/screenshots/line_collection_cmap_values_qualitative.png index 710cee119..662d3254d 100644 --- a/examples/screenshots/line_collection_cmap_values_qualitative.png +++ b/examples/screenshots/line_collection_cmap_values_qualitative.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba5fefc8e1043fe0ebd926a6b8e6ab19e724205a4c13e4d7740122cfe464e38b -size 67017 +oid sha256:4e5b5cb45e78ae24d72f3cb84e482fac7bf0a98cd9b9b934444d2e67c9910d57 +size 66565 diff --git a/examples/screenshots/line_collection_colors.png b/examples/screenshots/line_collection_colors.png index 6c1d05f04..3b90e5b4c 100644 --- a/examples/screenshots/line_collection_colors.png +++ b/examples/screenshots/line_collection_colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17d48f07310090b835e5cd2e6fa9c178db9af8954f4b0a9d52d21997ec229abd -size 57778 +oid sha256:4edf84af27535e4a30b48906ab3cacaeb38d073290828df3c5707620e222b4d3 +size 58635 diff --git a/examples/screenshots/line_collection_slicing.png b/examples/screenshots/line_collection_slicing.png index abb63760f..e0537a261 100644 --- a/examples/screenshots/line_collection_slicing.png +++ b/examples/screenshots/line_collection_slicing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed0d4fdb729409d07ec9ec9e05d915a04ebb237087d266591e7f46b0838e05b3 -size 130192 +oid sha256:66933c1fa349ebb4dd69b9bf396acb8f0aeeabbf17a3b7054d1f1e038a6e04be +size 129484 diff --git a/examples/screenshots/line_colorslice.png b/examples/screenshots/line_colorslice.png index 1f100d89e..f3374e221 100644 --- a/examples/screenshots/line_colorslice.png +++ b/examples/screenshots/line_colorslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b2c5562f4150ec69029a4a139469b0a2524a14078b78055df40d9b487946ce5 -size 57037 +oid sha256:d654aa666ac1f4cfbf228fc4c5fbd2f68eed841c7cc6265637d5b836b918314c +size 57989 diff --git a/examples/screenshots/line_dataslice.png b/examples/screenshots/line_dataslice.png index b2f963195..6ecf63b26 100644 --- a/examples/screenshots/line_dataslice.png +++ b/examples/screenshots/line_dataslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c31a12afa3e66c442e370e6157ad9a5aad225b21f0f95fb6a115066b1b4f2e73 -size 68811 +oid sha256:a9b93af2028eb0186dd75d74c079d5effdb284a8677e6eec1a7fd2c8de4c8498 +size 70489 diff --git a/examples/screenshots/line_stack.png b/examples/screenshots/line_stack.png index 786f434be..9a9ad4fd6 100644 --- a/examples/screenshots/line_stack.png +++ b/examples/screenshots/line_stack.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fcfa7c49d465ff9cfe472ee885bcc9d9a44106b82adfc151544847b95035d760 -size 121640 +oid sha256:4b6c2d1ee4c49ff5b193b5105b2794c6b5bd7a089a8a2c6fa03e09e02352aa65 +size 121462 diff --git a/examples/screenshots/linear_region_selectors_match_offsets.png b/examples/screenshots/linear_region_selectors_match_offsets.png index 9d2371403..327f14e72 100644 --- a/examples/screenshots/linear_region_selectors_match_offsets.png +++ b/examples/screenshots/linear_region_selectors_match_offsets.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f12310c09c4e84ea2c6f8245d1aa0ce9389a3d9637d7d4f9dc233bea173a0e3 -size 95366 +oid sha256:8fac4f439b34a5464792588b77856f08c127c0ee06fa77722818f8d6b48dd64c +size 95433 diff --git a/examples/screenshots/no-imgui-extent_frac_layout.png b/examples/screenshots/no-imgui-extent_frac_layout.png new file mode 100644 index 000000000..4dc3b2aa6 --- /dev/null +++ b/examples/screenshots/no-imgui-extent_frac_layout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5923e8b9f687f97d488b282b35f16234898ed1038b0737b7b57fb9cbd72ebf34 +size 157321 diff --git a/examples/screenshots/no-imgui-extent_layout.png b/examples/screenshots/no-imgui-extent_layout.png new file mode 100644 index 000000000..16d1ff446 --- /dev/null +++ b/examples/screenshots/no-imgui-extent_layout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2ffe0a8d625322cc22d2abdde80a3f179f01552dde974bbbd49f9e371ab39aa +size 138936 diff --git a/examples/screenshots/no-imgui-gridplot.png b/examples/screenshots/no-imgui-gridplot.png index 45571161d..7f870cf76 100644 --- a/examples/screenshots/no-imgui-gridplot.png +++ b/examples/screenshots/no-imgui-gridplot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a27ccf2230628980d16ab22a17df64504268da35a27cd1adb44102e64df033af -size 329247 +oid sha256:b31f2002053b5934ae78393214e67717d10bd567e590212eaff4062440657acd +size 292558 diff --git a/examples/screenshots/no-imgui-gridplot_non_square.png b/examples/screenshots/no-imgui-gridplot_non_square.png index f8c307c22..e08d64805 100644 --- a/examples/screenshots/no-imgui-gridplot_non_square.png +++ b/examples/screenshots/no-imgui-gridplot_non_square.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58f50c4fc1b00c9e78c840193d1e15d008b9fe1e7f2a3d8b90065be91e2178f5 -size 236474 +oid sha256:c9ef00db82a3559b4d7c77b68838f5876f98a2b9e80ef9ecb257f32c62161b5e +size 216512 diff --git a/examples/screenshots/no-imgui-gridplot_viewports_check.png b/examples/screenshots/no-imgui-gridplot_viewports_check.png new file mode 100644 index 000000000..2a8c0dc6f --- /dev/null +++ b/examples/screenshots/no-imgui-gridplot_viewports_check.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6818a7c8bdb29567bb09cfe00acaa6872a046d4d35a87ef2be7afa06c2a8a089 +size 44869 diff --git a/examples/screenshots/no-imgui-heatmap.png b/examples/screenshots/no-imgui-heatmap.png index 3d1cf5ef2..e91d06c4f 100644 --- a/examples/screenshots/no-imgui-heatmap.png +++ b/examples/screenshots/no-imgui-heatmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fac55efd9339b180b9e34d5cf244c473d6439e57e34f272c1a7e59183f1afa2 -size 98573 +oid sha256:875c15e74e7ea2eaa6b00ddbdd80b4775ecb1fe0002a5122371d49f975369cce +size 95553 diff --git a/examples/screenshots/no-imgui-image_cmap.png b/examples/screenshots/no-imgui-image_cmap.png index 6c565ca2b..2d42899fc 100644 --- a/examples/screenshots/no-imgui-image_cmap.png +++ b/examples/screenshots/no-imgui-image_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:82f7176a61e2c6953c22171bea561845bb79cb8179d76b20eef2b2cc475bbb23 -size 237327 +oid sha256:2b43bd64ceec8c5c1287a2df57abf7bd148955d6ba97a425b32ae53bad03a051 +size 216050 diff --git a/examples/screenshots/no-imgui-image_rgb.png b/examples/screenshots/no-imgui-image_rgb.png index 355238724..6be5205ac 100644 --- a/examples/screenshots/no-imgui-image_rgb.png +++ b/examples/screenshots/no-imgui-image_rgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fce532d713d2c664eb3b676e0128060ebf17241387134812b490d3ad398d42c2 -size 269508 +oid sha256:42516cd0719d5b33ec32523dd2efe7874398bac6d0aecb5163ff1cb5c105135f +size 244717 diff --git a/examples/screenshots/no-imgui-image_rgbvminvmax.png b/examples/screenshots/no-imgui-image_rgbvminvmax.png index 6282f2438..48d8fff95 100644 --- a/examples/screenshots/no-imgui-image_rgbvminvmax.png +++ b/examples/screenshots/no-imgui-image_rgbvminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42e01469f0f7da37d3c1c90225bf7c03c44badd1f3612ac9bf88eaed5eeb6850 -size 50145 +oid sha256:7f8a99a9172ae5edf98f0d189455fad2074a99f2280c9352675bab8d4c0e3491 +size 50751 diff --git a/examples/screenshots/no-imgui-image_simple.png b/examples/screenshots/no-imgui-image_simple.png index d00a166ce..1e4487757 100644 --- a/examples/screenshots/no-imgui-image_simple.png +++ b/examples/screenshots/no-imgui-image_simple.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8bb29f192617b9dde2490ce36c69bd8352b6ba5d068434bc53edaad91871356 -size 237960 +oid sha256:3cfa6469803f44a682c9ce7337ae265a8d60749070991e6f3a723eb37c5a9a23 +size 215410 diff --git a/examples/screenshots/no-imgui-image_small.png b/examples/screenshots/no-imgui-image_small.png index aca14cd69..3613a8139 100644 --- a/examples/screenshots/no-imgui-image_small.png +++ b/examples/screenshots/no-imgui-image_small.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1ea4bcf76158169bc06973457ea09997c13ecd4a91e6e634566beb31348ef68 -size 13194 +oid sha256:17ccf0014c7ba7054440e3daf8d4e2a397e9013d1aea804c40dc7302dad4171e +size 13327 diff --git a/examples/screenshots/no-imgui-image_vminvmax.png b/examples/screenshots/no-imgui-image_vminvmax.png index 6282f2438..48d8fff95 100644 --- a/examples/screenshots/no-imgui-image_vminvmax.png +++ b/examples/screenshots/no-imgui-image_vminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42e01469f0f7da37d3c1c90225bf7c03c44badd1f3612ac9bf88eaed5eeb6850 -size 50145 +oid sha256:7f8a99a9172ae5edf98f0d189455fad2074a99f2280c9352675bab8d4c0e3491 +size 50751 diff --git a/examples/screenshots/no-imgui-line.png b/examples/screenshots/no-imgui-line.png index 29610c612..cdc24e382 100644 --- a/examples/screenshots/no-imgui-line.png +++ b/examples/screenshots/no-imgui-line.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:709458b03d535bcf407fdae1720ccdcd11a5f79ccf673e85c7e64c5748f6d25e -size 173422 +oid sha256:d3952cf9b0c9d008a885dc4abb3aeaaed6fd94a5db05ba83c6f4c4c76fe6e925 +size 171519 diff --git a/examples/screenshots/no-imgui-line_cmap.png b/examples/screenshots/no-imgui-line_cmap.png index 9340e191e..4f2bbba43 100644 --- a/examples/screenshots/no-imgui-line_cmap.png +++ b/examples/screenshots/no-imgui-line_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69426f5aac61e59a08764626b2aded602e576479e652d76b6b3bf646e3218cc1 -size 48028 +oid sha256:d3c9ac8d2b8157ffd575e5ad2b2bb23b684b52403c2f4f021c52d100cfb28a83 +size 49048 diff --git a/examples/screenshots/no-imgui-line_cmap_more.png b/examples/screenshots/no-imgui-line_cmap_more.png index f0cea4ec1..8125be49f 100644 --- a/examples/screenshots/no-imgui-line_cmap_more.png +++ b/examples/screenshots/no-imgui-line_cmap_more.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df9a2ef9d54b417e0387116eb6e6215c54b7c939867d0d62c768768baae27e5f -size 129510 +oid sha256:5ddd88200aa824d4e05ba3f94fdb4216a1e7c7137b202cd8fb47997453dfd5a6 +size 126830 diff --git a/examples/screenshots/no-imgui-line_collection.png b/examples/screenshots/no-imgui-line_collection.png index ca74d3362..a31cf55fe 100644 --- a/examples/screenshots/no-imgui-line_collection.png +++ b/examples/screenshots/no-imgui-line_collection.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90f281301e8b23a22a5333e7b34316475907ac25ffc9a23b7395b7431c965343 -size 106518 +oid sha256:7d807f770c118e668c6bda1919856d7804f716a2bf95a5ae060345df1cd2b3c7 +size 102703 diff --git a/examples/screenshots/no-imgui-line_collection_cmap_values.png b/examples/screenshots/no-imgui-line_collection_cmap_values.png index df237aa1b..c909c766f 100644 --- a/examples/screenshots/no-imgui-line_collection_cmap_values.png +++ b/examples/screenshots/no-imgui-line_collection_cmap_values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f5a7257d121a15a8a35ca6e9c70de9d6fbb4977221c840dd34e25e67136f4ea -size 67209 +oid sha256:2e8612de5c3ee252ce9c8cc8afd5bd6075d5e242e8a93cd025e28ec82526120f +size 64698 diff --git a/examples/screenshots/no-imgui-line_collection_cmap_values_qualitative.png b/examples/screenshots/no-imgui-line_collection_cmap_values_qualitative.png index 0347f7361..61d5a21d0 100644 --- a/examples/screenshots/no-imgui-line_collection_cmap_values_qualitative.png +++ b/examples/screenshots/no-imgui-line_collection_cmap_values_qualitative.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89a7bc62495e6454ee008e15f1504211777cc01e52f303c18f6068fd38ab3c12 -size 70090 +oid sha256:7847cd4399ce5b43bda9985eb72467ad292744aaeb9e8d210dd6c86c4eb1a090 +size 67959 diff --git a/examples/screenshots/no-imgui-line_collection_colors.png b/examples/screenshots/no-imgui-line_collection_colors.png index dff4f83db..567bb4d06 100644 --- a/examples/screenshots/no-imgui-line_collection_colors.png +++ b/examples/screenshots/no-imgui-line_collection_colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78b14e90e5ae1e185abb51d94ac9d99c1a4318b0ddf79c26a55e6061f22c0ed9 -size 60447 +oid sha256:15216a0900bcaef492e5d9e3380db9f28d7b7e4bd11b26eb87ce956666dcd2b1 +size 58414 diff --git a/examples/screenshots/no-imgui-line_collection_slicing.png b/examples/screenshots/no-imgui-line_collection_slicing.png index 70c343361..c9bc6d931 100644 --- a/examples/screenshots/no-imgui-line_collection_slicing.png +++ b/examples/screenshots/no-imgui-line_collection_slicing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d6b4090d3ae9e38256c9f04e17bf2499f0a35348552f62e9c8d8dc97c9e760a7 -size 132125 +oid sha256:e8d3d7813580be188766c2d0200bcbff28122758d36d0faa846b0bb4dceac654 +size 130453 diff --git a/examples/screenshots/no-imgui-line_colorslice.png b/examples/screenshots/no-imgui-line_colorslice.png index 3befac6da..fe54de5d6 100644 --- a/examples/screenshots/no-imgui-line_colorslice.png +++ b/examples/screenshots/no-imgui-line_colorslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f161ad7f351b56c988e1b27155e3963be5191dc09cbaa55615026d07df07334 -size 56338 +oid sha256:be429bf910979cf4c9483b8ae1f7aa877fde64fb6ec8a4cf32be143f282c9103 +size 57353 diff --git a/examples/screenshots/no-imgui-line_dataslice.png b/examples/screenshots/no-imgui-line_dataslice.png index 957462d09..649a9df59 100644 --- a/examples/screenshots/no-imgui-line_dataslice.png +++ b/examples/screenshots/no-imgui-line_dataslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2f737e0afd8f57c7d621197d37fcf30199086f6c083ec0d3d8e5497965e6d12 -size 67938 +oid sha256:cf873f1479cec065f0062ce58ce78ddfbd5673654aacf0ecdbd559747ae741cb +size 69381 diff --git a/examples/screenshots/no-imgui-line_stack.png b/examples/screenshots/no-imgui-line_stack.png index 26f4a3af8..3ef24e73a 100644 --- a/examples/screenshots/no-imgui-line_stack.png +++ b/examples/screenshots/no-imgui-line_stack.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4dd69dc4be7a2283ec11a8427a75a2ddfe4be0cdbbdaedef3dcbf5f567c11ea7 -size 130519 +oid sha256:4b9d02719e7051c2a0e848cc828f21be52ac108c6f9be16795d1150a1e215371 +size 123674 diff --git a/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png b/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png index 9871d65c1..809908432 100644 --- a/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png +++ b/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:747b0915eeaf5985346e3b6807a550da53b516769d2517d7c2e0f189baefef91 -size 100604 +oid sha256:303d562f1a16f6a704415072d43ca08a51e12a702292b522e0f17f397b1aee60 +size 96668 diff --git a/examples/screenshots/no-imgui-rect_frac_layout.png b/examples/screenshots/no-imgui-rect_frac_layout.png new file mode 100644 index 000000000..4dc3b2aa6 --- /dev/null +++ b/examples/screenshots/no-imgui-rect_frac_layout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5923e8b9f687f97d488b282b35f16234898ed1038b0737b7b57fb9cbd72ebf34 +size 157321 diff --git a/examples/screenshots/no-imgui-rect_layout.png b/examples/screenshots/no-imgui-rect_layout.png new file mode 100644 index 000000000..16d1ff446 --- /dev/null +++ b/examples/screenshots/no-imgui-rect_layout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2ffe0a8d625322cc22d2abdde80a3f179f01552dde974bbbd49f9e371ab39aa +size 138936 diff --git a/examples/screenshots/no-imgui-scatter_cmap_iris.png b/examples/screenshots/no-imgui-scatter_cmap_iris.png index 35812357a..0d1f8dbb0 100644 --- a/examples/screenshots/no-imgui-scatter_cmap_iris.png +++ b/examples/screenshots/no-imgui-scatter_cmap_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74438dc47ff3fc1391b6952a52c66160fece0545de4ad40c13d3d56b2e093257 -size 59951 +oid sha256:7e197c84911cf7711d09653d6c54d7a756fbe4fe80daa84f0cf1a1d516217423 +size 60341 diff --git a/examples/screenshots/no-imgui-scatter_colorslice_iris.png b/examples/screenshots/no-imgui-scatter_colorslice_iris.png index 61812c8d7..84447c70f 100644 --- a/examples/screenshots/no-imgui-scatter_colorslice_iris.png +++ b/examples/screenshots/no-imgui-scatter_colorslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a02a21459deeca379a69b30054bebcc3739553b9d377d25b953315094e714d1a -size 35763 +oid sha256:780b680de7d3a22d2cb73a6829cad1e1066163e084b8daa9e8362f2543ba62eb +size 36881 diff --git a/examples/screenshots/no-imgui-scatter_dataslice_iris.png b/examples/screenshots/no-imgui-scatter_dataslice_iris.png index 9ef39785c..a19d66270 100644 --- a/examples/screenshots/no-imgui-scatter_dataslice_iris.png +++ b/examples/screenshots/no-imgui-scatter_dataslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21ccf85a9242f6d7a724c38797688abd804d9a565e818b81ea0c8931aa05ca4e -size 38337 +oid sha256:6b4f6635f48e047944c923ac46a9bd5b77e736f26421978ff74cd37a9677c622 +size 39457 diff --git a/examples/screenshots/no-imgui-scatter_iris.png b/examples/screenshots/no-imgui-scatter_iris.png index 91dc29397..631672504 100644 --- a/examples/screenshots/no-imgui-scatter_iris.png +++ b/examples/screenshots/no-imgui-scatter_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ec960574580af159f3502da09f1f34e841267985edb52b89baf034c1d49125e -size 37410 +oid sha256:80cc8c1ed5276b0b8cbd5aeb3151182a73984829f889195b57442a58c3124a43 +size 38488 diff --git a/examples/screenshots/no-imgui-scatter_size.png b/examples/screenshots/no-imgui-scatter_size.png index 6fadfec4d..241e38ad5 100644 --- a/examples/screenshots/no-imgui-scatter_size.png +++ b/examples/screenshots/no-imgui-scatter_size.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94b4b9d39f3d4ef2c46b6b4dd7f712ca612f31a7fc94ab5fad8015e48c637e91 -size 70290 +oid sha256:71f3db93ea28e773c708093319985fb0fe04fae9a8a78d4f4f764f0417979b72 +size 68596 diff --git a/examples/screenshots/rect_frac_layout.png b/examples/screenshots/rect_frac_layout.png new file mode 100644 index 000000000..7fe6d3d37 --- /dev/null +++ b/examples/screenshots/rect_frac_layout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5991b755432318310cfc2b4826bd9639cc234883aa06f1895817f710714cb58f +size 156297 diff --git a/examples/screenshots/rect_layout.png b/examples/screenshots/rect_layout.png new file mode 100644 index 000000000..dec391ac2 --- /dev/null +++ b/examples/screenshots/rect_layout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0cf23f845932023789e0823a105910e9f701d0f03c04e3c18488f0da62420921 +size 123409 diff --git a/examples/screenshots/scatter_cmap_iris.png b/examples/screenshots/scatter_cmap_iris.png index a887d1f99..c069d6b11 100644 --- a/examples/screenshots/scatter_cmap_iris.png +++ b/examples/screenshots/scatter_cmap_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d6bfba80eb737099040eebce9b70e1b261720f26cc895ec4b81ca21af60471c -size 60550 +oid sha256:fad40cf8004e31f7d30f4bb552ee1c7f79a499d3bad310c0eac83396f0aabd62 +size 61193 diff --git a/examples/screenshots/scatter_colorslice_iris.png b/examples/screenshots/scatter_colorslice_iris.png index e260df642..58c2b61fe 100644 --- a/examples/screenshots/scatter_colorslice_iris.png +++ b/examples/screenshots/scatter_colorslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:febd4aa7240eea70b2759337cf98be31cacc1b147859bf628e929ead0153ef9c -size 36791 +oid sha256:427587ef9a73bf9c3ea6e739b61d5af7380a5488c454a9d3653019b40d569292 +size 37589 diff --git a/examples/screenshots/scatter_dataslice_iris.png b/examples/screenshots/scatter_dataslice_iris.png index e5f05bb74..ab61f0405 100644 --- a/examples/screenshots/scatter_dataslice_iris.png +++ b/examples/screenshots/scatter_dataslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cfbc717281c15c6d1d8fe2989770bc9c46f42052c897c2270294ad1b4b40d66 -size 39296 +oid sha256:e3dd9ad854f41386d353ca0dae689a263eff942817727e328690427e2e62e2f3 +size 40112 diff --git a/examples/screenshots/scatter_iris.png b/examples/screenshots/scatter_iris.png index 9c452d448..01bd5cacd 100644 --- a/examples/screenshots/scatter_iris.png +++ b/examples/screenshots/scatter_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:98eab41312eb42cbffdf8add0651b55e63b5c2fb5f4523e32dc51ed28a1be369 -size 38452 +oid sha256:c7978b93f7eac8176c54ed0e39178424d9cb6474c73e9013d5164d3e88d54c95 +size 39147 diff --git a/examples/screenshots/scatter_size.png b/examples/screenshots/scatter_size.png index f2f036ea4..2f6c045f3 100644 --- a/examples/screenshots/scatter_size.png +++ b/examples/screenshots/scatter_size.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3522468f99c030cb27c225f009ecb4c7aafbd97cfc743cf1d07fb8d7ff8e0d4 -size 71336 +oid sha256:eb05b8378d94e16094738850dca6328caf7477c641bf474b9deae426344bc7a4 +size 70898 diff --git a/examples/selection_tools/unit_circle.py b/examples/selection_tools/unit_circle.py new file mode 100644 index 000000000..2850b1bc1 --- /dev/null +++ b/examples/selection_tools/unit_circle.py @@ -0,0 +1,142 @@ +""" +Unit circle +=========== + +Example with linear selectors on a sine and cosine function that demonstrates the unit circle. + +This shows how fastplotlib supports bidirectional events, drag the linear selector on the sine +or cosine function and they will both move together. + +Click on the sine or cosine function to set the colormap transform to illustrate the sine or +cosine function output values on the unit circle. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + + +import numpy as np +import fastplotlib as fpl + + +# helper function to make a cirlce +def make_circle(center, radius: float, n_points: int) -> np.ndarray: + theta = np.linspace(0, 2 * np.pi, n_points) + xs = radius * np.cos(theta) + ys = radius * np.sin(theta) + + return np.column_stack([xs, ys]) + center + + +# We will have 3 subplots in a layout like this: +""" +|========|========| +| | | +| | sine | +| | | +| circle |========| +| | | +| | cosine | +| | | +|========|========| +""" + +# we can define this layout using "extents", i.e. min and max ranges on the canvas +# (x_min, x_max, y_min, y_max) +# extents can be defined as fractions as shown here +extents = [ + (0, 0.5, 0, 1), # circle subplot + (0.5, 1, 0, 0.5), # sine subplot + (0.5, 1, 0.5, 1), # cosine subplot +] + +# create a figure with 3 subplots +figure = fpl.Figure( + extents=extents, + names=["unit circle", "sin(x)", "cos(x)"], + size=(700, 560) +) + +# set the axes to intersect at (0, 0, 0) to better illustrate the unit circle +for subplot in figure: + subplot.axes.intersection = (0, 0, 0) + subplot.toolbar = False # reduce clutter + +figure["sin(x)"].camera.maintain_aspect = False +figure["cos(x)"].camera.maintain_aspect = False + +# create sine and cosine data +xs = np.linspace(0, 2 * np.pi, 360) +sine = np.sin(xs) +cosine = np.cos(xs) + +# circle data +circle_data = make_circle(center=(0, 0), radius=1, n_points=360) + +# make the circle line graphic, set the cmap transform using the sine function +circle_graphic = figure["unit circle"].add_line( + circle_data, thickness=4, cmap="bwr", cmap_transform=sine +) + +# line to show the circle radius +# use it to indicate the current position of the sine and cosine selctors (below) +radius_data = np.array([[0, 0, 0], [*circle_data[0], 0]]) +circle_radius = figure["unit circle"].add_line( + radius_data, thickness=6, colors="magenta" +) + +# sine line graphic, cmap transform set from the sine function +sine_graphic = figure["sin(x)"].add_line( + sine, thickness=10, cmap="bwr", cmap_transform=sine +) + +# cosine line graphic, cmap transform set from the sine function +# illustrates the sine function values on the cosine graphic +cosine_graphic = figure["cos(x)"].add_line( + cosine, thickness=10, cmap="bwr", cmap_transform=sine +) + +# add linear selectors to the sine and cosine line graphics +sine_selector = sine_graphic.add_linear_selector() +cosine_selector = cosine_graphic.add_linear_selector() + + +def set_circle_cmap(ev): + # sets the cmap transforms + + cmap_transform = ev.graphic.data[:, 1] # y-val data of the sine or cosine graphic + for g in [sine_graphic, cosine_graphic]: + g.cmap.transform = cmap_transform + + # set circle cmap transform + circle_graphic.cmap.transform = cmap_transform + +# when the sine or cosine graphic is clicked, the cmap_transform +# of the sine, cosine and circle line graphics are all set from +# the y-values of the clicked line +sine_graphic.add_event_handler(set_circle_cmap, "click") +cosine_graphic.add_event_handler(set_circle_cmap, "click") + + +def set_x_val(ev): + # used to sync the two selectors + value = ev.info["value"] + index = ev.get_selected_index() + + sine_selector.selection = value + cosine_selector.selection = value + + circle_radius.data[1, :-1] = circle_data[index] + +# add same event handler to both graphics +sine_selector.add_event_handler(set_x_val, "selection") +cosine_selector.add_event_handler(set_x_val, "selection") + +figure.show() + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index 67519187b..7fbd32e2f 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -58,11 +58,11 @@ def test_examples_run(module, force_offscreen): @pytest.fixture def force_offscreen(): """Force the offscreen canvas to be selected by the auto gui module.""" - os.environ["WGPU_FORCE_OFFSCREEN"] = "true" + os.environ["RENDERCANVAS_FORCE_OFFSCREEN"] = "true" try: yield finally: - del os.environ["WGPU_FORCE_OFFSCREEN"] + del os.environ["RENDERCANVAS_FORCE_OFFSCREEN"] def test_that_we_are_on_lavapipe(): @@ -103,11 +103,10 @@ def test_example_screenshots(module, force_offscreen): # hacky but it works for now example.figure.imgui_renderer.render() + example.figure._fpl_reset_layout() # render each subplot for subplot in example.figure: subplot.viewport.render(subplot.scene, subplot.camera) - for dock in subplot.docks.values(): - dock.set_viewport_rect() # flush pygfx renderer example.figure.renderer.flush() diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index f72a87123..d6fce52fe 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -24,6 +24,7 @@ "line/*.py", "line_collection/*.py", "gridplot/*.py", + "window_layouts/*.py", "misc/*.py", "selection_tools/*.py", "guis/*.py", diff --git a/examples/window_layouts/README.rst b/examples/window_layouts/README.rst new file mode 100644 index 000000000..23684627b --- /dev/null +++ b/examples/window_layouts/README.rst @@ -0,0 +1,2 @@ +Window Layout Examples +====================== diff --git a/examples/window_layouts/extent_frac_layout.py b/examples/window_layouts/extent_frac_layout.py new file mode 100644 index 000000000..0c5293e09 --- /dev/null +++ b/examples/window_layouts/extent_frac_layout.py @@ -0,0 +1,74 @@ +""" +Fractional Extent Layout +======================== + +Create subplots using extents given as fractions of the canvas. +This example plots two images and their histograms in separate subplots + +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import imageio.v3 as iio +import fastplotlib as fpl + +# load images +img1 = iio.imread("imageio:astronaut.png") +img2 = iio.imread("imageio:wikkie.png") + +# calculate histograms +hist_1, edges_1 = np.histogram(img1) +centers_1 = edges_1[:-1] + np.diff(edges_1) / 2 + +hist_2, edges_2 = np.histogram(img2) +centers_2 = edges_2[:-1] + np.diff(edges_2) / 2 + +# figure size in pixels +size = (700, 560) + +# extent is (xmin, xmax, ymin, ymax) +# here it is defined as fractions of the canvas +extents = [ + (0, 0.3, 0, 0.5), # for image1 + (0, 0.3, 0.5, 1), # for image2 + (0.3, 1, 0, 0.5), # for image1 histogram + (0.3, 1, 0.5, 1), # for image2 histogram +] + +# create a figure using the rects and size +# also give each subplot a name +figure = fpl.Figure( + extents=extents, + names=["astronaut image", "wikkie image", "astronaut histogram", "wikkie histogram"], + size=size +) + +# add image to the corresponding subplots +figure["astronaut image"].add_image(img1) +figure["wikkie image"].add_image(img2) + +# add histogram to the corresponding subplots +figure["astronaut histogram"].add_line(np.column_stack([centers_1, hist_1])) +figure["wikkie histogram"].add_line(np.column_stack([centers_2, hist_2])) + + +for subplot in figure: + if "image" in subplot.name: + # remove axes from image subplots to reduce clutter + subplot.axes.visible = False + continue + + # don't maintain aspect ratio for the histogram subplots + subplot.camera.maintain_aspect = False + + +figure.show() + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/window_layouts/extent_layout.py b/examples/window_layouts/extent_layout.py new file mode 100644 index 000000000..e6facaaa2 --- /dev/null +++ b/examples/window_layouts/extent_layout.py @@ -0,0 +1,74 @@ +""" +Extent Layout +============= + +Create subplots using given extents in absolute pixels. +This example plots two images and their histograms in separate subplots + +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import imageio.v3 as iio +import fastplotlib as fpl + +# load images +img1 = iio.imread("imageio:astronaut.png") +img2 = iio.imread("imageio:wikkie.png") + +# calculate histograms +hist_1, edges_1 = np.histogram(img1) +centers_1 = edges_1[:-1] + np.diff(edges_1) / 2 + +hist_2, edges_2 = np.histogram(img2) +centers_2 = edges_2[:-1] + np.diff(edges_2) / 2 + +# figure size in pixels +size = (640, 480) + +# extent is (xmin, xmax, ymin, ymax) +# here it is defined in absolute pixels +extents = [ + (0, 200, 0, 240), # for image1 + (0, 200, 240, 480), # for image2 + (200, 640, 0, 240), # for image1 histogram + (200, 640, 240, 480), # for image2 histogram +] + +# create a figure using the rects and size +# also give each subplot a name +figure = fpl.Figure( + extents=extents, + names=["astronaut image", "wikkie image", "astronaut histogram", "wikkie histogram"], + size=size +) + +# add image to the corresponding subplots +figure["astronaut image"].add_image(img1) +figure["wikkie image"].add_image(img2) + +# add histogram to the corresponding subplots +figure["astronaut histogram"].add_line(np.column_stack([centers_1, hist_1])) +figure["wikkie histogram"].add_line(np.column_stack([centers_2, hist_2])) + + +for subplot in figure: + if "image" in subplot.name: + # remove axes from image subplots to reduce clutter + subplot.axes.visible = False + continue + + # don't maintain aspect ratio for the histogram subplots + subplot.camera.maintain_aspect = False + + +figure.show() + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/window_layouts/rect_frac_layout.py b/examples/window_layouts/rect_frac_layout.py new file mode 100644 index 000000000..072fa1107 --- /dev/null +++ b/examples/window_layouts/rect_frac_layout.py @@ -0,0 +1,74 @@ +""" +Rect Fractional Layout +====================== + +Create subplots using rects given as fractions of the canvas. +This example plots two images and their histograms in separate subplots + +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import imageio.v3 as iio +import fastplotlib as fpl + +# load images +img1 = iio.imread("imageio:astronaut.png") +img2 = iio.imread("imageio:wikkie.png") + +# calculate histograms +hist_1, edges_1 = np.histogram(img1) +centers_1 = edges_1[:-1] + np.diff(edges_1) / 2 + +hist_2, edges_2 = np.histogram(img2) +centers_2 = edges_2[:-1] + np.diff(edges_2) / 2 + +# figure size in pixels +size = (700, 560) + +# rect is (x, y, width, height) +# here it is defined as fractions of the canvas +rects = [ + (0, 0, 0.3, 0.5), # for image1 + (0, 0.5, 0.3, 0.5), # for image2 + (0.3, 0, 0.7, 0.5), # for image1 histogram + (0.3, 0.5, 0.7, 0.5), # for image2 histogram +] + +# create a figure using the rects and size +# also give each subplot a name +figure = fpl.Figure( + rects=rects, + names=["astronaut image", "wikkie image", "astronaut histogram", "wikkie histogram"], + size=size +) + +# add image to the corresponding subplots +figure["astronaut image"].add_image(img1) +figure["wikkie image"].add_image(img2) + +# add histogram to the corresponding subplots +figure["astronaut histogram"].add_line(np.column_stack([centers_1, hist_1])) +figure["wikkie histogram"].add_line(np.column_stack([centers_2, hist_2])) + + +for subplot in figure: + if "image" in subplot.name: + # remove axes from image subplots to reduce clutter + subplot.axes.visible = False + continue + + # don't maintain aspect ratio for the histogram subplots + subplot.camera.maintain_aspect = False + + +figure.show() + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/window_layouts/rect_layout.py b/examples/window_layouts/rect_layout.py new file mode 100644 index 000000000..962b8a4f1 --- /dev/null +++ b/examples/window_layouts/rect_layout.py @@ -0,0 +1,74 @@ +""" +Rect Layout +=========== + +Create subplots using given rects in absolute pixels. +This example plots two images and their histograms in separate subplots + +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import imageio.v3 as iio +import fastplotlib as fpl + +# load images +img1 = iio.imread("imageio:astronaut.png") +img2 = iio.imread("imageio:wikkie.png") + +# calculate histograms +hist_1, edges_1 = np.histogram(img1) +centers_1 = edges_1[:-1] + np.diff(edges_1) / 2 + +hist_2, edges_2 = np.histogram(img2) +centers_2 = edges_2[:-1] + np.diff(edges_2) / 2 + +# figure size in pixels +size = (640, 480) + +# a rect is (x, y, width, height) +# here it is defined in absolute pixels +rects = [ + (0, 0, 200, 240), # for image1 + (0, 240, 200, 240), # for image2 + (200, 0, 440, 240), # for image1 histogram + (200, 240, 440, 240), # for image2 histogram +] + +# create a figure using the rects and size +# also give each subplot a name +figure = fpl.Figure( + rects=rects, + names=["astronaut image", "wikkie image", "astronaut histogram", "wikkie histogram"], + size=size +) + +# add image to the corresponding subplots +figure["astronaut image"].add_image(img1) +figure["wikkie image"].add_image(img2) + +# add histogram to the corresponding subplots +figure["astronaut histogram"].add_line(np.column_stack([centers_1, hist_1])) +figure["wikkie histogram"].add_line(np.column_stack([centers_2, hist_2])) + + +for subplot in figure: + if "image" in subplot.name: + # remove axes from image subplots to reduce clutter + subplot.axes.visible = False + continue + + # don't maintain aspect ratio for the histogram subplots + subplot.camera.maintain_aspect = False + + +figure.show() + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/fastplotlib/VERSION b/fastplotlib/VERSION index 0d91a54c7..1d0ba9ea1 100644 --- a/fastplotlib/VERSION +++ b/fastplotlib/VERSION @@ -1 +1 @@ -0.3.0 +0.4.0 diff --git a/fastplotlib/graphics/_axes.py b/fastplotlib/graphics/_axes.py index 9541dceeb..10774fc2a 100644 --- a/fastplotlib/graphics/_axes.py +++ b/fastplotlib/graphics/_axes.py @@ -141,108 +141,6 @@ def yz(self) -> Grid: return self._yz -class Ruler(pygfx.Ruler): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.tick_text_mapper = None - self.font_size = 14 - - def _update_sub_objects(self, ticks, tick_auto_step): - """Update the sub-objects to show the given ticks.""" - assert isinstance(ticks, dict) - - tick_size = 5 - min_n_slots = 8 # todo: can be (much) higher when we use a single text object! - - # Load config - start_pos = self._start_pos - end_pos = self._end_pos - start_value = self._start_value - end_value = self.end_value - - # Derive some more variables - length = end_value - start_value - vec = end_pos - start_pos - if length: - vec /= length - - # Get array to store positions - n_slots = self.points.geometry.positions.nitems - n_positions = len(ticks) + 2 - if n_positions <= n_slots <= max(min_n_slots, 2 * n_positions): - # Re-use existing buffers - positions = self.points.geometry.positions.data - sizes = self.points.geometry.sizes.data - self.points.geometry.positions.update_range() - self.points.geometry.sizes.update_range() - else: - # Allocate new buffers - new_n_slots = max(min_n_slots, int(n_positions * 1.2)) - positions = np.zeros((new_n_slots, 3), np.float32) - sizes = np.zeros((new_n_slots,), np.float32) - self.points.geometry.positions = pygfx.Buffer(positions) - self.points.geometry.sizes = pygfx.Buffer(sizes) - # Allocate text objects - while len(self._text_object_pool) < new_n_slots: - ob = pygfx.Text( - pygfx.TextGeometry("", screen_space=True, font_size=self.font_size), - pygfx.TextMaterial(aa=False), - ) - self._text_object_pool.append(ob) - self._text_object_pool[new_n_slots:] = [] - # Reset children - self.clear() - self.add(self._line, self._points, *self._text_object_pool) - - def define_text(pos, text): - if self.tick_text_mapper is not None and text != "": - text = self.tick_text_mapper(text) - - ob = self._text_object_pool[index] - ob.geometry.anchor = self._text_anchor - ob.geometry.anchor_offset = self._text_anchor_offset - ob.geometry.set_text(text) - ob.local.position = pos - - # Apply start point - index = 0 - positions[0] = start_pos - if self._ticks_at_end_points: - sizes[0] = tick_size - define_text(start_pos, f"{self._start_value:0.4g}") - else: - sizes[0] = 0 - define_text(start_pos, f"") - - # Collect ticks - index += 1 - for value, text in ticks.items(): - pos = start_pos + vec * (value - start_value) - positions[index] = pos - sizes[index] = tick_size - define_text(pos, text) - index += 1 - - # Handle end point, and nullify remaining slots - positions[index:] = end_pos - sizes[index:] = 0 - for ob in self._text_object_pool[index:]: - ob.geometry.set_text("") - - # Show last tick? - if self._ticks_at_end_points: - sizes[index] = tick_size - define_text(end_pos, f"{end_value:0.4g}") - - # Hide the ticks close to the ends? - if self._ticks_at_end_points and ticks: - tick_values = list(ticks.keys()) - if abs(tick_values[0] - start_value) < 0.5 * tick_auto_step: - self._text_object_pool[1].geometry.set_text("") - if abs(tick_values[-1] - end_value) < 0.5 * tick_auto_step: - self._text_object_pool[index - 1].geometry.set_text("") - - class Axes: def __init__( self, @@ -283,9 +181,9 @@ def __init__( } # create ruler for each dim - self._x = Ruler(**x_kwargs) - self._y = Ruler(**y_kwargs) - self._z = Ruler(**z_kwargs) + self._x = pygfx.Ruler(**x_kwargs) + self._y = pygfx.Ruler(**y_kwargs) + self._z = pygfx.Ruler(**z_kwargs) self._offset = offset @@ -400,17 +298,17 @@ def offset(self, value: np.ndarray): self._offset = value @property - def x(self) -> Ruler: + def x(self) -> pygfx.Ruler: """x axis ruler""" return self._x @property - def y(self) -> Ruler: + def y(self) -> pygfx.Ruler: """y axis ruler""" return self._y @property - def z(self) -> Ruler: + def z(self) -> pygfx.Ruler: """z axis ruler""" return self._z @@ -516,7 +414,7 @@ def update_using_camera(self): return if self._plot_area.camera.fov == 0: - xpos, ypos, width, height = self._plot_area.get_rect() + xpos, ypos, width, height = self._plot_area.viewport.rect # orthographic projection, get ranges using inverse # get range of screen space by getting the corners diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index a25bc7176..61ad291ee 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -365,7 +365,7 @@ def _fpl_add_plot_area_hook(self, plot_area): self._plot_area = plot_area def __repr__(self): - rval = f"{self.__class__.__name__} @ {hex(id(self))}" + rval = f"{self.__class__.__name__}" if self.name is not None: return f"'{self.name}': {rval}" else: diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 1612414a1..1088dc005 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -53,6 +53,9 @@ def __init__(self, **kwargs): self._event_handlers = list() self._block_events = False + # used by @block_reentrance decorator to block re-entrance into set_value functions + self._reentrant_block: bool = False + @property def value(self) -> Any: """Graphic Feature value, must be implemented in subclass""" @@ -316,3 +319,33 @@ def __len__(self): def __repr__(self): return f"{self.__class__.__name__} buffer data:\n" f"{self.value.__repr__()}" + + +def block_reentrance(set_value): + # decorator to block re-entrant set_value methods + # useful when creating complex, circular, bidirectional event graphs + def set_value_wrapper(self: GraphicFeature, graphic_or_key, value): + """ + wraps GraphicFeature.set_value + + self: GraphicFeature instance + + graphic_or_key: graphic, or key if a BufferManager + + value: the value passed to set_value() + """ + # set_value is already in the middle of an execution, block re-entrance + if self._reentrant_block: + return + try: + # block re-execution of set_value until it has *fully* finished executing + self._reentrant_block = True + set_value(self, graphic_or_key, value) + except Exception as exc: + # raise original exception + raise exc # set_value has raised. The line above and the lines 2+ steps below are probably more relevant! + finally: + # set_value has finished executing, now allow future executions + self._reentrant_block = False + + return set_value_wrapper diff --git a/fastplotlib/graphics/_features/_common.py b/fastplotlib/graphics/_features/_common.py index fe32a485f..e9c49a475 100644 --- a/fastplotlib/graphics/_features/_common.py +++ b/fastplotlib/graphics/_features/_common.py @@ -1,6 +1,6 @@ import numpy as np -from ._base import GraphicFeature, FeatureEvent +from ._base import GraphicFeature, FeatureEvent, block_reentrance class Name(GraphicFeature): @@ -14,6 +14,7 @@ def __init__(self, value: str): def value(self) -> str: return self._value + @block_reentrance def set_value(self, graphic, value: str): if not isinstance(value, str): raise TypeError("`Graphic` name must be of type ") @@ -44,6 +45,7 @@ def _validate(self, value): def value(self) -> np.ndarray: return self._value + @block_reentrance def set_value(self, graphic, value: np.ndarray | list | tuple): self._validate(value) @@ -74,6 +76,7 @@ def _validate(self, value): def value(self) -> np.ndarray: return self._value + @block_reentrance def set_value(self, graphic, value: np.ndarray | list | tuple): self._validate(value) @@ -96,6 +99,7 @@ def __init__(self, value: bool): def value(self) -> bool: return self._value + @block_reentrance def set_value(self, graphic, value: bool): graphic.world_object.visible = value self._value = value @@ -117,6 +121,7 @@ def __init__(self, value: bool): def value(self) -> bool: return self._value + @block_reentrance def set_value(self, graphic, value: bool): self._value = value event = FeatureEvent(type="deleted", info={"value": value}) diff --git a/fastplotlib/graphics/_features/_image.py b/fastplotlib/graphics/_features/_image.py index b67bf1cd4..c0e2b28d2 100644 --- a/fastplotlib/graphics/_features/_image.py +++ b/fastplotlib/graphics/_features/_image.py @@ -5,7 +5,7 @@ import numpy as np import pygfx -from ._base import GraphicFeature, FeatureEvent +from ._base import GraphicFeature, FeatureEvent, block_reentrance from ...utils import ( make_colors, @@ -135,6 +135,7 @@ def __next__(self) -> tuple[pygfx.Texture, tuple[int, int], tuple[slice, slice]] def __getitem__(self, item): return self.value[item] + @block_reentrance def __setitem__(self, key, value): self.value[key] = value @@ -159,6 +160,7 @@ def __init__(self, value: float): def value(self) -> float: return self._value + @block_reentrance def set_value(self, graphic, value: float): vmax = graphic._material.clim[1] graphic._material.clim = (value, vmax) @@ -179,6 +181,7 @@ def __init__(self, value: float): def value(self) -> float: return self._value + @block_reentrance def set_value(self, graphic, value: float): vmin = graphic._material.clim[0] graphic._material.clim = (vmin, value) @@ -200,6 +203,7 @@ def __init__(self, value: str): def value(self) -> str: return self._value + @block_reentrance def set_value(self, graphic, value: str): new_colors = make_colors(256, value) graphic._material.map.texture.data[:] = new_colors @@ -226,6 +230,7 @@ def _validate(self, value): def value(self) -> str: return self._value + @block_reentrance def set_value(self, graphic, value: str): self._validate(value) @@ -254,6 +259,7 @@ def _validate(self, value): def value(self) -> str: return self._value + @block_reentrance def set_value(self, graphic, value: str): self._validate(value) diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py index c4e153a31..78e53f545 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -1,4 +1,4 @@ -from typing import Any, List +from typing import Any import numpy as np import pygfx @@ -11,6 +11,7 @@ BufferManager, FeatureEvent, to_gpu_supported_dtype, + block_reentrance, ) from .utils import parse_colors @@ -58,6 +59,7 @@ def __init__( super().__init__(data=data, isolated_buffer=isolated_buffer) + @block_reentrance def __setitem__( self, key: int | slice | np.ndarray[int | bool] | tuple[slice, ...], @@ -155,6 +157,7 @@ def __init__( def value(self) -> pygfx.Color: return self._value + @block_reentrance def set_value(self, graphic, value: str | np.ndarray | tuple | list | pygfx.Color): value = pygfx.Color(value) graphic.world_object.material.color = value @@ -174,6 +177,7 @@ def __init__(self, value: int | float): def value(self) -> float: return self._value + @block_reentrance def set_value(self, graphic, value: float | int): graphic.world_object.material.size = float(value) self._value = value @@ -192,6 +196,7 @@ def __init__(self, value: str): def value(self) -> str: return self._value + @block_reentrance def set_value(self, graphic, value: str): if "Line" in graphic.world_object.material.__class__.__name__: graphic.world_object.material.thickness_space = value @@ -243,6 +248,7 @@ def _fix_data(self, data): return to_gpu_supported_dtype(data) + @block_reentrance def __setitem__( self, key: int | slice | np.ndarray[int | bool] | tuple[slice, ...], @@ -318,6 +324,7 @@ def _fix_sizes( return sizes + @block_reentrance def __setitem__( self, key: int | slice | np.ndarray[int | bool] | list[int | bool], @@ -344,6 +351,7 @@ def __init__(self, value: float): def value(self) -> float: return self._value + @block_reentrance def set_value(self, graphic, value: float): graphic.world_object.material.thickness = value self._value = value @@ -392,6 +400,7 @@ def __init__( # set vertex colors from cmap self._vertex_colors[:] = colors + @block_reentrance def __setitem__(self, key: slice, cmap_name): if not isinstance(key, slice): raise TypeError( diff --git a/fastplotlib/graphics/_features/_selection_features.py b/fastplotlib/graphics/_features/_selection_features.py index c385f820f..c157023b4 100644 --- a/fastplotlib/graphics/_features/_selection_features.py +++ b/fastplotlib/graphics/_features/_selection_features.py @@ -1,9 +1,9 @@ -from typing import Sequence, Tuple +from typing import Sequence import numpy as np from ...utils import mesh_masks -from ._base import GraphicFeature, FeatureEvent +from ._base import GraphicFeature, FeatureEvent, block_reentrance class LinearSelectionFeature(GraphicFeature): @@ -54,6 +54,7 @@ def value(self) -> np.float32: """ return self._value + @block_reentrance def set_value(self, selector, value: float): # clip value between limits value = np.clip(value, self._limits[0], self._limits[1], dtype=np.float32) @@ -117,6 +118,7 @@ def axis(self) -> str: """one of "x" | "y" """ return self._axis + @block_reentrance def set_value(self, selector, value: Sequence[float]): """ Set start, stop range of selector @@ -231,6 +233,7 @@ def value(self) -> np.ndarray[float]: """ return self._value + @block_reentrance def set_value(self, selector, value: Sequence[float]): """ Set the selection of the rectangle selector. diff --git a/fastplotlib/graphics/_features/_text.py b/fastplotlib/graphics/_features/_text.py index baa2734d5..a95fe256c 100644 --- a/fastplotlib/graphics/_features/_text.py +++ b/fastplotlib/graphics/_features/_text.py @@ -2,7 +2,7 @@ import pygfx -from ._base import GraphicFeature, FeatureEvent +from ._base import GraphicFeature, FeatureEvent, block_reentrance class TextData(GraphicFeature): @@ -14,8 +14,9 @@ def __init__(self, value: str): def value(self) -> str: return self._value + @block_reentrance def set_value(self, graphic, value: str): - graphic.world_object.geometry.set_text(value) + graphic.world_object.set_text(value) self._value = value event = FeatureEvent(type="text", info={"value": value}) @@ -31,9 +32,10 @@ def __init__(self, value: float | int): def value(self) -> float | int: return self._value + @block_reentrance def set_value(self, graphic, value: float | int): - graphic.world_object.geometry.font_size = value - self._value = graphic.world_object.geometry.font_size + graphic.world_object.font_size = value + self._value = graphic.world_object.font_size event = FeatureEvent(type="font_size", info={"value": value}) self._call_event_handlers(event) @@ -48,6 +50,7 @@ def __init__(self, value: str | np.ndarray | list[float] | tuple[float]): def value(self) -> pygfx.Color: return self._value + @block_reentrance def set_value(self, graphic, value: str | np.ndarray | list[float] | tuple[float]): value = pygfx.Color(value) graphic.world_object.material.color = value @@ -66,6 +69,7 @@ def __init__(self, value: str | np.ndarray | list[float] | tuple[float]): def value(self) -> pygfx.Color: return self._value + @block_reentrance def set_value(self, graphic, value: str | np.ndarray | list[float] | tuple[float]): value = pygfx.Color(value) graphic.world_object.material.outline_color = value @@ -84,6 +88,7 @@ def __init__(self, value: float): def value(self) -> float: return self._value + @block_reentrance def set_value(self, graphic, value: float): graphic.world_object.material.outline_thickness = value self._value = graphic.world_object.material.outline_thickness diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 8fe505ba9..489c64930 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -55,7 +55,7 @@ def __init__( if provided, these values are used to map the colors from the cmap size_space: str, default "screen" - coordinate space in which the size is expressed (‘screen’, ‘world’, ‘model’) + coordinate space in which the size is expressed ("screen", "world", "model") **kwargs passed to Graphic diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 8dad7cd43..189af4844 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -62,7 +62,7 @@ def __init__( basically saves GPU VRAM when all scatter points are the same size size_space: str, default "screen" - coordinate space in which the size is expressed (‘screen’, ‘world’, ‘model’) + coordinate space in which the size is expressed ("screen", "world", "model") kwargs passed to Graphic diff --git a/fastplotlib/graphics/text.py b/fastplotlib/graphics/text.py index fcee6129b..e3794743a 100644 --- a/fastplotlib/graphics/text.py +++ b/fastplotlib/graphics/text.py @@ -79,13 +79,11 @@ def __init__( self._outline_thickness = TextOutlineThickness(outline_thickness) world_object = pygfx.Text( - pygfx.TextGeometry( - text=self.text, - font_size=self.font_size, - screen_space=screen_space, - anchor=anchor, - ), - pygfx.TextMaterial( + text=self.text, + font_size=self.font_size, + screen_space=screen_space, + anchor=anchor, + material=pygfx.TextMaterial( color=self.face_color, outline_color=self.outline_color, outline_thickness=self.outline_thickness, @@ -97,6 +95,11 @@ def __init__( self.offset = offset + @property + def world_object(self) -> pygfx.Text: + """Text world object""" + return super(TextGraphic, self).world_object + @property def text(self) -> str: """the text displayed""" diff --git a/fastplotlib/layouts/__init__.py b/fastplotlib/layouts/__init__.py index 4a4f45174..8fb1d54d8 100644 --- a/fastplotlib/layouts/__init__.py +++ b/fastplotlib/layouts/__init__.py @@ -1,11 +1,5 @@ from ._figure import Figure - -try: - import imgui_bundle -except ImportError: - IMGUI = False -else: - IMGUI = True +from ._utils import IMGUI if IMGUI: from ._imgui_figure import ImguiFigure diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py new file mode 100644 index 000000000..877a7fbab --- /dev/null +++ b/fastplotlib/layouts/_engine.py @@ -0,0 +1,390 @@ +from functools import partial + +import numpy as np +import pygfx + +from ._subplot import Subplot +from ._rect import RectManager + + +class UnderlayCamera(pygfx.Camera): + """ + Same as pygfx.ScreenCoordsCamera but y-axis is inverted. + + So top left corner is (0, 0). This is easier to manage because we + often resize using the bottom right corner. + + """ + + def _update_projection_matrix(self): + width, height = self._view_size + sx, sy, sz = 2 / width, 2 / height, 1 + dx, dy, dz = -1, 1, 0 # pygfx is -1, -1, 0 + m = sx, 0, 0, dx, 0, sy, 0, dy, 0, 0, sz, dz, 0, 0, 0, 1 + proj_matrix = np.array(m, dtype=float).reshape(4, 4) + proj_matrix.flags.writeable = False + return proj_matrix + + +class BaseLayout: + def __init__( + self, + renderer: pygfx.WgpuRenderer, + subplots: np.ndarray[Subplot], + canvas_rect: tuple[float, float], + moveable: bool, + resizeable: bool, + ): + """ + Base layout engine, subclass to create a usable layout engine. + """ + self._renderer = renderer + self._subplots: np.ndarray[Subplot] = subplots.ravel() + self._canvas_rect = canvas_rect + + self._last_pointer_pos: np.ndarray[np.float64, np.float64] = np.array( + [np.nan, np.nan] + ) + + # the current user action, move or resize + self._active_action: str | None = None + # subplot that is currently in action, i.e. currently being moved or resized + self._active_subplot: Subplot | None = None + # subplot that is in focus, i.e. being hovered by the pointer + self._subplot_focus: Subplot | None = None + + for subplot in self._subplots: + # highlight plane when pointer enters it + subplot.frame.plane.add_event_handler( + partial(self._highlight_plane, subplot), "pointer_enter" + ) + + if resizeable: + # highlight/unhighlight resize handler when pointer enters/leaves + subplot.frame.resize_handle.add_event_handler( + partial(self._highlight_resize_handler, subplot), "pointer_enter" + ) + subplot.frame.resize_handle.add_event_handler( + partial(self._unhighlight_resize_handler, subplot), "pointer_leave" + ) + + def _inside_render_rect(self, subplot: Subplot, pos: tuple[int, int]) -> bool: + """whether the pos is within the render area, used for filtering out pointer events""" + rect = subplot.frame.get_render_rect() + + x0, y0 = rect[:2] + + x1 = x0 + rect[2] + y1 = y0 + rect[3] + + if (x0 < pos[0] < x1) and (y0 < pos[1] < y1): + return True + + return False + + def canvas_resized(self, canvas_rect: tuple): + """ + called by figure when canvas is resized + + Parameters + ---------- + canvas_rect: (x, y, w, h) + the rect that pygfx can render to, excludes any areas used by imgui. + + """ + + self._canvas_rect = canvas_rect + for subplot in self._subplots: + subplot.frame.canvas_resized(canvas_rect) + + def _highlight_resize_handler(self, subplot: Subplot, ev): + if self._active_action == "resize": + return + + ev.target.material.color = subplot.frame.resize_handle_color.highlight + + def _unhighlight_resize_handler(self, subplot: Subplot, ev): + if self._active_action == "resize": + return + + ev.target.material.color = subplot.frame.resize_handle_color.idle + + def _highlight_plane(self, subplot: Subplot, ev): + if self._active_action is not None: + return + + # reset color of previous focus + if self._subplot_focus is not None: + self._subplot_focus.frame.plane.material.color = ( + subplot.frame.plane_color.idle + ) + + self._subplot_focus = subplot + ev.target.material.color = subplot.frame.plane_color.highlight + + def __len__(self): + return len(self._subplots) + + +class WindowLayout(BaseLayout): + def __init__( + self, + renderer, + subplots: np.ndarray[Subplot], + canvas_rect: tuple, + moveable=True, + resizeable=True, + ): + """ + Flexible layout engine that allows freely moving and resizing subplots. + Subplots are not allowed to overlap. + + We use a screenspace camera to perform an underlay render pass to draw the + subplot frames, there is no depth rendering so we do not allow overlaps. + + """ + + super().__init__(renderer, subplots, canvas_rect, moveable, resizeable) + + self._last_pointer_pos: np.ndarray[np.float64, np.float64] = np.array( + [np.nan, np.nan] + ) + + for subplot in self._subplots: + if moveable: + # start a move action + subplot.frame.plane.add_event_handler( + partial(self._action_start, subplot, "move"), "pointer_down" + ) + # start a resize action + subplot.frame.resize_handle.add_event_handler( + partial(self._action_start, subplot, "resize"), "pointer_down" + ) + + if moveable or resizeable: + # when pointer moves, do an iteration of move or resize action + self._renderer.add_event_handler(self._action_iter, "pointer_move") + + # end the action when pointer button goes up + self._renderer.add_event_handler(self._action_end, "pointer_up") + + def _new_extent_from_delta(self, delta: tuple[int, int]) -> np.ndarray: + delta_x, delta_y = delta + if self._active_action == "resize": + # subtract only from x1, y1 + new_extent = self._active_subplot.frame.extent - np.asarray( + [0, delta_x, 0, delta_y] + ) + else: + # moving + new_extent = self._active_subplot.frame.extent - np.asarray( + [delta_x, delta_x, delta_y, delta_y] + ) + + x0, x1, y0, y1 = new_extent + w = x1 - x0 + h = y1 - y0 + + # make sure width and height are valid + # min width, height is 50px + if w <= 50: # width > 0 + new_extent[:2] = self._active_subplot.frame.extent[:2] + + if h <= 50: # height > 0 + new_extent[2:] = self._active_subplot.frame.extent[2:] + + # ignore movement if this would cause an overlap + for subplot in self._subplots: + if subplot is self._active_subplot: + continue + + if subplot.frame.rect_manager.overlaps(new_extent): + # we have an overlap, need to ignore one or more deltas + # ignore x + if not subplot.frame.rect_manager.is_left_of( + x0 + ) or not subplot.frame.rect_manager.is_right_of(x1): + new_extent[:2] = self._active_subplot.frame.extent[:2] + + # ignore y + if not subplot.frame.rect_manager.is_above( + y0 + ) or not subplot.frame.rect_manager.is_below(y1): + new_extent[2:] = self._active_subplot.frame.extent[2:] + + # make sure all vals are non-negative + if (new_extent[:2] < 0).any(): + # ignore delta_x + new_extent[:2] = self._active_subplot.frame.extent[:2] + + if (new_extent[2:] < 0).any(): + # ignore delta_y + new_extent[2:] = self._active_subplot.frame.extent[2:] + + # canvas extent + cx0, cy0, cw, ch = self._canvas_rect + + # check if new x-range is beyond canvas x-max + if (new_extent[:2] > cx0 + cw).any(): + new_extent[:2] = self._active_subplot.frame.extent[:2] + + # check if new y-range is beyond canvas y-max + if (new_extent[2:] > cy0 + ch).any(): + new_extent[2:] = self._active_subplot.frame.extent[2:] + + return new_extent + + def _action_start(self, subplot: Subplot, action: str, ev): + if self._inside_render_rect(subplot, pos=(ev.x, ev.y)): + return + + if ev.button == 1: # left mouse button + self._active_action = action + if action == "resize": + subplot.frame.resize_handle.material.color = ( + subplot.frame.resize_handle_color.action + ) + elif action == "move": + subplot.frame.plane.material.color = subplot.frame.plane_color.action + else: + raise ValueError + + self._active_subplot = subplot + self._last_pointer_pos[:] = ev.x, ev.y + + def _action_iter(self, ev): + if self._active_action is None: + return + + delta_x, delta_y = self._last_pointer_pos - (ev.x, ev.y) + new_extent = self._new_extent_from_delta((delta_x, delta_y)) + self._active_subplot.frame.extent = new_extent + self._last_pointer_pos[:] = ev.x, ev.y + + def _action_end(self, ev): + self._active_action = None + if self._active_subplot is not None: + self._active_subplot.frame.resize_handle.material.color = ( + self._active_subplot.frame.resize_handle_color.idle + ) + self._active_subplot.frame.plane.material.color = ( + self._active_subplot.frame.plane_color.idle + ) + self._active_subplot = None + + self._last_pointer_pos[:] = np.nan + + def set_rect(self, subplot: Subplot, rect: tuple | list | np.ndarray): + """ + Set the rect of a Subplot + + Parameters + ---------- + subplot: Subplot + the subplot to set the rect of + + rect: (x, y, w, h) + as absolute pixels or fractional. + If width & height <= 1 the rect is assumed to be fractional. + Conversely, if width & height > 1 the rect is assumed to be in absolute pixels. + width & height must be > 0. Negative values are not allowed. + + """ + + new_rect = RectManager(*rect, self._canvas_rect) + extent = new_rect.extent + # check for overlaps + for s in self._subplots: + if s is subplot: + continue + + if s.frame.rect_manager.overlaps(extent): + raise ValueError(f"Given rect: {rect} overlaps with another subplot.") + + def set_extent(self, subplot: Subplot, extent: tuple | list | np.ndarray): + """ + Set the extent of a Subplot + + Parameters + ---------- + subplot: Subplot + the subplot to set the extent of + + extent: (xmin, xmax, ymin, ymax) + as absolute pixels or fractional. + If xmax & ymax <= 1 the extent is assumed to be fractional. + Conversely, if xmax & ymax > 1 the extent is assumed to be in absolute pixels. + Negative values are not allowed. xmax - xmin & ymax - ymin must be > 0. + + """ + + new_rect = RectManager.from_extent(extent, self._canvas_rect) + extent = new_rect.extent + # check for overlaps + for s in self._subplots: + if s is subplot: + continue + + if s.frame.rect_manager.overlaps(extent): + raise ValueError( + f"Given extent: {extent} overlaps with another subplot." + ) + + +class GridLayout(WindowLayout): + def __init__( + self, + renderer, + subplots: np.ndarray[Subplot], + canvas_rect: tuple[float, float, float, float], + shape: tuple[int, int], + ): + """ + Grid layout engine that auto-sets Frame and Subplot rects such that they maintain + a fixed grid layout. Does not allow freely moving or resizing subplots. + + """ + + super().__init__( + renderer, subplots, canvas_rect, moveable=False, resizeable=False + ) + + # {Subplot: (row_ix, col_ix)}, dict mapping subplots to their row and col index in the grid layout + self._subplot_grid_position: dict[Subplot, tuple[int, int]] + self._shape = shape + + @property + def shape(self) -> tuple[int, int]: + return self._shape + + def set_rect(self, subplot, rect: np.ndarray | list | tuple): + raise NotImplementedError( + "set_rect() not implemented for GridLayout which is an auto layout manager" + ) + + def set_extent(self, subplot, extent: np.ndarray | list | tuple): + raise NotImplementedError( + "set_extent() not implemented for GridLayout which is an auto layout manager" + ) + + def add_row(self): + raise NotImplementedError("Not yet implemented") + + def add_column(self): + raise NotImplementedError("Not yet implemented") + + def remove_row(self): + raise NotImplementedError("Not yet implemented") + + def remove_column(self): + raise NotImplementedError("Not yet implemented") + + def add_subplot(self): + raise NotImplementedError( + "Not implemented for GridLayout which is an auto layout manager" + ) + + def remove_subplot(self, subplot): + raise NotImplementedError( + "Not implemented for GridLayout which is an auto layout manager" + ) diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 70a4d41be..a1bae965e 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -1,8 +1,6 @@ import os from itertools import product, chain -from multiprocessing import Queue from pathlib import Path -from time import time import numpy as np from typing import Literal, Iterable @@ -13,10 +11,15 @@ from rendercanvas import BaseRenderCanvas -from ._video_writer import VideoWriterAV -from ._utils import make_canvas_and_renderer, create_controller, create_camera +from ._utils import ( + make_canvas_and_renderer, + create_controller, + create_camera, + get_extents_from_grid, +) from ._utils import controller_types as valid_controller_types from ._subplot import Subplot +from ._engine import GridLayout, WindowLayout, UnderlayCamera from .. import ImageGraphic @@ -24,6 +27,8 @@ class Figure: def __init__( self, shape: tuple[int, int] = (1, 1), + rects: list[tuple | np.ndarray] = None, + extents: list[tuple | np.ndarray] = None, cameras: ( Literal["2d", "3d"] | Iterable[Iterable[Literal["2d", "3d"]]] @@ -43,18 +48,36 @@ def __init__( controllers: pygfx.Controller | Iterable[Iterable[pygfx.Controller]] = None, canvas: str | BaseRenderCanvas | pygfx.Texture = None, renderer: pygfx.WgpuRenderer = None, + canvas_kwargs: dict = None, size: tuple[int, int] = (500, 300), names: list | np.ndarray = None, ): """ - A grid of subplots. + Create a Figure containing Subplots. Parameters ---------- - shape: (int, int), default (1, 1) - (n_rows, n_cols) - - cameras: "2d", "3", list of "2d" | "3d", Iterable of camera instances, or Iterable of "2d" | "3d", optional + shape: tuple[int, int], default (1, 1) + shape [n_rows, n_cols] that defines a grid of subplots + + rects: list of tuples or arrays + list of rects (x, y, width, height) that define the subplots. + rects can be defined in absolute pixels or as a fraction of the canvas. + If width & height <= 1 the rect is assumed to be fractional. + Conversely, if width & height > 1 the rect is assumed to be in absolute pixels. + width & height must be > 0. Negative values are not allowed. + + extents: list of tuples or arrays + list of extents (xmin, xmax, ymin, ymax) that define the subplots. + extents can be defined in absolute pixels or as a fraction of the canvas. + If xmax & ymax <= 1 the extent is assumed to be fractional. + Conversely, if xmax & ymax > 1 the extent is assumed to be in absolute pixels. + Negative values are not allowed. xmax - xmin & ymax - ymin must be > 0. + + If both ``rects`` and ``extents`` are provided, then ``rects`` takes precedence over ``extents``, i.e. + ``extents`` is ignored when ``rects`` are also provided. + + cameras: "2d", "3d", list of "2d" | "3d", Iterable of camera instances, or Iterable of "2d" | "3d", optional | if str, one of ``"2d"`` or ``"3d"`` indicating 2D or 3D cameras for all subplots | Iterable/list/array of ``2d`` and/or ``3d`` that specifies the camera type for each subplot | Iterable/list/array of pygfx.PerspectiveCamera instances @@ -69,7 +92,6 @@ def __init__( controller_ids: str, list of int, np.ndarray of int, or list with sublists of subplot str names, optional | If `None` a unique controller is created for each subplot | If "sync" all the subplots use the same controller - | If array/list it must be reshapeable to ``grid_shape``. This allows custom assignment of controllers @@ -90,52 +112,95 @@ def __init__( renderer: pygfx.Renderer, optional pygfx renderer instance + canvas_kwargs: dict, optional + kwargs to pass to the canvas + size: (int, int), optional - starting size of canvas, default (500, 300) + starting size of canvas in absolute pixels, default (500, 300) names: list or array of str, optional subplot names + """ - self._shape = shape + if rects is not None: + if not all(isinstance(v, (np.ndarray, tuple, list)) for v in rects): + raise TypeError( + f"rects must a list of arrays, tuples, or lists of rects (x, y, w, h), you have passed: {rects}" + ) + n_subplots = len(rects) + layout_mode = "rect" + extents = [None] * n_subplots + + elif extents is not None: + if not all(isinstance(v, (np.ndarray, tuple, list)) for v in extents): + raise TypeError( + f"extents must a list of arrays, tuples, or lists of extents (xmin, xmax, ymin, ymax), " + f"you have passed: {extents}" + ) + n_subplots = len(extents) + layout_mode = "extent" + rects = [None] * n_subplots + + else: + if not all(isinstance(v, (int, np.integer)) for v in shape): + raise TypeError("shape argument must be a tuple[n_rows, n_cols]") + n_subplots = shape[0] * shape[1] + layout_mode = "grid" + + # create fractional extents from the grid + extents = get_extents_from_grid(shape) + # empty rects + rects = [None] * n_subplots if names is not None: - if len(list(chain(*names))) != len(self): + subplot_names = np.asarray(names).flatten() + if subplot_names.size != n_subplots: raise ValueError( - "must provide same number of subplot `names` as specified by Figure `shape`" + f"must provide same number of subplot `names` as specified by shape, extents, or rects: {n_subplots}" ) + else: + if layout_mode == "grid": + subplot_names = np.asarray( + list(map(str, product(range(shape[0]), range(shape[1])))) + ) + else: + subplot_names = None - subplot_names = np.asarray(names).reshape(self.shape) + if canvas_kwargs is not None: + if size not in canvas_kwargs.keys(): + canvas_kwargs["size"] = size else: - subplot_names = None + canvas_kwargs = {"size": size, "max_fps": 60.0, "vsync": True} canvas, renderer = make_canvas_and_renderer( - canvas, renderer, canvas_kwargs={"size": size} + canvas, renderer, canvas_kwargs=canvas_kwargs ) + canvas.add_event_handler(self._fpl_reset_layout, "resize") + if isinstance(cameras, str): # create the array representing the views for each subplot in the grid - cameras = np.array([cameras] * len(self)).reshape(self.shape) + cameras = np.array([cameras] * n_subplots) - # list -> array if necessary - cameras = np.asarray(cameras).reshape(self.shape) + # list/tuple -> array if necessary + cameras = np.asarray(cameras).flatten() - if cameras.shape != self.shape: - raise ValueError("Number of cameras does not match the number of subplots") + if cameras.size != n_subplots: + raise ValueError( + f"Number of cameras: {cameras.size} does not match the number of subplots: {n_subplots}" + ) # create the cameras - subplot_cameras = np.empty(self.shape, dtype=object) - for i, j in product(range(self.shape[0]), range(self.shape[1])): - subplot_cameras[i, j] = create_camera(camera_type=cameras[i, j]) + subplot_cameras = np.empty(n_subplots, dtype=object) + for index in range(n_subplots): + subplot_cameras[index] = create_camera(camera_type=cameras[index]) # if controller instances have been specified for each subplot if controllers is not None: - # one controller for all subplots if isinstance(controllers, pygfx.Controller): - controllers = [controllers] * len(self) - # subplot_controllers[:] = controllers - # # subplot_controllers = np.asarray([controllers] * len(self), dtype=object) + controllers = [controllers] * n_subplots # individual controller instance specified for each subplot else: @@ -152,32 +217,28 @@ def __init__( "pygfx.Controller instances" ) - try: - controllers = np.asarray(controllers).reshape(shape) - except ValueError: + subplot_controllers: np.ndarray[pygfx.Controller] = np.asarray( + controllers + ).flatten() + if not subplot_controllers.size == n_subplots: raise ValueError( f"number of controllers passed must be the same as the number of subplots specified " - f"by shape: {self.shape}. You have passed: <{controllers.size}> controllers" + f"by shape, extents, or rects: {n_subplots}. You have passed: {subplot_controllers.size} controllers" ) from None - subplot_controllers: np.ndarray[pygfx.Controller] = np.empty( - self.shape, dtype=object - ) + for index in range(n_subplots): + subplot_controllers[index].add_camera(subplot_cameras[index]) - for i, j in product(range(self.shape[0]), range(self.shape[1])): - subplot_controllers[i, j] = controllers[i, j] - subplot_controllers[i, j].add_camera(subplot_cameras[i, j]) - - # parse controller_ids and controller_types to make desired controller for each supblot + # parse controller_ids and controller_types to make desired controller for each subplot else: if controller_ids is None: # individual controller for each subplot - controller_ids = np.arange(len(self)).reshape(self.shape) + controller_ids = np.arange(n_subplots) elif isinstance(controller_ids, str): if controller_ids == "sync": - # this will eventually make one controller for all subplots - controller_ids = np.zeros(self.shape, dtype=int) + # this will end up creating one controller to control the camera of every subplot + controller_ids = np.zeros(n_subplots, dtype=int) else: raise ValueError( f"`controller_ids` must be one of 'sync', an array/list of subplot names, or an array/list of " @@ -207,20 +268,24 @@ def __init__( ) # initialize controller_ids array - ids_init = np.arange(len(self)).reshape(self.shape) + ids_init = np.arange(n_subplots) # set id based on subplot position for each synced sublist - for i, sublist in enumerate(controller_ids): + for row_ix, sublist in enumerate(controller_ids): for name in sublist: ids_init[subplot_names == name] = -( - i + 1 - ) # use negative numbers because why not + row_ix + 1 + ) # use negative numbers to avoid collision with positive numbers from np.arange controller_ids = ids_init # integer ids elif all([isinstance(item, (int, np.integer)) for item in ids_flat]): - controller_ids = np.asarray(controller_ids).reshape(self.shape) + controller_ids = np.asarray(controller_ids).flatten() + if controller_ids.max() < 0: + raise ValueError( + "if passing an integer array of `controller_ids`, all the integers must be positive." + ) else: raise TypeError( @@ -228,25 +293,27 @@ def __init__( f"you have passed: {controller_ids}" ) - if controller_ids.shape != self.shape: + if controller_ids.size != n_subplots: raise ValueError( - "Number of controller_ids does not match the number of subplots" + f"Number of controller_ids does not match the number of subplots: {n_subplots}" ) if controller_types is None: # `create_controller()` will auto-determine controller for each subplot based on defaults - controller_types = np.array(["default"] * len(self)).reshape(self.shape) + controller_types = np.array(["default"] * n_subplots) # valid controller types if isinstance(controller_types, str): - controller_types = [[controller_types]] + controller_types = np.array([controller_types] * n_subplots) - types_flat = list(chain(*controller_types)) + controller_types: np.ndarray[pygfx.Controller] = np.asarray( + controller_types + ).flatten() # str controller_type or pygfx instances valid_str = list(valid_controller_types.keys()) + ["default"] # make sure each controller type is valid - for controller_type in types_flat: + for controller_type in controller_types: if controller_type is None: continue @@ -256,12 +323,8 @@ def __init__( f"Valid `controller_types` arguments are:\n {valid_str}" ) - controller_types: np.ndarray[pygfx.Controller] = np.asarray( - controller_types - ).reshape(self.shape) - # make the real controllers for each subplot - subplot_controllers = np.empty(shape=self.shape, dtype=object) + subplot_controllers = np.empty(shape=n_subplots, dtype=object) for cid in np.unique(controller_ids): cont_type = controller_types[controller_ids == cid] if np.unique(cont_type).size > 1: @@ -292,33 +355,67 @@ def __init__( self._canvas = canvas self._renderer = renderer - nrows, ncols = self.shape + if layout_mode == "grid": + n_rows, n_cols = shape + grid_index_iterator = list(product(range(n_rows), range(n_cols))) + self._subplots: np.ndarray[Subplot] = np.empty(shape=shape, dtype=object) + resizeable = False - self._subplots: np.ndarray[Subplot] = np.ndarray( - shape=(nrows, ncols), dtype=object - ) + else: + self._subplots: np.ndarray[Subplot] = np.empty( + shape=n_subplots, dtype=object + ) + resizeable = True - for i, j in self._get_iterator(): - position = (i, j) - camera = subplot_cameras[i, j] - controller = subplot_controllers[i, j] + for i in range(n_subplots): + camera = subplot_cameras[i] + controller = subplot_controllers[i] if subplot_names is not None: - name = subplot_names[i, j] + name = subplot_names[i] else: name = None - self._subplots[i, j] = Subplot( + subplot = Subplot( parent=self, - position=position, - parent_dims=(nrows, ncols), camera=camera, controller=controller, canvas=canvas, renderer=renderer, name=name, + rect=rects[i], + extent=extents[i], # figure created extents for grid layout + resizeable=resizeable, ) + if layout_mode == "grid": + row_ix, col_ix = grid_index_iterator[i] + self._subplots[row_ix, col_ix] = subplot + else: + self._subplots[i] = subplot + + if layout_mode == "grid": + self._layout = GridLayout( + self.renderer, + subplots=self._subplots, + canvas_rect=self.get_pygfx_render_area(), + shape=shape, + ) + + elif layout_mode == "rect" or layout_mode == "extent": + self._layout = WindowLayout( + self.renderer, + subplots=self._subplots, + canvas_rect=self.get_pygfx_render_area(), + ) + + self._underlay_camera = UnderlayCamera() + + self._underlay_scene = pygfx.Scene() + + for subplot in self._subplots.ravel(): + self._underlay_scene.add(subplot.frame._world_object) + self._animate_funcs_pre: list[callable] = list() self._animate_funcs_post: list[callable] = list() @@ -328,10 +425,20 @@ def __init__( self._output = None + self._pause_render = False + @property - def shape(self) -> tuple[int, int]: + def shape(self) -> list[tuple[int, int, int, int]] | tuple[int, int]: """[n_rows, n_cols]""" - return self._shape + if isinstance(self.layout, GridLayout): + return self.layout.shape + + @property + def layout(self) -> WindowLayout | GridLayout: + """ + Layout engine + """ + return self._layout @property def canvas(self) -> BaseRenderCanvas: @@ -346,54 +453,57 @@ def renderer(self) -> pygfx.WgpuRenderer: @property def controllers(self) -> np.ndarray[pygfx.Controller]: """controllers, read-only array, access individual subplots to change a controller""" - controllers = np.asarray( - [subplot.controller for subplot in self], dtype=object - ).reshape(self.shape) + controllers = np.asarray([subplot.controller for subplot in self], dtype=object) + + if isinstance(self.layout, GridLayout): + controllers = controllers.reshape(self.shape) + controllers.flags.writeable = False return controllers @property def cameras(self) -> np.ndarray[pygfx.Camera]: """cameras, read-only array, access individual subplots to change a camera""" - cameras = np.asarray( - [subplot.camera for subplot in self], dtype=object - ).reshape(self.shape) + cameras = np.asarray([subplot.camera for subplot in self], dtype=object) + + if isinstance(self.layout, GridLayout): + cameras = cameras.reshape(self.shape) + cameras.flags.writeable = False return cameras @property def names(self) -> np.ndarray[str]: """subplot names, read-only array, access individual subplots to change a name""" - names = np.asarray([subplot.name for subplot in self]).reshape(self.shape) + names = np.asarray([subplot.name for subplot in self]) + + if isinstance(self.layout, GridLayout): + names = names.reshape(self.shape) + names.flags.writeable = False return names - def __getitem__(self, index: tuple[int, int] | str) -> Subplot: - if isinstance(index, str): - for subplot in self._subplots.ravel(): - if subplot.name == index: - return subplot - raise IndexError(f"no subplot with given name: {index}") - else: - return self._subplots[index[0], index[1]] + def _render(self, draw=True): + # draw the underlay planes + self.renderer.render(self._underlay_scene, self._underlay_camera, flush=False) - def render(self, draw=True): # call the animation functions before render self._call_animate_functions(self._animate_funcs_pre) - for subplot in self: - subplot.render() + subplot._render() self.renderer.flush() - if draw: - self.canvas.request_draw() # call post-render animate functions self._call_animate_functions(self._animate_funcs_post) - def start_render(self): + if draw: + # needs to be here else events don't get processed + self.canvas.request_draw() + + def _start_render(self): """start render cycle""" - self.canvas.request_draw(self.render) + self.canvas.request_draw(self._render) def show( self, @@ -431,7 +541,7 @@ def show( if self._output: return self._output - self.start_render() + self._start_render() if sidecar_kwargs is None: sidecar_kwargs = dict() @@ -471,8 +581,8 @@ def show( elif self.canvas.__class__.__name__ == "OffscreenRenderCanvas": # for test and docs gallery screenshots + self._fpl_reset_layout() for subplot in self: - subplot.set_viewport_rect() subplot.axes.update_using_camera() # render call is blocking only on github actions for some reason, @@ -481,7 +591,7 @@ def show( # but it is necessary for the gallery images too so that's why this check is here if "RTD_BUILD" in os.environ.keys(): if os.environ["RTD_BUILD"] == "1": - self.render() + self._render() else: # assume GLFW self._output = self.canvas @@ -642,14 +752,18 @@ def export(self, uri: str | Path | bytes, **kwargs): def open_popup(self, *args, **kwargs): warn("popups only supported by ImguiFigure") - def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]: + def _fpl_reset_layout(self, *ev): + """set the viewport rects for all subplots, *ev argument is not used, exists because of renderer resize event""" + self.layout.canvas_resized(self.get_pygfx_render_area()) + + def get_pygfx_render_area(self, *args) -> tuple[float, float, float, float]: """ - Fet rect for the portion of the canvas that the pygfx renderer draws to, + Get rect for the portion of the canvas that the pygfx renderer draws to, i.e. non-imgui, part of canvas Returns ------- - tuple[int, int, int, int] + tuple[float, float, float, float] x_pos, y_pos, width, height """ @@ -658,29 +772,93 @@ def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]: return 0, 0, width, height - def _get_iterator(self): - return product(range(self.shape[0]), range(self.shape[1])) + def add_subplot( + self, + rect=None, + extent=None, + camera: str | pygfx.PerspectiveCamera = "2d", + controller: str | pygfx.Controller = None, + name: str = None, + ) -> Subplot: + if isinstance(self.layout, GridLayout): + raise NotImplementedError( + "`add_subplot()` is not implemented for Figures using a GridLayout" + ) + + raise NotImplementedError("Not yet implemented") + + camera = create_camera(camera) + controller = create_controller(controller, camera) + + subplot = Subplot( + parent=self, + camera=camera, + controller=controller, + canvas=self.canvas, + renderer=self.renderer, + name=name, + rect=rect, + extent=extent, # figure created extents for grid layout + resizeable=True, + ) + + return subplot + + def remove_subplot(self, subplot: Subplot): + raise NotImplementedError("Not yet implemented") + + if isinstance(self.layout, GridLayout): + raise NotImplementedError( + "`remove_subplot()` is not implemented for Figures using a GridLayout" + ) + + if subplot not in self._subplots.tolist(): + raise KeyError(f"given subplot: {subplot} not found in the layout.") + + subplot.clear() + self._underlay_scene.remove(subplot.frame._world_object) + subplot.frame._world_object.clear() + self.layout._subplots = None + subplots = self._subplots.tolist() + subplots.remove(subplot) + self.layout.remove_subplot(subplot) + del subplot + + self._subplots = np.asarray(subplots) + self.layout._subplots = self._subplots.ravel() + + def __getitem__(self, index: str | int | tuple[int, int]) -> Subplot: + if isinstance(index, str): + for subplot in self._subplots.ravel(): + if subplot.name == index: + return subplot + raise IndexError(f"no subplot with given name: {index}") + + if isinstance(self.layout, GridLayout): + return self._subplots[index[0], index[1]] + + return self._subplots[index] def __iter__(self): - self._current_iter = self._get_iterator() + self._current_iter = iter(range(len(self))) return self def __next__(self) -> Subplot: pos = self._current_iter.__next__() - return self._subplots[pos] + return self._subplots.ravel()[pos] def __len__(self): """number of subplots""" - return self.shape[0] * self.shape[1] + return len(self._layout) def __str__(self): - return f"{self.__class__.__name__} @ {hex(id(self))}" + return f"{self.__class__.__name__}" def __repr__(self): newline = "\n\t" return ( - f"fastplotlib.{self.__class__.__name__} @ {hex(id(self))}\n" + f"fastplotlib.{self.__class__.__name__}" f" Subplots:\n" f"\t{newline.join(subplot.__str__() for subplot in self)}" f"\n" diff --git a/fastplotlib/layouts/_frame.py b/fastplotlib/layouts/_frame.py new file mode 100644 index 000000000..cd2a1cbc2 --- /dev/null +++ b/fastplotlib/layouts/_frame.py @@ -0,0 +1,371 @@ +import numpy as np +import pygfx + +from ._rect import RectManager +from ._utils import IMGUI_TOOLBAR_HEIGHT +from ..utils.types import SelectorColorStates +from ..graphics import TextGraphic + + +""" +Each Subplot is framed by a 2D plane mesh, a rectangle. +The rectangles are viewed using the UnderlayCamera where (0, 0) is the top left corner. +We can control the bbox of this rectangle by changing the x and y boundaries of the rectangle. + +Note how the y values of the plane mesh are negative, this is because of the UnderlayCamera. +We always just keep the positive y value, and make it negative only when setting the plane mesh. + +Illustration: + +(0, 0) --------------------------------------------------- +---------------------------------------------------------- +---------------------------------------------------------- +--------------(x0, -y0) --------------- (x1, -y0) -------- +------------------------|||||||||||||||------------------- +------------------------|||||||||||||||------------------- +------------------------|||||||||||||||------------------- +------------------------|||rectangle|||------------------- +------------------------|||||||||||||||------------------- +------------------------|||||||||||||||------------------- +------------------------|||||||||||||||------------------- +--------------(x0, -y1) --------------- (x1, -y1)--------- +---------------------------------------------------------- +------------------------------------------- (canvas_width, canvas_height) + +""" + + +# wgsl shader snippet for SDF function that defines the resize handler, a lower right triangle. +sdf_wgsl_resize_handle = """ +// hardcode square root of 2 +let m_sqrt_2 = 1.4142135; + +// given a distance from an origin point, this defines the hypotenuse of a lower right triangle +let distance = (-coord.x + coord.y) / m_sqrt_2; + +// return distance for this position +return distance * size; +""" + + +class MeshMasks: + """Used set the x0, x1, y0, y1 positions of the plane mesh""" + + x0 = np.array( + [ + [False, False, False], + [True, False, False], + [False, False, False], + [True, False, False], + ] + ) + + x1 = np.array( + [ + [True, False, False], + [False, False, False], + [True, False, False], + [False, False, False], + ] + ) + + y0 = np.array( + [ + [False, True, False], + [False, True, False], + [False, False, False], + [False, False, False], + ] + ) + + y1 = np.array( + [ + [False, False, False], + [False, False, False], + [False, True, False], + [False, True, False], + ] + ) + + +masks = MeshMasks + + +class Frame: + # resize handle color states + resize_handle_color = SelectorColorStates( + idle=(0.6, 0.6, 0.6, 1), # gray + highlight=(1, 1, 1, 1), # white + action=(1, 0, 1, 1), # magenta + ) + + # plane color states + plane_color = SelectorColorStates( + idle=(0.1, 0.1, 0.1), # dark grey + highlight=(0.2, 0.2, 0.2), # less dark grey + action=(0.1, 0.1, 0.2), # dark gray-blue + ) + + def __init__( + self, + viewport, + rect, + extent, + resizeable, + title, + docks, + toolbar_visible, + canvas_rect, + ): + """ + Manages the plane mesh, resize handle point, and subplot title. + It also sets the viewport rects for the subplot rect and the rects of the docks. + + Note: This is a backend class not meant to be user-facing. + + Parameters + ---------- + viewport: pygfx.Viewport + Subplot viewport + + rect: tuple | np.ndarray + rect of this subplot + + extent: tuple | np.ndarray + extent of this subplot + + resizeable: bool + if the Frame is resizeable or not + + title: str + subplot title + + docks: dict[str, PlotArea] + subplot dock + + toolbar_visible: bool + toolbar visibility + + canvas_rect: tuple + figure canvas rect, the render area excluding any areas taken by imgui edge windows + + """ + + self.viewport = viewport + self.docks = docks + self._toolbar_visible = toolbar_visible + + # create rect manager to handle all the backend rect calculations + if rect is not None: + self._rect_manager = RectManager(*rect, canvas_rect) + elif extent is not None: + self._rect_manager = RectManager.from_extent(extent, canvas_rect) + else: + raise ValueError("Must provide `rect` or `extent`") + + wobjects = list() + + # make title graphic + if title is None: + title_text = "" + else: + title_text = title + self._title_graphic = TextGraphic(title_text, font_size=16, face_color="white") + wobjects.append(self._title_graphic.world_object) + + # init mesh of size 1 to graphically represent rect + geometry = pygfx.plane_geometry(1, 1) + material = pygfx.MeshBasicMaterial(color=self.plane_color.idle, pick_write=True) + self._plane = pygfx.Mesh(geometry, material) + wobjects.append(self._plane) + + # otherwise text isn't visible + self._plane.world.z = 0.5 + + # create resize handler at point (x1, y1) + x1, y1 = self.extent[[1, 3]] + self._resize_handle = pygfx.Points( + # note negative y since y is inverted in UnderlayCamera + # subtract 7 so that the bottom right corner of the triangle is at the center + pygfx.Geometry(positions=[[x1 - 7, -y1 + 7, 0]]), + pygfx.PointsMarkerMaterial( + color=self.resize_handle_color.idle, + marker="custom", + custom_sdf=sdf_wgsl_resize_handle, + size=12, + size_space="screen", + pick_write=True, + ), + ) + + if not resizeable: + # set all color states to transparent if Frame isn't resizeable + c = (0, 0, 0, 0) + self._resize_handle.material.color = c + self._resize_handle.material.edge_width = 0 + self.resize_handle_color = SelectorColorStates(c, c, c) + + wobjects.append(self._resize_handle) + + self._world_object = pygfx.Group() + self._world_object.add(*wobjects) + + self._reset() + self.reset_viewport() + + @property + def rect_manager(self) -> RectManager: + return self._rect_manager + + @property + def extent(self) -> np.ndarray: + """extent, (xmin, xmax, ymin, ymax)""" + # not actually stored, computed when needed + return self._rect_manager.extent + + @extent.setter + def extent(self, extent): + self._rect_manager.extent = extent + self._reset() + self.reset_viewport() + + @property + def rect(self) -> np.ndarray[int]: + """rect in absolute screen space, (x, y, w, h)""" + return self._rect_manager.rect + + @rect.setter + def rect(self, rect: np.ndarray): + self._rect_manager.rect = rect + self._reset() + self.reset_viewport() + + def reset_viewport(self): + """reset the viewport rect for the subplot and docks""" + + # get rect of the render area + x, y, w, h = self.get_render_rect() + + # dock sizes + s_left = self.docks["left"].size + s_top = self.docks["top"].size + s_right = self.docks["right"].size + s_bottom = self.docks["bottom"].size + + # top and bottom have same width + # subtract left and right dock sizes + w_top_bottom = w - s_left - s_right + # top and bottom have same x pos + x_top_bottom = x + s_left + + # set dock rects + self.docks["left"].viewport.rect = x, y, s_left, h + self.docks["top"].viewport.rect = x_top_bottom, y, w_top_bottom, s_top + self.docks["bottom"].viewport.rect = ( + x_top_bottom, + y + h - s_bottom, + w_top_bottom, + s_bottom, + ) + self.docks["right"].viewport.rect = x + w - s_right, y, s_right, h + + # calc subplot rect by adjusting for dock sizes + x += s_left + y += s_top + w -= s_left + s_right + h -= s_top + s_bottom + + # set subplot rect + self.viewport.rect = x, y, w, h + + def get_render_rect(self) -> tuple[float, float, float, float]: + """ + Get the actual render area of the subplot, including the docks. + + Excludes area taken by the subplot title and toolbar. Also adds a small amount of spacing around the subplot. + """ + # the rect of the entire Frame + x, y, w, h = self.rect + + x += 1 # add 1 so a 1 pixel edge is visible + w -= 2 # subtract 2, so we get a 1 pixel edge on both sides + + # add 4 pixels above and below title for better spacing + y = y + 4 + self._title_graphic.font_size + 4 + + # spacing on the bottom if imgui toolbar is visible + if self.toolbar_visible: + toolbar_space = IMGUI_TOOLBAR_HEIGHT + resize_handle_space = 0 + else: + toolbar_space = 0 + # need some space for resize handler if imgui toolbar isn't present + resize_handle_space = 13 + + # adjust for the 4 pixels from the line above + # also give space for resize handler if imgui toolbar is not present + h = ( + h + - 4 + - self._title_graphic.font_size + - toolbar_space + - 4 + - resize_handle_space + ) + + return x, y, w, h + + def _reset(self): + """reset the plane mesh using the current rect state""" + + x0, x1, y0, y1 = self._rect_manager.extent + w = self._rect_manager.w + + self._plane.geometry.positions.data[masks.x0] = x0 + self._plane.geometry.positions.data[masks.x1] = x1 + + # negative y because UnderlayCamera y is inverted + self._plane.geometry.positions.data[masks.y0] = -y0 + self._plane.geometry.positions.data[masks.y1] = -y1 + + self._plane.geometry.positions.update_full() + + # note negative y since y is inverted in UnderlayCamera + # subtract 7 so that the bottom right corner of the triangle is at the center + self._resize_handle.geometry.positions.data[0] = [x1 - 7, -y1 + 7, 0] + self._resize_handle.geometry.positions.update_full() + + # set subplot title position + x = x0 + (w / 2) + y = y0 + (self._title_graphic.font_size / 2) + self._title_graphic.world_object.world.x = x + self._title_graphic.world_object.world.y = -y - 4 # add 4 pixels for spacing + + @property + def toolbar_visible(self) -> bool: + return self._toolbar_visible + + @toolbar_visible.setter + def toolbar_visible(self, visible: bool): + self._toolbar_visible = visible + self.reset_viewport() + + @property + def title_graphic(self) -> TextGraphic: + return self._title_graphic + + @property + def plane(self) -> pygfx.Mesh: + """the plane mesh""" + return self._plane + + @property + def resize_handle(self) -> pygfx.Points: + """resize handler point""" + return self._resize_handle + + def canvas_resized(self, canvas_rect): + """called by layout is resized""" + self._rect_manager.canvas_resized(canvas_rect) + self._reset() + self.reset_viewport() diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index ea553f119..a04b681f5 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -9,9 +9,6 @@ class GraphicMethodsMixin: - def __init__(self): - pass - def _create_graphic(self, graphic_class, *args, **kwargs) -> Graphic: if "center" in kwargs.keys(): center = kwargs.pop("center") @@ -45,7 +42,7 @@ def add_image( ---------- data: array-like array-like, usually numpy.ndarray, must support ``memoryview()`` - | shape must be ``[x_dim, y_dim]`` + | shape must be ``[n_rows, n_cols]``, ``[n_rows, n_cols, 3]`` for RGB or ``[n_rows, n_cols, 4]`` for RGBA vmin: int, optional minimum value for color scaling, calculated from data if not provided @@ -185,6 +182,7 @@ def add_line( cmap: str = None, cmap_transform: Union[numpy.ndarray, Iterable] = None, isolated_buffer: bool = True, + size_space: str = "screen", **kwargs ) -> LineGraphic: """ @@ -217,6 +215,9 @@ def add_line( cmap_transform: 1D array-like of numerical values, optional if provided, these values are used to map the colors from the cmap + size_space: str, default "screen" + coordinate space in which the size is expressed ("screen", "world", "model") + **kwargs passed to Graphic @@ -232,6 +233,7 @@ def add_line( cmap, cmap_transform, isolated_buffer, + size_space, **kwargs ) @@ -346,6 +348,7 @@ def add_scatter( isolated_buffer: bool = True, sizes: Union[float, numpy.ndarray, Iterable[float]] = 1, uniform_size: bool = False, + size_space: str = "screen", **kwargs ) -> ScatterGraphic: """ @@ -386,6 +389,9 @@ def add_scatter( if True, uses a uniform buffer for the scatter point sizes, basically saves GPU VRAM when all scatter points are the same size + size_space: str, default "screen" + coordinate space in which the size is expressed ("screen", "world", "model") + kwargs passed to Graphic @@ -402,6 +408,7 @@ def add_scatter( isolated_buffer, sizes, uniform_size, + size_space, **kwargs ) diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py index 8621f4464..40145fe50 100644 --- a/fastplotlib/layouts/_imgui_figure.py +++ b/fastplotlib/layouts/_imgui_figure.py @@ -6,13 +6,12 @@ import imgui_bundle from imgui_bundle import imgui, icons_fontawesome_6 as fa -from wgpu.utils.imgui import ImguiRenderer +from wgpu.utils.imgui import ImguiRenderer, Stats from rendercanvas import BaseRenderCanvas import pygfx from ._figure import Figure -from ._utils import make_canvas_and_renderer from ..ui import EdgeWindow, SubplotToolbar, StandardRightClickMenu, Popup, GUI_EDGES from ..ui import ColormapPicker @@ -21,6 +20,8 @@ class ImguiFigure(Figure): def __init__( self, shape: tuple[int, int] = (1, 1), + rects: list[tuple | np.ndarray] = None, + extents: list[tuple | np.ndarray] = None, cameras: ( Literal["2d", "3d"] | Iterable[Iterable[Literal["2d", "3d"]]] @@ -40,28 +41,29 @@ def __init__( controllers: pygfx.Controller | Iterable[Iterable[pygfx.Controller]] = None, canvas: str | BaseRenderCanvas | pygfx.Texture = None, renderer: pygfx.WgpuRenderer = None, + canvas_kwargs: dict = None, size: tuple[int, int] = (500, 300), names: list | np.ndarray = None, ): self._guis: dict[str, EdgeWindow] = {k: None for k in GUI_EDGES} - canvas, renderer = make_canvas_and_renderer( - canvas, renderer, canvas_kwargs={"size": size} - ) - self._imgui_renderer = ImguiRenderer(renderer.device, canvas) - super().__init__( shape=shape, + rects=rects, + extents=extents, cameras=cameras, controller_types=controller_types, controller_ids=controller_ids, controllers=controllers, canvas=canvas, renderer=renderer, + canvas_kwargs=canvas_kwargs, size=size, names=names, ) + self._imgui_renderer = ImguiRenderer(self.renderer.device, self.canvas) + fronts_path = str( Path(imgui_bundle.__file__).parent.joinpath( "assets", "fonts", "Font_Awesome_6_Free-Solid-900.otf" @@ -80,12 +82,12 @@ def __init__( self.imgui_renderer.set_gui(self._draw_imgui) self._subplot_toolbars: np.ndarray[SubplotToolbar] = np.empty( - shape=self._subplots.shape, dtype=object + shape=self._subplots.size, dtype=object ) - for subplot in self._subplots.ravel(): + for i, subplot in enumerate(self._subplots.ravel()): toolbar = SubplotToolbar(subplot=subplot, fa_icons=self._fa_icons) - self._subplot_toolbars[subplot.position] = toolbar + self._subplot_toolbars[i] = toolbar self._right_click_menu = StandardRightClickMenu( figure=self, fa_icons=self._fa_icons @@ -93,6 +95,9 @@ def __init__( self._popups: dict[str, Popup] = {} + self.imgui_show_fps = False + self._stats = Stats(self.renderer.device, self.canvas) + self.register_popup(ColormapPicker) @property @@ -105,10 +110,16 @@ def imgui_renderer(self) -> ImguiRenderer: """imgui renderer""" return self._imgui_renderer - def render(self, draw=False): - super().render(draw) + def _render(self, draw=False): + if self.imgui_show_fps: + with self._stats: + super()._render(draw) + else: + super()._render(draw) self.imgui_renderer.render() + + # needs to be here else events don't get processed self.canvas.request_draw() def _draw_imgui(self) -> imgui.ImDrawData: @@ -164,11 +175,11 @@ def add_gui(self, gui: EdgeWindow): self.guis[location] = gui - self._reset_viewports() + self._fpl_reset_layout() def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]: """ - Fet rect for the portion of the canvas that the pygfx renderer draws to, + Get rect for the portion of the canvas that the pygfx renderer draws to, i.e. non-imgui, part of canvas Returns @@ -200,15 +211,6 @@ def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]: return xpos, ypos, max(1, width), max(1, height) - def _reset_viewports(self): - # TODO: think about moving this to Figure later, - # maybe also refactor Subplot and PlotArea so that - # the resize event is handled at the Figure level instead - for subplot in self: - subplot.set_viewport_rect() - for dock in subplot.docks.values(): - dock.set_viewport_rect() - def register_popup(self, popup: Popup.__class__): """ Register a popup class. Note that this takes the class, not an instance diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index e096a7f21..e780607ce 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -11,6 +11,7 @@ from ._utils import create_controller from ..graphics._base import Graphic from ..graphics.selectors._base_selector import BaseSelector +from ._graphic_methods_mixin import GraphicMethodsMixin from ..legends import Legend @@ -24,11 +25,10 @@ IPYTHON = get_ipython() -class PlotArea: +class PlotArea(GraphicMethodsMixin): def __init__( self, parent: Union["PlotArea", "Figure"], - position: tuple[int, int] | str, camera: pygfx.PerspectiveCamera, controller: pygfx.Controller, scene: pygfx.Scene, @@ -70,7 +70,6 @@ def __init__( """ self._parent = parent - self._position = position self._scene = scene self._canvas = canvas @@ -88,14 +87,10 @@ def __init__( self._animate_funcs_pre: list[callable] = list() self._animate_funcs_post: list[callable] = list() - self.renderer.add_event_handler(self.set_viewport_rect, "resize") - - # list of hex id strings for all graphics managed by this PlotArea - # the real Graphic instances are managed by REFERENCES + # list of all graphics managed by this PlotArea self._graphics: list[Graphic] = list() # selectors are in their own list so they can be excluded from scene bbox calculations - # managed similar to GRAPHICS for garbage collection etc. self._selectors: list[BaseSelector] = list() # legends, managed just like other graphics as explained above @@ -120,8 +115,6 @@ def __init__( self._background = pygfx.Background(None, self._background_material) self.scene.add(self._background) - self.set_viewport_rect() - def get_figure(self, obj=None): """Get Figure instance that contains this plot area""" if obj is None: @@ -141,11 +134,6 @@ def parent(self): """A parent if relevant""" return self._parent - @property - def position(self) -> tuple[int, int] | str: - """Position of this plot area within a larger layout (such as a Figure) if relevant""" - return self._position - @property def scene(self) -> pygfx.Scene: """The Scene where Graphics lie in this plot area""" @@ -284,22 +272,9 @@ def background_color(self, colors: str | tuple[float]): """1, 2, or 4 colors, each color must be acceptable by pygfx.Color""" self._background_material.set_colors(*colors) - def get_rect(self) -> tuple[float, float, float, float]: - """ - Returns the viewport rect to define the rectangle - occupied by the viewport w.r.t. the Canvas. - - If this is a subplot within a Figure, it returns the rectangle - for only this subplot w.r.t. the parent canvas. - - Must return: [x_pos, y_pos, width_viewport, height_viewport] - - """ - raise NotImplementedError("Must be implemented in subclass") - def map_screen_to_world( self, pos: tuple[float, float] | pygfx.PointerEvent - ) -> np.ndarray: + ) -> np.ndarray | None: """ Map screen position to world position @@ -333,17 +308,14 @@ def map_screen_to_world( # default z is zero for now return np.array([*pos_world[:2], 0]) - def set_viewport_rect(self, *args): - self.viewport.rect = self.get_rect() - - def render(self): + def _render(self): self._call_animate_functions(self._animate_funcs_pre) # does not flush, flush must be implemented in user-facing Plot objects self.viewport.render(self.scene, self.camera) for child in self.children: - child.render() + child._render() self._call_animate_functions(self._animate_funcs_post) @@ -739,7 +711,7 @@ def __str__(self): else: name = self.name - return f"{name}: {self.__class__.__name__} @ {hex(id(self))}" + return f"{name}: {self.__class__.__name__}" def __repr__(self): newline = "\n\t" diff --git a/fastplotlib/layouts/_rect.py b/fastplotlib/layouts/_rect.py new file mode 100644 index 000000000..aa84ee8a2 --- /dev/null +++ b/fastplotlib/layouts/_rect.py @@ -0,0 +1,239 @@ +import numpy as np + + +class RectManager: + """ + Backend management of a rect. Allows converting between rects and extents, also works with fractional inputs. + """ + + def __init__(self, x: float, y: float, w: float, h: float, canvas_rect: tuple): + # initialize rect state arrays + # used to store internal state of the rect in both fractional screen space and absolute screen space + # the purpose of storing the fractional rect is that it remains constant when the canvas resizes + self._rect_frac = np.zeros(4, dtype=np.float64) + self._rect_screen_space = np.zeros(4, dtype=np.float64) + self._canvas_rect = np.asarray(canvas_rect) + + self._set((x, y, w, h)) + + def _set(self, rect): + """ + Using the passed rect which is either absolute screen space or fractional, + set the internal fractional and absolute screen space rects + """ + rect = np.asarray(rect) + for val, name in zip(rect, ["x-position", "y-position", "width", "height"]): + if val < 0: + raise ValueError( + f"Invalid rect value < 0: {rect}\n All values must be non-negative." + ) + + if (rect[2:] <= 1).all(): # fractional bbox + self._set_from_fract(rect) + + elif (rect[2:] > 1).all(): # bbox in already in screen coords coordinates + self._set_from_screen_space(rect) + + else: + raise ValueError(f"Invalid rect: {rect}") + + def _set_from_fract(self, rect): + """set rect from fractional representation""" + _, _, cw, ch = self._canvas_rect + mult = np.array([cw, ch, cw, ch]) + + # check that widths, heights are valid: + if rect[0] + rect[2] > 1: + raise ValueError( + f"invalid fractional rect: {rect}\n x + width > 1: {rect[0]} + {rect[2]} > 1" + ) + if rect[1] + rect[3] > 1: + raise ValueError( + f"invalid fractional rect: {rect}\n y + height > 1: {rect[1]} + {rect[3]} > 1" + ) + + # assign values to the arrays, don't just change the reference + self._rect_frac[:] = rect + self._rect_screen_space[:] = self._rect_frac * mult + + def _set_from_screen_space(self, rect): + """set rect from screen space representation""" + _, _, cw, ch = self._canvas_rect + mult = np.array([cw, ch, cw, ch]) + # for screen coords allow (x, y) = 1 or 0, but w, h must be > 1 + # check that widths, heights are valid + if rect[0] + rect[2] > cw: + raise ValueError( + f"invalid rect: {rect}\n x + width > canvas width: {rect[0]} + {rect[2]} > {cw}" + ) + if rect[1] + rect[3] > ch: + raise ValueError( + f"invalid rect: {rect}\n y + height > canvas height: {rect[1]} + {rect[3]} >{ch}" + ) + + self._rect_frac[:] = rect / mult + self._rect_screen_space[:] = rect + + @property + def x(self) -> np.float64: + """x position""" + return self._rect_screen_space[0] + + @property + def y(self) -> np.float64: + """y position""" + return self._rect_screen_space[1] + + @property + def w(self) -> np.float64: + """width""" + return self._rect_screen_space[2] + + @property + def h(self) -> np.float64: + """height""" + return self._rect_screen_space[3] + + @property + def rect(self) -> np.ndarray: + """rect, (x, y, w, h)""" + return self._rect_screen_space + + @rect.setter + def rect(self, rect: np.ndarray | tuple): + self._set(rect) + + def canvas_resized(self, canvas_rect: tuple): + # called by Frame when canvas is resized + self._canvas_rect[:] = canvas_rect + # set new rect using existing rect_frac since this remains constant regardless of resize + self._set(self._rect_frac) + + @property + def x0(self) -> np.float64: + """x0 position""" + return self.x + + @property + def x1(self) -> np.float64: + """x1 position""" + return self.x + self.w + + @property + def y0(self) -> np.float64: + """y0 position""" + return self.y + + @property + def y1(self) -> np.float64: + """y1 position""" + return self.y + self.h + + @classmethod + def from_extent(cls, extent, canvas_rect): + """create a RectManager from an extent""" + rect = cls.extent_to_rect(extent, canvas_rect) + return cls(*rect, canvas_rect) + + @property + def extent(self) -> np.ndarray: + """extent, (xmin, xmax, ymin, ymax)""" + # not actually stored, computed when needed + return np.asarray([self.x0, self.x1, self.y0, self.y1]) + + @extent.setter + def extent(self, extent): + rect = RectManager.extent_to_rect(extent, canvas_rect=self._canvas_rect) + + self._set(rect) + + @staticmethod + def extent_to_rect(extent, canvas_rect): + """convert an extent to a rect""" + RectManager.validate_extent(extent, canvas_rect) + x0, x1, y0, y1 = extent + + # width and height + w = x1 - x0 + h = y1 - y0 + + return x0, y0, w, h + + @staticmethod + def validate_extent(extent: np.ndarray | tuple, canvas_rect: tuple): + extent = np.asarray(extent) + cx0, cy0, cw, ch = canvas_rect + + # make sure extent is valid + if (extent < 0).any(): + raise ValueError(f"extent must be non-negative, you have passed: {extent}") + + if extent[1] <= 1 or extent[3] <= 1: # if x1 <= 1, or y1 <= 1 + # if fractional rect, convert to full + if not (extent <= 1).all(): # if x1 and y1 <= 1, then all vals must be <= 1 + raise ValueError( + f"if passing a fractional extent, all values must be fractional, you have passed: {extent}" + ) + extent *= np.asarray([cw, cw, ch, ch]) + + x0, x1, y0, y1 = extent + + # width and height + w = x1 - x0 + h = y1 - y0 + + # check if x1 - x0 <= 0 + if w <= 0: + raise ValueError(f"extent x-range must be non-negative: {extent}") + + # check if y1 - y0 <= 0 + if h <= 0: + raise ValueError(f"extent y-range must be non-negative: {extent}") + + # calc canvas extent + cx1 = cx0 + cw + cy1 = cy0 + ch + canvas_extent = np.asarray([cx0, cx1, cy0, cy1]) + + if x0 < cx0 or x1 < cx0 or x0 > cx1 or x1 > cx1: + raise ValueError( + f"extent: {extent} x-range is beyond the bounds of the canvas: {canvas_extent}" + ) + if y0 < cy0 or y1 < cy0 or y0 > cy1 or y1 > cy1: + raise ValueError( + f"extent: {extent} y-range is beyond the bounds of the canvas: {canvas_extent}" + ) + + def is_above(self, y0, dist: int = 1) -> bool: + # our bottom < other top within given distance + return self.y1 < y0 + dist + + def is_below(self, y1, dist: int = 1) -> bool: + # our top > other bottom + return self.y0 > y1 - dist + + def is_left_of(self, x0, dist: int = 1) -> bool: + # our right_edge < other left_edge + # self.x1 < other.x0 + return self.x1 < x0 + dist + + def is_right_of(self, x1, dist: int = 1) -> bool: + # self.x0 > other.x1 + return self.x0 > x1 - dist + + def overlaps(self, extent: np.ndarray) -> bool: + """returns whether this rect overlaps with the given extent""" + x0, x1, y0, y1 = extent + return not any( + [ + self.is_above(y0), + self.is_below(y1), + self.is_left_of(x0), + self.is_right_of(x1), + ] + ) + + def __repr__(self): + s = f"{self._rect_frac}\n{self.rect}" + + return s diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index 7d52ebab2..73f669fe5 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -3,35 +3,30 @@ import numpy as np import pygfx - from rendercanvas import BaseRenderCanvas from ..graphics import TextGraphic from ._utils import create_camera, create_controller from ._plot_area import PlotArea -from ._graphic_methods_mixin import GraphicMethodsMixin +from ._frame import Frame from ..graphics._axes import Axes -# number of pixels taken by the imgui toolbar when present -IMGUI_TOOLBAR_HEIGHT = 39 - - -class Subplot(PlotArea, GraphicMethodsMixin): +class Subplot(PlotArea): def __init__( self, parent: Union["Figure"], - position: tuple[int, int], - parent_dims: tuple[int, int], camera: Literal["2d", "3d"] | pygfx.PerspectiveCamera, - controller: pygfx.Controller, + controller: pygfx.Controller | str, canvas: BaseRenderCanvas | pygfx.Texture, + rect: np.ndarray = None, + extent: np.ndarray = None, + resizeable: bool = True, renderer: pygfx.WgpuRenderer = None, name: str = None, ): """ - General plot object is found within a ``Figure``. Each ``Figure`` instance will have [n rows, n columns] - of subplots. + Subplot class. .. important:: ``Subplot`` is not meant to be constructed directly, it only exists as part of a ``Figure`` @@ -41,12 +36,6 @@ def __init__( parent: 'Figure' | None parent Figure instance - position: (int, int), optional - corresponds to the [row, column] position of the subplot within a ``Figure`` - - parent_dims: (int, int), optional - dimensions of the parent ``Figure`` - camera: str or pygfx.PerspectiveCamera, default '2d' indicates the FOV for the camera, '2d' sets ``fov = 0``, '3d' sets ``fov = 50``. ``fov`` can be changed at any time. @@ -67,31 +56,19 @@ def __init__( """ - super(GraphicMethodsMixin, self).__init__() - - if position is None: - position = (0, 0) - - if parent_dims is None: - parent_dims = (1, 1) - - self.nrows, self.ncols = parent_dims - camera = create_camera(camera) controller = create_controller(controller_type=controller, camera=camera) self._docks = dict() - self.spacing = 2 - - self._title_graphic: TextGraphic = None - - self._toolbar = True + if "Imgui" in parent.__class__.__name__: + toolbar_visible = True + else: + toolbar_visible = False - super(Subplot, self).__init__( + super().__init__( parent=parent, - position=position, camera=camera, controller=controller, scene=pygfx.Scene(), @@ -101,29 +78,48 @@ def __init__( ) for pos in ["left", "top", "right", "bottom"]: - dv = Dock(self, pos, size=0) + dv = Dock(self, size=0) dv.name = pos self.docks[pos] = dv self.children.append(dv) - if self.name is not None: - self.set_title(self.name) - self._axes = Axes(self) self.scene.add(self.axes.world_object) + self._frame = Frame( + viewport=self.viewport, + rect=rect, + extent=extent, + resizeable=resizeable, + title=name, + docks=self.docks, + toolbar_visible=toolbar_visible, + canvas_rect=parent.get_pygfx_render_area(), + ) + @property def axes(self) -> Axes: + """Axes object""" return self._axes @property def name(self) -> str: + """Subplot name""" return self._name @name.setter def name(self, name: str): + if name is None: + self._name = None + return + + for subplot in self.get_figure(self): + if (subplot is self) or (subplot is None): + continue + if subplot.name == name: + raise ValueError("subplot names must be unique") + self._name = name - self.set_title(name) @property def docks(self) -> dict: @@ -143,111 +139,43 @@ def docks(self) -> dict: @property def toolbar(self) -> bool: """show/hide toolbar""" - return self._toolbar + return self.frame.toolbar_visible @toolbar.setter def toolbar(self, visible: bool): - self._toolbar = bool(visible) - self.set_viewport_rect() + self.frame.toolbar_visible = visible + self.frame.reset_viewport() - def render(self): + def _render(self): self.axes.update_using_camera() - super().render() + super()._render() - def set_title(self, text: str): - """Sets the plot title, stored as a ``TextGraphic`` in the "top" dock area""" - if text is None: - return + @property + def title(self) -> TextGraphic: + """subplot title""" + return self._frame.title_graphic + @title.setter + def title(self, text: str): text = str(text) - if self._title_graphic is not None: - self._title_graphic.text = text - else: - tg = TextGraphic(text=text, font_size=18) - self._title_graphic = tg - - self.docks["top"].size = 35 - self.docks["top"].add_graphic(tg) - - self.center_title() - - def center_title(self): - """Centers name of subplot.""" - if self._title_graphic is None: - raise AttributeError("No title graphic is set") - - self._title_graphic.world_object.position = (0, 0, 0) - self.docks["top"].center_graphic(self._title_graphic, zoom=1.5) - self._title_graphic.world_object.position_y = -3.5 - - def get_rect(self) -> np.ndarray: - """ - Returns the bounding box that defines the Subplot within the canvas. + self.title.text = text - Returns - ------- - np.ndarray - x_position, y_position, width, height - - """ - row_ix, col_ix = self.position - - x_start_render, y_start_render, width_canvas_render, height_canvas_render = ( - self.parent.get_pygfx_render_area() - ) - - x_pos = ( - ( - (width_canvas_render / self.ncols) - + ((col_ix - 1) * (width_canvas_render / self.ncols)) - ) - + self.spacing - + x_start_render - ) - y_pos = ( - ( - (height_canvas_render / self.nrows) - + ((row_ix - 1) * (height_canvas_render / self.nrows)) - ) - + self.spacing - + y_start_render - ) - width_subplot = (width_canvas_render / self.ncols) - self.spacing - height_subplot = (height_canvas_render / self.nrows) - self.spacing - - if self.parent.__class__.__name__ == "ImguiFigure" and self.toolbar: - # leave space for imgui toolbar - height_subplot -= IMGUI_TOOLBAR_HEIGHT - - # clip so that min values are always 1, otherwise JupyterRenderCanvas causes issues because it - # initializes with a width of (0, 0) - rect = np.array([x_pos, y_pos, width_subplot, height_subplot]).clip(1) - - for dv in self.docks.values(): - rect = rect + dv.get_parent_rect_adjust() - - return rect + @property + def frame(self) -> Frame: + """Frame that the subplot lives in""" + return self._frame class Dock(PlotArea): - _valid_positions = ["right", "left", "top", "bottom"] - def __init__( self, parent: Subplot, - position: str, size: int, ): - if position not in self._valid_positions: - raise ValueError( - f"the `position` of an AnchoredViewport must be one of: {self._valid_positions}" - ) - self._size = size super().__init__( parent=parent, - position=position, camera=pygfx.OrthographicCamera(), controller=pygfx.PanZoomController(), scene=pygfx.Scene(), @@ -263,141 +191,10 @@ def size(self) -> int: @size.setter def size(self, s: int): self._size = s - self.parent.set_viewport_rect() - self.set_viewport_rect() - - def get_rect(self, *args): - """ - Returns the bounding box that defines this dock area within the canvas. - - Returns - ------- - np.ndarray - x_position, y_position, width, height - """ - if self.size == 0: - self.viewport.rect = None - return - - row_ix_parent, col_ix_parent = self.parent.position + self.get_figure()._fpl_reset_layout() - x_start_render, y_start_render, width_render_canvas, height_render_canvas = ( - self.parent.parent.get_pygfx_render_area() - ) - - spacing = 2 # spacing in pixels - - if self.position == "right": - x_pos = ( - (width_render_canvas / self.parent.ncols) - + ((col_ix_parent - 1) * (width_render_canvas / self.parent.ncols)) - + (width_render_canvas / self.parent.ncols) - - self.size - ) - y_pos = ( - (height_render_canvas / self.parent.nrows) - + ((row_ix_parent - 1) * (height_render_canvas / self.parent.nrows)) - ) + spacing - width_viewport = self.size - height_viewport = (height_render_canvas / self.parent.nrows) - spacing - - elif self.position == "left": - x_pos = (width_render_canvas / self.parent.ncols) + ( - (col_ix_parent - 1) * (width_render_canvas / self.parent.ncols) - ) - y_pos = ( - (height_render_canvas / self.parent.nrows) - + ((row_ix_parent - 1) * (height_render_canvas / self.parent.nrows)) - ) + spacing - width_viewport = self.size - height_viewport = (height_render_canvas / self.parent.nrows) - spacing - - elif self.position == "top": - x_pos = ( - (width_render_canvas / self.parent.ncols) - + ((col_ix_parent - 1) * (width_render_canvas / self.parent.ncols)) - + spacing - ) - y_pos = ( - (height_render_canvas / self.parent.nrows) - + ((row_ix_parent - 1) * (height_render_canvas / self.parent.nrows)) - ) + spacing - width_viewport = (width_render_canvas / self.parent.ncols) - spacing - height_viewport = self.size - - elif self.position == "bottom": - x_pos = ( - (width_render_canvas / self.parent.ncols) - + ((col_ix_parent - 1) * (width_render_canvas / self.parent.ncols)) - + spacing - ) - y_pos = ( - ( - (height_render_canvas / self.parent.nrows) - + ((row_ix_parent - 1) * (height_render_canvas / self.parent.nrows)) - ) - + (height_render_canvas / self.parent.nrows) - - self.size - ) - width_viewport = (width_render_canvas / self.parent.ncols) - spacing - height_viewport = self.size - else: - raise ValueError("invalid position") - - if self.parent.__class__.__name__ == "ImguiFigure" and self.parent.toolbar: - # leave space for imgui toolbar - height_viewport -= IMGUI_TOOLBAR_HEIGHT - - return [ - x_pos + x_start_render, - y_pos + y_start_render, - width_viewport, - height_viewport, - ] - - def get_parent_rect_adjust(self): - if self.position == "right": - return np.array( - [ - 0, # parent subplot x-position is same - 0, - -self.size, # width of parent subplot is `self.size` smaller - 0, - ] - ) - - elif self.position == "left": - return np.array( - [ - self.size, # `self.size` added to parent subplot x-position - 0, - -self.size, # width of parent subplot is `self.size` smaller - 0, - ] - ) - - elif self.position == "top": - return np.array( - [ - 0, - self.size, # `self.size` added to parent subplot y-position - 0, - -self.size, # height of parent subplot is `self.size` smaller - ] - ) - - elif self.position == "bottom": - return np.array( - [ - 0, - 0, # parent subplot y-position is same, - 0, - -self.size, # height of parent subplot is `self.size` smaller - ] - ) - - def render(self): + def _render(self): if self.size == 0: return - super().render() + super()._render() diff --git a/fastplotlib/layouts/_utils.py b/fastplotlib/layouts/_utils.py index b42971570..98a6268f1 100644 --- a/fastplotlib/layouts/_utils.py +++ b/fastplotlib/layouts/_utils.py @@ -1,10 +1,24 @@ import importlib +from itertools import product + +import numpy as np import pygfx from pygfx import WgpuRenderer, Texture, Renderer from ..utils.gui import BaseRenderCanvas, RenderCanvas +try: + import imgui_bundle +except ImportError: + IMGUI = False +else: + IMGUI = True + + +# number of pixels taken by the imgui toolbar when present +IMGUI_TOOLBAR_HEIGHT = 39 + def make_canvas_and_renderer( canvas: str | BaseRenderCanvas | Texture | None, @@ -17,12 +31,12 @@ def make_canvas_and_renderer( """ if canvas is None: - canvas = RenderCanvas(max_fps=60, **canvas_kwargs) + canvas = RenderCanvas(**canvas_kwargs) elif isinstance(canvas, str): import rendercanvas m = importlib.import_module("rendercanvas." + canvas) - canvas = m.RenderCanvas(max_fps=60, **canvas_kwargs) + canvas = m.RenderCanvas(**canvas_kwargs) elif not isinstance(canvas, (BaseRenderCanvas, Texture)): raise TypeError( f"canvas option must either be a valid BaseRenderCanvas implementation, a pygfx Texture" @@ -92,3 +106,20 @@ def create_controller( ) return controller_types[controller_type](camera) + + +def get_extents_from_grid( + shape: tuple[int, int], +) -> list[tuple[float, float, float, float]]: + """create fractional extents from a given grid shape""" + x_min = np.arange(0, 1, (1 / shape[1])) + x_max = x_min + 1 / shape[1] + y_min = np.arange(0, 1, (1 / shape[0])) + y_max = y_min + 1 / shape[0] + + extents = list() + for row_ix, col_ix in product(range(shape[0]), range(shape[1])): + extent = x_min[col_ix], x_max[col_ix], y_min[row_ix], y_max[row_ix] + extents.append(extent) + + return extents diff --git a/fastplotlib/layouts/_video_writer.py b/fastplotlib/layouts/_video_writer.py deleted file mode 100644 index b7e111b50..000000000 --- a/fastplotlib/layouts/_video_writer.py +++ /dev/null @@ -1,82 +0,0 @@ -from pathlib import Path -from multiprocessing import Queue, Process - - -def _get_av(): - try: - import av - except ImportError: - raise ModuleNotFoundError( - "Recording to video file requires `av`:\n" - "https://github.com/PyAV-Org/PyAV" - ) from None - else: - return av - - -class VideoWriterAV(Process): - """Video writer, uses PyAV in an external process to write frames to disk""" - - def __init__( - self, - path: Path | str, - queue: Queue, - fps: int, - width: int, - height: int, - codec: str, - pixel_format: str, - options: dict = None, - ): - super().__init__() - self.queue = queue - - av = _get_av() - self.container = av.open(path, mode="w") - - self.stream = self.container.add_stream(codec, rate=fps, options=options) - - # in case libx264, trim last rows and/or column - # because libx264 doesn't like non-even number width or height - if width % 2 != 0: - width -= 1 - if height % 2 != 0: - height -= 1 - - self.stream.width = width - self.stream.height = height - - self.stream.pix_fmt = pixel_format - - def run(self): - av = _get_av() - while True: - if self.queue.empty(): # no frame to write - continue - - frame = self.queue.get() - - # recording has ended - if frame is None: - self.container.close() - break - - frame = av.VideoFrame.from_ndarray( - frame[ - : self.stream.height, : self.stream.width - ], # trim if necessary because of x264 - format="rgb24", - ) - - for packet in self.stream.encode(frame): - self.container.mux(packet) - - # I don't exactly know what this does, copied from pyav example - for packet in self.stream.encode(): - self.container.mux(packet) - - # close file - self.container.close() - - # close process, release resources - self.close() diff --git a/fastplotlib/ui/_subplot_toolbar.py b/fastplotlib/ui/_subplot_toolbar.py index 6c1a81f73..a06e81b90 100644 --- a/fastplotlib/ui/_subplot_toolbar.py +++ b/fastplotlib/ui/_subplot_toolbar.py @@ -2,6 +2,7 @@ from ..layouts._subplot import Subplot from ._base import Window +from ..layouts._utils import IMGUI_TOOLBAR_HEIGHT class SubplotToolbar(Window): @@ -16,23 +17,27 @@ def __init__(self, subplot: Subplot, fa_icons: imgui.ImFont): def update(self): # get subplot rect - x, y, width, height = self._subplot.get_rect() + x, y, width, height = self._subplot.frame.rect # place the toolbar window below the subplot - pos = (x, y + height) + pos = (x + 1, y + height - IMGUI_TOOLBAR_HEIGHT) - imgui.set_next_window_size((width, 0)) + imgui.set_next_window_size((width - 18, 0)) imgui.set_next_window_pos(pos) - flags = imgui.WindowFlags_.no_collapse | imgui.WindowFlags_.no_title_bar + flags = ( + imgui.WindowFlags_.no_collapse + | imgui.WindowFlags_.no_title_bar + | imgui.WindowFlags_.no_background + ) - imgui.begin(f"Toolbar-{self._subplot.position}", p_open=None, flags=flags) + imgui.begin(f"Toolbar-{hex(id(self._subplot))}", p_open=None, flags=flags) # icons for buttons imgui.push_font(self._fa_icons) # push ID to prevent conflict between multiple figs with same UI imgui.push_id(self._id_counter) - with imgui_ctx.begin_horizontal(f"toolbar-{self._subplot.position}"): + with imgui_ctx.begin_horizontal(f"toolbar-{hex(id(self._subplot))}"): # autoscale button if imgui.button(fa.ICON_FA_MAXIMIZE): self._subplot.auto_scale() diff --git a/fastplotlib/ui/right_click_menus/_standard_menu.py b/fastplotlib/ui/right_click_menus/_standard_menu.py index 9a584043c..1937df858 100644 --- a/fastplotlib/ui/right_click_menus/_standard_menu.py +++ b/fastplotlib/ui/right_click_menus/_standard_menu.py @@ -55,7 +55,7 @@ def update(self): # open popup only if mouse was not moved between mouse_down and mouse_up events if self._last_right_click_pos == imgui.get_mouse_pos(): - if self.get_subplot(): + if self.get_subplot() is not False: # must explicitly check for False # open only if right click was inside a subplot imgui.open_popup(f"right-click-menu") @@ -64,7 +64,7 @@ def update(self): self.cleanup() if imgui.begin_popup(f"right-click-menu"): - if not self.get_subplot(): + if self.get_subplot() is False: # must explicitly check for False # for some reason it will still trigger at certain locations # despite open_popup() only being called when an actual # subplot is returned @@ -74,12 +74,11 @@ def update(self): return name = self.get_subplot().name - if name is None: - name = self.get_subplot().position - # text label at the top of the menu - imgui.text(f"subplot: {name}") - imgui.separator() + if name is not None: + # text label at the top of the menu + imgui.text(f"subplot: {name}") + imgui.separator() # autoscale, center, maintain aspect if imgui.menu_item(f"Autoscale", "", False)[0]: diff --git a/fastplotlib/utils/_plot_helpers.py b/fastplotlib/utils/_plot_helpers.py index ac0ff2cda..5a39b76d0 100644 --- a/fastplotlib/utils/_plot_helpers.py +++ b/fastplotlib/utils/_plot_helpers.py @@ -6,13 +6,14 @@ from ..graphics._collection_base import GraphicCollection -def get_nearest_graphics( +def get_nearest_graphics_indices( pos: tuple[float, float] | tuple[float, float, float], graphics: Sequence[Graphic] | GraphicCollection, -) -> np.ndarray[Graphic]: +) -> np.ndarray[int]: """ - Returns the nearest ``graphics`` to the passed position ``pos`` in world space. - Uses the distance between ``pos`` and the center of the bounding sphere for each graphic. + Returns indices of the nearest ``graphics`` to the passed position ``pos`` in world space + in order of closest to furtherst. Uses the distance between ``pos`` and the center of the + bounding sphere for each graphic. Parameters ---------- @@ -25,11 +26,10 @@ def get_nearest_graphics( Returns ------- - tuple[Graphic] - nearest graphics to ``pos`` in order + ndarray[int] + indices of the nearest nearest graphics to ``pos`` in order """ - if isinstance(graphics, GraphicCollection): graphics = graphics.graphics @@ -50,4 +50,31 @@ def get_nearest_graphics( distances = np.linalg.norm(centers[:, : len(pos)] - pos, ord=2, axis=1) sort_indices = np.argsort(distances) + return sort_indices + + +def get_nearest_graphics( + pos: tuple[float, float] | tuple[float, float, float], + graphics: Sequence[Graphic] | GraphicCollection, +) -> np.ndarray[Graphic]: + """ + Returns the nearest ``graphics`` to the passed position ``pos`` in world space. + Uses the distance between ``pos`` and the center of the bounding sphere for each graphic. + + Parameters + ---------- + pos: (x, y) | (x, y, z) + position in world space, z-axis is ignored when calculating L2 norms if ``pos`` is 2D + + graphics: Sequence, i.e. array, list, tuple, etc. of Graphic | GraphicCollection + the graphics from which to return a sorted array of graphics in order of closest + to furthest graphic + + Returns + ------- + ndarray[Graphic] + nearest graphics to ``pos`` in order + + """ + sort_indices = get_nearest_graphics_indices(pos, graphics) return np.asarray(graphics)[sort_indices] diff --git a/fastplotlib/utils/functions.py b/fastplotlib/utils/functions.py index 02dcd0572..6ad365e40 100644 --- a/fastplotlib/utils/functions.py +++ b/fastplotlib/utils/functions.py @@ -205,7 +205,7 @@ def make_colors(n_colors: int, cmap: str, alpha: float = 1.0) -> np.ndarray: def get_cmap_texture(name: str, alpha: float = 1.0) -> Texture: - return cmap_lib.Colormap(name).to_pygfx() + return Texture(get_cmap(name, alpha), dim=1) def make_colors_dict(labels: Sequence, cmap: str, **kwargs) -> OrderedDict: @@ -289,7 +289,7 @@ def quick_min_max(data: np.ndarray) -> tuple[float, float]: ): return data.min, data.max - while data.size > 1e6: + while np.prod(data.shape) > 1e6: ax = np.argmax(data.shape) sl = [slice(None)] * data.ndim sl[ax] = slice(None, None, 2) diff --git a/fastplotlib/utils/types.py b/fastplotlib/utils/types.py new file mode 100644 index 000000000..e99fce2fc --- /dev/null +++ b/fastplotlib/utils/types.py @@ -0,0 +1,4 @@ +from collections import namedtuple + + +SelectorColorStates = namedtuple("state", ["idle", "highlight", "action"]) diff --git a/fastplotlib/widgets/image_widget/_widget.py b/fastplotlib/widgets/image_widget/_widget.py index 31a8176e5..0fbc02be3 100644 --- a/fastplotlib/widgets/image_widget/_widget.py +++ b/fastplotlib/widgets/image_widget/_widget.py @@ -347,8 +347,6 @@ def __init__( """ self._initialized = False - self._names = None - if figure_kwargs is None: figure_kwargs = dict() @@ -425,7 +423,6 @@ def __init__( raise ValueError( "number of `names` for subplots must be same as the number of data arrays" ) - self._names = names else: raise TypeError( @@ -496,7 +493,7 @@ def __init__( self._dims_max_bounds[_dim], array.shape[i] ) - figure_kwargs_default = {"controller_ids": "sync"} + figure_kwargs_default = {"controller_ids": "sync", "names": names} # update the default kwargs with any user-specified kwargs # user specified kwargs will overwrite the defaults @@ -518,10 +515,6 @@ def __init__( self._histogram_widget = histogram_widget for data_ix, (d, subplot) in enumerate(zip(self.data, self.figure)): - if self._names is not None: - name = self._names[data_ix] - else: - name = None frame = self._process_indices(d, slice_indices=self._current_index) frame = self._process_frame_apply(frame, data_ix) @@ -554,8 +547,6 @@ def __init__( **graphic_kwargs, ) subplot.add_graphic(ig) - subplot.name = name - subplot.set_title(name) if self._histogram_widget: hlut = HistogramLUTTool(data=d, image_graphic=ig, name="histogram_lut") diff --git a/scripts/generate_add_graphic_methods.py b/scripts/generate_add_graphic_methods.py index d69185521..533ae77c6 100644 --- a/scripts/generate_add_graphic_methods.py +++ b/scripts/generate_add_graphic_methods.py @@ -34,8 +34,6 @@ def generate_add_graphics_methods(): f.write("from ..graphics._base import Graphic\n\n") f.write("\nclass GraphicMethodsMixin:\n") - f.write(" def __init__(self):\n") - f.write(" pass\n\n") f.write( " def _create_graphic(self, graphic_class, *args, **kwargs) -> Graphic:\n" diff --git a/setup.py b/setup.py index 14d0f0c5b..3ca95de0f 100644 --- a/setup.py +++ b/setup.py @@ -4,8 +4,8 @@ install_requires = [ "numpy>=1.23.0", - "pygfx~=0.7.0", - "wgpu>=0.18.1", + "pygfx~=0.9.0", + "wgpu>=0.20.0", "cmap>=0.1.3", ] diff --git a/tests/test_image_graphic.py b/tests/test_image_graphic.py index 0ea9979a6..02b982d80 100644 --- a/tests/test_image_graphic.py +++ b/tests/test_image_graphic.py @@ -2,6 +2,8 @@ from numpy import testing as npt import imageio.v3 as iio +import pygfx + import fastplotlib as fpl from fastplotlib.graphics._features import FeatureEvent from fastplotlib.utils import make_colors @@ -86,6 +88,10 @@ def test_gray(): # the entire image should be in the single Texture buffer npt.assert_almost_equal(ig.data.buffer[0, 0].data, GRAY_IMAGE) + assert isinstance(ig._material, pygfx.ImageBasicMaterial) + assert isinstance(ig._material.map, pygfx.TextureMap) + assert isinstance(ig._material.map.texture, pygfx.Texture) + ig.cmap = "viridis" assert ig.cmap == "viridis" check_event(graphic=ig, feature="cmap", value="viridis") diff --git a/tests/test_text_graphic.py b/tests/test_text_graphic.py index a13dfe690..deb25ca6b 100644 --- a/tests/test_text_graphic.py +++ b/tests/test_text_graphic.py @@ -25,7 +25,7 @@ def test_create_graphic(): assert text.font_size == 14 assert isinstance(text._font_size, FontSize) - assert text.world_object.geometry.font_size == 14 + assert text.world_object.font_size == 14 assert text.face_color == pygfx.Color("w") assert isinstance(text._face_color, TextFaceColor) @@ -82,7 +82,7 @@ def test_text_changes_events(): text.font_size = 10.0 assert text.font_size == 10.0 - assert text.world_object.geometry.font_size == 10 + assert text.world_object.font_size == 10 check_event(text, "font_size", 10) text.face_color = "r" pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy