diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index e69de29bb2d..b4d907d3ced 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -0,0 +1,26 @@
+# anything with no explicit code owners will be tagged to @core-devs
+*   @napari/core-devs
+
+# submodules
+napari/_vispy/      @brisvag @melonora
+napari/_qt/         @Czaki @DragaDoncila @psobolewskiPhD
+napari/_app_model/  @lucyleeow @DragaDoncila
+napari/benchmarks/  @Czaki @jni
+napari/plugins/     @Czaki @DragaDoncila @lucyleeow
+napari/qt/          @Czaki @jni
+napari/settings/    @Czaki @jni
+
+# specific layers
+napari/layers/image/    @Czaki @brisvag @andy-sweet @kephale
+napari/layers/labels/   @jni @Czaki @brisvag
+napari/layers/points/   @brisvag @kevinyamauchi @andy-sweet @DragaDoncila @kephale
+napari/layers/shapes/   @kevinyamauchi @DragaDoncila @melonora
+napari/layers/surface/  @brisvag @kevinyamauchi @Czaki
+napari/layers/tracks/   @jni @andy-sweet
+napari/layers/vectors/  @brisvag @kevinyamauchi @andy-sweet
+
+# docs
+examples/                            @melissawm @psobolewskiPhD @lucyleeow
+.github/workflows/build_docs.yml     @melissawm @psobolewskiPhD @lucyleeow
+.github/workflows/deploy_docs.yml    @melissawm @psobolewskiPhD
+.github/workflows/circleci.yml       @melissawm @psobolewskiPhD
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 00000000000..a9dc273b657
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,50 @@
+# Contributing to GitHub workflows and actions
+
+*Created: 2024-11-11; Updated:*
+
+See the napari website for more detailed contributor information:
+- [deployment](https://napari.org/stable/developers/contributing/documentation/docs_deployment.html)
+- [contributing guide](https://napari.org/stable/developers/contributing/index.html)
+- [core developer guide](https://napari.org/stable/developers/coredev/core_dev_guide.html)
+
+## Workflows and actions
+
+There are over 20 GitHub workflows found in `.github/workflows`.
+The team creates a workflow to automate manual actions and steps.
+This results in improved accuracy and quality. Some key workflows:
+- `actionlint.yml` does static testing of GitHub action workflows
+- benchmarks
+- `reusable_run_tox_test.yml` uses our constraint files to install the
+  compatible dependencies for each test environment which may differ
+  by OS and qt versions. It is called from `test_pull_request.yml` and `test_comprehensive.yml`, not directly. 
+- `upgrade_test_constraints.yml` automates upgrading dependencies for
+  our test environments. It also has extensive commenting on what the
+  upgrade process entails.
+
+If adding a workflow, please take a moment to explain its purpose at the
+top of its file.
+
+## Templates
+
+Used to provide a consistent user experience when submitting an issue or PR.
+napari uses the following:
+- `PULL_REQUEST_TEMPLATE.md`
+- `ISSUE_TEMPLATE` directory containing:
+   - `config.yml` to add the menu selector when "New Issue" button is pressed
+   - `design_related.md`
+   - `documentation.md`
+   - `feature_request.md`
+   - `bug_report.yml` config file to provide text areas for users to complete for bug reports.
+- `FUNDING.yml`: redirect GitHub to napari NumFOCUS account
+- Testing and bots
+   - `missing_translations.md`: used if an action detects a missing language translation
+   - `dependabot.yml`: opens a PR to notify maintainers of updates to dependencies
+   - `labeler.yml` is a labels config file for labeler action
+   - `BOT_REPO_UPDATE_FAIL_TEMPLATE.md` is an bot failure notification template
+   - `TEST_FAIL_TEMPLATE.md` is a test failure notification template
+
+## CODEOWNERS
+
+This `CODEOWNERS` file identifies which individuals are notified if a
+particular file or directory is found in a PR. Core team members can
+update if desired.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index cb2b58d7fa8..7ea4cab2a3e 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -15,5 +15,8 @@ a screenshot or a screen capture: "An image is worth a thousand words!" -->
   (open a PR on the docs repository (https://github.com/napari/docs) if relevant!)
 - I have added tests that prove my fix is effective or that my feature works
 - If I included new strings, I have used `trans._("some string")` to make them localizable.
-  (For more information see our [translations guide](https://napari.org/developers/translations.html)).
+  (For more information see our [translations guide](https://napari.org/stable/developers/contributing/translations.html)).
+- If an API has been modified, I have added a `.. versionadded::` or `.. versionchanged::`
+  directive to the appropriate docstring (For more information see
+  [the Sphinx documentation](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#describing-changes-between-versions)).
 -->
diff --git a/.github/labeler.yml b/.github/labeler.yml
index cde12857b5f..76569fdc77e 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -1,4 +1,7 @@
-# See: .github/workflows/labeler.yml and https://github.com/marketplace/actions/labeler
+# This config file maps code base files to GitHub labels.
+# We use `.github/workflow/labeler.yml` action and use this file to apply labels to PRs.
+# Repo: https://github.com/actions/labeler
+# Marketplace Action docs: https://github.com/marketplace/actions/labeler
 design:
   - changed-files:
     - any-glob-to-any-file: 'napari/_qt/qt_resources/**/*'
diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml
new file mode 100644
index 00000000000..0e4a16b7042
--- /dev/null
+++ b/.github/workflows/actionlint.yml
@@ -0,0 +1,19 @@
+name: Actionlint
+# https://github.com/rhysd/actionlint
+
+on:
+  pull_request:
+    paths:
+      - '.github/**'
+
+jobs:
+  actionlint:
+    name: Action lint
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: Check workflow files
+        run: |
+          bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
+          ./actionlint -color -ignore SC2129
+        shell: bash
diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml
index 2a41a2c658e..444e75f0b25 100644
--- a/.github/workflows/benchmarks.yml
+++ b/.github/workflows/benchmarks.yml
@@ -72,7 +72,7 @@ jobs:
       - uses: tlambert03/setup-qt-libs@v1
 
       - name: Setup asv
-        run: python -m pip install asv[virtualenv]
+        run: python -m pip install "asv[virtualenv]"
         env:
           PIP_CONSTRAINT: resources/constraints/benchmark.txt
 
@@ -91,9 +91,11 @@ jobs:
           # asv will checkout commits, which might contain LFS artifacts; ignore those errors since
           # they are probably just documentation PNGs not needed here anyway
           GIT_LFS_SKIP_SMUDGE: 1
+          HEAD_LABEL: ${{ github.event.pull_request.head.label }}
           PIP_CONSTRAINT: ${{ github.workspace }}/resources/constraints/benchmark.txt
         run: |
           set -euxo pipefail
+          read -ra cmd_options <<< "$ASV_OPTIONS"
 
           # ID this runner
           asv machine --yes
@@ -103,7 +105,7 @@ jobs:
             BASE_REF=${{ github.event.pull_request.base.sha }}
             CONTENDER_REF=${GITHUB_SHA}
             echo "Baseline:  ${BASE_REF} (${{ github.event.pull_request.base.label }})"
-            echo "Contender: ${CONTENDER_REF} (${{ github.event.pull_request.head.label }})"
+            echo "Contender: ${CONTENDER_REF} ($HEAD_LABEL)"
           elif [[ $GITHUB_EVENT_NAME == schedule ]]; then
             EVENT_NAME="cronjob"
             BASE_REF="${{ fromJSON(steps.latest_release.outputs.data).target_commitish }}"
@@ -118,12 +120,12 @@ jobs:
             echo "Contender: ${CONTENDER_REF} (workflow input)"
           fi
 
-          echo "EVENT_NAME=$EVENT_NAME" >> $GITHUB_ENV
-          echo "BASE_REF=$BASE_REF" >> $GITHUB_ENV
-          echo "CONTENDER_REF=$CONTENDER_REF" >> $GITHUB_ENV
+          echo "EVENT_NAME=$EVENT_NAME" >> "$GITHUB_ENV"
+          echo "BASE_REF=$BASE_REF" >> "$GITHUB_ENV"
+          echo "CONTENDER_REF=$CONTENDER_REF" >> "$GITHUB_ENV"
 
           # Run benchmarks for current commit against base
-          asv continuous $ASV_OPTIONS -b '${{ matrix.selection-regex }}' ${BASE_REF} ${CONTENDER_REF} \
+          asv continuous "${cmd_options[@]}" -b "${{ matrix.selection-regex }}" "${BASE_REF}" "${CONTENDER_REF}" \
           | sed -E "/Traceback | failed$|PERFORMANCE DECREASED/ s/^/::error:: /" \
           | tee asv_continuous.log
 
@@ -166,6 +168,9 @@ jobs:
           "[CI logs and artifacts](||BENCHMARK_CI_LOGS_URL||) for further details." \
           > .asv/results/message_${{ matrix.benchmark-name }}.txt
 
+          awk  '/Benchmark.*Parameter/,/SOME BENCHMARKS HAVE CHANGED SIGNIFICANTLY/' asv_continuous.log \
+          >> .asv/results/message_${{ matrix.benchmark-name }}.txt
+
           fi
 
       - uses: actions/upload-artifact@v4
diff --git a/.github/workflows/benchmarks_report.yml b/.github/workflows/benchmarks_report.yml
index e4c2e2352f8..f4244f72844 100644
--- a/.github/workflows/benchmarks_report.yml
+++ b/.github/workflows/benchmarks_report.yml
@@ -63,7 +63,7 @@ jobs:
       - name: Collect PR number if available
         run: |
           if [[ -f pr_number ]]; then
-            echo "PR_NUMBER=$(cat pr_number)" >> $GITHUB_ENV
+            echo "PR_NUMBER=$(cat pr_number)" >> "$GITHUB_ENV"
           fi
 
       - name: "Comment on PR"
diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml
index 4153d1c3381..1357378f614 100644
--- a/.github/workflows/deploy_docs.yml
+++ b/.github/workflows/deploy_docs.yml
@@ -4,6 +4,8 @@ on:
   push:
     branches:
       - main
+    tags:
+      - "v*"
   workflow_dispatch:
 
 concurrency:
@@ -16,6 +18,22 @@ jobs:
     name: Build docs on napari/docs
     runs-on: ubuntu-latest
     steps:
+      - name: get directory name
+        # if this is a tag, use the tag name as the directory name else dev
+        env:
+          REF: ${{ github.ref }}
+        run: |
+          TAG="${GITHUB_REF/refs\/tags\/v/}"
+          VER="${TAG/a*/}"  # remove alpha identifier
+          VER="${VER/b*/}"  # remove beta identifier
+          VER="${VER/rc*/}"  # remove rc identifier
+          VER="${VER/post*/}"  # remove post identifier
+
+          if [[ "$REF" == "refs/tags/v"* ]]; then
+            echo "branch_name=$VER" >> "$GITHUB_ENV"
+          else
+            echo "branch_name=dev" >> "$GITHUB_ENV"
+          fi
       - name: Trigger workflow and wait
         uses: convictional/trigger-workflow-and-wait@v1.6.5
         with:
@@ -25,3 +43,4 @@ jobs:
           workflow_file_name: build_and_deploy.yml
           trigger_workflow: true
           wait_workflow: true
+          client_payload: '{"target_directory": "${{ env.branch_name }}"}'
diff --git a/.github/workflows/docker-singularity-publish.yml b/.github/workflows/docker-publish.yml
similarity index 65%
rename from .github/workflows/docker-singularity-publish.yml
rename to .github/workflows/docker-publish.yml
index 02d8a11a50a..030ea7964c0 100644
--- a/.github/workflows/docker-singularity-publish.yml
+++ b/.github/workflows/docker-publish.yml
@@ -1,4 +1,4 @@
-name: Docker and Singularity build
+name: Docker build
 
 # This workflow uses actions that are not certified by GitHub.
 # They are provided by a third-party and are governed by
@@ -10,7 +10,7 @@ on:
 
   pull_request:
     paths:
-      - '.github/workflows/docker-singularity-publish.yaml'
+      - '.github/workflows/docker-publish.yml'
 
 #  schedule:
 #    - cron: '31 0 * * *'
@@ -48,7 +48,7 @@ jobs:
       # https://github.com/docker/login-action
       - name: Log into registry ${{ env.REGISTRY }}
         if: github.event_name != 'pull_request'
-        uses: docker/login-action@v3.2.0
+        uses: docker/login-action@v3.3.0
         with:
           registry: ${{ env.REGISTRY }}
           username: ${{ github.actor }}
@@ -81,7 +81,7 @@ jobs:
       # Build and push Docker image with Buildx (don't push on PR)
       # https://github.com/docker/build-push-action
       - name: Build and push Docker image
-        uses: docker/build-push-action@v5
+        uses: docker/build-push-action@v6
         id: docker_build
         with:
           context: .
@@ -97,53 +97,3 @@ jobs:
       - name: Test Docker image
         run: |
           docker run --rm --entrypoint=/bin/bash ${{ steps.docker_build.outputs.imageid }} -ec "python3 -m napari --version"
-
-# ----
-
-  build2:
-    needs: build1
-    runs-on: ubuntu-latest
-    container:
-      image: quay.io/singularity/docker2singularity:v4.1.0
-      options: --privileged
-    permissions:
-      contents: read
-      packages: write
-    strategy:
-      fail-fast: false
-      matrix:
-        recipe: ["Singularity"]
-
-    steps:
-      - name: Checkout repository
-        uses: actions/checkout@v4
-
-      - name: Continue if Singularity Recipe Exists
-        run: |
-          if [[ -f "${{ matrix.recipe }}" ]]; then
-            echo "keepgoing=true" >> $GITHUB_ENV
-          fi
-
-      - name: Build Container
-        if: ${{ env.keepgoing == 'true' }}
-        env:
-          recipe: ${{ matrix.recipe }}
-        run: |
-          ls
-          if [ -f "${{ matrix.recipe }}" ]; then
-             singularity build container.sif ${{ matrix.recipe }}
-             tag=latest
-          fi
-          # Build the container and name by tag
-          echo "Tag is $tag."
-          echo "tag=$tag" >> $GITHUB_ENV
-
-      - name: Login and Deploy Container
-        if: (github.event_name != 'pull_request')
-        env:
-          keepgoing: ${{ env.keepgoing }}
-        run: |
-          if [[ "${keepgoing}" == "true" ]]; then
-              echo ${{ secrets.GITHUB_TOKEN }} | singularity remote login -u ${{ secrets.GHCR_USERNAME }} --password-stdin oras://ghcr.io
-              singularity push container.sif oras://ghcr.io/${GITHUB_REPOSITORY}:${tag}
-          fi
diff --git a/.github/workflows/edit_pr_description.yml b/.github/workflows/edit_pr_description.yml
index df6af421f3a..ee0b33c4790 100644
--- a/.github/workflows/edit_pr_description.yml
+++ b/.github/workflows/edit_pr_description.yml
@@ -1,5 +1,5 @@
-name: Remove html In PR description
-# see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target
+name: Clean up PR description
+
 on:
   # see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target
   pull_request_target:
@@ -15,7 +15,7 @@ permissions:
 
 jobs:
   check_labels:
-    name: Remove html comments.
+    name: Remove html comments
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
diff --git a/.github/workflows/label_and_milesone_checker.yml b/.github/workflows/label_and_milesone_checker.yml
deleted file mode 100644
index 35f41a9e21a..00000000000
--- a/.github/workflows/label_and_milesone_checker.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-name: Label and milestone checker
-on:
-  pull_request:
-    types:
-      - opened
-      - synchronize
-      - reopened
-      - labeled
-      - unlabeled
-      - milestoned
-      - demilestoned
-  merge_group: # to be prepared on merge queue
-    types: [checks_requested]
-
-jobs:
-  check_labels_and_milestone:
-    if: (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ready to merge'))
-    name: Check labels and milestone
-    runs-on: ubuntu-latest
-    steps:
-      - name: Check labels
-        uses: docker://agilepathway/pull-request-label-checker:latest
-        with:
-          any_of: bugfix,feature,documentation,performance,enhancement,maintenance
-          repo_token: ${{ secrets.GITHUB_TOKEN }}
-
-      - name: Check milestone
-        if: github.event.pull_request.milestone == null
-        run: |
-          echo "Please add a milestone to this PR"
-          exit 1
diff --git a/.github/workflows/label_and_milestone_checker.yml b/.github/workflows/label_and_milestone_checker.yml
new file mode 100644
index 00000000000..8c2f73056c0
--- /dev/null
+++ b/.github/workflows/label_and_milestone_checker.yml
@@ -0,0 +1,70 @@
+name: Labels and milestone
+on:
+  pull_request:
+    types:
+      - opened
+      - synchronize
+      - reopened
+      - labeled
+      - unlabeled
+      - milestoned
+      - demilestoned
+  merge_group: # to be prepared on merge queue
+    types: [checks_requested]
+
+jobs:
+  check_labels:
+    if: (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ready to merge'))
+    name: Check ready-to-merge PR has at least one type label
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check labels
+        uses: docker://agilepathway/pull-request-label-checker:latest
+        with:
+          any_of: bugfix,feature,documentation,performance,enhancement,maintenance
+          repo_token: ${{ secrets.GITHUB_TOKEN }}
+
+  check_next_milestone:
+    name: Check milestone is next release
+    if: (github.event_name == 'pull_request' && github.event.pull_request.milestone != null)
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check milestone for closest due date
+        env:
+          GH_TOKEN: ${{ github.token }}
+          PR_MILESTONE_NAME: ${{ github.event.pull_request.milestone.title }}
+        run: |
+          # Install GitHub CLI if necessary
+          # sudo apt-get install -y gh
+
+          IFS='/' read -r repoOwner repoName <<< "${{ github.repository }}"
+
+          # Fetch the closest future milestone
+          # shellcheck disable=SC2016
+          CLOSEST_MILESTONE=$(gh api graphql -f query='
+            query($repoName: String!, $repoOwner: String!) {
+              repository(name: $repoName, owner: $repoOwner) {
+                milestones(states: OPEN, orderBy: {field: DUE_DATE, direction: ASC}, first: 100) {
+                  nodes {
+                    title
+                    number
+                    dueOn
+                  }
+                }
+              }
+          }' --jq '.data.repository.milestones.nodes | map(select(.dueOn >= now)) | .[0].number' -f repoName="$repoName" -f repoOwner="$repoOwner")
+
+          # Extract the milestone number of the current PR
+          PR_MILESTONE_NUMBER=$(gh api "/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" --jq '.milestone.number')
+          CLOSEST_MILESTONE_NAME=$(gh api "/repos/${{ github.repository }}/milestones/$CLOSEST_MILESTONE" --jq '.title')
+
+
+          # Check if the PR's milestone is the closest future milestone
+          if [ "$CLOSEST_MILESTONE" != "$PR_MILESTONE_NUMBER" ]; then
+            echo "If this PR can be merged in time for the earlier milestone,"
+            echo "changing the milestone to $CLOSEST_MILESTONE_NAME will make the check pass."
+            echo "If this PR must wait until $PR_MILESTONE_NAME,"
+            echo "remove the ready-to-merge tag to skip this check,"
+            echo "and re-add it when all earlier milestones are completed."
+            exit 1
+          fi
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
index 7269a1b2554..f1826173ea7 100644
--- a/.github/workflows/labeler.yml
+++ b/.github/workflows/labeler.yml
@@ -1,10 +1,10 @@
 # https://github.com/marketplace/actions/labeler
-name: "Pull Request Labeler"
+name: Add labels
 on:
 - pull_request_target
 
 jobs:
-  triage:
+  labeler:
     permissions:
       contents: read
       pull-requests: write
diff --git a/.github/workflows/make_release.yml b/.github/workflows/make_release.yml
index 9e1e7fe1499..c9267698f53 100644
--- a/.github/workflows/make_release.yml
+++ b/.github/workflows/make_release.yml
@@ -44,12 +44,13 @@ jobs:
           VER="${TAG/a*/}"  # remove alpha identifier
           VER="${VER/b*/}"  # remove beta identifier
           VER="${VER/rc*/}"  # remove rc identifier
+          VER="${VER/post*/}"  # remove post identifier
           RELEASE_NOTES_PATH="docs/docs/release/release_${VER//./_}.md"
 
-          echo "tag=${TAG}" >> $GITHUB_ENV
-          echo "release_notes_path=${RELEASE_NOTES_PATH}" >> $GITHUB_ENV
-          echo tag: ${TAG}
-          echo release_notes_path: ${RELEASE_NOTES_PATH}
+          echo "tag=${TAG}" >> "$GITHUB_ENV"
+          echo "release_notes_path=${RELEASE_NOTES_PATH}" >> "$GITHUB_ENV"
+          echo tag: "${TAG}"
+          echo release_notes_path: "${RELEASE_NOTES_PATH}"
           ls docs/docs/release
 
       - name: Create Release
@@ -62,6 +63,7 @@ jobs:
           body_path: ${{ env.release_notes_path }}
           draft: false
           prerelease: ${{ contains(env.tag, 'rc') || contains(env.tag, 'a') || contains(env.tag, 'b') }}
+          target_commitish: ${{ github.sha }}
           files: |
             dist/*
       - name: Publish PyPI Package
diff --git a/.github/workflows/reusable_build_wheel.yml b/.github/workflows/reusable_build_wheel.yml
index 15c6afae03c..4ea6574a259 100644
--- a/.github/workflows/reusable_build_wheel.yml
+++ b/.github/workflows/reusable_build_wheel.yml
@@ -25,7 +25,7 @@ jobs:
 
       - name: Build wheel
         run: |
-          python -m build --wheel --outdir dist/
+          python -m build --outdir dist/
 
       - name: Upload wheel
         uses: actions/upload-artifact@v4
diff --git a/.github/workflows/reusable_coverage_upload.yml b/.github/workflows/reusable_coverage_upload.yml
index 6aebd5f2b4c..9caf01086d1 100644
--- a/.github/workflows/reusable_coverage_upload.yml
+++ b/.github/workflows/reusable_coverage_upload.yml
@@ -37,7 +37,7 @@ jobs:
           python -Im coverage xml -o coverage.xml
 
           # Report and write to summary.
-          python -Im coverage report --format=markdown --skip-empty --skip-covered >> $GITHUB_STEP_SUMMARY
+          python -Im coverage report --format=markdown --skip-empty --skip-covered >> "$GITHUB_STEP_SUMMARY"
 
       - name: Upload coverage data
         uses: codecov/codecov-action@v4
diff --git a/.github/workflows/reusable_pip_test.yml b/.github/workflows/reusable_pip_test.yml
index 61fd00bddbe..1b70986fa08 100644
--- a/.github/workflows/reusable_pip_test.yml
+++ b/.github/workflows/reusable_pip_test.yml
@@ -22,10 +22,23 @@ jobs:
 
       - uses: tlambert03/setup-qt-libs@v1
 
-      - name: Install this commit
+      - name: Build wheel
         run: |
-          pip install --upgrade pip
-          pip install ./napari-from-github[pyqt,testing]
+          pip install --upgrade pip build
+          python -m build "./napari-from-github"
+          # there is a bug in build/setuptools that build only wheel will ignore manifest content.
+          # so we need to build sdist first and then build wheel
+
+      - name: get wheel path
+        run: |
+          WHEEL_PATH=$(ls napari-from-github/dist/*.whl)
+          echo "WHEEL_PATH=$WHEEL_PATH" >> "$GITHUB_ENV"
+
+      - name: Install napari from wheel
+        run: |
+          pip install "${{ env.WHEEL_PATH }}[pyqt,testing]"
+        shell:
+            bash
         env:
           PIP_CONSTRAINT: napari-from-github/resources/constraints/constraints_py3.9.txt
 
@@ -42,7 +55,8 @@ jobs:
 
       - name: Upload test artifacts
         if: failure()
-        uses: actions/upload-artifact@v4
+        uses: actions/upload-artifact@v4.4.0
         with:
           name: test artifacts pip install
           path: .pytest_tmp
+          include-hidden-files: true
diff --git a/.github/workflows/reusable_run_tox_test.yml b/.github/workflows/reusable_run_tox_test.yml
index df908646729..b808e122bc1 100644
--- a/.github/workflows/reusable_run_tox_test.yml
+++ b/.github/workflows/reusable_run_tox_test.yml
@@ -49,11 +49,11 @@ jobs:
       PYVISTA_OFF_SCREEN: True
       MIN_REQ: ${{ inputs.min_req }}
       FORCE_COLOR: 1
-      PIP_CONSTRAINT: ${{ github.workspace }}/resources/constraints/constraints_py${{ inputs.python_version }}${{ (((inputs.platform == 'macos-latest') && '_macos_arm') || '') }}${{ inputs.min_req && '_min_req' }}${{ inputs.constraints_suffix }}.txt
-      UV_CONSTRAINT: ${{ github.workspace }}/resources/constraints/constraints_py${{ inputs.python_version }}${{ (((inputs.platform == 'macos-latest') && '_macos_arm') || '') }}${{ inputs.min_req && '_min_req' }}${{ inputs.constraints_suffix }}.txt
+      PIP_CONSTRAINT: ${{ github.workspace }}/resources/constraints/constraints_py${{ inputs.python_version }}${{ ((startsWith(inputs.platform, 'windows') && '_windows') || '') }}${{ inputs.min_req && '_min_req' }}${{ inputs.constraints_suffix }}.txt
+      UV_CONSTRAINT: ${{ github.workspace }}/resources/constraints/constraints_py${{ inputs.python_version }}${{ ((startsWith(inputs.platform, 'windows') && '_windows') || '') }}${{ inputs.min_req && '_min_req' }}${{ inputs.constraints_suffix }}.txt
       # Above we calculate path to constraints file based on python version and platform
       # Because there is no single PyQt5-Qt5 package version available for all platforms we was forced to use
-      # different constraints files for macOS arm and other platforms
+      # different constraints files for Windows. An example with macOS arm64:
       # ${{ (((inputs.platform == 'macos-latest') && '_macos_arm') || '') }} - if platform is macOS-latest then add '_macos_arm' to constraints file name, else add nothing
       # ${{ inputs.min_req && '_min_req' }} - if min_req is set then add '_min_req' to constraints file name, else add nothing
       #  ${{ inputs.constraints_suffix }} - additional suffix for constraints file name (used for example testing).
@@ -69,7 +69,7 @@ jobs:
           path: dist
 
       - name: Set wheel path
-        run: echo "WHEEL_PATH=$(ls dist/*.whl)" >> $GITHUB_ENV
+        run: echo "WHEEL_PATH=$(ls dist/*.whl)" >> "$GITHUB_ENV"
         shell: bash
 
       - name: Set up Python ${{ inputs.python_version }}
@@ -85,6 +85,11 @@ jobs:
         uses: ts-graphviz/setup-graphviz@v2
         continue-on-error: true
 
+      - name: Set Windows resolution
+        if: runner.os == 'Windows'
+        run: Set-DisplayResolution -Width 1920 -Height 1080 -Force
+        shell: powershell
+
       # strategy borrowed from vispy for installing opengl libs on windows
       - name: Install Windows OpenGL
         if: runner.os == 'Windows'
@@ -92,6 +97,7 @@ jobs:
           git clone --depth 1 https://github.com/pyvista/gl-ci-helpers.git
           powershell gl-ci-helpers/appveyor/install_opengl.ps1
           if (Test-Path -Path "C:\Windows\system32\opengl32.dll" -PathType Leaf) {Exit 0} else {Exit 1}
+        shell: powershell
 
       - name: Disable ptrace security restrictions
         if: runner.os == 'Linux'
@@ -99,7 +105,7 @@ jobs:
           echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
 
       # tox and tox-gh-actions will take care of the "actual" installation
-      # of python dependendencies into a virtualenv.  see tox.ini for more
+      # of python dependencies into a virtualenv.  see tox.ini for more
       - name: Install dependencies
         run: |
           pip install --upgrade pip
@@ -133,10 +139,10 @@ jobs:
         # FOURTH=none
         shell: bash
         run: |
-          python tools/split_qt_backend.py 0 ${{ inputs.qt_backend }} >> $GITHUB_ENV
-          python tools/split_qt_backend.py 1 ${{ inputs.qt_backend }} >> $GITHUB_ENV
-          python tools/split_qt_backend.py 2 ${{ inputs.qt_backend }} >> $GITHUB_ENV
-          python tools/split_qt_backend.py 3 ${{ inputs.qt_backend }} >> $GITHUB_ENV
+          python tools/split_qt_backend.py 0 ${{ inputs.qt_backend }} >> "$GITHUB_ENV"
+          python tools/split_qt_backend.py 1 ${{ inputs.qt_backend }} >> "$GITHUB_ENV"
+          python tools/split_qt_backend.py 2 ${{ inputs.qt_backend }} >> "$GITHUB_ENV"
+          python tools/split_qt_backend.py 3 ${{ inputs.qt_backend }} >> "$GITHUB_ENV"
 
       - name: Test with tox main
         timeout-minutes: ${{ inputs.timeout }}
@@ -192,10 +198,11 @@ jobs:
 
       - name: Upload test artifacts
         if: failure()
-        uses: actions/upload-artifact@v4
+        uses: actions/upload-artifact@v4.4.0
         with:
           name: test artifacts ${{ inputs.platform }} py ${{ inputs.python_version }} ${{ inputs.toxenv || inputs.qt_backend }}
           path: .pytest_tmp
+          include-hidden-files: true
 
       - name: Upload leaked viewer graph
         if: failure()
@@ -212,9 +219,10 @@ jobs:
             ./report-*.json
 
       - name: Upload coverage data
-        uses: actions/upload-artifact@v4
+        uses: actions/upload-artifact@v4.4.0
         if: ${{ inputs.coverage == 'cov' }}
         with:
           name: coverage reports ${{ inputs.platform }} py ${{ inputs.python_version }} ${{ inputs.toxenv || inputs.qt_backend }}
+          include-hidden-files: true
           path: |
             ./.coverage.*
diff --git a/.github/workflows/test_comprehensive.yml b/.github/workflows/test_comprehensive.yml
index 9579f55438b..ae83c85bbc8 100644
--- a/.github/workflows/test_comprehensive.yml
+++ b/.github/workflows/test_comprehensive.yml
@@ -86,7 +86,6 @@ jobs:
       qt_backend: ${{ matrix.backend }}
       min_req: ${{ matrix.MIN_REQ }}
       coverage: cov
-      toxenv: ${{ matrix.toxenv }}
       tox_extras: ${{ matrix.tox_extras }}
 
   test_pip_install:
diff --git a/.github/workflows/test_prereleases.yml b/.github/workflows/test_prereleases.yml
index 3f1b466b09a..6c2bc577eff 100644
--- a/.github/workflows/test_prereleases.yml
+++ b/.github/workflows/test_prereleases.yml
@@ -47,12 +47,18 @@ jobs:
         uses: ts-graphviz/setup-graphviz@v2
         continue-on-error: true
 
+      - name: Set Windows resolution
+        if: runner.os == 'Windows'
+        run: Set-DisplayResolution -Width 1920 -Height 1080 -Force
+        shell: powershell
+
       - name: Install Windows OpenGL
         if: runner.os == 'Windows'
         run: |
           git clone --depth 1 https://github.com/pyvista/gl-ci-helpers.git
           powershell gl-ci-helpers/appveyor/install_opengl.ps1
           if (Test-Path -Path "C:\Windows\system32\opengl32.dll" -PathType Leaf) {Exit 0} else {Exit 1}
+        shell: powershell
 
       - name: Install dependencies
         run: |
diff --git a/.github/workflows/test_pull_requests.yml b/.github/workflows/test_pull_requests.yml
index dd25f4b764c..dff48b3eb90 100644
--- a/.github/workflows/test_pull_requests.yml
+++ b/.github/workflows/test_pull_requests.yml
@@ -48,6 +48,8 @@ jobs:
         run: pip install --upgrade pip
       - name: Install Napari dev
         run: pip install -e .[build]
+        env:
+          PIP_CONSTRAINT: resources/constraints/constraints_py3.11.txt
       - name: Make Typestubs
         run: |
           make typestubs
@@ -70,7 +72,7 @@ jobs:
           pip install --upgrade pip semgrep
           # f"..." and f'...' are the same for semgrep
           semgrep --error --lang python --pattern 'trans._(f"...")' napari
-          semgrep --error --lang python --pattern 'trans._($X.format(...))' napari
+          semgrep --error --lang python --pattern "trans._(\$X.format(...))" napari
 
   build_wheel:
     name: Build wheel
@@ -89,11 +91,13 @@ jobs:
             backend: pyqt5
             pydantic: "_pydantic_1"
             coverage: no_cov
+            min_req: ""
           - python: 3.12
             platform: ubuntu-latest
             backend: pyqt6
             pydantic: ""
             coverage: no_cov
+            min_req: ""
     with:
       python_version: ${{ matrix.python }}
       platform: ${{ matrix.platform }}
@@ -110,7 +114,7 @@ jobs:
       fail-fast: false
       matrix:
         platform: [ ubuntu-latest ]
-        python: [ "3.9", "3.11" ]
+        python: [ "3.10", "3.11" ]
         backend: [ "pyqt5,pyside6" ]
         coverage: [ cov ]
         pydantic: ["_pydantic_1"]
@@ -123,7 +127,7 @@ jobs:
           - python: "3.12"
             platform: windows-latest
             backend: pyqt6
-            coverage: no_cov
+            coverage: cov
           - python: "3.12"
             platform: macos-13
             backend: pyqt5
@@ -159,14 +163,12 @@ jobs:
             platform: ubuntu-latest
             backend: pyside2
             coverage: no_cov
-
     with:
       python_version: ${{ matrix.python }}
       platform: ${{ matrix.platform }}
       qt_backend: ${{ matrix.backend }}
       min_req: ${{ matrix.MIN_REQ }}
       coverage: ${{ matrix.coverage }}
-      toxenv: ${{ matrix.toxenv }}
       tox_extras: ${{ matrix.tox_extras }}
       constraints_suffix: ${{ matrix.pydantic }}
 
@@ -200,7 +202,7 @@ jobs:
   test_benchmarks:
     name: test benchmarks
     runs-on: ubuntu-latest
-#    needs: test_initial
+    needs: test_initial
     timeout-minutes: 60
     env:
       GIT_LFS_SKIP_SMUDGE: 1
@@ -229,7 +231,7 @@ jobs:
       - name: install dependencies
         run: |
           pip install --upgrade pip
-          pip install asv[virtualenv]
+          pip install "asv[virtualenv]"
         env:
           PIP_CONSTRAINT: resources/constraints/benchmark.txt
 
diff --git a/.github/workflows/test_translations.yml b/.github/workflows/test_translations.yml
index 2a6f2955537..4c9a1b76030 100644
--- a/.github/workflows/test_translations.yml
+++ b/.github/workflows/test_translations.yml
@@ -22,8 +22,8 @@ jobs:
           cache-dependency-path: pyproject.toml
       - name: Install napari
         run: |
-          pip install -e .[all]
-          pip install -e .[testing]
+          pip install -e ".[all]"
+          pip install -e ".[testing]"
       - name: Run check
         run: |
           python -m pytest -Wignore tools/ --tb=short
diff --git a/.github/workflows/test_typing.yml b/.github/workflows/test_typing.yml
index 28f3dbd8bbc..7d8d39a3b9b 100644
--- a/.github/workflows/test_typing.yml
+++ b/.github/workflows/test_typing.yml
@@ -1,4 +1,4 @@
-name: Test typing
+name: Type-check
 
 on:
   pull_request:
@@ -10,7 +10,7 @@ concurrency:
   cancel-in-progress: true
 
 jobs:
-  typing:
+  mypy:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
diff --git a/.github/workflows/test_vendored.yml b/.github/workflows/test_vendored.yml
index bc91321a5f0..897861bb58e 100644
--- a/.github/workflows/test_vendored.yml
+++ b/.github/workflows/test_vendored.yml
@@ -1,6 +1,7 @@
 name: Test vendored
 
 on:
+  workflow_dispatch: # Allow running on-demand
   schedule:
     # * is a special character in YAML so you have to quote this string
     - cron:  '0 2 * * *'
@@ -19,13 +20,18 @@ jobs:
         id: check_v
         run: python tools/check_vendored_modules.py --ci
 
+      - name: Set variables
+        run: echo "vendored=$(cat 'tools/vendored_modules.txt')" >> "$GITHUB_OUTPUT"
+        shell: bash
+
       - name: Create PR updating vendored modules
-        uses: peter-evans/create-pull-request@v6
+        uses: peter-evans/create-pull-request@v7
         with:
           commit-message: Update vendored modules.
           branch: update-vendored-examples
           delete-branch: true
           title: "[Automatic] Update ${{ steps.check_v.outputs.vendored }} vendored module"
+          labels: maintenance
           body: |
             This PR is automatically created and updated by napari GitHub
             action cron to keep vendored modules up to date.
diff --git a/.github/workflows/upgrade_test_constraints.yml b/.github/workflows/upgrade_test_constraints.yml
index cbd4a403ba4..cb51792edb6 100644
--- a/.github/workflows/upgrade_test_constraints.yml
+++ b/.github/workflows/upgrade_test_constraints.yml
@@ -86,11 +86,11 @@ jobs:
               "https://api.github.com/repos/${{ github.repository }}/pulls/$PR_number" \
           )
 
-          FULL_NAME=$(echo $PR_data  | jq -r .head.repo.full_name)
-          echo "FULL_NAME=$FULL_NAME" >> $GITHUB_ENV
+          FULL_NAME=$(echo "${PR_data}"  | jq -r .head.repo.full_name)
+          echo "FULL_NAME=$FULL_NAME" >> "$GITHUB_ENV"
 
-          BRANCH=$(echo $PR_data  | jq -r .head.ref)
-          echo "BRANCH=$BRANCH" >> $GITHUB_ENV
+          BRANCH=$(echo "${PR_data}"  | jq -r .head.ref)
+          echo "BRANCH=$BRANCH" >> "$GITHUB_ENV"
 
         env:
           GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -99,8 +99,8 @@ jobs:
         # when schedule or workflow_dispatch triggers workflow, then we need to get info about which branch to use
         if: github.event_name != 'issue_comment' && github.event_name != 'pull_request'
         run: |
-          echo "FULL_NAME=${{ github.repository }}" >> $GITHUB_ENV
-          echo "BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV
+          echo "FULL_NAME=${{ github.repository }}" >> "$GITHUB_ENV"
+          echo "BRANCH=${{ github.ref_name }}" >> "$GITHUB_ENV"
 
       - uses: actions/checkout@v4
         with:
@@ -156,7 +156,8 @@ jobs:
         run: |
           set -x
           pip install -U uv
-          flags="--quiet"
+          flags=(--quiet --extra pyqt5 --extra pyqt6 --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional)
+
           # Explanation of below commands
           # uv pip compile --python-version 3.9 - call uv pip compile but ensure proper interpreter
           # --upgrade upgrade to the latest possible version. Without this pip-compile will take a look to output files and reuse versions (so will ad something on when adding dependency.
@@ -164,27 +165,20 @@ jobs:
           # pyproject.toml resources/constraints/version_denylist.txt - source files. the resources/constraints/version_denylist.txt - contains our test specific constraints like pytes-cov`
           #
           # --extra pyqt5 etc - names of extra sections from pyproject.toml that should be checked for the dependencies list (maybe we could create a super extra section to collect them all in)
-          flags+=" --extra pyqt5"
-          flags+=" --extra pyqt6"
-          flags+=" --extra pyside2"
-          flags+=" --extra pyside6_experimental"
-          flags+=" --extra testing"
-          flags+=" --extra testing_extra"
-          flags+=" --extra optional"
           prefix="napari_repo"
           pyproject_toml="${prefix}/pyproject.toml"
           constraints="${prefix}/resources/constraints"
 
 
           for pyv in 3.9 3.10 3.11 3.12; do
-            uv pip compile --python-version ${pyv} --upgrade --output-file $constraints/constraints_py${pyv}.txt  $pyproject_toml $constraints/version_denylist.txt ${flags}
-            uv pip compile --python-version ${pyv} --upgrade --output-file $constraints/constraints_py${pyv}_pydantic_1.txt  $pyproject_toml $constraints/version_denylist.txt $constraints/pydantic_le_2.txt ${flags}
-            uv pip compile --python-platform aarch64-apple-darwin  --python-version ${pyv} --upgrade --output-file $constraints/constraints_py${pyv}_macos_arm.txt  $pyproject_toml $constraints/version_denylist.txt ${flags}
+            uv pip compile --python-version ${pyv} --upgrade --output-file $constraints/constraints_py${pyv}.txt  $pyproject_toml $constraints/version_denylist.txt "${flags[@]}"
+            uv pip compile --python-version ${pyv} --upgrade --output-file $constraints/constraints_py${pyv}_pydantic_1.txt  $pyproject_toml $constraints/version_denylist.txt $constraints/pydantic_le_2.txt "${flags[@]}"
+            uv pip compile --python-platform windows  --python-version ${pyv} --upgrade --output-file $constraints/constraints_py${pyv}_windows.txt  $pyproject_toml $constraints/version_denylist.txt "${flags[@]}"
           done
 
 
-          uv pip compile --python-version 3.9 --upgrade --output-file $constraints/constraints_py3.9_examples.txt $pyproject_toml $constraints/version_denylist.txt resources/constraints/version_denylist_examples.txt ${flags}
-          uv pip compile --python-version 3.10 --upgrade --output-file $constraints/constraints_py3.10_docs.txt $pyproject_toml $constraints/version_denylist.txt resources/constraints/version_denylist_examples.txt docs/requirements.txt $constraints/pydantic_le_2.txt ${flags}
+          uv pip compile --python-version 3.9 --upgrade --output-file $constraints/constraints_py3.9_examples.txt $pyproject_toml $constraints/version_denylist.txt resources/constraints/version_denylist_examples.txt "${flags[@]}"
+          uv pip compile --python-version 3.10 --upgrade --output-file $constraints/constraints_py3.10_docs.txt $pyproject_toml $constraints/version_denylist.txt resources/constraints/version_denylist_examples.txt docs/requirements.txt $constraints/pydantic_le_2.txt "${flags[@]}"
           uv pip compile --python-version 3.11 --upgrade --output-file ${prefix}/resources/requirements_mypy.txt ${prefix}/resources/requirements_mypy.in
 
       # END PYTHON DEPENDENCIES
diff --git a/.gitignore b/.gitignore
index f8a7108eaf9..03726c1aea2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,8 @@
 docs
 # Byte-compiled / optimized / DLL files
+tools/vendored_modules.txt
+tools/matplotlib
+
 __pycache__/
 *.py[cod]
 *$py.class
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 16c20a28a96..c382ae8cc0d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,22 +1,22 @@
 exclude: _vendor|vendored
 repos:
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.4.8
+  rev: v0.8.2
   hooks:
   - id: ruff-format
     exclude: examples
   - id: ruff
 - repo: https://github.com/seddonym/import-linter
-  rev: v2.0
+  rev: v2.1
   hooks:
   - id: import-linter
     stages: [manual]
 - repo: https://github.com/python-jsonschema/check-jsonschema
-  rev: 0.28.4
+  rev: 0.30.0
   hooks:
   - id: check-github-workflows
 - repo: https://github.com/pre-commit/pre-commit-hooks
-  rev: v4.6.0
+  rev: v5.0.0
   # .py files are skipped cause already checked by other hooks
   hooks:
   - id: check-yaml
diff --git a/CITATION.cff b/CITATION.cff
index 0483d680998..f27121ba964 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -23,6 +23,7 @@ authors:
 - given-names: Juan
   family-names: Nunez-Iglesias
   affiliation: Monash eResearch Centre, Monash University
+  orcid: https://orcid.org/0000-0002-7239-5828
   alias: jni
 - given-names: Peter
   family-names: Sobolewski
@@ -324,5 +325,10 @@ authors:
   family-names: Winston
   affiliation: Tobeva Software
   alias: pwinston
+- given-names: Rubin
+  family-names: Zhao
+  affiliation: Chinese Academy of Sciences - SIAT, Shenzhen, China
+  orcid: https://orcid.org/0009-0005-8264-5682
+  alias: BeanLi
 repository-code: https://github.com/napari/napari
 license: BSD-3-Clause
diff --git a/MANIFEST.in b/MANIFEST.in
index 72960011a7a..6e4af8c92e4 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -14,9 +14,9 @@ recursive-include napari *.py_tmpl
 recursive-exclude tools *
 recursive-exclude napari *.pyc
 exclude napari/benchmarks/*
+include napari/benchmarks/utils.py
 recursive-exclude resources *
 recursive-exclude binder *
 recursive-exclude examples *
 exclude dockerfile
 exclude EULA.md
-exclude Singularity
diff --git a/README.md b/README.md
index de8c63a857b..267d38d890f 100644
--- a/README.md
+++ b/README.md
@@ -50,7 +50,7 @@ import napari
 viewer = napari.view_image(data.cells3d(), channel_axis=1, ndisplay=3)
 ```
 
-![napari viewer with a multichannel image of cells displayed as two image layers: nuclei and membrane.](https://github.com/napari/docs/blob/main/docs/images/multichannel_cells.png)
+![napari viewer with a multichannel image of cells displayed as two image layers: nuclei and membrane.](./napari/resources/multichannel_cells.png)
 
 
 To use napari from inside a script, use `napari.run()`:
@@ -86,7 +86,7 @@ You can see details of [the project roadmap here](https://napari.org/stable/road
 
 ## contributing
 
-Contributions are encouraged! Please read our [contributing guide](https://napari.org/dev/developers/contributing/index.html) to get started. Given that we're in an early stage, you may want to reach out on our [Github Issues](https://github.com/napari/napari/issues) before jumping in. 
+Contributions are encouraged! Please read our [contributing guide](https://napari.org/dev/developers/contributing/index.html) to get started. Given that we're in an early stage, you may want to reach out on our [GitHub Issues](https://github.com/napari/napari/issues) before jumping in. 
 
 If you want to contribute or edit to our documentation, please go to [napari/docs](https://github.com/napari/docs). 
 
@@ -111,12 +111,14 @@ DOI of that version on our [zenodo page](https://zenodo.org/record/3555620). The
 
 We're a community partner on the [image.sc forum](https://forum.image.sc/tags/napari) and all help and support requests should be posted on the forum with the tag `napari`. We look forward to interacting with you there.
 
-Bug reports should be made on our [github issues](https://github.com/napari/napari/issues/new?template=bug_report.md) using
+Bug reports should be made on our [GitHub issues](https://github.com/napari/napari/issues/new?template=bug_report.md) using
 the bug report template. If you think something isn't working, don't hesitate to reach out - it is probably us and not you!
 
 ## institutional and funding partners
 
-<picture>
-  <source media="(prefers-color-scheme: dark)" srcset="https://chanzuckerberg.com/wp-content/themes/czi/img/logo-white.svg">
-  <img alt="CZI logo" src="https://chanzuckerberg.com/wp-content/themes/czi/img/logo.svg">
-</picture>
+<a href="https://chanzuckerberg.com/">
+  <picture>
+    <source media="(prefers-color-scheme: dark)" srcset="https://chanzuckerberg.com/wp-content/themes/czi/img/logo-white.svg">
+    <img alt="CZI logo" src="https://chanzuckerberg.com/wp-content/themes/czi/img/logo.svg">
+  </picture>
+</a>
diff --git a/Singularity b/Singularity
deleted file mode 100644
index e3310f98003..00000000000
--- a/Singularity
+++ /dev/null
@@ -1,5 +0,0 @@
-BootStrap: docker
-From: ghcr.io/napari/napari:main
-
-%post
-date +"%Y-%m-%d-%H%M" > /last_update
diff --git a/asv.conf.json b/asv.conf.json
index a3cd8efcc42..d7666898c33 100644
--- a/asv.conf.json
+++ b/asv.conf.json
@@ -45,7 +45,7 @@
     // should be written to.
     "html_dir": ".asv/html",
 
-    // The directory (relative to the current directory) where the benchamrks
+    // The directory (relative to the current directory) where the benchmarks
     // are stored
     "benchmark_dir": "napari/benchmarks",
 
diff --git a/binder/Desktop/napari.desktop b/binder/Desktop/napari.desktop
index 28a72a40296..04139571e83 100755
--- a/binder/Desktop/napari.desktop
+++ b/binder/Desktop/napari.desktop
@@ -1,7 +1,7 @@
 [Desktop Entry]
 Version=1.0
 Type=Application
-Name=napari 0.4.x
+Name=napari
 Exec=napari
 Icon=/home/jovyan/napari/resources/icon.ico
 Path=/home/jovyan/Desktop
diff --git a/binder/apt.txt b/binder/apt.txt
index 8450e7f3fad..882c2076439 100644
--- a/binder/apt.txt
+++ b/binder/apt.txt
@@ -5,11 +5,14 @@ xfce4-session
 xfce4-settings
 xorg
 xubuntu-icon-theme
-libxss1
-libpci3
-libasound2
-fonts-ubuntu
-qutebrowser
-htop
-nano
-libgles2
+tigervnc-standalone-server
+tigervnc-xorg-extension
+libegl1
+libx11-xcb-dev
+libglu1-mesa-dev
+libxrender-dev
+libxi-dev
+libxkbcommon-dev
+libxkbcommon-x11-dev
+libqt5x11extras5-dev
+libxcb-xinerama0
diff --git a/binder/environment.yml b/binder/environment.yml
index 487f0f0b454..ca1d4df0ebb 100644
--- a/binder/environment.yml
+++ b/binder/environment.yml
@@ -1,45 +1,18 @@
 channels:
-- conda-forge # Used by jupyter-desktop-server
+# install a nightly build of napari
+  - napari/label/nightly
+  - conda-forge # Used by jupyter-desktop-server
 dependencies:
-# See: https://github.com/conda-forge/napari-feedstock/blob/master/recipe/meta.yaml
-- python >=3.8
-# dependencies matched to pip
-- appdirs >=1.4.4
-- cachey >=0.2.1
-- certifi >=2020.6.20
-- dask >=2.1.0
-- imageio >=2.20
-- importlib-metadata >=1.5.0  # not needed for py>37 but keeping for noarch
-- jsonschema >=3.2.0
-- magicgui >=0.7.0
-- napari-console >=0.0.4
-- napari-plugin-engine >=0.1.9
-- napari-svg >=0.1.4
-- numpy >=1.18.5
-- numpydoc >=0.9.2
-- pillow
-- pint >=0.17
-- psutil >=5.0
-- pyopengl >=3.1.0
-- pyyaml >=5.1
-- pydantic >=1.8.1
-- qtpy >=1.7.0
-- scipy >=1.2.0
-- superqt >=0.4.1
-- tifffile >=2020.2.16
-- typing_extensions
-- toolz >=0.10.0
-- tqdm >=4.56.0
-- vispy >=0.6.4
-- wrapt >=1.11.1
+  - python = 3.12
+  - napari
 # additional dependencies for convenience in conda-forge
-- fsspec
-- pyqt
-- scikit-image
-- zarr
+  - fsspec
+  - pyqt
+  - scikit-image
+  - zarr
 
 # Required for desktop view on mybinder.org
-- websockify
-- pip
-- pip:
-  - jupyter-desktop-server
+  - websockify
+  - pip
+  - pip:
+    - jupyter-remote-desktop-proxy
diff --git a/binder/postBuild b/binder/postBuild
index 33fb1234462..a4b7d70eca9 100755
--- a/binder/postBuild
+++ b/binder/postBuild
@@ -8,6 +8,3 @@ cp -r binder/Desktop ${HOME}/Desktop
 mkdir -p ${HOME}/.config/xfce4/xfconf/xfce-perchannel-xml
 cp binder/xsettings.xml ${HOME}/.config/xfce4/xfconf/xfce-perchannel-xml/
 cp binder/xfce4-panel.xml ${HOME}/.config/xfce4/xfconf/xfce-perchannel-xml/
-
-# Install napari
-pip install ${HOME}/ --no-deps
diff --git a/dockerfile b/dockerfile
index 5a09fd37884..e7c3515ddfc 100644
--- a/dockerfile
+++ b/dockerfile
@@ -59,7 +59,7 @@ ARG DEBIAN_FRONTEND=noninteractive
 RUN apt-get update && apt-get install -y wget gnupg2 apt-transport-https \
     software-properties-common ca-certificates && \
     wget -O "/usr/share/keyrings/xpra.asc" https://xpra.org/xpra.asc && \
-    wget -O "/etc/apt/sources.list.d/xpra.sources" https://xpra.org/repos/jammy/xpra.sources
+    wget -O "/etc/apt/sources.list.d/xpra.sources" https://raw.githubusercontent.com/Xpra-org/xpra/master/packaging/repos/jammy/xpra.sources
 
 
 RUN apt-get update && \
diff --git a/examples/action_manager.py b/examples/action_manager.py
index 28a1eed245b..af875d70071 100644
--- a/examples/action_manager.py
+++ b/examples/action_manager.py
@@ -56,7 +56,7 @@ def register_action():
     # we give an action name to the action for configuration purposes as we need
     # it to be storable in json.
 
-    # By convention (may be enforce later), we do give an action name which is iprefixed
+    # By convention (may be enforced later), we do give an action name which is prefixed
     # by the name of the package it is defined in, here napari,
     action_manager.register_action(
         name='napari:rotate45',
diff --git a/examples/add_labels_with_features.py b/examples/add_labels_with_features.py
index 05314be4e0b..639c04663c1 100644
--- a/examples/add_labels_with_features.py
+++ b/examples/add_labels_with_features.py
@@ -16,7 +16,6 @@
 from skimage.segmentation import clear_border
 
 import napari
-from napari.utils.colormaps import DirectLabelColormap
 
 image = data.coins()[50:-50, 50:-50]
 
@@ -51,14 +50,17 @@
 colors = {1: 'white', 2: 'blue', 3: 'green', 4: 'red', 5: 'yellow',
           None: 'magenta'}
 # Here we provide a dict with color mappings for a subset of labels;
-# we also provide a default color (`None` key) which will be used by all other labels
+# when passed to `add_labels`, using the `colormap` kwarg, it will be
+# internally converted to a `napari.utils.colormaps.DirectLabelColormap`
+# Note: we also provide a default color (`None` key) which will be used
+# by all other labels
 
 # add the labels
 label_layer = viewer.add_labels(
     label_image,
     name='segmentation',
     features=label_features,
-    colormap=DirectLabelColormap(color_dict=colors),
+    colormap=colors,
 )
 
 if __name__ == '__main__':
diff --git a/examples/annotate_segmentation_with_text.py b/examples/annotate_segmentation_with_text.py
index d7fefdbb08f..8eaa884a7a3 100644
--- a/examples/annotate_segmentation_with_text.py
+++ b/examples/annotate_segmentation_with_text.py
@@ -3,7 +3,9 @@
 ===============================
 
 Perform a segmentation and annotate the results with
-bounding boxes and text
+bounding boxes and text.
+This example is fully explained in the following tutorial:
+https://napari.org/stable/tutorials/segmentation/annotate_segmentation.html
 
 .. tags:: analysis
 """
diff --git a/examples/clipping_planes_interactive_.py b/examples/clipping_planes_interactive_.py
index 88fcaa6c3be..9d7aee3af5d 100644
--- a/examples/clipping_planes_interactive_.py
+++ b/examples/clipping_planes_interactive_.py
@@ -75,9 +75,7 @@
 
 
 def point_in_bounding_box(point, bounding_box):
-    if np.all(point > bounding_box[0]) and np.all(point < bounding_box[1]):
-        return True
-    return False
+    return bool(np.all(point > bounding_box[0]) and np.all(point < bounding_box[1]))
 
 
 @viewer.mouse_drag_callbacks.append
@@ -156,7 +154,7 @@ def shift_plane_along_normal(viewer, event):
         drag_vector_canv = end_position_canv - start_position_canv
 
         # Project the drag vector onto the plane normal vector
-        # (in canvas coorinates)
+        # (in canvas coordinates)
         drag_projection_on_plane_normal = np.dot(
             drag_vector_canv, plane_normal_canv_normalised
         )
diff --git a/examples/dev/q_list_view.py b/examples/dev/q_list_view.py
index 63a9d65beee..ce7f29a0d6d 100644
--- a/examples/dev/q_list_view.py
+++ b/examples/dev/q_list_view.py
@@ -12,10 +12,10 @@
 """
 import napari
 from napari._qt.containers import QtListView
-from napari.qt import get_app
+from napari.qt import get_qapp
 from napari.utils.events import SelectableEventedList
 
-get_app()
+get_qapp()
 
 
 class MyObject:
diff --git a/examples/dev/q_node_tree.py b/examples/dev/q_node_tree.py
index 430b94bba65..03781f1cd5b 100644
--- a/examples/dev/q_node_tree.py
+++ b/examples/dev/q_node_tree.py
@@ -16,10 +16,10 @@
 """
 import napari
 from napari._qt.containers import QtNodeTreeView
-from napari.qt import get_app
+from napari.qt import get_qapp
 from napari.utils.tree import Group, Node
 
-get_app()
+get_qapp()
 
 # create a group of nodes.
 root = Group(
diff --git a/examples/embed_ipython_.py b/examples/embed_ipython_.py
index aca2abb0417..5c6f2a4deb8 100644
--- a/examples/embed_ipython_.py
+++ b/examples/embed_ipython_.py
@@ -20,7 +20,7 @@
 # any code
 text = 'some text'
 
-# initalize viewer
+# initialize viewer
 viewer = napari.Viewer()
 
 # embed ipython and run the magic command to use the qt event loop
diff --git a/examples/export_figure.py b/examples/export_figure.py
index a573e615444..250613b196c 100644
--- a/examples/export_figure.py
+++ b/examples/export_figure.py
@@ -2,9 +2,11 @@
 Export Figure
 =============
 
-Display one shapes layer ontop of one image layer using the ``add_shapes`` and
-``add_image`` APIs. When the window is closed it will print the coordinates of
-your shapes.
+Display a variety of layer types in the napari viewer and export the figure with `viewer.export_figure()`.
+The exported figure is then added back as an image layer.
+
+Exported figures include the extent of all data in 2D view, and does not presently work for 3D views.
+To capture the extent of the canvas, instead of the layers, see `viewer.screenshot()`: :ref:`sphx_glr_gallery_to_screenshot.py` and :ref:`sphx_glr_gallery_screenshot_and_export_figure.py`.
 
 .. tags:: visualization-advanced
 """
@@ -91,12 +93,16 @@
 size = np.array([10, 20, 20])
 viewer.add_points(points, size=size)
 
+# Add scale bar of a defined length to the exported figure
+viewer.scale_bar.visible = True
+viewer.scale_bar.length = 250
+
 # Export figure and change theme before and after exporting to show that the background canvas margins
 # are not in the exported figure.
 viewer.theme = "light"
 # Optionally for saving the exported figure: viewer.export_figure(path="export_figure.png")
-export_figure = viewer.export_figure()
-scaled_export_figure = viewer.export_figure(scale_factor=5)
+export_figure = viewer.export_figure(flash=False) # bug: default flash=True causes the canvas to be grayscale in docs
+scaled_export_figure = viewer.export_figure(scale_factor=5, flash=False)
 viewer.theme = "dark"
 
 viewer.add_image(export_figure, rgb=True, name='exported_figure')
diff --git a/examples/labels3d.py b/examples/labels3d.py
index 2e4a9da1124..34c7fc9b142 100644
--- a/examples/labels3d.py
+++ b/examples/labels3d.py
@@ -40,5 +40,7 @@
 
 labels_layer = viewer.add_labels(segmented)
 
+viewer.dims.ndisplay = 3
+
 if __name__ == '__main__':
     napari.run()
diff --git a/examples/live_tiffs_generator_.py b/examples/live_tiffs_generator_.py
index 3a5210d4d9f..938478478a0 100644
--- a/examples/live_tiffs_generator_.py
+++ b/examples/live_tiffs_generator_.py
@@ -39,7 +39,7 @@ def main(argv=sys.argv[1:]):
     fractions = np.linspace(0.05, 0.5, n)
     os.makedirs(outdir, exist_ok=True)
     for i, f in enumerate(fractions):
-        # We are using skimage binary_blobs which generate's synthetic binary
+        # We are using skimage binary_blobs which generates synthetic binary
         # image with several rounded blob-like objects and write them into files.
         curr_vol = 255 * data.binary_blobs(
             length=128, blob_size_fraction=0.05, n_dim=3, volume_fraction=f
diff --git a/examples/magic_parameter_sweep.py b/examples/magic_parameter_sweep.py
index 2c8bfe47471..ae2638af718 100644
--- a/examples/magic_parameter_sweep.py
+++ b/examples/magic_parameter_sweep.py
@@ -28,7 +28,7 @@
 # napari object type without actually importing or depending on napari.
 # We also use the `Annotated` type to pass an additional dictionary that can be used
 # to aid widget generation. The keys of the dictionary are keyword arguments to
-# the corresponding magicgui widget type. For more informaiton see
+# the corresponding magicgui widget type. For more information see
 # https://napari.org/magicgui/api/widgets.html.
 def gaussian_blur(
     layer: 'napari.layers.Image',
diff --git a/examples/magic_viewer.py b/examples/magic_viewer.py
index 09390e6528c..c703f4376f1 100644
--- a/examples/magic_viewer.py
+++ b/examples/magic_viewer.py
@@ -10,7 +10,7 @@
 import napari
 
 
-# annotating a paramater as `napari.Viewer` will automatically provide
+# annotating a parameter as `napari.Viewer` will automatically provide
 # the viewer that the function is embedded in, when the function is added to
 # the viewer with add_function_widget.
 def my_function(viewer: napari.Viewer):
diff --git a/examples/multiple_viewer_widget.py b/examples/multiple_viewer_widget.py
index e6f72c170fb..830e0befbca 100644
--- a/examples/multiple_viewer_widget.py
+++ b/examples/multiple_viewer_widget.py
@@ -359,12 +359,20 @@ def _layer_added(self, event):
 
         if isinstance(event.value, Labels):
             event.value.events.set_data.connect(self._set_data_refresh)
+            event.value.events.labels_update.connect(self._set_data_refresh)
             self.viewer_model1.layers[
                 event.value.name
             ].events.set_data.connect(self._set_data_refresh)
             self.viewer_model2.layers[
                 event.value.name
             ].events.set_data.connect(self._set_data_refresh)
+            event.value.events.labels_update.connect(self._set_data_refresh)
+            self.viewer_model1.layers[
+                event.value.name
+            ].events.labels_update.connect(self._set_data_refresh)
+            self.viewer_model2.layers[
+                event.value.name
+            ].events.labels_update.connect(self._set_data_refresh)
         if event.value.name != '.cross':
             self.viewer_model1.layers[event.value.name].events.data.connect(
                 self._sync_data
diff --git a/examples/notebook.ipynb b/examples/notebook.ipynb
index 478c770d356..9426d768fa2 100644
--- a/examples/notebook.ipynb
+++ b/examples/notebook.ipynb
@@ -21,6 +21,7 @@
    "outputs": [],
    "source": [
     "from skimage import data\n",
+    "\n",
     "import napari"
    ]
   },
diff --git a/examples/paint-nd.py b/examples/paint-nd.py
index 8de35392e50..cc54de44efe 100644
--- a/examples/paint-nd.py
+++ b/examples/paint-nd.py
@@ -28,7 +28,6 @@
 labels.n_edit_dimensions = 3
 labels.brush_size = 15
 labels.mode = 'paint'
-labels.n_dimensional = True
 
 if __name__ == '__main__':
     napari.run()
diff --git a/examples/scale_bar.py b/examples/scale_bar.py
index e8cb53bbf4d..764d0066f6e 100644
--- a/examples/scale_bar.py
+++ b/examples/scale_bar.py
@@ -21,7 +21,22 @@
     scale=(0.29, 0.26, 0.26),
 )
 viewer.scale_bar.visible = True
-viewer.scale_bar.unit = 'um'
+
+# Text options
+viewer.scale_bar.unit = 'um'  # set to None to diplay no unit
+viewer.scale_bar.length = 23  # length, in units, of the scale bar
+viewer.scale_bar.font_size = 20  # default is 10
+
+# Text color
+viewer.scale_bar.colored = True  # default value is False
+viewer.scale_bar.color = 'yellow'  # default value is magenta: (1,0,1,1)
+
+# Background box
+viewer.scale_bar.box = True  # add background box, default is False
+viewer.scale_bar.box_color = (0, 1, 1, 0.2)  # cyan with alpha=0.2
+
+# Scale bar position
+viewer.scale_bar.position = 'bottom_left'  # default is 'bottom_right'
 
 if __name__ == '__main__':
     napari.run()
diff --git a/examples/screenshot_and_export_figure.py b/examples/screenshot_and_export_figure.py
new file mode 100644
index 00000000000..fc1b78519cc
--- /dev/null
+++ b/examples/screenshot_and_export_figure.py
@@ -0,0 +1,102 @@
+"""
+Comparison of Screenshot and Figure Export
+==========================================
+
+Display multiple layer types, add scale bar, and take a screenshot or export a
+figure from a 'light' canvas. Then switch to a 'dark' canvas and display the
+screenshot and figure. Compare the limits of each export method. The screenshot
+will include the entire canvas, and results in some layers being clipped
+if it extends outside the canvas. This also means that screenshots will
+reflect the current zoom. In comparison, the `export_figure` will always
+include the extent of the layers and any other elements overlayed
+on the canvas, such as the scale bar. Exported figures also move the scale bar
+to within the margins of the canvas.
+
+Currently, 'export_figure` does not support the 3D view, but screenshot does.
+
+In the final grid state shown below, the first row represents exported images. The first two show that zoom is not reflected in the exported figure. The final one shows how the exported figure adapts to change in the layer extent. In the second row are the screenshots, showing the fact that the entire canvas is captured and that zoom is preserved.
+
+.. tags:: visualization-advanced
+"""
+
+import numpy as np
+from skimage import data
+
+import napari
+
+# Create a napari viewer with multiple layer types and add a scale bar.
+# One of the polygon shapes exists outside the image extent, which is
+# useful in displaying how figure export handles the extent of all layers.
+
+viewer = napari.Viewer()
+
+# add a 2D image layer
+img_layer = viewer.add_image(data.camera(), name='photographer')
+img_layer.colormap = 'gray'
+
+# polygon within image extent
+layer_within = viewer.add_shapes(
+    np.array([[11, 13], [111, 113], [22, 246]]),
+    shape_type='polygon',
+    face_color='coral',
+    name='shapes_within',
+)
+
+# add a polygon shape layer
+layer_outside = viewer.add_shapes(
+    np.array([[572, 222], [305, 292], [577, 440]]),
+    shape_type='polygon',
+    face_color='royalblue',
+    name='shapes_outside',
+)
+
+# add scale_bar with background box
+viewer.scale_bar.visible = True
+viewer.scale_bar.box = True
+# viewer.scale_bar.length = 150  # prevent dynamic adjustment of scale bar length
+
+
+# Take screenshots and export figures in 'light' theme, to show the canvas
+# margins and the extent of the exported figure.
+viewer.theme = 'light'
+screenshot = viewer.screenshot(flash=False) # bug: default flash=True causes the canvas to be grayscale in docs
+figure = viewer.export_figure(flash=False)
+# optionally, save the exported figure: viewer.export_figure(path='export_figure.png')
+# or screenshot: viewer.screenshot(path='screenshot.png')
+
+
+# Zoom in and take another screenshot and export figure to show the different
+# extents of the exported figure and screenshot.
+viewer.camera.zoom = 3
+screenshot_zoomed = viewer.screenshot(flash=False)
+figure_zoomed = viewer.export_figure(flash=False)
+
+
+# Remove the layer that exists outside the image extent and take another
+# figure export to show the extent of the exported figure without the
+# layer that exists outside the camera image extent.
+viewer.layers.remove(layer_outside)
+figure_no_outside_shape = viewer.export_figure(flash=False)
+
+
+# Display the screenshots and figures in 'dark' theme, and switch to grid mode
+# for comparison. In the final grid state shown, the first row represents exported
+# images. The first two show that zoom is not reflected in the exported figure.
+# The final one shows how the exported figure adapts to change in the layer extent.
+# In the second row are the screenshots, showing the fact that the entire canvas
+# is captured and that zoom is preserved.
+viewer.theme = 'dark'
+viewer.layers.select_all()
+viewer.layers.remove_selected()
+
+viewer.add_image(screenshot_zoomed, rgb=True, name='screenshot_zoomed')
+viewer.add_image(screenshot, rgb=True, name='screenshot')
+viewer.add_image(figure_no_outside_shape, rgb=True, name='figure_no_outside_shape')
+viewer.add_image(figure_zoomed, rgb=True, name='figure_zoomed')
+viewer.add_image(figure, rgb=True, name='figure')
+
+viewer.grid.enabled = True
+viewer.grid.shape = (2, 3)
+
+if __name__ == '__main__':
+    napari.run()
diff --git a/examples/show_points_based_on_feature.py b/examples/show_points_based_on_feature.py
index 5da4b47ee52..a97ee7f7932 100644
--- a/examples/show_points_based_on_feature.py
+++ b/examples/show_points_based_on_feature.py
@@ -21,7 +21,7 @@
         )
 
 
-# create a simple widget with magicgui which provides a slider that controls the visilibility
+# create a simple widget with magicgui which provides a slider that controls the visibility
 # of individual points based on their "confidence" value
 @magicgui(
     auto_call=True,
diff --git a/examples/surface_multi_texture_.py b/examples/surface_multi_texture.py
similarity index 98%
rename from examples/surface_multi_texture_.py
rename to examples/surface_multi_texture.py
index 43213cd92ac..2090dc22be1 100644
--- a/examples/surface_multi_texture_.py
+++ b/examples/surface_multi_texture.py
@@ -7,7 +7,7 @@
 
 Thanks to Emmanuel Reynaud and Luis Gutierrez for providing the gorgeous coral
 model for this demo. You can find the data on FigShare:
-https://doi.org/10.6084/m9.figshare.22348645
+https://zenodo.org/records/13380203
 
 More information on the methods used to generate this model can be found in *L.
 Gutierrez-Heredia, C. Keogh, E. G. Reynaud, Assessing the Capabilities of
@@ -55,7 +55,7 @@
 # Download the model
 # ------------------
 download = pooch.DOIDownloader(progressbar=True)
-doi = '10.6084/m9.figshare.22348645.v1'
+doi = '10.5281/zenodo.13380203'
 tmp_dir = pooch.os_cache('napari-surface-texture-example')
 os.makedirs(tmp_dir, exist_ok=True)
 data_files = {
diff --git a/examples/tiled-rendering-2d_.py b/examples/tiled-rendering-2d_.py
index 905dc8ff2e7..979737f5b93 100644
--- a/examples/tiled-rendering-2d_.py
+++ b/examples/tiled-rendering-2d_.py
@@ -24,9 +24,9 @@
 # important: if this is not set, the entire ~4GB array will be created!
 os.environ.setdefault('NAPARI_OCTREE', '1')
 
-import dask.array as da  # noqa: E402
+import dask.array as da
 
-import napari  # noqa: E402
+import napari
 
 ndim = 2
 data = da.random.randint(
diff --git a/examples/to_screenshot.py b/examples/to_screenshot.py
index a15dc2eb66a..8d2f41854dc 100644
--- a/examples/to_screenshot.py
+++ b/examples/to_screenshot.py
@@ -2,9 +2,14 @@
 To screenshot
 =============
 
-Display one shapes layer ontop of one image layer using the ``add_shapes`` and
-``add_image`` APIs. When the window is closed it will print the coordinates of
-your shapes.
+Display a variety of layer types in the napari viewer and take a screenshot of the viewer canvas with `viewer.screenshot()`.
+The screenshot is then added back as an image layer.
+
+Screenshots include all visible layers, bounded by the extent of the canvas, and is functional for 2D and 3D views.
+To capture the extent of all data in 2D view, see `viewer.export_figure()`: :ref:`sphx_glr_gallery_export_figure.py` and :ref:`sphx_glr_gallery_screenshot_and_export_figure.py`.
+
+This example code demonstrates screenshot shortcuts that do not include the viewer (e.g. `File` -> `Copy Screenshot to Clipboard`).
+To include the napari viewer in the screenshot, use `viewer.screenshot(canvas_only=False)` or e.g. `File` -> `Copy Screenshot with Viewer to Clipboard`).
 
 .. tags:: visualization-advanced
 """
@@ -121,7 +126,7 @@
 layer = viewer.add_vectors(pos, edge_width=2)
 
 # take screenshot
-screenshot = viewer.screenshot()
+screenshot = viewer.screenshot(flash=False) # bug: default flash=True causes the canvas to be grayscale in docs
 viewer.add_image(screenshot, rgb=True, name='screenshot')
 
 # from skimage.io import imsave
diff --git a/examples/viewer_fps_label.py b/examples/viewer_fps_label.py
index 84f089c94f6..98afcaf649d 100644
--- a/examples/viewer_fps_label.py
+++ b/examples/viewer_fps_label.py
@@ -20,7 +20,7 @@ def update_fps(fps):
 viewer.add_image(np.random.random((5, 5, 5)), colormap='red', opacity=0.8)
 viewer.text_overlay.visible = True
 # note: this is using a private attribute, so it might break
-# without warningin future versions!
+# without warning in future versions!
 viewer.window._qt_viewer.canvas._scene_canvas.measure_fps(callback=update_fps)
 
 if __name__ == '__main__':
diff --git a/napari/__init__.py b/napari/__init__.py
index ccd7a394d30..1c640367a6d 100644
--- a/napari/__init__.py
+++ b/napari/__init__.py
@@ -2,6 +2,8 @@
 
 from lazy_loader import attach as _attach
 
+from napari._check_numpy_version import limit_numpy1x_threads_on_macos_arm
+
 try:
     from napari._version import version as __version__
 except ImportError:
@@ -9,6 +11,9 @@
 
 # Allows us to use pydata/sparse arrays as layer data
 os.environ.setdefault('SPARSE_AUTO_DENSIFY', '1')
+limit_numpy1x_threads_on_macos_arm()
+
+del limit_numpy1x_threads_on_macos_arm
 del os
 
 # Add everything that needs to be accessible from the napari namespace here.
diff --git a/napari/__init__.pyi b/napari/__init__.pyi
index 1354203c169..5d8e3ecf52e 100644
--- a/napari/__init__.pyi
+++ b/napari/__init__.pyi
@@ -19,7 +19,12 @@ notification_manager: napari.utils.notifications.NotificationManager
 
 __all__ = (
     'Viewer',
+    '__version__',
     'current_viewer',
+    'gui_qt',
+    'notification_manager',
+    'run',
+    'save_layers',
     'view_image',
     'view_labels',
     'view_path',
@@ -28,9 +33,4 @@ __all__ = (
     'view_surface',
     'view_tracks',
     'view_vectors',
-    'save_layers',
-    'gui_qt',
-    'run',
-    'notification_manager',
-    '__version__',
 )
diff --git a/napari/__main__.py b/napari/__main__.py
index 1c2d46b2634..d80fc53b220 100644
--- a/napari/__main__.py
+++ b/napari/__main__.py
@@ -245,7 +245,7 @@ def _run() -> None:
             )
         # I *think* that Qt is looking in sys.argv for a flag `--plugins`,
         # which emits "WARNING: No such plugin for spec 'builtins'"
-        # so remove --plugin from sys.argv to prevent that warningz
+        # so remove --plugin from sys.argv to prevent that warning
         sys.argv.remove('--plugin')
 
     if any(p.endswith('.py') for p in args.paths):
diff --git a/napari/_app_model/__init__.py b/napari/_app_model/__init__.py
index a9a1ba48014..b1b4fed26f5 100644
--- a/napari/_app_model/__init__.py
+++ b/napari/_app_model/__init__.py
@@ -1,3 +1,3 @@
-from napari._app_model._app import get_app
+from napari._app_model._app import get_app, get_app_model
 
-__all__ = ['get_app']
+__all__ = ['get_app', 'get_app_model']
diff --git a/napari/_app_model/_app.py b/napari/_app_model/_app.py
index 1c2db1b9c98..83f18a2e925 100644
--- a/napari/_app_model/_app.py
+++ b/napari/_app_model/_app.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 from functools import lru_cache
+from warnings import warn
 
 from app_model import Application
 
@@ -8,6 +9,7 @@
     LAYERLIST_CONTEXT_ACTIONS,
     LAYERLIST_CONTEXT_SUBMENUS,
 )
+from napari.utils.translations import trans
 
 APP_NAME = 'napari'
 
@@ -29,7 +31,7 @@ def __init__(self, app_name=APP_NAME) -> None:
         self.menus.append_menu_items(LAYERLIST_CONTEXT_SUBMENUS)
 
     @classmethod
-    def get_app(cls, app_name: str = APP_NAME) -> NapariApplication:
+    def get_app_model(cls, app_name: str = APP_NAME) -> NapariApplication:
         return Application.get_app(app_name) or cls()  # type: ignore[return-value]
 
 
@@ -56,6 +58,21 @@ def _public_types(module):
     }
 
 
+# TODO: Remove in 0.6.0
 def get_app() -> NapariApplication:
+    """Get the Napari Application singleton. Now deprecated, use `get_app_model`."""
+    warn(
+        trans._(
+            '`NapariApplication` instance access through `get_app` is deprecated and will be removed in 0.6.0.\n'
+            'Please use `get_app_model` instead.\n',
+            deferred=True,
+        ),
+        category=FutureWarning,
+        stacklevel=2,
+    )
+    return get_app_model()
+
+
+def get_app_model() -> NapariApplication:
     """Get the Napari Application singleton."""
-    return NapariApplication.get_app()
+    return NapariApplication.get_app_model()
diff --git a/napari/_app_model/_tests/test_app.py b/napari/_app_model/_tests/test_app.py
index 3dd78cd5b76..0331330b328 100644
--- a/napari/_app_model/_tests/test_app.py
+++ b/napari/_app_model/_tests/test_app.py
@@ -1,19 +1,27 @@
-from napari._app_model import get_app
+import pytest
+
+from napari._app_model import get_app, get_app_model
 from napari.layers import Points
 
 
-def test_app(mock_app):
+def test_app(mock_app_model):
     """just make sure our app model is registering menus and commands"""
-    app = get_app()
+    app = get_app_model()
     assert app.name == 'test_app'
     assert list(app.menus)
     assert list(app.commands)
     # assert list(app.keybindings)  # don't have any yet
+    with pytest.warns(
+        FutureWarning,
+        match='`NapariApplication` instance access through `get_app` is deprecated and will be removed in 0.6.0.\nPlease use `get_app_model` instead.\n',
+    ):
+        deprecated_app = get_app()
+    assert app == deprecated_app
 
 
-def test_app_injection(mock_app):
+def test_app_injection(mock_app_model):
     """Simple test to make sure napari namespaces are working in app injection."""
-    app = get_app()
+    app = get_app_model()
 
     def use_points(points: 'Points'):
         return points
diff --git a/napari/_app_model/actions/_layerlist_context_actions.py b/napari/_app_model/actions/_layerlist_context_actions.py
index 4a2dee78e44..e88cf9e9a39 100644
--- a/napari/_app_model/actions/_layerlist_context_actions.py
+++ b/napari/_app_model/actions/_layerlist_context_actions.py
@@ -95,6 +95,17 @@
         menus=[{**LAYERCTX_SPLITMERGE, 'when': LLSCK.active_layer_is_rgb}],
         enablement=LLSCK.active_layer_is_rgb,
     ),
+    Action(
+        id='napari.layer.merge_rgb',
+        title=trans._('Merge to RGB'),
+        callback=partial(_layer_actions._merge_stack, rgb=True),
+        enablement=(
+            (LLSCK.num_selected_layers == 3)
+            & (LLSCK.num_selected_image_layers == LLSCK.num_selected_layers)
+            & LLSCK.all_selected_layers_same_shape
+        ),
+        menus=[LAYERCTX_SPLITMERGE],
+    ),
     Action(
         id='napari.layer.convert_to_labels',
         title=trans._('Convert to Labels'),
diff --git a/napari/_app_model/constants/_menus.py b/napari/_app_model/constants/_menus.py
index f1491ae953c..bdc3ce455ef 100644
--- a/napari/_app_model/constants/_menus.py
+++ b/napari/_app_model/constants/_menus.py
@@ -90,6 +90,8 @@ def contributables(cls) -> set['MenuId']:
 class MenuGroup:
     NAVIGATION = 'navigation'  # always the first group in any menu
     RENDER = '1_render'
+    # View menu
+    ZOOM = 'zoom'
     # Plugins menubar
     PLUGINS = '1_plugins'
     PLUGIN_MULTI_SUBMENU = '2_plugin_multi_submenu'
diff --git a/napari/_app_model/context/__init__.py b/napari/_app_model/context/__init__.py
index 94e9b2a381e..22c1d01ab79 100644
--- a/napari/_app_model/context/__init__.py
+++ b/napari/_app_model/context/__init__.py
@@ -10,8 +10,8 @@
 
 __all__ = [
     'Context',
-    'create_context',
-    'get_context',
     'LayerListContextKeys',
     'LayerListSelectionContextKeys',
+    'create_context',
+    'get_context',
 ]
diff --git a/napari/_app_model/context/_context.py b/napari/_app_model/context/_context.py
index 9e186096899..6c3e213dd8b 100644
--- a/napari/_app_model/context/_context.py
+++ b/napari/_app_model/context/_context.py
@@ -14,7 +14,7 @@
 if TYPE_CHECKING:
     from napari.utils.events import Event
 
-__all__ = ['create_context', 'get_context', 'Context', 'SettingsAwareContext']
+__all__ = ['Context', 'SettingsAwareContext', 'create_context', 'get_context']
 
 
 class ContextMapping(collections.abc.Mapping):
diff --git a/napari/_app_model/context/_layerlist_context.py b/napari/_app_model/context/_layerlist_context.py
index 1a27d602bd4..adf416dd86b 100644
--- a/napari/_app_model/context/_layerlist_context.py
+++ b/napari/_app_model/context/_layerlist_context.py
@@ -163,7 +163,7 @@ def _active_is_image_3d(s: LayerSel) -> bool:
     return (
         _active_type(s) == 'image'
         and _activ_ndim is not None
-        and (_activ_ndim > 3 or (_activ_ndim) > 2 and not _is_rgb(s))
+        and (_activ_ndim > 3 or ((_activ_ndim) > 2 and not _is_rgb(s)))
     )
 
 
diff --git a/napari/_app_model/utils.py b/napari/_app_model/utils.py
index 8e56312ee63..3d0ca815f25 100644
--- a/napari/_app_model/utils.py
+++ b/napari/_app_model/utils.py
@@ -3,12 +3,44 @@
 from app_model.expressions import parse_expression
 from app_model.types import Action, MenuItem, SubmenuItem
 
-from napari._app_model import get_app
+from napari._app_model import get_app_model
 from napari._app_model.constants import MenuGroup, MenuId
 
 MenuOrSubmenu = Union[MenuItem, SubmenuItem]
 
 
+def to_id_key(menu_path: str) -> str:
+    """Return final part of the menu path.
+
+    Parameters
+    ----------
+    menu_path : str
+        full string delineating the menu path
+
+    Returns
+    -------
+    str
+        final part of the menu path
+    """
+    return menu_path.split('/')[-1]
+
+
+def to_action_id(id_key: str) -> str:
+    """Return dummy action ID for the given id_key.
+
+    Parameters
+    ----------
+    id_key : str
+        key to use in action ID
+
+    Returns
+    -------
+    str
+        dummy action ID
+    """
+    return f'napari.{id_key}.empty_dummy'
+
+
 def contains_dummy_action(menu_items: list[MenuOrSubmenu]) -> bool:
     """True if one of the menu_items is the dummy action, otherwise False.
 
@@ -41,16 +73,15 @@ def is_empty_menu(menu_id: str) -> bool:
     bool
         True if the given menu_id is empty, otherwise False
     """
-    app = get_app()
+    app = get_app_model()
     if menu_id not in app.menus:
         return True
     if len(app.menus.get_menu(menu_id)) == 0:
         return True
-    if len(app.menus.get_menu(menu_id)) == 1 and contains_dummy_action(
-        app.menus.get_menu(menu_id)
-    ):
-        return True
-    return False
+    return bool(
+        len(app.menus.get_menu(menu_id)) == 1
+        and contains_dummy_action(app.menus.get_menu(menu_id))
+    )
 
 
 def no_op() -> None:
@@ -78,9 +109,9 @@ def get_dummy_action(menu_id: MenuId) -> tuple[Action, str]:
     # menu path is unique, otherwise, we will clash. Once we
     # move to using short menu keys, the key itself will be used
     # here and this will no longer be a concern.
-    id_key = menu_id.split('/')[-1]
+    id_key = to_id_key(menu_id)
     action = Action(
-        id=f'napari.{id_key}.empty_dummy',
+        id=to_action_id(id_key),
         title='Empty',
         callback=no_op,
         menus=[
diff --git a/napari/_check_numpy_version.py b/napari/_check_numpy_version.py
new file mode 100644
index 00000000000..f423c296718
--- /dev/null
+++ b/napari/_check_numpy_version.py
@@ -0,0 +1,90 @@
+"""
+This module is used to prevent a known issue with numpy<2 on macOS arm64
+architecture installed from pypi wheels
+(https://github.com/numpy/numpy/issues/21799).
+
+We use a method to set thread limits based on the threadpoolctl package, but
+reimplemented locally to prevent adding the dependency to napari.
+
+Note: if any issues surface with the method below, we could fall back on
+depending on threadpoolctl directly.
+
+TODO: This module can be removed once the minimum numpy version is 2+.
+"""
+
+import ctypes
+import logging
+import os
+import platform
+from pathlib import Path
+
+import numpy as np
+from packaging.version import parse as parse_version
+
+# if run with numpy<2 on macOS arm64 architecture compiled from pypi wheels,
+# then it will crash with bus error if numpy is used in different thread
+# Issue reported https://github.com/numpy/numpy/issues/21799
+if (
+    parse_version(np.__version__) < parse_version('2')
+    and platform.system() == 'Darwin'
+    and platform.machine() == 'arm64'
+):  # pragma: no cover
+    try:
+        NUMPY_VERSION_IS_THREADSAFE = (
+            'cibw-run'
+            not in np.show_config('dicts')['Python Information']['path']  # type: ignore
+        )
+    except (KeyError, TypeError):
+        NUMPY_VERSION_IS_THREADSAFE = True
+else:
+    NUMPY_VERSION_IS_THREADSAFE = True
+
+
+def limit_numpy1x_threads_on_macos_arm() -> (
+    None
+):  # pragma: no cover (macos only code)
+    """Set openblas to use single thread on macOS arm64 to prevent numpy crash.
+
+    On NumPy version<2 wheels on macOS ARM64 architectures, a BusError is
+    raised, crashing Python, if NumPy is accessed from multiple threads.
+    (See https://github.com/numpy/numpy/issues/21799.) This function uses the
+    global check above (NUMPY_VERSION_IS_THREADSAFE), and, if False, it loads
+    the linked OpenBLAS library and sets the number of threads to 1. This has
+    performance implications but prevents nasty crashes, and anyway can be
+    avoided by using more recent versions of NumPy.
+
+    This function is loading openblas library from numpy and set number of threads to 1.
+    See also:
+        https://github.com/OpenMathLib/OpenBLAS/wiki/faq#how-can-i-use-openblas-in-multi-threaded-applications
+
+    These changes seem to be sufficient to prevent the crashes.
+    """
+    if NUMPY_VERSION_IS_THREADSAFE:
+        return
+    # find openblas library
+    numpy_dir = Path(np.__file__).parent
+    if not (numpy_dir / '.dylibs').exists():
+        logging.warning(
+            'numpy .dylibs directory not found during try to prevent numpy crash'
+        )
+    # Recent numpy versions are built with cibuildwheel.
+    # Internally, it uses delocate, which stores the openblas
+    # library in the .dylibs directory.
+    # Since we only patch numpy<2, we can just search for the libopenblas
+    # dynamic library at this location.
+    blas_lib = list((numpy_dir / '.dylibs').glob('libopenblas*.dylib'))
+    if not blas_lib:
+        logging.warning(
+            'libopenblas not found during try to prevent numpy crash'
+        )
+        return
+    blas = ctypes.CDLL(str(blas_lib[0]), mode=os.RTLD_NOLOAD)
+    for suffix in ('', '64_', '_64'):
+        openblas_set_num_threads = getattr(
+            blas, f'openblas_set_num_threads{suffix}', None
+        )
+        if openblas_set_num_threads is not None:
+            openblas_set_num_threads(1)
+            break
+    else:
+        logging.warning('openblas_set_num_threads not found')
diff --git a/napari/_pydantic_compat.py b/napari/_pydantic_compat.py
index d9142b24e5c..f64769cee4d 100644
--- a/napari/_pydantic_compat.py
+++ b/napari/_pydantic_compat.py
@@ -64,6 +64,8 @@
 Color = color.Color
 
 __all__ = (
+    'ROOT_KEY',
+    'SHAPE_LIST',
     'BaseModel',
     'BaseSettings',
     'ClassAttribute',
@@ -72,15 +74,13 @@
     'ErrorWrapper',
     'Extra',
     'Field',
-    'ModelField',
     'GenericModel',
+    'ModelField',
     'ModelMetaclass',
     'PositiveInt',
     'PrivateAttr',
-    'ROOT_KEY',
     'SettingsError',
     'SettingsSourceCallable',
-    'SHAPE_LIST',
     'ValidationError',
     'color',
     'conlist',
diff --git a/napari/_qt/__init__.py b/napari/_qt/__init__.py
index 69667652bc2..fa7329c94ac 100644
--- a/napari/_qt/__init__.py
+++ b/napari/_qt/__init__.py
@@ -87,7 +87,7 @@
     warn(message=warn_message, stacklevel=1)
 
 
-from napari._qt.qt_event_loop import get_app, gui_qt, quit_app, run
+from napari._qt.qt_event_loop import get_app, get_qapp, gui_qt, quit_app, run
 from napari._qt.qt_main_window import Window
 
-__all__ = ['get_app', 'gui_qt', 'quit_app', 'run', 'Window']
+__all__ = ['Window', 'get_app', 'get_qapp', 'gui_qt', 'quit_app', 'run']
diff --git a/napari/_qt/_qapp_model/_menus.py b/napari/_qt/_qapp_model/_menus.py
index 21e79c19af0..7d76e57d8da 100644
--- a/napari/_qt/_qapp_model/_menus.py
+++ b/napari/_qt/_qapp_model/_menus.py
@@ -16,7 +16,7 @@ def build_qmodel_menu(
     Parameters
     ----------
     menu_id : str
-        ID of a menu registered with napari._app_model.get_app().menus
+        ID of a menu registered with napari._app_model.get_app_model().menus
     title : Optional[str]
         Title of the menu
     parent : Optional[QWidget]
@@ -27,8 +27,8 @@ def build_qmodel_menu(
     QModelMenu
         QMenu subclass populated with all items in `menu_id` menu.
     """
-    from napari._app_model import get_app
+    from napari._app_model import get_app_model
 
     return QModelMenu(
-        menu_id=menu_id, app=get_app(), title=title, parent=parent
+        menu_id=menu_id, app=get_app_model(), title=title, parent=parent
     )
diff --git a/napari/_qt/_qapp_model/_tests/test_debug_menu.py b/napari/_qt/_qapp_model/_tests/test_debug_menu.py
index fd8826f7e9d..d3b1041edc0 100644
--- a/napari/_qt/_qapp_model/_tests/test_debug_menu.py
+++ b/napari/_qt/_qapp_model/_tests/test_debug_menu.py
@@ -3,7 +3,7 @@
 
 import pytest
 
-from napari._app_model import get_app
+from napari._app_model import get_app_model
 from napari._qt._qapp_model._tests.utils import get_submenu_action
 
 
@@ -80,7 +80,7 @@ def test_start_stop_trace_actions(
     use_perfmon = perfmon_activation
     if use_perfmon:
         trace_file = tmp_path / 'trace.json'
-        app = get_app()
+        app = get_app_model()
         viewer = make_napari_viewer()
 
         # Check Debug menu exists and actions state
@@ -132,6 +132,9 @@ def assert_stop_recording():
 
         # Stop perf widget timer to prevent test failure on teardown
         viewer.window._qt_viewer.dockPerformance.widget().timer.stop()
+        qtbot.waitUntil(
+            lambda: not viewer.window._qt_viewer.dockPerformance.widget().timer.isActive()
+        )
     else:
         # Nothing to test
         pytest.skip('Perfmon is disabled')
diff --git a/napari/_qt/_qapp_model/_tests/test_dummy_actions.py b/napari/_qt/_qapp_model/_tests/test_dummy_actions.py
new file mode 100644
index 00000000000..e4eca46a560
--- /dev/null
+++ b/napari/_qt/_qapp_model/_tests/test_dummy_actions.py
@@ -0,0 +1,23 @@
+from napari._app_model.constants._menus import MenuId
+from napari._app_model.utils import to_id_key
+
+
+def assert_empty_keys_in_context(viewer):
+    context = viewer.window._qt_viewer._layers.model().sourceModel()._root._ctx
+    for menu_id in MenuId.contributables():
+        context_key = f'{to_id_key(menu_id)}_empty'
+        assert context_key in context
+
+
+def test_menu_viewer_relaunch(make_napari_viewer):
+    viewer = make_napari_viewer()
+    assert_empty_keys_in_context(viewer)
+    viewer.close()
+
+    viewer2 = make_napari_viewer()
+    # prior to #7106, this would fail
+    assert_empty_keys_in_context(viewer2)
+    viewer2.close()
+
+    # prior to #7106, creating this viewer would error
+    make_napari_viewer()
diff --git a/napari/_qt/_qapp_model/_tests/test_file_menu.py b/napari/_qt/_qapp_model/_tests/test_file_menu.py
index 9d53ef976c5..6221d7f3173 100644
--- a/napari/_qt/_qapp_model/_tests/test_file_menu.py
+++ b/napari/_qt/_qapp_model/_tests/test_file_menu.py
@@ -7,10 +7,11 @@
 from npe2.manifest.contributions import SampleDataURI
 from qtpy.QtGui import QGuiApplication
 
-from napari._app_model import get_app
+from napari._app_model import get_app_model
 from napari._app_model.constants import MenuId
 from napari._qt._qapp_model._tests.utils import get_submenu_action
 from napari.layers import Image
+from napari.plugins._tests.test_npe2 import mock_pm  # noqa: F401
 from napari.utils.action_manager import action_manager
 
 
@@ -34,7 +35,7 @@ def _(path): ...
         uri='some-path/some-file.tif',
     )
     tmp_plugin.manifest.contributions.sample_data = [my_sample]
-    app = get_app()
+    app = get_app_model()
     # Configures `app`, registers actions and initializes plugins
     make_napari_viewer()
     with mock.patch(
@@ -51,7 +52,7 @@ def test_plugin_display_name_use_for_multiple_samples(
     builtins,
 ):
     """Check 'display_name' used for submenu when plugin has >1 sample data."""
-    app = get_app()
+    app = get_app_model()
     viewer = make_napari_viewer()
 
     # builtins provides more than one sample,
@@ -73,7 +74,7 @@ def test_sample_menu_plugin_state_change(
 ):
     """Check samples submenu correct after plugin changes state."""
 
-    app = get_app()
+    app = get_app_model()
     pm = tmp_plugin.plugin_manager
     # Check no samples menu before plugin registration
     with pytest.raises(KeyError):
@@ -122,7 +123,7 @@ def test_sample_menu_single_data(
     tmp_plugin: DynamicPlugin,
 ):
     """Checks sample submenu correct when plugin has single sample data."""
-    app = get_app()
+    app = get_app_model()
     sample = SampleDataURI(
         key='tmp-sample-1',
         display_name='Temp Sample One',
@@ -139,6 +140,40 @@ def test_sample_menu_single_data(
     assert 'tmp_plugin:tmp-sample-1' in app.commands
 
 
+def test_sample_menu_sorted(
+    mock_pm,  # noqa: F811
+    mock_app_model,
+    tmp_plugin: DynamicPlugin,
+):
+    from napari._app_model import get_app_model
+    from napari.plugins import _initialize_plugins
+
+    # we make sure 'plugin-b' is registered first
+    tmp_plugin2 = tmp_plugin.spawn(name='plugin-b', register=True)
+    tmp_plugin1 = tmp_plugin.spawn(name='plugin-a', register=True)
+
+    @tmp_plugin1.contribute.sample_data(display_name='Sample 1')
+    def sample1(): ...
+
+    @tmp_plugin1.contribute.sample_data(display_name='Sample 2')
+    def sample2(): ...
+
+    @tmp_plugin2.contribute.sample_data(display_name='Sample 1')
+    def sample2_1(): ...
+
+    @tmp_plugin2.contribute.sample_data(display_name='Sample 2')
+    def sample2_2(): ...
+
+    _initialize_plugins()
+    samples_menu = list(get_app_model().menus.get_menu('napari/file/samples'))
+    submenus = [item for item in samples_menu if isinstance(item, SubmenuItem)]
+    assert len(submenus) == 3
+    # mock_pm registers a sample_manifest with two sample data contributions
+    assert submenus[0].title == 'My Plugin'
+    assert submenus[1].title == 'plugin-a'
+    assert submenus[2].title == 'plugin-b'
+
+
 def test_show_shortcuts_actions(make_napari_viewer):
     viewer = make_napari_viewer()
     assert viewer.window._pref_dialog is None
@@ -150,7 +185,7 @@ def test_show_shortcuts_actions(make_napari_viewer):
 
 def test_image_from_clipboard(make_napari_viewer):
     make_napari_viewer()
-    app = get_app()
+    app = get_app_model()
 
     # Ensure clipboard is empty
     QGuiApplication.clipboard().clear()
@@ -204,7 +239,7 @@ def test_open(
 ):
     """Test base `Open ...` actions can be triggered."""
     make_napari_viewer()
-    app = get_app()
+    app = get_app_model()
 
     # Check action command execution
     with (
@@ -278,7 +313,7 @@ def test_open_with_plugin(
 def test_preference_dialog(make_napari_viewer):
     """Test preferences action can be triggered."""
     make_napari_viewer()
-    app = get_app()
+    app = get_app_model()
 
     # Check action command execution
     with (
@@ -294,7 +329,7 @@ def test_preference_dialog(make_napari_viewer):
 
 def test_save_layers_enablement_updated_context(make_napari_viewer, builtins):
     """Test that enablement status of save layer actions updated correctly."""
-    get_app()
+    get_app_model()
     viewer = make_napari_viewer()
 
     save_layers_action = viewer.window.file_menu.findAction(
@@ -347,7 +382,7 @@ def test_save_layers(
 ):
     """Test save layer selected/all actions can be triggered."""
     viewer = make_napari_viewer()
-    app = get_app()
+    app = get_app_model()
 
     # Add selected layer
     layer = Image(np.random.random((10, 10)))
@@ -379,7 +414,7 @@ def test_screenshot(
 ):
     """Test screenshot actions can be triggered."""
     make_napari_viewer()
-    app = get_app()
+    app = get_app_model()
 
     # Check action command execution
     with mock.patch(patch_method) as mock_screenshot:
@@ -398,7 +433,7 @@ def test_screenshot(
 def test_screenshot_to_clipboard(make_napari_viewer, qtbot, action_id):
     """Test screenshot to clipboard actions can be triggered."""
     viewer = make_napari_viewer()
-    app = get_app()
+    app = get_app_model()
 
     # Add selected layer
     layer = Image(np.random.random((10, 10)))
@@ -436,7 +471,7 @@ def test_screenshot_to_clipboard(make_napari_viewer, qtbot, action_id):
 def test_restart(make_napari_viewer, action_id, patch_method):
     """Testrestart action can be triggered."""
     make_napari_viewer()
-    app = get_app()
+    app = get_app_model()
 
     # Check action command execution
     with mock.patch(patch_method) as mock_restart:
@@ -464,7 +499,7 @@ def test_restart(make_napari_viewer, action_id, patch_method):
 def test_close(make_napari_viewer, action_id, patch_method, method_params):
     """Test close/exit actions can be triggered."""
     make_napari_viewer()
-    app = get_app()
+    app = get_app_model()
     quit_app, confirm_need = method_params
 
     # Check action command execution
diff --git a/napari/_qt/_qapp_model/_tests/test_help_menu.py b/napari/_qt/_qapp_model/_tests/test_help_menu.py
index 7a10eb7ed55..3c821d127c3 100644
--- a/napari/_qt/_qapp_model/_tests/test_help_menu.py
+++ b/napari/_qt/_qapp_model/_tests/test_help_menu.py
@@ -6,7 +6,7 @@
 import pytest
 import requests
 
-from napari._app_model import get_app
+from napari._app_model import get_app_model
 from napari._qt._qapp_model.qactions._help import HELP_URLS
 
 
@@ -29,7 +29,7 @@ def test_help_urls(url):
     else ['napari.window.help.info'],
 )
 def test_about_action(make_napari_viewer, action_id):
-    app = get_app()
+    app = get_app_model()
     viewer = make_napari_viewer()
 
     with mock.patch(
diff --git a/napari/_qt/_qapp_model/_tests/test_layerlist_context_actions.py b/napari/_qt/_qapp_model/_tests/test_layerlist_context_actions.py
index 1e9b1112cbd..1aea2c3bd01 100644
--- a/napari/_qt/_qapp_model/_tests/test_layerlist_context_actions.py
+++ b/napari/_qt/_qapp_model/_tests/test_layerlist_context_actions.py
@@ -1,6 +1,6 @@
 import pytest
 
-from napari._app_model import get_app
+from napari._app_model import get_app_model
 from napari._app_model.actions._layerlist_context_actions import (
     LAYERLIST_CONTEXT_ACTIONS,
 )
@@ -20,13 +20,18 @@ def test_layer_actions_ctx_menu_execute_command(
         To check a set of functional tests related to these actions you can
         see: https://github.com/napari/napari/blob/main/napari/layers/_tests/test_layer_actions.py
     """
-    app = get_app()
+    app = get_app_model()
     make_napari_viewer()
     command_id = layer_action.id
 
     if command_id == 'napari.layer.merge_stack':
         with pytest.raises(IndexError, match=r'images list is empty'):
             app.commands.execute_command(command_id)
+    elif command_id == 'napari.layer.merge_rgb':
+        with pytest.raises(
+            ValueError, match='Merging to RGB requires exactly 3 Image'
+        ):
+            app.commands.execute_command(command_id)
     elif command_id in [
         'napari.layer.link_selected_layers',
         'napari.layer.unlink_selected_layers',
diff --git a/napari/_qt/_qapp_model/_tests/test_plugins_menu.py b/napari/_qt/_qapp_model/_tests/test_plugins_menu.py
index 02bb3543b88..6cb5ac480f3 100644
--- a/napari/_qt/_qapp_model/_tests/test_plugins_menu.py
+++ b/napari/_qt/_qapp_model/_tests/test_plugins_menu.py
@@ -6,11 +6,12 @@
 from npe2 import DynamicPlugin
 from qtpy.QtWidgets import QWidget
 
-from napari._app_model import get_app
+from napari._app_model import get_app_model
 from napari._app_model.constants import MenuId
 from napari._qt._qapp_model.qactions import _plugins, init_qactions
 from napari._qt._qplugins._qnpe2 import _toggle_or_get_widget
 from napari._tests.utils import skip_local_popups
+from napari.plugins._tests.test_npe2 import mock_pm  # noqa: F401
 
 
 class DummyWidget(QWidget):
@@ -27,7 +28,7 @@ def test_plugin_manager_action(make_napari_viewer):
 
     The test is skipped in case `napari_plugin_manager` is not available
     """
-    app = get_app()
+    app = get_app_model()
     viewer = make_napari_viewer()
 
     with mock.patch(
@@ -42,7 +43,7 @@ def test_plugin_manager_action(make_napari_viewer):
 def test_plugin_errors_action(make_napari_viewer):
     """Test plugin errors action."""
     make_napari_viewer()
-    app = get_app()
+    app = get_app_model()
 
     with mock.patch(
         'napari._qt._qapp_model.qactions._plugins.QtPluginErrReporter.exec_'
@@ -68,7 +69,7 @@ def test_toggle_or_get_widget(
     def widget1():
         return DummyWidget()
 
-    app = get_app()
+    app = get_app_model()
     # Viewer needs to be visible
     viewer = make_napari_viewer(show=True)
 
@@ -111,7 +112,7 @@ def test_plugin_single_widget_menu(
     def widget1():
         return DummyWidget()
 
-    app = get_app()
+    app = get_app_model()
     viewer = make_napari_viewer()
 
     assert tmp_plugin.display_name == 'Temp Plugin'
@@ -139,7 +140,7 @@ def widget1():
     def widget2():
         return DummyWidget()
 
-    app = get_app()
+    app = get_app_model()
     viewer = make_napari_viewer()
 
     assert tmp_plugin.display_name == 'Temp Plugin'
@@ -160,7 +161,7 @@ def test_plugin_menu_plugin_state_change(
     tmp_plugin: DynamicPlugin,
 ):
     """Check plugin menu items correct after a plugin changes state."""
-    app = get_app()
+    app = get_app_model()
     pm = tmp_plugin.plugin_manager
 
     # Register plugin q actions
@@ -214,7 +215,7 @@ def test_plugin_widget_checked(
     def widget_contrib():
         return DummyWidget()
 
-    app = get_app()
+    app = get_app_model()
     viewer = make_napari_viewer()
 
     assert 'tmp_plugin:Widget' in app.commands
@@ -273,3 +274,39 @@ def mockreturn(*args):
         'napari.window.plugins.plugin_install_dialog',
     )
     assert not plugin_install_action.isVisible()
+
+
+def test_plugins_menu_sorted(
+    mock_pm,  # noqa: F811
+    mock_app_model,
+    tmp_plugin: DynamicPlugin,
+):
+    from napari._app_model import get_app_model
+    from napari.plugins import _initialize_plugins
+
+    # we make sure 'plugin-b' is registered first
+    tmp_plugin2 = tmp_plugin.spawn(
+        name='plugin-b', plugin_manager=mock_pm, register=True
+    )
+    tmp_plugin1 = tmp_plugin.spawn(
+        name='plugin-a', plugin_manager=mock_pm, register=True
+    )
+
+    @tmp_plugin1.contribute.widget(display_name='Widget 1')
+    def widget1(): ...
+
+    @tmp_plugin1.contribute.widget(display_name='Widget 2')
+    def widget2(): ...
+
+    @tmp_plugin2.contribute.widget(display_name='Widget 1')
+    def widget2_1(): ...
+
+    @tmp_plugin2.contribute.widget(display_name='Widget 2')
+    def widget2_2(): ...
+
+    _initialize_plugins()
+    plugins_menu = list(get_app_model().menus.get_menu('napari/plugins'))
+    submenus = [item for item in plugins_menu if isinstance(item, SubmenuItem)]
+    assert len(submenus) == 2
+    assert submenus[0].title == 'plugin-a'
+    assert submenus[1].title == 'plugin-b'
diff --git a/napari/_qt/_qapp_model/_tests/test_processors.py b/napari/_qt/_qapp_model/_tests/test_processors.py
index cfb56d33225..43b348c30c5 100644
--- a/napari/_qt/_qapp_model/_tests/test_processors.py
+++ b/napari/_qt/_qapp_model/_tests/test_processors.py
@@ -1,16 +1,61 @@
-from typing import Union
+from typing import Optional, Union
 from unittest.mock import MagicMock
 
 import numpy as np
 import pytest
+from qtpy.QtWidgets import QWidget
 
 from napari._qt._qapp_model.injection._qprocessors import (
+    _add_future_data,
     _add_layer_data_to_viewer,
+    _add_layer_data_tuples_to_viewer,
+    _add_layer_to_viewer,
+    _add_plugin_dock_widget,
 )
+from napari.components import ViewerModel
+from napari.layers import Image
 from napari.types import ImageData, LabelsData
 
 
-def test_add_layer_data_to_viewer():
+def test_add_plugin_dock_widget(qtbot):
+    widget = QWidget()
+    viewer = MagicMock()
+    qtbot.addWidget(widget)
+    with pytest.raises(RuntimeError, match='No current `Viewer` found.'):
+        _add_plugin_dock_widget((widget, 'widget'))
+    _add_plugin_dock_widget((widget, 'widget'), viewer)
+    viewer.window.add_dock_widget.assert_called_with(widget, name='widget')
+
+
+def test_add_layer_data_tuples_to_viewer_invalid_data():
+    viewer = MagicMock()
+    error_data = (np.zeros((10, 10)), np.zeros((10, 10)))
+    with pytest.raises(
+        TypeError, match='Not a valid list of layer data tuples!'
+    ):
+        _add_layer_data_tuples_to_viewer(
+            data=error_data,
+            return_type=Union[ImageData, LabelsData],
+            viewer=viewer,
+        )
+
+
+def test_add_layer_data_tuples_to_viewer_valid_data():
+    viewer = ViewerModel()
+    valid_data = [
+        (np.zeros((10, 10)), {'name': 'layer1'}, 'image'),
+        (np.zeros((10, 20)), {'name': 'layer1'}, 'image'),
+    ]
+    _add_layer_data_tuples_to_viewer(
+        data=valid_data,
+        return_type=Union[ImageData, LabelsData],
+        viewer=viewer,
+    )
+    assert len(viewer.layers) == 1
+    assert np.array_equal(viewer.layers[0].data, np.zeros((10, 20)))
+
+
+def test_add_layer_data_to_viewer_return_type():
     v = MagicMock()
     with pytest.raises(TypeError, match='napari supports only Optional'):
         _add_layer_data_to_viewer(
@@ -18,3 +63,50 @@ def test_add_layer_data_to_viewer():
             return_type=Union[ImageData, LabelsData],
             viewer=v,
         )
+    _add_layer_data_to_viewer(
+        data=np.zeros((10, 10)),
+        return_type=Optional[ImageData],
+        viewer=v,
+    )
+    v.add_image.assert_called_once()
+
+
+def test_add_layer_data_to_viewer():
+    viewer = ViewerModel()
+    _add_layer_data_to_viewer(
+        data=np.zeros((10, 10)),
+        return_type=Optional[ImageData],
+        viewer=viewer,
+        layer_name='layer1',
+    )
+    assert len(viewer.layers) == 1
+    assert np.array_equal(viewer.layers[0].data, np.zeros((10, 10)))
+    _add_layer_data_to_viewer(
+        data=np.zeros((10, 20)),
+        return_type=Optional[ImageData],
+        viewer=viewer,
+        layer_name='layer1',
+    )
+    assert len(viewer.layers) == 1
+    assert np.array_equal(viewer.layers[0].data, np.zeros((10, 20)))
+
+
+def test_add_layer_to_viewer():
+    layer1 = Image(np.zeros((10, 10)))
+    layer2 = Image(np.zeros((10, 10)))
+    viewer = ViewerModel()
+    _add_layer_to_viewer(None)
+    assert len(viewer.layers) == 0
+    _add_layer_to_viewer(layer1, viewer=viewer)
+    assert len(viewer.layers) == 1
+    _add_layer_to_viewer(layer2, source={'parent': layer1}, viewer=viewer)
+    assert len(viewer.layers) == 2
+    assert layer2._source.parent == layer1
+
+
+def test_add_future_data():
+    future = MagicMock()
+    viewer = MagicMock()
+    _add_future_data(future, Union[ImageData, LabelsData])
+    _add_future_data(future, Union[ImageData, LabelsData], viewer=viewer)
+    assert future.add_done_callback.call_count == 2
diff --git a/napari/_qt/_qapp_model/_tests/test_qaction_layer.py b/napari/_qt/_qapp_model/_tests/test_qaction_layer.py
index 5315ab05c48..c9ff66c8975 100644
--- a/napari/_qt/_qapp_model/_tests/test_qaction_layer.py
+++ b/napari/_qt/_qapp_model/_tests/test_qaction_layer.py
@@ -20,7 +20,7 @@
 from napari.utils.transforms import Affine
 
 
-@pytest.fixture()
+@pytest.fixture
 def layer_list():
     layer_1 = SampleLayer(
         data=np.empty((10, 10)),
@@ -55,7 +55,7 @@ def layer_list():
     return ll
 
 
-@pytest.fixture()
+@pytest.fixture
 def layer_list_dim():
     layer_1 = SampleLayer(
         data=np.empty((5, 10, 10)),
diff --git a/napari/_qt/_qapp_model/_tests/test_qapp_model_menus.py b/napari/_qt/_qapp_model/_tests/test_qapp_model_menus.py
index f8ed8392a10..7864381ad06 100644
--- a/napari/_qt/_qapp_model/_tests/test_qapp_model_menus.py
+++ b/napari/_qt/_qapp_model/_tests/test_qapp_model_menus.py
@@ -2,7 +2,7 @@
 import pytest
 from app_model.types import Action
 
-from napari._app_model import get_app
+from napari._app_model import get_app_model
 from napari._app_model.constants import MenuId
 from napari._app_model.context import LayerListContextKeys as LLCK
 from napari._qt._qapp_model import build_qmodel_menu
@@ -13,7 +13,7 @@
 @pytest.mark.parametrize('menu_id', list(MenuId))
 def test_build_qmodel_menu(builtins, make_napari_viewer, qtbot, menu_id):
     """Test that we can build qmenus for all registered menu IDs."""
-    app = get_app()
+    app = get_app_model()
 
     # Configures `app`, registers actions and initializes plugins
     make_napari_viewer()
@@ -29,7 +29,7 @@ def test_build_qmodel_menu(builtins, make_napari_viewer, qtbot, menu_id):
 
 def test_update_menu_state_context(make_napari_viewer):
     """Test `_update_menu_state` correctly updates enabled/visible state."""
-    app = get_app()
+    app = get_app_model()
     viewer = make_napari_viewer()
 
     action = Action(
diff --git a/napari/_qt/_qapp_model/_tests/test_qproviders.py b/napari/_qt/_qapp_model/_tests/test_qproviders.py
index cb77a319521..b6935708eb4 100644
--- a/napari/_qt/_qapp_model/_tests/test_qproviders.py
+++ b/napari/_qt/_qapp_model/_tests/test_qproviders.py
@@ -4,7 +4,7 @@
 import pytest
 from app_model.types import Action
 
-from napari._app_model._app import get_app
+from napari._app_model._app import get_app_model
 from napari._qt._qapp_model.injection._qproviders import (
     _provide_active_layer,
     _provide_active_layer_list,
@@ -48,7 +48,7 @@ def my_viewer(viewer: Viewer) -> Viewer:
         title='some title',
         callback=my_viewer,
     )
-    app = get_app()
+    app = get_app_model()
     app.register_action(action)
     app.commands.execute_command('some.command.id')
     captured = capsys.readouterr()
diff --git a/napari/_qt/_qapp_model/_tests/test_togglers.py b/napari/_qt/_qapp_model/_tests/test_togglers.py
index 6bcb0c97e8d..1b0bd9cd526 100644
--- a/napari/_qt/_qapp_model/_tests/test_togglers.py
+++ b/napari/_qt/_qapp_model/_tests/test_togglers.py
@@ -1,5 +1,5 @@
 from napari import Viewer
-from napari._app_model._app import get_app
+from napari._app_model._app import get_app_model
 from napari._qt._qapp_model.qactions._toggle_action import (
     DockWidgetToggleAction,
     ViewerToggleAction,
@@ -8,7 +8,7 @@
 from napari.components import ViewerModel
 
 
-def test_viewer_toggler(mock_app):
+def test_viewer_toggler(mock_app_model):
     viewer = ViewerModel()
     action = ViewerToggleAction(
         id='some.command.id',
@@ -16,7 +16,7 @@ def test_viewer_toggler(mock_app):
         viewer_attribute='axes',
         sub_attribute='visible',
     )
-    app = get_app()
+    app = get_app_model()
     app.register_action(action)
 
     # Injection required as there is no current viewer, use a high weight (100)
@@ -41,7 +41,7 @@ def test_dock_widget_toggler(make_napari_viewer):
         title='Toggle Dock Widget',
         dock_widget='dockConsole',
     )
-    app = get_app()
+    app = get_app_model()
     app.register_action(action)
 
     with app.injection_store.register(
diff --git a/napari/_qt/_qapp_model/_tests/test_view_menu.py b/napari/_qt/_qapp_model/_tests/test_view_menu.py
index df553e47a9d..699a0f07e69 100644
--- a/napari/_qt/_qapp_model/_tests/test_view_menu.py
+++ b/napari/_qt/_qapp_model/_tests/test_view_menu.py
@@ -1,10 +1,12 @@
+import os
 import sys
 
 import numpy as np
 import pytest
 from qtpy.QtCore import QPoint, Qt
+from qtpy.QtWidgets import QApplication
 
-from napari._app_model import get_app
+from napari._app_model import get_app_model
 from napari._qt._qapp_model.qactions._view import (
     _get_current_tooltip_visibility,
     toggle_action_details,
@@ -12,6 +14,31 @@
 from napari._tests.utils import skip_local_focus, skip_local_popups
 
 
+def check_windows_style(viewer):
+    if os.name != 'nt':
+        return
+    import win32con
+    import win32gui
+
+    window_handle = viewer.window._qt_window.windowHandle()
+    window_handle_id = int(window_handle.winId())
+    window_style = win32gui.GetWindowLong(window_handle_id, win32con.GWL_STYLE)
+    assert window_style & win32con.WS_BORDER == win32con.WS_BORDER
+
+
+def check_view_menu_visibility(viewer, qtbot):
+    if viewer.window._qt_window.menuBar().isNativeMenuBar():
+        return
+
+    assert not viewer.window.view_menu.isVisible()
+    qtbot.keyClick(
+        viewer.window._qt_window.menuBar(), Qt.Key_V, modifier=Qt.AltModifier
+    )
+    qtbot.waitUntil(viewer.window.view_menu.isVisible)
+    viewer.window.view_menu.close()
+    assert not viewer.window.view_menu.isVisible()
+
+
 @pytest.mark.parametrize(
     ('action_id', 'action_title', 'viewer_attr', 'sub_attr'),
     toggle_action_details,
@@ -33,7 +60,7 @@ def test_toggle_axes_scale_bar_attr(
         * `colored`
         * `ticks`
     """
-    app = get_app()
+    app = get_app_model()
     viewer = make_napari_viewer()
 
     # Get viewer attribute to check (`axes` or `scale_bar`)
@@ -49,21 +76,35 @@ def test_toggle_axes_scale_bar_attr(
 
 
 @skip_local_popups
-def test_toggle_fullscreen(make_napari_viewer, qtbot):
-    """Test toggle fullscreen action."""
+@pytest.mark.qt_log_level_fail('WARNING')
+def test_toggle_fullscreen_from_normal(make_napari_viewer, qtbot):
+    """
+    Test toggle fullscreen action from normal window state.
+
+    Check that toggling from a normal state can be done without
+    generating any type of warning and menu bar elements are visible in any
+    window state.
+    """
     action_id = 'napari.window.view.toggle_fullscreen'
-    app = get_app()
+    app = get_app_model()
     viewer = make_napari_viewer(show=True)
 
     # Check initial default state (no fullscreen)
     assert not viewer.window._qt_window.isFullScreen()
 
+    # Check `View` menu can be seen in normal window state
+    check_view_menu_visibility(viewer, qtbot)
+
     # Check fullscreen state change
     app.commands.execute_command(action_id)
     if sys.platform == 'darwin':
         # On macOS, wait for the animation to complete
         qtbot.wait(250)
     assert viewer.window._qt_window.isFullScreen()
+    check_windows_style(viewer)
+
+    # Check `View` menu can be seen in fullscreen window state
+    check_view_menu_visibility(viewer, qtbot)
 
     # Check return to non fullscreen state
     app.commands.execute_command(action_id)
@@ -71,6 +112,58 @@ def test_toggle_fullscreen(make_napari_viewer, qtbot):
         # On macOS, wait for the animation to complete
         qtbot.wait(250)
     assert not viewer.window._qt_window.isFullScreen()
+    check_windows_style(viewer)
+
+    # Check `View` still menu can be seen in non fullscreen window state
+    check_view_menu_visibility(viewer, qtbot)
+
+
+@skip_local_popups
+@pytest.mark.qt_log_level_fail('WARNING')
+def test_toggle_fullscreen_from_maximized(make_napari_viewer, qtbot):
+    """
+    Test toggle fullscreen action from maximized window state.
+
+    Check that toggling from a maximized state can be done without
+    generating any type of warning and menu bar elements are visible in any
+    window state.
+    """
+    action_id = 'napari.window.view.toggle_fullscreen'
+    app = get_app_model()
+    viewer = make_napari_viewer(show=True)
+
+    # Check fullscreen state change while maximized
+    assert not viewer.window._qt_window.isMaximized()
+    viewer.window._qt_window.showMaximized()
+
+    # Check `View` menu can be seen in maximized window state
+    check_view_menu_visibility(viewer, qtbot)
+
+    # Check fullscreen state change
+    app.commands.execute_command(action_id)
+    if sys.platform == 'darwin':
+        # On macOS, wait for the animation to complete
+        qtbot.wait(250)
+    assert viewer.window._qt_window.isFullScreen()
+    check_windows_style(viewer)
+
+    # Check `View` menu can be seen in fullscreen window state coming from maximized state
+    check_view_menu_visibility(viewer, qtbot)
+
+    # Check return to non fullscreen state
+    app.commands.execute_command(action_id)
+    if sys.platform == 'darwin':
+        # On macOS, wait for the animation to complete
+        qtbot.wait(250)
+
+    def check_not_fullscreen():
+        assert not viewer.window._qt_window.isFullScreen()
+
+    qtbot.waitUntil(check_not_fullscreen)
+    check_windows_style(viewer)
+
+    # Check `View` still menu can be seen in non fullscreen window state
+    check_view_menu_visibility(viewer, qtbot)
 
 
 @skip_local_focus
@@ -86,7 +179,7 @@ def test_toggle_menubar(make_napari_viewer, qtbot):
     toggle action doesn't exist/isn't enabled there.
     """
     action_id = 'napari.window.view.toggle_menubar'
-    app = get_app()
+    app = get_app_model()
     viewer = make_napari_viewer(show=True)
 
     # Check initial state (visible menubar)
@@ -116,7 +209,7 @@ def test_toggle_menubar(make_napari_viewer, qtbot):
 def test_toggle_play(make_napari_viewer, qtbot):
     """Test toggle play action."""
     action_id = 'napari.window.view.toggle_play'
-    app = get_app()
+    app = get_app_model()
     viewer = make_napari_viewer()
 
     # Check action on empty viewer
@@ -133,7 +226,11 @@ def test_toggle_play(make_napari_viewer, qtbot):
     app.commands.execute_command(action_id)
     qtbot.waitUntil(lambda: viewer.window._qt_viewer.dims.is_playing)
     # Assert action stops play
-    app.commands.execute_command(action_id)
+    with qtbot.waitSignal(
+        viewer.window._qt_viewer.dims._animation_thread.finished
+    ):
+        app.commands.execute_command(action_id)
+        QApplication.processEvents()
     qtbot.waitUntil(lambda: not viewer.window._qt_viewer.dims.is_playing)
 
 
@@ -141,7 +238,7 @@ def test_toggle_play(make_napari_viewer, qtbot):
 def test_toggle_activity_dock(make_napari_viewer):
     """Test toggle activity dock"""
     action_id = 'napari.window.view.toggle_activity_dock'
-    app = get_app()
+    app = get_app_model()
     viewer = make_napari_viewer(show=True)
 
     # Check initial activity dock state (hidden)
@@ -172,7 +269,7 @@ def test_toggle_layer_tooltips(make_napari_viewer, qtbot):
     """Test toggle layer tooltips"""
     make_napari_viewer()
     action_id = 'napari.window.view.toggle_layer_tooltips'
-    app = get_app()
+    app = get_app_model()
 
     # Check initial layer tooltip visibility settings state (False)
     assert not _get_current_tooltip_visibility()
@@ -184,3 +281,36 @@ def test_toggle_layer_tooltips(make_napari_viewer, qtbot):
     # Restore layer tooltip visibility
     app.commands.execute_command(action_id)
     assert not _get_current_tooltip_visibility()
+
+
+def test_zoom_actions(make_napari_viewer):
+    """Test zoom actions"""
+    viewer = make_napari_viewer()
+    app = get_app_model()
+
+    viewer.add_image(np.ones((10, 10, 10)))
+
+    # get initial zoom state
+    initial_zoom = viewer.camera.zoom
+
+    # Check zoom in action
+    app.commands.execute_command('napari.viewer.camera.zoom_in')
+    assert viewer.camera.zoom == pytest.approx(1.5 * initial_zoom)
+
+    # Check zoom out action
+    app.commands.execute_command('napari.viewer.camera.zoom_out')
+    assert viewer.camera.zoom == pytest.approx(initial_zoom)
+
+    viewer.camera.zoom = 2
+    # Check reset zoom action
+    app.commands.execute_command('napari.viewer.fit_to_view')
+    assert viewer.camera.zoom == pytest.approx(initial_zoom)
+
+    # Check that angle is preserved
+    viewer.dims.ndisplay = 3
+    viewer.camera.angles = (90, 0, 0)
+    viewer.camera.zoom = 2
+    app.commands.execute_command('napari.viewer.fit_to_view')
+    # Zoom should be reset, but angle unchanged
+    assert viewer.camera.zoom == pytest.approx(initial_zoom)
+    assert viewer.camera.angles == (90, 0, 0)
diff --git a/napari/_qt/_qapp_model/_tests/test_window_menu.py b/napari/_qt/_qapp_model/_tests/test_window_menu.py
index 7325c2c36a6..67ed5cc2f60 100644
--- a/napari/_qt/_qapp_model/_tests/test_window_menu.py
+++ b/napari/_qt/_qapp_model/_tests/test_window_menu.py
@@ -1,6 +1,6 @@
 import pytest
 
-from napari._app_model import get_app
+from napari._app_model import get_app_model
 from napari._qt._qapp_model.qactions._window import toggle_action_details
 from napari._tests.utils import skip_local_popups
 
@@ -22,7 +22,7 @@ def test_toggle_dockwidget_actions(
     action_dockwidget_name,
     action_status_tooltip,
 ):
-    app = get_app()
+    app = get_app_model()
     viewer = make_napari_viewer(show=True)
     widget = getattr(viewer.window._qt_viewer, action_dockwidget_name)
     widget_initial_visibility = widget.isVisible()
diff --git a/napari/_qt/_qapp_model/injection/_qprocessors.py b/napari/_qt/_qapp_model/injection/_qprocessors.py
index 601efa43edb..138a0625299 100644
--- a/napari/_qt/_qapp_model/injection/_qprocessors.py
+++ b/napari/_qt/_qapp_model/injection/_qprocessors.py
@@ -125,7 +125,7 @@ def _add_layer_to_viewer(
         viewer.add_layer(layer)
 
 
-# here to prevent garbace collection of the future object while processing.
+# here to prevent garbage collection of the future object while processing.
 _FUTURES: set[Future] = set()
 
 
diff --git a/napari/_qt/_qapp_model/qactions/__init__.py b/napari/_qt/_qapp_model/qactions/__init__.py
index d7ab69a26a1..0e3644a759b 100644
--- a/napari/_qt/_qapp_model/qactions/__init__.py
+++ b/napari/_qt/_qapp_model/qactions/__init__.py
@@ -29,7 +29,7 @@ def init_qactions() -> None:
     - registering provider functions for the names added to the namespace
     - registering Qt-dependent actions with app-model (i.e. Q_*_ACTIONS actions).
     """
-    from napari._app_model import get_app
+    from napari._app_model import get_app_model
     from napari._qt._qapp_model.qactions._debug import (
         DEBUG_SUBMENUS,
         Q_DEBUG_ACTIONS,
@@ -56,7 +56,7 @@ def init_qactions() -> None:
     from napari._qt.qt_viewer import QtViewer
 
     # update the namespace with the Qt-specific types/providers/processors
-    app = get_app()
+    app = get_app_model()
     store = app.injection_store
     store.namespace = {
         **store.namespace,
@@ -105,16 +105,19 @@ def add_dummy_actions(context: Context) -> None:
     context : Context
         context to store functional keys used in `when` conditions
     """
-    from napari._app_model import get_app
+    from napari._app_model import get_app_model
     from napari._app_model.constants._menus import MenuId
     from napari._app_model.utils import get_dummy_action, is_empty_menu
 
-    app = get_app()
+    app = get_app_model()
 
     actions = []
     for menu_id in MenuId.contributables():
         dummmy_action, context_key = get_dummy_action(menu_id)
         if dummmy_action.id not in app.commands:
             actions.append(dummmy_action)
-            context[context_key] = partial(is_empty_menu, menu_id)
+        # NOTE: even if action is already registered, the `context` instance
+        # may be new e.g. when closing and relaunching a viewer
+        # in a notebook. Context key should be assigned regardless
+        context[context_key] = partial(is_empty_menu, menu_id)
     app.register_actions(actions)
diff --git a/napari/_qt/_qapp_model/qactions/_help.py b/napari/_qt/_qapp_model/qactions/_help.py
index b29d1df1d09..40ba1fad8ca 100644
--- a/napari/_qt/_qapp_model/qactions/_help.py
+++ b/napari/_qt/_qapp_model/qactions/_help.py
@@ -2,7 +2,7 @@
 
 import sys
 from functools import partial
-from webbrowser import open
+from webbrowser import open as web_open
 
 from app_model.types import Action, KeyBindingRule, KeyCode, KeyMod
 from packaging.version import parse
@@ -56,31 +56,31 @@ def _show_about(window: Window):
     Action(
         id='napari.window.help.getting_started',
         title=trans._('Getting started'),
-        callback=partial(open, url=HELP_URLS['getting_started']),
+        callback=partial(web_open, url=HELP_URLS['getting_started']),
         menus=[{'id': MenuId.MENUBAR_HELP}],
     ),
     Action(
         id='napari.window.help.tutorials',
         title=trans._('Tutorials'),
-        callback=partial(open, url=HELP_URLS['tutorials']),
+        callback=partial(web_open, url=HELP_URLS['tutorials']),
         menus=[{'id': MenuId.MENUBAR_HELP}],
     ),
     Action(
         id='napari.window.help.layers_guide',
         title=trans._('Using Layers Guides'),
-        callback=partial(open, url=HELP_URLS['layers_guide']),
+        callback=partial(web_open, url=HELP_URLS['layers_guide']),
         menus=[{'id': MenuId.MENUBAR_HELP}],
     ),
     Action(
         id='napari.window.help.examples',
         title=trans._('Examples Gallery'),
-        callback=partial(open, url=HELP_URLS['examples_gallery']),
+        callback=partial(web_open, url=HELP_URLS['examples_gallery']),
         menus=[{'id': MenuId.MENUBAR_HELP}],
     ),
     Action(
         id='napari.window.help.release_notes',
         title=trans._('Release Notes'),
-        callback=partial(open, url=HELP_URLS['release_notes']),
+        callback=partial(web_open, url=HELP_URLS['release_notes']),
         menus=[
             {
                 'id': MenuId.MENUBAR_HELP,
@@ -92,7 +92,7 @@ def _show_about(window: Window):
     Action(
         id='napari.window.help.github_issue',
         title=trans._('Report an issue on GitHub'),
-        callback=partial(open, url=HELP_URLS['github_issue']),
+        callback=partial(web_open, url=HELP_URLS['github_issue']),
         menus=[
             {
                 'id': MenuId.MENUBAR_HELP,
@@ -104,7 +104,7 @@ def _show_about(window: Window):
     Action(
         id='napari.window.help.homepage',
         title=trans._('napari homepage'),
-        callback=partial(open, url=HELP_URLS['homepage']),
+        callback=partial(web_open, url=HELP_URLS['homepage']),
         menus=[{'id': MenuId.MENUBAR_HELP, 'group': MenuGroup.NAVIGATION}],
     ),
 ]
diff --git a/napari/_qt/_qapp_model/qactions/_view.py b/napari/_qt/_qapp_model/qactions/_view.py
index a800aac9279..d0846ba682c 100644
--- a/napari/_qt/_qapp_model/qactions/_view.py
+++ b/napari/_qt/_qapp_model/qactions/_view.py
@@ -17,6 +17,7 @@
 from napari._qt.qt_viewer import QtViewer
 from napari.settings import get_settings
 from napari.utils.translations import trans
+from napari.viewer import Viewer
 
 # View submenus
 VIEW_SUBMENUS = [
@@ -61,6 +62,18 @@ def _get_current_tooltip_visibility() -> bool:
     return get_settings().appearance.layer_tooltip_visibility
 
 
+def _fit_to_view(viewer: Viewer):
+    viewer.reset_view(reset_camera_angle=False)
+
+
+def _zoom_in(viewer: Viewer):
+    viewer.camera.zoom *= 1.5
+
+
+def _zoom_out(viewer: Viewer):
+    viewer.camera.zoom /= 1.5
+
+
 Q_VIEW_ACTIONS: list[Action] = [
     Action(
         id='napari.window.view.toggle_fullscreen',
@@ -113,6 +126,45 @@ def _get_current_tooltip_visibility() -> bool:
         keybindings=[{'primary': KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyP}],
         toggled=ToggleRule(get_current=_get_current_play_status),
     ),
+    Action(
+        id='napari.viewer.fit_to_view',
+        title=trans._('Fit to View'),
+        menus=[
+            {
+                'id': MenuId.MENUBAR_VIEW,
+                'group': MenuGroup.ZOOM,
+                'order': 1,
+            }
+        ],
+        callback=_fit_to_view,
+        keybindings=[StandardKeyBinding.OriginalSize],
+    ),
+    Action(
+        id='napari.viewer.camera.zoom_in',
+        title=trans._('Zoom In'),
+        menus=[
+            {
+                'id': MenuId.MENUBAR_VIEW,
+                'group': MenuGroup.ZOOM,
+                'order': 1,
+            }
+        ],
+        callback=_zoom_in,
+        keybindings=[StandardKeyBinding.ZoomIn],
+    ),
+    Action(
+        id='napari.viewer.camera.zoom_out',
+        title=trans._('Zoom Out'),
+        menus=[
+            {
+                'id': MenuId.MENUBAR_VIEW,
+                'group': MenuGroup.ZOOM,
+                'order': 1,
+            }
+        ],
+        callback=_zoom_out,
+        keybindings=[StandardKeyBinding.ZoomOut],
+    ),
     Action(
         id='napari.window.view.toggle_activity_dock',
         title=trans._('Toggle Activity Dock'),
diff --git a/napari/_qt/_qplugins/__init__.py b/napari/_qt/_qplugins/__init__.py
index db3e5442843..06f3f1e09e4 100644
--- a/napari/_qt/_qplugins/__init__.py
+++ b/napari/_qt/_qplugins/__init__.py
@@ -5,7 +5,7 @@
 )
 
 __all__ = [
-    '_rebuild_npe1_samples_menu',
     '_rebuild_npe1_plugins_menu',
+    '_rebuild_npe1_samples_menu',
     '_register_qt_actions',
 ]
diff --git a/napari/_qt/_qplugins/_qnpe2.py b/napari/_qt/_qplugins/_qnpe2.py
index d1333a1f492..8f5af66c9dc 100644
--- a/napari/_qt/_qplugins/_qnpe2.py
+++ b/napari/_qt/_qplugins/_qnpe2.py
@@ -23,7 +23,7 @@
 from npe2 import plugin_manager as pm
 from qtpy.QtWidgets import QWidget
 
-from napari._app_model import get_app
+from napari._app_model import get_app_model
 from napari._app_model.constants import MenuGroup, MenuId
 from napari._qt._qapp_model.injection._qproviders import (
     _provide_viewer_or_raise,
@@ -49,7 +49,7 @@
 # can be easily deleted once npe1 is no longer supported.
 def _rebuild_npe1_samples_menu() -> None:  # pragma: no cover
     """Register submenu and actions for all npe1 plugins, clearing all first."""
-    app = get_app()
+    app = get_app_model()
     # Unregister all existing npe1 sample menu actions and submenus
     if unreg := plugin_manager._unreg_sample_submenus:
         unreg()
@@ -123,7 +123,7 @@ def _toggle_or_get_widget_npe1(
 
 def _rebuild_npe1_plugins_menu() -> None:
     """Register widget submenu and actions for all npe1 plugins, clearing all first."""
-    app = get_app()
+    app = get_app_model()
 
     # Unregister all existing npe1 plugin menu actions and submenus
     if unreg := plugin_manager._unreg_plugin_submenus:
@@ -369,7 +369,7 @@ def _build_widgets_submenu_actions(
 
     # if this plugin declares any menu items, its actions should have the
     # plugin name.
-    # TODO: update once plugin has self menus - they should't exclude it
+    # TODO: update once plugin has self menus - they shouldn't exclude it
     # from the shorter name
     declares_menu_items = any(
         len(pm.instance()._command_menu_map[mf.name][command.id])
@@ -439,7 +439,7 @@ def _register_qt_actions(mf: PluginManifest) -> None:
     This is called when a plugin is registered or enabled and it adds the
     plugin's sample and widget actions and submenus to the app model registry.
     """
-    app = get_app()
+    app = get_app_model()
     samples_submenu, sample_actions = _build_samples_submenu_actions(mf)
     widgets_submenu, widget_actions = _build_widgets_submenu_actions(mf)
 
diff --git a/napari/_qt/_tests/test_app.py b/napari/_qt/_tests/test_app.py
index 0022aff40a1..674599cf7d1 100644
--- a/napari/_qt/_tests/test_app.py
+++ b/napari/_qt/_tests/test_app.py
@@ -5,10 +5,26 @@
 import pytest
 from qtpy.QtWidgets import QAction, QShortcut
 
-from napari._qt.qt_event_loop import _ipython_has_eventloop, run, set_app_id
+from napari._qt.qt_event_loop import (
+    _ipython_has_eventloop,
+    get_app,
+    get_qapp,
+    run,
+    set_app_id,
+)
 
 
-@pytest.mark.skipif(os.name != 'Windows', reason='Windows specific')
+def test_qapp(qapp):
+    qapp = get_qapp()
+    with pytest.warns(
+        FutureWarning,
+        match='`QApplication` instance access through `get_app` is deprecated and will be removed in 0.6.0.\nPlease use `get_qapp` instead.\n',
+    ):
+        deprecated_qapp = get_app()
+    assert qapp == deprecated_qapp
+
+
+@pytest.mark.skipif(os.name != 'nt', reason='Windows specific')
 def test_windows_grouping_overwrite(qapp):
     import ctypes
 
@@ -26,8 +42,10 @@ def get_app_id():
     assert get_app_id() == 'test_text'
     set_app_id('custom_string')
     assert get_app_id() == 'custom_string'
-    set_app_id('')
-    assert get_app_id() == ''
+    set_app_id('')  # app id can't be an empty string
+    assert get_app_id() == 'custom_string'
+    set_app_id(' ')
+    assert get_app_id() == ' '
 
 
 def test_run_outside_ipython(make_napari_viewer, qapp, monkeypatch):
diff --git a/napari/_qt/_tests/test_async_slicing.py b/napari/_qt/_tests/test_async_slicing.py
index 1d47d4edc53..4446ae06216 100644
--- a/napari/_qt/_tests/test_async_slicing.py
+++ b/napari/_qt/_tests/test_async_slicing.py
@@ -19,12 +19,12 @@
 from napari.utils.events import Event
 
 
-@pytest.fixture()
+@pytest.fixture
 def rng() -> np.random.Generator:
     return np.random.default_rng(0)
 
 
-@pytest.fixture()
+@pytest.fixture
 def _enable_async(_fresh_settings, make_napari_viewer):
     """
     This fixture depends on _fresh_settings and make_napari_viewer
@@ -66,8 +66,8 @@ def layer_loaded(ly):
 
     for i in range(viewer.dims.nsteps[0]):
         viewer.dims.current_step = (i, 0, 0)
-        qtbot.waitUntil(partial(layer_loaded, l0), timeout=50)
-        qtbot.waitUntil(partial(layer_loaded, l1), timeout=50)
+        qtbot.waitUntil(partial(layer_loaded, l0), timeout=500)
+        qtbot.waitUntil(partial(layer_loaded, l1), timeout=500)
 
 
 @pytest.mark.usefixtures('_enable_async')
@@ -226,6 +226,19 @@ def test_async_slice_vectors_on_current_step_change(make_napari_viewer, qtbot):
     )
 
 
+@pytest.mark.usefixtures('_enable_async')
+def test_async_slice_two_layers_shutdown(make_napari_viewer):
+    """See https://github.com/napari/napari/issues/6685"""
+    viewer = make_napari_viewer()
+    # To reproduce the issue, we need two points layers where the second has
+    # some non-zero coordinates.
+    viewer.add_points()
+    points = viewer.add_points()
+    points.add([[1, 2]])
+
+    viewer.close()
+
+
 def setup_viewer_for_async_slicing(
     viewer: Viewer,
     layer: Layer,
diff --git a/napari/_qt/_tests/test_open_file.py b/napari/_qt/_tests/test_open_file.py
new file mode 100644
index 00000000000..5096cb766db
--- /dev/null
+++ b/napari/_qt/_tests/test_open_file.py
@@ -0,0 +1,21 @@
+from unittest import mock
+
+import pytest
+
+
+@pytest.mark.parametrize('stack', [True, False])
+def test_open_files_dialog(make_napari_viewer, stack):
+    """Check `QtViewer._open_files_dialog` correnct when `stack=True`."""
+    viewer = make_napari_viewer()
+    with (
+        mock.patch(
+            'napari._qt.qt_viewer.QtViewer._open_file_dialog_uni'
+        ) as mock_file,
+        mock.patch('napari._qt.qt_viewer.QtViewer._qt_open') as mock_open,
+    ):
+        viewer.window._qt_viewer._open_files_dialog(stack=stack)
+    mock_open.assert_called_once_with(
+        mock_file.return_value,
+        choose_plugin=False,
+        stack=stack,
+    )
diff --git a/napari/_qt/_tests/test_plugin_widgets.py b/napari/_qt/_tests/test_plugin_widgets.py
index 80096f675a8..a9923b829ac 100644
--- a/napari/_qt/_tests/test_plugin_widgets.py
+++ b/napari/_qt/_tests/test_plugin_widgets.py
@@ -8,7 +8,7 @@
 from qtpy.QtWidgets import QWidget
 
 import napari
-from napari._app_model import get_app
+from napari._app_model import get_app_model
 from napari._qt._qplugins._qnpe2 import _get_widget_viewer_param
 from napari._qt.qt_main_window import _instantiate_dock_widget
 from napari.utils._proxies import PublicOnlyProxy
@@ -182,7 +182,7 @@ def test_widget_types_supported(
     # instance of a widget
     tmp_plugin.contribute.widget(display_name='Widget')(Widget)
 
-    app = get_app()
+    app = get_app_model()
     viewer = make_napari_viewer()
 
     # `side_effect` required so widget is added to window and then
diff --git a/napari/_qt/_tests/test_qt_notifications.py b/napari/_qt/_tests/test_qt_notifications.py
index bd91f467244..73bd6715d72 100644
--- a/napari/_qt/_tests/test_qt_notifications.py
+++ b/napari/_qt/_tests/test_qt_notifications.py
@@ -42,7 +42,7 @@ def _raise():
     raise ValueError('error!')
 
 
-@pytest.fixture()
+@pytest.fixture
 def _clean_current(monkeypatch, qtbot):
     from napari._qt.qt_main_window import _QtMainWindow
 
@@ -95,7 +95,7 @@ def _raise_on_call(self, *args, **kwargs):
     )
 
 
-@pytest.fixture()
+@pytest.fixture
 def count_show(monkeypatch, qtbot):
     stat = ShowStatus()
 
@@ -207,7 +207,7 @@ def test_notification_display(count_show, severity, monkeypatch):
     and warnings.showwarning... and that it emits an event which is an instance
     of napari.utils.notifications.Notification.
 
-    in `get_app()`, we connect `notification_manager.notification_ready` to
+    in `get_qapp()`, we connect `notification_manager.notification_ready` to
     `NapariQtNotification.show_notification`, so all we have to test here is
     that show_notification is capable of receiving various event types.
     (we don't need to test that )
diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py
index a2a9fe958f7..5af54126293 100644
--- a/napari/_qt/_tests/test_qt_viewer.py
+++ b/napari/_qt/_tests/test_qt_viewer.py
@@ -367,6 +367,7 @@ def test_points_layer_display_correct_slice_on_scale(make_napari_viewer):
     np.testing.assert_equal(response.indices, [0])
 
 
+@pytest.mark.slow
 @skip_on_win_ci
 def test_qt_viewer_clipboard_with_flash(make_napari_viewer, qtbot):
     viewer = make_napari_viewer()
@@ -453,6 +454,7 @@ def test_qt_viewer_clipboard_without_flash(make_napari_viewer):
     assert not hasattr(viewer.window._qt_window, '_flash_animation')
 
 
+@pytest.mark.key_bindings
 def test_active_keybindings(make_napari_viewer):
     """Test instantiating viewer."""
     viewer = make_napari_viewer()
@@ -517,6 +519,35 @@ def on_click(layer, event):
     view.canvas._process_mouse_event(mouse_press_callbacks, mouse_event)
 
 
+def test_process_mouse_event_2d_layer_3d_viewer(make_napari_viewer):
+    """Test that _process_mouse_events can handle 2d layers in 3D.
+
+    This is a test for: https://github.com/napari/napari/issues/7299
+    """
+
+    # make a mock mouse event
+    new_pos = [5, 5]
+    mouse_event = MouseEvent(
+        pos=new_pos,
+    )
+    data = np.zeros((20, 20))
+
+    viewer = make_napari_viewer()
+    view = viewer.window._qt_viewer
+    image = viewer.add_image(data)
+
+    @image.mouse_drag_callbacks.append
+    def on_click(layer, event):
+        expected_position = view.canvas._map_canvas2world(new_pos)
+        np.testing.assert_almost_equal(expected_position, list(event.position))
+
+    assert viewer.dims.ndisplay == 2
+    view.canvas._process_mouse_event(mouse_press_callbacks, mouse_event)
+
+    viewer.dims.ndisplay = 3
+    view.canvas._process_mouse_event(mouse_press_callbacks, mouse_event)
+
+
 @skip_local_popups
 def test_memory_leaking(qtbot, make_napari_viewer):
     data = np.zeros((5, 20, 20, 20), dtype=int)
@@ -742,18 +773,10 @@ def _update_data(
     return color_box_color, middle_pixel
 
 
-@pytest.fixture()
-def qt_viewer_with_controls(qtbot):
-    qt_viewer = QtViewer(viewer=ViewerModel())
-    qt_viewer.show()
+@pytest.fixture
+def qt_viewer_with_controls(qt_viewer):
     qt_viewer.controls.show()
-    yield qt_viewer
-    qt_viewer.controls.hide()
-    qt_viewer.controls.close()
-    qt_viewer.hide()
-    qt_viewer.close()
-    qt_viewer._instances.clear()
-    qtbot.wait(50)
+    return qt_viewer
 
 
 @skip_local_popups
@@ -865,16 +888,12 @@ def test_axis_labels(make_napari_viewer):
     assert tuple(axes_visual.node.text.text) == ('2', '1', '0')
 
 
-@pytest.fixture()
-def qt_viewer(qtbot):
-    qt_viewer = QtViewer(ViewerModel())
-    qt_viewer.show()
-    qt_viewer.resize(460, 460)
+@pytest.fixture
+def qt_viewer(qtbot, qt_viewer_: QtViewer):
+    qt_viewer_.show()
+    qt_viewer_.resize(460, 460)
     QApplication.processEvents()
-    yield qt_viewer
-    qt_viewer.close()
-    qt_viewer._instances.clear()
-    del qt_viewer
+    return qt_viewer_
 
 
 def _find_margin(data: np.ndarray, additional_margin: int) -> tuple[int, int]:
@@ -1009,6 +1028,7 @@ def test_shortcut_passing(make_napari_viewer):
     assert layer.mode == 'erase'
 
 
+@pytest.mark.slow
 @pytest.mark.parametrize('mode', ['direct', 'random'])
 def test_selection_collision(qt_viewer: QtViewer, mode):
     data = np.zeros((10, 10), dtype=np.uint8)
@@ -1090,6 +1110,7 @@ def test_all_supported_dtypes(qt_viewer):
         )
 
 
+@pytest.mark.slow
 def test_more_than_uint16_colors(qt_viewer):
     pytest.importorskip('numba')
     # this test is slow (10s locally)
diff --git a/napari/_qt/_tests/test_qt_window.py b/napari/_qt/_tests/test_qt_window.py
index 9494fb0702e..4c500e79554 100644
--- a/napari/_qt/_tests/test_qt_window.py
+++ b/napari/_qt/_tests/test_qt_window.py
@@ -144,3 +144,17 @@ def test_screenshot_to_file(make_napari_viewer, tmp_path):
     )
     screenshot_array_from_file = QImg2array(QImage(screenshot_file_path))
     assert np.array_equal(screenshot_array, screenshot_array_from_file)
+
+
+def test_set_status_and_tooltip(make_napari_viewer):
+    viewer = make_napari_viewer()
+    # create active layer
+    viewer.add_image(np.zeros((10, 10)))
+    assert viewer.status == 'Ready'
+    assert viewer.tooltip.text == ''
+    viewer.window._qt_window.set_status_and_tooltip(('Text1', 'Text2'))
+    assert viewer.status == 'Text1'
+    assert viewer.tooltip.text == 'Text2'
+    viewer.window._qt_window.set_status_and_tooltip(None)
+    assert viewer.status == 'Text1'
+    assert viewer.tooltip.text == 'Text2'
diff --git a/napari/_qt/_tests/test_sigint_interupt.py b/napari/_qt/_tests/test_sigint_interupt.py
index 6343a596ad9..aa798b4ea3b 100644
--- a/napari/_qt/_tests/test_sigint_interupt.py
+++ b/napari/_qt/_tests/test_sigint_interupt.py
@@ -6,7 +6,7 @@
 from napari._qt.utils import _maybe_allow_interrupt
 
 
-@pytest.fixture()
+@pytest.fixture
 def platform_simulate_ctrl_c():
     import signal
     from functools import partial
diff --git a/napari/_qt/_tests/test_threads.py b/napari/_qt/_tests/test_threads.py
new file mode 100644
index 00000000000..6ed5f06e09a
--- /dev/null
+++ b/napari/_qt/_tests/test_threads.py
@@ -0,0 +1,53 @@
+from unittest.mock import MagicMock
+
+import pytest
+
+from napari._qt.threads.status_checker import StatusChecker
+from napari.components import ViewerModel
+
+
+@pytest.mark.usefixtures('qapp')
+def test_create():
+    StatusChecker(ViewerModel())
+
+
+@pytest.mark.usefixtures('qapp')
+def test_no_emmit_no_ref(monkeypatch):
+    """Calling calculate_status should not emit after viewer is deleted."""
+    model = ViewerModel()
+    status_checker = StatusChecker(model)
+    monkeypatch.setattr(
+        status_checker,
+        'status_and_tooltip_changed',
+        MagicMock(side_effect=RuntimeError('Should not emit')),
+    )
+    del model
+    status_checker.calculate_status()
+
+
+def test_terminate_no_ref(monkeypatch):
+    """Test that the thread terminates when the viewer is garbage collected."""
+    model = ViewerModel()
+    status_checker = StatusChecker(model)
+    del model
+    status_checker.run()
+    assert not status_checker._terminate
+
+
+def test_waiting_on_no_request(monkeypatch, qtbot):
+    def _check_status(value):
+        return value == ('Ready', '')
+
+    model = ViewerModel()
+    model.mouse_over_canvas = True
+    status_checker = StatusChecker(model)
+    status_checker.start()
+    with qtbot.waitSignal(
+        status_checker.status_and_tooltip_changed,
+        timeout=1000,
+        check_params_cb=_check_status,
+    ):
+        status_checker.trigger_status_update()
+    status_checker.terminate()
+
+    qtbot.wait_until(lambda: status_checker.isFinished())
diff --git a/napari/_qt/containers/__init__.py b/napari/_qt/containers/__init__.py
index 5294a7685cd..6a71acc2599 100644
--- a/napari/_qt/containers/__init__.py
+++ b/napari/_qt/containers/__init__.py
@@ -12,10 +12,8 @@
 from napari._qt.containers.qt_tree_view import QtNodeTreeView
 
 __all__ = [
-    'create_model',
-    'create_view',
-    'AxisModel',
     'AxisList',
+    'AxisModel',
     'QtAxisListModel',
     'QtLayerList',
     'QtLayerListModel',
@@ -23,4 +21,6 @@
     'QtListView',
     'QtNodeTreeModel',
     'QtNodeTreeView',
+    'create_model',
+    'create_view',
 ]
diff --git a/napari/_qt/containers/_base_item_model.py b/napari/_qt/containers/_base_item_model.py
index 5143031bc52..4ecc802c860 100644
--- a/napari/_qt/containers/_base_item_model.py
+++ b/napari/_qt/containers/_base_item_model.py
@@ -230,7 +230,7 @@ def _split_nested_index(
         if isinstance(nested_index, int):
             return QModelIndex(), nested_index
         # Tuple indexes are used in NestableEventedList, so we support them
-        # here so that subclasses needn't reimplmenet our _on_begin_* methods
+        # here so that subclasses needn't reimplement our _on_begin_* methods
         par = QModelIndex()
         *_p, idx = nested_index
         for i in _p:
diff --git a/napari/_qt/containers/_layer_delegate.py b/napari/_qt/containers/_layer_delegate.py
index 098f54403c5..17df3c0cc1b 100644
--- a/napari/_qt/containers/_layer_delegate.py
+++ b/napari/_qt/containers/_layer_delegate.py
@@ -208,7 +208,7 @@ def editorEvent(
             self.show_context_menu(index, model, pnt, option.widget)
 
         # if the user clicks quickly on the visibility checkbox, we *don't*
-        # want it to be interpreted as a double-click.  We want the visibilty
+        # want it to be interpreted as a double-click.  We want the visibility
         # to simply be toggled.
         if event.type() == QMouseEvent.MouseButtonDblClick:
             self.initStyleOption(option, index)
diff --git a/napari/_qt/containers/_tests/test_qt_layer_list.py b/napari/_qt/containers/_tests/test_qt_layer_list.py
index eb80c74e785..3b059ec016c 100644
--- a/napari/_qt/containers/_tests/test_qt_layer_list.py
+++ b/napari/_qt/containers/_tests/test_qt_layer_list.py
@@ -1,9 +1,12 @@
+import threading
+
 import numpy as np
 from qtpy.QtCore import QModelIndex, QPoint, Qt
 from qtpy.QtWidgets import QLineEdit, QStyleOptionViewItem
 
 from napari._qt.containers import QtLayerList
 from napari._qt.containers._layer_delegate import LayerDelegate
+from napari._tests.utils import skip_local_focus
 from napari.components import LayerList
 from napari.layers import Image, Shapes
 
@@ -193,6 +196,63 @@ def make_qt_layer_list_with_delegate(qtbot):
     return image1, image2, image3, layers, view, delegate
 
 
+@skip_local_focus
+def test_drag_and_drop_layers(qtbot):
+    """
+    Test drag and drop actions with pyautogui to change layer list order.
+
+    Notes:
+        * For this test to pass locally on macOS, you need to give the Terminal/iTerm
+          application accessibility permissions:
+              `System Settings > Privacy & Security > Accessibility`
+
+        See https://github.com/asweigart/pyautogui/issues/247 and
+        https://github.com/asweigart/pyautogui/issues/247#issuecomment-437668855.
+    """
+    view, images = make_qt_layer_list_with_layers(qtbot)
+    with qtbot.waitExposed(view):
+        view.show()
+
+    # check initial element is the one expected (last element in the layerlist)
+    name = view.model().data(
+        layer_to_model_index(view, 0), Qt.ItemDataRole.DisplayRole
+    )
+    assert name == images[-1].name
+
+    # drag and drop event simulation
+    base_pos = view.mapToGlobal(view.rect().topLeft())
+    start_pos = base_pos + QPoint(50, 10)
+    start_x = start_pos.x()
+    start_y = start_pos.y()
+    end_pos = base_pos + QPoint(100, 100)
+    end_x = end_pos.x()
+    end_y = end_pos.y()
+
+    drag_drop = threading.Thread(
+        target=drag_and_drop, args=(start_x, start_y, end_x, end_y)
+    )
+    drag_drop.start()
+
+    def check_drag_and_drop():
+        # check layerlist first element corresponds with first layer in the GUI
+        name = view.model().data(
+            layer_to_model_index(view, 0), Qt.ItemDataRole.DisplayRole
+        )
+        return name == images[0].name
+
+    qtbot.waitUntil(check_drag_and_drop)
+
+
+def drag_and_drop(start_x, start_y, end_x, end_y):
+    # simulate a drag and drop action with pyautogui
+    import pyautogui
+
+    pyautogui.moveTo(start_x, start_y, duration=0.2)
+    pyautogui.mouseDown()
+    pyautogui.moveTo(end_x, end_y, duration=0.2)
+    pyautogui.mouseUp()
+
+
 def make_qt_layer_list_with_layer(qtbot) -> tuple[QtLayerList, Image]:
     image = Image(np.zeros((4, 3)))
     layers = LayerList([image])
@@ -201,6 +261,15 @@ def make_qt_layer_list_with_layer(qtbot) -> tuple[QtLayerList, Image]:
     return view, image
 
 
+def make_qt_layer_list_with_layers(qtbot) -> tuple[QtLayerList, list[Image]]:
+    image1 = Image(np.zeros((4, 3)), name='image1')
+    image2 = Image(np.zeros((4, 3)), name='image2')
+    layers = LayerList([image1, image2])
+    view = QtLayerList(layers)
+    qtbot.addWidget(view)
+    return view, [image1, image2]
+
+
 def layer_to_model_index(view: QtLayerList, layer_index: int) -> QModelIndex:
     return view.model().index(layer_index, 0, view.rootIndex())
 
diff --git a/napari/_qt/containers/_tests/test_qt_tree.py b/napari/_qt/containers/_tests/test_qt_tree.py
index de1c91e3de9..e107d620cc5 100644
--- a/napari/_qt/containers/_tests/test_qt_tree.py
+++ b/napari/_qt/containers/_tests/test_qt_tree.py
@@ -7,7 +7,7 @@
 from napari.utils.tree import Group, Node
 
 
-@pytest.fixture()
+@pytest.fixture
 def tree_model(qapp):
     root = Group(
         [
diff --git a/napari/_qt/containers/qt_layer_list.py b/napari/_qt/containers/qt_layer_list.py
index 27fef905cc4..3e3ba465ac0 100644
--- a/napari/_qt/containers/qt_layer_list.py
+++ b/napari/_qt/containers/qt_layer_list.py
@@ -84,5 +84,9 @@ def keyPressEvent(self, e: Optional[QKeyEvent]) -> None:
             e.ignore()
         elif e.key() != Qt.Key.Key_Space:
             super().keyPressEvent(e)
-        if e.key() not in (Qt.Key.Key_Backspace, Qt.Key.Key_Delete):
+        if e.key() not in (
+            Qt.Key.Key_Backspace,
+            Qt.Key.Key_Delete,
+            Qt.Key.Key_Return,
+        ):
             e.ignore()  # pass key events up to viewer
diff --git a/napari/_qt/dialogs/_tests/test_preferences_dialog.py b/napari/_qt/dialogs/_tests/test_preferences_dialog.py
index 8babbd41cf3..93327623778 100644
--- a/napari/_qt/dialogs/_tests/test_preferences_dialog.py
+++ b/napari/_qt/dialogs/_tests/test_preferences_dialog.py
@@ -17,18 +17,37 @@
 )
 from napari.settings import NapariSettings, get_settings
 from napari.settings._constants import BrushSizeOnMouseModifiers, LabelDTypes
+from napari.utils.interactions import Shortcut
+from napari.utils.key_bindings import KeyBinding
 
 
-@pytest.fixture()
+@pytest.fixture
 def pref(qtbot):
     dlg = PreferencesDialog()
     qtbot.addWidget(dlg)
+    # check settings default values and change them for later checks
     settings = get_settings()
+    # change theme setting (default `dark`)
     assert settings.appearance.theme == 'dark'
     dlg._settings.appearance.theme = 'light'
     assert get_settings().appearance.theme == 'light'
+    # change highlight setting related value (default thickness `1`)
+    assert get_settings().appearance.highlight.highlight_thickness == 1
     dlg._settings.appearance.highlight.highlight_thickness = 5
     assert get_settings().appearance.highlight.highlight_thickness == 5
+    # change `napari:reset_scroll_progress` shortcut/keybinding (default keybinding `Ctrl`/`Control`)
+    # a copy of the initial `shortcuts` dictionary needs to be done since, to trigger an
+    # event update from the `ShortcutsSettings` model, the whole `shortcuts` dictionary
+    # needs to be reassigned.
+    assert dlg._settings.shortcuts.shortcuts[
+        'napari:reset_scroll_progress'
+    ] == [KeyBinding.from_str('Ctrl')]
+    shortcuts = dlg._settings.shortcuts.shortcuts.copy()
+    shortcuts['napari:reset_scroll_progress'] = [KeyBinding.from_str('U')]
+    dlg._settings.shortcuts.shortcuts = shortcuts
+    assert dlg._settings.shortcuts.shortcuts[
+        'napari:reset_scroll_progress'
+    ] == [KeyBinding.from_str('U')]
     return dlg
 
 
@@ -83,7 +102,7 @@ def test_font_size_widget(qtbot, pref):
     font_size_widget.state = new_font_size
     assert get_settings().appearance.font_size == new_font_size
 
-    # check a theme change keeps setted font size value
+    # verify that a theme change preserves the font size value
     assert get_settings().appearance.theme == 'light'
     get_settings().appearance.theme = 'dark'
     assert get_settings().appearance.font_size == new_font_size
@@ -207,21 +226,40 @@ def test_preferences_dialog_escape(qtbot, pref):
     assert get_settings().appearance.theme == 'light'
 
 
+@pytest.mark.key_bindings
 def test_preferences_dialog_cancel(qtbot, pref):
     with qtbot.waitSignal(pref.finished):
         pref._button_cancel.click()
     assert get_settings().appearance.theme == 'dark'
+    assert get_settings().shortcuts.shortcuts[
+        'napari:reset_scroll_progress'
+    ] == [KeyBinding.from_str('Ctrl')]
 
 
+@pytest.mark.key_bindings
 def test_preferences_dialog_restore(qtbot, pref, monkeypatch):
     theme_widget = pref._stack.widget(1).widget().widget.widgets['theme']
     highlight_widget = (
         pref._stack.widget(1).widget().widget.widgets['highlight']
     )
+    shortcut_widget = (
+        pref._stack.widget(3).widget().widget.widgets['shortcuts']
+    )
+
     assert get_settings().appearance.theme == 'light'
     assert theme_widget.state == 'light'
     assert get_settings().appearance.highlight.highlight_thickness == 5
     assert highlight_widget.state['highlight_thickness'] == 5
+    assert get_settings().shortcuts.shortcuts[
+        'napari:reset_scroll_progress'
+    ] == [KeyBinding.from_str('U')]
+    assert KeyBinding.from_str(
+        Shortcut.parse_platform(
+            shortcut_widget._table.item(
+                0, shortcut_widget._shortcut_col
+            ).text()
+        )
+    ) == KeyBinding.from_str('U')
 
     monkeypatch.setattr(
         QMessageBox, 'question', lambda *a: QMessageBox.RestoreDefaults
@@ -232,3 +270,13 @@ def test_preferences_dialog_restore(qtbot, pref, monkeypatch):
     assert theme_widget.state == 'dark'
     assert get_settings().appearance.highlight.highlight_thickness == 1
     assert highlight_widget.state['highlight_thickness'] == 1
+    assert get_settings().shortcuts.shortcuts[
+        'napari:reset_scroll_progress'
+    ] == [KeyBinding.from_str('Ctrl')]
+    assert KeyBinding.from_str(
+        Shortcut.parse_platform(
+            shortcut_widget._table.item(
+                0, shortcut_widget._shortcut_col
+            ).text()
+        )
+    ) == KeyBinding.from_str('Ctrl')
diff --git a/napari/_qt/dialogs/_tests/test_reader_dialog.py b/napari/_qt/dialogs/_tests/test_reader_dialog.py
index 3c724f7fcdd..4585eafbb1a 100644
--- a/napari/_qt/dialogs/_tests/test_reader_dialog.py
+++ b/napari/_qt/dialogs/_tests/test_reader_dialog.py
@@ -3,21 +3,24 @@
 
 import numpy as np
 import pytest
+import zarr
 from npe2 import DynamicPlugin
 from npe2.manifest.contributions import SampleDataURI
 from qtpy.QtWidgets import QLabel, QRadioButton
 
-from napari._app_model import get_app
+from napari._app_model import get_app_model
 from napari._qt.dialogs.qt_reader_dialog import (
     QtReaderDialog,
     open_with_dialog_choices,
     prepare_remaining_readers,
 )
+from napari._qt.qt_viewer import QtViewer
+from napari.components import ViewerModel
 from napari.errors.reader_errors import ReaderPluginError
 from napari.settings import get_settings
 
 
-@pytest.fixture()
+@pytest.fixture
 def reader_dialog(qtbot):
     def _reader_dialog(**kwargs):
         widget = QtReaderDialog(**kwargs)
@@ -148,7 +151,7 @@ def _(path): ...
     )
     tmp_plugin.manifest.contributions.sample_data = [my_sample]
 
-    app = get_app()
+    app = get_app_model()
     # required so setup steps run in init of `Viewer` and `Window`
     viewer = make_napari_viewer()
     # Ensure that `handle_gui_reading`` is not passed the sample plugin name
@@ -164,13 +167,14 @@ def _(path): ...
     )
 
 
-def test_open_with_dialog_choices_persist(
-    builtins, make_napari_viewer, tmp_path
-):
+def test_open_with_dialog_choices_persist(builtins, tmp_path, qtbot):
     pth = tmp_path / 'my-file.npy'
     np.save(pth, np.random.random((10, 10)))
 
-    viewer = make_napari_viewer()
+    viewer = ViewerModel()
+    qt_viewer = QtViewer(viewer)
+    qtbot.addWidget(qt_viewer)
+
     open_with_dialog_choices(
         display_name=builtins.display_name,
         persist=True,
@@ -178,13 +182,41 @@ def test_open_with_dialog_choices_persist(
         readers={builtins.name: builtins.display_name},
         paths=[str(pth)],
         stack=False,
-        qt_viewer=viewer.window._qt_viewer,
+        qt_viewer=qt_viewer,
     )
     assert len(viewer.layers) == 1
     # make sure extension was saved with *
     assert get_settings().plugins.extension2reader['*.npy'] == builtins.name
 
 
+def test_open_with_dialog_choices_persist_dir(builtins, tmp_path, qtbot):
+    pth = tmp_path / 'data.zarr'
+    z = zarr.open(
+        store=str(pth), mode='w', shape=(10, 10), chunks=(5, 5), dtype='f4'
+    )
+    z[:] = np.random.random((10, 10))
+
+    viewer = ViewerModel()
+    qt_viewer = QtViewer(viewer)
+    qtbot.addWidget(qt_viewer)
+
+    open_with_dialog_choices(
+        display_name=builtins.display_name,
+        persist=True,
+        extension=str(pth),
+        readers={builtins.name: builtins.display_name},
+        paths=[str(pth)],
+        stack=False,
+        qt_viewer=qt_viewer,
+    )
+    assert len(viewer.layers) == 1
+    # make sure extension was saved without * and with trailing slash
+    assert (
+        get_settings().plugins.extension2reader[f'{pth}{os.sep}']
+        == builtins.name
+    )
+
+
 def test_open_with_dialog_choices_raises(make_napari_viewer):
     viewer = make_napari_viewer()
 
diff --git a/napari/_qt/dialogs/qt_notification.py b/napari/_qt/dialogs/qt_notification.py
index fbfe7602319..c86f363f0ba 100644
--- a/napari/_qt/dialogs/qt_notification.py
+++ b/napari/_qt/dialogs/qt_notification.py
@@ -156,6 +156,8 @@ def show(self):
         """Show the message with a fade and slight slide in from the bottom."""
         super().show()
         self.slide_in()
+        if self.parent() is not None and not self.parent().isActiveWindow():
+            return
         if self.DISMISS_AFTER > 0:
             self.timer.setInterval(self.DISMISS_AFTER)
             self.timer.setSingleShot(True)
diff --git a/napari/_qt/dialogs/qt_reader_dialog.py b/napari/_qt/dialogs/qt_reader_dialog.py
index faabfa674d6..0846fc46b0e 100644
--- a/napari/_qt/dialogs/qt_reader_dialog.py
+++ b/napari/_qt/dialogs/qt_reader_dialog.py
@@ -237,7 +237,7 @@ def prepare_remaining_readers(
         raises previous error if no readers are left to try
     """
     readers = get_potential_readers(paths[0])
-    # remove plugin we already tried e.g. prefered plugin
+    # remove plugin we already tried e.g. preferred plugin
     if plugin_name in readers:
         del readers[plugin_name]
     # if there's no other readers left, raise the exception
@@ -294,8 +294,10 @@ def open_with_dialog_choices(
     qt_viewer.viewer.open(paths, stack=stack, plugin=plugin_name, **kwargs)
 
     if persist:
-        if not extension.endswith(os.sep):
-            extension = '*' + extension
+        if not os.path.isabs(extension):
+            extension = f'*{extension}'
+        elif os.path.isdir(extension) and not extension.endswith(os.sep):
+            extension += os.sep
         get_settings().plugins.extension2reader = {
             **get_settings().plugins.extension2reader,
             extension: plugin_name,
diff --git a/napari/_qt/layer_controls/_tests/test_qt_image_base_layer_.py b/napari/_qt/layer_controls/_tests/test_qt_image_base_layer_.py
index 2cd97d068a4..2d8730b4b11 100644
--- a/napari/_qt/layer_controls/_tests/test_qt_image_base_layer_.py
+++ b/napari/_qt/layer_controls/_tests/test_qt_image_base_layer_.py
@@ -131,11 +131,11 @@ def test_qt_image_controls_change_contrast(qtbot):
     assert tuple(layer.contrast_limits) == (0.1, 0.8)
 
 
-def test_tensorstore_clim_popup():
+def test_tensorstore_clim_popup(qtbot):
     """Regression to test, makes sure it works with tensorstore dtype"""
     ts = pytest.importorskip('tensorstore')
     layer = Image(ts.array(np.random.rand(20, 20)))
-    QContrastLimitsPopup(layer)
+    qtbot.addWidget(QContrastLimitsPopup(layer))
 
 
 def test_blending_opacity_slider(qtbot):
diff --git a/napari/_qt/layer_controls/_tests/test_qt_labels_layer.py b/napari/_qt/layer_controls/_tests/test_qt_labels_layer.py
index 9a20f80dbec..3d0ad9816b5 100644
--- a/napari/_qt/layer_controls/_tests/test_qt_labels_layer.py
+++ b/napari/_qt/layer_controls/_tests/test_qt_labels_layer.py
@@ -3,6 +3,10 @@
 
 from napari._qt.layer_controls.qt_labels_controls import QtLabelsControls
 from napari.layers import Labels
+from napari.layers.labels._labels_constants import (
+    IsoCategoricalGradientMode,
+    LabelsRendering,
+)
 from napari.utils.colormaps import DirectLabelColormap, colormap_utils
 
 np.random.seed(0)
@@ -19,7 +23,7 @@
 )
 
 
-@pytest.fixture()
+@pytest.fixture
 def make_labels_controls(qtbot, colormap=None):
     def _make_labels_controls(colormap=colormap):
         layer = Labels(_LABELS, colormap=colormap)
@@ -136,3 +140,52 @@ def test_change_label_selector_range(make_labels_controls):
 
     assert qtctrl.selectionSpinBox.minimum() == -128
     assert qtctrl.selectionSpinBox.maximum() == 127
+
+
+def test_change_iso_gradient_mode(make_labels_controls):
+    """Changing the iso gradient mode should update the layer and vice versa."""
+    layer, qtctrl = make_labels_controls()
+    qtctrl.ndisplay = 3
+    assert layer.rendering == LabelsRendering.ISO_CATEGORICAL
+    assert layer.iso_gradient_mode == IsoCategoricalGradientMode.FAST
+
+    # Change the iso gradient mode via the control, check the layer
+    qtctrl.isoGradientComboBox.setCurrentEnum(
+        IsoCategoricalGradientMode.SMOOTH
+    )
+    assert layer.iso_gradient_mode == IsoCategoricalGradientMode.SMOOTH
+
+    # Change the iso gradient mode via the layer, check the control
+    layer.iso_gradient_mode = IsoCategoricalGradientMode.FAST
+    assert (
+        qtctrl.isoGradientComboBox.currentEnum()
+        == IsoCategoricalGradientMode.FAST
+    )
+
+
+def test_iso_gradient_mode_hidden_for_2d(make_labels_controls):
+    """Test that the iso gradient mode control is hidden with 2D view."""
+    layer, qtctrl = make_labels_controls()
+    assert qtctrl.isoGradientComboBox.isHidden()
+    layer.data = np.random.randint(5, size=(10, 15), dtype=np.uint8)
+    assert qtctrl.isoGradientComboBox.isHidden()
+    qtctrl.ndisplay = 3
+    assert not qtctrl.isoGradientComboBox.isHidden()
+    qtctrl.ndisplay = 2
+    assert qtctrl.isoGradientComboBox.isHidden()
+
+
+def test_iso_gradient_mode_with_rendering(make_labels_controls):
+    """Test the iso gradeint mode control is enabled for iso_categorical rendering."""
+    layer, qtctrl = make_labels_controls()
+    qtctrl.ndisplay = 3
+    assert layer.rendering == LabelsRendering.ISO_CATEGORICAL
+    assert (
+        qtctrl.isoGradientComboBox.currentText()
+        == IsoCategoricalGradientMode.FAST
+    )
+    assert qtctrl.isoGradientComboBox.isEnabled()
+    layer.rendering = LabelsRendering.TRANSLUCENT
+    assert not qtctrl.isoGradientComboBox.isEnabled()
+    layer.rendering = LabelsRendering.ISO_CATEGORICAL
+    assert qtctrl.isoGradientComboBox.isEnabled()
diff --git a/napari/_qt/layer_controls/_tests/test_qt_layer_controls.py b/napari/_qt/layer_controls/_tests/test_qt_layer_controls.py
index 29d67dc2bcd..d4941b4fc9d 100644
--- a/napari/_qt/layer_controls/_tests/test_qt_layer_controls.py
+++ b/napari/_qt/layer_controls/_tests/test_qt_layer_controls.py
@@ -134,7 +134,7 @@ class LayerTypeWithData(NamedTuple):
 _LINES_DATA = np.random.random((6, 2, 2))
 
 
-@pytest.fixture()
+@pytest.fixture
 def create_layer_controls(qtbot):
     def _create_layer_controls(layer_type_with_data):
         if layer_type_with_data.colormap:
@@ -181,7 +181,7 @@ def _create_layer_controls(layer_type_with_data):
         'vectors',
     ],
 )
-@pytest.mark.qt_no_exception_capture()
+@pytest.mark.qt_no_exception_capture
 @pytest.mark.skipif(os.environ.get('MIN_REQ', '0') == '1', reason='min req')
 def test_create_layer_controls(
     qtbot, create_layer_controls, layer_type_with_data, capsys
@@ -243,7 +243,7 @@ def test_create_layer_controls(
         _VECTORS,
     ],
 )
-@pytest.mark.qt_no_exception_capture()
+@pytest.mark.qt_no_exception_capture
 @pytest.mark.skipif(os.environ.get('MIN_REQ', '0') == '1', reason='min req')
 def test_create_layer_controls_spin(
     qtbot, create_layer_controls, layer_type_with_data, capsys
@@ -319,7 +319,7 @@ def test_create_layer_controls_spin(
         _VECTORS,
     ],
 )
-@pytest.mark.qt_no_exception_capture()
+@pytest.mark.qt_no_exception_capture
 @pytest.mark.skipif(os.environ.get('MIN_REQ', '0') == '1', reason='min req')
 def test_create_layer_controls_qslider(
     qtbot, create_layer_controls, layer_type_with_data, capsys
@@ -401,7 +401,7 @@ def test_create_layer_controls_qslider(
         _VECTORS,
     ],
 )
-@pytest.mark.qt_no_exception_capture()
+@pytest.mark.qt_no_exception_capture
 @pytest.mark.skipif(os.environ.get('MIN_REQ', '0') == '1', reason='min req')
 def test_create_layer_controls_qcolorswatchedit(
     qtbot, create_layer_controls, layer_type_with_data, capsys
diff --git a/napari/_qt/layer_controls/_tests/test_qt_points_layer.py b/napari/_qt/layer_controls/_tests/test_qt_points_layer.py
index 0efac388e05..cd080285a0c 100644
--- a/napari/_qt/layer_controls/_tests/test_qt_points_layer.py
+++ b/napari/_qt/layer_controls/_tests/test_qt_points_layer.py
@@ -99,3 +99,22 @@ def test_current_size_slider_properly_initialized(qtbot):
     assert slider.minimum() == 1
     assert slider.value() == 10
     assert layer.current_size == 10
+
+
+def test_size_slider_represents_current_size(qtbot):
+    """Changing the current_size attribute should update the slider"""
+    layer = Points(np.random.rand(10, 2))
+    qtctrl = QtPointsControls(layer)
+    qtbot.addWidget(qtctrl)
+    slider = qtctrl.sizeSlider
+    slider.setValue(10)
+
+    # Initial value
+    assert slider.value() == 10
+    assert layer.current_size == 10
+
+    # Size event needs to be triggered manually, because no points are selected.
+    layer.current_size = 5
+    layer.events.current_size()
+    assert slider.value() == 5
+    assert layer.current_size == 5
diff --git a/napari/_qt/layer_controls/_tests/test_qt_surface_layer.py b/napari/_qt/layer_controls/_tests/test_qt_surface_layer.py
new file mode 100644
index 00000000000..c93b8f68cdf
--- /dev/null
+++ b/napari/_qt/layer_controls/_tests/test_qt_surface_layer.py
@@ -0,0 +1,25 @@
+import numpy as np
+
+from napari._qt.layer_controls.qt_surface_controls import QtSurfaceControls
+from napari.layers import Surface
+from napari.layers.surface._surface_constants import SHADING_TRANSLATION
+
+data = np.array([[0, 0], [0, 20], [10, 0], [10, 10]])
+faces = np.array([[0, 1, 2], [1, 2, 3]])
+values = np.linspace(0, 1, len(data))
+_SURFACE = (data, faces, values)
+
+
+def test_shading_combobox(qtbot):
+    layer = Surface(_SURFACE)
+    qtctrl = QtSurfaceControls(layer)
+    qtbot.addWidget(qtctrl)
+    assert qtctrl.shadingComboBox.currentText() == layer.shading
+
+    for display, shading in SHADING_TRANSLATION.items():
+        qtctrl.shadingComboBox.setCurrentText(display)
+        assert layer.shading == shading
+
+    for display, shading in SHADING_TRANSLATION.items():
+        layer.shading = shading
+        assert qtctrl.shadingComboBox.currentText() == display
diff --git a/napari/_qt/layer_controls/_tests/test_qt_tracks_layer.py b/napari/_qt/layer_controls/_tests/test_qt_tracks_layer.py
index 1978cf824ef..576dc6a8fe0 100644
--- a/napari/_qt/layer_controls/_tests/test_qt_tracks_layer.py
+++ b/napari/_qt/layer_controls/_tests/test_qt_tracks_layer.py
@@ -6,12 +6,12 @@
 from napari.layers import Tracks
 
 
-@pytest.fixture()
+@pytest.fixture
 def null_data() -> np.ndarray:
     return np.zeros((2, 4))
 
 
-@pytest.fixture()
+@pytest.fixture
 def properties() -> dict[str, list]:
     return {
         'track_id': [0, 0],
diff --git a/napari/_qt/layer_controls/qt_image_controls.py b/napari/_qt/layer_controls/qt_image_controls.py
index 997bf4ed82e..fd3614ebd3e 100644
--- a/napari/_qt/layer_controls/qt_image_controls.py
+++ b/napari/_qt/layer_controls/qt_image_controls.py
@@ -97,6 +97,11 @@ def __init__(self, layer) -> None:
         self.interpComboBox.currentTextChanged.connect(
             self.changeInterpolation
         )
+        self.interpComboBox.setToolTip(
+            trans._(
+                'Texture interpolation for display.\nnearest and linear are most performant.'
+            )
+        )
         self.interpLabel = QLabel(trans._('interpolation:'))
 
         renderComboBox = QComboBox(self)
@@ -186,22 +191,22 @@ def __init__(self, layer) -> None:
 
         self.layout().addRow(self.button_grid)
         self.layout().addRow(self.opacityLabel, self.opacitySlider)
+        self.layout().addRow(trans._('blending:'), self.blendComboBox)
         self.layout().addRow(
             trans._('contrast limits:'), self.contrastLimitsSlider
         )
         self.layout().addRow(trans._('auto-contrast:'), self.autoScaleBar)
         self.layout().addRow(trans._('gamma:'), self.gammaSlider)
         self.layout().addRow(trans._('colormap:'), colormap_layout)
-        self.layout().addRow(trans._('blending:'), self.blendComboBox)
         self.layout().addRow(self.interpLabel, self.interpComboBox)
         self.layout().addRow(self.depictionLabel, self.depictionComboBox)
-        self.layout().addRow(self.renderLabel, self.renderComboBox)
-        self.layout().addRow(self.isoThresholdLabel, self.isoThresholdSlider)
-        self.layout().addRow(self.attenuationLabel, self.attenuationSlider)
         self.layout().addRow(self.planeNormalLabel, self.planeNormalButtons)
         self.layout().addRow(
             self.planeThicknessLabel, self.planeThicknessSlider
         )
+        self.layout().addRow(self.renderLabel, self.renderComboBox)
+        self.layout().addRow(self.isoThresholdLabel, self.isoThresholdSlider)
+        self.layout().addRow(self.attenuationLabel, self.attenuationSlider)
 
     def changeInterpolation(self, text):
         """Change interpolation mode for image display.
@@ -346,7 +351,11 @@ def _update_rendering_parameter_visibility(self):
     def _update_plane_parameter_visibility(self):
         """Hide plane rendering controls if they aren't needed."""
         depiction = VolumeDepiction(self.layer.depiction)
-        visible = depiction == VolumeDepiction.PLANE and self.ndisplay == 3
+        visible = (
+            depiction == VolumeDepiction.PLANE
+            and self.ndisplay == 3
+            and self.layer.ndim >= 3
+        )
         self.planeNormalButtons.setVisible(visible)
         self.planeNormalLabel.setVisible(visible)
         self.planeThicknessSlider.setVisible(visible)
diff --git a/napari/_qt/layer_controls/qt_image_controls_base.py b/napari/_qt/layer_controls/qt_image_controls_base.py
index 6b6b853b96a..1433aeb3529 100644
--- a/napari/_qt/layer_controls/qt_image_controls_base.py
+++ b/napari/_qt/layer_controls/qt_image_controls_base.py
@@ -152,6 +152,12 @@ def __init__(self, layer: Image) -> None:
         self.colorbarLabel.setToolTip(trans._('Colorbar'))
 
         self._on_colormap_change()
+        if self.__class__ == QtBaseImageControls:
+            # This base class is only instantiated in tests. When it's not a
+            # concrete subclass, we need to parent the button_grid to the
+            # layout so that qtbot will correctly clean up all instantiated
+            # widgets.
+            self.layout().addRow(self.button_grid)
 
     def changeColor(self, text):
         """Change colormap on the layer model.
@@ -281,7 +287,7 @@ def reset():
 
         reset_btn = QPushButton('reset')
         reset_btn.setObjectName('reset_clims_button')
-        reset_btn.setToolTip(trans._('autoscale contrast to data range'))
+        reset_btn.setToolTip(trans._('Autoscale contrast to data range'))
         reset_btn.setFixedWidth(45)
         reset_btn.clicked.connect(reset)
         self._layout.addWidget(
@@ -295,7 +301,7 @@ def reset():
             range_btn = QPushButton('full range')
             range_btn.setObjectName('full_clim_range_button')
             range_btn.setToolTip(
-                trans._('set contrast range to full bit-depth')
+                trans._('Set contrast range to full bit-depth')
             )
             range_btn.setFixedWidth(75)
             range_btn.clicked.connect(layer.reset_contrast_limits_range)
diff --git a/napari/_qt/layer_controls/qt_labels_controls.py b/napari/_qt/layer_controls/qt_labels_controls.py
index 59911507a5c..2cd5a2e1fec 100644
--- a/napari/_qt/layer_controls/qt_labels_controls.py
+++ b/napari/_qt/layer_controls/qt_labels_controls.py
@@ -11,7 +11,7 @@
     QSpinBox,
     QWidget,
 )
-from superqt import QLargeIntSpinBox
+from superqt import QEnumComboBox, QLargeIntSpinBox
 
 from napari._qt.layer_controls.qt_layer_controls_base import QtLayerControls
 from napari._qt.utils import set_widgets_enabled_with_opacity
@@ -19,6 +19,7 @@
 from napari._qt.widgets.qt_mode_buttons import QtModePushButton
 from napari.layers.labels._labels_constants import (
     LABEL_COLOR_MODE_TRANSLATIONS,
+    IsoCategoricalGradientMode,
     LabelColorMode,
     LabelsRendering,
     Mode,
@@ -99,6 +100,9 @@ def __init__(self, layer) -> None:
         super().__init__(layer)
 
         self.layer.events.rendering.connect(self._on_rendering_change)
+        self.layer.events.iso_gradient_mode.connect(
+            self._on_iso_gradient_mode_change
+        )
         self.layer.events.colormap.connect(self._on_colormap_change)
         self.layer.events.selected_label.connect(
             self._on_selected_label_change
@@ -145,14 +149,14 @@ def __init__(self, layer) -> None:
         color_mode_comboBox.activated.connect(self.change_color_mode)
 
         contig_cb = QCheckBox()
-        contig_cb.setToolTip(trans._('contiguous editing'))
+        contig_cb.setToolTip(trans._('Contiguous editing'))
         contig_cb.stateChanged.connect(self.change_contig)
         self.contigCheckBox = contig_cb
         self._on_contiguous_change()
 
         ndim_sb = QSpinBox()
         self.ndimSpinBox = ndim_sb
-        ndim_sb.setToolTip(trans._('number of dimensions for label editing'))
+        ndim_sb.setToolTip(trans._('Number of dimensions for label editing'))
         ndim_sb.valueChanged.connect(self.change_n_edit_dim)
         ndim_sb.setMinimum(2)
         ndim_sb.setMaximum(self.layer.ndim)
@@ -162,7 +166,9 @@ def __init__(self, layer) -> None:
 
         self.contourSpinBox = QLargeIntSpinBox()
         self.contourSpinBox.setRange(0, dtype_lims[1])
-        self.contourSpinBox.setToolTip(trans._('display contours of labels'))
+        self.contourSpinBox.setToolTip(
+            trans._('Set width of displayed label contours')
+        )
         self.contourSpinBox.valueChanged.connect(self.change_contour)
         self.contourSpinBox.setKeyboardTracking(False)
         self.contourSpinBox.setAlignment(Qt.AlignmentFlag.AlignCenter)
@@ -170,7 +176,7 @@ def __init__(self, layer) -> None:
 
         preserve_labels_cb = QCheckBox()
         preserve_labels_cb.setToolTip(
-            trans._('preserve existing labels while painting')
+            trans._('Preserve existing labels while painting')
         )
         preserve_labels_cb.stateChanged.connect(self.change_preserve_labels)
         self.preserveLabelsCheckBox = preserve_labels_cb
@@ -189,7 +195,7 @@ def __init__(self, layer) -> None:
             layer,
             'shuffle',
             slot=self.changeColor,
-            tooltip=trans._('shuffle colors'),
+            tooltip=trans._('Shuffle colors'),
         )
 
         self.pick_button = self._radio_button(
@@ -237,17 +243,27 @@ def __init__(self, layer) -> None:
         self.button_grid.addWidget(self.fill_button, 0, 4)
         self.button_grid.addWidget(self.pick_button, 0, 5)
 
-        renderComboBox = QComboBox(self)
-        rendering_options = [i.value for i in LabelsRendering]
-        renderComboBox.addItems(rendering_options)
-        index = renderComboBox.findText(
-            self.layer.rendering, Qt.MatchFlag.MatchFixedString
-        )
-        renderComboBox.setCurrentIndex(index)
-        renderComboBox.currentTextChanged.connect(self.changeRendering)
+        renderComboBox = QEnumComboBox(enum_class=LabelsRendering)
+        renderComboBox.setCurrentEnum(LabelsRendering(self.layer.rendering))
+        renderComboBox.currentEnumChanged.connect(self.changeRendering)
         self.renderComboBox = renderComboBox
         self.renderLabel = QLabel(trans._('rendering:'))
 
+        isoGradientComboBox = QEnumComboBox(
+            enum_class=IsoCategoricalGradientMode
+        )
+        isoGradientComboBox.setCurrentEnum(
+            IsoCategoricalGradientMode(self.layer.iso_gradient_mode)
+        )
+        isoGradientComboBox.currentEnumChanged.connect(
+            self.changeIsoGradientMode
+        )
+        isoGradientComboBox.setEnabled(
+            self.layer.rendering == LabelsRendering.ISO_CATEGORICAL
+        )
+        self.isoGradientComboBox = isoGradientComboBox
+        self.isoGradientLabel = QLabel(trans._('gradient\nmode:'))
+
         self._on_ndisplay_changed()
 
         color_layout = QHBoxLayout()
@@ -256,11 +272,12 @@ def __init__(self, layer) -> None:
         color_layout.addWidget(self.selectionSpinBox)
 
         self.layout().addRow(self.button_grid)
-        self.layout().addRow(trans._('label:'), color_layout)
         self.layout().addRow(self.opacityLabel, self.opacitySlider)
-        self.layout().addRow(trans._('brush size:'), self.brushSizeSlider)
         self.layout().addRow(trans._('blending:'), self.blendComboBox)
+        self.layout().addRow(trans._('label:'), color_layout)
+        self.layout().addRow(trans._('brush size:'), self.brushSizeSlider)
         self.layout().addRow(self.renderLabel, self.renderComboBox)
+        self.layout().addRow(self.isoGradientLabel, self.isoGradientComboBox)
         self.layout().addRow(trans._('color mode:'), self.colorModeComboBox)
         self.layout().addRow(trans._('contour:'), self.contourSpinBox)
         self.layout().addRow(trans._('n edit dim:'), self.ndimSpinBox)
@@ -319,12 +336,12 @@ def _on_data_change(self):
         dtype_lims = get_dtype_limits(get_dtype(self.layer))
         self.selectionSpinBox.setRange(*dtype_lims)
 
-    def changeRendering(self, text):
+    def changeRendering(self, rendering_mode: LabelsRendering):
         """Change rendering mode for image display.
 
         Parameters
         ----------
-        text : str
+        rendering_mode : LabelsRendering
             Rendering mode used by vispy.
             Selects a preset rendering mode in vispy that determines how
             volume is displayed:
@@ -335,7 +352,23 @@ def changeRendering(self, text):
               location, lighning calculations are performed to give the visual
               appearance of a surface.
         """
-        self.layer.rendering = text
+        self.isoGradientComboBox.setEnabled(
+            rendering_mode == LabelsRendering.ISO_CATEGORICAL
+        )
+        self.layer.rendering = rendering_mode
+
+    def changeIsoGradientMode(self, gradient_mode: IsoCategoricalGradientMode):
+        """Change gradient mode for isosurface rendering.
+
+        Parameters
+        ----------
+        gradient_mode : IsoCategoricalGradientMode
+            Gradient mode for the isosurface rendering method.
+            Selects the finite-difference gradient method for the isosurface shader:
+            * fast: simple finite difference gradient along each axis
+            * smooth: isotropic Sobel gradient, smoother but more computationally expensive
+        """
+        self.layer.iso_gradient_mode = gradient_mode
 
     def changeColor(self):
         """Change colormap of the label layer."""
@@ -469,19 +502,27 @@ def _on_show_selected_label_change(self):
     def _on_rendering_change(self):
         """Receive layer model rendering change event and update dropdown menu."""
         with self.layer.events.rendering.blocker():
-            index = self.renderComboBox.findText(
-                self.layer.rendering, Qt.MatchFlag.MatchFixedString
+            self.renderComboBox.setCurrentEnum(
+                LabelsRendering(self.layer.rendering)
+            )
+
+    def _on_iso_gradient_mode_change(self):
+        """Receive layer model iso_gradient_mode change event and update dropdown menu."""
+        with self.layer.events.iso_gradient_mode.blocker():
+            self.isoGradientComboBox.setCurrentEnum(
+                IsoCategoricalGradientMode(self.layer.iso_gradient_mode)
             )
-            self.renderComboBox.setCurrentIndex(index)
 
     def _on_editable_or_visible_change(self):
         super()._on_editable_or_visible_change()
         self._set_polygon_tool_state()
 
     def _on_ndisplay_changed(self):
-        render_visible = self.ndisplay == 3
-        self.renderComboBox.setVisible(render_visible)
-        self.renderLabel.setVisible(render_visible)
+        show_3d_widgets = self.ndisplay == 3
+        self.renderComboBox.setVisible(show_3d_widgets)
+        self.renderLabel.setVisible(show_3d_widgets)
+        self.isoGradientComboBox.setVisible(show_3d_widgets)
+        self.isoGradientLabel.setVisible(show_3d_widgets)
         self._on_editable_or_visible_change()
         self._set_polygon_tool_state()
         super()._on_ndisplay_changed()
diff --git a/napari/_qt/layer_controls/qt_layer_controls_base.py b/napari/_qt/layer_controls/qt_layer_controls_base.py
index d5f72c40e0c..4f2fed8adb2 100644
--- a/napari/_qt/layer_controls/qt_layer_controls_base.py
+++ b/napari/_qt/layer_controls/qt_layer_controls_base.py
@@ -157,6 +157,12 @@ def __init__(self, layer: Layer) -> None:
         self.opacityLabel.setEnabled(
             self.layer.blending not in NO_OPACITY_BLENDING_MODES
         )
+        if self.__class__ == QtLayerControls:
+            # This base class is only instantiated in tests. When it's not a
+            # concrete subclass, we need to parent the button_grid to the
+            # layout so that qtbot will correctly clean up all instantiated
+            # widgets.
+            self.layout().addRow(self.button_grid)
 
     def changeOpacity(self, value):
         """Change opacity value on the layer model.
diff --git a/napari/_qt/layer_controls/qt_points_controls.py b/napari/_qt/layer_controls/qt_points_controls.py
index e6fb4503dae..22ce6bab186 100644
--- a/napari/_qt/layer_controls/qt_points_controls.py
+++ b/napari/_qt/layer_controls/qt_points_controls.py
@@ -87,6 +87,7 @@ def __init__(self, layer) -> None:
         )
         self.layer.events.symbol.connect(self._on_current_symbol_change)
         self.layer.events.size.connect(self._on_current_size_change)
+        self.layer.events.current_size.connect(self._on_current_size_change)
         self.layer.events.current_border_color.connect(
             self._on_current_border_color_change
         )
@@ -125,11 +126,15 @@ def __init__(self, layer) -> None:
 
         self.faceColorEdit = QColorSwatchEdit(
             initial_color=self.layer.current_face_color,
-            tooltip=trans._('click to set current face color'),
+            tooltip=trans._(
+                'Click to set the face color of currently selected points and any added afterwards.'
+            ),
         )
         self.borderColorEdit = QColorSwatchEdit(
             initial_color=self.layer.current_border_color,
-            tooltip=trans._('click to set current border color'),
+            tooltip=trans._(
+                'Click to set the border color of currently selected points and any added afterwards.'
+            ),
         )
         self.faceColorEdit.color_changed.connect(self.changeCurrentFaceColor)
         self.borderColorEdit.color_changed.connect(
@@ -162,7 +167,7 @@ def __init__(self, layer) -> None:
         self.outOfSliceCheckBox.stateChanged.connect(self.change_out_of_slice)
 
         self.textDispCheckBox = QCheckBox()
-        self.textDispCheckBox.setToolTip(trans._('toggle text visibility'))
+        self.textDispCheckBox.setToolTip(trans._('Toggle text visibility'))
         self.textDispCheckBox.setChecked(self.layer.text.visible)
         self.textDispCheckBox.stateChanged.connect(self.change_text_visibility)
 
@@ -197,8 +202,8 @@ def __init__(self, layer) -> None:
 
         self.layout().addRow(self.button_grid)
         self.layout().addRow(self.opacityLabel, self.opacitySlider)
-        self.layout().addRow(trans._('point size:'), self.sizeSlider)
         self.layout().addRow(trans._('blending:'), self.blendComboBox)
+        self.layout().addRow(trans._('point size:'), self.sizeSlider)
         self.layout().addRow(trans._('symbol:'), self.symbolComboBox)
         self.layout().addRow(trans._('face color:'), self.faceColorEdit)
         self.layout().addRow(trans._('border color:'), self.borderColorEdit)
diff --git a/napari/_qt/layer_controls/qt_shapes_controls.py b/napari/_qt/layer_controls/qt_shapes_controls.py
index 3290dd2bd2e..1108462673f 100644
--- a/napari/_qt/layer_controls/qt_shapes_controls.py
+++ b/napari/_qt/layer_controls/qt_shapes_controls.py
@@ -119,6 +119,11 @@ def __init__(self, layer) -> None:
         sld.setValue(int(value))
         sld.valueChanged.connect(self.changeWidth)
         self.widthSlider = sld
+        self.widthSlider.setToolTip(
+            trans._(
+                'Set the edge width of currently selected shapes and any added afterwards.'
+            )
+        )
 
         self.select_button = self._radio_button(
             layer, 'select', Mode.SELECT, True, 'activate_select_mode'
@@ -236,27 +241,31 @@ def __init__(self, layer) -> None:
 
         self.faceColorEdit = QColorSwatchEdit(
             initial_color=self.layer.current_face_color,
-            tooltip=trans._('click to set current face color'),
+            tooltip=trans._(
+                'Click to set the face color of currently selected shapes and any added afterwards.'
+            ),
         )
         self._on_current_face_color_change()
         self.edgeColorEdit = QColorSwatchEdit(
             initial_color=self.layer.current_edge_color,
-            tooltip=trans._('click to set current edge color'),
+            tooltip=trans._(
+                'Click to set the edge color of currently selected shapes and any added afterwards'
+            ),
         )
         self._on_current_edge_color_change()
         self.faceColorEdit.color_changed.connect(self.changeFaceColor)
         self.edgeColorEdit.color_changed.connect(self.changeEdgeColor)
 
         text_disp_cb = QCheckBox()
-        text_disp_cb.setToolTip(trans._('toggle text visibility'))
+        text_disp_cb.setToolTip(trans._('Toggle text visibility'))
         text_disp_cb.setChecked(self.layer.text.visible)
         text_disp_cb.stateChanged.connect(self.change_text_visibility)
         self.textDispCheckBox = text_disp_cb
 
         self.layout().addRow(self.button_grid)
         self.layout().addRow(self.opacityLabel, self.opacitySlider)
-        self.layout().addRow(trans._('edge width:'), self.widthSlider)
         self.layout().addRow(trans._('blending:'), self.blendComboBox)
+        self.layout().addRow(trans._('edge width:'), self.widthSlider)
         self.layout().addRow(trans._('face color:'), self.faceColorEdit)
         self.layout().addRow(trans._('edge color:'), self.edgeColorEdit)
         self.layout().addRow(trans._('display text:'), self.textDispCheckBox)
diff --git a/napari/_qt/layer_controls/qt_surface_controls.py b/napari/_qt/layer_controls/qt_surface_controls.py
index 9a0edfd30bb..279d831c6b5 100644
--- a/napari/_qt/layer_controls/qt_surface_controls.py
+++ b/napari/_qt/layer_controls/qt_surface_controls.py
@@ -48,6 +48,8 @@ class QtSurfaceControls(QtBaseImageControls):
     def __init__(self, layer) -> None:
         super().__init__(layer)
 
+        self.layer.events.shading.connect(self._on_shading_change)
+
         colormap_layout = QHBoxLayout()
         colormap_layout.addWidget(self.colorbarLabel)
         colormap_layout.addWidget(self.colormapComboBox)
@@ -65,13 +67,13 @@ def __init__(self, layer) -> None:
 
         self.layout().addRow(self.button_grid)
         self.layout().addRow(self.opacityLabel, self.opacitySlider)
+        self.layout().addRow(trans._('blending:'), self.blendComboBox)
         self.layout().addRow(
             trans._('contrast limits:'), self.contrastLimitsSlider
         )
         self.layout().addRow(trans._('auto-contrast:'), self.autoScaleBar)
         self.layout().addRow(trans._('gamma:'), self.gammaSlider)
         self.layout().addRow(trans._('colormap:'), colormap_layout)
-        self.layout().addRow(trans._('blending:'), self.blendComboBox)
         self.layout().addRow(trans._('shading:'), self.shadingComboBox)
 
     def changeShading(self, text):
@@ -82,3 +84,12 @@ def changeShading(self, text):
             Name of shading mode, eg: 'flat', 'smooth', 'none'.
         """
         self.layer.shading = self.shadingComboBox.currentData()
+
+    def _on_shading_change(self):
+        """Receive layer model shading change event and update combobox."""
+        with self.layer.events.shading.blocker():
+            self.shadingComboBox.setCurrentIndex(
+                self.shadingComboBox.findData(
+                    SHADING_TRANSLATION[self.layer.shading]
+                )
+            )
diff --git a/napari/_qt/layer_controls/qt_tracks_controls.py b/napari/_qt/layer_controls/qt_tracks_controls.py
index 7147c804ecb..fcb8b1701fd 100644
--- a/napari/_qt/layer_controls/qt_tracks_controls.py
+++ b/napari/_qt/layer_controls/qt_tracks_controls.py
@@ -102,10 +102,10 @@ def __init__(self, layer) -> None:
         self.colormap_combobox.currentTextChanged.connect(self.change_colormap)
 
         self.layout().addRow(self.button_grid)
+        self.layout().addRow(self.opacityLabel, self.opacitySlider)
+        self.layout().addRow(trans._('blending:'), self.blendComboBox)
         self.layout().addRow(trans._('color by:'), self.color_by_combobox)
         self.layout().addRow(trans._('colormap:'), self.colormap_combobox)
-        self.layout().addRow(trans._('blending:'), self.blendComboBox)
-        self.layout().addRow(self.opacityLabel, self.opacitySlider)
         self.layout().addRow(trans._('tail width:'), self.tail_width_slider)
         self.layout().addRow(trans._('tail length:'), self.tail_length_slider)
         self.layout().addRow(trans._('head length:'), self.head_length_slider)
diff --git a/napari/_qt/layer_controls/qt_vectors_controls.py b/napari/_qt/layer_controls/qt_vectors_controls.py
index 3c22fbce50e..0f2ebf28260 100644
--- a/napari/_qt/layer_controls/qt_vectors_controls.py
+++ b/napari/_qt/layer_controls/qt_vectors_controls.py
@@ -92,7 +92,7 @@ def __init__(self, layer) -> None:
         self.edgeColorEdit = QColorSwatchEdit(
             initial_color=self.layer.edge_color,
             tooltip=trans._(
-                'click to set current edge color',
+                'Click to set current edge color',
             ),
         )
         self.edgeColorEdit.color_changed.connect(self.change_edge_color_direct)
@@ -147,9 +147,9 @@ def __init__(self, layer) -> None:
 
         self.layout().addRow(self.button_grid)
         self.layout().addRow(self.opacityLabel, self.opacitySlider)
+        self.layout().addRow(trans._('blending:'), self.blendComboBox)
         self.layout().addRow(trans._('width:'), self.widthSpinBox)
         self.layout().addRow(trans._('length:'), self.lengthSpinBox)
-        self.layout().addRow(trans._('blending:'), self.blendComboBox)
         self.layout().addRow(
             trans._('vector style:'), self.vector_style_comboBox
         )
diff --git a/napari/_qt/perf/_tests/test_perf.py b/napari/_qt/perf/_tests/test_perf.py
index 0248a358f20..01414ded860 100644
--- a/napari/_qt/perf/_tests/test_perf.py
+++ b/napari/_qt/perf/_tests/test_perf.py
@@ -37,7 +37,7 @@
 }
 
 
-@pytest.fixture()
+@pytest.fixture
 def perf_config(tmp_path: Path):
     trace_path = tmp_path / 'trace.json'
     config_path = tmp_path / 'perfmon.json'
@@ -47,7 +47,7 @@ def perf_config(tmp_path: Path):
     return stub(path=config_path, trace_path=trace_path)
 
 
-@pytest.fixture()
+@pytest.fixture
 def perfmon_script(tmp_path):
     script = PERFMON_SCRIPT
     if 'coverage' in sys.modules:
diff --git a/napari/_qt/perf/qt_event_tracing.py b/napari/_qt/perf/qt_event_tracing.py
index c460834793b..9d6965cd7a8 100644
--- a/napari/_qt/perf/qt_event_tracing.py
+++ b/napari/_qt/perf/qt_event_tracing.py
@@ -59,7 +59,7 @@ def __init__(self) -> None:
         self.string_name = {}
         for name in vars(QEvent):
             attribute = getattr(QEvent, name)
-            if type(attribute) == QEvent.Type:
+            if type(attribute) is QEvent.Type:
                 self.string_name[attribute] = name
 
     def as_string(self, event: QEvent.Type) -> str:
diff --git a/napari/_qt/qt_event_loop.py b/napari/_qt/qt_event_loop.py
index 5690a5d29e9..d932175ad22 100644
--- a/napari/_qt/qt_event_loop.py
+++ b/napari/_qt/qt_event_loop.py
@@ -61,7 +61,22 @@ def set_app_id(app_id):
 _IPYTHON_WAS_HERE_FIRST = 'IPython' in sys.modules
 
 
-def get_app(
+# TODO: Remove in napari 0.6.0
+def get_app(*args, **kwargs) -> QApplication:
+    """Get or create the Qt QApplication. Now deprecated, use `get_qapp`."""
+    warn(
+        trans._(
+            '`QApplication` instance access through `get_app` is deprecated and will be removed in 0.6.0.\n'
+            'Please use `get_qapp` instead.\n',
+            deferred=True,
+        ),
+        category=FutureWarning,
+        stacklevel=2,
+    )
+    return get_qapp(*args, **kwargs)
+
+
+def get_qapp(
     *,
     app_name: Optional[str] = None,
     app_version: Optional[str] = None,
diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py
index 75493a0c181..e7866465ff7 100644
--- a/napari/_qt/qt_main_window.py
+++ b/napari/_qt/qt_main_window.py
@@ -32,7 +32,7 @@
     Qt,
     Slot,
 )
-from qtpy.QtGui import QIcon
+from qtpy.QtGui import QHideEvent, QIcon, QShowEvent
 from qtpy.QtWidgets import (
     QApplication,
     QDialog,
@@ -44,7 +44,6 @@
     QToolTip,
     QWidget,
 )
-from superqt.utils import QSignalThrottler
 
 from napari._app_model.constants import MenuId
 from napari._app_model.context import create_context, get_context
@@ -61,11 +60,12 @@
 from napari._qt.dialogs.qt_notification import NapariQtNotification
 from napari._qt.qt_event_loop import (
     NAPARI_ICON_PATH,
-    get_app,
+    get_qapp,
     quit_app as quit_app_,
 )
 from napari._qt.qt_resources import get_stylesheet
 from napari._qt.qt_viewer import QtViewer
+from napari._qt.threads.status_checker import StatusChecker
 from napari._qt.utils import QImg2array, qbytearray_to_str, str_to_qbytearray
 from napari._qt.widgets.qt_viewer_dock_widget import (
     _SHORTCUT_DEPRECATION_STRING,
@@ -80,6 +80,7 @@
 from napari.settings import get_settings
 from napari.utils import perf
 from napari.utils._proxies import PublicOnlyProxy
+from napari.utils.events import Event
 from napari.utils.io import imsave
 from napari.utils.misc import (
     in_ipython,
@@ -145,7 +146,6 @@ def __init__(
         self.setWindowTitle(self._qt_viewer.viewer.title)
 
         self._maximized_flag = False
-        self._fullscreen_flag = False
         self._normal_geometry = QRect()
         self._window_size = None
         self._window_pos = None
@@ -195,22 +195,51 @@ def __init__(
         # were defined somewhere in the `_qt` module and imported in init_qactions
         init_qactions()
 
-        self.status_throttler = QSignalThrottler(parent=self)
-        self.status_throttler.setTimeout(50)
-        self._throttle_cursor_to_status_connection(viewer)
-
-    def _throttle_cursor_to_status_connection(self, viewer: 'Viewer'):
-        # In the GUI we expect lots of changes to the cursor position, so
-        # replace the direct connection with a throttled one.
         with contextlib.suppress(IndexError):
             viewer.cursor.events.position.disconnect(
-                viewer._update_status_bar_from_cursor
+                viewer.update_status_from_cursor
             )
-        viewer.cursor.events.position.connect(self.status_throttler.throttle)
-        self.status_throttler.triggered.connect(
-            viewer._update_status_bar_from_cursor
+
+        self.status_thread = StatusChecker(viewer, parent=self)
+        self.status_thread.status_and_tooltip_changed.connect(
+            self.set_status_and_tooltip
+        )
+        viewer.cursor.events.position.connect(
+            self.status_thread.trigger_status_update
+        )
+        settings.appearance.events.update_status_based_on_layer.connect(
+            self._toggle_status_thread
         )
 
+    def _toggle_status_thread(self, event: Event):
+        if event.value:
+            self.status_thread.start()
+        else:
+            self.status_thread.terminate()
+
+    def showEvent(self, event: QShowEvent):
+        """Override to handle window state changes."""
+        settings = get_settings()
+        if settings.appearance.update_status_based_on_layer:
+            self.status_thread.start()
+        super().showEvent(event)
+
+    def hideEvent(self, event: QHideEvent):
+        self.status_thread.terminate()
+        super().hideEvent(event)
+
+    def set_status_and_tooltip(
+        self, status_and_tooltip: Optional[tuple[Union[str, dict], str]]
+    ):
+        if status_and_tooltip is None:
+            return
+        self._qt_viewer.viewer.status = status_and_tooltip[0]
+        self._qt_viewer.viewer.tooltip.text = status_and_tooltip[1]
+        if (
+            active := self._qt_viewer.viewer.layers.selection.active
+        ) is not None:
+            self._qt_viewer.viewer.help = active.help
+
     def statusBar(self) -> 'ViewerStatusBar':
         return super().statusBar()
 
@@ -251,62 +280,26 @@ def event(self, e: QEvent) -> bool:
 
         return res
 
-    def isFullScreen(self):
-        # Needed to prevent errors when going to fullscreen mode on Windows
-        # Use a flag attribute to determine if the window is in full screen mode
-        # See https://bugreports.qt.io/browse/QTBUG-41309
-        # Based on https://github.com/spyder-ide/spyder/pull/7720
-        return self._fullscreen_flag
-
-    def showNormal(self):
-        # Needed to prevent errors when going to fullscreen mode on Windows. Here we:
-        #   * Set fullscreen flag
-        #   * Remove `Qt.FramelessWindowHint` and `Qt.WindowStaysOnTopHint` window flags if needed
-        #   * Set geometry to previously stored normal geometry or default empty QRect
-        # Always call super `showNormal` to set Qt window state
-        # See https://bugreports.qt.io/browse/QTBUG-41309
-        # Based on https://github.com/spyder-ide/spyder/pull/7720
-        self._fullscreen_flag = False
-        if os.name == 'nt':
-            self.setWindowFlags(
-                self.windowFlags()
-                ^ (
-                    Qt.WindowType.FramelessWindowHint
-                    | Qt.WindowType.WindowStaysOnTopHint
-                )
-            )
-            self.setGeometry(self._normal_geometry)
-        super().showNormal()
-
     def showFullScreen(self):
-        # Needed to prevent errors when going to fullscreen mode on Windows. Here we:
-        #   * Set fullscreen flag
-        #   * Add `Qt.FramelessWindowHint` and `Qt.WindowStaysOnTopHint` window flags if needed
-        #   * Call super `showNormal` to update the normal screen geometry to apply it later if needed
-        #   * Save window normal geometry if needed
-        #   * Get screen geometry
-        #   * Set geometry window to use total screen geometry +1 in every direction if needed
-        # If the workaround is not needed just call super `showFullScreen`
-        # See https://bugreports.qt.io/browse/QTBUG-41309
-        # Based on https://github.com/spyder-ide/spyder/pull/7720
-        self._fullscreen_flag = True
-        if os.name == 'nt':
-            self.setWindowFlags(
-                self.windowFlags()
-                | Qt.WindowType.FramelessWindowHint
-                | Qt.WindowType.WindowStaysOnTopHint
-            )
-            super().showNormal()
-            self._normal_geometry = self.normalGeometry()
-            screen_rect = self.windowHandle().screen().geometry()
-            self.setGeometry(
-                screen_rect.left() - 1,
-                screen_rect.top() - 1,
-                screen_rect.width() + 2,
-                screen_rect.height() + 2,
+        super().showFullScreen()
+        # Handle OpenGL based windows fullscreen issue on Windows.
+        # For more info see:
+        #  * https://doc.qt.io/qt-6/windows-issues.html#fullscreen-opengl-based-windows
+        #  * https://bugreports.qt.io/browse/QTBUG-41309
+        #  * https://bugreports.qt.io/browse/QTBUG-104511
+        if os.name != 'nt':
+            return
+        import win32con
+        import win32gui
+
+        if self.windowHandle():
+            handle = int(self.windowHandle().winId())
+            win32gui.SetWindowLong(
+                handle,
+                win32con.GWL_STYLE,
+                win32gui.GetWindowLong(handle, win32con.GWL_STYLE)
+                | win32con.WS_BORDER,
             )
-        else:
-            super().showFullScreen()
 
     def eventFilter(self, source, event):
         # Handle showing hidden menubar on mouse move event.
@@ -447,8 +440,6 @@ def _save_current_window_settings(self):
 
     def close(self, quit_app=False, confirm_need=False):
         """Override to handle closing app or just the window."""
-        if hasattr(self.status_throttler, '_timer'):
-            self.status_throttler._timer.stop()
         if not quit_app and not self._qt_viewer.viewer.layers:
             return super().close()
         confirm_need_local = confirm_need and self._is_close_dialog[quit_app]
@@ -573,6 +564,9 @@ def closeEvent(self, event):
             event.ignore()
             return
 
+        self.status_thread.terminate()
+        self.status_thread.wait()
+
         if self._ev and self._ev.isRunning():
             self._ev.quit()
 
@@ -651,7 +645,7 @@ class Window:
 
     def __init__(self, viewer: 'Viewer', *, show: bool = True) -> None:
         # create QApplication if it doesn't already exist
-        qapp = get_app()
+        qapp = get_qapp()
 
         # Dictionary holding dock widgets
         self._dock_widgets: MutableMapping[str, QtViewerDockWidget] = (
@@ -684,10 +678,7 @@ def __init__(self, viewer: 'Viewer', *, show: bool = True) -> None:
         # **and** the layerlist context key are available when we update
         # menus. We need a single context to contain all keys required for
         # menu update, so we add them to the layerlist context for now.
-        if self._qt_viewer._layers is not None:
-            add_dummy_actions(
-                self._qt_viewer._layers.model().sourceModel()._root._ctx
-            )
+        add_dummy_actions(self._qt_viewer.viewer.layers._ctx)
         self._update_theme()
         self._update_theme_font_size()
         get_settings().appearance.events.theme.connect(self._update_theme)
@@ -830,7 +821,7 @@ def _status_bar(self):
 
     def _update_menu_state(self, menu: MenuStr):
         """Update enabled/visible state of menu item with context."""
-        layerlist = self._qt_viewer._layers.model().sourceModel()._root
+        layerlist = self._qt_viewer.viewer.layers
         menu_model = getattr(self, menu)
         menu_model.update_from_context(get_context(layerlist))
 
@@ -1089,7 +1080,7 @@ def add_dock_widget(
         widget: Union[QWidget, 'Widget'],
         *,
         name: str = '',
-        area: str = 'right',
+        area: Optional[str] = None,
         allowed_areas: Optional[Sequence[str]] = None,
         shortcut=_sentinel,
         add_vertical_stretch=True,
@@ -1146,6 +1137,12 @@ def add_dock_widget(
 
             self._unnamed_dockwidget_count += 1
 
+        if area is None:
+            settings = get_settings()
+            area = settings.application.plugin_widget_positions.get(
+                name, 'right'
+            )
+
         if shortcut is not _sentinel:
             warnings.warn(
                 _SHORTCUT_DEPRECATION_STRING.format(shortcut=shortcut),
@@ -1206,7 +1203,7 @@ def _add_viewer_dock_widget(
         menu : QMenu, optional
             Menu bar to add toggle action to. If `None` nothing added to menu.
         """
-        # Find if any othe dock widgets are currently in area
+        # Find if any other dock widgets are currently in area
         current_dws_in_area = [
             dw
             for dw in self._qt_window.findChildren(QDockWidget)
@@ -1479,7 +1476,7 @@ def show(self, *, block=False):
         # B) it is not the first time a QMainWindow is being created
 
         # `app_name` will be "napari" iff the application was instantiated in
-        # get_app(). isActiveWindow() will be True if it is the second time a
+        # get_qapp(). isActiveWindow() will be True if it is the second time a
         # _qt_window has been created.
         # See #721, #732, #735, #795, #1594
         app_name = QApplication.instance().applicationName()
@@ -1520,11 +1517,13 @@ def _update_theme(self, event=None, extra_variables=None):
                     {'font_size': f'{settings.appearance.font_size}pt'}
                 )
             # set the style sheet with the theme name and extra_variables
-            self._qt_window.setStyleSheet(
-                get_stylesheet(
-                    actual_theme_name, extra_variables=extra_variables
-                )
+            style_sheet = get_stylesheet(
+                actual_theme_name, extra_variables=extra_variables
             )
+            self._qt_window.setStyleSheet(style_sheet)
+            self._qt_viewer.setStyleSheet(style_sheet)
+            if self._qt_viewer._console:
+                self._qt_viewer._console._update_theme(style_sheet=style_sheet)
 
     def _status_changed(self, event):
         """Update status bar.
diff --git a/napari/_qt/qt_resources/__init__.py b/napari/_qt/qt_resources/__init__.py
index 791e8213777..e26d5b76eab 100644
--- a/napari/_qt/qt_resources/__init__.py
+++ b/napari/_qt/qt_resources/__init__.py
@@ -4,7 +4,7 @@
 from napari._qt.qt_resources._svg import QColoredSVGIcon
 from napari.settings import get_settings
 
-__all__ = ['get_stylesheet', 'QColoredSVGIcon']
+__all__ = ['QColoredSVGIcon', 'get_stylesheet']
 
 
 STYLE_PATH = (Path(__file__).parent / 'styles').resolve()
diff --git a/napari/_qt/qt_resources/styles/02_custom.qss b/napari/_qt/qt_resources/styles/02_custom.qss
index 2638cc48bcd..60389ed4531 100644
--- a/napari/_qt/qt_resources/styles/02_custom.qss
+++ b/napari/_qt/qt_resources/styles/02_custom.qss
@@ -116,6 +116,7 @@ QtConsole > QTextEdit {
   selection-background-color: {{ highlight }};
   margin: 10px;
   font-family: Menlo, Consolas, "Ubuntu Mono", "Roboto Mono", "DejaVu Sans Mono", monospace;
+  font-size: {{ font_size }};
 }
 .inverted {
   background-color: {{ background }};
diff --git a/napari/_qt/qt_viewer.py b/napari/_qt/qt_viewer.py
index 93aa48c04a0..4067a47db61 100644
--- a/napari/_qt/qt_viewer.py
+++ b/napari/_qt/qt_viewer.py
@@ -563,7 +563,7 @@ def _get_console(self) -> Optional[QtConsole]:
 
             with warnings.catch_warnings():
                 warnings.filterwarnings('ignore')
-                console = QtConsole(self.viewer)
+                console = QtConsole(self.viewer, style_sheet=self.styleSheet())
                 console.push(
                     {'napari': napari, 'action_manager': action_manager}
                 )
@@ -914,13 +914,8 @@ def _open_files_dialog(self, choose_plugin=False, stack=False):
         """Add files from the menubar."""
         filenames = self._open_file_dialog_uni(trans._('Select file(s)...'))
 
-        if (filenames != []) and (filenames is not None):
-            for filename in filenames:
-                self._qt_open(
-                    [filename],
-                    choose_plugin=choose_plugin,
-                    stack=stack,
-                )
+        if filenames:
+            self._qt_open(filenames, choose_plugin=choose_plugin, stack=stack)
             update_open_history(filenames[0])
 
     def _open_files_dialog_as_stack_dialog(self, choose_plugin=False):
@@ -1179,7 +1174,9 @@ def closeEvent(self, event):
         event : qtpy.QtCore.QCloseEvent
             Event from the Qt context.
         """
-        self.layers.close()
+        if self._layers is not None:
+            # do not create layerlist if it does not exist yet.
+            self.layers.close()
 
         # if the viewer.QtDims object is playing an axis, we need to terminate
         # the AnimationThread before close, otherwise it will cause a segFault
diff --git a/napari/_qt/qthreading.py b/napari/_qt/qthreading.py
index 7f216ae601b..ae1ea7d33da 100644
--- a/napari/_qt/qthreading.py
+++ b/napari/_qt/qthreading.py
@@ -19,8 +19,8 @@
     'FunctionWorker',
     'GeneratorWorker',
     'create_worker',
-    'thread_worker',
     'register_threadworker_processors',
+    'thread_worker',
 ]
 
 wait_for_workers_to_quit = _qthreading.WorkerBase.await_workers
@@ -350,11 +350,11 @@ def register_threadworker_processors():
     import magicgui
 
     from napari import layers, types
-    from napari._app_model import get_app
+    from napari._app_model import get_app_model
     from napari.types import LayerDataTuple
     from napari.utils import _magicgui as _mgui
 
-    app = get_app()
+    app = get_app_model()
 
     for _type in (LayerDataTuple, list[LayerDataTuple]):
         t = FunctionWorker[_type]
diff --git a/napari/_qt/threads/__init__.py b/napari/_qt/threads/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/napari/_qt/threads/status_checker.py b/napari/_qt/threads/status_checker.py
new file mode 100644
index 00000000000..2276288b7ff
--- /dev/null
+++ b/napari/_qt/threads/status_checker.py
@@ -0,0 +1,133 @@
+"""A performant, dedicated thread to compute cursor status and signal updates to a viewer."""
+
+from __future__ import annotations
+
+import os
+from threading import Event
+from typing import TYPE_CHECKING
+from weakref import ref
+
+from qtpy.QtCore import QObject, QThread, Signal
+
+from napari.utils.notifications import Notification, notification_manager
+
+if TYPE_CHECKING:
+    from napari.components import ViewerModel
+
+
+class StatusChecker(QThread):
+    """A dedicated thread for performant updating of the status bar.
+
+    This class offloads the job of computing the cursor status into a separate thread,
+    Qt Signals are used to update the main viewer with the status string.
+
+    Prior to https://github.com/napari/napari/pull/7146, status bar updates
+    happened on the main thread in the viewer model, which could be
+    expensive in some layers and resulted in bad performance when some
+    layers were selected.
+
+    Because the thread runs a single infinite while loop, the updates are
+    naturally throttled since they can only be sent at the rate which updates
+    can be computed, but no faster.
+
+    Attributes
+    ----------
+    _need_status_update : threading.Event
+        An Event (fancy thread-safe bool-like to synchronize threads)
+        for keeping track of when the status needs updating
+        (because the cursor has moved).
+    _terminate : bool
+        If set to True, the status checker thread needs to be terminated.
+        When the QtViewer is being closed, it sets this flag to terminate
+        the status checker thread.
+        After _terminate is set to True, no more status updates are sent.
+        Default: False.
+    viewer_ref : weakref.ref[napari.viewer.ViewerModel]
+        A weak reference to the viewer which is providing status updates.
+        We keep a weak reference to the viewer so the status checker thread
+        will not prevent the viewer from being garbage collected.
+        We proactively check the viewer to determine if a new status update
+        needs to be computed and emitted.
+    """
+
+    # Create a Signal to establish a lightweight communication mechanism between the
+    # viewer and the status checker thread for cursor events and related status
+    status_and_tooltip_changed = Signal(object)
+
+    def __init__(self, viewer: ViewerModel, parent: QObject | None = None):
+        super().__init__(parent=parent)
+        self.viewer_ref = ref(viewer)
+        self._need_status_update = Event()
+        self._need_status_update.clear()
+        self._terminate = False
+
+    def trigger_status_update(self) -> None:
+        """Trigger a status update computation.
+
+        When the cursor moves, the viewer will call this to instruct
+        the status checker to update the viewer with the present status.
+        """
+        self._need_status_update.set()
+
+    def terminate(self) -> None:
+        """Terminate the status checker thread.
+
+        For proper cleanup,it's important to set _terminate to True before
+        calling _needs_status_update.set.
+        """
+        self._terminate = True
+        self._need_status_update.set()
+
+    def start(
+        self, priority: QThread.Priority = QThread.Priority.InheritPriority
+    ) -> None:
+        """Start the status checker thread.
+
+        Make sure to set the _terminate attribute to False prior to start.
+        """
+        self._terminate = False
+        super().start(priority)
+
+    def run(self) -> None:
+        while not self._terminate:
+            if self.viewer_ref() is None:
+                # Stop thread when viewer is closed
+                return
+            if self._need_status_update.is_set():
+                self._need_status_update.clear()
+                self.calculate_status()
+            else:
+                self._need_status_update.wait()
+
+    def calculate_status(self) -> None:
+        """Calculate the status and emit the signal.
+
+        If the viewer is not available, do nothing. Otherwise,
+        emit the signal that the status has changed.
+        """
+        viewer = self.viewer_ref()
+        if viewer is None:
+            return
+
+        try:
+            # Calculate the status change from cursor's movement
+            res = viewer._calc_status_from_cursor()
+        except Exception as e:  # pragma: no cover # noqa: BLE001
+            # Our codebase is not threadsafe. It is possible that an
+            # ViewerModel or Layer state is changed while we are trying to
+            # calculate the status, which may cause an exception.
+            # All exceptions are caught and handled to keep updates
+            # from crashing the thread. The exception is logged
+            # and a notification is sent.
+            notification_manager.dispatch(Notification.from_exception(e))
+            return
+        # Emit the signal with the updated status
+        self.status_and_tooltip_changed.emit(res)
+
+
+if os.environ.get('ASV') == 'true':
+    # This is a hack to make sure that the StatusChecker thread is not
+    # running when the benchmark is running. This is because the
+    # StatusChecker thread may introduce some noise in the benchmark
+    # results from waiting on its termination.
+    StatusChecker.start = lambda self, priority=0: None  # type: ignore[assignment]
diff --git a/napari/_qt/utils.py b/napari/_qt/utils.py
index c4628a38dbb..5de2dd4ab30 100644
--- a/napari/_qt/utils.py
+++ b/napari/_qt/utils.py
@@ -243,7 +243,10 @@ def combine_widgets(
                 container.layout().addWidget(widget)
             return container
     raise TypeError(
-        trans._('"widget" must be a QWidget or a sequence of QWidgets')
+        trans._(
+            '"widgets" must be a QWidget, a magicgui Widget or a sequence of '
+            'such types'
+        )
     )
 
 
diff --git a/napari/_qt/widgets/_slider_compat.py b/napari/_qt/widgets/_slider_compat.py
index d6616908bfc..5459267bb60 100644
--- a/napari/_qt/widgets/_slider_compat.py
+++ b/napari/_qt/widgets/_slider_compat.py
@@ -8,4 +8,4 @@
     from superqt import QLabeledSlider as QSlider
 
 
-__all__ = ['QSlider', 'QDoubleSlider']
+__all__ = ['QDoubleSlider', 'QSlider']
diff --git a/napari/_qt/widgets/_tests/test_qt_dims.py b/napari/_qt/widgets/_tests/test_qt_dims.py
index debbdfee1f8..fedbca4e576 100644
--- a/napari/_qt/widgets/_tests/test_qt_dims.py
+++ b/napari/_qt/widgets/_tests/test_qt_dims.py
@@ -293,7 +293,7 @@ def test_slider_press_updates_last_used(qtbot):
             assert widg.isVisibleTo(view)
             assert view.dims.last_used == i
         else:
-            # sliders should not be visible for the follwing dims and the
+            # sliders should not be visible for the following dims and the
             # last_used should fallback to the first available dim with a
             # visible slider (dim 0)
             assert not widg.isVisibleTo(view)
@@ -321,7 +321,6 @@ def test_play_button(qtbot):
 
     qtbot.mouseClick(button, Qt.LeftButton)
     qtbot.waitUntil(lambda: not view.is_playing)
-    qtbot.waitUntil(lambda: view._animation_worker is None)
 
     with patch.object(button.popup, 'show_above_mouse') as mock_popup:
         qtbot.mouseClick(button, Qt.RightButton)
@@ -336,3 +335,4 @@ def test_play_button(qtbot):
     assert slider.fps == -button.fpsspin.value() == -11
     button.mode_combo.setCurrentText('once')
     assert slider.loop_mode == button.mode_combo.currentText() == 'once'
+    qtbot.waitUntil(view._animation_thread.isFinished)
diff --git a/napari/_qt/widgets/_tests/test_qt_dims_2.py b/napari/_qt/widgets/_tests/test_qt_dims_2.py
index acd5ae9b227..0d1dac281f1 100644
--- a/napari/_qt/widgets/_tests/test_qt_dims_2.py
+++ b/napari/_qt/widgets/_tests/test_qt_dims_2.py
@@ -48,7 +48,6 @@ def test_not_playing_after_ndim_changes(qtbot):
     dims.ndim = 2
 
     qtbot.waitUntil(lambda: not view.is_playing)
-    qtbot.waitUntil(lambda: view._animation_worker is None)
 
 
 def test_not_playing_after_ndisplay_changes(qtbot):
@@ -63,7 +62,6 @@ def test_not_playing_after_ndisplay_changes(qtbot):
     dims.ndisplay = 3
 
     qtbot.waitUntil(lambda: not view.is_playing)
-    qtbot.waitUntil(lambda: view._animation_worker is None)
 
 
 def test_set_axis_labels_after_ndim_changes(qtbot):
diff --git a/napari/_qt/widgets/_tests/test_qt_dock_widget.py b/napari/_qt/widgets/_tests/test_qt_dock_widget.py
index a4f0d13cdf1..ad731803da3 100644
--- a/napari/_qt/widgets/_tests/test_qt_dock_widget.py
+++ b/napari/_qt/widgets/_tests/test_qt_dock_widget.py
@@ -8,6 +8,8 @@
     QWidget,
 )
 
+from napari._qt.utils import combine_widgets
+
 
 def test_add_dock_widget(make_napari_viewer):
     """Test basic add_dock_widget functionality"""
@@ -68,9 +70,10 @@ def test_add_dock_widget_raises(make_napari_viewer):
         viewer.window.add_dock_widget(widg, name='test')
 
 
-def test_remove_dock_widget_orphans_widget(make_napari_viewer):
+def test_remove_dock_widget_orphans_widget(make_napari_viewer, qtbot):
     viewer = make_napari_viewer()
     widg = QPushButton('button')
+    qtbot.addWidget(widg)
 
     assert not widg.parent()
     dw = viewer.window.add_dock_widget(
@@ -85,9 +88,10 @@ def test_remove_dock_widget_orphans_widget(make_napari_viewer):
     assert not widg.parent()
 
 
-def test_remove_dock_widget_by_widget_reference(make_napari_viewer):
+def test_remove_dock_widget_by_widget_reference(make_napari_viewer, qtbot):
     viewer = make_napari_viewer()
     widg = QPushButton('button')
+    qtbot.addWidget(widg)
 
     dw = viewer.window.add_dock_widget(widg, name='test')
     assert widg.parent() is dw
@@ -139,3 +143,11 @@ def test_adding_stretch(make_napari_viewer):
     dw = viewer.window.add_dock_widget(widg, area='bottom')
     assert widg.layout().count() == 1
     dw.close()
+
+
+def test_combine_widgets_error():
+    """Check error raised when combining widgets with invalid types."""
+    with pytest.raises(
+        TypeError, match='"widgets" must be a QWidget, a magicgui'
+    ):
+        combine_widgets(['string'])
diff --git a/napari/_qt/widgets/_tests/test_qt_extension2reader.py b/napari/_qt/widgets/_tests/test_qt_extension2reader.py
index afff727a529..4c7adad4383 100644
--- a/napari/_qt/widgets/_tests/test_qt_extension2reader.py
+++ b/napari/_qt/widgets/_tests/test_qt_extension2reader.py
@@ -7,7 +7,7 @@
 from napari.settings import get_settings
 
 
-@pytest.fixture()
+@pytest.fixture
 def extension2reader_widget(qtbot):
     def _extension2reader_widget(**kwargs):
         widget = Extension2ReaderTable(**kwargs)
@@ -19,7 +19,7 @@ def _extension2reader_widget(**kwargs):
     return _extension2reader_widget
 
 
-@pytest.fixture()
+@pytest.fixture
 def tif_reader(tmp_plugin: DynamicPlugin):
     tmp2 = tmp_plugin.spawn(name='tif_reader', register=True)
 
@@ -29,7 +29,7 @@ def _(path): ...
     return tmp2
 
 
-@pytest.fixture()
+@pytest.fixture
 def npy_reader(tmp_plugin: DynamicPlugin):
     tmp2 = tmp_plugin.spawn(name='npy_reader', register=True)
 
diff --git a/napari/_qt/widgets/_tests/test_qt_highlight_preview.py b/napari/_qt/widgets/_tests/test_qt_highlight_preview.py
index 17a5fb3e13e..cd128a3d3b2 100644
--- a/napari/_qt/widgets/_tests/test_qt_highlight_preview.py
+++ b/napari/_qt/widgets/_tests/test_qt_highlight_preview.py
@@ -8,7 +8,7 @@
 )
 
 
-@pytest.fixture()
+@pytest.fixture
 def star_widget(qtbot):
     def _star_widget(**kwargs):
         widget = QtStar(**kwargs)
@@ -20,7 +20,7 @@ def _star_widget(**kwargs):
     return _star_widget
 
 
-@pytest.fixture()
+@pytest.fixture
 def triangle_widget(qtbot):
     def _triangle_widget(**kwargs):
         widget = QtTriangle(**kwargs)
@@ -32,7 +32,7 @@ def _triangle_widget(**kwargs):
     return _triangle_widget
 
 
-@pytest.fixture()
+@pytest.fixture
 def highlight_preview_widget(qtbot):
     def _highlight_preview_widget(**kwargs):
         widget = QtHighlightPreviewWidget(**kwargs)
diff --git a/napari/_qt/widgets/_tests/test_qt_play.py b/napari/_qt/widgets/_tests/test_qt_play.py
index dc1fce5632b..75a25de1031 100644
--- a/napari/_qt/widgets/_tests/test_qt_play.py
+++ b/napari/_qt/widgets/_tests/test_qt_play.py
@@ -5,7 +5,7 @@
 import pytest
 
 from napari._qt.widgets.qt_dims import QtDims
-from napari._qt.widgets.qt_dims_slider import AnimationWorker
+from napari._qt.widgets.qt_dims_slider import AnimationThread
 from napari.components import Dims
 from napari.settings._constants import LoopMode
 
@@ -26,7 +26,8 @@ def make_worker(
     slider_widget.fps = fps
     slider_widget.frame_range = frame_range
 
-    worker = AnimationWorker(slider_widget)
+    worker = AnimationThread()
+    worker.set_slider(slider_widget)
     worker._count = 0
     worker.nz = nz
 
@@ -69,6 +70,7 @@ def go():
 ]
 
 
+@pytest.mark.slow
 @pytest.mark.parametrize(
     ('nframes', 'fps', 'mode', 'rng', 'result'), CONDITIONS
 )
@@ -95,11 +97,11 @@ def test_animation_thread_once(qtbot):
         qtbot, nframes=nframes, loop_mode=LoopMode.ONCE
     ) as worker:
         with qtbot.waitSignal(worker.finished, timeout=8000):
-            worker.work()
+            worker.start()
     assert worker.current == worker.nz
 
 
-@pytest.fixture()
+@pytest.fixture
 def ref_view(make_napari_viewer):
     """basic viewer with data that we will use a few times
 
@@ -113,7 +115,7 @@ def ref_view(make_napari_viewer):
     viewer = make_napari_viewer()
 
     np.random.seed(0)
-    data = np.random.random((10, 10, 15))
+    data = np.random.random((2, 10, 10, 15))
     viewer.add_image(data)
     yield ref(viewer.window._qt_viewer)
     viewer.close()
@@ -123,7 +125,7 @@ def test_play_raises_index_errors(qtbot, ref_view):
     view = ref_view()
     # play axis is out of range
     with pytest.raises(IndexError):
-        view.dims.play(4, 20)
+        view.dims.play(5, 20)
 
     # data doesn't have 20 frames
     with pytest.raises(IndexError):
@@ -155,3 +157,40 @@ def increment(e):
         view.dims.play(2, 20)
     view.dims.dims.events.current_step.disconnect(increment)
     assert not view.dims.is_playing
+
+
+def test_change_play_axis(ref_view, qtbot):
+    """Make sure changing the play axis stops the current animation.
+
+    Prior to https://github.com/napari/napari/pull/7158, starting a new play
+    animation resulted in QThread warnings and could crash Python in
+    some environments. In the future, we may want to allow multiple
+    multiple simultaneous play axes [1]_, so this test should be changed
+    or removed when we do that.
+
+    ..[1] https://github.com/napari/napari/pull/6300#issuecomment-1757696072
+    """
+    view = ref_view()
+    with qtbot.waitSignal(view.dims._animation_thread.started):
+        view.dims.play(0, 20)
+    qtbot.waitUntil(lambda: view.dims.is_playing)
+    assert view.dims._animation_thread.slider.axis == 0
+    view.dims.play(1, 20)
+    assert view.dims._animation_thread.slider.axis == 1
+    assert view.dims.is_playing
+    with qtbot.waitSignal(view.dims._animation_thread.finished):
+        view.dims.stop()
+
+
+def test_change_play_fps(ref_view, qtbot):
+    """Make sure changing the play fps stops the current animation"""
+    view = ref_view()
+    with qtbot.waitSignal(view.dims._animation_thread.started):
+        view.dims.play(0, 20)
+    qtbot.waitUntil(lambda: view.dims.is_playing)
+    assert view.dims._animation_thread.slider.fps == 20
+    view.dims.play(0, 30)
+    assert view.dims._animation_thread.slider.fps == 30
+    assert view.dims.is_playing
+    with qtbot.waitSignal(view.dims._animation_thread.finished):
+        view.dims.stop()
diff --git a/napari/_qt/widgets/_tests/test_qt_range_slider_popup.py b/napari/_qt/widgets/_tests/test_qt_range_slider_popup.py
index a46d91145d2..027764c8e38 100644
--- a/napari/_qt/widgets/_tests/test_qt_range_slider_popup.py
+++ b/napari/_qt/widgets/_tests/test_qt_range_slider_popup.py
@@ -6,7 +6,7 @@
 range_ = (0, 500)
 
 
-@pytest.fixture()
+@pytest.fixture
 def popup(qtbot):
     popup = QRangeSliderPopup()
     popup.slider.setRange(*range_)
diff --git a/napari/_qt/widgets/_tests/test_qt_size_preview.py b/napari/_qt/widgets/_tests/test_qt_size_preview.py
index 82fd625c01d..5e5a5cec829 100644
--- a/napari/_qt/widgets/_tests/test_qt_size_preview.py
+++ b/napari/_qt/widgets/_tests/test_qt_size_preview.py
@@ -6,7 +6,7 @@
 )
 
 
-@pytest.fixture()
+@pytest.fixture
 def preview_widget(qtbot):
     def _preview_widget(**kwargs):
         widget = QtFontSizePreview(**kwargs)
@@ -18,7 +18,7 @@ def _preview_widget(**kwargs):
     return _preview_widget
 
 
-@pytest.fixture()
+@pytest.fixture
 def font_size_preview_widget(qtbot):
     def _font_size_preview_widget(**kwargs):
         widget = QtSizeSliderPreviewWidget(**kwargs)
diff --git a/napari/_qt/widgets/_tests/test_qt_tooltip.py b/napari/_qt/widgets/_tests/test_qt_tooltip.py
index a7d1f77caee..64da88b7334 100644
--- a/napari/_qt/widgets/_tests/test_qt_tooltip.py
+++ b/napari/_qt/widgets/_tests/test_qt_tooltip.py
@@ -1,5 +1,6 @@
 import os
 import sys
+from unittest.mock import patch
 
 import pytest
 from qtpy.QtCore import QPointF
@@ -13,7 +14,8 @@
     os.environ.get('CI', False) and sys.platform == 'darwin',
     reason='Timeouts when running on macOS CI',
 )
-def test_qt_tooltip_label(qtbot):
+@patch.object(QToolTip, 'showText')
+def test_qt_tooltip_label(show_text, qtbot):
     tooltip_text = 'Test QtToolTipLabel showing a tooltip'
     widget = QtToolTipLabel('Label with a tooltip')
     widget.setToolTip(tooltip_text)
@@ -25,5 +27,5 @@ def test_qt_tooltip_label(qtbot):
     pos = QPointF(widget.rect().center())
     event = QEnterEvent(pos, pos, QPointF(widget.pos()) + pos)
     widget.enterEvent(event)
-    qtbot.waitUntil(lambda: QToolTip.isVisible())
-    qtbot.waitUntil(lambda: QToolTip.text() == tooltip_text)
+    assert show_text.called
+    assert show_text.call_args[0][1] == tooltip_text
diff --git a/napari/_qt/widgets/_tests/test_qt_viewer_buttons.py b/napari/_qt/widgets/_tests/test_qt_viewer_buttons.py
index 56261a10d27..4f0b21b755b 100644
--- a/napari/_qt/widgets/_tests/test_qt_viewer_buttons.py
+++ b/napari/_qt/widgets/_tests/test_qt_viewer_buttons.py
@@ -1,13 +1,17 @@
+from unittest.mock import Mock
+
 import pytest
-from qtpy.QtCore import QPoint
+from qtpy.QtCore import QPoint, Qt
 from qtpy.QtWidgets import QApplication
 
+from napari._app_model._app import get_app_model
 from napari._qt.dialogs.qt_modal import QtPopup
 from napari._qt.widgets.qt_viewer_buttons import QtViewerButtons
 from napari.components.viewer_model import ViewerModel
+from napari.viewer import Viewer
 
 
-@pytest.fixture()
+@pytest.fixture
 def qt_viewer_buttons(qtbot):
     # create viewer model and buttons
     viewer = ViewerModel()
@@ -131,3 +135,48 @@ def test_ndisplay_button_popup(qt_viewer_buttons, qtbot):
         == viewer_buttons.perspective_slider.value()
         == 10
     )
+
+
+def test_toggle_ndisplay(mock_app_model, qt_viewer_buttons, qtbot):
+    """Check `toggle_ndisplay` works via `mouseClick`."""
+    viewer, viewer_buttons = qt_viewer_buttons
+    assert viewer_buttons.ndisplayButton
+
+    app = get_app_model()
+
+    assert viewer.dims.ndisplay == 2
+    with app.injection_store.register(
+        providers=[
+            (lambda: viewer, Viewer, 100),
+        ]
+    ):
+        qtbot.mouseClick(viewer_buttons.ndisplayButton, Qt.LeftButton)
+        assert viewer.dims.ndisplay == 3
+
+
+def test_transpose_rotate_button(monkeypatch, qt_viewer_buttons, qtbot):
+    """
+    Click should trigger `transpose_axes`. Alt/Option-click should trigger `rotate_layers.`
+    """
+    _, viewer_buttons = qt_viewer_buttons
+    assert viewer_buttons.transposeDimsButton
+
+    action_manager_mock = Mock(trigger=Mock())
+
+    # Monkeypatch the action_manager instance to prevent viewer error
+    monkeypatch.setattr(
+        'napari._qt.widgets.qt_viewer_buttons.action_manager',
+        action_manager_mock,
+    )
+    modifiers = Qt.AltModifier
+    qtbot.mouseClick(
+        viewer_buttons.transposeDimsButton, Qt.LeftButton, modifiers
+    )
+    action_manager_mock.trigger.assert_called_with('napari:rotate_layers')
+
+    trigger_mock = Mock()
+    monkeypatch.setattr(
+        'napari.utils.action_manager.ActionManager.trigger', trigger_mock
+    )
+    qtbot.mouseClick(viewer_buttons.transposeDimsButton, Qt.LeftButton)
+    trigger_mock.assert_called_with('napari:transpose_axes')
diff --git a/napari/_qt/widgets/_tests/test_shortcut_editor_widget.py b/napari/_qt/widgets/_tests/test_shortcut_editor_widget.py
index 70e0246afe3..e66eb419f2d 100644
--- a/napari/_qt/widgets/_tests/test_shortcut_editor_widget.py
+++ b/napari/_qt/widgets/_tests/test_shortcut_editor_widget.py
@@ -1,3 +1,4 @@
+import itertools
 import sys
 from unittest.mock import patch
 
@@ -11,13 +12,14 @@
 from napari.settings import get_settings
 from napari.utils.action_manager import action_manager
 from napari.utils.interactions import KEY_SYMBOLS
+from napari.utils.key_bindings import KeyBinding
 
 META_CONTROL_KEY = Qt.KeyboardModifier.ControlModifier
 if sys.platform == 'darwin':
     META_CONTROL_KEY = Qt.KeyboardModifier.MetaModifier
 
 
-@pytest.fixture()
+@pytest.fixture
 def shortcut_editor_widget(qtbot):
     # Always reset shortcuts (settings and action manager)
     get_settings().shortcuts.reset()
@@ -46,26 +48,73 @@ def test_shortcut_editor_defaults(
     shortcut_editor_widget()
 
 
-def test_layer_actions(shortcut_editor_widget):
+@pytest.mark.key_bindings
+def test_potentially_conflicting_actions(shortcut_editor_widget):
     widget = shortcut_editor_widget()
     assert widget.layer_combo_box.currentText() == widget.VIEWER_KEYBINDINGS
-    actions1 = widget._get_layer_actions()
-    assert actions1 == widget.key_bindings_strs[widget.VIEWER_KEYBINDINGS]
+    actions1 = widget._get_potential_conflicting_actions()
+    expected_actions1 = []
+    for group, keybindings in widget.key_bindings_strs.items():
+        expected_actions1.extend(
+            zip(itertools.repeat(group), keybindings.items())
+        )
+    assert actions1 == expected_actions1
     widget.layer_combo_box.setCurrentText('Labels layer')
-    actions2 = widget._get_layer_actions()
-    assert actions2 == {**widget.key_bindings_strs['Labels layer'], **actions1}
+    actions2 = widget._get_potential_conflicting_actions()
+    expected_actions2 = list(
+        zip(
+            itertools.repeat('Labels layer'),
+            widget.key_bindings_strs['Labels layer'].items(),
+        )
+    )
+    expected_actions2.extend(
+        zip(
+            itertools.repeat(widget.VIEWER_KEYBINDINGS),
+            widget.key_bindings_strs[widget.VIEWER_KEYBINDINGS].items(),
+        )
+    )
+    assert actions2 == expected_actions2
 
 
+@pytest.mark.key_bindings
 def test_mark_conflicts(shortcut_editor_widget, qtbot):
     widget = shortcut_editor_widget()
-    widget._table.item(0, widget._shortcut_col).setText('U')
+    ctrl_keybinding = KeyBinding.from_str('Ctrl')
+    u_keybinding = KeyBinding.from_str('U')
     act = widget._table.item(0, widget._action_col).text()
-    assert action_manager._shortcuts[act][0] == 'U'
+
+    # Add check for initial/default keybinding (first shortcuts column) and
+    # added one (second shortcuts column)
+    assert action_manager._shortcuts[act][0] == ctrl_keybinding
+    widget._table.item(0, widget._shortcut_col2).setText(str(u_keybinding))
+    assert action_manager._shortcuts[act][1] == str(u_keybinding)
+
+    # Check conflicts detection using `KeyBindingLike` params
+    # (`KeyBinding`, `str` and `int` representations of a shortcut)
+    with patch.object(WarnPopup, 'exec_') as mock:
+        assert not widget._mark_conflicts(ctrl_keybinding, 1)
+        assert mock.called
+    with patch.object(WarnPopup, 'exec_') as mock:
+        assert not widget._mark_conflicts(str(ctrl_keybinding), 1)
+        assert mock.called
+    with patch.object(WarnPopup, 'exec_') as mock:
+        assert not widget._mark_conflicts(int(ctrl_keybinding), 1)
+        assert mock.called
+
     with patch.object(WarnPopup, 'exec_') as mock:
-        assert not widget._mark_conflicts(action_manager._shortcuts[act][0], 1)
+        assert not widget._mark_conflicts(u_keybinding, 1)
         assert mock.called
-    assert widget._mark_conflicts('Y', 1)
-    # "Y" is arbitrary chosen and on conflict with existing shortcut should be changed
+    with patch.object(WarnPopup, 'exec_') as mock:
+        assert not widget._mark_conflicts(str(u_keybinding), 1)
+        assert mock.called
+
+    # Check no conflicts are found using `KeyBindingLike` params
+    # (`KeyBinding`, `str` and `int` representations of a shortcut)
+    # "H" is arbitrary chosen and on conflict with existing shortcut should be changed
+    h_keybinding = KeyBinding.from_str('H')
+    assert widget._mark_conflicts(h_keybinding, 1)
+    assert widget._mark_conflicts(str(h_keybinding), 1)
+    assert widget._mark_conflicts(int(h_keybinding), 1)
     qtbot.add_widget(widget._warn_dialog)
 
 
@@ -73,9 +122,9 @@ def test_restore_defaults(shortcut_editor_widget):
     widget = shortcut_editor_widget()
     shortcut = widget._table.item(0, widget._shortcut_col).text()
     assert shortcut == KEY_SYMBOLS['Ctrl']
-    widget._table.item(0, widget._shortcut_col).setText('R')
+    widget._table.item(0, widget._shortcut_col).setText('H')
     shortcut = widget._table.item(0, widget._shortcut_col).text()
-    assert shortcut == 'R'
+    assert shortcut == 'H'
     with patch(
         'napari._qt.widgets.qt_keyboard_settings.QMessageBox.question'
     ) as mock:
@@ -86,6 +135,7 @@ def test_restore_defaults(shortcut_editor_widget):
     assert shortcut == KEY_SYMBOLS['Ctrl']
 
 
+@pytest.mark.key_bindings
 @skip_local_focus
 @pytest.mark.parametrize(
     ('key', 'modifier', 'key_symbols'),
diff --git a/napari/_qt/widgets/qt_dims.py b/napari/_qt/widgets/qt_dims.py
index a984d1c9b14..e687836cf74 100644
--- a/napari/_qt/widgets/qt_dims.py
+++ b/napari/_qt/widgets/qt_dims.py
@@ -6,7 +6,10 @@
 from qtpy.QtGui import QFont, QFontMetrics
 from qtpy.QtWidgets import QSizePolicy, QVBoxLayout, QWidget
 
-from napari._qt.widgets.qt_dims_slider import QtDimSliderWidget
+from napari._qt.widgets.qt_dims_slider import (
+    AnimationThread,
+    QtDimSliderWidget,
+)
 from napari.components.dims import Dims
 from napari.settings._constants import LoopMode
 from napari.utils.translations import trans
@@ -44,8 +47,7 @@ def __init__(self, dims: Dims, parent=None) -> None:
         # True / False if slider is or is not displayed
         self._displayed_sliders = []
 
-        self._animation_thread = None
-        self._animation_worker = None
+        self._animation_thread = AnimationThread(self)
 
         # Initialises the layout:
         layout = QVBoxLayout()
@@ -293,23 +295,27 @@ def play(
         if axis >= self.dims.ndim:
             raise IndexError(trans._('axis argument out of range'))
 
-        if self.is_playing:
-            if self._animation_worker.axis == axis:
-                self.slider_widgets[axis]._update_play_settings(
-                    fps, loop_mode, frame_range
-                )
-                return
-
-            self.stop()
+        if self.is_playing and self._animation_thread.axis == axis:
+            self.slider_widgets[axis]._update_play_settings(
+                fps, loop_mode, frame_range
+            )
+            return
 
         # we want to avoid playing a dimension that does not have a slider
         # (like X or Y, or a third dimension in volume view.)
         if self._displayed_sliders[axis]:
-            work = self.slider_widgets[axis]._play(fps, loop_mode, frame_range)
-            if work:
-                self._animation_worker, self._animation_thread = work
+            if self._animation_thread.isRunning():
+                self._animation_thread.slider.play_button._handle_stop()
+            self.slider_widgets[axis]._update_play_settings(
+                fps, loop_mode, frame_range
+            )
+            self._animation_thread.set_slider(self.slider_widgets[axis])
+            self._animation_thread.frame_requested.connect(self._set_frame)
+            if not self._animation_thread.isRunning():
+                self._animation_thread.start()
             else:
-                self._animation_worker, self._animation_thread = None, None
+                self._animation_thread.slider.play_button._handle_start()
+
         else:
             warnings.warn(
                 trans._(
@@ -321,23 +327,13 @@ def play(
     @Slot()
     def stop(self):
         """Stop axis animation"""
-        if self._animation_worker is not None:
-            # Thread will be stop by the worker
-            self._animation_worker._stop()
-
-    @Slot()
-    def cleaned_worker(self):
-        self._animation_thread = None
-        self._animation_worker = None
-        self.dims._play_ready = True
+        self._animation_thread._stop()
 
     @property
     def is_playing(self):
         """Return True if any axis is currently animated."""
         try:
-            return (
-                self._animation_thread and self._animation_thread.isRunning()
-            )
+            return not self._animation_thread._waiter.is_set()
         except RuntimeError as e:  # pragma: no cover
             if (
                 'wrapped C/C++ object of type' not in e.args[0]
diff --git a/napari/_qt/widgets/qt_dims_slider.py b/napari/_qt/widgets/qt_dims_slider.py
index 8c7d3299b66..0f1286a025a 100644
--- a/napari/_qt/widgets/qt_dims_slider.py
+++ b/napari/_qt/widgets/qt_dims_slider.py
@@ -1,8 +1,9 @@
+import threading
 from typing import TYPE_CHECKING, Optional
 from weakref import ref
 
 import numpy as np
-from qtpy.QtCore import QObject, Qt, QThread, QTimer, Signal, Slot
+from qtpy.QtCore import QObject, Qt, QThread, Signal, Slot
 from qtpy.QtGui import QIntValidator
 from qtpy.QtWidgets import (
     QApplication,
@@ -17,10 +18,9 @@
     QPushButton,
     QWidget,
 )
-from superqt import QElidingLineEdit, ensure_object_thread
+from superqt import QElidingLineEdit
 
 from napari._qt.dialogs.qt_modal import QtPopup
-from napari._qt.qthreading import _new_worker_qthread
 from napari._qt.widgets.qt_scrollbar import ModifiedScrollBar
 from napari.settings import get_settings
 from napari.settings._constants import LoopMode
@@ -371,58 +371,6 @@ def _update_play_settings(self, fps, loop_mode, frame_range):
         if frame_range is not None:
             self.frame_range = frame_range
 
-    def _play(
-        self,
-        fps: Optional[float] = None,
-        loop_mode: Optional[str] = None,
-        frame_range: Optional[tuple[int, int]] = None,
-    ):
-        """Animate (play) axis. Same API as QtDims.play()
-
-        Putting the AnimationWorker logic here makes it easier to call
-        QtDims.play(axis), or hit the keybinding, and have each axis remember
-        it's own settings (fps, mode, etc...).
-
-        Parameters
-        ----------
-        fps : float
-            Frames per second for animation.
-        loop_mode : napari._qt._constants.LoopMode
-            Loop mode for animation.
-            Available options for the loop mode string enumeration are:
-            - LoopMode.ONCE
-                Animation will stop once movie reaches the max frame
-                (if fps > 0) or the first frame (if fps < 0).
-            - LoopMode.LOOP
-                Movie will return to the first frame after reaching
-                the last frame, looping continuously until stopped.
-            - LoopMode.BACK_AND_FORTH
-                Movie will loop continuously until stopped,
-                reversing direction when the maximum or minimum frame
-                has been reached.
-        frame_range : tuple(int, int)
-            Frame range as tuple/list with range (minimum_frame, maximum_frame)
-        """
-
-        # having this here makes sure that using the QtDims.play() API
-        # keeps the play preferences synchronized with the play_button.popup
-        self._update_play_settings(fps, loop_mode, frame_range)
-
-        # setting fps to 0 just stops the animation
-        if fps == 0:
-            return None
-
-        worker, thread = _new_worker_qthread(
-            AnimationWorker,
-            self,
-            _start_thread=True,
-            _connect={'frame_requested': self.qt_dims._set_frame},
-        )
-        thread.finished.connect(self.qt_dims.cleaned_worker)
-        thread.finished.connect(self.play_stopped)
-        self.play_started.emit()
-        return worker, thread
-
     def resizeEvent(self, event):
         """Emit a signal to inform about a size change."""
         self.size_changed.emit()
@@ -585,7 +533,7 @@ def _handle_stop(self):
         self.style().polish(self)
 
 
-class AnimationWorker(QObject):
+class AnimationThread(QThread):
     """A thread to keep the animation timer independent of the main event loop.
 
     This prevents mouseovers and other events from causing animation lag. See
@@ -593,36 +541,40 @@ class AnimationWorker(QObject):
     """
 
     frame_requested = Signal(int, int)  # axis, point
-    finished = Signal()
-    started = Signal()
 
-    def __init__(self, slider) -> None:
-        # FIXME there are attributes defined outsid of __init__.
-        super().__init__()
+    def __init__(self, parent: Optional[QObject] = None) -> None:
+        # FIXME there are attributes defined outside of __init__.
+        super().__init__(parent=parent)
         self._interval = 1
-        self.slider = slider
-        self.dims = slider.dims
-        self.axis = slider.axis
-        self.loop_mode = slider.loop_mode
+        self.slider = None
+        self._waiter = threading.Event()
 
-        self.timer = QTimer()
+    def run(self):
+        self.work()
 
-        slider.fps_changed.connect(self.set_fps)
-        slider.mode_changed.connect(self.set_loop_mode)
-        slider.range_changed.connect(self.set_frame_range)
+    def set_slider(self, slider):
+        prev_slider = self.slider
+        self.slider = slider
         self.set_fps(self.slider.fps)
         self.set_frame_range(slider.frame_range)
-
-        # after dims.set_current_step is called, it will emit a dims.events.current_step()
-        # we use this to update this threads current frame (in case it
-        # was some other event that updated the axis)
-        self.dims.events.current_step.connect(self._on_axis_changed)
-        self.current = max(self.dims.current_step[self.axis], self.min_point)
+        if prev_slider is not None:
+            prev_slider.fps_changed.disconnect(self.set_fps)
+            prev_slider.range_changed.disconnect(self.set_frame_range)
+            prev_slider.dims.events.current_step.disconnect(
+                self._on_axis_changed
+            )
+            self.finished.disconnect(prev_slider.play_button._handle_stop)
+            self.started.disconnect(prev_slider.play_button._handle_start)
+        slider.fps_changed.connect(self.set_fps)
+        slider.range_changed.connect(self.set_frame_range)
+        slider.dims.events.current_step.connect(self._on_axis_changed)
+        self.finished.connect(slider.play_button._handle_stop)
+        self.started.connect(slider.play_button._handle_start)
+        self.current = max(
+            slider.dims.current_step[slider.axis], self.min_point
+        )
         self.current = min(self.current, self.max_point)
 
-        self.timer.setSingleShot(True)
-        self.timer.timeout.connect(self.advance)
-
     @property
     def interval(self):
         return self._interval
@@ -630,7 +582,6 @@ def interval(self):
     @interval.setter
     def interval(self, value):
         self._interval = value
-        self.timer.setInterval(int(self._interval))
 
     @Slot()
     def work(self):
@@ -642,18 +593,18 @@ def work(self):
                 self.frame_requested.emit(self.axis, self.min_point)
             elif self.step < 0 and self.current <= self.min_point + 1:
                 self.frame_requested.emit(self.axis, self.max_point)
-            self.timer.start()
         else:
             # immediately advance one frame
             self.advance()
-        self.started.emit()
+        self._waiter.clear()
+        self._waiter.wait(self.interval / 1000)
+        while not self._waiter.is_set():
+            self.advance()
+            self._waiter.wait(self.interval / 1000)
 
-    @ensure_object_thread
     def _stop(self):
         """Stop the animation."""
-        if self.timer.isActive():
-            self.timer.stop()
-            self.finish()
+        self._waiter.set()
 
     @Slot(float)
     def set_fps(self, fps):
@@ -701,28 +652,6 @@ def set_frame_range(self, frame_range):
             )
         self.max_point += 1  # range is inclusive
 
-    @Slot(str)
-    def set_loop_mode(self, mode):
-        """Set the loop mode for the animation.
-
-        Parameters
-        ----------
-        mode : str
-            Loop mode for animation.
-            Available options for the loop mode string enumeration are:
-            - LoopMode.ONCE
-                Animation will stop once movie reaches the max frame
-                (if fps > 0) or the first frame (if fps < 0).
-            - LoopMode.LOOP
-                Movie will return to the first frame after reaching
-                the last frame, looping continuously until stopped.
-            - LoopMode.BACK_AND_FORTH
-                Movie will loop continuously until stopped,
-                reversing direction when the maximum or minimum frame
-                has been reached.
-        """
-        self.loop_mode = LoopMode(mode)
-
     @Slot()
     def advance(self):
         """Advance the current frame in the animation.
@@ -755,25 +684,26 @@ def advance(self):
                 return self.finish()
         with self.dims.events.current_step.blocker(self._on_axis_changed):
             self.frame_requested.emit(self.axis, self.current)
-        self.timer.start()
+        # self.timer.start()
         return None
 
+    @property
+    def loop_mode(self):
+        return self.slider.loop_mode
+
+    @property
+    def axis(self):
+        return self.slider.axis
+
+    @property
+    def dims(self):
+        return self.slider.dims
+
     def finish(self):
         """Emit the finished event signal."""
-        self.finished.emit()
+        self._stop()
 
     def _on_axis_changed(self):
         """Update the current frame if the axis has changed."""
         # slot for external events to update the current frame
         self.current = self.dims.current_step[self.axis]
-
-    def moveToThread(self, thread: QThread):
-        """Move the animation to a given thread.
-
-        Parameters
-        ----------
-        thread : QThread
-            The thread to move the animation to.
-        """
-        super().moveToThread(thread)
-        self.timer.moveToThread(thread)
diff --git a/napari/_qt/widgets/qt_dims_sorter.py b/napari/_qt/widgets/qt_dims_sorter.py
index 7f92c14a949..3e3f2f866ee 100644
--- a/napari/_qt/widgets/qt_dims_sorter.py
+++ b/napari/_qt/widgets/qt_dims_sorter.py
@@ -17,7 +17,7 @@ def set_dims_order(dims: Dims, order: tuple[int, ...]):
     order : tuple of int
         New dimension order.
     """
-    if type(order[0]) == AxisModel:
+    if type(order[0]) is AxisModel:
         order = [a.axis for a in order]
     dims.order = order
 
@@ -50,7 +50,7 @@ def __init__(self, dims: Dims, parent: QWidget) -> None:
 
         self.view = QtListView(self.axis_list)
         if len(self.axis_list) <= 2:
-            # prevent exess space in popup
+            # prevent excess space in popup
             self.view.setSizeAdjustPolicy(QtListView.AdjustToContents)
 
         layout = QGridLayout()
diff --git a/napari/_qt/widgets/qt_keyboard_settings.py b/napari/_qt/widgets/qt_keyboard_settings.py
index 02cf6a38604..484a008ee71 100644
--- a/napari/_qt/widgets/qt_keyboard_settings.py
+++ b/napari/_qt/widgets/qt_keyboard_settings.py
@@ -1,4 +1,5 @@
 import contextlib
+import itertools
 import sys
 from collections import OrderedDict
 from typing import Optional
@@ -94,6 +95,10 @@ def __init__(
                     all_actions.pop(name)
             self.key_bindings_strs[f'{layer.__name__} layer'] = actions
 
+        # Don't include actions without keymapproviders
+        for action_name in all_actions.copy():
+            if all_actions[action_name].keymapprovider is None:
+                all_actions.pop(action_name)
         # Left over actions can go here.
         self.key_bindings_strs[self.VIEWER_KEYBINDINGS] = all_actions
 
@@ -288,16 +293,44 @@ def _set_table(self, layer_str: str = ''):
             item.setFlags(Qt.ItemFlag.NoItemFlags)
             self._table.setItem(0, 0, item)
 
-    def _get_layer_actions(self):
+    def _get_potential_conflicting_actions(self):
+        """
+        Get all actions we want to avoid keybinding conflicts with.
+
+        If current selected keybinding group is a layer, return
+        the selected layer actions and viewer actions.
+        If the current selected keybinding group is viewer,
+        return viewer actions and actions from all layers.
+
+        Returns
+        -------
+        actions_all: list[tuple[str, tuple[str, Action]]]
+            Tuple of group names and actions, to avoid keybinding
+            conflicts with.
+            Format:
+                [
+                    ('keybinding_group', ('action_name', Action)),
+                    ...
+                ]
+        """
         current_layer_text = self.layer_combo_box.currentText()
-        layer_actions = self.key_bindings_strs[current_layer_text]
-        actions_all = layer_actions.copy()
-        if current_layer_text is not self.VIEWER_KEYBINDINGS:
-            viewer_actions = self.key_bindings_strs[self.VIEWER_KEYBINDINGS]
+        actions_all = list(self._get_group_actions(current_layer_text))
 
-            actions_all.update(viewer_actions)
+        if current_layer_text != self.VIEWER_KEYBINDINGS:
+            actions_all = list(self._get_group_actions(current_layer_text))
+            actions_all.extend(
+                self._get_group_actions(self.VIEWER_KEYBINDINGS)
+            )
+        else:
+            actions_all = []
+            for group in self.key_bindings_strs:
+                actions_all.extend(self._get_group_actions(group))
         return actions_all
 
+    def _get_group_actions(self, group_name):
+        group_actions = self.key_bindings_strs[group_name]
+        return zip(itertools.repeat(group_name), group_actions.items())
+
     def _restore_shortcuts(self, row):
         action_name = self._table.item(row, self._action_col).text()
         shortcuts = action_manager._shortcuts.get(action_name, [])
@@ -311,42 +344,74 @@ def _restore_shortcuts(self, row):
                 else ''
             )
 
+    def _show_conflicts_warning(
+        self, new_shortcut, conflicting_actions, conflicting_rows
+    ):
+        # create string listing info of all the conflicts found
+        conflicting_actions_string = '<ul>'
+        for group, action_description in conflicting_actions:
+            conflicting_actions_string += trans._(
+                '<li><b>{action_description}</b> in the <b>{group}</b> group</li>',
+                action_description=action_description,
+                group=group,
+            )
+        conflicting_actions_string += '</ul>'
+
+        # show warning symbols
+        self._show_warning_icons(conflicting_rows)
+
+        # show warning message
+        message = trans._(
+            'The keybinding <b>{new_shortcut}</b> is already assigned to:'
+            '{conflicting_actions_string}'
+            'Change or clear conflicting shortcuts before assigning <b>{new_shortcut}</b> to this one.',
+            new_shortcut=new_shortcut,
+            conflicting_actions_string=conflicting_actions_string,
+        )
+        self._show_warning(conflicting_rows[0], message)
+
+        self._restore_shortcuts(conflicting_rows[0])
+
+        self._cleanup_warning_icons(conflicting_rows)
+
     def _mark_conflicts(self, new_shortcut, row) -> bool:
         # Go through all layer actions to determine if the new shortcut is already here.
         current_action = self._table.item(row, self._action_col).text()
-        actions_all = self._get_layer_actions()
+        actions_all = self._get_potential_conflicting_actions()
         current_item = self._table.currentItem()
-        for row1, (action_name, action) in enumerate(actions_all.items()):
+        conflicting_rows = [row]
+        conflicting_actions = []
+        for conflicting_row, (group, (action_name, action)) in enumerate(
+            actions_all
+        ):
             shortcuts = action_manager._shortcuts.get(action_name, [])
 
-            if new_shortcut not in shortcuts:
+            if Shortcut(new_shortcut).qt not in [
+                Shortcut(shortcut).qt for shortcut in shortcuts
+            ]:
                 continue
+
             # Shortcut is here (either same action or not), don't replace in settings.
             if action_name != current_action:
-                # the shortcut is saved to a different action
-
-                # show warning symbols
-                self._show_warning_icons([row, row1])
-
-                # show warning message
-                message = trans._(
-                    'The keybinding <b>{new_shortcut}</b>  is already assigned to <b>{action_description}</b>; change or clear that shortcut before assigning <b>{new_shortcut}</b> to this one.',
-                    new_shortcut=new_shortcut,
-                    action_description=action.description,
-                )
-                self._show_warning(row, message)
-
-                self._restore_shortcuts(row)
-
-                self._cleanup_warning_icons([row, row1])
-
-                return False
+                # the shortcut is saved to a different action, save conflicting shortcut info
+                if conflicting_row < self._table.rowCount():
+                    # only save row number for conflicts that are inside the current table
+                    conflicting_rows.append(conflicting_row)
+                conflicting_actions.append((group, action.description))
 
             # This shortcut was here.  Reformat and reset text.
             format_shortcut = Shortcut(new_shortcut).platform
             with lock_keybind_update(self):
                 current_item.setText(format_shortcut)
 
+        if len(conflicting_actions) > 0:
+            # show conflicts message and mark conflicting rows as necessary
+            self._show_conflicts_warning(
+                new_shortcut, conflicting_actions, conflicting_rows
+            )
+
+            return False
+
         return True
 
     def _show_bind_shortcut_error(
@@ -387,19 +452,6 @@ def _set_keybinding(self, row, col):
         self._table.setCurrentItem(self._table.item(row, col))
 
         if col in {self._shortcut_col, self._shortcut_col2}:
-            # Get all layer actions and viewer actions in order to determine
-            # the new shortcut is not already set to an action.
-
-            current_layer_text = self.layer_combo_box.currentText()
-            layer_actions = self.key_bindings_strs[current_layer_text]
-            actions_all = layer_actions.copy()
-            if current_layer_text is not self.VIEWER_KEYBINDINGS:
-                viewer_actions = self.key_bindings_strs[
-                    self.VIEWER_KEYBINDINGS
-                ]
-
-                actions_all.update(viewer_actions)
-
             # get the current item from shortcuts column
             current_item = self._table.currentItem()
             new_shortcut = Shortcut.parse_platform(current_item.text())
@@ -521,14 +573,6 @@ def _show_warning(self, row: int, message: str) -> None:
             text=message,
         )
         self._warn_dialog.move(global_point)
-
-        # Styling adjustments.
-        self._warn_dialog.resize(250, self._warn_dialog.sizeHint().height())
-
-        self._warn_dialog._message.resize(
-            200, self._warn_dialog._message.sizeHint().height()
-        )
-
         self._warn_dialog.exec_()
 
     def value(self):
@@ -548,6 +592,14 @@ def value(self):
 
         return value
 
+    def setValue(self, state):
+        for action, shortcuts in state.items():
+            action_manager.unbind_shortcut(action)
+            for shortcut in shortcuts:
+                action_manager.bind_shortcut(action, shortcut)
+
+        self._set_table(layer_str=self.layer_combo_box.currentText())
+
 
 class ShortcutDelegate(QItemDelegate):
     """Delegate that handles when user types in new shortcut."""
diff --git a/napari/_qt/widgets/qt_splash_screen.py b/napari/_qt/widgets/qt_splash_screen.py
index 401e9b5bcf1..a0e0f962da9 100644
--- a/napari/_qt/widgets/qt_splash_screen.py
+++ b/napari/_qt/widgets/qt_splash_screen.py
@@ -2,12 +2,12 @@
 from qtpy.QtGui import QPixmap
 from qtpy.QtWidgets import QSplashScreen
 
-from napari._qt.qt_event_loop import NAPARI_ICON_PATH, get_app
+from napari._qt.qt_event_loop import NAPARI_ICON_PATH, get_qapp
 
 
 class NapariSplashScreen(QSplashScreen):
     def __init__(self, width=360) -> None:
-        get_app()
+        get_qapp()
         pm = QPixmap(NAPARI_ICON_PATH).scaled(
             width,
             width,
diff --git a/napari/_qt/widgets/qt_theme_sample.py b/napari/_qt/widgets/qt_theme_sample.py
index bbb67381f0d..dcdc462fafe 100644
--- a/napari/_qt/widgets/qt_theme_sample.py
+++ b/napari/_qt/widgets/qt_theme_sample.py
@@ -157,11 +157,11 @@ def screenshot(self, path=None):
     import logging
     import sys
 
-    from napari._qt.qt_event_loop import get_app
+    from napari._qt.qt_event_loop import get_qapp
     from napari.utils.theme import available_themes
 
     themes = [sys.argv[1]] if len(sys.argv) > 1 else available_themes()
-    app = get_app()
+    app = get_qapp()
     widgets = []
     for n, theme in enumerate(themes):
         try:
diff --git a/napari/_qt/widgets/qt_viewer_buttons.py b/napari/_qt/widgets/qt_viewer_buttons.py
index 78968027132..b18743a25f9 100644
--- a/napari/_qt/widgets/qt_viewer_buttons.py
+++ b/napari/_qt/widgets/qt_viewer_buttons.py
@@ -2,8 +2,9 @@
 from functools import partial, wraps
 from typing import TYPE_CHECKING
 
-from qtpy.QtCore import QPoint, Qt
+from qtpy.QtCore import QEvent, QPoint, Qt
 from qtpy.QtWidgets import (
+    QApplication,
     QFormLayout,
     QFrame,
     QHBoxLayout,
@@ -141,8 +142,14 @@ def __init__(self, viewer: 'ViewerModel') -> None:
         rdb.customContextMenuRequested.connect(self._open_roll_popup)
 
         self.transposeDimsButton = QtViewerPushButton(
-            'transpose', action='napari:transpose_axes'
+            'transpose',
+            action='napari:transpose_axes',
+            extra_tooltip_text=trans._(
+                '\nAlt/option-click to rotate visible axes'
+            ),
         )
+        self.transposeDimsButton.installEventFilter(self)
+
         self.resetViewButton = QtViewerPushButton(
             'home', action='napari:reset_view'
         )
@@ -183,6 +190,18 @@ def _set_ndisplay_mode_checkstate(event):
         layout.addStretch(0)
         self.setLayout(layout)
 
+    def eventFilter(self, qobject, event):
+        """Have Alt/Option key rotate layers with the transpose button."""
+        modifiers = QApplication.keyboardModifiers()
+        if (
+            modifiers == Qt.AltModifier
+            and qobject == self.transposeDimsButton
+            and event.type() == QEvent.MouseButtonPress
+        ):
+            action_manager.trigger('napari:rotate_layers')
+            return True
+        return False
+
     def open_perspective_popup(self):
         """Show a slider to control the viewer `camera.perspective`."""
         if self.viewer.dims.ndisplay != 3:
@@ -395,7 +414,12 @@ class QtViewerPushButton(QPushButton):
 
     @_omit_viewer_args
     def __init__(
-        self, button_name: str, tooltip: str = '', slot=None, action: str = ''
+        self,
+        button_name: str,
+        tooltip: str = '',
+        slot=None,
+        action: str = '',
+        extra_tooltip_text: str = '',
     ) -> None:
         super().__init__()
 
@@ -404,4 +428,6 @@ def __init__(
         if slot is not None:
             self.clicked.connect(slot)
         if action:
-            action_manager.bind_button(action, self)
+            action_manager.bind_button(
+                action, self, extra_tooltip_text=extra_tooltip_text
+            )
diff --git a/napari/_qt/widgets/qt_viewer_dock_widget.py b/napari/_qt/widgets/qt_viewer_dock_widget.py
index 1b46bff7118..4280b8c14d1 100644
--- a/napari/_qt/widgets/qt_viewer_dock_widget.py
+++ b/napari/_qt/widgets/qt_viewer_dock_widget.py
@@ -18,6 +18,7 @@
 )
 
 from napari._qt.utils import combine_widgets, qt_signals_blocked
+from napari.settings import get_settings
 from napari.utils.translations import trans
 
 if TYPE_CHECKING:
@@ -33,6 +34,13 @@
     shortcut='{shortcut}',
 )
 
+dock_area_to_str = {
+    Qt.DockWidgetArea.LeftDockWidgetArea: 'left',
+    Qt.DockWidgetArea.RightDockWidgetArea: 'right',
+    Qt.DockWidgetArea.TopDockWidgetArea: 'top',
+    Qt.DockWidgetArea.BottomDockWidgetArea: 'bottom',
+}
+
 
 class QtViewerDockWidget(QDockWidget):
     """Wrap a QWidget in a QDockWidget and forward viewer events
@@ -152,6 +160,17 @@ def __init__(
         self.setTitleBarWidget(self.title)
         self.visibilityChanged.connect(self._on_visibility_changed)
 
+        self.dockLocationChanged.connect(self._update_default_dock_area)
+
+    def _update_default_dock_area(self, value):
+        if value not in dock_area_to_str:
+            return
+        settings = get_settings()
+        settings.application.plugin_widget_positions[self.name] = (
+            dock_area_to_str[value]
+        )
+        settings._maybe_save()
+
     @property
     def _parent(self):
         """
diff --git a/napari/_tests/test_adding_removing.py b/napari/_tests/test_adding_removing.py
index 914d512f08e..68e9354af41 100644
--- a/napari/_tests/test_adding_removing.py
+++ b/napari/_tests/test_adding_removing.py
@@ -37,7 +37,7 @@ def test_layers_removed_on_close(make_napari_viewer):
 def test_layer_multiple_viewers(make_napari_viewer):
     """Test layer on multiple viewers."""
     # Check that a layer can be added and removed from
-    # mutliple viewers. See https://github.com/napari/napari/issues/1503
+    # multiple viewers. See https://github.com/napari/napari/issues/1503
     # for more detail.
     viewer_a = make_napari_viewer()
     viewer_b = make_napari_viewer()
diff --git a/napari/_tests/test_advanced.py b/napari/_tests/test_advanced.py
index 8a8a5d39981..6b2eac081d1 100644
--- a/napari/_tests/test_advanced.py
+++ b/napari/_tests/test_advanced.py
@@ -157,7 +157,7 @@ def test_range_one_images_and_points(make_napari_viewer):
     assert np.sum(view.dims._displayed_sliders) == 3
 
 
-@pytest.mark.enable_console()
+@pytest.mark.enable_console
 @pytest.mark.filterwarnings('ignore::DeprecationWarning:jupyter_client')
 def test_update_console(make_napari_viewer):
     """Test updating the console with local variables."""
@@ -181,7 +181,7 @@ def test_update_console(make_napari_viewer):
         del viewer.window._qt_viewer.console.shell.user_ns[k]
 
 
-@pytest.mark.enable_console()
+@pytest.mark.enable_console
 @pytest.mark.filterwarnings('ignore::DeprecationWarning:jupyter_client')
 def test_update_lazy_console(make_napari_viewer, caplog):
     """Test updating the console with local variables,
diff --git a/napari/_tests/test_cli.py b/napari/_tests/test_cli.py
index ad93d65e1a4..ad164b062ea 100644
--- a/napari/_tests/test_cli.py
+++ b/napari/_tests/test_cli.py
@@ -8,7 +8,7 @@
 from napari import __main__
 
 
-@pytest.fixture()
+@pytest.fixture
 def mock_run():
     """mock to prevent starting the event loop."""
     with mock.patch('napari._qt.widgets.qt_splash_screen.NapariSplashScreen'):
diff --git a/napari/_tests/test_conftest_fixtures.py b/napari/_tests/test_conftest_fixtures.py
index b60f8213031..1bf063e3e4f 100644
--- a/napari/_tests/test_conftest_fixtures.py
+++ b/napari/_tests/test_conftest_fixtures.py
@@ -14,7 +14,7 @@ def run(self):
         self.mutex.lock()
 
 
-@pytest.mark.disable_qthread_start()
+@pytest.mark.disable_qthread_start
 def test_disable_qthread(qapp):
     t = _TestThread()
     t.mutex.lock()
@@ -32,7 +32,7 @@ def test_qthread_running(qtbot):
     qtbot.waitUntil(t.isFinished, timeout=2000)
 
 
-@pytest.mark.disable_qtimer_start()
+@pytest.mark.disable_qtimer_start
 def test_disable_qtimer(qtbot):
     t = QTimer()
     t.setInterval(100)
diff --git a/napari/_tests/test_examples.py b/napari/_tests/test_examples.py
index 210d7142919..ee897eb12f7 100644
--- a/napari/_tests/test_examples.py
+++ b/napari/_tests/test_examples.py
@@ -29,7 +29,6 @@
     'embed_ipython_.py',  # fails without monkeypatch
     'new_theme.py',  # testing theme is extremely slow on CI
     'dynamic-projections-dask.py',  # extremely slow / does not finish
-    'surface_multi_texture_.py',  # resource not available
 ]
 # To skip examples during docs build end name with `_.py`
 
@@ -52,7 +51,7 @@
 if os.getenv('CI') and os.name == 'nt' and 'to_screenshot.py' in examples:
     examples.remove('to_screenshot.py')
 
-@pytest.fixture()
+@pytest.fixture
 def _example_monkeypatch(monkeypatch):
     # hide viewer window
     monkeypatch.setattr(Window, 'show', lambda *a: None)
diff --git a/napari/_tests/test_key_bindings.py b/napari/_tests/test_key_bindings.py
index d67b198bb67..810b5607dce 100644
--- a/napari/_tests/test_key_bindings.py
+++ b/napari/_tests/test_key_bindings.py
@@ -1,9 +1,11 @@
 from unittest.mock import Mock
 
 import numpy as np
+import pytest
 from vispy import keys
 
 
+@pytest.mark.key_bindings
 def test_viewer_key_bindings(make_napari_viewer):
     """Test adding key bindings to the viewer"""
     np.random.seed(0)
@@ -76,6 +78,7 @@ def key_shift_callback(v):
     mock_shift_release.reset_mock()
 
 
+@pytest.mark.key_bindings
 def test_layer_key_bindings(make_napari_viewer):
     """Test adding key bindings to a layer"""
     np.random.seed(0)
diff --git a/napari/_tests/test_magicgui.py b/napari/_tests/test_magicgui.py
index 48ab814c624..4ac2e32c526 100644
--- a/napari/_tests/test_magicgui.py
+++ b/napari/_tests/test_magicgui.py
@@ -11,6 +11,7 @@
 from napari._tests.utils import layer_test_data
 from napari.layers import Image, Labels, Layer
 from napari.utils._proxies import PublicOnlyProxy
+from napari.utils.migrations import _DeprecatingDict
 from napari.utils.misc import all_subclasses
 
 if TYPE_CHECKING:
@@ -81,9 +82,6 @@ def func_optional(a: bool) -> 'typing.Optional[napari.types.ImageData]':
     assert len(viewer.layers) == 1
 
 
-@pytest.mark.skipif(
-    sys.version_info < (3, 9), reason='Futures not subscriptable before py3.9'
-)
 @pytest.mark.parametrize(('LayerType', 'data', 'ndim'), test_data)
 def test_magicgui_add_future_data(
     qtbot, make_napari_viewer, LayerType, data, ndim
@@ -321,7 +319,10 @@ def test_mgui_forward_refs(name, monkeypatch):
     monkeypatch.delitem(sys.modules, 'napari')
     monkeypatch.delitem(sys.modules, 'napari.viewer')
     monkeypatch.delitem(sys.modules, 'napari.types')
-    # need to clear all of these submodules too, otherise the layers are oddly not
+    monkeypatch.setattr(
+        'napari.utils.action_manager.action_manager._actions', {}
+    )
+    # need to clear all of these submodules too, otherwise the layers are oddly not
     # subclasses of napari.layers.Layer, and napari.layers.NAMES
     # oddly ends up as an empty set
     for m in list(sys.modules):
@@ -346,3 +347,21 @@ def test_layers_populate_immediately(make_napari_viewer):
     assert not len(labels_layer.choices)
     viewer.window.add_dock_widget(labels_layer)
     assert len(labels_layer.choices) == 1
+
+
+def test_from_layer_data_tuple_accept_deprecating_dict(make_napari_viewer):
+    """Test that a function returning a layer data tuple runs without error."""
+    viewer = make_napari_viewer()
+
+    @magicgui
+    def from_layer_data_tuple() -> types.LayerDataTuple:
+        data = np.zeros((10, 10))
+        meta = _DeprecatingDict({'name': 'test_image'})
+        layer_type = 'image'
+        return data, meta, layer_type
+
+    viewer.window.add_dock_widget(from_layer_data_tuple)
+    from_layer_data_tuple()
+    assert len(viewer.layers) == 1
+    assert isinstance(viewer.layers[0], Image)
+    assert viewer.layers[0].name == 'test_image'
diff --git a/napari/_tests/test_mouse_bindings.py b/napari/_tests/test_mouse_bindings.py
index 54084088831..6df2ef14ce4 100644
--- a/napari/_tests/test_mouse_bindings.py
+++ b/napari/_tests/test_mouse_bindings.py
@@ -2,8 +2,15 @@
 from unittest.mock import Mock
 
 import numpy as np
+import pytest
 
 from napari._tests.utils import skip_on_win_ci
+from napari.layers import Image
+from napari.layers.base._base_constants import InteractionBoxHandle
+from napari.layers.base._base_mouse_bindings import (
+    highlight_box_handles,
+    transform_with_box,
+)
 
 
 @skip_on_win_ci
@@ -248,3 +255,37 @@ def move_callback(_layer, event):
     mock_drag.method.assert_not_called()
     mock_release.method.assert_not_called()
     mock_move.method.assert_not_called()
+
+
+@pytest.mark.parametrize(
+    ('position', 'dims_displayed', 'nearby_handle'),
+    [
+        # Postion inside the transform box space so the inside value should be set as selected
+        ([0, 3], [0, 1], InteractionBoxHandle.INSIDE),
+        ([0, 3, 3], [1, 2], InteractionBoxHandle.INSIDE),
+        # Postion outside the transform box space so no handle should be set as selected
+        ([0, 11], [0, 1], None),
+        ([0, 11, 11], [1, 2], None),
+        # When 3 dimensions are being displayed no `highlight_box_handles` logic should be run
+        ([0, 3, 3], [0, 1, 2], None),
+    ],
+)
+def test_highlight_box_handles(position, dims_displayed, nearby_handle):
+    layer = Image(np.empty((10, 10)))
+    event = Mock(
+        position=position, dims_displayed=dims_displayed, modifiers=[None]
+    )
+    highlight_box_handles(
+        layer,
+        event,
+    )
+    # mouse event should be detected over the expected handle
+    assert layer._overlays['transform_box'].selected_handle == nearby_handle
+
+
+def test_transform_box():
+    layer = Image(np.empty((10, 10)))
+    event = Mock(position=[0, 3], dims_displayed=[0, 1], modifiers=[None])
+    next(transform_with_box(layer, event))
+    # no interaction has been done so affine should be the same as the initial
+    assert layer.affine == layer._initial_affine
diff --git a/napari/_tests/test_viewer.py b/napari/_tests/test_viewer.py
index c4d41b79b91..5abd80062c4 100644
--- a/napari/_tests/test_viewer.py
+++ b/napari/_tests/test_viewer.py
@@ -1,4 +1,5 @@
 import os
+from unittest.mock import Mock
 
 import numpy as np
 import pytest
@@ -54,7 +55,7 @@ def test_all_viewer_actions_are_accessible_via_shortcut(make_napari_viewer):
     _assert_shortcuts_exist_for_each_action(Viewer)
 
 
-@pytest.mark.xfail()
+@pytest.mark.xfail
 def test_non_existing_bindings():
     """
     Those are condition tested in next unittest; but do not exists; this is
@@ -336,6 +337,7 @@ def test_emitting_data_doesnt_change_points_value(make_napari_viewer):
     assert layer._value is None
     viewer.mouse_over_canvas = True
     viewer.cursor.position = tuple(layer.data[1])
+    viewer._calc_status_from_cursor()
     assert layer._value == 1
 
     layer.events.data(value=layer.data)
@@ -414,3 +416,32 @@ def test_reset_non_empty(make_napari_viewer):
     viewer = make_napari_viewer()
     viewer.add_points([(0, 1), (2, 3)])
     viewer.reset()
+
+
+def test_running_status_thread(make_napari_viewer, qtbot, monkeypatch):
+    viewer = make_napari_viewer()
+    settings = get_settings()
+    start_mock, stop_mock = Mock(), Mock()
+    monkeypatch.setattr(
+        viewer.window._qt_window.status_thread, 'start', start_mock
+    )
+    monkeypatch.setattr(
+        viewer.window._qt_window.status_thread, 'terminate', stop_mock
+    )
+    assert settings.appearance.update_status_based_on_layer
+    settings.appearance.update_status_based_on_layer = False
+    stop_mock.assert_called_once()
+    start_mock.assert_not_called()
+    settings.appearance.update_status_based_on_layer = True
+    start_mock.assert_called_once()
+
+
+def test_negative_translate(make_napari_viewer, qtbot):
+    """Check that negative translation behaves as expected.
+
+    See https://github.com/napari/napari/issues/7248
+    """
+    data = np.random.random((1, 3, 10, 12, 12))
+    viewer = make_napari_viewer()
+    _ = viewer.add_image(data, translate=(-1, 0, 0))
+    assert viewer.dims.range[2].start == -1
diff --git a/napari/_tests/test_with_screenshot.py b/napari/_tests/test_with_screenshot.py
index 9e61b4ef116..5cac6651284 100644
--- a/napari/_tests/test_with_screenshot.py
+++ b/napari/_tests/test_with_screenshot.py
@@ -2,6 +2,7 @@
 import pytest
 
 from napari._tests.utils import skip_local_popups, skip_on_win_ci
+from napari.layers import Shapes
 from napari.utils._test_utils import read_only_mouse_event
 from napari.utils.interactions import (
     mouse_move_callbacks,
@@ -10,6 +11,14 @@
 )
 
 
+@pytest.fixture
+def qt_viewer(qt_viewer_):
+    # show the qt_viewer and hide its welcome widget
+    qt_viewer_.show()
+    qt_viewer_.set_welcome_visible(False)
+    return qt_viewer_
+
+
 @skip_on_win_ci
 @skip_local_popups
 def test_z_order_adding_removing_images(make_napari_viewer):
@@ -545,3 +554,42 @@ def test_blending_modes_with_canvas(make_napari_viewer):
     img2_layer.blending = 'minimum'
     screenshot = viewer.screenshot(canvas_only=True, flash=False)
     np.testing.assert_array_equal(screenshot[:, :, 0], np.minimum(img1, img2))
+
+
+@skip_local_popups
+def test_active_layer_highlight_visibility(qt_viewer):
+    viewer = qt_viewer.viewer
+
+    # take initial screenshot (full black/empty screenshot since welcome message is hidden)
+    launch_screenshot = qt_viewer.screenshot(flash=False)
+    # check screenshot ignoring alpha
+    assert launch_screenshot[..., :-1].max() == 0
+
+    # add shapes layer setting edge and face color to `black` (so shapes aren't
+    # visible unless they're selected), create a rectangle and select the created shape
+    shapes_layer: Shapes = viewer.add_shapes(
+        edge_color='black', face_color='black'
+    )
+    shapes_layer.add_rectangles([[0, 0], [1, 1]])
+    shapes_layer.selected_data = {0}
+
+    # there should be a highlight so a screenshot should have something visible
+    highlight_screenshot = qt_viewer.screenshot(flash=False)
+    # check screenshot ignoring alpha
+    assert highlight_screenshot[..., :-1].max() > 0
+
+    # clear viewer layer selection
+    viewer.layers.selection.clear()
+
+    # there shouldn't be a highlight so a new screenshot shouldn't have something visible
+    no_highlight_screenshot = qt_viewer.screenshot(flash=False)
+    # check screenshot ignoring alpha
+    assert no_highlight_screenshot[..., :-1].max() == 0
+
+    # select again the layer with the rectangle shape
+    viewer.layers.selection.add(shapes_layer)
+
+    # there should be a highlight so a screenshot should have something visible
+    reselection_highlight_screenshot = qt_viewer.screenshot(flash=False)
+    # check screenshot ignoring alpha
+    assert reselection_highlight_screenshot[..., :-1].max() > 0
diff --git a/napari/_vendor/qt_json_builder/qt_jsonschema_form/widgets.py b/napari/_vendor/qt_json_builder/qt_jsonschema_form/widgets.py
index 90d0c143086..34997a749f4 100644
--- a/napari/_vendor/qt_json_builder/qt_jsonschema_form/widgets.py
+++ b/napari/_vendor/qt_json_builder/qt_jsonschema_form/widgets.py
@@ -616,8 +616,7 @@ def setDescription(self, description: str):
 
     @state.setter
     def state(self, state: dict):
-        # self.setValue(state)
-        return None
+        self.setValue(state)
 
     def configure(self):
         self.valueChanged.connect(self.on_changed.emit)
diff --git a/napari/_vispy/__init__.py b/napari/_vispy/__init__.py
index 62c74353697..9628463ed6f 100644
--- a/napari/_vispy/__init__.py
+++ b/napari/_vispy/__init__.py
@@ -26,15 +26,15 @@
 from napari._vispy.utils.visual import create_vispy_layer, create_vispy_overlay
 
 __all__ = [
+    'VispyAxesOverlay',
     'VispyCamera',
     'VispyCanvas',
-    'VispyAxesOverlay',
-    'VispySelectionBoxOverlay',
+    'VispyLabelsPolygonOverlay',
     'VispyScaleBarOverlay',
-    'VispyTransformBoxOverlay',
+    'VispySelectionBoxOverlay',
     'VispyTextOverlay',
-    'VispyLabelsPolygonOverlay',
-    'quaternion2euler_degrees',
+    'VispyTransformBoxOverlay',
     'create_vispy_layer',
     'create_vispy_overlay',
+    'quaternion2euler_degrees',
 ]
diff --git a/napari/_vispy/_tests/test_vispy_image_layer.py b/napari/_vispy/_tests/test_vispy_image_layer.py
index 13164162ef3..92c188aeb9c 100644
--- a/napari/_vispy/_tests/test_vispy_image_layer.py
+++ b/napari/_vispy/_tests/test_vispy_image_layer.py
@@ -86,12 +86,12 @@ def test_no_float32_texture_support(monkeypatch):
     VispyImageLayer(image)
 
 
-@pytest.fixture()
+@pytest.fixture
 def im_layer() -> Image:
     return Image(np.zeros((10, 10)))
 
 
-@pytest.fixture()
+@pytest.fixture
 def pyramid_layer() -> Image:
     return Image([np.zeros((20, 20)), np.zeros((10, 10))])
 
@@ -201,27 +201,36 @@ def test_transforming_child_node_pyramid(pyramid_layer):
     )
 
 
-@pytest.mark.parametrize('scale', [(1, 1, 1), (2, 2, 2)])
+@pytest.mark.parametrize('scale', [1, 2])
+@pytest.mark.parametrize('ndim', [3, 4])
 @pytest.mark.parametrize('ndisplay', [2, 3])
-def test_node_origin_is_consistent_with_multiscale(scale, ndisplay):
+def test_node_origin_is_consistent_with_multiscale(
+    scale: int, ndim: int, ndisplay: int
+):
     """See https://github.com/napari/napari/issues/6320"""
+    scales = (scale,) * ndim
+
     # Define multi-scale image data with two levels where the
     # higher resolution is twice as high as the lower resolution.
-    image = Image(data=[np.zeros((8, 8, 8)), np.zeros((4, 4, 4))], scale=scale)
+    image = Image(
+        data=[np.zeros((8,) * ndim), np.zeros((4,) * ndim)], scale=scales
+    )
     vispy_image = VispyImageLayer(image)
 
     # Take a full slice at the highest resolution.
-    image.corner_pixels = np.array([[0, 0, 0], [8, 8, 8]])
+    image.corner_pixels = np.array([[0] * ndim, [8] * ndim])
     image._data_level = 0
-    image._slice_dims(Dims(ndim=3, ndisplay=ndisplay, point=(1, 0, 0)))
+    # Use a slice point of (1, 0, 0, ...) to have some non-zero slice coordinates.
+    point = (1,) + (0,) * (ndim - 1)
+    image._slice_dims(Dims(ndim=ndim, ndisplay=ndisplay, point=point))
     # Map the node's data origin to a vispy scene coordinate.
-    high_res_origin = vispy_image.node.transform.map((0, 0))
+    high_res_origin = vispy_image.node.transform.map((0,) * ndisplay)
 
     # Take a full slice at the lowest resolution and map the origin again.
-    image.corner_pixels = np.array([[0, 0, 0], [4, 4, 4]])
+    image.corner_pixels = np.array([[0] * ndim, [4] * ndim])
     image._data_level = 1
-    image._slice_dims(Dims(ndim=3, ndisplay=ndisplay, point=(1, 0, 0)))
-    low_res_origin = vispy_image.node.transform.map((0, 0))
+    image._slice_dims(Dims(ndim=ndim, ndisplay=ndisplay, point=point))
+    low_res_origin = vispy_image.node.transform.map((0,) * ndisplay)
 
     # The exact origin may depend on certain parameter values, but the
     # full high and low resolution slices should always map to the same
diff --git a/napari/_vispy/_tests/test_vispy_labels_layer.py b/napari/_vispy/_tests/test_vispy_labels_layer.py
index 51822f17d4e..b3b6d5ab3ad 100644
--- a/napari/_vispy/_tests/test_vispy_labels_layer.py
+++ b/napari/_vispy/_tests/test_vispy_labels_layer.py
@@ -3,7 +3,9 @@
 import zarr
 from qtpy.QtCore import QCoreApplication
 
-from napari._tests.utils import skip_local_popups
+from napari._tests.utils import skip_local_popups, skip_on_win_ci
+from napari._vispy.visuals.volume import Volume as VolumeNode
+from napari.layers.labels._labels_constants import IsoCategoricalGradientMode
 from napari.utils.interactions import mouse_press_callbacks
 
 
@@ -32,55 +34,54 @@ def make_labels_layer(array_type, shape):
 
 @skip_local_popups
 @pytest.mark.parametrize('array_type', ['numpy', 'zarr', 'tensorstore'])
-def test_labels_painting(make_napari_viewer, array_type):
+def test_labels_painting(qtbot, array_type, qt_viewer):
     """Check that painting labels paints on the canvas.
 
     This should work regardless of array type. See:
     https://github.com/napari/napari/issues/6079
     """
-    viewer = make_napari_viewer(show=True)
+    viewer = qt_viewer.viewer
     labels = make_labels_layer(array_type, shape=(20, 20))
     layer = viewer.add_labels(labels)
     QCoreApplication.instance().processEvents()
     layer.paint((10, 10), 1, refresh=True)
-    visual = viewer.window._qt_viewer.layer_to_visual[layer]
+    visual = qt_viewer.layer_to_visual[layer]
     assert np.any(visual.node._data)
 
 
 @skip_local_popups
 @pytest.mark.parametrize('array_type', ['numpy', 'zarr', 'tensorstore'])
-def test_labels_fill_slice(make_napari_viewer, array_type):
+def test_labels_fill_slice(qtbot, array_type, qt_viewer):
     """Check that painting labels paints only on current slice.
 
     This should work regardless of array type. See:
     https://github.com/napari/napari/issues/6079
     """
-    viewer = make_napari_viewer(show=True)
     labels = make_labels_layer(array_type, shape=(3, 20, 20))
     labels[0, :, :] = 1
     labels[1, 10, 10] = 1
     labels[2, :, :] = 1
+
+    viewer = qt_viewer.viewer
     layer = viewer.add_labels(labels)
     layer.n_edit_dimensions = 3
     QCoreApplication.instance().processEvents()
     layer.fill((1, 10, 10), 13, refresh=True)
-    visual = viewer.window._qt_viewer.layer_to_visual[layer]
+    visual = qt_viewer.layer_to_visual[layer]
     assert np.sum(visual.node._data) == 13
 
 
 @skip_local_popups
 @pytest.mark.parametrize('array_type', ['numpy', 'zarr', 'tensorstore'])
-def test_labels_painting_with_mouse(
-    MouseEvent, make_napari_viewer, array_type
-):
+def test_labels_painting_with_mouse(MouseEvent, qtbot, array_type, qt_viewer):
     """Check that painting labels paints on the canvas when using mouse.
 
     This should work regardless of array type. See:
     https://github.com/napari/napari/issues/6079
     """
-    viewer = make_napari_viewer(show=True)
     labels = make_labels_layer(array_type, shape=(20, 20))
 
+    viewer = qt_viewer.viewer
     layer = viewer.add_labels(labels)
     QCoreApplication.instance().processEvents()
 
@@ -91,7 +92,37 @@ def test_labels_painting_with_mouse(
         position=(0, 10, 10),
         dims_displayed=(0, 1),
     )
-    visual = viewer.window._qt_viewer.layer_to_visual[layer]
+    visual = qt_viewer.layer_to_visual[layer]
     assert not np.any(visual.node._data)
     mouse_press_callbacks(layer, event)
     assert np.any(visual.node._data)
+
+
+@skip_local_popups
+@skip_on_win_ci
+def test_labels_iso_gradient_modes(qtbot, qt_viewer):
+    """Check that we can set `iso_gradient_mode` with `iso_categorical` rendering (test shader)."""
+    # NOTE: this test currently segfaults on Windows CI, but confirmed working locally
+    # because it's a segfault, we have to skip instead of xfail
+    qt_viewer.show()
+    viewer = qt_viewer.viewer
+
+    labels = make_labels_layer('numpy', shape=(32, 32, 32))
+    labels[14:18, 14:18, 14:18] = 1
+    layer = viewer.add_labels(labels)
+    visual = qt_viewer.layer_to_visual[layer]
+
+    viewer.dims.ndisplay = 3
+    QCoreApplication.instance().processEvents()
+    assert layer.rendering == 'iso_categorical'
+    assert isinstance(visual.node, VolumeNode)
+
+    layer.iso_gradient_mode = IsoCategoricalGradientMode.SMOOTH
+    QCoreApplication.instance().processEvents()
+    assert layer.iso_gradient_mode == 'smooth'
+    assert visual.node.iso_gradient_mode == 'smooth'
+
+    layer.iso_gradient_mode = IsoCategoricalGradientMode.FAST
+    QCoreApplication.instance().processEvents()
+    assert layer.iso_gradient_mode == 'fast'
+    assert visual.node.iso_gradient_mode == 'fast'
diff --git a/napari/_vispy/_tests/test_vispy_scale_bar_visual.py b/napari/_vispy/_tests/test_vispy_scale_bar_visual.py
index 4ba0b9b3a89..e899f2f42b2 100644
--- a/napari/_vispy/_tests/test_vispy_scale_bar_visual.py
+++ b/napari/_vispy/_tests/test_vispy_scale_bar_visual.py
@@ -5,4 +5,7 @@
 def test_scale_bar_instantiation(make_napari_viewer):
     viewer = make_napari_viewer()
     model = ScaleBarOverlay()
-    VispyScaleBarOverlay(overlay=model, viewer=viewer)
+    vispy_scale_bar = VispyScaleBarOverlay(overlay=model, viewer=viewer)
+    assert vispy_scale_bar.overlay.length is None
+    model.length = 50
+    assert vispy_scale_bar.overlay.length == 50
diff --git a/napari/_vispy/_tests/test_vispy_surface_layer.py b/napari/_vispy/_tests/test_vispy_surface_layer.py
index 0f110b7f7d3..2138844ecd8 100644
--- a/napari/_vispy/_tests/test_vispy_surface_layer.py
+++ b/napari/_vispy/_tests/test_vispy_surface_layer.py
@@ -8,7 +8,7 @@
 from napari.layers import Surface
 
 
-@pytest.fixture()
+@pytest.fixture
 def cube_layer():
     vertices, faces, _ = create_cube()
     return Surface((vertices['position'] * 100, faces))
@@ -48,7 +48,7 @@ def test_add_texture(cube_layer, texture_shape):
 
     texture = np.random.random(texture_shape).astype(np.float32)
     cube_layer.texture = texture
-    # no texture filter initally
+    # no texture filter initially
     assert visual._texture_filter is None
 
     # the texture filter is created when texture + texcoords are added
@@ -114,7 +114,7 @@ def test_vertex_colors(cube_layer):
 
 
 @skip_local_popups
-def test_check_surface_without_visible_faces(make_napari_viewer):
+def test_check_surface_without_visible_faces(qtbot, qt_viewer):
     points = np.array(
         [
             [0, 0.0, 0.0, 0.0],
@@ -127,8 +127,9 @@ def test_check_surface_without_visible_faces(make_napari_viewer):
     )
     faces = np.array([[0, 1, 2], [3, 4, 5]])
     layer = Surface((points, faces))
-    viewer = make_napari_viewer(ndisplay=3)
-    viewer.show()
+    qt_viewer.show()
+    viewer = qt_viewer.viewer
     viewer.add_layer(layer)
     # The following with throw an exception.
     viewer.reset()
+    qt_viewer.hide()
diff --git a/napari/_vispy/camera.py b/napari/_vispy/camera.py
index 954b84ccbfd..088f21edcb8 100644
--- a/napari/_vispy/camera.py
+++ b/napari/_vispy/camera.py
@@ -237,7 +237,8 @@ def viewbox_mouse_event(self, event):
             if (
                 self.mouse_zoom
                 and event.type in ('mouse_wheel', 'gesture_zoom')
-                or self.mouse_pan
+            ) or (
+                self.mouse_pan
                 and event.type
                 in ('mouse_move', 'mouse_press', 'mouse_release')
             ):
diff --git a/napari/_vispy/canvas.py b/napari/_vispy/canvas.py
index ab04e261ad1..349ab524b7d 100644
--- a/napari/_vispy/canvas.py
+++ b/napari/_vispy/canvas.py
@@ -316,7 +316,8 @@ def _on_interactive(self) -> None:
         )
 
     def _map_canvas2world(
-        self, position: tuple[int, ...]
+        self,
+        position: tuple[int, ...],
     ) -> tuple[float, float]:
         """Map position from canvas pixels into world coordinates.
 
@@ -333,9 +334,13 @@ def _map_canvas2world(
         """
         nd = self.viewer.dims.ndisplay
         transform = self.view.scene.transform
-        mapped_position = transform.imap(list(position))[:nd]
-        position_world_slice = mapped_position[::-1]
-
+        # cartesian to homogeneous coordinates
+        mapped_position = transform.imap(list(position))
+        if nd == 3:
+            mapped_position = mapped_position[0:nd] / mapped_position[nd]
+        else:
+            mapped_position = mapped_position[0:nd]
+        position_world_slice = np.array(mapped_position[::-1])
         # handle position for 3D views of 2D data
         nd_point = len(self.viewer.dims.point)
         if nd_point < nd:
@@ -382,9 +387,7 @@ def _process_mouse_event(
             return
 
         # Add the view ray to the event
-        event.view_direction = self.viewer.camera.calculate_nd_view_direction(
-            self.viewer.dims.ndim, self.viewer.dims.displayed
-        )
+        event.view_direction = self._calculate_view_direction(event.pos)
         event.up_direction = self.viewer.camera.calculate_nd_up_direction(
             self.viewer.dims.ndim, self.viewer.dims.displayed
         )
@@ -634,6 +637,40 @@ def _add_overlay_to_visual(self, overlay: Overlay) -> None:
             vispy_overlay.node.parent = self.view.scene
         self._overlay_to_visual[overlay] = vispy_overlay
 
+    def _calculate_view_direction(self, event_pos: list[float]) -> list[float]:
+        """calculate view direction by ray shot from the camera"""
+        # this method is only implemented for 3 dimension
+        if self.viewer.dims.ndisplay == 2 or self.viewer.dims.ndim == 2:
+            return self.viewer.camera.calculate_nd_view_direction(
+                self.viewer.dims.ndim, self.viewer.dims.displayed
+            )
+        x, y = event_pos
+        w, h = self.size
+        nd = self.viewer.dims.ndisplay
+
+        transform = self.view.scene.transform
+        # map click pos to scene coordinates
+        click_scene = transform.imap([x, y, 0, 1])
+        # canvas center at infinite far z- (eye position in canvas coordinates)
+        eye_canvas = [w / 2, h / 2, -1e10, 1]
+        # map eye pos to scene coordinates
+        eye_scene = transform.imap(eye_canvas)
+        # homogeneous coordinate to cartesian
+        click_scene = click_scene[0:nd] / click_scene[nd]
+        # homogeneous coordinate to cartesian
+        eye_scene = eye_scene[0:nd] / eye_scene[nd]
+
+        # calculate direction of the ray
+        d = click_scene - eye_scene
+        d = d[0:nd]
+        d = d / np.linalg.norm(d)
+        # xyz to zyx
+        d = list(d[::-1])
+        # convert to nd view direction
+        view_direction_nd = np.zeros(self.viewer.dims.ndim)
+        view_direction_nd[list(self.viewer.dims.displayed)] = d
+        return view_direction_nd
+
     def screenshot(self) -> QImage:
         """Return a QImage based on what is shown in the viewer."""
         return self.native.grabFramebuffer()
diff --git a/napari/_vispy/layers/base.py b/napari/_vispy/layers/base.py
index 91375ee67a7..74b13418220 100644
--- a/napari/_vispy/layers/base.py
+++ b/napari/_vispy/layers/base.py
@@ -199,10 +199,9 @@ def _on_overlays_change(self):
                 overlay_visual.close()
 
     def _on_matrix_change(self):
+        dims_displayed = self.layer._slice_input.displayed
         # mypy: self.layer._transforms.simplified cannot be None
-        transform = self.layer._transforms.simplified.set_slice(
-            self.layer._slice_input.displayed
-        )
+        transform = self.layer._transforms.simplified.set_slice(dims_displayed)
         # convert NumPy axis ordering to VisPy axis ordering
         # by reversing the axes order and flipping the linear
         # matrix
@@ -222,15 +221,18 @@ def _on_matrix_change(self):
         ):
             # The last downsample factor is used because we only ever show the
             # last/lowest multi-scale level for 3D.
-            translate += (self.layer.downsample_factors[-1][::-1] - 1) / 2
+            translate += (
+                # displayed dimensions, order inverted to match VisPy, then
+                # adjust by half a pixel per downscale level
+                self.layer.downsample_factors[-1][dims_displayed][::-1] - 1
+            ) / 2
 
         # Embed in the top left corner of a 4x4 affine matrix
         affine_matrix = np.eye(4)
         affine_matrix[: matrix.shape[0], : matrix.shape[1]] = matrix
         affine_matrix[-1, : len(translate)] = translate
 
-        child_offset = np.zeros(len(self.layer._slice_input.displayed))
-        dims_displayed = self.layer._slice_input.displayed
+        child_offset = np.zeros(len(dims_displayed))
 
         if self._array_like and self.layer._slice_input.ndisplay == 2:
             # Perform pixel offset to shift origin from top left corner
@@ -238,7 +240,7 @@ def _on_matrix_change(self):
             # Note this offset is only required for array like data in
             # 2D.
             offset_matrix = self.layer._data_to_world.set_slice(
-                self.layer._slice_input.displayed
+                dims_displayed
             ).linear_matrix
             offset = -offset_matrix @ np.ones(offset_matrix.shape[1]) / 2
             # Convert NumPy axis ordering to VisPy axis ordering
diff --git a/napari/_vispy/layers/labels.py b/napari/_vispy/layers/labels.py
index 221919a8f56..2e174242aaa 100644
--- a/napari/_vispy/layers/labels.py
+++ b/napari/_vispy/layers/labels.py
@@ -206,6 +206,9 @@ def __init__(self, layer, node=None, texture_format='r8') -> None:
         self.layer.events.labels_update.connect(self._on_partial_labels_update)
         self.layer.events.selected_label.connect(self._on_colormap_change)
         self.layer.events.show_selected_label.connect(self._on_colormap_change)
+        self.layer.events.iso_gradient_mode.connect(
+            self._on_iso_gradient_mode_change
+        )
         self.layer.events.data.connect(self._on_colormap_change)
         # as we generate colormap texture based on the data type, we need to
         # update it when the data type changes
@@ -291,6 +294,10 @@ def _on_colormap_change(self, event=None):
         else:
             self.node.cmap = VispyColormap(*colormap)
 
+    def _on_iso_gradient_mode_change(self):
+        if isinstance(self.node, VolumeNode):
+            self.node.iso_gradient_mode = self.layer.iso_gradient_mode
+
     def _on_partial_labels_update(self, event):
         if not self.layer.loaded:
             return
@@ -299,6 +306,7 @@ def _on_partial_labels_update(self, event):
         ndims = len(event.offset)
 
         if self.node._texture.shape[:ndims] != raw_displayed.shape[:ndims]:
+            # TODO: I'm confused by this whole process; should this refresh be changed?
             self.layer.refresh()
             return
 
@@ -310,6 +318,7 @@ def _on_partial_labels_update(self, event):
     def reset(self, event=None) -> None:
         super().reset()
         self._on_colormap_change()
+        self._on_iso_gradient_mode_change()
 
 
 class LabelLayerNode(ScalarFieldLayerNode):
diff --git a/napari/_vispy/layers/scalar_field.py b/napari/_vispy/layers/scalar_field.py
index 2db95dae00e..9c2be23fb60 100644
--- a/napari/_vispy/layers/scalar_field.py
+++ b/napari/_vispy/layers/scalar_field.py
@@ -103,8 +103,8 @@ def _on_data_change(self) -> None:
             ndisplay, getattr(data, 'dtype', None)
         )
 
-        if ndisplay == 3 and self.layer.ndim == 2:
-            data = np.expand_dims(data, axis=0)
+        if ndisplay > data.ndim:
+            data = data.reshape((1,) * (ndisplay - data.ndim) + data.shape)
 
         # Check if data exceeds MAX_TEXTURE_SIZE and downsample
         if self.MAX_TEXTURE_SIZE_2D is not None and ndisplay == 2:
@@ -114,8 +114,7 @@ def _on_data_change(self) -> None:
 
         # Check if ndisplay has changed current node type needs updating
         if (ndisplay == 3 and not isinstance(node, VolumeNode)) or (
-            ndisplay == 2
-            and not isinstance(node, ImageVisual)
+            (ndisplay == 2 and not isinstance(node, ImageVisual))
             or node != self.node
         ):
             self._on_display_change(data)
diff --git a/napari/_vispy/overlays/scale_bar.py b/napari/_vispy/overlays/scale_bar.py
index 590c51a9227..94c9e3e0263 100644
--- a/napari/_vispy/overlays/scale_bar.py
+++ b/napari/_vispy/overlays/scale_bar.py
@@ -16,7 +16,7 @@ class VispyScaleBarOverlay(ViewerOverlayMixin, VispyCanvasOverlay):
     """Scale bar in world coordinates."""
 
     def __init__(self, *, viewer, overlay, parent=None) -> None:
-        self._target_length = 150
+        self._target_length = 150.0
         self._scale = 1
         self._unit: pint.Unit
 
@@ -35,6 +35,7 @@ def __init__(self, *, viewer, overlay, parent=None) -> None:
         self.overlay.events.font_size.connect(self._on_text_change)
         self.overlay.events.ticks.connect(self._on_data_change)
         self.overlay.events.unit.connect(self._on_unit_change)
+        self.overlay.events.length.connect(self._on_length_change)
 
         self.viewer.events.theme.connect(self._on_data_change)
         self.viewer.camera.events.zoom.connect(self._on_zoom_change)
@@ -45,6 +46,9 @@ def _on_unit_change(self):
         self._unit = get_unit_registry()(self.overlay.unit)
         self._on_zoom_change(force=True)
 
+    def _on_length_change(self):
+        self._on_zoom_change(force=True)
+
     def _calculate_best_length(
         self, desired_length: float
     ) -> tuple[float, pint.Quantity]:
@@ -74,7 +78,7 @@ def _calculate_best_length(
         # validate if quantity is dimensionless and lower than 1 to prevent
         # the scale bar to extend beyond the canvas when zooming.
         # If the value falls in those conditions, we use the corresponding
-        # prefered value but scaled to take into account the actual value
+        # preferred value but scaled to take into account the actual value
         # magnitude. See https://github.com/napari/napari/issues/5914
         magnitude_1000 = floor(log(new_quantity.magnitude, 1000))
         scaled_magnitude = new_quantity.magnitude * 1000 ** (-magnitude_1000)
@@ -112,18 +116,24 @@ def _on_zoom_change(self, *, force: bool = False):
         # convert desired length to world size
         target_world_pixels = scale_canvas2world * target_canvas_pixels
 
-        # calculate the desired length as well as update the value and units
-        target_world_pixels_rounded, new_dim = self._calculate_best_length(
-            target_world_pixels
-        )
-        target_canvas_pixels_rounded = (
-            target_world_pixels_rounded / scale_canvas2world
-        )
-        scale = target_canvas_pixels_rounded
+        # If length is set, use that value to calculate the scale bar length
+        if self.overlay.length is not None:
+            target_canvas_pixels = self.overlay.length / scale_canvas2world
+            new_dim = self.overlay.length * self._unit.units
+        else:
+            # calculate the desired length as well as update the value and units
+            target_world_pixels_rounded, new_dim = self._calculate_best_length(
+                target_world_pixels
+            )
+            target_canvas_pixels = (
+                target_world_pixels_rounded / scale_canvas2world
+            )
+
+        scale = target_canvas_pixels
 
         # Update scalebar and text
         self.node.transform.scale = [scale, 1, 1, 1]
-        self.node.text.text = f'{new_dim:~}'
+        self.node.text.text = f'{new_dim:g~#P}'
         self.x_size = scale  # needed to offset properly
         self._on_position_change()
 
@@ -171,3 +181,4 @@ def reset(self):
         self._on_data_change()
         self._on_box_change()
         self._on_text_change()
+        self._on_length_change()
diff --git a/napari/_vispy/utils/text.py b/napari/_vispy/utils/text.py
index 0a3468d0426..0902936db1b 100644
--- a/napari/_vispy/utils/text.py
+++ b/napari/_vispy/utils/text.py
@@ -71,6 +71,4 @@ def _has_visible_text(layer: Union[Points, Shapes]) -> bool:
         and text.string.constant == ''
     ):
         return False
-    if len(layer._indices_view) == 0:
-        return False
-    return True
+    return len(layer._indices_view) != 0
diff --git a/napari/_vispy/visuals/volume.py b/napari/_vispy/visuals/volume.py
index 20fe9d44527..36d174a3561 100644
--- a/napari/_vispy/visuals/volume.py
+++ b/napari/_vispy/visuals/volume.py
@@ -1,8 +1,12 @@
 from vispy.scene.visuals import Volume as BaseVolume
 
 from napari._vispy.visuals.util import TextureMixin
+from napari.layers.labels._labels_constants import IsoCategoricalGradientMode
 
 FUNCTION_DEFINITIONS = """
+// switch for clamping values at volume limits
+uniform bool u_clamp_at_border;
+
 // the tolerance for testing equality of floats with floatEqual and floatNotEqual
 const float equality_tolerance = 1e-8;
 
@@ -22,7 +26,6 @@
     return equal;
 }
 
-
 // the background value for the iso_categorical shader
 const float categorical_bg_value = 0;
 
@@ -33,7 +36,9 @@
     adjacent_bg = adjacent_bg * int( floatEqual(val_pos, categorical_bg_value) );
     return adjacent_bg;
 }
+"""
 
+CALCULATE_COLOR_DEFINITION = """
 vec4 calculateShadedCategoricalColor(vec4 betterColor, vec3 loc, vec3 step)
 {
     // Calculate color by incorporating ambient and diffuse lighting
@@ -43,33 +48,13 @@
     float val0 = colorToVal(color0);
     float val1 = 0;
     float val2 = 0;
-    int n_bg_borders = 0;
 
     // View direction
     vec3 V = normalize(view_ray);
 
-    // calculate normal vector from gradient
-    vec3 N; // normal
-    color1 = $get_data(loc+vec3(-step[0],0.0,0.0));
-    color2 = $get_data(loc+vec3(step[0],0.0,0.0));
-    val1 = colorToVal(color1);
-    val2 = colorToVal(color2);
-    N[0] = val1 - val2;
-    n_bg_borders += detectAdjacentBackground(val1, val2);
-
-    color1 = $get_data(loc+vec3(0.0,-step[1],0.0));
-    color2 = $get_data(loc+vec3(0.0,step[1],0.0));
-    val1 = colorToVal(color1);
-    val2 = colorToVal(color2);
-    N[1] = val1 - val2;
-    n_bg_borders += detectAdjacentBackground(val1, val2);
-
-    color1 = $get_data(loc+vec3(0.0,0.0,-step[2]));
-    color2 = $get_data(loc+vec3(0.0,0.0,step[2]));
-    val1 = colorToVal(color1);
-    val2 = colorToVal(color2);
-    N[2] = val1 - val2;
-    n_bg_borders += detectAdjacentBackground(val1, val2);
+    // Calculate normal vector from gradient
+    vec3 N;
+    N = calculateGradient(loc, step, val0);
 
     // Normalize and flip normal so it points towards viewer
     N = normalize(N);
@@ -92,11 +77,6 @@
 
         // Calculate lighting properties
         float lambertTerm = clamp( dot(N,L), 0.0, 1.0 );
-        if (n_bg_borders > 0) {
-            // to fix dim pixels due to poor normal estimation,
-            // we give a default lambda to pixels surrounded by background
-            lambertTerm = 0.5;
-        }
 
         // Calculate mask
         float mask1 = lightEnabled;
@@ -115,6 +95,126 @@
 }
 """
 
+FAST_GRADIENT_DEFINITION = """
+vec3 calculateGradient(vec3 loc, vec3 step, float current_val) {
+    // calculate gradient within the volume by finite differences
+
+    vec3 G = vec3(0.0);
+
+    float prev;
+    float next;
+    int in_bounds;
+
+    for (int i=0; i<3; i++) {
+        vec3 ax_step = vec3(0.0);
+        ax_step[i] = step[i];
+
+        vec3 prev_loc = loc - ax_step;
+        if (u_clamp_at_border || (prev_loc[i] >= 0.0 && prev_loc[i] <= 1.0)) {
+            prev = colorToVal($get_data(prev_loc));
+        } else {
+            prev = categorical_bg_value;
+        }
+
+        vec3 next_loc = loc + ax_step;
+        if (u_clamp_at_border || (next_loc[i] >= 0.0 && next_loc[i] <= 1.0)) {
+            next = colorToVal($get_data(next_loc));
+        } else {
+            next = categorical_bg_value;
+        }
+
+        // add to the gradient where the adjacent voxels are both background
+        // to fix dim pixels due to poor normal estimation
+        G[i] = next - prev + (next - current_val) * 2.0 * detectAdjacentBackground(prev, next);
+    }
+
+    return G;
+}
+"""
+
+SMOOTH_GRADIENT_DEFINITION = """
+vec3 calculateGradient(vec3 loc, vec3 step, float current_val) {
+    // calculate gradient within the volume by finite differences
+    // using a 3D sobel-feldman convolution kernel
+
+    // the kernel here is a 3x3 cube, centered on the sample at `loc`
+    // the kernel for G.z looks like this:
+
+    // [ +1 +2 +1 ]
+    // [ +2 +4 +2 ]    <-- "loc - step.z" is in the center
+    // [ +1 +2 +1 ]
+
+    // [  0  0  0 ]
+    // [  0  0  0 ]    <-- "loc" is in the center
+    // [  0  0  0 ]
+
+    // [ -1 -2 -1 ]
+    // [ -2 -4 -2 ]    <-- "loc + step.z" is in the center
+    // [ -1 -2 -1 ]
+
+    // kernels for G.x and G.y similar, but transposed
+    // see https://en.wikipedia.org/wiki/Sobel_operator#Extension_to_other_dimensions
+
+    vec3 G = vec3(0.0);
+    // next and prev are the directly adjacent values along x, y, and z
+    vec3 next = vec3(0.0);
+    vec3 prev = vec3(0.0);
+
+    float val;
+    bool is_on_border = false;
+    for (int i=-1; i <= 1; i++) {
+        for (int j=-1; j <= 1; j++) {
+            for (int k=-1; k <= 1; k++) {
+                if (is_on_border && (i != 0 && j != 0 && k != 0)) {
+                    // we only care about on-axis values if we are on a border
+                    continue;
+                }
+                vec3 sample_loc = loc + vec3(i, j, k) * step;
+                bool is_in_bounds = all(greaterThanEqual(sample_loc, vec3(0.0)))
+                    && all(lessThanEqual(sample_loc, vec3(1.0)));
+
+                if (is_in_bounds || u_clamp_at_border) {
+                    val = colorToVal($get_data(sample_loc));
+                } else {
+                    val = categorical_bg_value;
+                }
+
+                G.x += val * -float(i) *
+                    (1 + float(j == 0 || k == 0) + 2 * float(j == 0 && k == 0));
+                G.y += val * -float(j) *
+                    (1 + float(i == 0 || k == 0) + 2 * float(i == 0 && k == 0));
+                G.z += val * -float(k) *
+                    (1 + float(i == 0 || j == 0) + 2 * float(i == 0 && j == 0));
+
+                next.x += int(i == 1 && j == 0 && k == 0) * val;
+                next.y += int(i == 0 && j == 1 && k == 0) * val;
+                next.z += int(i == 0 && j == 0 && k == 1) * val;
+                prev.x += int(i == -1 && j == 0 && k == 0) * val;
+                prev.y += int(i == 0 && j == -1 && k == 0) * val;
+                prev.z += int(i == 0 && j == 0 && k == -1) * val;
+
+                is_on_border = is_on_border || (!is_in_bounds && (i == 0 || j == 0 || k == 0));
+            }
+        }
+    }
+
+    if (is_on_border && u_clamp_at_border) {
+        // fallback to simple gradient calculation if we are on the border
+        // and clamping is enabled (old behavior with dark/hollow faces at the border)
+        // this makes the faces in `fast` and `smooth` look the same in both clamping modes
+        G = next - prev;
+    } else {
+        // add to the gradient where the adjacent voxels are both background
+        // to fix dim pixels due to poor normal estimation
+        G.x = G.x + (next.x - current_val) * 2.0 * detectAdjacentBackground(prev.x, next.x);
+        G.y = G.y + (next.y - current_val) * 2.0 * detectAdjacentBackground(prev.y, next.y);
+        G.z = G.z + (next.z - current_val) * 2.0 * detectAdjacentBackground(prev.z, next.z);
+    }
+
+    return G;
+}
+"""
+
 ISO_CATEGORICAL_SNIPPETS = {
     'before_loop': """
         vec4 color3 = vec4(0.0);  // final color
@@ -195,7 +295,24 @@
 
 shaders = BaseVolume._shaders.copy()
 before, after = shaders['fragment'].split('void main()')
-shaders['fragment'] = before + FUNCTION_DEFINITIONS + 'void main()' + after
+FAST_GRADIENT_SHADER = (
+    before
+    + FUNCTION_DEFINITIONS
+    + FAST_GRADIENT_DEFINITION
+    + CALCULATE_COLOR_DEFINITION
+    + 'void main()'
+    + after
+)
+SMOOTH_GRADIENT_SHADER = (
+    before
+    + FUNCTION_DEFINITIONS
+    + SMOOTH_GRADIENT_DEFINITION
+    + CALCULATE_COLOR_DEFINITION
+    + 'void main()'
+    + after
+)
+
+shaders['fragment'] = FAST_GRADIENT_SHADER
 
 rendering_methods = BaseVolume._rendering_methods.copy()
 rendering_methods['iso_categorical'] = ISO_CATEGORICAL_SNIPPETS
@@ -203,6 +320,49 @@
 
 
 class Volume(TextureMixin, BaseVolume):
+    """This class extends the vispy Volume visual to add categorical isosurface rendering."""
+
     # add the new rendering method to the snippets dict
     _shaders = shaders
     _rendering_methods = rendering_methods
+
+    def __init__(self, *args, **kwargs) -> None:  # type: ignore [no-untyped-def]
+        super().__init__(*args, **kwargs)
+        self.unfreeze()
+        self.clamp_at_border = False
+        self.iso_gradient_mode = IsoCategoricalGradientMode.FAST.value
+        self.freeze()
+
+    @property
+    def iso_gradient_mode(self) -> str:
+        return str(self._iso_gradient_mode)
+
+    @iso_gradient_mode.setter
+    def iso_gradient_mode(self, value: str) -> None:
+        self._iso_gradient_mode = IsoCategoricalGradientMode(value)
+        self.shared_program.frag = (
+            SMOOTH_GRADIENT_SHADER
+            if value == IsoCategoricalGradientMode.SMOOTH
+            else FAST_GRADIENT_SHADER
+        )
+        self.shared_program['u_clamp_at_border'] = self._clamp_at_border
+        self.update()
+
+    @property
+    def clamp_at_border(self) -> bool:
+        """Clamp values beyond volume limits when computing isosurface gradients.
+
+        This has an effect on the appearance of labels at the border of the volume.
+
+            True: labels will appear darker at the border. [DEFAULT]
+
+            False: labels will appear brighter at the border, as if the volume extends beyond its
+            actual limits but the labels do not.
+        """
+        return self._clamp_at_border
+
+    @clamp_at_border.setter
+    def clamp_at_border(self, value: bool) -> None:
+        self._clamp_at_border = value
+        self.shared_program['u_clamp_at_border'] = self._clamp_at_border
+        self.update()
diff --git a/napari/benchmarks/README.md b/napari/benchmarks/README.md
index 2e5d68b3456..c29c8057a17 100644
--- a/napari/benchmarks/README.md
+++ b/napari/benchmarks/README.md
@@ -21,3 +21,22 @@ To run a single benchmark (Vectors3DSuite.time_refresh) with the environment you
 To compare benchmarks across branches, run using conda environments (instead of virtualenv), and limit to the `Labels2DSuite` benchmarks:
 
 `asv continuous main fix_benchmark_ci -q --environment conda --bench Labels2DSuite`
+
+
+## Debugging
+
+To simplify debugging we can run the benchmarks in the current environment as simple python functions script.
+
+You could do this by running the following command:
+
+```bash
+python -m napari.benchmarks benchmark_shapes_layer.Shapes3DSuite.time_get_value
+```
+
+or
+
+```bash
+python napari/benchmarks/benchmark_shapes_layer.py Shapes3DSuite.time_get_value
+```
+
+Passing the proper benchmark identifier as argument.
diff --git a/napari/benchmarks/__main__.py b/napari/benchmarks/__main__.py
new file mode 100644
index 00000000000..6815793111f
--- /dev/null
+++ b/napari/benchmarks/__main__.py
@@ -0,0 +1,41 @@
+import argparse
+import importlib
+from typing import NamedTuple
+
+from .utils import run_benchmark_from_module
+
+
+class BenchmarkIdentifier(NamedTuple):
+    module: str
+    klass: str
+    method: str
+
+
+def split_identifier(value: str) -> BenchmarkIdentifier:
+    """Split a string into a module and class identifier."""
+    parts = value.split('.')
+    if len(parts) != 3:
+        raise argparse.ArgumentError(
+            "Benchmark identifier should be in the form 'module.class.benchmark'"
+        )
+    return BenchmarkIdentifier(*parts)
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description='Run selected napari benchmarks for debugging.'
+    )
+    parser.add_argument(
+        'benchmark', type=split_identifier, help='Benchmark to run.'
+    )
+    args = parser.parse_args()
+    module = importlib.import_module(
+        f'.{args.benchmark.module}', package='napari.benchmarks'
+    )
+    run_benchmark_from_module(
+        module, args.benchmark.klass, args.benchmark.method
+    )
+
+
+if __name__ == '__main__':
+    main()
diff --git a/napari/benchmarks/benchmark_evented_model.py b/napari/benchmarks/benchmark_evented_model.py
index cfde095b129..51298b8265e 100644
--- a/napari/benchmarks/benchmark_evented_model.py
+++ b/napari/benchmarks/benchmark_evented_model.py
@@ -57,3 +57,9 @@ def long_connection(event):
         self.model.events.e.connect(long_connection)
 
         self.model.d = 15
+
+
+if __name__ == '__main__':
+    from utils import run_benchmark
+
+    run_benchmark()
diff --git a/napari/benchmarks/benchmark_image_layer.py b/napari/benchmarks/benchmark_image_layer.py
index 498553fe490..cdd7e2c5da1 100644
--- a/napari/benchmarks/benchmark_image_layer.py
+++ b/napari/benchmarks/benchmark_image_layer.py
@@ -103,3 +103,9 @@ def mem_layer(self, n):
     def mem_data(self, n):
         """Memory used by raw data."""
         return self.data
+
+
+if __name__ == '__main__':
+    from utils import run_benchmark
+
+    run_benchmark()
diff --git a/napari/benchmarks/benchmark_import.py b/napari/benchmarks/benchmark_import.py
index cb5c0a43c3f..23f58d8614a 100644
--- a/napari/benchmarks/benchmark_import.py
+++ b/napari/benchmarks/benchmark_import.py
@@ -6,3 +6,9 @@ class ImportTimeSuite:
     def time_import(self):
         cmd = [sys.executable, '-c', 'import napari']
         subprocess.run(cmd, stderr=subprocess.PIPE)
+
+
+if __name__ == '__main__':
+    from utils import run_benchmark
+
+    run_benchmark()
diff --git a/napari/benchmarks/benchmark_labels_layer.py b/napari/benchmarks/benchmark_labels_layer.py
index 99e271aeec9..f28ccc52830 100644
--- a/napari/benchmarks/benchmark_labels_layer.py
+++ b/napari/benchmarks/benchmark_labels_layer.py
@@ -215,3 +215,9 @@ def mem_layer(self, *_):
     def mem_data(self, *_):
         """Memory used by raw data."""
         return self.data
+
+
+if __name__ == '__main__':
+    from utils import run_benchmark
+
+    run_benchmark()
diff --git a/napari/benchmarks/benchmark_points_layer.py b/napari/benchmarks/benchmark_points_layer.py
index d4e02afbb57..0a8c9eb0340 100644
--- a/napari/benchmarks/benchmark_points_layer.py
+++ b/napari/benchmarks/benchmark_points_layer.py
@@ -153,3 +153,9 @@ def setup(self, num_points, mask_shape, point_size):
 
     def time_to_mask(self, num_points, mask_shape, point_size):
         self.layer.to_mask(shape=mask_shape)
+
+
+if __name__ == '__main__':
+    from utils import run_benchmark
+
+    run_benchmark()
diff --git a/napari/benchmarks/benchmark_python_layer.py b/napari/benchmarks/benchmark_python_layer.py
index 109dad62006..66557204926 100644
--- a/napari/benchmarks/benchmark_python_layer.py
+++ b/napari/benchmarks/benchmark_python_layer.py
@@ -14,3 +14,9 @@ def time_coerce_symbols1(self):
 
     def time_coerce_symbols2(self):
         coerce_symbols(self.symbols2)
+
+
+if __name__ == '__main__':
+    from utils import run_benchmark
+
+    run_benchmark()
diff --git a/napari/benchmarks/benchmark_qt_slicing.py b/napari/benchmarks/benchmark_qt_slicing.py
index 25a25b4f1f2..6646db883fd 100644
--- a/napari/benchmarks/benchmark_qt_slicing.py
+++ b/napari/benchmarks/benchmark_qt_slicing.py
@@ -186,3 +186,9 @@ def time_z_scroll(self, *args):
 
     def teardown(self, *args):
         self.viewer.window.close()
+
+
+if __name__ == '__main__':
+    from utils import run_benchmark
+
+    run_benchmark()
diff --git a/napari/benchmarks/benchmark_qt_viewer.py b/napari/benchmarks/benchmark_qt_viewer.py
index 6a1db0024a7..4bd64397b71 100644
--- a/napari/benchmarks/benchmark_qt_viewer.py
+++ b/napari/benchmarks/benchmark_qt_viewer.py
@@ -17,3 +17,9 @@ def teardown(self):
     def time_create_viewer(self):
         """Time to create the viewer."""
         self.viewer = napari.Viewer()
+
+
+if __name__ == '__main__':
+    from utils import run_benchmark
+
+    run_benchmark()
diff --git a/napari/benchmarks/benchmark_qt_viewer_image.py b/napari/benchmarks/benchmark_qt_viewer_image.py
index 96136fccfab..24ae61928d2 100644
--- a/napari/benchmarks/benchmark_qt_viewer_image.py
+++ b/napari/benchmarks/benchmark_qt_viewer_image.py
@@ -276,3 +276,9 @@ def time_change_gamma(self, n):
         self.viewer.layers[0].gamma = 0.5
         self.viewer.layers[0].gamma = 0.8
         self.viewer.layers[0].gamma = 1.3
+
+
+if __name__ == '__main__':
+    from utils import run_benchmark
+
+    run_benchmark()
diff --git a/napari/benchmarks/benchmark_qt_viewer_labels.py b/napari/benchmarks/benchmark_qt_viewer_labels.py
index 393b51c00ec..902a4085819 100644
--- a/napari/benchmarks/benchmark_qt_viewer_labels.py
+++ b/napari/benchmarks/benchmark_qt_viewer_labels.py
@@ -219,3 +219,9 @@ def time_iterate_components(self, *args):
 
     def time_zoom_change(self, *args):
         self._time_zoom_change(*args)
+
+
+if __name__ == '__main__':
+    from utils import run_benchmark
+
+    run_benchmark()
diff --git a/napari/benchmarks/benchmark_qt_viewer_vectors.py b/napari/benchmarks/benchmark_qt_viewer_vectors.py
index d6780cabfb9..d5908907b6b 100644
--- a/napari/benchmarks/benchmark_qt_viewer_vectors.py
+++ b/napari/benchmarks/benchmark_qt_viewer_vectors.py
@@ -48,3 +48,9 @@ def time_vectors_multi_refresh(self, n):
         self.viewer.layers[0].refresh()
         self.viewer.layers[0].refresh()
         self.viewer.layers[0].refresh()
+
+
+if __name__ == '__main__':
+    from utils import run_benchmark
+
+    run_benchmark()
diff --git a/napari/benchmarks/benchmark_shapes_layer.py b/napari/benchmarks/benchmark_shapes_layer.py
index 89aafa61612..7fb431ac71b 100644
--- a/napari/benchmarks/benchmark_shapes_layer.py
+++ b/napari/benchmarks/benchmark_shapes_layer.py
@@ -46,7 +46,8 @@ def time_update_thumbnail(self, n):
 
     def time_get_value(self, n):
         """Time to get current value."""
-        self.layer.get_value((0,) * 2)
+        for i in range(100):
+            self.layer.get_value((i,) * 2)
 
     def mem_layer(self, n):
         """Memory used by layer."""
@@ -209,3 +210,9 @@ def time_select_shape(self, n):
         mouse_release_callbacks(self.layer, release_event)
 
     time_select_shape.param_names = ['n_shapes']
+
+
+if __name__ == '__main__':
+    from utils import run_benchmark
+
+    run_benchmark()
diff --git a/napari/benchmarks/benchmark_surface_layer.py b/napari/benchmarks/benchmark_surface_layer.py
index 02e5bc588d9..2bb133f26ef 100644
--- a/napari/benchmarks/benchmark_surface_layer.py
+++ b/napari/benchmarks/benchmark_surface_layer.py
@@ -91,3 +91,9 @@ def mem_layer(self, n):
     def mem_data(self, n):
         """Memory used by raw data."""
         return self.data
+
+
+if __name__ == '__main__':
+    from utils import run_benchmark
+
+    run_benchmark()
diff --git a/napari/benchmarks/benchmark_text_manager.py b/napari/benchmarks/benchmark_text_manager.py
index 836562afb6a..9fba1668018 100644
--- a/napari/benchmarks/benchmark_text_manager.py
+++ b/napari/benchmarks/benchmark_text_manager.py
@@ -68,3 +68,9 @@ def time_remove_as_batch(self, n, string):
     time_remove_as_batch.warmup_time = 0
     # See https://asv.readthedocs.io/en/stable/benchmarks.html#timing-benchmarks
     # for more details
+
+
+if __name__ == '__main__':
+    from utils import run_benchmark
+
+    run_benchmark()
diff --git a/napari/benchmarks/benchmark_tracks_layer.py b/napari/benchmarks/benchmark_tracks_layer.py
index ce7e2457c19..3868e41864c 100644
--- a/napari/benchmarks/benchmark_tracks_layer.py
+++ b/napari/benchmarks/benchmark_tracks_layer.py
@@ -45,3 +45,9 @@ def time_create_layer(self, *_) -> None:
 
     def time_update_layer(self, *_) -> None:
         self.layer.data = self.data
+
+
+if __name__ == '__main__':
+    from utils import run_benchmark
+
+    run_benchmark()
diff --git a/napari/benchmarks/benchmark_vectors_layer.py b/napari/benchmarks/benchmark_vectors_layer.py
index 6fdca4226a0..7dfd7d6fda3 100644
--- a/napari/benchmarks/benchmark_vectors_layer.py
+++ b/napari/benchmarks/benchmark_vectors_layer.py
@@ -99,3 +99,9 @@ def mem_layer(self, n):
     def mem_data(self, n):
         """Memory used by raw data."""
         return self.data
+
+
+if __name__ == '__main__':
+    from utils import run_benchmark
+
+    run_benchmark()
diff --git a/napari/benchmarks/utils.py b/napari/benchmarks/utils.py
index e046dfd2fb9..3abf2363346 100644
--- a/napari/benchmarks/utils.py
+++ b/napari/benchmarks/utils.py
@@ -2,6 +2,7 @@
 import os
 from collections.abc import Sequence
 from functools import lru_cache
+from types import ModuleType
 from typing import (
     Callable,
     Literal,
@@ -222,3 +223,51 @@ def labeled_particles(
         return labels, densities, points
     else:  # noqa: RET505
         return labels
+
+
+def run_benchmark_from_module(
+    module: ModuleType, klass_name: str, method_name: str
+):
+    klass = getattr(module, klass_name)
+    if getattr(klass, 'params', None):
+        skip_if = getattr(klass, 'skip_params', {})
+        if isinstance(klass.params[0], Sequence):
+            params = itertools.product(*klass.params)
+        else:
+            params = ((i,) for i in klass.params)
+        for param in params:
+            if param in skip_if:
+                continue
+            obj = klass()
+            try:
+                obj.setup(*param)
+            except NotImplementedError:
+                continue
+            getattr(obj, method_name)(*param)
+            getattr(obj, 'teardown', lambda: None)()
+    else:
+        obj = klass()
+        try:
+            obj.setup()
+        except NotImplementedError:
+            return
+        getattr(obj, method_name)()
+        getattr(obj, 'teardown', lambda: None)()
+
+
+def run_benchmark():
+    import argparse
+    import inspect
+
+    parser = argparse.ArgumentParser(description='Run benchmark')
+    parser.add_argument(
+        'benchmark', type=str, help='Name of the benchmark to run', default=''
+    )
+
+    args = parser.parse_args()
+
+    benchmark_selection = args.benchmark.split('.')
+
+    # get module of parent frame
+    call_module = inspect.getmodule(inspect.currentframe().f_back)
+    run_benchmark_from_module(call_module, *benchmark_selection)
diff --git a/napari/components/_layer_slicer.py b/napari/components/_layer_slicer.py
index 08b11c51599..88fdfeef415 100644
--- a/napari/components/_layer_slicer.py
+++ b/napari/components/_layer_slicer.py
@@ -258,13 +258,7 @@ def shutdown(self) -> None:
         This should only be called from the main thread.
         """
         logger.debug('_LayerSlicer.shutdown')
-        # Replace with cancel_futures=True in shutdown when we drop support
-        # for Python 3.8
-        with self._lock_layers_to_task:
-            tasks = tuple(self._layers_to_task.values())
-        for task in tasks:
-            task.cancel()
-        self._executor.shutdown(wait=True)
+        self._executor.shutdown(wait=True, cancel_futures=True)
         self.events.disconnect()
         self.events.ready.disconnect()
 
diff --git a/napari/components/_tests/test_layer_slicer.py b/napari/components/_tests/test_layer_slicer.py
index 063fbf2c6f2..318994d3f7a 100644
--- a/napari/components/_tests/test_layer_slicer.py
+++ b/napari/components/_tests/test_layer_slicer.py
@@ -68,7 +68,7 @@ def _slice_dims(self, *args, **kwargs) -> None:
         self.slice_count += 1
 
 
-@pytest.fixture()
+@pytest.fixture
 def layer_slicer():
     layer_slicer = _LayerSlicer()
     layer_slicer._force_sync = False
diff --git a/napari/components/_tests/test_multichannel.py b/napari/components/_tests/test_multichannel.py
index 49edce8876f..bbc010135b4 100644
--- a/napari/components/_tests/test_multichannel.py
+++ b/napari/components/_tests/test_multichannel.py
@@ -37,7 +37,7 @@
     # split single RGB image
     ((15, 10, 3), {'colormap': ['red', 'green', 'blue']}),
     # multiple RGB images
-    ((15, 10, 5, 3), {'channel_axis': 2, 'rgb': True}),
+    ((45, 40, 5, 3), {'channel_axis': 2, 'rgb': True}),
     # Test adding multichannel image with custom names.
     ((), {'name': ['multi ' + str(i + 3) for i in range(5)]}),
     # Test adding multichannel image with custom contrast limits.
diff --git a/napari/components/_tests/test_scale_bar.py b/napari/components/_tests/test_scale_bar.py
index 984027cd458..4489cc0e7d3 100644
--- a/napari/components/_tests/test_scale_bar.py
+++ b/napari/components/_tests/test_scale_bar.py
@@ -5,3 +5,9 @@ def test_scale_bar():
     """Test creating scale bar object"""
     scale_bar = ScaleBarOverlay()
     assert scale_bar is not None
+
+
+def test_scale_bar_fixed_length():
+    """Test creating scale bar object with fixed length"""
+    scale_bar = ScaleBarOverlay(length=50)
+    assert scale_bar.length == 50
diff --git a/napari/components/_tests/test_viewer_keybindings.py b/napari/components/_tests/test_viewer_keybindings.py
index a4f68ae9db2..1bfb0508a51 100644
--- a/napari/components/_tests/test_viewer_keybindings.py
+++ b/napari/components/_tests/test_viewer_keybindings.py
@@ -1,7 +1,13 @@
+import numpy as np
 import pytest
 
+from napari._tests.utils import (
+    add_layer_by_type,
+    layer_test_data,
+)
 from napari.components._viewer_key_bindings import (
     hold_for_pan_zoom,
+    rotate_layers,
     show_only_layer_above,
     show_only_layer_below,
     toggle_selected_visibility,
@@ -14,6 +20,7 @@
 from napari.utils.theme import available_themes, get_system_theme
 
 
+@pytest.mark.key_bindings
 def test_theme_toggle_keybinding():
     viewer = ViewerModel()
     assert viewer.theme == get_settings().appearance.theme
@@ -143,6 +150,36 @@ def test_show_only_layer_below():
     assert viewer.layers[0].visible
 
 
+@pytest.mark.parametrize(('layer_class', 'data', 'ndim'), layer_test_data)
+def test_rotate_layers(layer_class, data, ndim):
+    """Test rotate layers works with all layer types/data"""
+    viewer = ViewerModel()
+    layer = add_layer_by_type(viewer, layer_class, data, visible=True)
+    np.testing.assert_array_equal(
+        layer.affine.rotate, np.eye(ndim, dtype=float)
+    )
+    rotate_layers(viewer)
+    np.testing.assert_array_equal(
+        layer.affine.rotate[-2:, -2:], np.array([[0, -1], [1, 0]], dtype=float)
+    )
+
+
+def test_rotate_layers_in_3D():
+    """Test that rotate layers is disabled in 3D viewer mode"""
+    viewer = ViewerModel()
+    layer = add_layer_by_type(
+        viewer, Points, np.array([[1, 2, 3]]), visible=True
+    )
+    initial_rotation_matrix = layer.affine.rotate[-2:, -2:]
+    viewer.dims.ndisplay = 3
+    assert viewer.dims.ndisplay == 3
+    rotate_layers(viewer)
+    # with ndisplay == 3 rotation is disabled, the matrix should not have changed
+    np.testing.assert_array_equal(
+        layer.affine.rotate[-2:, -2:], initial_rotation_matrix
+    )
+
+
 def make_viewer_with_three_layers():
     """Helper function to create a viewer with three layers"""
     viewer = ViewerModel()
diff --git a/napari/components/_tests/test_viewer_model.py b/napari/components/_tests/test_viewer_model.py
index a98b643ab51..dad1b47ff15 100644
--- a/napari/components/_tests/test_viewer_model.py
+++ b/napari/components/_tests/test_viewer_model.py
@@ -593,23 +593,32 @@ def test_active_layer():
     viewer.add_image(np.random.random((5, 5, 10, 15)))
     assert len(viewer.layers) == 1
     assert viewer.layers.selection.active == viewer.layers[0]
+    assert viewer.layers[0]._highlight_visible
 
     # Check newly added layer is active
     viewer.add_image(np.random.random((5, 6, 5, 10, 15)))
     assert len(viewer.layers) == 2
     assert viewer.layers.selection.active == viewer.layers[1]
+    assert not viewer.layers[0]._highlight_visible
+    assert viewer.layers[1]._highlight_visible
 
     # Check no active layer after unselecting all
     viewer.layers.selection.clear()
     assert viewer.layers.selection.active is None
+    assert not viewer.layers[0]._highlight_visible
+    assert not viewer.layers[1]._highlight_visible
 
     # Check selected layer is active
     viewer.layers.selection.add(viewer.layers[0])
     assert viewer.layers.selection.active == viewer.layers[0]
+    assert viewer.layers[0]._highlight_visible
+    assert not viewer.layers[1]._highlight_visible
 
     # Check no layer is active if both layers are selected
     viewer.layers.selection.add(viewer.layers[1])
     assert viewer.layers.selection.active is None
+    assert not viewer.layers[0]._highlight_visible
+    assert not viewer.layers[1]._highlight_visible
 
 
 def test_active_layer_status_update():
@@ -625,7 +634,9 @@ def test_active_layer_status_update():
     time.sleep(1)
     viewer.mouse_over_canvas = True
     viewer.cursor.position = [1, 1, 1, 1, 1]
-    assert viewer.status == viewer.layers.selection.active.get_status(
+    assert viewer._calc_status_from_cursor()[
+        0
+    ] == viewer.layers.selection.active.get_status(
         viewer.cursor.position, world=True
     )
 
@@ -996,3 +1007,46 @@ def test_make_layer_visible_after_slicing():
     layer.visible = True
 
     np.testing.assert_array_equal(layer._slice.image.raw, data[0])
+
+
+def test_get_status_text():
+    viewer = ViewerModel(ndisplay=2)
+    viewer.mouse_over_canvas = False
+    assert viewer._calc_status_from_cursor() is None
+    viewer.mouse_over_canvas = True
+    assert viewer._calc_status_from_cursor() == ('Ready', '')
+    viewer.cursor.position = (1, 2)
+    viewer.add_labels(
+        np.zeros((10, 10), dtype='uint8'), features={'a': [1, 2]}
+    )
+    viewer.tooltip.visible = False
+    assert viewer._calc_status_from_cursor() == (
+        {
+            'coordinates': ' [1 2]: 0; a: 1',
+            'layer_base': 'Labels',
+            'layer_name': 'Labels',
+            'plugin': '',
+            'source_type': '',
+        },
+        '',
+    )
+    viewer.tooltip.visible = True
+    assert viewer._calc_status_from_cursor() == (
+        {
+            'coordinates': ' [1 2]: 0; a: 1',
+            'layer_base': 'Labels',
+            'layer_name': 'Labels',
+            'plugin': '',
+            'source_type': '',
+        },
+        'a: 1',
+    )
+    viewer.update_status_from_cursor()
+    assert viewer.status == {
+        'coordinates': ' [1 2]: 0; a: 1',
+        'layer_base': 'Labels',
+        'layer_name': 'Labels',
+        'plugin': '',
+        'source_type': '',
+    }
+    assert viewer.tooltip.text == 'a: 1'
diff --git a/napari/components/_tests/test_viewer_mouse_bindings.py b/napari/components/_tests/test_viewer_mouse_bindings.py
index 48182250cf4..7fea1cb01d4 100644
--- a/napari/components/_tests/test_viewer_mouse_bindings.py
+++ b/napari/components/_tests/test_viewer_mouse_bindings.py
@@ -1,7 +1,10 @@
+from unittest.mock import Mock
+
 import numpy as np
 import pytest
 
 from napari.components import ViewerModel
+from napari.components._viewer_mouse_bindings import double_click_to_zoom
 from napari.utils._test_utils import read_only_mouse_event
 from napari.utils.interactions import mouse_wheel_callbacks
 
@@ -68,3 +71,60 @@ def test_paint(modifiers, native, expected_dim):
     )
     mouse_wheel_callbacks(viewer, event)
     assert np.equal(viewer.dims.point, expected_dim[3]).all()
+
+
+def test_double_click_to_zoom():
+    viewer = ViewerModel()
+    data = np.zeros((10, 10, 10))
+    viewer.add_image(data)
+
+    # Ensure `pan_zoom` mode is active
+    assert viewer.layers.selection.active.mode == 'pan_zoom'
+
+    # Mock the mouse event
+    event = Mock()
+    event.modifiers = []
+    event.position = [100, 100]
+
+    viewer.camera.center = (0, 0, 0)
+    initial_zoom = viewer.camera.zoom
+    initial_center = np.asarray(viewer.camera.center)
+    assert viewer.dims.ndisplay == 2
+
+    double_click_to_zoom(viewer, event)
+
+    assert viewer.camera.zoom == initial_zoom * 2
+    # should be half way between the old center and the event.position
+    assert np.allclose(viewer.camera.center, (0, 50, 50))
+
+    # Assert the camera center has moved correctly in 3D
+    viewer.dims.ndisplay = 3
+    assert viewer.dims.ndisplay == 3
+    # reset to initial values
+    viewer.camera.center = initial_center
+    viewer.camera.zoom = initial_zoom
+
+    event.position = [0, 100, 100]
+    double_click_to_zoom(viewer, event)
+    assert viewer.camera.zoom == initial_zoom * 2
+    assert np.allclose(viewer.camera.center, (0, 50, 50))
+
+    # Test with Alt key pressed
+    event.modifiers = ['Alt']
+
+    double_click_to_zoom(viewer, event)
+
+    # Assert the zoom level is back to initial
+    assert viewer.camera.zoom == initial_zoom
+    # Assert the camera center is back to initial
+    assert np.allclose(viewer.camera.center, (0, 0, 0))
+
+    # Test in a mode other than pan_zoom
+    viewer.layers.selection.active.mode = 'transform'
+    assert viewer.layers.selection.active.mode != 'pan_zoom'
+
+    double_click_to_zoom(viewer, event)
+
+    # Assert nothing has changed
+    assert viewer.camera.zoom == initial_zoom
+    assert np.allclose(viewer.camera.center, (0, 0, 0))
diff --git a/napari/components/_viewer_key_bindings.py b/napari/components/_viewer_key_bindings.py
index 09a9b4049a3..62ef0efb398 100644
--- a/napari/components/_viewer_key_bindings.py
+++ b/napari/components/_viewer_key_bindings.py
@@ -2,11 +2,14 @@
 
 from typing import TYPE_CHECKING
 
+import numpy as np
 from app_model.types import KeyCode, KeyMod
 
 from napari.components.viewer_model import ViewerModel
 from napari.utils.action_manager import action_manager
+from napari.utils.notifications import show_info
 from napari.utils.theme import available_themes, get_system_theme
+from napari.utils.transforms import Affine
 from napari.utils.translations import trans
 
 if TYPE_CHECKING:
@@ -44,7 +47,7 @@ def extend_selection_to_layer_below(viewer: Viewer):
     viewer.layers.select_previous(shift=True)
 
 
-@register_viewer_action(trans._('Reset scroll.'))
+@register_viewer_action(trans._('Reset scroll'))
 def reset_scroll_progress(viewer: Viewer):
     # on key press
     viewer.dims._scroll_progress = 0
@@ -57,7 +60,7 @@ def reset_scroll_progress(viewer: Viewer):
 reset_scroll_progress.__doc__ = trans._('Reset dims scroll progress')
 
 
-@register_viewer_action(trans._('Toggle 2D/3D view.'))
+@register_viewer_action(trans._('Toggle 2D/3D view'))
 def toggle_ndisplay(viewer: Viewer):
     if viewer.dims.ndisplay == 2:
         viewer.dims.ndisplay = 3
@@ -70,7 +73,7 @@ def toggle_ndisplay(viewer: Viewer):
 # ```
 # RuntimeError: wrapped C/C++ object of type CanvasBackendDesktop has been deleted
 # ```
-@register_viewer_action(trans._('Toggle current viewer theme.'))
+@register_viewer_action(trans._('Toggle current viewer theme'))
 def toggle_theme(viewer: ViewerModel):
     """Toggle theme for current viewer"""
     themes = available_themes()
@@ -87,36 +90,36 @@ def toggle_theme(viewer: ViewerModel):
     viewer.theme = themes[idx]
 
 
-@register_viewer_action(trans._('Reset view to original state.'))
+@register_viewer_action(trans._('Reset view to original state'))
 def reset_view(viewer: Viewer):
     viewer.reset_view()
 
 
-@register_viewer_action(trans._('Delete selected layers.'))
+@register_viewer_action(trans._('Delete selected layers'))
 def delete_selected_layers(viewer: Viewer):
     viewer.layers.remove_selected()
 
 
 @register_viewer_action(
-    trans._('Increment dimensions slider to the left.'), repeatable=True
+    trans._('Increment dimensions slider to the left'), repeatable=True
 )
 def increment_dims_left(viewer: Viewer):
     viewer.dims._increment_dims_left()
 
 
 @register_viewer_action(
-    trans._('Increment dimensions slider to the right.'), repeatable=True
+    trans._('Increment dimensions slider to the right'), repeatable=True
 )
 def increment_dims_right(viewer: Viewer):
     viewer.dims._increment_dims_right()
 
 
-@register_viewer_action(trans._('Move focus of dimensions slider up.'))
+@register_viewer_action(trans._('Move focus of dimensions slider up'))
 def focus_axes_up(viewer: Viewer):
     viewer.dims._focus_up()
 
 
-@register_viewer_action(trans._('Move focus of dimensions slider down.'))
+@register_viewer_action(trans._('Move focus of dimensions slider down'))
 def focus_axes_down(viewer: Viewer):
     viewer.dims._focus_down()
 
@@ -124,7 +127,7 @@ def focus_axes_down(viewer: Viewer):
 # Use non-breaking spaces and non-breaking hyphen for Preferences table
 @register_viewer_action(
     trans._(
-        'Change order of the visible axes, e.g.\u00a0[0,\u00a01,\u00a02]\u00a0\u2011>\u00a0[2,\u00a00,\u00a01].'
+        'Change order of the visible axes, e.g.\u00a0[0,\u00a01,\u00a02]\u00a0\u2011>\u00a0[2,\u00a00,\u00a01]'
     ),
 )
 def roll_axes(viewer: Viewer):
@@ -134,14 +137,50 @@ def roll_axes(viewer: Viewer):
 # Use non-breaking spaces and non-breaking hyphen for Preferences table
 @register_viewer_action(
     trans._(
-        'Transpose order of the last two visible axes, e.g.\u00a0[0,\u00a01]\u00a0\u2011>\u00a0[1,\u00a00].'
+        'Transpose order of the last two visible axes, e.g.\u00a0[0,\u00a01]\u00a0\u2011>\u00a0[1,\u00a00]'
     ),
 )
 def transpose_axes(viewer: Viewer):
     viewer.dims.transpose()
 
 
-@register_viewer_action(trans._('Toggle grid mode.'))
+@register_viewer_action(trans._('Rotate layers 90 degrees counter-clockwise'))
+def rotate_layers(viewer: Viewer):
+    if viewer.dims.ndisplay == 3:
+        show_info(trans._('Rotating layers only works in 2D'))
+        return
+    for layer in viewer.layers:
+        if layer.ndim == 2:
+            visible_dims = [0, 1]
+        else:
+            visible_dims = list(viewer.dims.displayed)
+
+        initial_affine = layer.affine.set_slice(visible_dims)
+        # want to rotate around a fixed refernce for all layers
+        center = (
+            np.asarray(viewer.dims.range)[:, 0][
+                np.asarray(viewer.dims.displayed)
+            ]
+            + (
+                np.asarray(viewer.dims.range)[:, 1][
+                    np.asarray(viewer.dims.displayed)
+                ]
+                - np.asarray(viewer.dims.range)[:, 0][
+                    np.asarray(viewer.dims.displayed)
+                ]
+            )
+            / 2
+        )
+        new_affine = (
+            Affine(translate=center)
+            .compose(Affine(rotate=90))
+            .compose(Affine(translate=-center))
+            .compose(initial_affine)
+        )
+        layer.affine = layer.affine.replace_slice(visible_dims, new_affine)
+
+
+@register_viewer_action(trans._('Toggle grid mode'))
 def toggle_grid(viewer: Viewer):
     viewer.grid.enabled = not viewer.grid.enabled
 
@@ -158,13 +197,13 @@ def toggle_unselected_visibility(viewer: Viewer):
             layer.visible = not layer.visible
 
 
-@register_viewer_action(trans._('Select and show only layer above.'))
+@register_viewer_action(trans._('Select and show only layer above'))
 def show_only_layer_above(viewer):
     viewer.layers.select_next()
     _show_only_selected_layer(viewer)
 
 
-@register_viewer_action(trans._('Select and show only layer below.'))
+@register_viewer_action(trans._('Select and show only layer below'))
 def show_only_layer_below(viewer):
     viewer.layers.select_previous()
     _show_only_selected_layer(viewer)
diff --git a/napari/components/_viewer_mouse_bindings.py b/napari/components/_viewer_mouse_bindings.py
index 02da667bc83..c70319ee4cc 100644
--- a/napari/components/_viewer_mouse_bindings.py
+++ b/napari/components/_viewer_mouse_bindings.py
@@ -1,3 +1,6 @@
+import numpy as np
+
+
 def dims_scroll(viewer, event):
     """Scroll the dimensions slider."""
     if 'Control' not in event.modifiers:
@@ -13,3 +16,26 @@ def dims_scroll(viewer, event):
         else:
             viewer.dims._increment_dims_right()
             viewer.dims._scroll_progress -= 1
+
+
+def double_click_to_zoom(viewer, event):
+    """Zoom in on double click by zoom_factor; zoom out with Alt."""
+    if (
+        viewer.layers.selection.active
+        and viewer.layers.selection.active.mode != 'pan_zoom'
+    ):
+        return
+    # if Alt held down, zoom out instead
+    zoom_factor = 0.5 if 'Alt' in event.modifiers else 2
+
+    viewer.camera.zoom *= zoom_factor
+    if viewer.dims.ndisplay == 3:
+        viewer.camera.center = np.asarray(viewer.camera.center) + (
+            np.asarray(event.position)[np.asarray(viewer.dims.displayed)]
+            - np.asarray(viewer.camera.center)
+        ) * (1 - 1 / zoom_factor)
+    else:
+        viewer.camera.center = np.asarray(viewer.camera.center)[-2:] + (
+            np.asarray(event.position)[-2:]
+            - np.asarray(viewer.camera.center)[-2:]
+        ) * (1 - 1 / zoom_factor)
diff --git a/napari/components/overlays/__init__.py b/napari/components/overlays/__init__.py
index a8a8fbb4b47..e5179b5b482 100644
--- a/napari/components/overlays/__init__.py
+++ b/napari/components/overlays/__init__.py
@@ -16,14 +16,14 @@
 
 __all__ = [
     'AxesOverlay',
-    'Overlay',
-    'CanvasOverlay',
     'BoundingBoxOverlay',
-    'SelectionBoxOverlay',
-    'TransformBoxOverlay',
+    'BrushCircleOverlay',
+    'CanvasOverlay',
     'LabelsPolygonOverlay',
+    'Overlay',
     'ScaleBarOverlay',
     'SceneOverlay',
+    'SelectionBoxOverlay',
     'TextOverlay',
-    'BrushCircleOverlay',
+    'TransformBoxOverlay',
 ]
diff --git a/napari/components/overlays/scale_bar.py b/napari/components/overlays/scale_bar.py
index 365d56ee064..b911a638e11 100644
--- a/napari/components/overlays/scale_bar.py
+++ b/napari/components/overlays/scale_bar.py
@@ -35,6 +35,9 @@ class ScaleBarOverlay(CanvasOverlay):
     unit : Optional[str]
         Unit to be used by the scale bar. The value can be set
         to `None` to display no units.
+    length : Optional[float]
+        Fixed length of the scale bar in physical units. If set to `None`,
+        it is determined automatically based on zoom level.
     position : CanvasPosition
         The position of the overlay in the canvas.
     visible : bool
@@ -54,3 +57,4 @@ class ScaleBarOverlay(CanvasOverlay):
         default_factory=lambda: ColorValue([0, 0, 0, 0.6])
     )
     unit: Optional[str] = None
+    length: Optional[float] = None
diff --git a/napari/components/viewer_model.py b/napari/components/viewer_model.py
index 1874928456d..aea9315db82 100644
--- a/napari/components/viewer_model.py
+++ b/napari/components/viewer_model.py
@@ -4,7 +4,7 @@
 import itertools
 import os
 import warnings
-from collections.abc import Iterator, Sequence
+from collections.abc import Iterator, Mapping, Sequence
 from functools import lru_cache
 from pathlib import Path
 from typing import (
@@ -17,14 +17,17 @@
 
 import numpy as np
 
-# This cannot be condition to TYPE_CHEKCKING or the stubgen fails
-# with underfined Context.
+# This cannot be condition to TYPE_CHECKING or the stubgen fails
+# with undefined Context.
 from app_model.expressions import Context
 
 from napari import layers
 from napari._pydantic_compat import Extra, Field, PrivateAttr, validator
 from napari.components._layer_slicer import _LayerSlicer
-from napari.components._viewer_mouse_bindings import dims_scroll
+from napari.components._viewer_mouse_bindings import (
+    dims_scroll,
+    double_click_to_zoom,
+)
 from napari.components.camera import Camera
 from napari.components.cursor import Cursor, CursorStyle
 from napari.components.dims import Dims
@@ -268,15 +271,13 @@ def __init__(
         #        the source of truth, and is now defined in world space. This exposed an existing
         #        bug where if a field in Dims is modified by the root_validator, events won't
         #        be fired for it. This won't happen for properties because we have dependency
-        #        checks. To fix this, we need dep checks for fileds (psygnal!) and then we
+        #        checks. To fix this, we need dep checks for fields (psygnal!) and then we
         #        can remove the following line. Note that because of this we fire double events,
         #        but this should be ok because we have early returns when slices are unchanged.
         self.dims.events.current_step.connect(self._update_layers)
         self.dims.events.margin_left.connect(self._update_layers)
         self.dims.events.margin_right.connect(self._update_layers)
-        self.cursor.events.position.connect(
-            self._update_status_bar_from_cursor
-        )
+        self.cursor.events.position.connect(self.update_status_from_cursor)
         self.layers.events.inserted.connect(self._on_add_layer)
         self.layers.events.removed.connect(self._on_remove_layer)
         self.layers.events.reordered.connect(self._on_grid_change)
@@ -285,6 +286,7 @@ def __init__(
 
         # Add mouse callback
         self.mouse_wheel_callbacks.append(dims_scroll)
+        self.mouse_double_click_callbacks.append(double_click_to_zoom)
 
         self._overlays.update({k: v() for k, v in DEFAULT_OVERLAYS.items()})
 
@@ -377,7 +379,9 @@ def _sliced_extent_world_augmented(self) -> np.ndarray:
             )
         return self.layers._extent_world_augmented[:, self.dims.displayed]
 
-    def reset_view(self, *, margin: float = 0.05) -> None:
+    def reset_view(
+        self, *, margin: float = 0.05, reset_camera_angle: bool = True
+    ) -> None:
         """Reset the camera view.
 
         Parameters
@@ -406,7 +410,7 @@ def reset_view(self, *, margin: float = 0.05) -> None:
         )
         assert len(center) in (2, 3)
         self.camera.center = center
-        # zoom is definied as the number of canvas pixels per world pixel
+        # zoom is defined as the number of canvas pixels per world pixel
         # The default value used below will zoom such that the whole field
         # of view will occupy 95% of the canvas on the most filled axis
 
@@ -428,7 +432,8 @@ def reset_view(self, *, margin: float = 0.05) -> None:
             self.camera.zoom = scale_factor * np.min(
                 np.array(self._canvas_size) / scale
             )
-        self.camera.angles = (0, 0, 90)
+        if reset_camera_angle:
+            self.camera.angles = (0, 0, 90)
 
         # Emit a reset view event, which is no longer used internally, but
         # which maybe useful for building on napari.
@@ -473,6 +478,9 @@ def _update_layers(self, *, layers=None):
         # shown with this position may be incorrect. See the discussion for more details:
         # https://github.com/napari/napari/pull/5377#discussion_r1036280855
         position = list(self.cursor.position)
+        if len(position) < self.dims.ndim:
+            # cursor dimensionality is outdated — reset to correct dimension
+            position = [0.0] * self.dims.ndim
         for ind in self.dims.order[: -self.dims.ndisplay]:
             position[ind] = self.dims.point[ind]
         self.cursor.position = tuple(position)
@@ -483,19 +491,24 @@ def _on_active_layer(self, event):
         if active_layer is None:
             for layer in self.layers:
                 layer.update_transform_box_visibility(False)
+                layer.update_highlight_visibility(False)
             self.help = ''
             self.cursor.style = CursorStyle.STANDARD
+            self.camera.mouse_pan = True
+            self.camera.mouse_zoom = True
         else:
             active_layer.update_transform_box_visibility(True)
+            active_layer.update_highlight_visibility(True)
             for layer in self.layers:
                 if layer != active_layer:
                     layer.update_transform_box_visibility(False)
+                    layer.update_highlight_visibility(False)
             self.help = active_layer.help
             self.cursor.style = active_layer.cursor
             self.cursor.size = active_layer.cursor_size
             self.camera.mouse_pan = active_layer.mouse_pan
             self.camera.mouse_zoom = active_layer.mouse_zoom
-            self._update_status_bar_from_cursor()
+            self.update_status_from_cursor()
 
     @staticmethod
     def rounded_division(min_val, max_val, precision):
@@ -550,33 +563,41 @@ def _update_async(self, event: Event) -> None:
         """Set layer slicer to force synchronous if async is disabled."""
         self._layer_slicer._force_sync = not event.value
 
-    def _update_status_bar_from_cursor(self, event=None):
-        """Update the status bar based on the current cursor position.
-
-        This is generally used as a callback when cursor.position is updated.
-        """
-        # Update status and help bar based on active layer
+    def _calc_status_from_cursor(
+        self,
+    ) -> Optional[tuple[Union[str, Dict], str]]:
         if not self.mouse_over_canvas:
-            return
+            return None
         active = self.layers.selection.active
         if active is not None:
-            self.status = active.get_status(
+            status = active.get_status(
                 self.cursor.position,
                 view_direction=self.cursor._view_direction,
                 dims_displayed=list(self.dims.displayed),
                 world=True,
             )
 
-            self.help = active.help
             if self.tooltip.visible:
-                self.tooltip.text = active._get_tooltip_text(
+                tooltip_text = active._get_tooltip_text(
                     np.asarray(self.cursor.position),
                     view_direction=np.asarray(self.cursor._view_direction),
                     dims_displayed=list(self.dims.displayed),
                     world=True,
                 )
-        else:
-            self.status = 'Ready'
+            else:
+                tooltip_text = ''
+
+            return status, tooltip_text
+
+        return 'Ready', ''
+
+    def update_status_from_cursor(self):
+        """Update the status and tooltip from the cursor position."""
+        status = self._calc_status_from_cursor()
+        if status is not None:
+            self.status, self.tooltip.text = status
+        if (active := self.layers.selection.active) is not None:
+            self.help = active.help
 
     def _on_grid_change(self):
         """Arrange the current layers is a 2D grid."""
@@ -1438,7 +1459,7 @@ def _add_layers_with_plugins(
     def _add_layer_from_data(
         self,
         data,
-        meta: Optional[Dict[str, Any]] = None,
+        meta: Optional[Mapping[str, Any]] = None,
         layer_type: Optional[str] = None,
     ) -> list[Layer]:
         """Add arbitrary layer data to the viewer.
@@ -1581,12 +1602,12 @@ def _unify_data_and_user_kwargs(
 ) -> FullLayerData:
     """Merge data returned from plugins with options specified by user.
 
-    If ``data == (_data, _meta, _type)``.  Then:
+    If ``data == (data_, meta_, type_)``.  Then:
 
-    - ``kwargs`` will be used to update ``_meta``
-    - ``layer_type`` will replace ``_type`` and, if provided, ``_meta`` keys
+    - ``kwargs`` will be used to update ``meta_``
+    - ``layer_type`` will replace ``type_`` and, if provided, ``meta_`` keys
         will be pruned to layer_type-appropriate kwargs
-    - ``fallback_name`` is used if ``not _meta.get('name')``
+    - ``fallback_name`` is used if ``not meta_.get('name')``
 
     .. note:
 
@@ -1613,13 +1634,16 @@ def _unify_data_and_user_kwargs(
     FullLayerData
         Fully qualified LayerData tuple with user-provided overrides.
     """
-    _data, _meta, _type = _normalize_layer_data(data)
+    data_, meta_, type_ = _normalize_layer_data(data)
 
     if layer_type:
         # the user has explicitly requested this be a certain layer type
         # strip any kwargs from the plugin that are no longer relevant
-        _meta = prune_kwargs(_meta, layer_type)
-        _type = layer_type
+        meta_ = prune_kwargs(meta_, layer_type)
+        type_ = layer_type
+
+    if not isinstance(meta_, dict):
+        meta_ = dict(meta_)
 
     if kwargs:
         # if user provided kwargs, use to override any meta dict values that
@@ -1628,14 +1652,14 @@ def _unify_data_and_user_kwargs(
         # both layer_type and additional keyword arguments to viewer.open(),
         # it is their responsibility to make sure the kwargs match the
         # layer_type.
-        _meta.update(prune_kwargs(kwargs, _type) if not layer_type else kwargs)
+        meta_.update(prune_kwargs(kwargs, type_) if not layer_type else kwargs)
 
-    if not _meta.get('name') and fallback_name:
-        _meta['name'] = fallback_name
-    return (_data, _meta, _type)
+    if not meta_.get('name') and fallback_name:
+        meta_['name'] = fallback_name
+    return data_, meta_, type_
 
 
-def prune_kwargs(kwargs: dict[str, Any], layer_type: str) -> dict[str, Any]:
+def prune_kwargs(kwargs: Mapping[str, Any], layer_type: str) -> dict[str, Any]:
     """Return copy of ``kwargs`` with only keys valid for ``add_<layer_type>``
 
     Parameters
diff --git a/napari/conftest.py b/napari/conftest.py
index 5437536e87d..7c52e252c25 100644
--- a/napari/conftest.py
+++ b/napari/conftest.py
@@ -31,10 +31,13 @@ def get_reader(path):
 
 from __future__ import annotations
 
+import contextlib
 import os
 import sys
+import threading
 from concurrent.futures import ThreadPoolExecutor
 from contextlib import suppress
+from functools import partial
 from itertools import chain
 from multiprocessing.pool import ThreadPool
 from pathlib import Path
@@ -71,7 +74,7 @@ def get_reader(path):
         xauth.touch()
 
 
-@pytest.fixture()
+@pytest.fixture
 def layer_data_and_types():
     """Fixture that provides some layers and filenames
 
@@ -153,7 +156,7 @@ def layer(request):
     return None
 
 
-@pytest.fixture()
+@pytest.fixture
 def layers():
     """Fixture that supplies a layers list for testing.
 
@@ -254,7 +257,7 @@ def _auto_shutdown_dask_threadworkers():
 HistoryManager.enabled = False
 
 
-@pytest.fixture()
+@pytest.fixture
 def napari_svg_name():
     """the plugin name changes with npe2 to `napari-svg` from `svg`."""
     from importlib.metadata import version
@@ -274,13 +277,13 @@ def npe2pm_(npe2pm, monkeypatch):
     return npe2pm
 
 
-@pytest.fixture()
+@pytest.fixture
 def builtins(npe2pm_: TestPluginManager):
     with npe2pm_.tmp_plugin(package='napari') as plugin:
         yield plugin
 
 
-@pytest.fixture()
+@pytest.fixture
 def tmp_plugin(npe2pm_: TestPluginManager):
     with npe2pm_.tmp_plugin() as plugin:
         plugin.manifest.package_metadata = PackageMetadata(  # type: ignore[call-arg]
@@ -290,6 +293,140 @@ def tmp_plugin(npe2pm_: TestPluginManager):
         yield plugin
 
 
+@pytest.fixture
+def viewer_model():
+    from napari.components import ViewerModel
+
+    return ViewerModel()
+
+
+@pytest.fixture
+def qt_viewer_(qtbot, viewer_model, monkeypatch):
+    from napari._qt.qt_viewer import QtViewer
+
+    viewer = QtViewer(viewer_model)
+
+    original_controls = viewer.__class__.controls.fget
+    original_layers = viewer.__class__.layers.fget
+    original_layer_buttons = viewer.__class__.layerButtons.fget
+    original_viewer_buttons = viewer.__class__.viewerButtons.fget
+    original_dock_layer_list = viewer.__class__.dockLayerList.fget
+    original_dock_layer_controls = viewer.__class__.dockLayerControls.fget
+    original_dock_console = viewer.__class__.dockConsole.fget
+    original_dock_performance = viewer.__class__.dockPerformance.fget
+
+    def hide_widget(widget):
+        widget.hide()
+
+    def hide_and_clear_qt_viewer(viewer: QtViewer):
+        viewer._instances.clear()
+        viewer.hide()
+
+    def patched_controls(self):
+        if self._controls is None:
+            self._controls = original_controls(self)
+            qtbot.addWidget(self._controls, before_close_func=hide_widget)
+        return self._controls
+
+    def patched_layers(self):
+        if self._layers is None:
+            self._layers = original_layers(self)
+            qtbot.addWidget(self._layers, before_close_func=hide_widget)
+        return self._layers
+
+    def patched_layer_buttons(self):
+        if self._layersButtons is None:
+            self._layersButtons = original_layer_buttons(self)
+            qtbot.addWidget(self._layersButtons, before_close_func=hide_widget)
+        return self._layersButtons
+
+    def patched_viewer_buttons(self):
+        if self._viewerButtons is None:
+            self._viewerButtons = original_viewer_buttons(self)
+            qtbot.addWidget(self._viewerButtons, before_close_func=hide_widget)
+        return self._viewerButtons
+
+    def patched_dock_layer_list(self):
+        if self._dockLayerList is None:
+            self._dockLayerList = original_dock_layer_list(self)
+            qtbot.addWidget(self._dockLayerList, before_close_func=hide_widget)
+        return self._dockLayerList
+
+    def patched_dock_layer_controls(self):
+        if self._dockLayerControls is None:
+            self._dockLayerControls = original_dock_layer_controls(self)
+            qtbot.addWidget(
+                self._dockLayerControls, before_close_func=hide_widget
+            )
+        return self._dockLayerControls
+
+    def patched_dock_console(self):
+        if self._dockConsole is None:
+            self._dockConsole = original_dock_console(self)
+            qtbot.addWidget(self._dockConsole, before_close_func=hide_widget)
+        return self._dockConsole
+
+    def patched_dock_performance(self):
+        if self._dockPerformance is None:
+            self._dockPerformance = original_dock_performance(self)
+            qtbot.addWidget(
+                self._dockPerformance, before_close_func=hide_widget
+            )
+        return self._dockPerformance
+
+    monkeypatch.setattr(
+        viewer.__class__, 'controls', property(patched_controls)
+    )
+    monkeypatch.setattr(viewer.__class__, 'layers', property(patched_layers))
+    monkeypatch.setattr(
+        viewer.__class__, 'layerButtons', property(patched_layer_buttons)
+    )
+    monkeypatch.setattr(
+        viewer.__class__, 'viewerButtons', property(patched_viewer_buttons)
+    )
+    monkeypatch.setattr(
+        viewer.__class__, 'dockLayerList', property(patched_dock_layer_list)
+    )
+    monkeypatch.setattr(
+        viewer.__class__,
+        'dockLayerControls',
+        property(patched_dock_layer_controls),
+    )
+    monkeypatch.setattr(
+        viewer.__class__, 'dockConsole', property(patched_dock_console)
+    )
+    monkeypatch.setattr(
+        viewer.__class__, 'dockPerformance', property(patched_dock_performance)
+    )
+
+    qtbot.addWidget(viewer, before_close_func=hide_and_clear_qt_viewer)
+    return viewer
+
+
+@pytest.fixture
+def qt_viewer(qt_viewer_):
+    """We created `qt_viewer_` fixture to allow modifying qt_viewer
+    if module-level-specific modifications are necessary.
+    For example, in `test_qt_viewer.py`.
+    """
+    return qt_viewer_
+
+
+@pytest.fixture(autouse=True)
+def _clear_cached_action_injection():
+    """Automatically clear cached property `Action.injected`.
+
+    Allows action manager actions to be injected using current provider/processors
+    and dependencies. See #7219 for details.
+    To be removed after ActionManager deprecation.
+    """
+    from napari.utils.action_manager import action_manager
+
+    for action in action_manager._actions.values():
+        if 'injected' in action.__dict__:
+            del action.__dict__['injected']
+
+
 def _event_check(instance):
     def _prepare_check(name, no_event_):
         def check(instance, no_event=no_event_):
@@ -389,7 +526,7 @@ def _disable_notification_dismiss_timer(monkeypatch):
         monkeypatch.setattr(NapariQtNotification, 'FADE_OUT_RATE', 0)
 
 
-@pytest.fixture()
+@pytest.fixture
 def single_threaded_executor():
     executor = ThreadPoolExecutor(max_workers=1)
     yield executor
@@ -423,26 +560,60 @@ def _get_calling_place(depth=1):  # pragma: no cover
     return result
 
 
-@pytest.fixture()
+@pytest.fixture
 def _dangling_qthreads(monkeypatch, qtbot, request):
     from qtpy.QtCore import QThread
 
     base_start = QThread.start
     thread_dict = WeakKeyDictionary()
+    base_constructor = QThread.__init__
+
+    def run_with_trace(self):  # pragma: no cover
+        """
+        QThread.run but adding execution to sys.settrace when measuring coverage.
+
+        See https://github.com/nedbat/coveragepy/issues/686#issuecomment-634932753
+        and `init_with_trace`. When running QThreads during testing, we monkeypatch
+        the QThread constructor and run methods with traceable equivalents.
+        """
+        if 'coverage' in sys.modules:
+            # https://github.com/nedbat/coveragepy/issues/686#issuecomment-634932753
+            sys.settrace(threading._trace_hook)
+        self._base_run()
+
+    def init_with_trace(self, *args, **kwargs):
+        """Constructor for QThread adding tracing for coverage measurements.
+
+        Functions running in QThreads don't get measured by coverage.py, see
+        https://github.com/nedbat/coveragepy/issues/686. Therefore, we will
+        monkeypatch the constructor to add to the thread to `sys.settrace` when
+        we call `run` and `coverage` is in `sys.modules`.
+        """
+        base_constructor(self, *args, **kwargs)
+        self._base_run = self.run
+        self.run = partial(run_with_trace, self)
+
     # dict of threads that have been started but not yet terminated
 
     if 'disable_qthread_start' in request.keywords:
 
-        def my_start(self, priority=QThread.InheritPriority):
-            """dummy function to prevent thread start"""
+        def start_with_save_reference(self, priority=QThread.InheritPriority):
+            """Dummy function to prevent thread starts."""
 
     else:
 
-        def my_start(self, priority=QThread.InheritPriority):
+        def start_with_save_reference(self, priority=QThread.InheritPriority):
+            """Thread start function with logs to detect hanging threads.
+
+            Saves a weak reference to the thread and detects hanging threads,
+            as well as where the threads were started.
+            """
             thread_dict[self] = _get_calling_place()
             base_start(self, priority)
 
-    monkeypatch.setattr(QThread, 'start', my_start)
+    monkeypatch.setattr(QThread, 'start', start_with_save_reference)
+    monkeypatch.setattr(QThread, '__init__', init_with_trace)
+
     yield
 
     dangling_threads_li = []
@@ -483,7 +654,7 @@ def my_start(self, priority=QThread.InheritPriority):
     )
 
 
-@pytest.fixture()
+@pytest.fixture
 def _dangling_qthread_pool(monkeypatch, request):
     from qtpy.QtCore import QThreadPool
 
@@ -539,7 +710,7 @@ def my_start(self, runnable, priority=0):
     )
 
 
-@pytest.fixture()
+@pytest.fixture
 def _dangling_qtimers(monkeypatch, request):
     from qtpy.QtCore import QTimer
 
@@ -645,7 +816,7 @@ def _flush_mock(self):
     """There are no waiting events."""
 
 
-@pytest.fixture()
+@pytest.fixture
 def _disable_throttling(monkeypatch):
     """Disable qthrottler from superqt.
 
@@ -662,7 +833,7 @@ def _disable_throttling(monkeypatch):
     )
 
 
-@pytest.fixture()
+@pytest.fixture
 def _dangling_qanimations(monkeypatch, request):
     from qtpy.QtCore import QPropertyAnimation
 
@@ -708,15 +879,141 @@ def my_start(self):
     )
 
 
+with contextlib.suppress(ImportError):
+    # in headless test suite we don't have Qt bindings
+    # So we cannot inherit from QtBot and declare the fixture
+
+    from pytestqt.qtbot import QtBot
+
+    class QtBotWithOnCloseRenaming(QtBot):
+        """Modified QtBot that renames widgets when closing them in tests.
+
+        After a test ends that uses QtBot, all instantiated widgets added to
+        the bot have their name changed to 'handled_widget'. This allows us to
+        detect leaking widgets at the end of a test run, and avoid the
+        segmentation faults that often result from such leaks. [1]_
+
+        See Also
+        --------
+        `_find_dangling_widgets`: fixture that finds all widgets that have not
+        been renamed to 'handled_widget'.
+
+        References
+        ----------
+        .. [1] https://czaki.github.io/blog/2024/09/16/preventing-segfaults-in-test-suite-that-has-qt-tests/
+        """
+
+        def addWidget(self, widget, *, before_close_func=None):
+            if widget.objectName() == '':
+                # object does not have a name, so we can set it
+                widget.setObjectName('handled_widget')
+                before_close_func_ = before_close_func
+            elif before_close_func is None:
+                # there is no custom teardown function,
+                # so we provide one that will set object name
+
+                def before_close_func_(w):
+                    w.setObjectName('handled_widget')
+            else:
+                # user provided custom teardown function,
+                # so we need to wrap it to set object name
+
+                def before_close_func_(w):
+                    before_close_func(w)
+                    w.setObjectName('handled_widget')
+
+            super().addWidget(widget, before_close_func=before_close_func_)
+
+    @pytest.fixture
+    def qtbot(qapp, request):  # pragma: no cover
+        """Fixture to create a QtBotWithOnCloseRenaming instance for testing.
+
+        Make sure to call addWidget for each top-level widget you create to
+        ensure that they are properly closed after the test ends.
+
+        The `qapp` fixture is used to ensure that the QApplication is created
+        before, so we need it, even without using it directly in this fixture.
+        """
+        return QtBotWithOnCloseRenaming(request)
+
+
+@pytest.fixture
+def _find_dangling_widgets(request, qtbot):
+    yield
+
+    from qtpy.QtWidgets import QApplication
+
+    from napari._qt.qt_main_window import _QtMainWindow
+
+    top_level_widgets = QApplication.topLevelWidgets()
+
+    viewer_weak_set = getattr(request.node, '_viewer_weak_set', set())
+
+    problematic_widgets = []
+
+    for widget in top_level_widgets:
+        if widget.parent() is not None:
+            continue
+        if (
+            isinstance(widget, _QtMainWindow)
+            and widget._qt_viewer.viewer in viewer_weak_set
+        ):
+            continue
+
+        if widget.__class__.__module__.startswith('qtconsole'):
+            continue
+
+        if widget.objectName() == 'handled_widget':
+            continue
+
+        if widget.__class__.__name__ == 'CanvasBackendDesktop':
+            # TODO: we don't understand why this class leaks in
+            #  napari/_tests/test_sys_info.py, so we make an exception
+            #  here and we don't raise when this class leaks.
+            continue
+
+        problematic_widgets.append(widget)
+
+    if problematic_widgets:
+        text = '\n'.join(
+            f'Widget: {widget} of type {type(widget)} with name {widget.objectName()}'
+            for widget in problematic_widgets
+        )
+
+        for widget in problematic_widgets:
+            widget.setObjectName('handled_widget')
+
+        raise RuntimeError(f'Found dangling widgets:\n{text}')
+
+
 def pytest_runtest_setup(item):
+    """Add Qt leak detection fixtures *only* in tests using the qapp fixture.
+
+    Because we have headless test suite that does not include Qt, we cannot
+    simply use `@pytest.fixture(autouse=True)` on all our fixtures for
+    detecting leaking Qt objects.
+
+    Instead, here we detect whether the `qapp` fixture is being used, detecting
+    tests that use Qt and need to be checked for Qt objects leaks.
+
+    A note to maintainers: tests *may* attempt to use Qt classes but not use
+    the `qapp` fixture. This is BAD, and may cause Qt failures to be reported
+    far away from the problematic code or test. If you find any tests
+    instantiating Qt objects but not using qapp or qtbot, please submit a PR
+    adding the qtbot fixture and adding any top-level Qt widgets with::
+
+        qtbot.addWidget(widget_instance)
+
+    """
+
     if 'qapp' in item.fixturenames:
         # here we do autouse for dangling fixtures only if qapp is used
         if 'qtbot' not in item.fixturenames:
             # for proper waiting for threads to finish
             item.fixturenames.append('qtbot')
-
         item.fixturenames.extend(
             [
+                '_find_dangling_widgets',
                 '_dangling_qthread_pool',
                 '_dangling_qanimations',
                 '_dangling_qthreads',
diff --git a/napari/experimental/__init__.py b/napari/experimental/__init__.py
index cdbe8b174c2..3084b1832ff 100644
--- a/napari/experimental/__init__.py
+++ b/napari/experimental/__init__.py
@@ -5,7 +5,7 @@
 )
 
 __all__ = [
-    'link_layers',
     'layers_linked',
+    'link_layers',
     'unlink_layers',
 ]
diff --git a/napari/layers/__init__.py b/napari/layers/__init__.py
index 0e3978cd43a..ebb6d8129e3 100644
--- a/napari/layers/__init__.py
+++ b/napari/layers/__init__.py
@@ -17,7 +17,7 @@
 from napari.layers.vectors import Vectors
 from napari.utils.misc import all_subclasses as _all_subcls
 
-# isabstact check is to exclude _ImageBase class
+# isabstract check is to exclude _ImageBase class
 NAMES: set[str] = {
     subclass.__name__.lower()
     for subclass in _all_subcls(Layer)
@@ -25,6 +25,7 @@
 }
 
 __all__ = [
+    'NAMES',
     'Image',
     'Labels',
     'Layer',
@@ -33,5 +34,4 @@
     'Surface',
     'Tracks',
     'Vectors',
-    'NAMES',
 ]
diff --git a/napari/layers/_layer_actions.py b/napari/layers/_layer_actions.py
index 3016e9fde8c..5c6df6c42eb 100644
--- a/napari/layers/_layer_actions.py
+++ b/napari/layers/_layer_actions.py
@@ -5,11 +5,13 @@
 
 from __future__ import annotations
 
+import warnings
 from typing import TYPE_CHECKING, cast
 
 import numpy as np
 import numpy.typing as npt
 
+from napari import layers
 from napari.layers import Image, Labels, Layer
 from napari.layers._source import layer_source
 from napari.layers.utils import stack_utils
@@ -53,7 +55,6 @@ def _convert(ll: LayerList, type_: str) -> None:
 
     for lay in list(ll.selection):
         idx = ll.index(lay)
-
         if isinstance(lay, Shapes) and type_ == 'labels':
             data = lay.to_labels()
             idx += 1
@@ -66,7 +67,25 @@ def _convert(ll: LayerList, type_: str) -> None:
             data = lay.data
             # int image layer to labels is fully reversible
             ll.pop(idx)
-        new_layer = Layer.create(data, lay._get_base_state(), type_)
+        # projection mode may not be compatible with new type,
+        # we're ok with dropping it in that case
+        layer_type = getattr(layers, type_.title())
+        state = lay._get_base_state()
+        try:
+            layer_type._projectionclass(state['projection_mode'].value)
+        except ValueError:
+            state['projection_mode'] = 'none'
+            warnings.warn(
+                trans._(
+                    'projection mode "{mode}" is not compatible with {type_} layers. Falling back to "none".',
+                    mode=state['projection_mode'],
+                    type_=type_.title(),
+                    deferred=True,
+                ),
+                category=UserWarning,
+                stacklevel=1,
+            )
+        new_layer = Layer.create(data, state, type_)
         ll.insert(idx, new_layer)
 
 
@@ -86,7 +105,6 @@ def _convert_to_image(ll: LayerList) -> None:
 def _merge_stack(ll: LayerList, rgb: bool = False) -> None:
     # force selection to follow LayerList ordering
     imgs = cast(list[Image], [layer for layer in ll if layer in ll.selection])
-    assert all(isinstance(layer, Image) for layer in imgs)
     merged = (
         stack_utils.merge_rgb(imgs)
         if rgb
diff --git a/napari/layers/_scalar_field/scalar_field.py b/napari/layers/_scalar_field/scalar_field.py
index 4ae81c93019..1f0e08431fc 100644
--- a/napari/layers/_scalar_field/scalar_field.py
+++ b/napari/layers/_scalar_field/scalar_field.py
@@ -1,10 +1,10 @@
 from __future__ import annotations
 
 import types
-from abc import ABC
+from abc import ABC, abstractmethod
 from collections.abc import Sequence
 from contextlib import nullcontext
-from typing import TYPE_CHECKING, Union
+from typing import TYPE_CHECKING, Optional, Union, cast
 
 import numpy as np
 from numpy import typing as npt
@@ -26,6 +26,7 @@
 from napari.utils.events import Event
 from napari.utils.events.event import WarningEmitter
 from napari.utils.events.event_utils import connect_no_arg
+from napari.utils.geometry import clamp_point_to_bounding_box
 from napari.utils.naming import magic_name
 from napari.utils.translations import trans
 
@@ -371,7 +372,7 @@ def data_level(self, level: int) -> None:
         if self._data_level == level:
             return
         self._data_level = level
-        self.refresh()
+        self.refresh(extent=False)
 
     def _get_level_shapes(self):
         data = self.data
@@ -458,6 +459,7 @@ def custom_interpolation_kernel_2d(self, value):
         self._custom_interpolation_kernel_2d = np.array(value, np.float32)
         self.events.custom_interpolation_kernel_2d()
 
+    @abstractmethod
     def _raw_to_displayed(self, raw: np.ndarray) -> np.ndarray:
         """Determine displayed image from raw image.
 
@@ -588,6 +590,106 @@ def _get_value(self, position):
 
         return value
 
+    def _get_value_ray(
+        self,
+        start_point: Optional[np.ndarray],
+        end_point: Optional[np.ndarray],
+        dims_displayed: list[int],
+    ) -> Optional[int]:
+        """Get the first non-background value encountered along a ray.
+
+        Parameters
+        ----------
+        start_point : np.ndarray
+            (n,) array containing the start point of the ray in data coordinates.
+        end_point : np.ndarray
+            (n,) array containing the end point of the ray in data coordinates.
+        dims_displayed : List[int]
+            The indices of the dimensions currently displayed in the viewer.
+
+        Returns
+        -------
+        value : Optional[int]
+            The first non-background value encountered along the ray. If none
+            was encountered or the viewer is in 2D mode, returns None.
+        """
+        if start_point is None or end_point is None:
+            return None
+        if len(dims_displayed) == 3:
+            # only use get_value_ray on 3D for now
+            # we use dims_displayed because the image slice
+            # has its dimensions  in th same order as the vispy
+            # Volume
+            # Account for downsampling in the case of multiscale
+            # -1 means lowest resolution here.
+            start_point = (
+                start_point[dims_displayed]
+                / self.downsample_factors[-1][dims_displayed]
+            )
+            end_point = (
+                end_point[dims_displayed]
+                / self.downsample_factors[-1][dims_displayed]
+            )
+            start_point = cast(np.ndarray, start_point)
+            end_point = cast(np.ndarray, end_point)
+            sample_ray = end_point - start_point
+            length_sample_vector = np.linalg.norm(sample_ray)
+            n_points = int(2 * length_sample_vector)
+            sample_points = np.linspace(
+                start_point, end_point, n_points, endpoint=True
+            )
+            im_slice = self._slice.image.raw
+            # ensure the bounding box is for the proper multiscale level
+            bounding_box = self._display_bounding_box_at_level(
+                dims_displayed, self.data_level
+            )
+            # the display bounding box is returned as a closed interval
+            # (i.e. the endpoint is included) by the method, but we need
+            # open intervals in the code that follows, so we add 1.
+            bounding_box[:, 1] += 1
+
+            clamped = clamp_point_to_bounding_box(
+                sample_points,
+                bounding_box,
+            ).astype(int)
+            values = im_slice[tuple(clamped.T)]
+            return self._calculate_value_from_ray(values)
+
+        return None
+
+    @abstractmethod
+    def _calculate_value_from_ray(self, values):
+        raise NotImplementedError
+
+    def _get_value_3d(
+        self,
+        start_point: Optional[np.ndarray],
+        end_point: Optional[np.ndarray],
+        dims_displayed: list[int],
+    ) -> Optional[int]:
+        """Get the first non-background value encountered along a ray.
+
+        Parameters
+        ----------
+        start_point : np.ndarray
+            (n,) array containing the start point of the ray in data coordinates.
+        end_point : np.ndarray
+            (n,) array containing the end point of the ray in data coordinates.
+        dims_displayed : List[int]
+            The indices of the dimensions currently displayed in the viewer.
+
+        Returns
+        -------
+        value : int
+            The first non-zero value encountered along the ray. If a
+            non-zero value is not encountered, returns None.
+        """
+        return self._get_value_ray(
+            start_point=start_point,
+            end_point=end_point,
+            dims_displayed=dims_displayed,
+        )
+
     def _get_offset_data_position(self, position: npt.NDArray) -> npt.NDArray:
         """Adjust position for offset between viewer and data coordinates.
 
diff --git a/napari/layers/_source.py b/napari/layers/_source.py
index f15ce9e693a..4dc822aeaf3 100644
--- a/napari/layers/_source.py
+++ b/napari/layers/_source.py
@@ -58,7 +58,9 @@ def __deepcopy__(self, memo: Any) -> Self:
 
 # layer source context management
 
-_LAYER_SOURCE: ContextVar[dict] = ContextVar('_LAYER_SOURCE', default={})
+_LAYER_SOURCE: ContextVar[dict | None] = ContextVar(
+    '_LAYER_SOURCE', default=None
+)
 
 
 @contextmanager
@@ -97,7 +99,7 @@ def layer_source(**source_kwargs: Any) -> Generator[None, None, None]:
     >>> assert points.source == Source(path='file.ext', reader_plugin='plugin')  # doctest: +SKIP
 
     """
-    token = _LAYER_SOURCE.set({**_LAYER_SOURCE.get(), **source_kwargs})
+    token = _LAYER_SOURCE.set({**(_LAYER_SOURCE.get() or {}), **source_kwargs})
     try:
         yield
     finally:
@@ -109,4 +111,4 @@ def current_source() -> Source:
 
     The main place this function is used is in :meth:`Layer.__init__`.
     """
-    return Source(**_LAYER_SOURCE.get())
+    return Source(**(_LAYER_SOURCE.get() or {}))
diff --git a/napari/layers/_tests/test_dask_layers.py b/napari/layers/_tests/test_dask_layers.py
index 02d114bb815..8694696381a 100644
--- a/napari/layers/_tests/test_dask_layers.py
+++ b/napari/layers/_tests/test_dask_layers.py
@@ -53,7 +53,7 @@ def mock_set_view_slice():
     layer._set_view_slice = mock_set_view_slice
     layer.set_view_slice()
 
-    # adding a dask array will reate cache and turn off task fusion,
+    # adding a dask array will create cache and turn off task fusion,
     # *but only* during slicing (see "mock_set_view_slice" above)
     assert _dask_utils._DASK_CACHE.cache.available_bytes > 100
     assert not _dask_utils._DASK_CACHE.active
@@ -83,7 +83,7 @@ def test_list_of_dask_arrays_doesnt_create_cache():
     assert dask.config.get('optimization.fuse.active', None) == original
 
 
-@pytest.fixture()
+@pytest.fixture
 def delayed_dask_stack():
     """A 4D (20, 10, 10, 10) delayed dask array, simulates disk io."""
     # we will return a dict with a 'calls' variable that tracks call count
diff --git a/napari/layers/_tests/test_layer_actions.py b/napari/layers/_tests/test_layer_actions.py
index 2986263366f..a328114c368 100644
--- a/napari/layers/_tests/test_layer_actions.py
+++ b/napari/layers/_tests/test_layer_actions.py
@@ -39,7 +39,7 @@ def test_split_stack():
 
 def test_split_rgb():
     layer_list = LayerList()
-    layer_list.append(Image(np.random.random((8, 8, 3))))
+    layer_list.append(Image(np.random.random((48, 48, 3))))
     assert len(layer_list) == 1
     assert layer_list[0].rgb is True
 
@@ -48,7 +48,7 @@ def test_split_rgb():
     assert len(layer_list) == 3
 
     for idx in range(3):
-        assert layer_list[idx].data.shape == (8, 8)
+        assert layer_list[idx].data.shape == (48, 48)
 
 
 def test_merge_stack():
@@ -64,6 +64,30 @@ def test_merge_stack():
     assert layer_list[0].data.shape == (2, 8, 8)
 
 
+def test_merge_stack_rgb():
+    layer_list = LayerList()
+    layer_list.append(Image(np.random.rand(8, 8)))
+    layer_list.append(Image(np.random.rand(8, 8)))
+    layer_list.append(Image(np.random.rand(8, 8)))
+    assert len(layer_list) == 3
+
+    layer_list.selection.active = layer_list[0]
+    layer_list.selection.add(layer_list[1])
+    layer_list.selection.add(layer_list[2])
+
+    # check that without R G B colormaps we warn
+    with pytest.raises(ValueError, match='Missing colormap'):
+        _merge_stack(layer_list, rgb=True)
+
+    layer_list[0].colormap = 'red'
+    layer_list[1].colormap = 'green'
+    layer_list[2].colormap = 'blue'
+    _merge_stack(layer_list, rgb=True)
+    assert len(layer_list) == 1
+    assert layer_list[0].data.shape == (8, 8, 3)
+    assert layer_list[0].rgb is True
+
+
 def test_toggle_visibility():
     """Test toggling visibility of a layer."""
     layer_list = LayerList()
@@ -288,6 +312,21 @@ def test_convert_layer(layer, type_):
         )  # check array data not copied unnecessarily
 
 
+def test_convert_warns_with_projecton_mode():
+    # inplace
+    ll = LayerList(
+        [Image(np.random.rand(10, 10).astype(int), projection_mode='mean')]
+    )
+    with pytest.warns(UserWarning, match='projection mode'):
+        _convert(ll, 'labels')
+    assert isinstance(ll['Image'], Labels)
+    # not inplace
+    ll = LayerList([Image(np.random.rand(10, 10), projection_mode='mean')])
+    with pytest.warns(UserWarning, match='projection mode'):
+        _convert(ll, 'labels')
+    assert isinstance(ll['Image [1]'], Labels)
+
+
 def make_three_layer_layerlist():
     layer_list = LayerList()
     layer_list.append(Points([[0, 0]], name='test'))
diff --git a/napari/layers/base/__init__.py b/napari/layers/base/__init__.py
index edef36e7d66..a9d782e3eac 100644
--- a/napari/layers/base/__init__.py
+++ b/napari/layers/base/__init__.py
@@ -1,4 +1,4 @@
 from napari.layers.base._base_constants import ActionType
 from napari.layers.base.base import Layer, no_op
 
-__all__ = ['Layer', 'no_op', 'ActionType']
+__all__ = ['ActionType', 'Layer', 'no_op']
diff --git a/napari/layers/base/_base_mouse_bindings.py b/napari/layers/base/_base_mouse_bindings.py
index 3c910433f41..3bc15729763 100644
--- a/napari/layers/base/_base_mouse_bindings.py
+++ b/napari/layers/base/_base_mouse_bindings.py
@@ -28,12 +28,11 @@ def highlight_box_handles(layer: 'Layer', event: Event) -> None:
     # we work in data space so we're axis aligned which simplifies calculation
     # same as Layer.world_to_data
     world_to_data = (
-        layer._transforms[1:].set_slice(event.dims_displayed).inverse
+        layer._transforms[1:].set_slice(layer._slice_input.displayed).inverse
     )
     pos = np.array(world_to_data(event.position))[event.dims_displayed]
-
     handle_coords = generate_transform_box_from_layer(
-        layer, event.dims_displayed
+        layer, layer._slice_input.displayed
     )
     # TODO: dynamically set tolerance based on canvas size so it's not hard to pick small layer
     nearby_handle = get_nearby_handle(pos, handle_coords)
@@ -51,7 +50,9 @@ def _translate_with_box(
 ) -> None:
     offset = mouse_pos - initial_mouse_pos
     new_affine = Affine(translate=offset).compose(initial_affine)
-    layer.affine = layer.affine.replace_slice(event.dims_displayed, new_affine)
+    layer.affine = layer.affine.replace_slice(
+        layer._slice_input.displayed, new_affine
+    )
 
 
 def _rotate_with_box(
@@ -89,7 +90,9 @@ def _rotate_with_box(
         .compose(Affine(translate=-initial_center))
         .compose(initial_affine)
     )
-    layer.affine = layer.affine.replace_slice(event.dims_displayed, new_affine)
+    layer.affine = layer.affine.replace_slice(
+        layer._slice_input.displayed, new_affine
+    )
 
 
 def _scale_with_box(
@@ -163,7 +166,9 @@ def _scale_with_box(
         # compose with the original affine
         .compose(initial_affine)
     )
-    layer.affine = layer.affine.replace_slice(event.dims_displayed, new_affine)
+    layer.affine = layer.affine.replace_slice(
+        layer._slice_input.displayed, new_affine
+    )
 
 
 def transform_with_box(
@@ -178,13 +183,13 @@ def transform_with_box(
     # we work in data space so we're axis aligned which simplifies calculation
     # same as Layer.data_to_world
     simplified = layer._transforms[1:].simplified
-    initial_data_to_world = simplified.set_slice(event.dims_displayed)
+    initial_data_to_world = simplified.set_slice(layer._slice_input.displayed)
     initial_world_to_data = initial_data_to_world.inverse
     initial_mouse_pos = np.array(event.position)[event.dims_displayed]
     initial_mouse_pos_data = initial_world_to_data(initial_mouse_pos)
 
     initial_handle_coords_data = generate_transform_box_from_layer(
-        layer, event.dims_displayed
+        layer, layer._slice_input.displayed
     )
     nearby_handle = get_nearby_handle(
         initial_mouse_pos_data, initial_handle_coords_data
@@ -198,11 +203,11 @@ def transform_with_box(
     initial_handle_coords = initial_data_to_world(initial_handle_coords_data)
 
     # initial layer transform so we can calculate changes later
-    initial_affine = layer.affine.set_slice(event.dims_displayed)
+    initial_affine = layer.affine.set_slice(layer._slice_input.displayed)
 
     # needed for rescaling
     initial_data2physical = layer._transforms['data2physical'].set_slice(
-        event.dims_displayed
+        layer._slice_input.displayed
     )
 
     # needed for resize and rotate
diff --git a/napari/layers/base/_tests/test_base.py b/napari/layers/base/_tests/test_base.py
index 96c2b029896..4cac78835bd 100644
--- a/napari/layers/base/_tests/test_base.py
+++ b/napari/layers/base/_tests/test_base.py
@@ -104,3 +104,44 @@ def test_axis_labels_error():
 
     with pytest.raises(ValueError, match='must have length ndim'):
         SampleLayer(np.empty((10, 10)), axis_labels=('x', 'y', 'z'))
+
+
+def test_non_visible_mode():
+    layer = SampleLayer(np.empty((10, 10)))
+    layer.mode = 'transform'
+
+    # change layer visibility and check the layer mode gets updated
+    layer.visible = False
+    assert layer.mode == 'pan_zoom'
+    layer.visible = True
+    assert layer.mode == 'transform'
+
+
+def test_world_to_displayed_data_normal_3D():
+    layer = SampleLayer(np.empty((10, 10, 10)))
+    layer.scale = (1, 3, 2)
+
+    normal_vector = [0, 1, 1]
+
+    expected_transformed_vector = [0, 3 * (13**0.5) / 13, 2 * (13**0.5) / 13]
+
+    transformed_vector = layer._world_to_displayed_data_normal(
+        normal_vector, dims_displayed=[0, 1, 2]
+    )
+
+    assert np.allclose(transformed_vector, expected_transformed_vector)
+
+
+def test_world_to_displayed_data_normal_4D():
+    layer = SampleLayer(np.empty((10, 10, 10, 10)))
+    layer.scale = (1, 3, 2, 1)
+
+    normal_vector = [0, 1, 1]
+
+    expected_transformed_vector = [0, 3 * (13**0.5) / 13, 2 * (13**0.5) / 13]
+
+    transformed_vector = layer._world_to_displayed_data_normal(
+        normal_vector, dims_displayed=[0, 1, 2]
+    )
+
+    assert np.allclose(transformed_vector, expected_transformed_vector)
diff --git a/napari/layers/base/_tests/test_mouse_bindings.py b/napari/layers/base/_tests/test_mouse_bindings.py
index 236b4daca14..c7ab16159bd 100644
--- a/napari/layers/base/_tests/test_mouse_bindings.py
+++ b/napari/layers/base/_tests/test_mouse_bindings.py
@@ -1,13 +1,41 @@
 from unittest.mock import Mock
 
 import numpy as np
+import pytest
 
-from napari.layers.base._base_mouse_bindings import _rotate_with_box
+from napari.layers.base._base_mouse_bindings import (
+    _rotate_with_box,
+    _translate_with_box,
+)
 from napari.utils.transforms import Affine
 
 
-def test_interaction_box_rotation():
+@pytest.mark.parametrize('dims_displayed', [[0, 1], [1, 2]])
+def test_interaction_box_translation(dims_displayed):
     layer = Mock(affine=Affine())
+    layer._slice_input.displayed = [0, 1]
+    initial_affine = Affine()
+    initial_mouse_pos = np.asarray([3, 3], dtype=np.float32)
+    mouse_pos = np.asarray([6, 5], dtype=np.float32)
+    event = Mock(dims_displayed=dims_displayed, modifiers=[None])
+    _translate_with_box(
+        layer,
+        initial_affine,
+        initial_mouse_pos,
+        mouse_pos,
+        event,
+    )
+    # translate should be equal to [3, 2] from doing [6, 5] - [3, 3]
+    assert np.array_equal(
+        layer.affine.translate,
+        Affine(translate=np.asarray([3, 2], dtype=np.float32)).translate,
+    )
+
+
+@pytest.mark.parametrize('dims_displayed', [[0, 1], [1, 2]])
+def test_interaction_box_rotation(dims_displayed):
+    layer = Mock(affine=Affine())
+    layer._slice_input.displayed = [0, 1]
     initial_affine = Affine()
     initial_mouse_pos = Mock()
     # rotation handle is 8th
@@ -27,7 +55,7 @@ def test_interaction_box_rotation():
     )
     initial_center = np.asarray([3, 3], dtype=np.float32)
     mouse_pos = np.asarray([6, 5], dtype=np.float32)
-    event = Mock(dims_displayed=[0, 1], modifiers=[None])
+    event = Mock(dims_displayed=dims_displayed, modifiers=[None])
     _rotate_with_box(
         layer,
         initial_affine,
@@ -37,12 +65,14 @@ def test_interaction_box_rotation():
         mouse_pos,
         event,
     )
-    # should be ~33 degrees
+    # should be approximately 33 degrees
     assert np.allclose(layer.affine.rotate, Affine(rotate=33.69).rotate)
 
 
-def test_interaction_box_fixed_rotation():
+@pytest.mark.parametrize('dims_displayed', [[0, 1], [1, 2]])
+def test_interaction_box_fixed_rotation(dims_displayed):
     layer = Mock(affine=Affine())
+    layer._slice_input.displayed = [0, 1]
     initial_affine = Affine()
     initial_mouse_pos = Mock()
     # rotation handle is 8th
@@ -74,6 +104,4 @@ def test_interaction_box_fixed_rotation():
         event,
     )
     # should be 45 degrees
-    assert np.allclose(
-        layer.affine.rotate, Affine(rotate=45).rotate
-    )  # now lets use shift to fix
+    assert np.allclose(layer.affine.rotate, Affine(rotate=45).rotate)
diff --git a/napari/layers/base/base.py b/napari/layers/base/base.py
index 532541f5313..5542c6d49c5 100644
--- a/napari/layers/base/base.py
+++ b/napari/layers/base/base.py
@@ -9,7 +9,7 @@
 import warnings
 from abc import ABC, ABCMeta, abstractmethod
 from collections import defaultdict
-from collections.abc import Generator, Hashable, Sequence
+from collections.abc import Generator, Hashable, Mapping, Sequence
 from contextlib import contextmanager
 from functools import cached_property
 from typing import (
@@ -361,6 +361,7 @@ def __init__(
         # Needs to be imported here to avoid circular import in _source
         from napari.layers._source import current_source
 
+        self._highlight_visible = True
         self._unique_id = None
         self._source = current_source()
         self.dask_optimized_slicing = configure_dask(data, cache)
@@ -368,6 +369,7 @@ def __init__(
         self._opacity = opacity
         self._blending = Blending(blending)
         self._visible = visible
+        self._visible_mode = None
         self._freeze = False
         self._status = 'Ready'
         self._help = ''
@@ -455,6 +457,7 @@ def __init__(
             source=self,
             axis_labels=Event,
             data=Event,
+            metadata=Event,
             affine=Event,
             blending=Event,
             cursor=Event,
@@ -543,7 +546,7 @@ def _mode_setter_helper(self, mode_in: Union[Mode, str]) -> StringEnum:
         TRANSFORM = self._modeclass.TRANSFORM  # type: ignore[attr-defined]
         assert mode is not None
 
-        if not self.editable:
+        if not self.editable or not self.visible:
             mode = PAN_ZOOM
         if mode == self._mode:
             return mode
@@ -589,6 +592,10 @@ def update_transform_box_visibility(self, visible):
                 self.mode == TRANSFORM and visible
             )
 
+    def update_highlight_visibility(self, visible):
+        self._highlight_visible = visible
+        self._set_highlight(force=True)
+
     @property
     def mode(self) -> str:
         """str: Interactive mode
@@ -625,7 +632,7 @@ def projection_mode(self, mode):
         if self._projection_mode != mode:
             self._projection_mode = mode
             self.events.projection_mode()
-            self.refresh()
+            self.refresh(extent=False)
 
     @property
     def unique_id(self) -> Hashable:
@@ -665,6 +672,7 @@ def metadata(self) -> dict:
     def metadata(self, value: dict) -> None:
         self._metadata.clear()
         self._metadata.update(value)
+        self.events.metadata()
 
     @property
     def source(self) -> Source:
@@ -768,9 +776,22 @@ def visible(self) -> bool:
     @visible.setter
     def visible(self, visible: bool) -> None:
         self._visible = visible
-        self.refresh()
+
+        if visible:
+            # needed because things might have changed while invisible
+            # and refresh is noop while invisible
+            self.refresh(extent=False)
+        self._on_visible_changed()
         self.events.visible()
 
+    def _on_visible_changed(self) -> None:
+        """Execute side-effects on this layer related to changes of the visible state."""
+        if self.visible and self._visible_mode:
+            self.mode = self._visible_mode
+        else:
+            self._visible_mode = self.mode
+            self.mode = self._modeclass.PAN_ZOOM  # type: ignore[attr-defined]
+
     @property
     def editable(self) -> bool:
         """bool: Whether the current layer data is editable from the viewer."""
@@ -827,7 +848,7 @@ def scale(self, scale: Optional[npt.NDArray]) -> None:
         if scale is None:
             scale = np.array([1] * self.ndim)
         self._transforms['data2physical'].scale = np.array(scale)
-        self._clear_extents_and_refresh()
+        self.refresh()
         self.events.scale()
 
     @property
@@ -838,7 +859,7 @@ def translate(self) -> npt.NDArray:
     @translate.setter
     def translate(self, translate: npt.ArrayLike) -> None:
         self._transforms['data2physical'].translate = np.array(translate)
-        self._clear_extents_and_refresh()
+        self.refresh()
         self.events.translate()
 
     @property
@@ -849,7 +870,7 @@ def rotate(self) -> npt.NDArray:
     @rotate.setter
     def rotate(self, rotate: npt.NDArray) -> None:
         self._transforms['data2physical'].rotate = rotate
-        self._clear_extents_and_refresh()
+        self.refresh()
         self.events.rotate()
 
     @property
@@ -860,7 +881,7 @@ def shear(self) -> npt.NDArray:
     @shear.setter
     def shear(self, shear: npt.NDArray) -> None:
         self._transforms['data2physical'].shear = shear
-        self._clear_extents_and_refresh()
+        self.refresh()
         self.events.shear()
 
     @property
@@ -876,7 +897,7 @@ def affine(self, affine: Union[npt.ArrayLike, Affine]) -> None:
         self._transforms[2] = coerce_affine(
             affine, ndim=self.ndim, name='physical2world'
         )
-        self._clear_extents_and_refresh()
+        self.refresh()
         self.events.affine()
 
     def _reset_affine(self) -> None:
@@ -910,7 +931,7 @@ def _update_dims(self) -> None:
 
         self._ndim = ndim
 
-        self._clear_extents_and_refresh()
+        self.refresh()
 
     @property
     @abstractmethod
@@ -1028,17 +1049,6 @@ def _clear_extent_augmented(self) -> None:
             del self._extent_augmented
         self.events._extent_augmented()
 
-    def _clear_extents_and_refresh(self) -> None:
-        """Clears the cached extents, emits events and refreshes the layer.
-
-        This should be called whenever this data or transform information
-        changes, and should be called before any other related events
-        are emitted so that they use the updated extent values.
-        """
-        self._clear_extent()
-        self._clear_extent_augmented()
-        self.refresh()
-
     @property
     def _data_slice(self) -> _ThickNDSlice:
         """Slice in data coordinates."""
@@ -1288,7 +1298,12 @@ def _slice_dims(
         slice_input = self._make_slice_input(dims)
         if force or (self._slice_input != slice_input):
             self._slice_input = slice_input
-            self._refresh_sync()
+            self._refresh_sync(
+                data_displayed=True,
+                thumbnail=True,
+                highlight=True,
+                extent=True,
+            )
 
     def _make_slice_input(
         self,
@@ -1515,7 +1530,16 @@ def _block_refresh(self):
         finally:
             self._refresh_blocked = previous
 
-    def refresh(self, event: Optional[Event] = None) -> None:
+    def refresh(
+        self,
+        event: Optional[Event] = None,
+        *,
+        thumbnail: bool = True,
+        data_displayed: bool = True,
+        highlight: bool = True,
+        extent: bool = True,
+        force: bool = False,
+    ) -> None:
         """Refresh all layer data based on current view slice."""
         if self._refresh_blocked:
             logger.debug('Layer.refresh blocked: %s', self)
@@ -1526,14 +1550,35 @@ def refresh(self, event: Optional[Event] = None) -> None:
             self.events.reload(layer=self)
         # Otherwise, slice immediately on the calling thread.
         else:
-            self._refresh_sync()
+            self._refresh_sync(
+                thumbnail=thumbnail,
+                data_displayed=data_displayed,
+                highlight=highlight,
+                extent=extent,
+                force=force,
+            )
 
-    def _refresh_sync(self, event: Optional[Event] = None) -> None:
+    def _refresh_sync(
+        self,
+        *,
+        thumbnail: bool = False,
+        data_displayed: bool = False,
+        highlight: bool = False,
+        extent: bool = False,
+        force: bool = False,
+    ) -> None:
         logger.debug('Layer._refresh_sync: %s', self)
-        if self.visible:
+        if not (self.visible or force):
+            return
+        if extent:
+            self._clear_extent()
+            self._clear_extent_augmented()
+        if data_displayed:
             self.set_view_slice()
             self.events.set_data()
+        if thumbnail:
             self._update_thumbnail()
+        if highlight:
             self._set_highlight(force=True)
 
     def world_to_data(self, position: npt.ArrayLike) -> npt.NDArray:
@@ -1658,6 +1703,47 @@ def _world_to_displayed_data_ray(
         vector_data_ndisplay /= np.linalg.norm(vector_data_ndisplay)
         return vector_data_ndisplay
 
+    def _world_to_displayed_data_normal(
+        self, vector_world: npt.ArrayLike, dims_displayed: list[int]
+    ) -> np.ndarray:
+        """Convert a normal vector defining an orientation from world coordinates to data coordinates.
+
+        Parameters
+        ----------
+        vector_world : tuple, list, 1D array
+            A vector in world coordinates.
+        dims_displayed : list[int]
+            Indices of displayed dimensions of the data.
+
+        Returns
+        -------
+        np.ndarray
+            Transformed normal vector (unit vector) in data coordinates.
+
+        Notes
+        -----
+        This method is adapted from napari-threedee under BSD-3-Clause License.
+        For more information see also:
+        https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/geometry/transforming-normals.html
+        """
+
+        # the napari transform is from layer -> world.
+        # We want the inverse of the world ->  layer, so we just take the napari transform
+        inverse_transform = self._transforms[1:].simplified.linear_matrix
+
+        # Extract the relevant submatrix based on dims_displayed
+        submatrix = inverse_transform[np.ix_(dims_displayed, dims_displayed)]
+        transpose_inverse_transform = submatrix.T
+
+        # transform the vector
+        transformed_vector = np.matmul(
+            transpose_inverse_transform, vector_world
+        )
+
+        transformed_vector /= np.linalg.norm(transformed_vector)
+
+        return transformed_vector
+
     def _world_to_layer_dims(
         self, *, world_dims: npt.NDArray, ndim_world: int
     ) -> np.ndarray:
@@ -2002,7 +2088,7 @@ def _update_draw(
             ):
                 self._data_level = level
                 self.corner_pixels = corners
-                self.refresh()
+                self.refresh(extent=False, thumbnail=False)
         else:
             # set the data_level so that it is the lowest resolution in 3d view
             if self.multiscale is True:
@@ -2209,7 +2295,7 @@ def __copy__(self):
     def create(
         cls,
         data: Any,
-        meta: Optional[dict] = None,
+        meta: Optional[Mapping] = None,
         layer_type: Optional[str] = None,
     ) -> Layer:
         """Create layer from `data` of type `layer_type`.
diff --git a/napari/layers/image/_image_key_bindings.py b/napari/layers/image/_image_key_bindings.py
index f5bc837ce61..2deec98516e 100644
--- a/napari/layers/image/_image_key_bindings.py
+++ b/napari/layers/image/_image_key_bindings.py
@@ -3,15 +3,17 @@
 from collections.abc import Generator
 from typing import Callable, Union
 
-from app_model.types import KeyCode
-
 import napari
 from napari.layers.base._base_constants import Mode
 from napari.layers.image.image import Image
 from napari.layers.utils.interactivity_utils import (
     orient_plane_normal_around_cursor,
 )
-from napari.layers.utils.layer_utils import register_layer_action
+from napari.layers.utils.layer_utils import (
+    register_layer_action,
+    register_layer_attr_action,
+)
+from napari.utils.action_manager import action_manager
 from napari.utils.events import Event
 from napari.utils.translations import trans
 
@@ -22,26 +24,32 @@ def register_image_action(
     return register_layer_action(Image, description, repeatable)
 
 
-@Image.bind_key(KeyCode.KeyZ, overwrite=True)
+def register_image_mode_action(
+    description: str,
+) -> Callable[[Callable], Callable]:
+    return register_layer_attr_action(Image, description, 'mode')
+
+
 @register_image_action(trans._('Orient plane normal along z-axis'))
 def orient_plane_normal_along_z(layer: Image) -> None:
     orient_plane_normal_around_cursor(layer, plane_normal=(1, 0, 0))
 
 
-@Image.bind_key(KeyCode.KeyY, overwrite=True)
-@register_image_action(trans._('orient plane normal along y-axis'))
+@register_image_action(trans._('Orient plane normal along y-axis'))
 def orient_plane_normal_along_y(layer: Image) -> None:
     orient_plane_normal_around_cursor(layer, plane_normal=(0, 1, 0))
 
 
-@Image.bind_key(KeyCode.KeyX, overwrite=True)
-@register_image_action(trans._('orient plane normal along x-axis'))
+@register_image_action(trans._('Orient plane normal along x-axis'))
 def orient_plane_normal_along_x(layer: Image) -> None:
     orient_plane_normal_around_cursor(layer, plane_normal=(0, 0, 1))
 
 
-@Image.bind_key(KeyCode.KeyO, overwrite=True)
-@register_image_action(trans._('orient plane normal along view direction'))
+@register_image_action(
+    trans._(
+        'Orient plane normal along view direction\nHold down to have plane follow camera'
+    )
+)
 def orient_plane_normal_along_view_direction(
     layer: Image,
 ) -> Union[None, Generator[None, None, None]]:
@@ -53,7 +61,7 @@ def sync_plane_normal_with_view_direction(
         event: Union[None, Event] = None,
     ) -> None:
         """Plane normal syncronisation mouse callback."""
-        layer.plane.normal = layer._world_to_displayed_data_ray(
+        layer.plane.normal = layer._world_to_displayed_data_normal(
             viewer.camera.view_direction, [-3, -2, -1]
         )
 
@@ -68,22 +76,33 @@ def sync_plane_normal_with_view_direction(
     return None
 
 
-@register_image_action(trans._('orient plane normal along view direction'))
+# The generator function above can't be bound to a button, so here
+# is a non-generator version of the function
 def orient_plane_normal_along_view_direction_no_gen(layer: Image) -> None:
     viewer = napari.viewer.current_viewer()
     if viewer is None or viewer.dims.ndisplay != 3:
         return
-    layer.plane.normal = layer._world_to_displayed_data_ray(
+    layer.plane.normal = layer._world_to_displayed_data_normal(
         viewer.camera.view_direction, [-3, -2, -1]
     )
 
 
-@register_image_action(trans._('Transform'))
+# register the non-generator without a keybinding
+# this way the generator version owns the keybinding
+action_manager.register_action(
+    name='napari:orient_plane_normal_along_view_direction_no_gen',
+    command=orient_plane_normal_along_view_direction_no_gen,
+    description=trans._('Orient plane normal along view direction button'),
+    keymapprovider=None,
+)
+
+
+@register_image_mode_action(trans._('Transform'))
 def activate_image_transform_mode(layer: Image) -> None:
     layer.mode = str(Mode.TRANSFORM)
 
 
-@register_image_action(trans._('Pan/zoom'))
+@register_image_mode_action(trans._('Pan/zoom'))
 def activate_image_pan_zoom_mode(layer: Image) -> None:
     layer.mode = str(Mode.PAN_ZOOM)
 
diff --git a/napari/layers/image/_image_utils.py b/napari/layers/image/_image_utils.py
index 0086358abb3..b7c833134a6 100644
--- a/napari/layers/image/_image_utils.py
+++ b/napari/layers/image/_image_utils.py
@@ -12,10 +12,11 @@
 from napari.utils.translations import trans
 
 
-def guess_rgb(shape: tuple[int, ...]) -> bool:
+def guess_rgb(shape: tuple[int, ...], min_side_len: int = 30) -> bool:
     """Guess if the passed shape comes from rgb data.
 
-    If last dim is 3 or 4 assume the data is rgb, including rgba.
+    If last dim is 3 or 4 and other dims are larger (>30), assume the data is
+    rgb, including rgba.
 
     Parameters
     ----------
@@ -29,8 +30,13 @@ def guess_rgb(shape: tuple[int, ...]) -> bool:
     """
     ndim = len(shape)
     last_dim = shape[-1]
+    viewed_dims = shape[-3:-1]
 
-    return ndim > 2 and last_dim in (3, 4)
+    return (
+        ndim > 2
+        and last_dim in (3, 4)
+        and all(d > min_side_len for d in viewed_dims)
+    )
 
 
 def guess_multiscale(
diff --git a/napari/layers/image/_tests/test_image.py b/napari/layers/image/_tests/test_image.py
index ff6207c9e56..d610064457b 100644
--- a/napari/layers/image/_tests/test_image.py
+++ b/napari/layers/image/_tests/test_image.py
@@ -145,7 +145,7 @@ def test_5D_image_shape_1():
 
 def test_rgb_image():
     """Test instantiating Image layer with RGB data."""
-    shape = (10, 15, 3)
+    shape = (40, 45, 3)
     np.random.seed(0)
     data = np.random.random(shape)
     layer = Image(data)
@@ -160,7 +160,7 @@ def test_rgb_image():
 
 def test_rgba_image():
     """Test instantiating Image layer with RGBA data."""
-    shape = (10, 15, 4)
+    shape = (40, 45, 4)
     np.random.seed(0)
     data = np.random.random(shape)
     layer = Image(data)
@@ -175,7 +175,7 @@ def test_rgba_image():
 
 def test_negative_rgba_image():
     """Test instantiating Image layer with negative RGBA data."""
-    shape = (10, 15, 4)
+    shape = (40, 45, 4)
     np.random.seed(0)
     # Data between -1.0 and 1.0
     data = 2 * np.random.random(shape) - 1
@@ -565,26 +565,45 @@ def test_value():
 
 
 @pytest.mark.parametrize(
-    ('position', 'view_direction', 'dims_displayed', 'world'),
+    (
+        'position',
+        'view_direction',
+        'dims_displayed',
+        'world',
+        'render_mode',
+        'result',
+    ),
     [
-        ((0, 0, 0), [1, 0, 0], [0, 1, 2], False),
-        ((0, 0, 0), [1, 0, 0], [0, 1, 2], True),
-        ((0, 0, 0, 0), [0, 1, 0, 0], [1, 2, 3], True),
+        ((0, 0, 0), [1, 0, 0], [0, 1, 2], False, 'mip', 0),
+        ((0, 0, 0), [1, 0, 0], [0, 1, 2], True, 'mip', 0),
+        ((2, 2, 2), [1, 0, 0], [0, 1, 2], False, 'mip', 1),
+        ((2, 2, 2), [1, 0, 0], [0, 1, 2], False, 'minip', 0),
+        ((2, 2, 2), [1, 0, 0], [0, 1, 2], False, 'average', 1 / 5),
+        ((2, 2, 2), [1, 0, 0], [0, 1, 2], False, 'translucent', 0),
+        # not quite as expected for additive
+        ((2, 2, 2), [1, 0, 0], [0, 1, 2], False, 'additive', 2),
+        ((2, 2, 2), [1, 0, 0], [0, 1, 2], False, 'iso', None),
+        ((2, 2, 2), [1, 0, 0], [0, 1, 2], False, 'attenuated_mip', 0),
+        ((0, 2, 2, 2), [0, 1, 0, 0], [1, 2, 3], False, 'mip', 1),
     ],
 )
-def test_value_3d(position, view_direction, dims_displayed, world):
-    """Currently get_value should return None in 3D"""
-    np.random.seed(0)
-    data = np.random.random((10, 15, 15))
-    layer = Image(data)
-    layer._slice_dims(Dims(ndim=3, ndisplay=3))
+def test_value_3d(
+    position, view_direction, dims_displayed, world, render_mode, result
+):
+    data = np.zeros((5, 5, 5, 5))
+    data[:, 2, 2, 2] = 1
+    layer = Image(data, rendering=render_mode)
+    layer._slice_dims(Dims(ndim=4, ndisplay=3))
     value = layer.get_value(
         position,
         view_direction=view_direction,
         dims_displayed=dims_displayed,
         world=world,
     )
-    assert value is None
+    if result is None:
+        assert value is None
+    else:
+        npt.assert_allclose(value, result)
 
 
 def test_message():
@@ -992,7 +1011,7 @@ def test_thick_slice_multiscale():
 
     layer.projection_mode = 'mean'
     # NOTE that here we rescale slicing to twice the non-multiscale test
-    # in order to get the same results, becase the actual full scale image
+    # in order to get the same results, because the actual full scale image
     # is doubled in size
     layer._slice_dims(
         Dims(
@@ -1031,6 +1050,12 @@ def test_thick_slice_multiscale():
     )
 
 
+def test_contrast_outside_range():
+    data = np.zeros((64, 64), dtype=np.uint8)
+
+    Image(data, contrast_limits=(0, 1000))
+
+
 def test_docstring():
     validate_all_params_in_docstring(Image)
     validate_kwargs_sorted(Image)
diff --git a/napari/layers/image/_tests/test_image_utils.py b/napari/layers/image/_tests/test_image_utils.py
index 0fa8955bf4d..f8d9e56e0a1 100644
--- a/napari/layers/image/_tests/test_image_utils.py
+++ b/napari/layers/image/_tests/test_image_utils.py
@@ -1,3 +1,4 @@
+import inspect
 import time
 
 import dask
@@ -19,22 +20,44 @@
 
 
 def test_guess_rgb():
-    shape = (10, 15)
+    sig = inspect.signature(guess_rgb)
+    min_side_len = sig.parameters['min_side_len'].default
+
+    shape = (10, 15)  # 2D only
+    assert not guess_rgb(shape)
+
+    shape = (40, 45, 6)  # final dim is too long
+    assert not guess_rgb(shape)
+
+    shape = (min_side_len - 1, min_side_len - 1, 3)  # 2D sides too small
     assert not guess_rgb(shape)
 
-    shape = (10, 15, 6)
+    shape = (min_side_len - 1, min_side_len + 1, 3)  # one 2D side too small
     assert not guess_rgb(shape)
 
-    shape = (10, 15, 3)
+    shape = (min_side_len + 1, min_side_len + 1, 3)
     assert guess_rgb(shape)
 
-    shape = (10, 15, 4)
+    shape = (512, 512, 3)
     assert guess_rgb(shape)
 
+    shape = (100, 100, 4)
+    assert guess_rgb(shape)
+
+    shape = (10, 10, 3)
+    assert guess_rgb(shape, min_side_len=5)
+
 
 @given(shape=array_shapes(min_dims=3, min_side=0))
 def test_guess_rgb_property(shape):
-    assert guess_rgb(shape) == (shape[-1] in (3, 4))
+    sig = inspect.signature(guess_rgb)
+    min_side_len = sig.parameters['min_side_len'].default
+
+    assert guess_rgb(shape) == (
+        shape[-1] in (3, 4)
+        and shape[-2] > min_side_len
+        and shape[-3] > min_side_len
+    )
 
 
 def test_guess_multiscale():
diff --git a/napari/layers/image/_tests/test_multiscale.py b/napari/layers/image/_tests/test_multiscale.py
index f96e7307eb2..3dab6842293 100644
--- a/napari/layers/image/_tests/test_multiscale.py
+++ b/napari/layers/image/_tests/test_multiscale.py
@@ -123,7 +123,7 @@ def test_non_uniform_3D_multiscale():
 
 def test_rgb_multiscale():
     """Test instantiating Image layer with RGB data."""
-    shapes = [(40, 20, 3), (20, 10, 3), (10, 5, 3)]
+    shapes = [(40, 32, 3), (20, 16, 3), (10, 8, 3)]
     np.random.seed(0)
     data = [np.random.random(s) for s in shapes]
     layer = Image(data, multiscale=True)
@@ -138,7 +138,7 @@ def test_rgb_multiscale():
 
 def test_3D_rgb_multiscale():
     """Test instantiating Image layer with 3D RGB data."""
-    shapes = [(8, 40, 20, 3), (4, 20, 10, 3), (2, 10, 5, 3)]
+    shapes = [(8, 40, 32, 3), (4, 20, 16, 3), (2, 10, 8, 3)]
     np.random.seed(0)
     data = [np.random.random(s) for s in shapes]
     layer = Image(data, multiscale=True)
@@ -153,7 +153,7 @@ def test_3D_rgb_multiscale():
 
 def test_non_rgb_image():
     """Test forcing Image layer to be 3D and not rgb."""
-    shapes = [(40, 20, 3), (20, 10, 3), (10, 5, 3)]
+    shapes = [(40, 32, 3), (20, 16, 3), (10, 8, 3)]
     np.random.seed(0)
     data = [np.random.random(s) for s in shapes]
     layer = Image(data, multiscale=True, rgb=False)
diff --git a/napari/layers/image/image.py b/napari/layers/image/image.py
index 96be6404ad4..dfd148e577d 100644
--- a/napari/layers/image/image.py
+++ b/napari/layers/image/image.py
@@ -2,6 +2,7 @@
 
 from __future__ import annotations
 
+import typing
 import warnings
 from typing import Any, Literal, Union, cast
 
@@ -263,15 +264,14 @@ def __init__(
     ):
         # Determine if rgb
         data_shape = data.shape if hasattr(data, 'shape') else data[0].shape
-        rgb_guess = guess_rgb(data_shape)
-        if rgb and not rgb_guess:
+        if rgb and not guess_rgb(data_shape, min_side_len=0):
             raise ValueError(
                 trans._(
                     "'rgb' was set to True but data does not have suitable dimensions."
                 )
             )
         if rgb is None:
-            rgb = rgb_guess
+            rgb = guess_rgb(data_shape)
 
         self.rgb = rgb
         super().__init__(
@@ -400,7 +400,9 @@ def _update_slice_response(self, response: _ImageSliceResponse) -> None:
         if self._keep_auto_contrast:
             data = response.image.raw
             input_data = data[-1] if self.multiscale else data
-            self.contrast_limits = calc_data_range(input_data, rgb=self.rgb)
+            self.contrast_limits = calc_data_range(
+                typing.cast(LayerDataProtocol, input_data), rgb=self.rgb
+            )
 
         super()._update_slice_response(response)
 
@@ -612,6 +614,9 @@ def _update_thumbnail(self):
                 image, zoom_factor, prefilter=False, order=0
             )
             low, high = self.contrast_limits
+            if np.issubdtype(downsampled.dtype, np.integer):
+                low = max(low, np.iinfo(downsampled.dtype).min)
+                high = min(high, np.iinfo(downsampled.dtype).max)
             downsampled = np.clip(downsampled, low, high)
             color_range = high - low
             if color_range != 0:
@@ -683,6 +688,55 @@ def contrast_limits(self, contrast_limits):
             prev = self._keep_auto_contrast
             self._keep_auto_contrast = False
             try:
-                self.refresh()
+                self.refresh(highlight=False, extent=False)
             finally:
                 self._keep_auto_contrast = prev
+
+    def _calculate_value_from_ray(self, values):
+        # translucent is special: just return the first value, no matter what
+        if self.rendering == ImageRendering.TRANSLUCENT:
+            return np.ravel(values)[0]
+        # iso is weird too: just return None always
+        if self.rendering == ImageRendering.ISO:
+            return None
+
+        # if the whole ray is NaN, we should see nothing, so return None
+        # this check saves us some warnings later as well, so better do it now
+        if np.all(np.isnan(values)):
+            return None
+
+        # "summary" renderings; they do not represent a specific pixel, so we just
+        # return the summary value. We should probably differentiate these somehow.
+        # these are also probably not the same as how the gpu does it...
+        if self.rendering == ImageRendering.AVERAGE:
+            return np.nanmean(values)
+        if self.rendering == ImageRendering.ADDITIVE:
+            # TODO: this is "broken" cause same pixel gets multisampled...
+            #       but it looks like it's also overdoing it in vispy vis too?
+            #       I don't know if there's a way to *not* do it...
+            return np.nansum(values)
+
+        # all the following cases are returning the *actual* value of the image at the
+        # "selected" pixel, whose position changes depending on the rendering mode.
+        if self.rendering == ImageRendering.MIP:
+            return np.nanmax(values)
+        if self.rendering == ImageRendering.MINIP:
+            return np.nanmin(values)
+        if self.rendering == ImageRendering.ATTENUATED_MIP:
+            # normalize values so attenuation applies from 0 to 1
+            values_attenuated = (
+                values - self.contrast_limits[0]
+            ) / self.contrast_limits[1]
+            # approx, step size is actually calculated with int(lenght(ray) * 2)
+            step_size = 0.5
+            sumval = (
+                step_size
+                * np.cumsum(np.clip(values_attenuated, 0, 1))
+                * len(values_attenuated)
+            )
+            scale = np.exp(-self.attenuation * (sumval - 1))
+            return values[np.nanargmin(values_attenuated * scale)]
+
+        raise RuntimeError(  # pragma: no cover
+            f'ray value calculation not implemented for {self.rendering}'
+        )
diff --git a/napari/layers/labels/_labels_constants.py b/napari/layers/labels/_labels_constants.py
index 6f24c1d8eb2..76024784713 100644
--- a/napari/layers/labels/_labels_constants.py
+++ b/napari/layers/labels/_labels_constants.py
@@ -86,3 +86,15 @@ class LabelsRendering(StringEnum):
 
     TRANSLUCENT = auto()
     ISO_CATEGORICAL = auto()
+
+
+class IsoCategoricalGradientMode(StringEnum):
+    """IsoCategoricalGradientMode: Gradient mode for the IsoCategorical rendering mode.
+
+    Selects the finite-difference gradient method for the isosurface shader:
+        * fast: use a simple finite difference gradient along each axis
+        * smooth: use an isotropic Sobel gradient, smoother but more computationally expensive
+    """
+
+    FAST = auto()
+    SMOOTH = auto()
diff --git a/napari/layers/labels/_labels_key_bindings.py b/napari/layers/labels/_labels_key_bindings.py
index 3ded76391eb..608f0eeacc0 100644
--- a/napari/layers/labels/_labels_key_bindings.py
+++ b/napari/layers/labels/_labels_key_bindings.py
@@ -72,7 +72,7 @@ def activate_labels_erase_mode(layer: Labels):
 
 @register_label_action(
     trans._(
-        'Set the currently selected label to the largest used label plus one.'
+        'Set the currently selected label to the largest used label plus one'
     ),
 )
 def new_label(layer: Labels):
@@ -97,7 +97,7 @@ def new_label(layer: Labels):
 
 
 @register_label_action(
-    trans._('Swap between the selected label and the background label.'),
+    trans._('Swap between the selected label and the background label'),
 )
 def swap_selected_and_background_labels(layer: Labels):
     """Swap between the selected label and the background label."""
@@ -105,21 +105,21 @@ def swap_selected_and_background_labels(layer: Labels):
 
 
 @register_label_action(
-    trans._('Decrease the currently selected label by one.'),
+    trans._('Decrease the currently selected label by one'),
 )
 def decrease_label_id(layer: Labels):
     layer.selected_label -= 1
 
 
 @register_label_action(
-    trans._('Increase the currently selected label by one.'),
+    trans._('Increase the currently selected label by one'),
 )
 def increase_label_id(layer: Labels):
     layer.selected_label += 1
 
 
 @register_label_action(
-    trans._('Decrease the paint brush size by one.'),
+    trans._('Decrease the paint brush size by one'),
     repeatable=True,
 )
 def decrease_brush_size(layer: Labels):
@@ -132,7 +132,7 @@ def decrease_brush_size(layer: Labels):
 
 
 @register_label_action(
-    trans._('Increase the paint brush size by one.'),
+    trans._('Increase the paint brush size by one'),
     repeatable=True,
 )
 def increase_brush_size(layer: Labels):
diff --git a/napari/layers/labels/_tests/test_labels.py b/napari/layers/labels/_tests/test_labels.py
index 67a87017056..4dfc1247ca5 100644
--- a/napari/layers/labels/_tests/test_labels.py
+++ b/napari/layers/labels/_tests/test_labels.py
@@ -3,7 +3,7 @@
 import time
 from collections import defaultdict
 from dataclasses import dataclass
-from tempfile import TemporaryDirectory
+from importlib.metadata import version
 
 import numpy as np
 import numpy.testing as npt
@@ -11,6 +11,7 @@
 import pytest
 import xarray as xr
 import zarr
+from packaging.version import parse as parse_version
 from skimage import data as sk_data
 
 from napari._tests.utils import check_layer_world_data_extent
@@ -31,7 +32,7 @@
 )
 
 
-@pytest.fixture()
+@pytest.fixture
 def direct_colormap():
     """Return a DirectLabelColormap."""
     return DirectLabelColormap(
@@ -44,7 +45,7 @@ def direct_colormap():
     )
 
 
-@pytest.fixture()
+@pytest.fixture
 def random_colormap():
     """Return a LabelColormap."""
     return label_colormap(50)
@@ -118,6 +119,20 @@ def test_bool_labels():
     assert all(np.issubdtype(d.dtype, np.integer) for d in layer.data)
 
 
+def test_editing_bool_labels():
+    # make random data, mostly 0s
+    data = np.random.random((10, 10)) > 0.7
+    # create layer, which may convert bool to uint8 *as a view*
+    layer = Labels(data)
+    # paint the whole layer with 1
+    layer.paint_polygon(
+        points=[[-1, -1], [-1, 11], [11, 11], [11, -1]],
+        new_label=1,
+    )
+    # check that the original data has been correspondingly modified
+    assert np.all(data)
+
+
 def test_changing_labels():
     """Test changing Labels data."""
     shape_a = (10, 15)
@@ -1003,7 +1018,7 @@ def test_world_data_extent():
         'mode',
         'selected_label',
         'preserve_labels',
-        'n_dimensional',
+        'n_edit_dimensions',
     ),
     list(
         itertools.product(
@@ -1011,7 +1026,7 @@ def test_world_data_extent():
             ['fill', 'erase', 'paint'],
             [1, 20, 100],
             [True, False],
-            [True, False],
+            [3, 2],
         )
     ),
 )
@@ -1020,7 +1035,7 @@ def test_undo_redo(
     mode,
     selected_label,
     preserve_labels,
-    n_dimensional,
+    n_edit_dimensions,
 ):
     blobs = sk_data.binary_blobs(length=64, volume_fraction=0.3, n_dim=3)
     layer = Labels(blobs)
@@ -1029,7 +1044,7 @@ def test_undo_redo(
     layer.mode = mode
     layer.selected_label = selected_label
     layer.preserve_labels = preserve_labels
-    layer.n_edit_dimensions = 3 if n_dimensional else 2
+    layer.n_edit_dimensions = n_edit_dimensions
     coord = np.random.random((3,)) * (np.array(blobs.shape) - 1)
     while layer.data[tuple(coord.astype(int))] == 0 and np.any(layer.data):
         coord = np.random.random((3,)) * (np.array(blobs.shape) - 1)
@@ -1110,38 +1125,43 @@ def test_large_label_values():
     assert len(np.unique(mapped.reshape((-1, 4)), axis=0)) == 4
 
 
-def test_fill_tensorstore():
+if parse_version(version('zarr')) > parse_version('3.0.0a0'):
+    driver = [(2, 'zarr'), (3, 'zarr3')]
+else:
+    driver = [(2, 'zarr')]
+
+
+@pytest.mark.parametrize(('zarr_version', 'zarr_driver'), driver)
+def test_fill_tensorstore(tmp_path, zarr_version, zarr_driver):
     ts = pytest.importorskip('tensorstore')
 
     labels = np.zeros((5, 7, 8, 9), dtype=int)
     labels[1, 2:4, 4:6, 4:6] = 1
     labels[1, 3:5, 5:7, 6:8] = 2
     labels[2, 3:5, 5:7, 6:8] = 3
-    with TemporaryDirectory(suffix='.zarr') as fout:
-        labels_temp = zarr.open(
-            fout,
-            mode='w',
-            shape=labels.shape,
-            dtype=np.uint32,
-            chunks=(1, 1, 8, 9),
-        )
-        labels_temp[:] = labels
-        labels_ts_spec = {
-            'driver': 'zarr',
-            'kvstore': {'driver': 'file', 'path': fout},
-            'path': '',
-            'metadata': {
-                'dtype': labels_temp.dtype.str,
-                'order': labels_temp.order,
-                'shape': labels.shape,
-            },
-        }
-        data = ts.open(labels_ts_spec, create=False, open=True).result()
-        layer = Labels(data)
-        layer.n_edit_dimensions = 3
-        layer.fill((1, 4, 6, 7), 4)
-        modified_labels = np.where(labels == 2, 4, labels)
-        np.testing.assert_array_equal(modified_labels, np.asarray(data))
+
+    file_path = str(tmp_path / 'labels.zarr')
+
+    labels_temp = zarr.open(
+        store=file_path,
+        mode='w',
+        shape=labels.shape,
+        dtype=np.uint32,
+        chunks=(1, 1, 8, 9),
+        zarr_version=zarr_version,
+    )
+    labels_temp[:] = labels
+    labels_ts_spec = {
+        'driver': zarr_driver,
+        'kvstore': {'driver': 'file', 'path': file_path},
+        'path': '',
+    }
+    data = ts.open(labels_ts_spec, create=False, open=True).result()
+    layer = Labels(data)
+    layer.n_edit_dimensions = 3
+    layer.fill((1, 4, 6, 7), 4)
+    modified_labels = np.where(labels == 2, 4, labels)
+    np.testing.assert_array_equal(modified_labels, np.asarray(data))
 
 
 def test_fill_with_xarray():
@@ -1752,3 +1772,13 @@ def test_events_defined(self, event_define_check, obj):
 def test_docstring():
     validate_all_params_in_docstring(Labels)
     validate_kwargs_sorted(Labels)
+
+
+def test_new_colormap_int8():
+    """Check that int8 labels colors can be shuffled without overflow.
+
+    See https://github.com/napari/napari/issues/7277.
+    """
+    data = np.arange(-128, 128, dtype=np.int8).reshape((16, 16))
+    layer = Labels(data)
+    layer.new_colormap(seed=0)
diff --git a/napari/layers/labels/_tests/test_labels_key_bindings.py b/napari/layers/labels/_tests/test_labels_key_bindings.py
index 81b9c387a02..f3975ac9569 100644
--- a/napari/layers/labels/_tests/test_labels_key_bindings.py
+++ b/napari/layers/labels/_tests/test_labels_key_bindings.py
@@ -8,7 +8,7 @@
 )
 
 
-@pytest.fixture()
+@pytest.fixture
 def labels_data_4d():
     labels = np.zeros((5, 7, 8, 9), dtype=int)
     labels[1, 2:4, 4:6, 4:6] = 1
diff --git a/napari/layers/labels/_tests/test_labels_utils.py b/napari/layers/labels/_tests/test_labels_utils.py
index ae044f6a2d9..181f9003df1 100644
--- a/napari/layers/labels/_tests/test_labels_utils.py
+++ b/napari/layers/labels/_tests/test_labels_utils.py
@@ -59,7 +59,7 @@ def test_get_dtype():
 
     data = data.astype(int)
     int_layer = Labels(data)
-    assert get_dtype(int_layer) == int
+    assert get_dtype(int_layer) is np.dtype(int)
 
 
 def test_first_nonzero_coordinate():
diff --git a/napari/layers/labels/labels.py b/napari/layers/labels/labels.py
index bd6a2d6125e..87400e05bce 100644
--- a/napari/layers/labels/labels.py
+++ b/napari/layers/labels/labels.py
@@ -1,3 +1,4 @@
+import typing
 import warnings
 from collections import deque
 from collections.abc import Sequence
@@ -8,7 +9,6 @@
     ClassVar,
     Optional,
     Union,
-    cast,
 )
 
 import numpy as np
@@ -28,6 +28,7 @@
 from napari.layers.image._image_utils import guess_multiscale
 from napari.layers.image._slice import _ImageSliceResponse
 from napari.layers.labels._labels_constants import (
+    IsoCategoricalGradientMode,
     LabelColorMode,
     LabelsRendering,
     Mode,
@@ -59,7 +60,6 @@
 from napari.utils.colormaps.colormap_utils import shuffle_and_extend_colormap
 from napari.utils.events import EmitterGroup, Event
 from napari.utils.events.custom_types import Array
-from napari.utils.geometry import clamp_point_to_bounding_box
 from napari.utils.misc import StringEnum, _is_array_type
 from napari.utils.naming import magic_name
 from napari.utils.status_messages import generate_layer_coords_status
@@ -109,6 +109,12 @@ class Labels(ScalarFieldBase):
     features : dict[str, array-like] or DataFrame
         Features table where each row corresponds to a label and each column
         is a feature. The first row corresponds to the background label.
+    iso_gradient_mode : str
+        Method for calulating the gradient (used to get the surface normal) in the
+        'iso_categorical' rendering mode. Must be one of {'fast', 'smooth'}.
+        'fast' uses a simple finite difference gradient in x, y, and z. 'smooth' uses an
+        isotropic Sobel gradient, which is smoother but more computationally expensive.
+        The default value is 'fast'.
     metadata : dict
         Layer metadata.
     multiscale : bool
@@ -206,6 +212,11 @@ class Labels(ScalarFieldBase):
         with a thickness equal to its value. Must be >= 0.
     brush_size : float
         Size of the paint brush in data coordinates.
+    iso_gradient_mode : str
+        Method for calulating the gradient (used to get the surface normal) in the
+        'iso_categorical' rendering mode. Must be one of {'fast', 'smooth'}.
+        'fast' uses a simple finite difference gradient in x, y, and z. 'smooth' uses an
+        isotropic Sobel gradient, which is smoother but more computationally expensive.
     selected_label : int
         Index of selected label. Can be greater than the current maximum label.
     mode : str
@@ -298,6 +309,7 @@ def __init__(
         depiction='volume',
         experimental_clipping_planes=None,
         features=None,
+        iso_gradient_mode=IsoCategoricalGradientMode.FAST.value,
         metadata=None,
         multiscale=None,
         name=None,
@@ -363,6 +375,7 @@ def __init__(
             contiguous=Event,
             contour=Event,
             features=Event,
+            iso_gradient_mode=Event,
             labels_update=Event,
             n_edit_dimensions=Event,
             paint=Event,
@@ -387,6 +400,8 @@ def __init__(
         self._contiguous = True
         self._brush_size = 10
 
+        self._iso_gradient_mode = IsoCategoricalGradientMode(iso_gradient_mode)
+
         self._selected_label = 1
         self.colormap.selection = self._selected_label
         self.colormap.use_selection = self._show_selected_label
@@ -431,6 +446,27 @@ def rendering(self, rendering):
         self._rendering = LabelsRendering(rendering)
         self.events.rendering()
 
+    @property
+    def iso_gradient_mode(self) -> str:
+        """Return current gradient mode for isosurface rendering.
+
+        Selects the finite-difference gradient method for the isosurface shader. Options include:
+            * ``fast``: use a simple finite difference gradient along each axis
+            * ``smooth``: use an isotropic Sobel gradient, smoother but more
+              computationally expensive
+
+        Returns
+        -------
+        str
+            The current gradient mode
+        """
+        return str(self._iso_gradient_mode)
+
+    @iso_gradient_mode.setter
+    def iso_gradient_mode(self, value: Union[IsoCategoricalGradientMode, str]):
+        self._iso_gradient_mode = IsoCategoricalGradientMode(value)
+        self.events.iso_gradient_mode()
+
     @property
     def contiguous(self):
         """bool: fill bucket changes only connected pixels of same label."""
@@ -461,7 +497,7 @@ def contour(self, contour: int) -> None:
             raise ValueError('contour value must be >= 0')
         self._contour = int(contour)
         self.events.contour()
-        self.refresh()
+        self.refresh(extent=False)
 
     @property
     def brush_size(self):
@@ -529,7 +565,7 @@ def _set_colormap(self, colormap):
         self._color_mode = color_mode
         self.events.colormap()  # Will update the LabelVispyColormap shader
         self.events.selected_label()
-        self.refresh()
+        self.refresh(extent=False)
 
     @property
     def data(self) -> Union[LayerDataProtocol, MultiScaleData]:
@@ -607,17 +643,13 @@ def _is_default_colors(self, color):
         bool
             True if color contains only default colors, otherwise False.
         """
-        if {None, self.colormap.background_value} != set(color.keys()):
-            return False
-
-        if not np.allclose(color[None], [0, 0, 0, 1]):
-            return False
-        if not np.allclose(
-            color[self.colormap.background_value], [0, 0, 0, 0]
-        ):
-            return False
-
-        return True
+        return (
+            {None, self.colormap.background_value} == set(color.keys())
+            and np.allclose(color[None], [0, 0, 0, 1])
+            and np.allclose(
+                color[self.colormap.background_value], [0, 0, 0, 0]
+            )
+        )
 
     def _ensure_int_labels(self, data):
         """Ensure data is integer by converting from bool if required, raising an error otherwise."""
@@ -636,7 +668,7 @@ def _ensure_int_labels(self, data):
                     )
                 )
             if data_level.dtype == bool:
-                int_data.append(data_level.astype(np.int8))
+                int_data.append(data_level.view(np.uint8))
             else:
                 int_data.append(data_level)
         data = int_data
@@ -658,6 +690,7 @@ def _get_state(self) -> dict[str, Any]:
                 'multiscale': self.multiscale,
                 'properties': self.properties,
                 'rendering': self.rendering,
+                'iso_gradient_mode': self.iso_gradient_mode,
                 'depiction': self.depiction,
                 'plane': self.plane.dict(),
                 'experimental_clipping_planes': [
@@ -688,7 +721,7 @@ def selected_label(self, selected_label):
         self.events.selected_label()
 
         if self.show_selected_label:
-            self.refresh()
+            self.refresh(extent=False)
 
     def swap_selected_and_background_labels(self):
         """Swap between the selected label and the background label."""
@@ -708,7 +741,7 @@ def show_selected_label(self, show_selected):
         self.colormap.use_selection = show_selected
         self.colormap.selection = self.selected_label
         self.events.show_selected_label(show_selected_label=show_selected)
-        self.refresh()
+        self.refresh(extent=False)
 
     # Only overriding to change the docstring
     @property
@@ -953,108 +986,6 @@ def get_color(self, label):
             col = self.colormap.map(label)
         return col
 
-    def _get_value_ray(
-        self,
-        start_point: Optional[np.ndarray],
-        end_point: Optional[np.ndarray],
-        dims_displayed: list[int],
-    ) -> Optional[int]:
-        """Get the first non-background value encountered along a ray.
-
-        Parameters
-        ----------
-        start_point : np.ndarray
-            (n,) array containing the start point of the ray in data coordinates.
-        end_point : np.ndarray
-            (n,) array containing the end point of the ray in data coordinates.
-        dims_displayed : List[int]
-            The indices of the dimensions currently displayed in the viewer.
-
-        Returns
-        -------
-        value : Optional[int]
-            The first non-zero value encountered along the ray. If none
-            was encountered or the viewer is in 2D mode, None is returned.
-        """
-        if start_point is None or end_point is None:
-            return None
-        if len(dims_displayed) == 3:
-            # only use get_value_ray on 3D for now
-            # we use dims_displayed because the image slice
-            # has its dimensions  in th same order as the vispy
-            # Volume
-            # Account for downsampling in the case of multiscale
-            # -1 means lowest resolution here.
-            start_point = (
-                start_point[dims_displayed]
-                / self.downsample_factors[-1][dims_displayed]
-            )
-            end_point = (
-                end_point[dims_displayed]
-                / self.downsample_factors[-1][dims_displayed]
-            )
-            start_point = cast(np.ndarray, start_point)
-            end_point = cast(np.ndarray, end_point)
-            sample_ray = end_point - start_point
-            length_sample_vector = np.linalg.norm(sample_ray)
-            n_points = int(2 * length_sample_vector)
-            sample_points = np.linspace(
-                start_point, end_point, n_points, endpoint=True
-            )
-            im_slice = self._slice.image.raw
-            # ensure the bounding box is for the proper multiscale level
-            bounding_box = self._display_bounding_box_at_level(
-                dims_displayed, self.data_level
-            )
-            # the display bounding box is returned as a closed interval
-            # (i.e. the endpoint is included) by the method, but we need
-            # open intervals in the code that follows, so we add 1.
-            bounding_box[:, 1] += 1
-
-            clamped = clamp_point_to_bounding_box(
-                sample_points,
-                bounding_box,
-            ).astype(int)
-            values = im_slice[tuple(clamped.T)]
-            nonzero_indices = np.flatnonzero(values)
-            if len(nonzero_indices > 0):
-                # if a nonzer0 value was found, return the first one
-                return values[nonzero_indices[0]]
-
-        return None
-
-    def _get_value_3d(
-        self,
-        start_point: Optional[np.ndarray],
-        end_point: Optional[np.ndarray],
-        dims_displayed: list[int],
-    ) -> Optional[int]:
-        """Get the first non-background value encountered along a ray.
-
-        Parameters
-        ----------
-        start_point : np.ndarray
-            (n,) array containing the start point of the ray in data coordinates.
-        end_point : np.ndarray
-            (n,) array containing the end point of the ray in data coordinates.
-        dims_displayed : List[int]
-            The indices of the dimensions currently displayed in the viewer.
-
-        Returns
-        -------
-        value : int
-            The first non-zero value encountered along the ray. If a
-            non-zero value is not encountered, returns 0 (the background value).
-        """
-        return (
-            self._get_value_ray(
-                start_point=start_point,
-                end_point=end_point,
-                dims_displayed=dims_displayed,
-            )
-            or 0
-        )
-
     def _reset_history(self, event=None):
         self._undo_history = deque(maxlen=self._history_limit)
         self._redo_history = deque(maxlen=self._history_limit)
@@ -1496,7 +1427,7 @@ def data_setitem(self, indices, value, refresh=True):
         if self.contour > 0:
             # Expand the slice by 1 pixel as the changes can go beyond
             # the original slice because of the morphological dilation
-            # (1 pixel because get_countours always applies 1 pixel dilation)
+            # (1 pixel because get_contours always applies 1 pixel dilation)
             updated_slice = expand_slice(updated_slice, self.data.shape, 1)
         else:
             # update data view
@@ -1517,6 +1448,12 @@ def data_setitem(self, indices, value, refresh=True):
         if refresh is True:
             self._partial_labels_refresh()
 
+    def _calculate_value_from_ray(self, values):
+        non_bg = values != self.colormap.background_value
+        if not np.any(non_bg):
+            return None
+        return values[np.argmax(np.ravel(non_bg))]
+
     def get_status(
         self,
         position: Optional[npt.ArrayLike] = None,
@@ -1635,7 +1572,9 @@ def _get_properties(
         if value is None:
             return []
 
-        label_value = value[1] if self.multiscale else value
+        label_value: int = typing.cast(
+            int, value[1] if self.multiscale else value
+        )
         if label_value not in self._label_index:
             return [trans._('[No Properties]')]
 
diff --git a/napari/layers/points/_points_key_bindings.py b/napari/layers/points/_points_key_bindings.py
index 0332d930ad9..62a98727fae 100644
--- a/napari/layers/points/_points_key_bindings.py
+++ b/napari/layers/points/_points_key_bindings.py
@@ -67,7 +67,7 @@ def paste(layer: Points) -> None:
 
 
 @register_points_action(
-    trans._('Select/Deselect all points in the current view slice.'),
+    trans._('Select/Deselect all points in the current view slice'),
 )
 def select_all_in_slice(layer: Points) -> None:
     new_selected = set(layer._indices_view[: len(layer._view_data)])
@@ -97,7 +97,7 @@ def select_all_in_slice(layer: Points) -> None:
 
 
 @register_points_action(
-    trans._('Select/Deselect all points in the layer.'),
+    trans._('Select/Deselect all points in the layer'),
 )
 def select_all_data(layer: Points) -> None:
     # If all points are already selected, deselect all points
diff --git a/napari/layers/points/_points_utils.py b/napari/layers/points/_points_utils.py
index b3c0a0b03a9..7ef85db1e78 100644
--- a/napari/layers/points/_points_utils.py
+++ b/napari/layers/points/_points_utils.py
@@ -172,7 +172,7 @@ def _points_in_box_3d(
     plane_basis = np.column_stack([up_direction, horz_direction, box_normal])
 
     # transform the points and bounding box into a new basis
-    # such that tha boudning box is axis aligned
+    # such that the bounding box is axis aligned
     bbox_corners_axis_aligned = bbox_corners @ plane_basis
     bbox_corners_axis_aligned = bbox_corners_axis_aligned[:, :2]
     points_axis_aligned = projected_points @ plane_basis
@@ -297,7 +297,7 @@ def coerce_symbols(
     # if a symbol is a unique string or Symbol instance, convert it to a
     # proper Symbol instance
     if isinstance(symbol, (str, Symbol)):
-        return np.array(symbol_conversion(symbol), dtype=object)
+        return np.array([symbol_conversion(symbol)], dtype=object)
 
     if not isinstance(symbol, np.ndarray):
         symbol = np.array(symbol)
diff --git a/napari/layers/points/_slice.py b/napari/layers/points/_slice.py
index f68108ca586..48bfb5caf2c 100644
--- a/napari/layers/points/_slice.py
+++ b/napari/layers/points/_slice.py
@@ -69,7 +69,7 @@ def __call__(self) -> _PointSliceResponse:
         # Return early if no data
         if len(self.data) == 0:
             return _PointSliceResponse(
-                indices=np.array([]),
+                indices=np.array([], dtype=int),
                 scale=np.empty(0),
                 slice_input=self.slice_input,
                 request_id=self.id,
diff --git a/napari/layers/points/_tests/test_points.py b/napari/layers/points/_tests/test_points.py
index 29b4ceb2afe..6c28bbefb6e 100644
--- a/napari/layers/points/_tests/test_points.py
+++ b/napari/layers/points/_tests/test_points.py
@@ -680,6 +680,11 @@ def test_symbol():
     layer.symbol = symbol
     assert np.array_equal(layer.symbol, expected)
 
+    with pytest.raises(
+        ValueError, match='Symbol array must be the same length as data'
+    ):
+        layer.symbol = symbol[1:5]
+
     layer = Points(data, symbol='star')
     assert np.array_equiv(layer.symbol, 'star')
 
@@ -2657,6 +2662,23 @@ def test_events_callback(old_name, new_name, value):
     old_name_callback.assert_called_once()
 
 
+def test_changing_symbol():
+    """Changing the symbol should update the UI"""
+    layer = Points(np.random.rand(2, 2))
+
+    assert layer.symbol[1].value == 'disc'
+    assert layer.current_symbol.value == 'disc'
+
+    # select a point and change its symbol
+    layer.selected_data = {1}
+    layer.current_symbol = 'square'
+    assert layer.symbol[1].value == 'square'
+    # add a point and check that it has the new symbol
+    layer.add([1, 1])
+    assert layer.symbol[2].value == 'square'
+    assert layer.symbol[0].value == 'disc'
+
+
 def test_docstring():
     validate_all_params_in_docstring(Points)
     validate_kwargs_sorted(Points)
diff --git a/napari/layers/points/_tests/test_points_key_bindings.py b/napari/layers/points/_tests/test_points_key_bindings.py
index 6f2bba3e3e7..9c72d1308c8 100644
--- a/napari/layers/points/_tests/test_points_key_bindings.py
+++ b/napari/layers/points/_tests/test_points_key_bindings.py
@@ -1,6 +1,9 @@
+import pytest
+
 from napari.layers.points import Points, _points_key_bindings as key_bindings
 
 
+@pytest.mark.key_bindings
 def test_modes(layer):
     data = [[1, 3], [8, 4], [10, 10], [15, 4]]
     layer = Points(data, size=1)
@@ -13,6 +16,7 @@ def test_modes(layer):
     assert layer.mode == 'pan_zoom'
 
 
+@pytest.mark.key_bindings
 def test_copy_paste(layer):
     data = [[1, 3], [8, 4], [10, 10], [15, 4]]
     layer = Points(data, size=1)
@@ -31,6 +35,7 @@ def test_copy_paste(layer):
     assert len(layer._clipboard) > 0
 
 
+@pytest.mark.key_bindings
 def test_select_all_in_slice(layer):
     data = [[1, 3], [8, 4], [10, 10], [15, 4]]
     layer = Points(data, size=1)
@@ -47,6 +52,7 @@ def test_select_all_in_slice(layer):
     assert len(layer.selected_data) == 0
 
 
+@pytest.mark.key_bindings
 def test_select_all_in_slice_3d_data(layer):
     data = [[0, 1, 3], [0, 8, 4], [0, 10, 10], [1, 15, 4]]
     layer = Points(data, size=1)
@@ -63,6 +69,7 @@ def test_select_all_in_slice_3d_data(layer):
     assert len(layer.selected_data) == 0
 
 
+@pytest.mark.key_bindings
 def test_select_all_data(layer):
     data = [[1, 3], [8, 4], [10, 10], [15, 4]]
     layer = Points(data, size=1)
@@ -79,6 +86,7 @@ def test_select_all_data(layer):
     assert len(layer.selected_data) == 0
 
 
+@pytest.mark.key_bindings
 def test_select_all_data_3d_data(layer):
     data = [[0, 1, 3], [0, 8, 4], [0, 10, 10], [1, 15, 4]]
     layer = Points(data, size=1)
diff --git a/napari/layers/points/_tests/test_points_mouse_bindings.py b/napari/layers/points/_tests/test_points_mouse_bindings.py
index 8a6b942daee..04cd2f89753 100644
--- a/napari/layers/points/_tests/test_points_mouse_bindings.py
+++ b/napari/layers/points/_tests/test_points_mouse_bindings.py
@@ -40,7 +40,7 @@ def read_only_event(*args, **kwargs):
     return ReadOnlyWrapper(Event(*args, **kwargs), exceptions=('handled',))
 
 
-@pytest.fixture()
+@pytest.fixture
 def create_known_points_layer_2d():
     """Create points layer with known coordinates
 
@@ -67,7 +67,7 @@ def create_known_points_layer_2d():
     return layer, n_points, known_non_point
 
 
-@pytest.fixture()
+@pytest.fixture
 def create_known_points_layer_3d():
     """Create 3D points layer with known coordinates displayed in 3D.
 
@@ -475,7 +475,7 @@ def test_unselecting_points(create_known_points_layer_2d):
     assert len(layer.selected_data) == 0
 
     # check that this also works with scaled data and position near a point (see #5737)
-    # we are taking the first point and shiftling *slightly* more than the point size
+    # we are taking the first point and shifting *slightly* more than the point size
     layer.scale = 100, 100
     pos = np.array(layer.data[0])
     pos[1] += layer.size[0] * 2
diff --git a/napari/layers/points/points.py b/napari/layers/points/points.py
index 39d158ae36f..b898faebd34 100644
--- a/napari/layers/points/points.py
+++ b/napari/layers/points/points.py
@@ -617,7 +617,7 @@ def __init__(
         self.antialiasing = antialiasing
 
         # Trigger generation of view slice and thumbnail
-        self.refresh()
+        self.refresh(extent=False)
 
     @classmethod
     def _add_deprecated_properties(cls) -> None:
@@ -655,8 +655,7 @@ def data(self, data: Optional[np.ndarray]) -> None:
         data_not_empty = (
             data is not None
             and (isinstance(data, np.ndarray) and data.size > 0)
-            or (isinstance(data, list) and len(data) > 0)
-        )
+        ) or (isinstance(data, list) and len(data) > 0)
         kwargs = {
             'value': self.data,
             'vertex_indices': ((),),
@@ -935,7 +934,7 @@ def out_of_slice_display(self, out_of_slice_display: bool) -> None:
         self._out_of_slice_display = bool(out_of_slice_display)
         self.events.out_of_slice_display()
         self.events.n_dimensional()
-        self.refresh()
+        self.refresh(extent=False)
 
     @property
     def n_dimensional(self) -> bool:
@@ -957,9 +956,18 @@ def symbol(self) -> np.ndarray:
     def symbol(self, symbol: Union[str, np.ndarray, list]) -> None:
         coerced_symbols = coerce_symbols(symbol)
         # If a single symbol has been converted, this will broadcast it to
-        # the number of points in the data. If symbols is alread an array,
+        # the number of points in the data. If symbols is already an array,
         # this will check that it is the correct length.
-        coerced_symbols = np.broadcast_to(coerced_symbols, self.data.shape[0])
+        if coerced_symbols.size == 1:
+            coerced_symbols = np.full(
+                self.data.shape[0], coerced_symbols[0], dtype=object
+            )
+        else:
+            coerced_symbols = np.array(coerced_symbols)
+            if coerced_symbols.size != self.data.shape[0]:
+                raise ValueError(
+                    'Symbol array must be the same length as data.'
+                )
         self._symbol = coerced_symbols
         self.events.symbol()
         self.events.highlight()
@@ -1011,8 +1019,8 @@ def size(self, size: Union[float, np.ndarray, list]) -> None:
                     category=DeprecationWarning,
                     stacklevel=2,
                 )
-        self._clear_extent_augmented()
-        self.refresh()
+        # TODO: technically not needed to cleat the non-augmented extent... maybe it's fine like this to avoid complexity
+        self.refresh(highlight=False)
 
     @property
     def current_size(self) -> Union[int, float]:
@@ -1051,8 +1059,8 @@ def current_size(self, size: Union[None, float]) -> None:
         if self._update_properties and len(self.selected_data) > 0:
             idx = np.fromiter(self.selected_data, dtype=int)
             self.size[idx] = size
-            self._clear_extent_augmented()
-            self.refresh()
+            # TODO: also here technically no need to clear base extent
+            self.refresh(highlight=False)
             self.events.size()
         self.events.current_size()
 
@@ -1108,7 +1116,7 @@ def shown(self) -> npt.NDArray:
     @shown.setter
     def shown(self, shown):
         self._shown = np.broadcast_to(shown, self.data.shape[0]).astype(bool)
-        self.refresh()
+        self.refresh(extent=False, highlight=False)
 
     @property
     def border_width(self) -> np.ndarray:
@@ -1141,7 +1149,7 @@ def border_width(
 
         self._border_width = border_width
         self.events.border_width(value=border_width)
-        self.refresh()
+        self.refresh(extent=False)
 
     @property
     def border_width_is_relative(self) -> bool:
@@ -1173,7 +1181,7 @@ def current_border_width(self, border_width: Union[None, float]) -> None:
         if self._update_properties and len(self.selected_data) > 0:
             idx = np.fromiter(self.selected_data, dtype=int)
             self.border_width[idx] = border_width
-            self.refresh()
+            self.refresh(highlight=False)
             self.events.border_width()
         self.events.current_border_width()
 
@@ -2000,7 +2008,9 @@ def _set_highlight(self, force: bool = False) -> None:
         self._value_stored = copy(self._value)
         self._drag_box_stored = copy(self._drag_box)
 
-        if self._value is not None or len(self._selected_view) > 0:
+        if self._highlight_visible and (
+            self._value is not None or len(self._selected_view) > 0
+        ):
             if len(self._selected_view) > 0:
                 index = copy(self._selected_view)
                 # highlight the hovered point if not in adding mode
@@ -2030,7 +2040,7 @@ def _set_highlight(self, force: bool = False) -> None:
             self._highlight_index = []
 
         # only display dragging selection box in 2D
-        if self._is_selecting:
+        if self._highlight_visible and self._is_selecting:
             if self._drag_normal is None:
                 pos = create_box(self._drag_box)
             else:
diff --git a/napari/layers/shapes/_shape_list.py b/napari/layers/shapes/_shape_list.py
index cabec6900f0..ecde52b4027 100644
--- a/napari/layers/shapes/_shape_list.py
+++ b/napari/layers/shapes/_shape_list.py
@@ -1,7 +1,7 @@
 import typing
 from collections.abc import Generator, Iterable, Sequence
 from contextlib import contextmanager
-from functools import wraps
+from functools import cached_property, wraps
 from typing import Literal, Union
 
 import numpy as np
@@ -495,6 +495,7 @@ def _add_single_shape(
         if z_refresh:
             # Set z_order
             self._update_z_order()
+        self._clear_cache()
 
     def _add_multiple_shapes(
         self,
@@ -658,6 +659,7 @@ def _make_index(length, shape_index, cval=0):
         if z_refresh:
             # Set z_order
             self._update_z_order()
+        self._clear_cache()
 
     @_batch_dec
     def remove_all(self):
@@ -720,6 +722,7 @@ def remove(self, index, renumber=True):
                 self._mesh.vertices_index[indices, 0] - 1
             )
             self._update_z_order()
+        self._clear_cache()
 
     @_batch_dec
     def _update_mesh_vertices(self, index, edge=False, face=False):
@@ -754,6 +757,7 @@ def _update_mesh_vertices(self, index, edge=False, face=False):
             indices = self._index == index
             self._vertices[indices] = shape.data_displayed
             self._update_displayed()
+        self._clear_cache()
 
     @_batch_dec
     def _update_z_order(self):
@@ -1018,15 +1022,18 @@ def transform(self, index, transform):
         self.remove(index, renumber=False)
         self.add(shape, shape_index=index)
         self._update_z_order()
+        self._clear_cache()
 
-    def outline(self, indices: Union[int, Sequence[int]]):
+    def outline(
+        self, indices: Union[int, Sequence[int]]
+    ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
         """Finds outlines of shapes listed in indices
 
         Parameters
         ----------
-        indices : int | list
-            Location in list of the shapes to be outline. If list must be a
-            list of int
+        indices : int | Sequence[int]
+            Location in list of the shapes to be outline.
+            If sequence, all elements should be ints
 
         Returns
         -------
@@ -1037,11 +1044,20 @@ def outline(self, indices: Union[int, Sequence[int]]):
         triangles : np.ndarray
             Mx3 array of any indices of vertices for triangles of outline
         """
+        if isinstance(indices, Sequence) and len(indices) == 1:
+            indices = indices[0]
         if not isinstance(indices, Sequence):
-            indices = [indices]
+            shape = self.shapes[indices]
+            return (
+                shape._edge_vertices,
+                shape._edge_offsets,
+                shape._edge_triangles,
+            )
         return self.outlines(indices)
 
-    def outlines(self, indices: Sequence[int]):
+    def outlines(
+        self, indices: Sequence[int]
+    ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
         """Finds outlines of shapes listed in indices
 
         Parameters
@@ -1058,31 +1074,10 @@ def outlines(self, indices: Sequence[int]):
         triangles : np.ndarray
             Mx3 array of any indices of vertices for triangles of outline
         """
-        indices_set = set(indices)
-        meshes = self._mesh.triangles_index
-        triangle_indices = [
-            i
-            for i, x in enumerate(meshes)
-            if x[0] in indices_set and x[1] == 1
-        ]
-        meshes = self._mesh.vertices_index
-        vertices_indices = [
-            i
-            for i, x in enumerate(meshes)
-            if x[0] in indices_set and x[1] == 1
-        ]
-
-        offsets = self._mesh.vertices_offsets[vertices_indices]
-        centers = self._mesh.vertices_centers[vertices_indices]
-        triangles = self._mesh.triangles[triangle_indices]
-
-        t_ind = self._mesh.triangles_index[triangle_indices][:, 0]
-        inds = self._mesh.vertices_index[vertices_indices][:, 0]
-        starts = np.unique(inds, return_index=True)[1]
-        for i, ind in enumerate(indices):
-            inds = t_ind == ind
-            adjust_index = starts[i] - vertices_indices[starts[i]]
-            triangles[inds] = triangles[inds] + adjust_index
+        shapes_list = [self.shapes[i] for i in indices]
+        offsets = np.vstack([s._edge_offsets for s in shapes_list])
+        centers = np.vstack([s._edge_vertices for s in shapes_list])
+        triangles = np.vstack([s._edge_triangles for s in shapes_list])
 
         return centers, offsets, triangles
 
@@ -1110,6 +1105,11 @@ def shapes_in_box(self, corners):
 
         return shapes
 
+    @cached_property
+    def _bounding_boxes(self):
+        data = np.array([s.bounding_box for s in self.shapes])
+        return data[:, 0], data[:, 1]
+
     def inside(self, coord):
         """Determines if any shape at given coord by looking inside triangle
         meshes. Looks only at displayed shapes
@@ -1125,17 +1125,30 @@ def inside(self, coord):
             Index of shape if any that is at the coordinates. Returns `None`
             if no shape is found.
         """
-        triangles = self._mesh.vertices[self._mesh.displayed_triangles]
-        indices = inside_triangles(triangles - coord)
-        shapes = self._mesh.displayed_triangles_index[indices, 0]
-
-        if len(shapes) == 0:
+        if not self.shapes:
+            return None
+        bounding_boxes = self._bounding_boxes
+        in_bbox = np.all(
+            (bounding_boxes[0] <= coord) * (bounding_boxes[1] >= coord),
+            axis=1,
+        )
+        inside_indices = np.flatnonzero(in_bbox)
+        if inside_indices.size == 0:
+            return None
+        try:
+            z_index = [self.shapes[i].z_index for i in inside_indices]
+            pos = np.argsort(z_index)
+            return next(
+                inside_indices[p]
+                for p in pos[::-1]
+                if np.any(
+                    inside_triangles(
+                        self.shapes[inside_indices[p]]._all_triangles() - coord
+                    )
+                )
+            )
+        except StopIteration:
             return None
-
-        z_list = self._z_order.tolist()
-        order_indices = np.array([z_list.index(m) for m in shapes])
-        ordered_shapes = shapes[np.argsort(order_indices)]
-        return ordered_shapes[0]
 
     def _inside_3d(self, ray_position: np.ndarray, ray_direction: np.ndarray):
         """Determines if any shape is intersected by a ray by looking inside triangle
@@ -1334,7 +1347,7 @@ def to_colors(
         # If there are too many shapes to render responsively, just render
         # the top max_shapes shapes
         if max_shapes is not None and len(z_order_in_view) > max_shapes:
-            z_order_in_view = z_order_in_view[0:max_shapes]
+            z_order_in_view = z_order_in_view[:max_shapes]
 
         for ind in z_order_in_view:
             mask = self.shapes[ind].to_mask(
@@ -1347,3 +1360,6 @@ def to_colors(
             colors[mask, :] = col
 
         return colors
+
+    def _clear_cache(self):
+        self.__dict__.pop('_bounding_boxes', None)
diff --git a/napari/layers/shapes/_shapes_key_bindings.py b/napari/layers/shapes/_shapes_key_bindings.py
index 970b2542c44..21564ec7be0 100644
--- a/napari/layers/shapes/_shapes_key_bindings.py
+++ b/napari/layers/shapes/_shapes_key_bindings.py
@@ -187,7 +187,7 @@ def move_shapes_selection_to_back(layer: Shapes) -> None:
 
 @register_shapes_action(
     trans._(
-        'Finish any drawing, for example when using the path or polygon tool.'
+        'Finish any drawing, for example when using the path or polygon tool'
     ),
 )
 def finish_drawing_shape(layer: Shapes) -> None:
diff --git a/napari/layers/shapes/_shapes_models/_polgyon_base.py b/napari/layers/shapes/_shapes_models/_polygon_base.py
similarity index 95%
rename from napari/layers/shapes/_shapes_models/_polgyon_base.py
rename to napari/layers/shapes/_shapes_models/_polygon_base.py
index ca94ebe4270..ae93d42cc1b 100644
--- a/napari/layers/shapes/_shapes_models/_polgyon_base.py
+++ b/napari/layers/shapes/_shapes_models/_polygon_base.py
@@ -80,6 +80,12 @@ def data(self, data):
             )
 
         self._data = data
+        self._bounding_box = np.array(
+            [
+                np.min(data, axis=0),
+                np.max(data, axis=0),
+            ]
+        )
         self._update_displayed_data()
 
     def _update_displayed_data(self) -> None:
@@ -117,10 +123,6 @@ def _update_displayed_data(self) -> None:
         self._set_meshes(data, face=self._filled, closed=self._closed)
         self._box = create_box(self.data_displayed)
 
-        data_not_displayed = self.data[:, self.dims_not_displayed]
         self.slice_key = np.round(
-            [
-                np.min(data_not_displayed, axis=0),
-                np.max(data_not_displayed, axis=0),
-            ]
+            self._bounding_box[:, self.dims_not_displayed]
         ).astype('int')
diff --git a/napari/layers/shapes/_shapes_models/_tests/test_shapes_models.py b/napari/layers/shapes/_shapes_models/_tests/test_shapes_models.py
index 460d6958ef2..fc008fc2874 100644
--- a/napari/layers/shapes/_shapes_models/_tests/test_shapes_models.py
+++ b/napari/layers/shapes/_shapes_models/_tests/test_shapes_models.py
@@ -1,6 +1,7 @@
 import sys
 
 import numpy as np
+import numpy.testing as npt
 import pytest
 from vispy.geometry import PolygonData
 
@@ -24,7 +25,7 @@ def test_rectangle():
     assert shape.data_displayed.shape == (4, 2)
     assert shape.slice_key.shape == (2, 0)
 
-    # If given two corners, representation will be exapanded to four
+    # If given two corners, representation will be expanded to four
     data = 20 * np.random.random((2, 2))
     shape = Rectangle(data)
     assert len(shape.data) == 4
@@ -32,6 +33,48 @@ def test_rectangle():
     assert shape.slice_key.shape == (2, 0)
 
 
+def test_rectangle_bounding_box():
+    """Test that the bounding box is correctly updated based on edge width."""
+    data = [[10, 10], [20, 20]]
+    shape = Rectangle(data)
+    npt.assert_array_equal(
+        shape.bounding_box, np.array([[9.5, 9.5], [20.5, 20.5]])
+    )
+    shape.edge_width = 2
+    npt.assert_array_equal(shape.bounding_box, np.array([[9, 9], [21, 21]]))
+    shape.edge_width = 4
+    npt.assert_array_equal(shape.bounding_box, np.array([[8, 8], [22, 22]]))
+
+
+def test_rectangle_shift():
+    shape = Rectangle(np.array([[0, 0], [1, 0], [1, 1], [0, 1]]))
+    npt.assert_array_equal(
+        shape.bounding_box, np.array([[-0.5, -0.5], [1.5, 1.5]])
+    )
+
+    shape.shift((1, 1))
+    npt.assert_array_equal(
+        shape.data, np.array([[1, 1], [2, 1], [2, 2], [1, 2]])
+    )
+    npt.assert_array_equal(
+        shape.bounding_box, np.array([[0.5, 0.5], [2.5, 2.5]])
+    )
+
+
+def test_rectangle_rotate():
+    shape = Rectangle(np.array([[1, 2], [-1, 2], [-1, -2], [1, -2]]))
+    npt.assert_array_equal(
+        shape.bounding_box, np.array([[-1.5, -2.5], [1.5, 2.5]])
+    )
+    shape.rotate(-90)
+    npt.assert_array_almost_equal(
+        shape.data, np.array([[-2, 1], [-2, -1], [2, -1], [2, 1]])
+    )
+    npt.assert_array_almost_equal(
+        shape.bounding_box, np.array([[-2.5, -1.5], [2.5, 1.5]])
+    )
+
+
 def test_nD_rectangle():
     """Test creating Shape with a random nD rectangle."""
     # Test a single four corner planar 3D rectangle
@@ -82,7 +125,7 @@ def test_polygon_data_triangle_module():
 
 def test_polygon():
     """Test creating Shape with a random polygon."""
-    # Test a single six vertex polygon
+    # Test a single non convex six vertex polygon
     data = np.array(
         [
             [10.97627008, 14.30378733],
@@ -108,7 +151,7 @@ def test_polygon2():
     shape = Polygon(data, interpolation_order=3)
     # should get many triangles
 
-    expected_face = (249, 2) if 'triangle' in sys.modules else (251, 2)
+    expected_face = (249, 2)
 
     assert shape._edge_vertices.shape == (500, 2)
     assert shape._face_vertices.shape == expected_face
@@ -198,7 +241,7 @@ def test_ellipse():
     assert shape.data_displayed.shape == (4, 2)
     assert shape.slice_key.shape == (2, 0)
 
-    # If center radii, representation will be exapanded to four corners
+    # If center radii, representation will be expanded to four corners
     data = 20 * np.random.random((2, 2))
     shape = Ellipse(data)
     assert len(shape.data) == 4
@@ -219,3 +262,32 @@ def test_nD_ellipse():
 
     shape.ndisplay = 3
     assert shape.data_displayed.shape == (4, 3)
+
+
+def test_ellipse_shift():
+    shape = Ellipse(np.array([[0, 0], [1, 0], [1, 1], [0, 1]]))
+    npt.assert_array_equal(
+        shape.bounding_box, np.array([[-0.5, -0.5], [1.5, 1.5]])
+    )
+
+    shape.shift((1, 1))
+    npt.assert_array_equal(
+        shape.data, np.array([[1, 1], [2, 1], [2, 2], [1, 2]])
+    )
+    npt.assert_array_equal(
+        shape.bounding_box, np.array([[0.5, 0.5], [2.5, 2.5]])
+    )
+
+
+def test_ellipse_rotate():
+    shape = Ellipse(np.array([[1, 2], [-1, 2], [-1, -2], [1, -2]]))
+    npt.assert_array_equal(
+        shape.bounding_box, np.array([[-1.5, -2.5], [1.5, 2.5]])
+    )
+    shape.rotate(-90)
+    npt.assert_array_almost_equal(
+        shape.data, np.array([[-2, 1], [-2, -1], [2, -1], [2, 1]])
+    )
+    npt.assert_array_almost_equal(
+        shape.bounding_box, np.array([[-2.5, -1.5], [2.5, 1.5]])
+    )
diff --git a/napari/layers/shapes/_shapes_models/ellipse.py b/napari/layers/shapes/_shapes_models/ellipse.py
index 2c21ecb308c..c9440cd6039 100644
--- a/napari/layers/shapes/_shapes_models/ellipse.py
+++ b/napari/layers/shapes/_shapes_models/ellipse.py
@@ -77,6 +77,12 @@ def data(self, data):
             )
 
         self._data = data
+        self._bounding_box = np.round(
+            [
+                np.min(data, axis=0),
+                np.max(data, axis=0),
+            ]
+        )
         self._update_displayed_data()
 
     def _update_displayed_data(self) -> None:
@@ -88,13 +94,9 @@ def _update_displayed_data(self) -> None:
         self._face_triangles = triangles
         self._box = rectangle_to_box(self.data_displayed)
 
-        data_not_displayed = self.data[:, self.dims_not_displayed]
-        self.slice_key = np.round(
-            [
-                np.min(data_not_displayed, axis=0),
-                np.max(data_not_displayed, axis=0),
-            ]
-        ).astype('int')
+        self.slice_key = self._bounding_box[:, self.dims_not_displayed].astype(
+            'int'
+        )
 
     def transform(self, transform):
         """Performs a linear transform on the shape
@@ -118,3 +120,9 @@ def transform(self, transform):
         self._edge_vertices = centers
         self._edge_offsets = offsets
         self._edge_triangles = triangles
+        self._bounding_box = np.array(
+            [
+                np.min(self._data, axis=0),
+                np.max(self._data, axis=0),
+            ]
+        )
diff --git a/napari/layers/shapes/_shapes_models/line.py b/napari/layers/shapes/_shapes_models/line.py
index a7626f1ec34..39e1a5d1c8e 100644
--- a/napari/layers/shapes/_shapes_models/line.py
+++ b/napari/layers/shapes/_shapes_models/line.py
@@ -62,6 +62,13 @@ def data(self, data):
             )
 
         self._data = data
+        self._bounding_box = np.array(
+            [
+                np.min(data, axis=0),
+                np.max(data, axis=0),
+            ]
+        )
+
         self._update_displayed_data()
 
     def _update_displayed_data(self) -> None:
@@ -70,10 +77,6 @@ def _update_displayed_data(self) -> None:
         self._set_meshes(self.data_displayed, face=False, closed=False)
         self._box = create_box(self.data_displayed)
 
-        data_not_displayed = self.data[:, self.dims_not_displayed]
         self.slice_key = np.round(
-            [
-                np.min(data_not_displayed, axis=0),
-                np.max(data_not_displayed, axis=0),
-            ]
+            self._bounding_box[:, self.dims_not_displayed]
         ).astype('int')
diff --git a/napari/layers/shapes/_shapes_models/path.py b/napari/layers/shapes/_shapes_models/path.py
index fc01fef1f3d..e265d922d36 100644
--- a/napari/layers/shapes/_shapes_models/path.py
+++ b/napari/layers/shapes/_shapes_models/path.py
@@ -1,4 +1,4 @@
-from napari.layers.shapes._shapes_models._polgyon_base import PolygonBase
+from napari.layers.shapes._shapes_models._polygon_base import PolygonBase
 
 
 class Path(PolygonBase):
diff --git a/napari/layers/shapes/_shapes_models/polygon.py b/napari/layers/shapes/_shapes_models/polygon.py
index 0b566c55471..54eaaa9fd53 100644
--- a/napari/layers/shapes/_shapes_models/polygon.py
+++ b/napari/layers/shapes/_shapes_models/polygon.py
@@ -1,4 +1,4 @@
-from napari.layers.shapes._shapes_models._polgyon_base import PolygonBase
+from napari.layers.shapes._shapes_models._polygon_base import PolygonBase
 
 
 class Polygon(PolygonBase):
diff --git a/napari/layers/shapes/_shapes_models/rectangle.py b/napari/layers/shapes/_shapes_models/rectangle.py
index baee56a56c2..07c9eaf299f 100644
--- a/napari/layers/shapes/_shapes_models/rectangle.py
+++ b/napari/layers/shapes/_shapes_models/rectangle.py
@@ -68,6 +68,12 @@ def data(self, data):
             )
 
         self._data = data
+        self._bounding_box = np.array(
+            [
+                np.min(data, axis=0),
+                np.max(data, axis=0),
+            ]
+        )
         self._update_displayed_data()
 
     def _update_displayed_data(self) -> None:
@@ -77,10 +83,6 @@ def _update_displayed_data(self) -> None:
         self._face_vertices = self.data_displayed
         self._face_triangles = np.array([[0, 1, 2], [0, 2, 3]])
         self._box = rectangle_to_box(self.data_displayed)
-        data_not_displayed = self.data[:, self.dims_not_displayed]
-        self.slice_key = np.round(
-            [
-                np.min(data_not_displayed, axis=0),
-                np.max(data_not_displayed, axis=0),
-            ]
-        ).astype('int')
+        self.slice_key = self._bounding_box[:, self.dims_not_displayed].astype(
+            'int'
+        )
diff --git a/napari/layers/shapes/_shapes_models/shape.py b/napari/layers/shapes/_shapes_models/shape.py
index a1cc560e833..dbd01c9bb7f 100644
--- a/napari/layers/shapes/_shapes_models/shape.py
+++ b/napari/layers/shapes/_shapes_models/shape.py
@@ -119,6 +119,7 @@ def __init__(
         self.name = ''
 
         self._data: npt.NDArray
+        self._bounding_box = np.empty((0, self.ndisplay))
 
     @property
     @abstractmethod
@@ -164,6 +165,15 @@ def dims_displayed(self):
         """tuple: Dimensions that are displayed."""
         return self.dims_order[-self.ndisplay :]
 
+    @property
+    def bounding_box(self) -> np.ndarray:
+        """(2, N) array, bounding box of the object."""
+        # We add +-0.5 to handle edge width
+        return self._bounding_box[:, self.dims_displayed] + [
+            [-0.5 * self.edge_width],
+            [0.5 * self.edge_width],
+        ]
+
     @property
     def dims_not_displayed(self):
         """tuple: Dimensions that are not displayed."""
@@ -254,6 +264,23 @@ def _set_meshes(
             self._face_vertices = np.empty((0, self.ndisplay))
             self._face_triangles = np.empty((0, 3), dtype=np.uint32)
 
+    def _all_triangles(self):
+        """Return all triangles for the shape
+
+        Returns
+        -------
+        np.ndarray
+            Nx3 array of vertex indices that form the triangles for the shape
+        """
+        return np.vstack(
+            [
+                self._face_vertices[self._face_triangles],
+                (self._edge_vertices + self.edge_width * self._edge_offsets)[
+                    self._edge_triangles
+                ],
+            ]
+        )
+
     def transform(self, transform: npt.NDArray) -> None:
         """Performs a linear transform on the shape
 
@@ -276,6 +303,12 @@ def transform(self, transform: npt.NDArray) -> None:
         self._edge_vertices = centers
         self._edge_offsets = offsets
         self._edge_triangles = triangles
+        self._bounding_box = np.array(
+            [
+                np.min(self._data, axis=0),
+                np.max(self._data, axis=0),
+            ]
+        )
 
     def shift(self, shift: npt.NDArray) -> None:
         """Performs a 2D shift on the shape
@@ -291,6 +324,9 @@ def shift(self, shift: npt.NDArray) -> None:
         self._edge_vertices = self._edge_vertices + shift
         self._box = self._box + shift
         self._data[:, self.dims_displayed] = self.data_displayed + shift
+        self._bounding_box[:, self.dims_displayed] = (
+            self._bounding_box[:, self.dims_displayed] + shift
+        )
 
     def scale(self, scale, center=None):
         """Performs a scaling on the shape
@@ -309,6 +345,7 @@ def scale(self, scale, center=None):
         if center is None:
             self.transform(transform)
         else:
+            center = np.array(center)
             self.shift(-center)
             self.transform(transform)
             self.shift(center)
@@ -330,6 +367,7 @@ def rotate(self, angle, center=None):
         if center is None:
             self.transform(transform)
         else:
+            center = np.array(center)
             self.shift(-center)
             self.transform(transform)
             self.shift(center)
diff --git a/napari/layers/shapes/_shapes_mouse_bindings.py b/napari/layers/shapes/_shapes_mouse_bindings.py
index 00ace03fb12..81376a5c22b 100644
--- a/napari/layers/shapes/_shapes_mouse_bindings.py
+++ b/napari/layers/shapes/_shapes_mouse_bindings.py
@@ -872,3 +872,8 @@ def _move_active_element_under_cursor(
             shapes = layer.selected_data
             layer._selected_box = layer.interaction_box(shapes)
             layer.refresh()
+
+
+def _set_highlight(layer: Shapes, event: MouseEvent) -> None:
+    if event.type in {'mouse_press', 'mouse_wheel'}:
+        layer._set_highlight()
diff --git a/napari/layers/shapes/_shapes_utils.py b/napari/layers/shapes/_shapes_utils.py
index 1879d0fdffb..792d66e17f6 100644
--- a/napari/layers/shapes/_shapes_utils.py
+++ b/napari/layers/shapes/_shapes_utils.py
@@ -20,6 +20,55 @@
     triangulate = None
 
 
+def _is_convex(poly: npt.NDArray) -> bool:
+    """Check whether a polygon is convex.
+
+    Parameters
+    ----------
+    poly: numpy array of floats, shape (N, 3)
+        Polygon vertices, in order.
+
+    Returns
+    -------
+    bool
+        True if the given polygon is convex.
+    """
+    fst = poly[:-2]
+    snd = poly[1:-1]
+    thrd = poly[2:]
+    orn_set = np.unique(orientation(fst.T, snd.T, thrd.T))
+    if orn_set.size != 1:
+        return False
+    return (orn_set[0] == orientation(poly[-2], poly[-1], poly[0])) and (
+        orn_set[0] == orientation(poly[-1], poly[0], poly[1])
+    )
+
+
+def _fan_triangulation(poly: npt.NDArray) -> tuple[npt.NDArray, npt.NDArray]:
+    """Return a fan triangulation of a given polygon.
+
+    https://en.wikipedia.org/wiki/Fan_triangulation
+
+    Parameters
+    ----------
+    poly: numpy array of float, shape (N, 3)
+        Polygon vertices, in order.
+
+    Returns
+    -------
+    vertices : numpy array of float, shape (N, 3)
+        The vertices of the triangulation. In this case, the input array.
+    triangles : numpy array of int, shape (N, 3)
+        The triangles of the triangulation, as triplets of indices into the
+        vertices array.
+    """
+    vertices = np.copy(poly)
+    triangles = np.zeros((len(poly) - 2, 3), dtype=np.uint32)
+    triangles[:, 1] = np.arange(1, len(poly) - 1)
+    triangles[:, 2] = np.arange(2, len(poly))
+    return vertices, triangles
+
+
 def inside_boxes(boxes):
     """Checks which boxes contain the origin. Boxes need not be axis aligned
 
@@ -186,7 +235,7 @@ def lines_intersect(p1, q1, p2, q2):
         return True
 
     # p2, q2 and q1 are collinear and q1 lies on segment p2q2
-    if o4 == 0 and on_segment(p2, q1, q2):
+    if o4 == 0 and on_segment(p2, q1, q2):  # noqa: SIM103
         return True
 
     # Doesn't fall into any special cases
@@ -510,7 +559,7 @@ def triangulate_ellipse(
     # Compute the transformation matrix from the unit circle
     # to our current ellipse.
     # ... it's easy just the 1/2 minor/major axes for the two column
-    # note that our transform shape will depends on wether we are 2D-> 2D (matrix, 2 by 2),
+    # note that our transform shape will depends on whether we are 2D-> 2D (matrix, 2 by 2),
     # or 2D -> 3D (matrix 2 by 3).
     transform = np.stack((ax1, ax2))
     if corners.shape == (4, 2):
@@ -566,6 +615,8 @@ def triangulate_face(data: npt.NDArray) -> tuple[npt.NDArray, npt.NDArray]:
 
         res = triangulate({'vertices': data, 'segments': edges}, 'p')
         vertices, triangles = res['vertices'], res['triangles']
+    elif _is_convex(data):
+        vertices, triangles = _fan_triangulation(data)
     else:
         vertices, triangles = PolygonData(vertices=data).triangulate()
 
diff --git a/napari/layers/shapes/_tests/test_shape_list.py b/napari/layers/shapes/_tests/test_shape_list.py
index 4602b9be242..a6e34d25592 100644
--- a/napari/layers/shapes/_tests/test_shape_list.py
+++ b/napari/layers/shapes/_tests/test_shape_list.py
@@ -1,4 +1,5 @@
 import numpy as np
+import numpy.testing as npt
 import pytest
 
 from napari.layers.shapes._shape_list import ShapeList
@@ -23,6 +24,58 @@ def test_adding_to_shape_list():
     assert shape_list.shapes[0] == shape
 
 
+def test_reset_bounding_box_rotation():
+    """Test if rotating shape resets bounding box."""
+    shape = Rectangle(np.array([[0, 0], [10, 10]]))
+    shape_list = ShapeList()
+    shape_list.add(shape)
+    npt.assert_array_almost_equal(
+        shape_list._bounding_boxes, np.array([[[-0.5, -0.5]], [[10.5, 10.5]]])
+    )
+    shape_list.rotate(0, 45, (5, 5))
+    p = 5 * np.sqrt(2) + 0.5
+    npt.assert_array_almost_equal(
+        shape.bounding_box, np.array([[5 - p, 5 - p], [5 + p, 5 + p]])
+    )
+    npt.assert_array_almost_equal(
+        shape_list._bounding_boxes, shape.bounding_box[:, np.newaxis, :]
+    )
+
+
+def test_reset_bounding_box_shift():
+    """Test if shifting shape resets bounding box."""
+    shape = Rectangle(np.array([[0, 0], [10, 10]]))
+    shape_list = ShapeList()
+    shape_list.add(shape)
+    npt.assert_array_almost_equal(
+        shape_list._bounding_boxes, shape.bounding_box[:, np.newaxis, :]
+    )
+    shape_list.shift(0, np.array([5, 5]))
+    npt.assert_array_almost_equal(
+        shape.bounding_box, np.array([[4.5, 4.5], [15.5, 15.5]])
+    )
+    npt.assert_array_almost_equal(
+        shape_list._bounding_boxes, shape.bounding_box[:, np.newaxis, :]
+    )
+
+
+def test_reset_bounding_box_scale():
+    """Test if scaling shape resets the bounding box."""
+    shape = Rectangle(np.array([[0, 0], [10, 10]]))
+    shape_list = ShapeList()
+    shape_list.add(shape)
+    npt.assert_array_almost_equal(
+        shape_list._bounding_boxes, shape.bounding_box[:, np.newaxis, :]
+    )
+    shape_list.scale(0, 2, (5, 5))
+    npt.assert_array_almost_equal(
+        shape.bounding_box, np.array([[-5.5, -5.5], [15.5, 15.5]])
+    )
+    npt.assert_array_almost_equal(
+        shape_list._bounding_boxes, shape.bounding_box[:, np.newaxis, :]
+    )
+
+
 def test_shape_list_outline():
     """Test ShapeList outline method."""
     np.random.seed(0)
diff --git a/napari/layers/shapes/_tests/test_shapes.py b/napari/layers/shapes/_tests/test_shapes.py
index 3fa068f1869..26f91e4fcac 100644
--- a/napari/layers/shapes/_tests/test_shapes.py
+++ b/napari/layers/shapes/_tests/test_shapes.py
@@ -2103,6 +2103,7 @@ def test_value():
     np.random.seed(0)
     data = 20 * np.random.random(shape)
     data[-1, :] = [[0, 0], [0, 10], [10, 0], [10, 10]]
+    assert Shapes([]).get_value((0,) * 2) == (None, None)
     layer = Shapes(data)
     value = layer.get_value((0,) * 2)
     assert value == (9, None)
@@ -2117,6 +2118,16 @@ def test_value():
     assert value == (None, None)
 
 
+def test_value_non_convex():
+    """Test getting the value of the data at the current coordinates."""
+    data = [
+        [[0, 0], [10, 10], [20, 0], [10, 5]],
+    ]
+    layer = Shapes(data, shape_type='polygon')
+    assert layer.get_value((1,) * 2) == (0, None)
+    assert layer.get_value((10, 3)) == (None, None)
+
+
 @pytest.mark.parametrize(
     (
         'position',
@@ -2384,6 +2395,14 @@ def test_shapes_add_delete_only_emit_two_events():
     assert emitted_events.call_count == 4
 
 
+def test_clean_selection_on_set_data():
+    data = [[[0, 0], (10, 10)], [[0, 15], [10, 25]]]
+    layer = Shapes(data)
+    layer.selected_data = {0}
+    layer.data = [[[0, 0], (10, 10)]]
+    assert layer.selected_data == set()
+
+
 def test_docstring():
     validate_all_params_in_docstring(Shapes)
     validate_kwargs_sorted(Shapes)
diff --git a/napari/layers/shapes/_tests/test_shapes_mouse_bindings.py b/napari/layers/shapes/_tests/test_shapes_mouse_bindings.py
index c11a2d9682c..032eda35b88 100644
--- a/napari/layers/shapes/_tests/test_shapes_mouse_bindings.py
+++ b/napari/layers/shapes/_tests/test_shapes_mouse_bindings.py
@@ -16,7 +16,7 @@
 )
 
 
-@pytest.fixture()
+@pytest.fixture
 def create_known_shapes_layer():
     """Create shapes layer with known coordinates
 
diff --git a/napari/layers/shapes/_tests/test_shapes_utils.py b/napari/layers/shapes/_tests/test_shapes_utils.py
index 581e213de16..c36af87018a 100644
--- a/napari/layers/shapes/_tests/test_shapes_utils.py
+++ b/napari/layers/shapes/_tests/test_shapes_utils.py
@@ -303,7 +303,7 @@ def _regen_testcases():
 ]
 
 
-@pytest.fixture()
+@pytest.fixture
 def create_complex_shape():
     shape = np.array(
         [
diff --git a/napari/layers/shapes/shapes.py b/napari/layers/shapes/shapes.py
index abaaf6cc860..b1c3243003f 100644
--- a/napari/layers/shapes/shapes.py
+++ b/napari/layers/shapes/shapes.py
@@ -30,6 +30,7 @@
     shape_classes,
 )
 from napari.layers.shapes._shapes_mouse_bindings import (
+    _set_highlight,
     add_ellipse,
     add_line,
     add_path_polygon,
@@ -616,6 +617,8 @@ def __init__(
         )
 
         # Trigger generation of view slice and thumbnail
+        self.mouse_wheel_callbacks.append(_set_highlight)
+        self.mouse_drag_callbacks.append(_set_highlight)
         self.refresh()
 
     def _initialize_current_color_for_empty_layer(
@@ -671,6 +674,7 @@ def data(self):
     @data.setter
     def data(self, data):
         self._finish_drawing()
+        self.selected_data = set()
         prior_data = len(self.data) > 0
         data, shape_type = extract_shape_type(data)
         n_new_shapes = number_of_shapes(data)
@@ -714,8 +718,7 @@ def data(self, data):
         data_not_empty = (
             data is not None
             and (isinstance(data, np.ndarray) and data.size > 0)
-            or (isinstance(data, list) and len(data) > 0)
-        )
+        ) or (isinstance(data, list) and len(data) > 0)
         kwargs = {
             'value': self.data,
             'vertex_indices': ((),),
@@ -1292,6 +1295,8 @@ def selected_data(self, selected_data):
                 with self.block_update_properties():
                     self.current_properties = unique_properties
 
+        self._set_highlight()
+
     @property
     def _is_moving(self) -> bool:
         return self._private_is_moving
@@ -1716,12 +1721,15 @@ def mode(self, val: Union[str, Mode]):
         }
 
         # don't update thumbnail on mode changes
-        with self.block_thumbnail_update():
-            if not (mode in draw_modes and self._mode in draw_modes):
-                # Shapes._finish_drawing() calls Shapes.refresh()
+        if not (mode in draw_modes and self._mode in draw_modes):
+            # Shapes._finish_drawing() calls Shapes.refresh() via Shapes._update_dims()
+            # so we need to block thumbnail update from here
+            # TODO: this is not great... ideally we should no longer need this blocking system
+            #       but maybe follow up PR
+            with self.block_thumbnail_update():
                 self._finish_drawing()
-            else:
-                self.refresh()
+        else:
+            self.refresh(data_displayed=False, extent=False, thumbnail=False)
 
     def _reset_editable(self) -> None:
         self.editable = self._slice_input.ndisplay == 2
@@ -2400,7 +2408,7 @@ def _set_view_slice(self):
 
     def interaction_box(self, index):
         """Create the interaction box around a shape or list of shapes.
-        If a single index is passed then the boudning box will be inherited
+        If a single index is passed then the bounding box will be inherited
         from that shapes interaction box. If list of indices is passed it will
         be computed directly.
 
@@ -2461,8 +2469,10 @@ def _outline_shapes(self):
             Mx3 array of any indices of vertices for triangles of outline or
             None
         """
-        if self._value is not None and (
-            self._value[0] is not None or len(self.selected_data) > 0
+        if (
+            self._highlight_visible
+            and self._value is not None
+            and (self._value[0] is not None or len(self.selected_data) > 0)
         ):
             if len(self.selected_data) > 0:
                 index = list(self.selected_data)
@@ -2503,9 +2513,9 @@ def _compute_vertices_and_box(self):
         width : float
             Width of the box edge
         """
-        if len(self.selected_data) > 0:
+        if self._highlight_visible and len(self.selected_data) > 0:
             if self._mode == Mode.SELECT:
-                # If in select mode just show the interaction boudning box
+                # If in select mode just show the interaction bounding box
                 # including its vertices and the rotation handle
                 box = self._selected_box[Box.WITH_HANDLE]
                 if self._value[0] is None or self._value[1] is None:
@@ -2555,7 +2565,7 @@ def _compute_vertices_and_box(self):
                 edge_color = 'white'
                 pos = None
                 width = 0
-        elif self._is_selecting:
+        elif self._highlight_visible and self._is_selecting:
             # If currently dragging a selection box just show an outline of
             # that box
             vertices = np.empty((0, 2))
@@ -2789,16 +2799,6 @@ def _transform_box(self, transform, center=(0, 0)):
             box[Box.HANDLE] = box[Box.TOP_CENTER] + r * handle_vec / cur_len
         self._selected_box = box + center
 
-    def _update_draw(
-        self, scale_factor, corner_pixels_displayed, shape_threshold
-    ):
-        prev_scale = self.scale_factor
-        super()._update_draw(
-            scale_factor, corner_pixels_displayed, shape_threshold
-        )
-        # update highlight only if scale has changed, otherwise causes a cycle
-        self._set_highlight(force=(prev_scale != self.scale_factor))
-
     def _get_value(self, position):
         """Value of the data at a position in data coordinates.
 
@@ -3013,7 +3013,7 @@ def move_to_front(self) -> None:
         new_z_index = max(self._data_view._z_index) + 1
         for index in self.selected_data:
             self._data_view.update_z_index(index, new_z_index)
-        self.refresh()
+        self.refresh(extent=False, highlight=False)
 
     def move_to_back(self) -> None:
         """Moves selected objects to be displayed behind all others."""
@@ -3022,7 +3022,7 @@ def move_to_back(self) -> None:
         new_z_index = min(self._data_view._z_index) - 1
         for index in self.selected_data:
             self._data_view.update_z_index(index, new_z_index)
-        self.refresh()
+        self.refresh(extent=False, highlight=False)
 
     def _copy_data(self) -> None:
         """Copy selected shapes to clipboard."""
diff --git a/napari/layers/surface/surface.py b/napari/layers/surface/surface.py
index 6f824a26fee..7bee420ba9a 100644
--- a/napari/layers/surface/surface.py
+++ b/napari/layers/surface/surface.py
@@ -434,7 +434,7 @@ def faces(self, faces: np.ndarray) -> None:
 
         self.faces = faces
 
-        self.refresh()
+        self.refresh(extent=False)
         self.events.data(value=self.data)
         self._reset_editable()
 
diff --git a/napari/layers/tracks/_track_utils.py b/napari/layers/tracks/_track_utils.py
index d937470c8c0..3d1f7d067f5 100644
--- a/napari/layers/tracks/_track_utils.py
+++ b/napari/layers/tracks/_track_utils.py
@@ -1,6 +1,7 @@
-from typing import TYPE_CHECKING, Optional, Union
+from typing import Optional, Union
 
 import numpy as np
+import numpy.typing as npt
 import pandas as pd
 from scipy.sparse import coo_matrix
 from scipy.spatial import cKDTree
@@ -9,9 +10,6 @@
 from napari.utils.events.custom_types import Array
 from napari.utils.translations import trans
 
-if TYPE_CHECKING:
-    import numpy.typing as npt
-
 
 class TrackManager:
     """Manage track data and simplify interactions with the Tracks layer.
@@ -75,12 +73,12 @@ def __init__(self, data: np.ndarray) -> None:
         self._points_lookup: dict[int, slice]
         self._ordered_points_idx: npt.NDArray
 
-        self._track_vertices = None
-        self._track_connex = None
+        self._track_vertices: npt.NDArray | None = None
+        self._track_connex: npt.NDArray | None = None
 
         self._graph: Optional[dict[int, list[int]]] = None
         self._graph_vertices = None
-        self._graph_connex = None
+        self._graph_connex: npt.NDArray | None = None
 
     @staticmethod
     def _fast_points_lookup(sorted_time: np.ndarray) -> dict[int, slice]:
@@ -105,7 +103,7 @@ def data(self) -> np.ndarray:
         return self._data
 
     @data.setter
-    def data(self, data: Union[list, np.ndarray]):
+    def data(self, data: Union[list, np.ndarray]) -> None:
         """set the vertex data and build the vispy arrays for display"""
 
         # convert data to a numpy array if it is not already one
@@ -143,7 +141,7 @@ def data(self, data: Union[list, np.ndarray]):
         ).tocsr()
 
     @property
-    def features(self):
+    def features(self) -> pd.DataFrame:
         """Dataframe-like features table.
 
         It is an implementation detail that this is a `pandas.DataFrame`. In the future,
@@ -176,7 +174,7 @@ def properties(self) -> dict[str, np.ndarray]:
         return self._feature_table.properties()
 
     @properties.setter
-    def properties(self, properties: dict[str, Array]):
+    def properties(self, properties: dict[str, Array]) -> None:
         """set track properties"""
         self.features = properties
 
@@ -186,25 +184,25 @@ def graph(self) -> Optional[dict[int, list[int]]]:
         return self._graph
 
     @graph.setter
-    def graph(self, graph: dict[int, Union[int, list[int]]]):
+    def graph(self, graph: dict[int, Union[int, list[int]]]) -> None:
         """set the track graph"""
         self._graph = self._normalize_track_graph(graph)
 
     @property
-    def track_ids(self):
+    def track_ids(self) -> npt.NDArray[np.uint32]:
         """return the track identifiers"""
         return self.data[:, 0].astype(np.uint32)
 
     @property
-    def unique_track_ids(self):
+    def unique_track_ids(self) -> npt.NDArray[np.uint32]:
         """return the unique track identifiers"""
         return np.unique(self.track_ids)
 
-    def __len__(self):
+    def __len__(self) -> int:
         """return the number of tracks"""
         return len(self.unique_track_ids) if self.data is not None else 0
 
-    def _vertex_indices_from_id(self, track_id: int):
+    def _vertex_indices_from_id(self, track_id: int) -> npt.NDArray:
         """return the vertices corresponding to a track id"""
         return self._id2idxs[track_id].nonzero()[1]
 
@@ -271,7 +269,7 @@ def _normalize_track_graph(
 
         return new_graph
 
-    def build_tracks(self):
+    def build_tracks(self) -> None:
         """build the tracks"""
 
         # Track ids associated to all vertices, sorted by time
@@ -293,12 +291,13 @@ def build_tracks(self):
         self._track_vertices = track_vertices
         self._track_connex = track_connex
 
-    def build_graph(self):
+    def build_graph(self) -> None:
         """build the track graph"""
 
         graph_vertices = []
         graph_connex = []
 
+        assert self.graph is not None
         for node_idx, parents_idx in self.graph.items():
             # we join from the first observation of the node, to the last
             # observation of the parent
@@ -335,7 +334,7 @@ def vertex_properties(self, color_by: str) -> np.ndarray:
 
         return self.properties[color_by]
 
-    def get_value(self, coords):
+    def get_value(self, coords: npt.NDArray) -> Optional[npt.NDArray]:
         """use a kd-tree to lookup the ID of the nearest tree"""
         if self._kdtree is None:
             return None
@@ -380,7 +379,7 @@ def graph_vertices(self) -> Optional[np.ndarray]:
         return self._graph_vertices
 
     @property
-    def graph_connex(self):
+    def graph_connex(self) -> Optional[npt.NDArray]:
         """vertex connections for drawing the graph"""
         return self._graph_connex
 
diff --git a/napari/layers/tracks/_tracks_key_bindings.py b/napari/layers/tracks/_tracks_key_bindings.py
index 94e7b839e4d..7c971b46a63 100644
--- a/napari/layers/tracks/_tracks_key_bindings.py
+++ b/napari/layers/tracks/_tracks_key_bindings.py
@@ -1,3 +1,5 @@
+from typing import Callable
+
 from napari.layers.base._base_constants import Mode
 from napari.layers.tracks.tracks import Tracks
 from napari.layers.utils.layer_utils import (
@@ -7,11 +9,15 @@
 from napari.utils.translations import trans
 
 
-def register_tracks_action(description: str, repeatable: bool = False):
+def register_tracks_action(
+    description: str, repeatable: bool = False
+) -> Callable[[Callable], Callable]:
     return register_layer_action(Tracks, description, repeatable)
 
 
-def register_tracks_mode_action(description):
+def register_tracks_mode_action(
+    description: str,
+) -> Callable[[Callable], Callable]:
     return register_layer_attr_action(Tracks, description, 'mode')
 
 
@@ -21,7 +27,7 @@ def activate_tracks_transform_mode(layer: Tracks) -> None:
 
 
 @register_tracks_mode_action(trans._('Pan/zoom'))
-def activate_tracks_pan_zoom_mode(layer: Tracks):
+def activate_tracks_pan_zoom_mode(layer: Tracks) -> None:
     layer.mode = str(Mode.PAN_ZOOM)
 
 
diff --git a/napari/layers/tracks/tracks.py b/napari/layers/tracks/tracks.py
index e6eabfd3299..830da5c8a7f 100644
--- a/napari/layers/tracks/tracks.py
+++ b/napari/layers/tracks/tracks.py
@@ -260,7 +260,7 @@ def _get_state(self) -> dict[str, Any]:
         )
         return state
 
-    def _set_view_slice(self):
+    def _set_view_slice(self) -> None:
         """Sets the view given the indices to slice with."""
 
         # if the displayed dims have changed, update the shader data
@@ -274,7 +274,7 @@ def _set_view_slice(self):
 
         return
 
-    def _get_value(self, position) -> int:
+    def _get_value(self, position) -> Optional[int]:
         """Value of the data at a position in data coordinates.
 
         Use a kd-tree to lookup the ID of the nearest tree.
@@ -289,9 +289,12 @@ def _get_value(self, position) -> int:
         value : int or None
             Index of track that is at the current coordinate if any.
         """
-        return self._manager.get_value(np.array(position))
+        val = self._manager.get_value(np.array(position))
+        if val is None:
+            return None
+        return int(val)
 
-    def _update_thumbnail(self):
+    def _update_thumbnail(self) -> None:
         """Update thumbnail with current points and colors."""
         colormapped = np.zeros(self._thumbnail_shape)
         colormapped[..., 3] = 1
@@ -312,7 +315,7 @@ def _update_thumbnail(self):
                 points = self._view_data[thumbnail_indices]
             else:
                 points = self._view_data
-                thumbnail_indices = range(len(self._view_data))
+                thumbnail_indices = np.array(range(len(self._view_data)))
 
             # get the track coords here
             coords = np.floor(
@@ -323,6 +326,8 @@ def _update_thumbnail(self):
             )
 
             # modulate track colors as per colormap/current_time
+            assert self.track_times is not None
+            assert self.current_time is not None
             colors = self.track_colors[thumbnail_indices]
             times = self.track_times[thumbnail_indices]
             alpha = (self.head_length + self.current_time - times) / (
@@ -333,7 +338,8 @@ def _update_thumbnail(self):
             colormapped[coords[:, 1], coords[:, 0]] = colors
 
         colormapped[..., 3] *= self.opacity
-        self.thumbnail = colormapped
+        colormapped[np.isnan(colormapped)] = 0
+        self.thumbnail = colormapped.astype(np.uint8)
 
     @property
     def _view_data(self):
@@ -360,7 +366,7 @@ def _pad_display_data(self, vertices):
         return data[:, (2, 1, 0)]  # z, y, x -> x, y, z
 
     @property
-    def current_time(self):
+    def current_time(self) -> Optional[int]:
         """current time according to the first dimension"""
         # TODO(arl): get the correct index here
         time_step = self._data_slice.point[0]
@@ -384,7 +390,7 @@ def data(self) -> np.ndarray:
         return self._manager.data
 
     @data.setter
-    def data(self, data: np.ndarray):
+    def data(self, data: np.ndarray) -> None:
         """set the data and build the vispy arrays for display"""
         # set the data and build the tracks
         self._manager.data = data
@@ -406,7 +412,7 @@ def data(self, data: np.ndarray):
         self._reset_editable()
 
     @property
-    def features(self):
+    def features(self) -> pd.DataFrame:
         """Dataframe-like features table.
 
         It is an implementation detail that this is a `pandas.DataFrame`. In the future,
@@ -438,7 +444,7 @@ def properties(self) -> dict[str, np.ndarray]:
         return self._manager.properties
 
     @properties.setter
-    def properties(self, properties: dict[str, np.ndarray]):
+    def properties(self, properties: dict[str, np.ndarray]) -> None:
         """set track properties"""
         self.features = properties
 
@@ -453,7 +459,7 @@ def graph(self) -> Optional[dict[int, list[int]]]:
         return self._manager.graph
 
     @graph.setter
-    def graph(self, graph: dict[int, Union[int, list[int]]]):
+    def graph(self, graph: dict[int, Union[int, list[int]]]) -> None:
         """Set the track graph."""
         # Ignored type, because mypy can't handle different signatures
         # on getters and setters; see https://github.com/python/mypy/issues/3004
@@ -467,7 +473,7 @@ def tail_width(self) -> float:
         return self._tail_width
 
     @tail_width.setter
-    def tail_width(self, tail_width: float):
+    def tail_width(self, tail_width: float) -> None:
         self._tail_width: float = np.clip(tail_width, 0.5, self._max_width)
         self.events.tail_width()
 
@@ -477,7 +483,7 @@ def tail_length(self) -> int:
         return self._tail_length
 
     @tail_length.setter
-    def tail_length(self, tail_length: int):
+    def tail_length(self, tail_length: int) -> None:
         if tail_length > self._max_length:
             self._max_length = tail_length
         self._tail_length: int = tail_length
@@ -488,7 +494,7 @@ def head_length(self) -> int:
         return self._head_length
 
     @head_length.setter
-    def head_length(self, head_length: int):
+    def head_length(self, head_length: int) -> None:
         if head_length > self._max_length:
             self._max_length = head_length
         self._head_length: int = head_length
@@ -500,10 +506,12 @@ def display_id(self) -> bool:
         return self._display_id
 
     @display_id.setter
-    def display_id(self, value: bool):
+    def display_id(self, value: bool) -> None:
         self._display_id = value
         self.events.display_id()
-        self.refresh()
+        # TODO: this refresh is only here to trigger setting the id text...
+        #       a bit overkill? But maybe for a future PR.
+        self.refresh(extent=False, thumbnail=False)
 
     @property
     def display_tail(self) -> bool:
@@ -511,7 +519,7 @@ def display_tail(self) -> bool:
         return self._display_tail
 
     @display_tail.setter
-    def display_tail(self, value: bool):
+    def display_tail(self, value: bool) -> None:
         self._display_tail = value
         self.events.display_tail()
 
@@ -521,7 +529,7 @@ def display_graph(self) -> bool:
         return self._display_graph
 
     @display_graph.setter
-    def display_graph(self, value: bool):
+    def display_graph(self, value: bool) -> None:
         self._display_graph = value
         self.events.display_graph()
 
@@ -530,7 +538,7 @@ def color_by(self) -> str:
         return self._color_by
 
     @color_by.setter
-    def color_by(self, color_by: str):
+    def color_by(self, color_by: str) -> None:
         """set the property to color vertices by"""
         if color_by not in self.properties_to_color_by:
             raise ValueError(
@@ -549,7 +557,7 @@ def colormap(self) -> str:
         return self._colormap
 
     @colormap.setter
-    def colormap(self, colormap: str):
+    def colormap(self, colormap: str) -> None:
         """set the default colormap"""
         if colormap not in AVAILABLE_COLORMAPS:
             raise ValueError(
@@ -570,11 +578,11 @@ def colormaps_dict(self) -> dict[str, Colormap]:
     # Ignored type because mypy doesn't recognise colormaps_dict as a property
     # TODO: investigate and fix this - not sure why this is the case?
     @colormaps_dict.setter  # type: ignore[attr-defined]
-    def colomaps_dict(self, colormaps_dict: dict[str, Colormap]):
+    def colomaps_dict(self, colormaps_dict: dict[str, Colormap]) -> None:
         # validate the dictionary entries?
         self._colormaps_dict = colormaps_dict
 
-    def _recolor_tracks(self):
+    def _recolor_tracks(self) -> None:
         """recolor the tracks"""
 
         # this catch prevents a problem coloring the tracks if the data is
@@ -612,7 +620,7 @@ def track_colors(self) -> Optional[np.ndarray]:
         return self._track_colors
 
     @property
-    def graph_connex(self) -> np.ndarray:
+    def graph_connex(self) -> Optional[np.ndarray]:
         """vertex connections for drawing the graph"""
         return self._manager.graph_connex
 
@@ -629,6 +637,7 @@ def graph_times(self) -> Optional[np.ndarray]:
     @property
     def track_labels(self) -> tuple:
         """return track labels at the current time"""
+        assert self.current_time is not None
         labels, positions = self._manager.track_labels(self.current_time)
 
         # if there are no labels, return empty for vispy
@@ -638,7 +647,7 @@ def track_labels(self) -> tuple:
         padded_positions = self._pad_display_data(positions)
         return labels, padded_positions
 
-    def _check_color_by_in_features(self):
+    def _check_color_by_in_features(self) -> None:
         if self._color_by not in self.features.columns:
             warn(
                 (
diff --git a/napari/layers/utils/_tests/test_color_encoding.py b/napari/layers/utils/_tests/test_color_encoding.py
index ab13489e330..32494e0a6c1 100644
--- a/napari/layers/utils/_tests/test_color_encoding.py
+++ b/napari/layers/utils/_tests/test_color_encoding.py
@@ -16,7 +16,7 @@ def make_features_with_no_columns(*, num_rows) -> pd.DataFrame:
     return pd.DataFrame({}, index=range(num_rows))
 
 
-@pytest.fixture()
+@pytest.fixture
 def features() -> pd.DataFrame:
     return pd.DataFrame(
         {
diff --git a/napari/layers/utils/_tests/test_layer_utils.py b/napari/layers/utils/_tests/test_layer_utils.py
index 603abc1f3f6..e58647c2f58 100644
--- a/napari/layers/utils/_tests/test_layer_utils.py
+++ b/napari/layers/utils/_tests/test_layer_utils.py
@@ -64,7 +64,7 @@ def test_calc_data_range():
     clim = calc_data_range(data)
     np.testing.assert_array_equal(clim, (0, 2))
 
-    # Try large data mutlidimensional
+    # Try large data multidimensional
     data = np.zeros((3, 1000, 1000))
     data[0, 0, 0] = 0
     data[0, 0, 1] = 2
@@ -344,7 +344,7 @@ def test_feature_table_from_layer_with_properties_as_dataframe():
     pd.testing.assert_frame_equal(feature_table.values, TEST_FEATURES)
 
 
-@pytest.fixture()
+@pytest.fixture
 def feature_table():
     return _FeatureTable(TEST_FEATURES.copy(deep=True), num_data=4)
 
diff --git a/napari/layers/utils/_tests/test_plane.py b/napari/layers/utils/_tests/test_plane.py
index e567163a48b..5f2af4d0371 100644
--- a/napari/layers/utils/_tests/test_plane.py
+++ b/napari/layers/utils/_tests/test_plane.py
@@ -106,3 +106,18 @@ def test_clipping_plane_list_from_bounding_box():
     assert isinstance(plane_list, ClippingPlaneList)
     assert len(plane_list) == 6
     assert plane_list.as_array().sum() == 0  # everything is mirrored around 0
+
+
+def test_clipping_plane_list_add_plane():
+    plane_list = ClippingPlaneList()
+    plane_list.add_plane()
+    assert len(plane_list) == 1
+    assert plane_list[0].enabled
+
+    pos = (0, 0, 0)
+    norm = (0, 0, 1)
+    plane_list.add_plane(position=pos, normal=norm, enabled=False)
+    assert len(plane_list) == 2
+    assert not plane_list[1].enabled
+    assert plane_list[1].position == pos
+    assert plane_list[1].normal == norm
diff --git a/napari/layers/utils/_tests/test_stack_utils.py b/napari/layers/utils/_tests/test_stack_utils.py
index ee753051d36..5dcf9f896ba 100644
--- a/napari/layers/utils/_tests/test_stack_utils.py
+++ b/napari/layers/utils/_tests/test_stack_utils.py
@@ -4,7 +4,9 @@
 from napari.layers import Image
 from napari.layers.utils.stack_utils import (
     images_to_stack,
+    merge_rgb,
     split_channels,
+    split_rgb,
     stack_to_images,
 )
 from napari.utils.transforms import Affine
@@ -21,7 +23,7 @@ def test_stack_to_images_basic():
     assert len(images) == 2
 
     for i in images:
-        assert type(stack) == type(i)
+        assert type(stack) is type(i)
         assert i.data.shape == (10, 128, 128)
 
 
@@ -53,7 +55,7 @@ def test_stack_to_images_rgb():
     assert len(images) == 3
 
     for i in images:
-        assert type(stack) == type(i)
+        assert type(stack) is type(i)
         assert i.data.shape == (10, 128, 128)
         assert i.scale.shape == (3,)
         assert i.rgb is False
@@ -69,7 +71,7 @@ def test_stack_to_images_4_channels():
     assert len(images) == 4
     assert images[-2].colormap.name == 'red'
     for i in images:
-        assert type(stack) == type(i)
+        assert type(stack) is type(i)
         assert i.data.shape == (128, 128)
 
 
@@ -83,7 +85,7 @@ def test_stack_to_images_0_rgb():
     assert len(images) == 10
     for i in images:
         assert i.rgb
-        assert type(stack) == type(i)
+        assert type(stack) is type(i)
         assert i.data.shape == (128, 128, 3)
 
 
@@ -97,7 +99,7 @@ def test_stack_to_images_1_channel():
     assert len(images) == 1
     for i in images:
         assert i.rgb is False
-        assert type(stack) == type(i)
+        assert type(stack) is type(i)
         assert i.data.shape == (10, 128, 128)
 
 
@@ -138,6 +140,24 @@ def test_images_to_stack_none_scale():
     assert list(stack.translate) == [0, 0, -1, 2]
 
 
+def test_split_and_merge_rgb():
+    """Test merging 3 images with RGB colormaps into single RGB image."""
+    # Make an RGB
+    data = np.random.randint(0, 100, (10, 128, 128, 3))
+    stack = Image(data)
+    assert stack.rgb is True
+
+    # split the RGB into 3 images
+    images = split_rgb(stack)
+    assert len(images) == 3
+    colormaps = {image.colormap.name for image in images}
+    assert colormaps == {'red', 'green', 'blue'}
+
+    # merge the 3 images back into an RGB
+    rgb_image = merge_rgb(images)
+    assert rgb_image.rgb is True
+
+
 @pytest.fixture(
     params=[
         {
diff --git a/napari/layers/utils/_tests/test_string_encoding.py b/napari/layers/utils/_tests/test_string_encoding.py
index 391f248bcec..b79d6d83886 100644
--- a/napari/layers/utils/_tests/test_string_encoding.py
+++ b/napari/layers/utils/_tests/test_string_encoding.py
@@ -15,7 +15,7 @@ def make_features_with_no_columns(*, num_rows) -> pd.DataFrame:
     return pd.DataFrame({}, index=range(num_rows))
 
 
-@pytest.fixture()
+@pytest.fixture
 def features() -> pd.DataFrame:
     return pd.DataFrame(
         {
@@ -25,7 +25,7 @@ def features() -> pd.DataFrame:
     )
 
 
-@pytest.fixture()
+@pytest.fixture
 def numeric_features() -> pd.DataFrame:
     return pd.DataFrame(
         {
diff --git a/napari/layers/utils/_tests/test_style_encoding.py b/napari/layers/utils/_tests/test_style_encoding.py
index 8679773d0bb..2a4b2e123d1 100644
--- a/napari/layers/utils/_tests/test_style_encoding.py
+++ b/napari/layers/utils/_tests/test_style_encoding.py
@@ -23,7 +23,7 @@
 from napari.utils.events.custom_types import Array
 
 
-@pytest.fixture()
+@pytest.fixture
 def features() -> pd.DataFrame:
     return pd.DataFrame(
         {
diff --git a/napari/layers/utils/interactivity_utils.py b/napari/layers/utils/interactivity_utils.py
index 0653b1d8122..02679290b85 100644
--- a/napari/layers/utils/interactivity_utils.py
+++ b/napari/layers/utils/interactivity_utils.py
@@ -149,7 +149,7 @@ def orient_plane_normal_around_cursor(
         layer.plane.position = intersection
 
     # update plane normal
-    layer.plane.normal = layer._world_to_displayed_data_ray(
+    layer.plane.normal = layer._world_to_displayed_data_normal(
         np.asarray(plane_normal), dims_displayed=layer._slice_input.displayed
     )
 
diff --git a/napari/layers/utils/layer_utils.py b/napari/layers/utils/layer_utils.py
index 29084d069de..5464043e042 100644
--- a/napari/layers/utils/layer_utils.py
+++ b/napari/layers/utils/layer_utils.py
@@ -311,7 +311,7 @@ def segment_normal(a, b, p=(0, 0, 1)) -> np.ndarray:
     """
     d = b - a
 
-    norm: Any  # float or array or float, mypy has some difficulities.
+    norm: Any  # float or array or float, mypy has some difficulties.
 
     if d.ndim == 1:
         normal = np.array([d[1], -d[0]]) if len(d) == 2 else np.cross(d, p)
diff --git a/napari/layers/utils/plane.py b/napari/layers/utils/plane.py
index c4730b08160..07e356d9dee 100644
--- a/napari/layers/utils/plane.py
+++ b/napari/layers/utils/plane.py
@@ -236,3 +236,7 @@ def from_bounding_box(
                     )
                 )
         return cls(planes)
+
+    def add_plane(self, **kwargs: Any) -> None:
+        """Add a clipping plane to the list."""
+        self.append(ClippingPlane(**kwargs))
diff --git a/napari/layers/utils/stack_utils.py b/napari/layers/utils/stack_utils.py
index 996bb8cfa8a..7e5950321c0 100644
--- a/napari/layers/utils/stack_utils.py
+++ b/napari/layers/utils/stack_utils.py
@@ -286,15 +286,38 @@ def images_to_stack(images: list[Image], axis: int = 0, **kwargs) -> Image:
     if not images:
         raise IndexError(trans._('images list is empty', deferred=True))
 
+    if not all(isinstance(layer, Image) for layer in images):
+        non_image_layers = [
+            (layer.name, type(layer).__name__)
+            for layer in images
+            if not isinstance(layer, Image)
+        ]
+        raise ValueError(
+            trans._(
+                'All selected layers to be merged must be Image layers. '
+                'The following layers are not Image layers: '
+                f'{", ".join(f"{name} ({layer_type})" for name, layer_type in non_image_layers)}'
+            )
+        )
+
     data, meta, _ = images[0].as_layer_data_tuple()
 
-    kwargs.setdefault('scale', np.insert(meta['scale'], axis, 1))
-    kwargs.setdefault('translate', np.insert(meta['translate'], axis, 0))
+    # RGB images do not need extra dimensions inserted into metadata
+    if 'rgb' not in kwargs:
+        kwargs.setdefault('scale', np.insert(meta['scale'], axis, 1))
+        kwargs.setdefault('translate', np.insert(meta['translate'], axis, 0))
 
     meta.update(kwargs)
-    meta['units'] = (pint.get_application_registry().pixel,) + meta['units']
-    meta['axis_labels'] = (f'axis -{data.ndim + 1}',) + meta['axis_labels']
     new_data = np.stack([image.data for image in images], axis=axis)
+
+    # RGB images do not need extra dimensions inserted into metadata
+    # They can use the meta dict from one of the source image layers
+    if not meta['rgb']:
+        meta['units'] = (pint.get_application_registry().pixel,) + meta[
+            'units'
+        ]
+        meta['axis_labels'] = (f'axis -{data.ndim + 1}',) + meta['axis_labels']
+
     return Image(new_data, **meta)
 
 
@@ -302,6 +325,38 @@ def merge_rgb(images: list[Image]) -> Image:
     """Variant of images_to_stack that makes an RGB from 3 images."""
     if not (len(images) == 3 and all(isinstance(x, Image) for x in images)):
         raise ValueError(
-            trans._('merge_rgb requires 3 images layers', deferred=True)
+            trans._(
+                'Merging to RGB requires exactly 3 Image layers', deferred=True
+            )
+        )
+    if not all(image.data.shape == images[0].data.shape for image in images):
+        all_shapes = [(image.name, image.data.shape) for image in images]
+        raise ValueError(
+            trans._(
+                'Shape mismatch! To merge to RGB, all selected Image layers (with R, G, and B colormaps) must have the same shape. '
+                'Mismatched shapes: '
+                f'{", ".join(f"{name} (shape: {shape})" for name, shape in all_shapes)}'
+            )
+        )
+
+    # we will check for the presence of R G B colormaps to determine how to merge
+    colormaps = {image.colormap.name for image in images}
+    r_g_b = ['red', 'green', 'blue']
+    if colormaps != set(r_g_b):
+        missing_colormaps = set(r_g_b) - colormaps
+        raise ValueError(
+            trans._(
+                'Missing colormap(s): {missing_colormaps}! To merge layers to RGB, ensure you have red, green, and blue as layer colormaps.',
+                missing_colormaps=missing_colormaps,
+                deferred=True,
+            )
         )
-    return images_to_stack(images, axis=-1, rgb=True)
+
+    # use the R G B colormaps to order the images for merging
+    imgs = [
+        image
+        for color in r_g_b
+        for image in images
+        if image.colormap.name == color
+    ]
+    return images_to_stack(imgs, axis=-1, rgb=True)
diff --git a/napari/layers/vectors/_vector_utils.py b/napari/layers/vectors/_vector_utils.py
index 2f660293ecd..4da2a91459f 100644
--- a/napari/layers/vectors/_vector_utils.py
+++ b/napari/layers/vectors/_vector_utils.py
@@ -108,5 +108,4 @@ def fix_data_vectors(
                 ndim=ndim,
             )
         )
-    ndim = data_ndim
-    return vectors, ndim
+    return vectors, data_ndim
diff --git a/napari/layers/vectors/vectors.py b/napari/layers/vectors/vectors.py
index ed14c8d7cdb..8f77b36ff6b 100644
--- a/napari/layers/vectors/vectors.py
+++ b/napari/layers/vectors/vectors.py
@@ -477,7 +477,7 @@ def out_of_slice_display(self) -> bool:
     def out_of_slice_display(self, out_of_slice_display: bool) -> None:
         self._out_of_slice_display = out_of_slice_display
         self.events.out_of_slice_display()
-        self.refresh()
+        self.refresh(extent=False)
 
     @property
     def edge_width(self) -> float:
@@ -489,7 +489,7 @@ def edge_width(self, edge_width: float):
         self._edge_width = edge_width
 
         self.events.edge_width()
-        self.refresh()
+        self.refresh(extent=False)
 
     @property
     def vector_style(self) -> str:
@@ -510,7 +510,7 @@ def vector_style(self, vector_style: str):
         self._vector_style = VectorStyle(vector_style)
         if self._vector_style != old_vector_style:
             self.events.vector_style()
-            self.refresh()
+            self.refresh(extent=False, thumbnail=False)
 
     @property
     def length(self) -> float:
diff --git a/napari/plugins/__init__.py b/napari/plugins/__init__.py
index 31eafdb4716..d6ca99be37c 100644
--- a/napari/plugins/__init__.py
+++ b/napari/plugins/__init__.py
@@ -8,7 +8,7 @@
 from napari.plugins._plugin_manager import NapariPluginManager
 from napari.settings import get_settings
 
-__all__ = ('plugin_manager', 'menu_item_template')
+__all__ = ('menu_item_template', 'plugin_manager')
 
 from napari.utils.theme import _install_npe2_themes
 
diff --git a/napari/plugins/_npe2.py b/napari/plugins/_npe2.py
index 48d1f65dc0c..7f45d9c4dc9 100644
--- a/napari/plugins/_npe2.py
+++ b/napari/plugins/_npe2.py
@@ -329,7 +329,11 @@ def on_plugins_registered(manifests: set[PluginManifest]):
 
     'Registered' means that a manifest has been provided or discovered.
     """
-    for mf in manifests:
+    sorted_manifests = sorted(
+        manifests,
+        key=lambda mf: mf.display_name if mf.display_name else mf.name,
+    )
+    for mf in sorted_manifests:
         if not pm.is_disabled(mf.name):
             _register_manifest_actions(mf)
             _safe_register_qt_actions(mf)
@@ -341,9 +345,9 @@ def _register_manifest_actions(mf: PluginManifest) -> None:
     This is called when a plugin is registered or enabled and it adds the
     plugin's menus and submenus to the app model registry.
     """
-    from napari._app_model import get_app
+    from napari._app_model import get_app_model
 
-    app = get_app()
+    app = get_app_model()
     actions, submenus = _npe2_manifest_to_actions(mf)
 
     context = pm.get_context(cast('PluginName', mf.name))
@@ -397,7 +401,7 @@ def _npe2_manifest_to_actions(
     # Filter widgets as are registered via `_safe_register_qt_actions`
     widget_ids = {widget.command for widget in mf.contributions.widgets or ()}
 
-    # We want to register all `Actions` so they appear in the command pallete
+    # We want to register all `Actions` so they appear in the command palette
     actions: list[Action] = []
     for cmd in mf.contributions.commands or ():
         if cmd.id not in sample_data_ids | widget_ids:
diff --git a/napari/plugins/_plugin_manager.py b/napari/plugins/_plugin_manager.py
index b1ecc78a9ed..8218fda1569 100644
--- a/napari/plugins/_plugin_manager.py
+++ b/napari/plugins/_plugin_manager.py
@@ -150,7 +150,7 @@ def unregister(
             self._theme_data,
             self._function_widgets,
         ):
-            _dict.pop(_name, None)  # type: ignore
+            _dict.pop(_name, None)
 
         self.events.unregistered(value=_name)
 
diff --git a/napari/plugins/_tests/test_npe2.py b/napari/plugins/_tests/test_npe2.py
index 9771b376412..8c79728edcc 100644
--- a/napari/plugins/_tests/test_npe2.py
+++ b/napari/plugins/_tests/test_npe2.py
@@ -19,7 +19,7 @@
 MANIFEST_PATH = Path(__file__).parent / '_sample_manifest.yaml'
 
 
-@pytest.fixture()
+@pytest.fixture
 def mock_pm(npe2pm: 'TestPluginManager'):
     from napari.plugins import _initialize_plugins
 
@@ -177,11 +177,11 @@ def test_widget_iterator(mock_pm):
     assert wdgs == [('dock', (PLUGIN_NAME, ['My Widget']))]
 
 
-def test_plugin_actions(mock_pm: 'TestPluginManager', mock_app):
-    from napari._app_model import get_app
+def test_plugin_actions(mock_pm: 'TestPluginManager', mock_app_model):
+    from napari._app_model import get_app_model
     from napari.plugins import _initialize_plugins
 
-    app = get_app()
+    app = get_app_model()
     # nothing yet registered with this menu
     assert 'napari/file/new_layer' not in app.menus
     # menus_items1 = list(app.menus.get_menu('napari/file/new_layer'))
diff --git a/napari/plugins/_tests/test_utils.py b/napari/plugins/_tests/test_utils.py
index baaba640231..a8f4086367c 100644
--- a/napari/plugins/_tests/test_utils.py
+++ b/napari/plugins/_tests/test_utils.py
@@ -1,3 +1,5 @@
+import os.path
+
 from npe2 import DynamicPlugin
 
 from napari.plugins.utils import (
@@ -79,7 +81,7 @@ def test_get_preferred_reader_more_nested():
 def test_get_preferred_reader_abs_path():
     get_settings().plugins.extension2reader = {
         # abs path so highest specificity
-        '/asdf/*.tif': 'most-specific-plugin',
+        os.path.realpath('/asdf/*.tif'): 'most-specific-plugin',
         # less nested so less specificity
         '*.tif': 'generic-tif-plugin',
         # more nested so higher specificity
@@ -171,6 +173,17 @@ def test_get_preferred_reader_no_extension():
     assert get_preferred_reader('my_file') is None
 
 
+def test_get_preferred_reader_full_path(tmp_path, monkeypatch):
+    (tmp_path / 'my_file.zarr').mkdir()
+    zarr_path = str(tmp_path / 'my_file.zarr')
+
+    assert get_preferred_reader(zarr_path) is None
+    get_settings().plugins.extension2reader[f'{zarr_path}/'] = 'fake-plugin'
+    assert get_preferred_reader(zarr_path) == 'fake-plugin'
+    monkeypatch.chdir(tmp_path)
+    assert get_preferred_reader('./my_file.zarr') == 'fake-plugin'
+
+
 def test_get_potential_readers_gives_napari(
     builtins, tmp_plugin: DynamicPlugin
 ):
diff --git a/napari/plugins/io.py b/napari/plugins/io.py
index a7437a1c1df..99f1f5f40a9 100644
--- a/napari/plugins/io.py
+++ b/napari/plugins/io.py
@@ -77,8 +77,8 @@ def read_data_with_plugins(
 
     res = _npe2.read(paths, plugin, stack=stack)
     if res is not None:
-        _ld, hookimpl = res
-        return [] if _is_null_layer_sentinel(_ld) else _ld, hookimpl
+        ld_, hookimpl = res
+        return [] if _is_null_layer_sentinel(ld_) else list(ld_), hookimpl
 
     hook_caller = plugin_manager.hook.napari_get_reader
     paths = [abspath_or_url(p, must_exist=True) for p in paths]
diff --git a/napari/plugins/utils.py b/napari/plugins/utils.py
index fc8600b85fc..2704aaf3ffc 100644
--- a/napari/plugins/utils.py
+++ b/napari/plugins/utils.py
@@ -84,7 +84,7 @@ def _get_preferred_readers(path: PathLike) -> list[tuple[str, str]]:
     filtered_preferences : List[Tuple[str, str]]
         Filtered patterns and their corresponding readers.
     """
-    path = str(path)
+    path = os.path.realpath(str(path))
 
     if osp.isdir(path) and not path.endswith(os.sep):
         path = path + os.sep
diff --git a/napari/qt/__init__.py b/napari/qt/__init__.py
index 00dcbbd72dc..301d9fbc5bf 100644
--- a/napari/qt/__init__.py
+++ b/napari/qt/__init__.py
@@ -1,4 +1,4 @@
-from napari._qt.qt_event_loop import get_app, run
+from napari._qt.qt_event_loop import get_app, get_qapp, run
 from napari._qt.qt_main_window import Window
 from napari._qt.qt_resources import get_current_stylesheet, get_stylesheet
 from napari._qt.qt_viewer import QtViewer
@@ -7,14 +7,15 @@
 from napari.qt.threading import create_worker, thread_worker
 
 __all__ = (
-    'create_worker',
     'QtToolTipLabel',
     'QtViewer',
     'QtViewerButtons',
-    'thread_worker',
     'Window',
+    'create_worker',
     'get_app',
-    'get_stylesheet',
     'get_current_stylesheet',
+    'get_qapp',
+    'get_stylesheet',
     'run',
+    'thread_worker',
 )
diff --git a/napari/qt/threading.py b/napari/qt/threading.py
index b327f82dadb..784aa58d600 100644
--- a/napari/qt/threading.py
+++ b/napari/qt/threading.py
@@ -13,11 +13,11 @@
 
 # all of these might be used by an end-user when subclassing
 __all__ = (
-    'create_worker',
     'FunctionWorker',
     'GeneratorWorker',
     'GeneratorWorkerSignals',
-    'thread_worker',
     'WorkerBase',
     'WorkerBaseSignals',
+    'create_worker',
+    'thread_worker',
 )
diff --git a/napari/resources/__init__.py b/napari/resources/__init__.py
index c7fa45089fe..4227a3d7046 100644
--- a/napari/resources/__init__.py
+++ b/napari/resources/__init__.py
@@ -7,9 +7,9 @@
 )
 
 __all__ = [
-    'get_colorized_svg',
-    'get_icon_path',
-    'ICON_PATH',
     'ICONS',
+    'ICON_PATH',
     'LOADING_GIF_PATH',
+    'get_colorized_svg',
+    'get_icon_path',
 ]
diff --git a/napari/resources/multichannel_cells.png b/napari/resources/multichannel_cells.png
new file mode 100644
index 00000000000..a6e2b792af3
Binary files /dev/null and b/napari/resources/multichannel_cells.png differ
diff --git a/napari/settings/__init__.py b/napari/settings/__init__.py
index dffd8d63bcb..0ce1406f6fb 100644
--- a/napari/settings/__init__.py
+++ b/napari/settings/__init__.py
@@ -8,7 +8,7 @@
 )
 from napari.utils.translations import trans
 
-__all__ = ['NapariSettings', 'get_settings', 'CURRENT_SCHEMA_VERSION']
+__all__ = ['CURRENT_SCHEMA_VERSION', 'NapariSettings', 'get_settings']
 
 
 class _SettingsProxy:
diff --git a/napari/settings/_appearance.py b/napari/settings/_appearance.py
index 81957f4782f..bd696ffe11f 100644
--- a/napari/settings/_appearance.py
+++ b/napari/settings/_appearance.py
@@ -52,6 +52,13 @@ class AppearanceSettings(EventedModel):
         title=trans._('Show layer tooltips'),
         description=trans._('Toggle to display a tooltip on mouse hover.'),
     )
+    update_status_based_on_layer: bool = Field(
+        True,
+        title=trans._('Update status based on layer'),
+        description=trans._(
+            'Calculate status bar based on current active layer and mose position.'
+        ),
+    )
 
     def update(
         self, values: Union['EventedModel', dict], recurse: bool = True
diff --git a/napari/settings/_application.py b/napari/settings/_application.py
index aea3bd7ad26..e966d1badae 100644
--- a/napari/settings/_application.py
+++ b/napari/settings/_application.py
@@ -213,6 +213,14 @@ class ApplicationSettings(EventedModel):
         ),
     )
 
+    plugin_widget_positions: dict[str, str] = Field(
+        default={},
+        title=trans._('Plugin widget positions'),
+        description=trans._(
+            'Per-widget last saved position of plugin dock widgets. This setting is managed by the application.'
+        ),
+    )
+
     @validator('window_state', allow_reuse=True)
     def _validate_qbtye(cls, v: str) -> str:
         if v and (not isinstance(v, str) or not v.startswith('!QBYTE_')):
@@ -239,4 +247,5 @@ class NapariConfig:
             'open_history',
             'save_history',
             'ipy_interactive',
+            'plugin_widget_positions',
         )
diff --git a/napari/settings/_base.py b/napari/settings/_base.py
index c4c4d22a4c5..cc511cdf003 100644
--- a/napari/settings/_base.py
+++ b/napari/settings/_base.py
@@ -134,7 +134,7 @@ def dict(
         include: Union[AbstractSetIntStr, MappingIntStrAny] = None,  # type: ignore
         exclude: Union[AbstractSetIntStr, MappingIntStrAny] = None,  # type: ignore
         by_alias: bool = False,
-        exclude_unset: bool = False,  # type: ignore [override]  # deprecated parameter
+        exclude_unset: bool = False,
         exclude_defaults: bool = False,
         exclude_none: bool = False,
         exclude_env: bool = False,
@@ -320,31 +320,33 @@ def _inner(settings: BaseSettings) -> dict[str, Any]:
                     # otherwise, look for the standard nested env var
                     else:
                         env_val = env_vars.get(f'{env_name}_{subf.name}')
-                        if env_val is not None:
-                            break
 
-                is_complex, all_json_fail = super_eset.field_is_complex(subf)
-                if env_val is not None and is_complex:
-                    try:
-                        env_val = settings.__config__.json_loads(env_val)
-                    except ValueError as e:
-                        if not all_json_fail:
-                            msg = trans._(
-                                'error parsing JSON for "{env_name}"',
-                                deferred=True,
-                                env_name=env_name,
+                    is_complex, all_json_fail = super_eset.field_is_complex(
+                        subf
+                    )
+                    if env_val is not None and is_complex:
+                        try:
+                            env_val = settings.__config__.json_loads(env_val)
+                        except ValueError as e:
+                            if not all_json_fail:
+                                msg = trans._(
+                                    'error parsing JSON for "{env_name}"',
+                                    deferred=True,
+                                    env_name=env_name,
+                                )
+                                raise SettingsError(msg) from e
+
+                        if isinstance(env_val, dict):
+                            explode = super_eset.explode_env_vars(
+                                field, env_vars
                             )
-                            raise SettingsError(msg) from e
-
-                    if isinstance(env_val, dict):
-                        explode = super_eset.explode_env_vars(field, env_vars)
-                        env_val = deep_update(env_val, explode)
+                            env_val = deep_update(env_val, explode)
 
-                # if we found an env var, store it and return it
-                if env_val is not None:
-                    if field.alias not in d:
-                        d[field.alias] = {}
-                    d[field.alias][subf.name] = env_val
+                    # if we found an env var, store it and return it
+                    if env_val is not None:
+                        if field.alias not in d:
+                            d[field.alias] = {}
+                        d[field.alias][subf.name] = env_val
         return d
 
     return _inner
diff --git a/napari/settings/_tests/test_migrations.py b/napari/settings/_tests/test_migrations.py
index 9cf7eb660d7..df7f873ab86 100644
--- a/napari/settings/_tests/test_migrations.py
+++ b/napari/settings/_tests/test_migrations.py
@@ -8,7 +8,7 @@
 from napari.settings import NapariSettings, _migrations
 
 
-@pytest.fixture()
+@pytest.fixture
 def test_migrator(monkeypatch):
     # this fixture makes sure we're not using _migrations.MIGRATORS for tests
     # but rather only using migrators that get declared IN the test
diff --git a/napari/settings/_tests/test_settings.py b/napari/settings/_tests/test_settings.py
index 9d65c16448c..bad8b197c23 100644
--- a/napari/settings/_tests/test_settings.py
+++ b/napari/settings/_tests/test_settings.py
@@ -12,7 +12,7 @@
 from napari.utils.theme import get_theme, register_theme
 
 
-@pytest.fixture()
+@pytest.fixture
 def test_settings(tmp_path):
     """A fixture that can be used to test and save settings"""
     from napari.settings import NapariSettings
@@ -219,6 +219,20 @@ def test_settings_env_variables(monkeypatch):
     monkeypatch.setenv('NAPARI_PLUGINS_EXTENSION2READER', '{"*.zarr": "hi"}')
     assert NapariSettings(None).plugins.extension2reader == {'*.zarr': 'hi'}
 
+    # can also use short `env` name for EventedSettings class
+    assert NapariSettings(None).experimental.async_ is False
+    monkeypatch.setenv('NAPARI_ASYNC', '1')
+    assert NapariSettings(None).experimental.async_ is True
+
+
+def test_two_env_variable_settings(monkeypatch):
+    assert NapariSettings(None).experimental.async_ is False
+    assert NapariSettings(None).experimental.autoswap_buffers is False
+    monkeypatch.setenv('NAPARI_EXPERIMENTAL_ASYNC_', '1')
+    monkeypatch.setenv('NAPARI_EXPERIMENTAL_AUTOSWAP_BUFFERS', '1')
+    assert NapariSettings(None).experimental.async_ is True
+    assert NapariSettings(None).experimental.autoswap_buffers is True
+
 
 def test_settings_env_variables_fails(monkeypatch):
     monkeypatch.setenv('NAPARI_APPEARANCE_THEME', 'FOOBAR')
@@ -264,7 +278,7 @@ def test_settings_env_variables_do_not_write_to_disk(tmp_path, monkeypatch):
     assert settings.env_settings()['appearance']['theme'] == 'dark'
 
     # when we save it shouldn't use environment variables and it shouldn't
-    # have overriden our non-default value of `theme: light`
+    # have overridden our non-default value of `theme: light`
     settings.save()
     disk_settings = fake_path.read_text()
     assert 'theme: light' in disk_settings
@@ -274,6 +288,29 @@ def test_settings_env_variables_do_not_write_to_disk(tmp_path, monkeypatch):
     assert NapariSettings(fake_path).appearance.theme == 'light'
 
 
+def test_settings_env_variables_override_file(tmp_path, monkeypatch):
+    # create a settings file with async_ = true
+    data = 'experimental:\n   async_: true\n   autoswap_buffers: true'
+    fake_path = tmp_path / 'fake_path.yml'
+    fake_path.write_text(data)
+
+    # make sure they wrote correctly
+    disk_settings = fake_path.read_text()
+    assert 'async_: true' in disk_settings
+    assert 'autoswap_buffers: true' in disk_settings
+    # make sure they load correctly
+    assert NapariSettings(fake_path).experimental.async_ is True
+    assert NapariSettings(fake_path).experimental.autoswap_buffers is True
+
+    # now load settings again with an Env-var override
+    monkeypatch.setenv('NAPARI_ASYNC', '0')
+    monkeypatch.setenv('NAPARI_AUTOSWAP', '0')
+    settings = NapariSettings(fake_path)
+    # make sure the override worked, and save again
+    assert settings.experimental.async_ is False
+    assert settings.experimental.autoswap_buffers is False
+
+
 def test_settings_only_saves_non_default_values(monkeypatch, tmp_path):
     from yaml import safe_load
 
diff --git a/napari/types.py b/napari/types.py
index dd848a0c645..fd62a2a50b7 100644
--- a/napari/types.py
+++ b/napari/types.py
@@ -1,4 +1,4 @@
-from collections.abc import Iterable, Sequence
+from collections.abc import Iterable, Mapping, Sequence
 from functools import partial, wraps
 from pathlib import Path
 from types import TracebackType
@@ -28,28 +28,28 @@
 
 
 __all__ = [
+    'ArrayBase',
     'ArrayLike',
-    'LayerTypeName',
+    'AugmentedWidget',
+    'ExcInfo',
     'FullLayerData',
+    'ImageData',
+    'LabelsData',
     'LayerData',
+    'LayerDataTuple',
+    'LayerTypeName',
     'PathLike',
     'PathOrPaths',
+    'PointsData',
     'ReaderFunction',
-    'WriterFunction',
-    'ExcInfo',
-    'WidgetCallable',
-    'AugmentedWidget',
     'SampleData',
     'SampleDict',
-    'ArrayBase',
-    'ImageData',
-    'LabelsData',
-    'PointsData',
     'ShapesData',
     'SurfaceData',
     'TracksData',
     'VectorsData',
-    'LayerDataTuple',
+    'WidgetCallable',
+    'WriterFunction',
     'image_reader_to_layerdata_reader',
 ]
 
@@ -63,8 +63,8 @@
 
 # layer data may be: (data,) (data, meta), or (data, meta, layer_type)
 # using "Any" for the data type until ArrayLike is more mature.
-FullLayerData = tuple[Any, dict, LayerTypeName]
-LayerData = Union[tuple[Any], tuple[Any, dict], FullLayerData]
+FullLayerData = tuple[Any, Mapping, LayerTypeName]
+LayerData = Union[tuple[Any], tuple[Any, Mapping], FullLayerData]
 
 PathLike = Union[str, Path]
 PathOrPaths = Union[PathLike, Sequence[PathLike]]
diff --git a/napari/utils/__init__.py b/napari/utils/__init__.py
index 266f00ae8b1..99ba6f4ff31 100644
--- a/napari/utils/__init__.py
+++ b/napari/utils/__init__.py
@@ -1,3 +1,4 @@
+from napari._check_numpy_version import NUMPY_VERSION_IS_THREADSAFE
 from napari.utils._dask_utils import resize_dask_cache
 from napari.utils.colormaps.colormap import (
     Colormap,
@@ -12,13 +13,14 @@
 from napari.utils.progress import cancelable_progress, progrange, progress
 
 __all__ = (
+    'NUMPY_VERSION_IS_THREADSAFE',
     'Colormap',
-    'DirectLabelColormap',
     'CyclicLabelColormap',
+    'DirectLabelColormap',
+    'NotebookScreenshot',
     'cancelable_progress',
     'citation_text',
     'nbscreenshot',
-    'NotebookScreenshot',
     'progrange',
     'progress',
     'resize_dask_cache',
diff --git a/napari/utils/_tests/test_action_manager.py b/napari/utils/_tests/test_action_manager.py
index cbf2a29cbb2..443e2ce524a 100644
--- a/napari/utils/_tests/test_action_manager.py
+++ b/napari/utils/_tests/test_action_manager.py
@@ -9,7 +9,7 @@
 from napari.utils.action_manager import ActionManager
 
 
-@pytest.fixture()
+@pytest.fixture
 def action_manager():
     """
     Unlike normal napari we use a different instance we have complete control
diff --git a/napari/utils/_tests/test_io.py b/napari/utils/_tests/test_io.py
index ca5f3d05c36..c30e3c39713 100644
--- a/napari/utils/_tests/test_io.py
+++ b/napari/utils/_tests/test_io.py
@@ -1,10 +1,14 @@
+import struct
+
 import numpy as np
 import pytest
+import tifffile
 from imageio.v3 import imread
 
 from napari.utils.io import imsave
 
 
+@pytest.mark.slow
 @pytest.mark.parametrize(
     'image_file', ['image', 'image.png', 'image.tif', 'image.bmp']
 )
@@ -67,3 +71,41 @@ def test_imsave_float(tmp_path, image_file):
 
     else:
         assert not image_file_path.is_file()
+
+
+def test_imsave_large_file(monkeypatch, tmp_path):
+    """Test saving a bigtiff file.
+
+    In napari IO, we use compression when saving to tiff. bigtiff mode
+    is required when data size *after compression* is over 4GB. However,
+    bigtiff is not as broadly implemented as normal tiff, so we don't want to
+    always use that flag. Therefore, in our internal code, we catch the error
+    raised when trying to write a too-large TIFF file, then rewrite using
+    bigtiff. This test checks that the mechanism works correctly.
+
+    Generating 4GB+ of uncompressible data is expensive, so here we:
+
+    1. Generate a smaller amount of "random" data using np.empty.
+    2. Monkeypatch tifffile's save routine to raise an error *as if* bigtiff
+       was required for this small dataset.
+    3. This triggers saving as bigtiff.
+    4. We check that the file was correctly saved as bigtiff.
+    """
+    old_write = tifffile.imwrite
+
+    def raise_no_bigtiff(*args, **kwargs):
+        if 'bigtiff' not in kwargs:
+            raise struct.error
+        old_write(*args, **kwargs)
+
+    monkeypatch.setattr(tifffile, 'imwrite', raise_no_bigtiff)
+
+    # create image data. It can be <4GB compressed because we
+    # monkeypatched tifffile.imwrite to raise an error if bigtiff is not set
+    data = np.empty((20, 200, 200), dtype='uint16')
+
+    # create image and assert image file creation
+    image_path = str(tmp_path / 'data.tif')
+    imsave(image_path, data)
+    with tifffile.TiffFile(image_path) as tiff:
+        assert tiff.is_bigtiff
diff --git a/napari/utils/_tests/test_key_bindings.py b/napari/utils/_tests/test_key_bindings.py
index c0b125a28e9..0e98b560834 100644
--- a/napari/utils/_tests/test_key_bindings.py
+++ b/napari/utils/_tests/test_key_bindings.py
@@ -17,6 +17,7 @@
 )
 
 
+@pytest.mark.key_bindings
 def test_bind_key():
     kb = {}
 
@@ -61,6 +62,7 @@ def spam():
     assert key == KeyBinding.from_str('Shift-A')
 
 
+@pytest.mark.key_bindings
 def test_bind_key_decorator():
     kb = {}
 
@@ -70,6 +72,7 @@ def foo(): ...
     assert kb == {KeyBinding.from_str('A'): foo}
 
 
+@pytest.mark.key_bindings
 def test_keymap_provider():
     class Foo(KeymapProvider): ...
 
@@ -89,6 +92,7 @@ class Baz(KeymapProvider):
     assert Baz.class_keymap == {KeyBinding.from_str('A'): ...}
 
 
+@pytest.mark.key_bindings
 def test_bind_keymap():
     class Foo: ...
 
@@ -133,6 +137,7 @@ class Baz(Bar):
     class_keymap = {'F': lambda x: setattr(x, 'F', 16)}
 
 
+@pytest.mark.key_bindings
 def test_handle_single_keymap_provider():
     foo = Foo()
 
@@ -185,6 +190,7 @@ def test_handle_single_keymap_provider():
     assert not hasattr(foo, 'C')
 
 
+@pytest.mark.key_bindings
 @patch('napari.utils.key_bindings.USER_KEYMAP', new_callable=dict)
 def test_bind_user_key(keymap_mock):
     foo = Foo()
@@ -217,6 +223,7 @@ def abc():
     assert x == 42
 
 
+@pytest.mark.key_bindings
 def test_handle_multiple_keymap_providers():
     foo = Foo()
     bar = Bar()
@@ -281,6 +288,7 @@ def catch_all(x):
     assert not hasattr(foo, 'B')
 
 
+@pytest.mark.key_bindings
 def test_inherited_keymap():
     baz = Baz()
     handler = KeymapHandler()
@@ -302,6 +310,7 @@ def test_inherited_keymap():
     }
 
 
+@pytest.mark.key_bindings
 def test_handle_on_release_bindings():
     def make_42(x):
         # on press
@@ -348,6 +357,7 @@ class Baz(KeymapProvider):
     assert baz.aliiiens == 0
 
 
+@pytest.mark.key_bindings
 def test_bind_key_method():
     class Foo2(KeymapProvider): ...
 
@@ -365,6 +375,7 @@ def bar():
     assert Foo2.class_keymap[KeyBinding.from_str('B')] is bar
 
 
+@pytest.mark.key_bindings
 def test_bind_key_doc():
     doc = inspect.getdoc(bind_key)
     doc = doc.split('Notes\n-----\n')[-1]
diff --git a/napari/utils/_tests/test_progress.py b/napari/utils/_tests/test_progress.py
index 9ba7da1cea0..b7050ff7314 100644
--- a/napari/utils/_tests/test_progress.py
+++ b/napari/utils/_tests/test_progress.py
@@ -95,7 +95,7 @@ def test_progress_set_disable():
     pbr = progress(
         total=5, disable=True, desc='This description will not be set by tqdm.'
     )
-    # make sure the dummy desscription (empty string) was set
+    # make sure the dummy description (empty string) was set
     assert pbr.desc == 'progress: '
     pbr.close()
 
diff --git a/napari/utils/_tests/test_proxies.py b/napari/utils/_tests/test_proxies.py
index 4454b0336b3..00c456f5021 100644
--- a/napari/utils/_tests/test_proxies.py
+++ b/napari/utils/_tests/test_proxies.py
@@ -31,7 +31,7 @@ class TestClass:
         tc_read_only.x = 5
 
 
-@pytest.fixture()
+@pytest.fixture
 def _patched_root_dir():
     """Simulate a call from outside of napari"""
     with patch('napari.utils.misc.ROOT_DIR', new='/some/other/package'):
diff --git a/napari/utils/_tests/test_theme.py b/napari/utils/_tests/test_theme.py
index b2003698518..9f2b7d644a6 100644
--- a/napari/utils/_tests/test_theme.py
+++ b/napari/utils/_tests/test_theme.py
@@ -1,6 +1,3 @@
-import os
-import sys
-
 import pytest
 from npe2 import PluginManager, PluginManifest, __version__ as npe2_version
 from npe2.manifest.schema import ContributionPoints
@@ -111,11 +108,6 @@ def test_rebuild_theme_settings():
     settings.appearance.theme = 'another-theme'
 
 
-@pytest.mark.skipif(
-    os.getenv('CI') and sys.version_info < (3, 9),
-    reason='Testing theme on CI is extremely slow ~ 15s per test.'
-    'Skip for now until we find the reason',
-)
 @pytest.mark.parametrize(
     'color',
     [
@@ -132,11 +124,6 @@ def test_theme(color):
     theme.background = color
 
 
-@pytest.mark.skipif(
-    os.getenv('CI') and sys.version_info < (3, 9),
-    reason='Testing theme on CI is extremely slow ~ 15s per test.'
-    'Skip for now until we find the reason',
-)
 def test_theme_font_size():
     theme = get_theme('dark')
     theme.font_size = '15pt'
diff --git a/napari/utils/_tests/test_translations.py b/napari/utils/_tests/test_translations.py
index 884237323bf..00fcdac9e76 100644
--- a/napari/utils/_tests/test_translations.py
+++ b/napari/utils/_tests/test_translations.py
@@ -81,7 +81,7 @@
 es_CO_mo = b'\xde\x12\x04\x95\x00\x00\x00\x00\t\x00\x00\x00\x1c\x00\x00\x00d\x00\x00\x00\r\x00\x00\x00\xac\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x1c\x00\x00\x00\xe1\x00\x00\x00D\x00\x00\x00\xfe\x00\x00\x00\x11\x00\x00\x00C\x01\x00\x00!\x00\x00\x00U\x01\x00\x00E\x00\x00\x00w\x01\x00\x00u\x00\x00\x00\xbd\x01\x00\x00/\x00\x00\x003\x02\x00\x00H\x00\x00\x00c\x02\x00\x00\x0e\x01\x00\x00\xac\x02\x00\x00\x1a\x00\x00\x00\xbb\x03\x00\x00@\x00\x00\x00\xd6\x03\x00\x00\x11\x00\x00\x00\x17\x04\x00\x00 \x00\x00\x00)\x04\x00\x004\x00\x00\x00J\x04\x00\x00V\x00\x00\x00\x7f\x04\x00\x00\x1e\x00\x00\x00\xd6\x04\x00\x00+\x00\x00\x00\xf5\x04\x00\x00\x01\x00\x00\x00\t\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x03\x00\x00\x00\x08\x00\x00\x00\x06\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x02\x00\x00\x00\x00I have napari\x00I have naparis\x00I have {n} napari with {variable}\x00I have {n} naparis with {variable}\x00More about napari\x00More about napari with {variable}\x00plural-context\x04I have napari with context\x00I have naparis with context\x00plural-context-variables\x04I have {n} napari with {variable} and context\x00I have {n} naparis with {variable} and context\x00singular-context\x04More about napari with context\x00singular-context-variables\x04More about napari with context and {variable}\x00Project-Id-Version: \nPO-Revision-Date: 2021-04-08 08:14-0500\nLanguage-Team: \nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\nX-Generator: Poedit 2.4.2\nLast-Translator: \nPlural-Forms: nplurals=2; plural=(n != 1);\nLanguage: es_CO\n\x00Tengo napari\x00Tengo naparis\x00Tengo {n} napari con {variable}\x00Tengo {n} naparis con {variable}\x00M\xc3\xa1s sobre napari\x00M\xc3\xa1s sobre napari con {variable}\x00Tengo napari con contexto\x00Tengo naparis con contexto\x00Tengo {n} napari con {variable} y contexto\x00Tengo {n} naparis con {variable} y contexto\x00M\xc3\xa1s sobre napari con contexto\x00M\xc3\xa1s sobre napari con contexto y {variable}\x00'
 
 
-@pytest.fixture()
+@pytest.fixture
 def trans(tmp_path):
     """A good plugin that uses entry points."""
     distinfo = tmp_path / 'napari_language_pack_es_CO-0.1.0.dist-info'
diff --git a/napari/utils/_testsupport.py b/napari/utils/_testsupport.py
index e7e0b4e88b1..873fc8d5fd7 100644
--- a/napari/utils/_testsupport.py
+++ b/napari/utils/_testsupport.py
@@ -81,7 +81,7 @@ def fail_obj_graph(Klass):  # pragma: no cover
         )
 
 
-@pytest.fixture()
+@pytest.fixture
 def napari_plugin_manager(monkeypatch):
     """A napari plugin manager that blocks discovery by default.
 
@@ -139,14 +139,14 @@ def pytest_runtest_makereport(item, call):
     setattr(item, f'rep_{rep.when}', rep)
 
 
-@pytest.fixture()
-def mock_app():
+@pytest.fixture
+def mock_app_model():
     """Mock clean 'test_app' `NapariApplication` instance.
 
-    This fixture must be used whenever `napari._app_model.get_app()` is called to
+    This fixture must be used whenever `napari._app_model.get_app_model()` is called to
     return a 'test_app' `NapariApplication` instead of the 'napari'
     `NapariApplication`. The `make_napari_viewer` fixture is already equipped with
-    a `mock_app`.
+    a `mock_app_model`.
 
     Note that `NapariApplication` registers app-model actions.
     If this is not desired, please create a clean
@@ -168,18 +168,18 @@ def mock_app():
 
     app = NapariApplication('test_app')
     app.injection_store.namespace = _napari_names
-    with patch.object(NapariApplication, 'get_app', return_value=app):
+    with patch.object(NapariApplication, 'get_app_model', return_value=app):
         try:
             yield app
         finally:
             Application.destroy('test_app')
 
 
-@pytest.fixture()
+@pytest.fixture
 def make_napari_viewer(
     qtbot,
     request: 'FixtureRequest',
-    mock_app,
+    mock_app_model,
     napari_plugin_manager,
     monkeypatch,
 ):
@@ -268,6 +268,7 @@ def test_something_with_a_viewer(make_napari_viewer):
     init_qactions.cache_clear()
 
     viewers: WeakSet[Viewer] = WeakSet()
+    request.node._viewer_weak_set = viewers
 
     # may be overridden by using the parameter `strict_qt`
     _strict = False
@@ -276,9 +277,9 @@ def test_something_with_a_viewer(make_napari_viewer):
     prior_exception = getattr(sys, 'last_value', None)
     is_internal_test = request.module.__name__.startswith('napari.')
 
-    # disable throttling cursor event in tests
+    # disable thread for status checker
     monkeypatch.setattr(
-        'napari._qt.qt_main_window._QtMainWindow._throttle_cursor_to_status_connection',
+        'napari._qt.threads.status_checker.StatusChecker.start',
         _empty,
     )
 
@@ -359,8 +360,10 @@ def actual_factory(
     if _strict and getattr(sys, 'last_value', None) is prior_exception:
         QApplication.processEvents()
         leak = set(QApplication.topLevelWidgets()).difference(initial)
+        leak = (x for x in leak if x.objectName() != 'handled_widget')
         # still not sure how to clean up some of the remaining vispy
         # vispy.app.backends._qt.CanvasBackendDesktop widgets...
+        # observed in `test_sys_info.py`
         if any(n.__class__.__name__ != 'CanvasBackendDesktop' for n in leak):
             # just a warning... but this can be converted to test errors
             # in pytest with `-W error`
@@ -382,7 +385,7 @@ def actual_factory(
                 warnings.warn(msg)
 
 
-@pytest.fixture()
+@pytest.fixture
 def make_napari_viewer_proxy(make_napari_viewer, monkeypatch):
     """Fixture that returns a function for creating a napari viewer wrapped in proxy.
     Use in the same way like `make_napari_viewer` fixture.
@@ -414,7 +417,7 @@ def actual_factory(*model_args, ensure_main_thread=True, **model_kwargs):
     return actual_factory
 
 
-@pytest.fixture()
+@pytest.fixture
 def MouseEvent():
     """Create a subclass for simulating vispy mouse events.
 
diff --git a/napari/utils/action_manager.py b/napari/utils/action_manager.py
index 1221fbfcdd1..1c1dc521c3c 100644
--- a/napari/utils/action_manager.py
+++ b/napari/utils/action_manager.py
@@ -46,9 +46,9 @@ def injected(self) -> Callable[..., Future]:
         layer into the commands.  See :func:`inject_napari_dependencies` for
         details.
         """
-        from napari._app_model import get_app
+        from napari._app_model import get_app_model
 
-        return get_app().injection_store.inject(self.command)
+        return get_app_model().injection_store.inject(self.command)
 
 
 class ActionManager:
@@ -104,7 +104,7 @@ def register_action(
         name: str,
         command: Callable,
         description: str,
-        keymapprovider: KeymapProvider,
+        keymapprovider: Optional[KeymapProvider],
         repeatable: bool = False,
     ):
         """
@@ -161,7 +161,8 @@ def register_action(
         self._actions[name] = Action(
             command, description, keymapprovider, repeatable
         )
-        self._update_shortcut_bindings(name)
+        if keymapprovider:
+            self._update_shortcut_bindings(name)
 
     def _update_shortcut_bindings(self, name: str):
         """
@@ -173,7 +174,7 @@ def _update_shortcut_bindings(self, name: str):
         if name not in self._shortcuts:
             return
         action = self._actions[name]
-        km_provider: KeymapProvider = action.keymapprovider
+        km_provider = action.keymapprovider
         if hasattr(km_provider, 'bind_key'):
             for shortcut in self._shortcuts[name]:
                 # NOTE: it would be better if we could bind `self.trigger` here
diff --git a/napari/utils/colormaps/__init__.py b/napari/utils/colormaps/__init__.py
index e4abb55da66..2fc239a7c5d 100644
--- a/napari/utils/colormaps/__init__.py
+++ b/napari/utils/colormaps/__init__.py
@@ -27,14 +27,14 @@
     'ALL_COLORMAPS',
     'AVAILABLE_COLORMAPS',
     'CYMRGB',
-    'Colormap',
-    'CyclicLabelColormap',
-    'DirectLabelColormap',
-    'LabelColormap',
     'INVERSE_COLORMAPS',
     'MAGENTA_GREEN',
     'RGB',
     'SIMPLE_COLORMAPS',
+    'Colormap',
+    'CyclicLabelColormap',
+    'DirectLabelColormap',
+    'LabelColormap',
     'ValidColormapArg',
     'color_dict_to_colormap',
     'direct_colormap',
diff --git a/napari/utils/colormaps/_accelerated_cmap.py b/napari/utils/colormaps/_accelerated_cmap.py
index d6a7c35908d..2104adfcce7 100644
--- a/napari/utils/colormaps/_accelerated_cmap.py
+++ b/napari/utils/colormaps/_accelerated_cmap.py
@@ -17,9 +17,9 @@
 
 
 __all__ = (
+    'labels_raw_to_texture_direct',
     'minimum_dtype_for_labels',
     'zero_preserving_modulo',
-    'labels_raw_to_texture_direct',
     'zero_preserving_modulo_numpy',
 )
 
@@ -74,6 +74,11 @@ def zero_preserving_modulo_numpy(
         The result: 0 for the ``to_zero`` value, ``values % n + 1``
         everywhere else.
     """
+    if n > np.iinfo(dtype).max:
+        # n is to big, modulo will be pointless
+        res = values.astype(dtype)
+        res[values == to_zero] = 0
+        return res
     res = ((values - 1) % n + 1).astype(dtype)
     res[values == to_zero] = 0
     return res
diff --git a/napari/utils/colormaps/_tests/test_colormap.py b/napari/utils/colormaps/_tests/test_colormap.py
index 78803f837f0..e5ea228be62 100644
--- a/napari/utils/colormaps/_tests/test_colormap.py
+++ b/napari/utils/colormaps/_tests/test_colormap.py
@@ -146,7 +146,7 @@ def test_minimum_dtype_for_labels(num, dtype):
     assert _accelerated_cmap.minimum_dtype_for_labels(num) == dtype
 
 
-@pytest.fixture()
+@pytest.fixture
 def _disable_jit(monkeypatch):
     """Fixture to temporarily disable numba JIT during testing.
 
@@ -176,7 +176,7 @@ def test_cast_labels_to_minimum_type_auto(num: int, dtype, monkeypatch):
     assert cast_arr[2] == 10**6 % num + 5
 
 
-@pytest.fixture()
+@pytest.fixture
 def direct_label_colormap():
     return DirectLabelColormap(
         color_dict={
diff --git a/napari/utils/colormaps/categorical_colormap.py b/napari/utils/colormaps/categorical_colormap.py
index 6dc426975c3..301458de70d 100644
--- a/napari/utils/colormaps/categorical_colormap.py
+++ b/napari/utils/colormaps/categorical_colormap.py
@@ -107,13 +107,10 @@ def validate_type(cls, val):
         )
 
     def __eq__(self, other):
-        if isinstance(other, CategoricalColormap):
-            if not compare_colormap_dicts(self.colormap, other.colormap):
-                return False
-            if not np.allclose(
+        return (
+            isinstance(other, CategoricalColormap)
+            and compare_colormap_dicts(self.colormap, other.colormap)
+            and np.allclose(
                 self.fallback_color.values, other.fallback_color.values
-            ):
-                return False
-            return True
-
-        return False
+            )
+        )
diff --git a/napari/utils/colormaps/colormap_utils.py b/napari/utils/colormaps/colormap_utils.py
index a8967b5e968..21e54c3e900 100644
--- a/napari/utils/colormaps/colormap_utils.py
+++ b/napari/utils/colormaps/colormap_utils.py
@@ -112,7 +112,7 @@
 }
 
 
-# dictionay for bop colormap objects
+# dictionary for bop colormap objects
 BOP_COLORMAPS = {
     name: Colormap(value, name=name, display_name=display_name)
     for name, (display_name, value) in bopd.items()
diff --git a/napari/utils/colormaps/vendored/_cm.py b/napari/utils/colormaps/vendored/_cm.py
index bfc1d64d312..73b562bf209 100644
--- a/napari/utils/colormaps/vendored/_cm.py
+++ b/napari/utils/colormaps/vendored/_cm.py
@@ -941,7 +941,7 @@ def _g36(x): return 2 * x - 1
     )
 
 
-# The next 7 palettes are from the Yorick scientific visalisation package,
+# The next 7 palettes are from the Yorick scientific visualisation package,
 # an evolution of the GIST package, both by David H. Munro.
 # They are released under a BSD-like license (see LICENSE_YORICK in
 # the license directory of the matplotlib source distribution).
diff --git a/napari/utils/colormaps/vendored/_cm_listed.py b/napari/utils/colormaps/vendored/_cm_listed.py
index 1ecae8e223f..b90e0a23acb 100644
--- a/napari/utils/colormaps/vendored/_cm_listed.py
+++ b/napari/utils/colormaps/vendored/_cm_listed.py
@@ -1,1511 +1,1501 @@
 from .colors import ListedColormap
 
-_magma_data = [
-    [0.001462, 0.000466, 0.013866],
-    [0.002258, 0.001295, 0.018331],
-    [0.003279, 0.002305, 0.023708],
-    [0.004512, 0.003490, 0.029965],
-    [0.005950, 0.004843, 0.037130],
-    [0.007588, 0.006356, 0.044973],
-    [0.009426, 0.008022, 0.052844],
-    [0.011465, 0.009828, 0.060750],
-    [0.013708, 0.011771, 0.068667],
-    [0.016156, 0.013840, 0.076603],
-    [0.018815, 0.016026, 0.084584],
-    [0.021692, 0.018320, 0.092610],
-    [0.024792, 0.020715, 0.100676],
-    [0.028123, 0.023201, 0.108787],
-    [0.031696, 0.025765, 0.116965],
-    [0.035520, 0.028397, 0.125209],
-    [0.039608, 0.031090, 0.133515],
-    [0.043830, 0.033830, 0.141886],
-    [0.048062, 0.036607, 0.150327],
-    [0.052320, 0.039407, 0.158841],
-    [0.056615, 0.042160, 0.167446],
-    [0.060949, 0.044794, 0.176129],
-    [0.065330, 0.047318, 0.184892],
-    [0.069764, 0.049726, 0.193735],
-    [0.074257, 0.052017, 0.202660],
-    [0.078815, 0.054184, 0.211667],
-    [0.083446, 0.056225, 0.220755],
-    [0.088155, 0.058133, 0.229922],
-    [0.092949, 0.059904, 0.239164],
-    [0.097833, 0.061531, 0.248477],
-    [0.102815, 0.063010, 0.257854],
-    [0.107899, 0.064335, 0.267289],
-    [0.113094, 0.065492, 0.276784],
-    [0.118405, 0.066479, 0.286321],
-    [0.123833, 0.067295, 0.295879],
-    [0.129380, 0.067935, 0.305443],
-    [0.135053, 0.068391, 0.315000],
-    [0.140858, 0.068654, 0.324538],
-    [0.146785, 0.068738, 0.334011],
-    [0.152839, 0.068637, 0.343404],
-    [0.159018, 0.068354, 0.352688],
-    [0.165308, 0.067911, 0.361816],
-    [0.171713, 0.067305, 0.370771],
-    [0.178212, 0.066576, 0.379497],
-    [0.184801, 0.065732, 0.387973],
-    [0.191460, 0.064818, 0.396152],
-    [0.198177, 0.063862, 0.404009],
-    [0.204935, 0.062907, 0.411514],
-    [0.211718, 0.061992, 0.418647],
-    [0.218512, 0.061158, 0.425392],
-    [0.225302, 0.060445, 0.431742],
-    [0.232077, 0.059889, 0.437695],
-    [0.238826, 0.059517, 0.443256],
-    [0.245543, 0.059352, 0.448436],
-    [0.252220, 0.059415, 0.453248],
-    [0.258857, 0.059706, 0.457710],
-    [0.265447, 0.060237, 0.461840],
-    [0.271994, 0.060994, 0.465660],
-    [0.278493, 0.061978, 0.469190],
-    [0.284951, 0.063168, 0.472451],
-    [0.291366, 0.064553, 0.475462],
-    [0.297740, 0.066117, 0.478243],
-    [0.304081, 0.067835, 0.480812],
-    [0.310382, 0.069702, 0.483186],
-    [0.316654, 0.071690, 0.485380],
-    [0.322899, 0.073782, 0.487408],
-    [0.329114, 0.075972, 0.489287],
-    [0.335308, 0.078236, 0.491024],
-    [0.341482, 0.080564, 0.492631],
-    [0.347636, 0.082946, 0.494121],
-    [0.353773, 0.085373, 0.495501],
-    [0.359898, 0.087831, 0.496778],
-    [0.366012, 0.090314, 0.497960],
-    [0.372116, 0.092816, 0.499053],
-    [0.378211, 0.095332, 0.500067],
-    [0.384299, 0.097855, 0.501002],
-    [0.390384, 0.100379, 0.501864],
-    [0.396467, 0.102902, 0.502658],
-    [0.402548, 0.105420, 0.503386],
-    [0.408629, 0.107930, 0.504052],
-    [0.414709, 0.110431, 0.504662],
-    [0.420791, 0.112920, 0.505215],
-    [0.426877, 0.115395, 0.505714],
-    [0.432967, 0.117855, 0.506160],
-    [0.439062, 0.120298, 0.506555],
-    [0.445163, 0.122724, 0.506901],
-    [0.451271, 0.125132, 0.507198],
-    [0.457386, 0.127522, 0.507448],
-    [0.463508, 0.129893, 0.507652],
-    [0.469640, 0.132245, 0.507809],
-    [0.475780, 0.134577, 0.507921],
-    [0.481929, 0.136891, 0.507989],
-    [0.488088, 0.139186, 0.508011],
-    [0.494258, 0.141462, 0.507988],
-    [0.500438, 0.143719, 0.507920],
-    [0.506629, 0.145958, 0.507806],
-    [0.512831, 0.148179, 0.507648],
-    [0.519045, 0.150383, 0.507443],
-    [0.525270, 0.152569, 0.507192],
-    [0.531507, 0.154739, 0.506895],
-    [0.537755, 0.156894, 0.506551],
-    [0.544015, 0.159033, 0.506159],
-    [0.550287, 0.161158, 0.505719],
-    [0.556571, 0.163269, 0.505230],
-    [0.562866, 0.165368, 0.504692],
-    [0.569172, 0.167454, 0.504105],
-    [0.575490, 0.169530, 0.503466],
-    [0.581819, 0.171596, 0.502777],
-    [0.588158, 0.173652, 0.502035],
-    [0.594508, 0.175701, 0.501241],
-    [0.600868, 0.177743, 0.500394],
-    [0.607238, 0.179779, 0.499492],
-    [0.613617, 0.181811, 0.498536],
-    [0.620005, 0.183840, 0.497524],
-    [0.626401, 0.185867, 0.496456],
-    [0.632805, 0.187893, 0.495332],
-    [0.639216, 0.189921, 0.494150],
-    [0.645633, 0.191952, 0.492910],
-    [0.652056, 0.193986, 0.491611],
-    [0.658483, 0.196027, 0.490253],
-    [0.664915, 0.198075, 0.488836],
-    [0.671349, 0.200133, 0.487358],
-    [0.677786, 0.202203, 0.485819],
-    [0.684224, 0.204286, 0.484219],
-    [0.690661, 0.206384, 0.482558],
-    [0.697098, 0.208501, 0.480835],
-    [0.703532, 0.210638, 0.479049],
-    [0.709962, 0.212797, 0.477201],
-    [0.716387, 0.214982, 0.475290],
-    [0.722805, 0.217194, 0.473316],
-    [0.729216, 0.219437, 0.471279],
-    [0.735616, 0.221713, 0.469180],
-    [0.742004, 0.224025, 0.467018],
-    [0.748378, 0.226377, 0.464794],
-    [0.754737, 0.228772, 0.462509],
-    [0.761077, 0.231214, 0.460162],
-    [0.767398, 0.233705, 0.457755],
-    [0.773695, 0.236249, 0.455289],
-    [0.779968, 0.238851, 0.452765],
-    [0.786212, 0.241514, 0.450184],
-    [0.792427, 0.244242, 0.447543],
-    [0.798608, 0.247040, 0.444848],
-    [0.804752, 0.249911, 0.442102],
-    [0.810855, 0.252861, 0.439305],
-    [0.816914, 0.255895, 0.436461],
-    [0.822926, 0.259016, 0.433573],
-    [0.828886, 0.262229, 0.430644],
-    [0.834791, 0.265540, 0.427671],
-    [0.840636, 0.268953, 0.424666],
-    [0.846416, 0.272473, 0.421631],
-    [0.852126, 0.276106, 0.418573],
-    [0.857763, 0.279857, 0.415496],
-    [0.863320, 0.283729, 0.412403],
-    [0.868793, 0.287728, 0.409303],
-    [0.874176, 0.291859, 0.406205],
-    [0.879464, 0.296125, 0.403118],
-    [0.884651, 0.300530, 0.400047],
-    [0.889731, 0.305079, 0.397002],
-    [0.894700, 0.309773, 0.393995],
-    [0.899552, 0.314616, 0.391037],
-    [0.904281, 0.319610, 0.388137],
-    [0.908884, 0.324755, 0.385308],
-    [0.913354, 0.330052, 0.382563],
-    [0.917689, 0.335500, 0.379915],
-    [0.921884, 0.341098, 0.377376],
-    [0.925937, 0.346844, 0.374959],
-    [0.929845, 0.352734, 0.372677],
-    [0.933606, 0.358764, 0.370541],
-    [0.937221, 0.364929, 0.368567],
-    [0.940687, 0.371224, 0.366762],
-    [0.944006, 0.377643, 0.365136],
-    [0.947180, 0.384178, 0.363701],
-    [0.950210, 0.390820, 0.362468],
-    [0.953099, 0.397563, 0.361438],
-    [0.955849, 0.404400, 0.360619],
-    [0.958464, 0.411324, 0.360014],
-    [0.960949, 0.418323, 0.359630],
-    [0.963310, 0.425390, 0.359469],
-    [0.965549, 0.432519, 0.359529],
-    [0.967671, 0.439703, 0.359810],
-    [0.969680, 0.446936, 0.360311],
-    [0.971582, 0.454210, 0.361030],
-    [0.973381, 0.461520, 0.361965],
-    [0.975082, 0.468861, 0.363111],
-    [0.976690, 0.476226, 0.364466],
-    [0.978210, 0.483612, 0.366025],
-    [0.979645, 0.491014, 0.367783],
-    [0.981000, 0.498428, 0.369734],
-    [0.982279, 0.505851, 0.371874],
-    [0.983485, 0.513280, 0.374198],
-    [0.984622, 0.520713, 0.376698],
-    [0.985693, 0.528148, 0.379371],
-    [0.986700, 0.535582, 0.382210],
-    [0.987646, 0.543015, 0.385210],
-    [0.988533, 0.550446, 0.388365],
-    [0.989363, 0.557873, 0.391671],
-    [0.990138, 0.565296, 0.395122],
-    [0.990871, 0.572706, 0.398714],
-    [0.991558, 0.580107, 0.402441],
-    [0.992196, 0.587502, 0.406299],
-    [0.992785, 0.594891, 0.410283],
-    [0.993326, 0.602275, 0.414390],
-    [0.993834, 0.609644, 0.418613],
-    [0.994309, 0.616999, 0.422950],
-    [0.994738, 0.624350, 0.427397],
-    [0.995122, 0.631696, 0.431951],
-    [0.995480, 0.639027, 0.436607],
-    [0.995810, 0.646344, 0.441361],
-    [0.996096, 0.653659, 0.446213],
-    [0.996341, 0.660969, 0.451160],
-    [0.996580, 0.668256, 0.456192],
-    [0.996775, 0.675541, 0.461314],
-    [0.996925, 0.682828, 0.466526],
-    [0.997077, 0.690088, 0.471811],
-    [0.997186, 0.697349, 0.477182],
-    [0.997254, 0.704611, 0.482635],
-    [0.997325, 0.711848, 0.488154],
-    [0.997351, 0.719089, 0.493755],
-    [0.997351, 0.726324, 0.499428],
-    [0.997341, 0.733545, 0.505167],
-    [0.997285, 0.740772, 0.510983],
-    [0.997228, 0.747981, 0.516859],
-    [0.997138, 0.755190, 0.522806],
-    [0.997019, 0.762398, 0.528821],
-    [0.996898, 0.769591, 0.534892],
-    [0.996727, 0.776795, 0.541039],
-    [0.996571, 0.783977, 0.547233],
-    [0.996369, 0.791167, 0.553499],
-    [0.996162, 0.798348, 0.559820],
-    [0.995932, 0.805527, 0.566202],
-    [0.995680, 0.812706, 0.572645],
-    [0.995424, 0.819875, 0.579140],
-    [0.995131, 0.827052, 0.585701],
-    [0.994851, 0.834213, 0.592307],
-    [0.994524, 0.841387, 0.598983],
-    [0.994222, 0.848540, 0.605696],
-    [0.993866, 0.855711, 0.612482],
-    [0.993545, 0.862859, 0.619299],
-    [0.993170, 0.870024, 0.626189],
-    [0.992831, 0.877168, 0.633109],
-    [0.992440, 0.884330, 0.640099],
-    [0.992089, 0.891470, 0.647116],
-    [0.991688, 0.898627, 0.654202],
-    [0.991332, 0.905763, 0.661309],
-    [0.990930, 0.912915, 0.668481],
-    [0.990570, 0.920049, 0.675675],
-    [0.990175, 0.927196, 0.682926],
-    [0.989815, 0.934329, 0.690198],
-    [0.989434, 0.941470, 0.697519],
-    [0.989077, 0.948604, 0.704863],
-    [0.988717, 0.955742, 0.712242],
-    [0.988367, 0.962878, 0.719649],
-    [0.988033, 0.970012, 0.727077],
-    [0.987691, 0.977154, 0.734536],
-    [0.987387, 0.984288, 0.742002],
-    [0.987053, 0.991438, 0.749504],
-]
+_magma_data = [[0.001462, 0.000466, 0.013866],
+               [0.002258, 0.001295, 0.018331],
+               [0.003279, 0.002305, 0.023708],
+               [0.004512, 0.003490, 0.029965],
+               [0.005950, 0.004843, 0.037130],
+               [0.007588, 0.006356, 0.044973],
+               [0.009426, 0.008022, 0.052844],
+               [0.011465, 0.009828, 0.060750],
+               [0.013708, 0.011771, 0.068667],
+               [0.016156, 0.013840, 0.076603],
+               [0.018815, 0.016026, 0.084584],
+               [0.021692, 0.018320, 0.092610],
+               [0.024792, 0.020715, 0.100676],
+               [0.028123, 0.023201, 0.108787],
+               [0.031696, 0.025765, 0.116965],
+               [0.035520, 0.028397, 0.125209],
+               [0.039608, 0.031090, 0.133515],
+               [0.043830, 0.033830, 0.141886],
+               [0.048062, 0.036607, 0.150327],
+               [0.052320, 0.039407, 0.158841],
+               [0.056615, 0.042160, 0.167446],
+               [0.060949, 0.044794, 0.176129],
+               [0.065330, 0.047318, 0.184892],
+               [0.069764, 0.049726, 0.193735],
+               [0.074257, 0.052017, 0.202660],
+               [0.078815, 0.054184, 0.211667],
+               [0.083446, 0.056225, 0.220755],
+               [0.088155, 0.058133, 0.229922],
+               [0.092949, 0.059904, 0.239164],
+               [0.097833, 0.061531, 0.248477],
+               [0.102815, 0.063010, 0.257854],
+               [0.107899, 0.064335, 0.267289],
+               [0.113094, 0.065492, 0.276784],
+               [0.118405, 0.066479, 0.286321],
+               [0.123833, 0.067295, 0.295879],
+               [0.129380, 0.067935, 0.305443],
+               [0.135053, 0.068391, 0.315000],
+               [0.140858, 0.068654, 0.324538],
+               [0.146785, 0.068738, 0.334011],
+               [0.152839, 0.068637, 0.343404],
+               [0.159018, 0.068354, 0.352688],
+               [0.165308, 0.067911, 0.361816],
+               [0.171713, 0.067305, 0.370771],
+               [0.178212, 0.066576, 0.379497],
+               [0.184801, 0.065732, 0.387973],
+               [0.191460, 0.064818, 0.396152],
+               [0.198177, 0.063862, 0.404009],
+               [0.204935, 0.062907, 0.411514],
+               [0.211718, 0.061992, 0.418647],
+               [0.218512, 0.061158, 0.425392],
+               [0.225302, 0.060445, 0.431742],
+               [0.232077, 0.059889, 0.437695],
+               [0.238826, 0.059517, 0.443256],
+               [0.245543, 0.059352, 0.448436],
+               [0.252220, 0.059415, 0.453248],
+               [0.258857, 0.059706, 0.457710],
+               [0.265447, 0.060237, 0.461840],
+               [0.271994, 0.060994, 0.465660],
+               [0.278493, 0.061978, 0.469190],
+               [0.284951, 0.063168, 0.472451],
+               [0.291366, 0.064553, 0.475462],
+               [0.297740, 0.066117, 0.478243],
+               [0.304081, 0.067835, 0.480812],
+               [0.310382, 0.069702, 0.483186],
+               [0.316654, 0.071690, 0.485380],
+               [0.322899, 0.073782, 0.487408],
+               [0.329114, 0.075972, 0.489287],
+               [0.335308, 0.078236, 0.491024],
+               [0.341482, 0.080564, 0.492631],
+               [0.347636, 0.082946, 0.494121],
+               [0.353773, 0.085373, 0.495501],
+               [0.359898, 0.087831, 0.496778],
+               [0.366012, 0.090314, 0.497960],
+               [0.372116, 0.092816, 0.499053],
+               [0.378211, 0.095332, 0.500067],
+               [0.384299, 0.097855, 0.501002],
+               [0.390384, 0.100379, 0.501864],
+               [0.396467, 0.102902, 0.502658],
+               [0.402548, 0.105420, 0.503386],
+               [0.408629, 0.107930, 0.504052],
+               [0.414709, 0.110431, 0.504662],
+               [0.420791, 0.112920, 0.505215],
+               [0.426877, 0.115395, 0.505714],
+               [0.432967, 0.117855, 0.506160],
+               [0.439062, 0.120298, 0.506555],
+               [0.445163, 0.122724, 0.506901],
+               [0.451271, 0.125132, 0.507198],
+               [0.457386, 0.127522, 0.507448],
+               [0.463508, 0.129893, 0.507652],
+               [0.469640, 0.132245, 0.507809],
+               [0.475780, 0.134577, 0.507921],
+               [0.481929, 0.136891, 0.507989],
+               [0.488088, 0.139186, 0.508011],
+               [0.494258, 0.141462, 0.507988],
+               [0.500438, 0.143719, 0.507920],
+               [0.506629, 0.145958, 0.507806],
+               [0.512831, 0.148179, 0.507648],
+               [0.519045, 0.150383, 0.507443],
+               [0.525270, 0.152569, 0.507192],
+               [0.531507, 0.154739, 0.506895],
+               [0.537755, 0.156894, 0.506551],
+               [0.544015, 0.159033, 0.506159],
+               [0.550287, 0.161158, 0.505719],
+               [0.556571, 0.163269, 0.505230],
+               [0.562866, 0.165368, 0.504692],
+               [0.569172, 0.167454, 0.504105],
+               [0.575490, 0.169530, 0.503466],
+               [0.581819, 0.171596, 0.502777],
+               [0.588158, 0.173652, 0.502035],
+               [0.594508, 0.175701, 0.501241],
+               [0.600868, 0.177743, 0.500394],
+               [0.607238, 0.179779, 0.499492],
+               [0.613617, 0.181811, 0.498536],
+               [0.620005, 0.183840, 0.497524],
+               [0.626401, 0.185867, 0.496456],
+               [0.632805, 0.187893, 0.495332],
+               [0.639216, 0.189921, 0.494150],
+               [0.645633, 0.191952, 0.492910],
+               [0.652056, 0.193986, 0.491611],
+               [0.658483, 0.196027, 0.490253],
+               [0.664915, 0.198075, 0.488836],
+               [0.671349, 0.200133, 0.487358],
+               [0.677786, 0.202203, 0.485819],
+               [0.684224, 0.204286, 0.484219],
+               [0.690661, 0.206384, 0.482558],
+               [0.697098, 0.208501, 0.480835],
+               [0.703532, 0.210638, 0.479049],
+               [0.709962, 0.212797, 0.477201],
+               [0.716387, 0.214982, 0.475290],
+               [0.722805, 0.217194, 0.473316],
+               [0.729216, 0.219437, 0.471279],
+               [0.735616, 0.221713, 0.469180],
+               [0.742004, 0.224025, 0.467018],
+               [0.748378, 0.226377, 0.464794],
+               [0.754737, 0.228772, 0.462509],
+               [0.761077, 0.231214, 0.460162],
+               [0.767398, 0.233705, 0.457755],
+               [0.773695, 0.236249, 0.455289],
+               [0.779968, 0.238851, 0.452765],
+               [0.786212, 0.241514, 0.450184],
+               [0.792427, 0.244242, 0.447543],
+               [0.798608, 0.247040, 0.444848],
+               [0.804752, 0.249911, 0.442102],
+               [0.810855, 0.252861, 0.439305],
+               [0.816914, 0.255895, 0.436461],
+               [0.822926, 0.259016, 0.433573],
+               [0.828886, 0.262229, 0.430644],
+               [0.834791, 0.265540, 0.427671],
+               [0.840636, 0.268953, 0.424666],
+               [0.846416, 0.272473, 0.421631],
+               [0.852126, 0.276106, 0.418573],
+               [0.857763, 0.279857, 0.415496],
+               [0.863320, 0.283729, 0.412403],
+               [0.868793, 0.287728, 0.409303],
+               [0.874176, 0.291859, 0.406205],
+               [0.879464, 0.296125, 0.403118],
+               [0.884651, 0.300530, 0.400047],
+               [0.889731, 0.305079, 0.397002],
+               [0.894700, 0.309773, 0.393995],
+               [0.899552, 0.314616, 0.391037],
+               [0.904281, 0.319610, 0.388137],
+               [0.908884, 0.324755, 0.385308],
+               [0.913354, 0.330052, 0.382563],
+               [0.917689, 0.335500, 0.379915],
+               [0.921884, 0.341098, 0.377376],
+               [0.925937, 0.346844, 0.374959],
+               [0.929845, 0.352734, 0.372677],
+               [0.933606, 0.358764, 0.370541],
+               [0.937221, 0.364929, 0.368567],
+               [0.940687, 0.371224, 0.366762],
+               [0.944006, 0.377643, 0.365136],
+               [0.947180, 0.384178, 0.363701],
+               [0.950210, 0.390820, 0.362468],
+               [0.953099, 0.397563, 0.361438],
+               [0.955849, 0.404400, 0.360619],
+               [0.958464, 0.411324, 0.360014],
+               [0.960949, 0.418323, 0.359630],
+               [0.963310, 0.425390, 0.359469],
+               [0.965549, 0.432519, 0.359529],
+               [0.967671, 0.439703, 0.359810],
+               [0.969680, 0.446936, 0.360311],
+               [0.971582, 0.454210, 0.361030],
+               [0.973381, 0.461520, 0.361965],
+               [0.975082, 0.468861, 0.363111],
+               [0.976690, 0.476226, 0.364466],
+               [0.978210, 0.483612, 0.366025],
+               [0.979645, 0.491014, 0.367783],
+               [0.981000, 0.498428, 0.369734],
+               [0.982279, 0.505851, 0.371874],
+               [0.983485, 0.513280, 0.374198],
+               [0.984622, 0.520713, 0.376698],
+               [0.985693, 0.528148, 0.379371],
+               [0.986700, 0.535582, 0.382210],
+               [0.987646, 0.543015, 0.385210],
+               [0.988533, 0.550446, 0.388365],
+               [0.989363, 0.557873, 0.391671],
+               [0.990138, 0.565296, 0.395122],
+               [0.990871, 0.572706, 0.398714],
+               [0.991558, 0.580107, 0.402441],
+               [0.992196, 0.587502, 0.406299],
+               [0.992785, 0.594891, 0.410283],
+               [0.993326, 0.602275, 0.414390],
+               [0.993834, 0.609644, 0.418613],
+               [0.994309, 0.616999, 0.422950],
+               [0.994738, 0.624350, 0.427397],
+               [0.995122, 0.631696, 0.431951],
+               [0.995480, 0.639027, 0.436607],
+               [0.995810, 0.646344, 0.441361],
+               [0.996096, 0.653659, 0.446213],
+               [0.996341, 0.660969, 0.451160],
+               [0.996580, 0.668256, 0.456192],
+               [0.996775, 0.675541, 0.461314],
+               [0.996925, 0.682828, 0.466526],
+               [0.997077, 0.690088, 0.471811],
+               [0.997186, 0.697349, 0.477182],
+               [0.997254, 0.704611, 0.482635],
+               [0.997325, 0.711848, 0.488154],
+               [0.997351, 0.719089, 0.493755],
+               [0.997351, 0.726324, 0.499428],
+               [0.997341, 0.733545, 0.505167],
+               [0.997285, 0.740772, 0.510983],
+               [0.997228, 0.747981, 0.516859],
+               [0.997138, 0.755190, 0.522806],
+               [0.997019, 0.762398, 0.528821],
+               [0.996898, 0.769591, 0.534892],
+               [0.996727, 0.776795, 0.541039],
+               [0.996571, 0.783977, 0.547233],
+               [0.996369, 0.791167, 0.553499],
+               [0.996162, 0.798348, 0.559820],
+               [0.995932, 0.805527, 0.566202],
+               [0.995680, 0.812706, 0.572645],
+               [0.995424, 0.819875, 0.579140],
+               [0.995131, 0.827052, 0.585701],
+               [0.994851, 0.834213, 0.592307],
+               [0.994524, 0.841387, 0.598983],
+               [0.994222, 0.848540, 0.605696],
+               [0.993866, 0.855711, 0.612482],
+               [0.993545, 0.862859, 0.619299],
+               [0.993170, 0.870024, 0.626189],
+               [0.992831, 0.877168, 0.633109],
+               [0.992440, 0.884330, 0.640099],
+               [0.992089, 0.891470, 0.647116],
+               [0.991688, 0.898627, 0.654202],
+               [0.991332, 0.905763, 0.661309],
+               [0.990930, 0.912915, 0.668481],
+               [0.990570, 0.920049, 0.675675],
+               [0.990175, 0.927196, 0.682926],
+               [0.989815, 0.934329, 0.690198],
+               [0.989434, 0.941470, 0.697519],
+               [0.989077, 0.948604, 0.704863],
+               [0.988717, 0.955742, 0.712242],
+               [0.988367, 0.962878, 0.719649],
+               [0.988033, 0.970012, 0.727077],
+               [0.987691, 0.977154, 0.734536],
+               [0.987387, 0.984288, 0.742002],
+               [0.987053, 0.991438, 0.749504]]
 
-_inferno_data = [
-    [0.001462, 0.000466, 0.013866],
-    [0.002267, 0.001270, 0.018570],
-    [0.003299, 0.002249, 0.024239],
-    [0.004547, 0.003392, 0.030909],
-    [0.006006, 0.004692, 0.038558],
-    [0.007676, 0.006136, 0.046836],
-    [0.009561, 0.007713, 0.055143],
-    [0.011663, 0.009417, 0.063460],
-    [0.013995, 0.011225, 0.071862],
-    [0.016561, 0.013136, 0.080282],
-    [0.019373, 0.015133, 0.088767],
-    [0.022447, 0.017199, 0.097327],
-    [0.025793, 0.019331, 0.105930],
-    [0.029432, 0.021503, 0.114621],
-    [0.033385, 0.023702, 0.123397],
-    [0.037668, 0.025921, 0.132232],
-    [0.042253, 0.028139, 0.141141],
-    [0.046915, 0.030324, 0.150164],
-    [0.051644, 0.032474, 0.159254],
-    [0.056449, 0.034569, 0.168414],
-    [0.061340, 0.036590, 0.177642],
-    [0.066331, 0.038504, 0.186962],
-    [0.071429, 0.040294, 0.196354],
-    [0.076637, 0.041905, 0.205799],
-    [0.081962, 0.043328, 0.215289],
-    [0.087411, 0.044556, 0.224813],
-    [0.092990, 0.045583, 0.234358],
-    [0.098702, 0.046402, 0.243904],
-    [0.104551, 0.047008, 0.253430],
-    [0.110536, 0.047399, 0.262912],
-    [0.116656, 0.047574, 0.272321],
-    [0.122908, 0.047536, 0.281624],
-    [0.129285, 0.047293, 0.290788],
-    [0.135778, 0.046856, 0.299776],
-    [0.142378, 0.046242, 0.308553],
-    [0.149073, 0.045468, 0.317085],
-    [0.155850, 0.044559, 0.325338],
-    [0.162689, 0.043554, 0.333277],
-    [0.169575, 0.042489, 0.340874],
-    [0.176493, 0.041402, 0.348111],
-    [0.183429, 0.040329, 0.354971],
-    [0.190367, 0.039309, 0.361447],
-    [0.197297, 0.038400, 0.367535],
-    [0.204209, 0.037632, 0.373238],
-    [0.211095, 0.037030, 0.378563],
-    [0.217949, 0.036615, 0.383522],
-    [0.224763, 0.036405, 0.388129],
-    [0.231538, 0.036405, 0.392400],
-    [0.238273, 0.036621, 0.396353],
-    [0.244967, 0.037055, 0.400007],
-    [0.251620, 0.037705, 0.403378],
-    [0.258234, 0.038571, 0.406485],
-    [0.264810, 0.039647, 0.409345],
-    [0.271347, 0.040922, 0.411976],
-    [0.277850, 0.042353, 0.414392],
-    [0.284321, 0.043933, 0.416608],
-    [0.290763, 0.045644, 0.418637],
-    [0.297178, 0.047470, 0.420491],
-    [0.303568, 0.049396, 0.422182],
-    [0.309935, 0.051407, 0.423721],
-    [0.316282, 0.053490, 0.425116],
-    [0.322610, 0.055634, 0.426377],
-    [0.328921, 0.057827, 0.427511],
-    [0.335217, 0.060060, 0.428524],
-    [0.341500, 0.062325, 0.429425],
-    [0.347771, 0.064616, 0.430217],
-    [0.354032, 0.066925, 0.430906],
-    [0.360284, 0.069247, 0.431497],
-    [0.366529, 0.071579, 0.431994],
-    [0.372768, 0.073915, 0.432400],
-    [0.379001, 0.076253, 0.432719],
-    [0.385228, 0.078591, 0.432955],
-    [0.391453, 0.080927, 0.433109],
-    [0.397674, 0.083257, 0.433183],
-    [0.403894, 0.085580, 0.433179],
-    [0.410113, 0.087896, 0.433098],
-    [0.416331, 0.090203, 0.432943],
-    [0.422549, 0.092501, 0.432714],
-    [0.428768, 0.094790, 0.432412],
-    [0.434987, 0.097069, 0.432039],
-    [0.441207, 0.099338, 0.431594],
-    [0.447428, 0.101597, 0.431080],
-    [0.453651, 0.103848, 0.430498],
-    [0.459875, 0.106089, 0.429846],
-    [0.466100, 0.108322, 0.429125],
-    [0.472328, 0.110547, 0.428334],
-    [0.478558, 0.112764, 0.427475],
-    [0.484789, 0.114974, 0.426548],
-    [0.491022, 0.117179, 0.425552],
-    [0.497257, 0.119379, 0.424488],
-    [0.503493, 0.121575, 0.423356],
-    [0.509730, 0.123769, 0.422156],
-    [0.515967, 0.125960, 0.420887],
-    [0.522206, 0.128150, 0.419549],
-    [0.528444, 0.130341, 0.418142],
-    [0.534683, 0.132534, 0.416667],
-    [0.540920, 0.134729, 0.415123],
-    [0.547157, 0.136929, 0.413511],
-    [0.553392, 0.139134, 0.411829],
-    [0.559624, 0.141346, 0.410078],
-    [0.565854, 0.143567, 0.408258],
-    [0.572081, 0.145797, 0.406369],
-    [0.578304, 0.148039, 0.404411],
-    [0.584521, 0.150294, 0.402385],
-    [0.590734, 0.152563, 0.400290],
-    [0.596940, 0.154848, 0.398125],
-    [0.603139, 0.157151, 0.395891],
-    [0.609330, 0.159474, 0.393589],
-    [0.615513, 0.161817, 0.391219],
-    [0.621685, 0.164184, 0.388781],
-    [0.627847, 0.166575, 0.386276],
-    [0.633998, 0.168992, 0.383704],
-    [0.640135, 0.171438, 0.381065],
-    [0.646260, 0.173914, 0.378359],
-    [0.652369, 0.176421, 0.375586],
-    [0.658463, 0.178962, 0.372748],
-    [0.664540, 0.181539, 0.369846],
-    [0.670599, 0.184153, 0.366879],
-    [0.676638, 0.186807, 0.363849],
-    [0.682656, 0.189501, 0.360757],
-    [0.688653, 0.192239, 0.357603],
-    [0.694627, 0.195021, 0.354388],
-    [0.700576, 0.197851, 0.351113],
-    [0.706500, 0.200728, 0.347777],
-    [0.712396, 0.203656, 0.344383],
-    [0.718264, 0.206636, 0.340931],
-    [0.724103, 0.209670, 0.337424],
-    [0.729909, 0.212759, 0.333861],
-    [0.735683, 0.215906, 0.330245],
-    [0.741423, 0.219112, 0.326576],
-    [0.747127, 0.222378, 0.322856],
-    [0.752794, 0.225706, 0.319085],
-    [0.758422, 0.229097, 0.315266],
-    [0.764010, 0.232554, 0.311399],
-    [0.769556, 0.236077, 0.307485],
-    [0.775059, 0.239667, 0.303526],
-    [0.780517, 0.243327, 0.299523],
-    [0.785929, 0.247056, 0.295477],
-    [0.791293, 0.250856, 0.291390],
-    [0.796607, 0.254728, 0.287264],
-    [0.801871, 0.258674, 0.283099],
-    [0.807082, 0.262692, 0.278898],
-    [0.812239, 0.266786, 0.274661],
-    [0.817341, 0.270954, 0.270390],
-    [0.822386, 0.275197, 0.266085],
-    [0.827372, 0.279517, 0.261750],
-    [0.832299, 0.283913, 0.257383],
-    [0.837165, 0.288385, 0.252988],
-    [0.841969, 0.292933, 0.248564],
-    [0.846709, 0.297559, 0.244113],
-    [0.851384, 0.302260, 0.239636],
-    [0.855992, 0.307038, 0.235133],
-    [0.860533, 0.311892, 0.230606],
-    [0.865006, 0.316822, 0.226055],
-    [0.869409, 0.321827, 0.221482],
-    [0.873741, 0.326906, 0.216886],
-    [0.878001, 0.332060, 0.212268],
-    [0.882188, 0.337287, 0.207628],
-    [0.886302, 0.342586, 0.202968],
-    [0.890341, 0.347957, 0.198286],
-    [0.894305, 0.353399, 0.193584],
-    [0.898192, 0.358911, 0.188860],
-    [0.902003, 0.364492, 0.184116],
-    [0.905735, 0.370140, 0.179350],
-    [0.909390, 0.375856, 0.174563],
-    [0.912966, 0.381636, 0.169755],
-    [0.916462, 0.387481, 0.164924],
-    [0.919879, 0.393389, 0.160070],
-    [0.923215, 0.399359, 0.155193],
-    [0.926470, 0.405389, 0.150292],
-    [0.929644, 0.411479, 0.145367],
-    [0.932737, 0.417627, 0.140417],
-    [0.935747, 0.423831, 0.135440],
-    [0.938675, 0.430091, 0.130438],
-    [0.941521, 0.436405, 0.125409],
-    [0.944285, 0.442772, 0.120354],
-    [0.946965, 0.449191, 0.115272],
-    [0.949562, 0.455660, 0.110164],
-    [0.952075, 0.462178, 0.105031],
-    [0.954506, 0.468744, 0.099874],
-    [0.956852, 0.475356, 0.094695],
-    [0.959114, 0.482014, 0.089499],
-    [0.961293, 0.488716, 0.084289],
-    [0.963387, 0.495462, 0.079073],
-    [0.965397, 0.502249, 0.073859],
-    [0.967322, 0.509078, 0.068659],
-    [0.969163, 0.515946, 0.063488],
-    [0.970919, 0.522853, 0.058367],
-    [0.972590, 0.529798, 0.053324],
-    [0.974176, 0.536780, 0.048392],
-    [0.975677, 0.543798, 0.043618],
-    [0.977092, 0.550850, 0.039050],
-    [0.978422, 0.557937, 0.034931],
-    [0.979666, 0.565057, 0.031409],
-    [0.980824, 0.572209, 0.028508],
-    [0.981895, 0.579392, 0.026250],
-    [0.982881, 0.586606, 0.024661],
-    [0.983779, 0.593849, 0.023770],
-    [0.984591, 0.601122, 0.023606],
-    [0.985315, 0.608422, 0.024202],
-    [0.985952, 0.615750, 0.025592],
-    [0.986502, 0.623105, 0.027814],
-    [0.986964, 0.630485, 0.030908],
-    [0.987337, 0.637890, 0.034916],
-    [0.987622, 0.645320, 0.039886],
-    [0.987819, 0.652773, 0.045581],
-    [0.987926, 0.660250, 0.051750],
-    [0.987945, 0.667748, 0.058329],
-    [0.987874, 0.675267, 0.065257],
-    [0.987714, 0.682807, 0.072489],
-    [0.987464, 0.690366, 0.079990],
-    [0.987124, 0.697944, 0.087731],
-    [0.986694, 0.705540, 0.095694],
-    [0.986175, 0.713153, 0.103863],
-    [0.985566, 0.720782, 0.112229],
-    [0.984865, 0.728427, 0.120785],
-    [0.984075, 0.736087, 0.129527],
-    [0.983196, 0.743758, 0.138453],
-    [0.982228, 0.751442, 0.147565],
-    [0.981173, 0.759135, 0.156863],
-    [0.980032, 0.766837, 0.166353],
-    [0.978806, 0.774545, 0.176037],
-    [0.977497, 0.782258, 0.185923],
-    [0.976108, 0.789974, 0.196018],
-    [0.974638, 0.797692, 0.206332],
-    [0.973088, 0.805409, 0.216877],
-    [0.971468, 0.813122, 0.227658],
-    [0.969783, 0.820825, 0.238686],
-    [0.968041, 0.828515, 0.249972],
-    [0.966243, 0.836191, 0.261534],
-    [0.964394, 0.843848, 0.273391],
-    [0.962517, 0.851476, 0.285546],
-    [0.960626, 0.859069, 0.298010],
-    [0.958720, 0.866624, 0.310820],
-    [0.956834, 0.874129, 0.323974],
-    [0.954997, 0.881569, 0.337475],
-    [0.953215, 0.888942, 0.351369],
-    [0.951546, 0.896226, 0.365627],
-    [0.950018, 0.903409, 0.380271],
-    [0.948683, 0.910473, 0.395289],
-    [0.947594, 0.917399, 0.410665],
-    [0.946809, 0.924168, 0.426373],
-    [0.946392, 0.930761, 0.442367],
-    [0.946403, 0.937159, 0.458592],
-    [0.946903, 0.943348, 0.474970],
-    [0.947937, 0.949318, 0.491426],
-    [0.949545, 0.955063, 0.507860],
-    [0.951740, 0.960587, 0.524203],
-    [0.954529, 0.965896, 0.540361],
-    [0.957896, 0.971003, 0.556275],
-    [0.961812, 0.975924, 0.571925],
-    [0.966249, 0.980678, 0.587206],
-    [0.971162, 0.985282, 0.602154],
-    [0.976511, 0.989753, 0.616760],
-    [0.982257, 0.994109, 0.631017],
-    [0.988362, 0.998364, 0.644924],
-]
+_inferno_data = [[0.001462, 0.000466, 0.013866],
+                 [0.002267, 0.001270, 0.018570],
+                 [0.003299, 0.002249, 0.024239],
+                 [0.004547, 0.003392, 0.030909],
+                 [0.006006, 0.004692, 0.038558],
+                 [0.007676, 0.006136, 0.046836],
+                 [0.009561, 0.007713, 0.055143],
+                 [0.011663, 0.009417, 0.063460],
+                 [0.013995, 0.011225, 0.071862],
+                 [0.016561, 0.013136, 0.080282],
+                 [0.019373, 0.015133, 0.088767],
+                 [0.022447, 0.017199, 0.097327],
+                 [0.025793, 0.019331, 0.105930],
+                 [0.029432, 0.021503, 0.114621],
+                 [0.033385, 0.023702, 0.123397],
+                 [0.037668, 0.025921, 0.132232],
+                 [0.042253, 0.028139, 0.141141],
+                 [0.046915, 0.030324, 0.150164],
+                 [0.051644, 0.032474, 0.159254],
+                 [0.056449, 0.034569, 0.168414],
+                 [0.061340, 0.036590, 0.177642],
+                 [0.066331, 0.038504, 0.186962],
+                 [0.071429, 0.040294, 0.196354],
+                 [0.076637, 0.041905, 0.205799],
+                 [0.081962, 0.043328, 0.215289],
+                 [0.087411, 0.044556, 0.224813],
+                 [0.092990, 0.045583, 0.234358],
+                 [0.098702, 0.046402, 0.243904],
+                 [0.104551, 0.047008, 0.253430],
+                 [0.110536, 0.047399, 0.262912],
+                 [0.116656, 0.047574, 0.272321],
+                 [0.122908, 0.047536, 0.281624],
+                 [0.129285, 0.047293, 0.290788],
+                 [0.135778, 0.046856, 0.299776],
+                 [0.142378, 0.046242, 0.308553],
+                 [0.149073, 0.045468, 0.317085],
+                 [0.155850, 0.044559, 0.325338],
+                 [0.162689, 0.043554, 0.333277],
+                 [0.169575, 0.042489, 0.340874],
+                 [0.176493, 0.041402, 0.348111],
+                 [0.183429, 0.040329, 0.354971],
+                 [0.190367, 0.039309, 0.361447],
+                 [0.197297, 0.038400, 0.367535],
+                 [0.204209, 0.037632, 0.373238],
+                 [0.211095, 0.037030, 0.378563],
+                 [0.217949, 0.036615, 0.383522],
+                 [0.224763, 0.036405, 0.388129],
+                 [0.231538, 0.036405, 0.392400],
+                 [0.238273, 0.036621, 0.396353],
+                 [0.244967, 0.037055, 0.400007],
+                 [0.251620, 0.037705, 0.403378],
+                 [0.258234, 0.038571, 0.406485],
+                 [0.264810, 0.039647, 0.409345],
+                 [0.271347, 0.040922, 0.411976],
+                 [0.277850, 0.042353, 0.414392],
+                 [0.284321, 0.043933, 0.416608],
+                 [0.290763, 0.045644, 0.418637],
+                 [0.297178, 0.047470, 0.420491],
+                 [0.303568, 0.049396, 0.422182],
+                 [0.309935, 0.051407, 0.423721],
+                 [0.316282, 0.053490, 0.425116],
+                 [0.322610, 0.055634, 0.426377],
+                 [0.328921, 0.057827, 0.427511],
+                 [0.335217, 0.060060, 0.428524],
+                 [0.341500, 0.062325, 0.429425],
+                 [0.347771, 0.064616, 0.430217],
+                 [0.354032, 0.066925, 0.430906],
+                 [0.360284, 0.069247, 0.431497],
+                 [0.366529, 0.071579, 0.431994],
+                 [0.372768, 0.073915, 0.432400],
+                 [0.379001, 0.076253, 0.432719],
+                 [0.385228, 0.078591, 0.432955],
+                 [0.391453, 0.080927, 0.433109],
+                 [0.397674, 0.083257, 0.433183],
+                 [0.403894, 0.085580, 0.433179],
+                 [0.410113, 0.087896, 0.433098],
+                 [0.416331, 0.090203, 0.432943],
+                 [0.422549, 0.092501, 0.432714],
+                 [0.428768, 0.094790, 0.432412],
+                 [0.434987, 0.097069, 0.432039],
+                 [0.441207, 0.099338, 0.431594],
+                 [0.447428, 0.101597, 0.431080],
+                 [0.453651, 0.103848, 0.430498],
+                 [0.459875, 0.106089, 0.429846],
+                 [0.466100, 0.108322, 0.429125],
+                 [0.472328, 0.110547, 0.428334],
+                 [0.478558, 0.112764, 0.427475],
+                 [0.484789, 0.114974, 0.426548],
+                 [0.491022, 0.117179, 0.425552],
+                 [0.497257, 0.119379, 0.424488],
+                 [0.503493, 0.121575, 0.423356],
+                 [0.509730, 0.123769, 0.422156],
+                 [0.515967, 0.125960, 0.420887],
+                 [0.522206, 0.128150, 0.419549],
+                 [0.528444, 0.130341, 0.418142],
+                 [0.534683, 0.132534, 0.416667],
+                 [0.540920, 0.134729, 0.415123],
+                 [0.547157, 0.136929, 0.413511],
+                 [0.553392, 0.139134, 0.411829],
+                 [0.559624, 0.141346, 0.410078],
+                 [0.565854, 0.143567, 0.408258],
+                 [0.572081, 0.145797, 0.406369],
+                 [0.578304, 0.148039, 0.404411],
+                 [0.584521, 0.150294, 0.402385],
+                 [0.590734, 0.152563, 0.400290],
+                 [0.596940, 0.154848, 0.398125],
+                 [0.603139, 0.157151, 0.395891],
+                 [0.609330, 0.159474, 0.393589],
+                 [0.615513, 0.161817, 0.391219],
+                 [0.621685, 0.164184, 0.388781],
+                 [0.627847, 0.166575, 0.386276],
+                 [0.633998, 0.168992, 0.383704],
+                 [0.640135, 0.171438, 0.381065],
+                 [0.646260, 0.173914, 0.378359],
+                 [0.652369, 0.176421, 0.375586],
+                 [0.658463, 0.178962, 0.372748],
+                 [0.664540, 0.181539, 0.369846],
+                 [0.670599, 0.184153, 0.366879],
+                 [0.676638, 0.186807, 0.363849],
+                 [0.682656, 0.189501, 0.360757],
+                 [0.688653, 0.192239, 0.357603],
+                 [0.694627, 0.195021, 0.354388],
+                 [0.700576, 0.197851, 0.351113],
+                 [0.706500, 0.200728, 0.347777],
+                 [0.712396, 0.203656, 0.344383],
+                 [0.718264, 0.206636, 0.340931],
+                 [0.724103, 0.209670, 0.337424],
+                 [0.729909, 0.212759, 0.333861],
+                 [0.735683, 0.215906, 0.330245],
+                 [0.741423, 0.219112, 0.326576],
+                 [0.747127, 0.222378, 0.322856],
+                 [0.752794, 0.225706, 0.319085],
+                 [0.758422, 0.229097, 0.315266],
+                 [0.764010, 0.232554, 0.311399],
+                 [0.769556, 0.236077, 0.307485],
+                 [0.775059, 0.239667, 0.303526],
+                 [0.780517, 0.243327, 0.299523],
+                 [0.785929, 0.247056, 0.295477],
+                 [0.791293, 0.250856, 0.291390],
+                 [0.796607, 0.254728, 0.287264],
+                 [0.801871, 0.258674, 0.283099],
+                 [0.807082, 0.262692, 0.278898],
+                 [0.812239, 0.266786, 0.274661],
+                 [0.817341, 0.270954, 0.270390],
+                 [0.822386, 0.275197, 0.266085],
+                 [0.827372, 0.279517, 0.261750],
+                 [0.832299, 0.283913, 0.257383],
+                 [0.837165, 0.288385, 0.252988],
+                 [0.841969, 0.292933, 0.248564],
+                 [0.846709, 0.297559, 0.244113],
+                 [0.851384, 0.302260, 0.239636],
+                 [0.855992, 0.307038, 0.235133],
+                 [0.860533, 0.311892, 0.230606],
+                 [0.865006, 0.316822, 0.226055],
+                 [0.869409, 0.321827, 0.221482],
+                 [0.873741, 0.326906, 0.216886],
+                 [0.878001, 0.332060, 0.212268],
+                 [0.882188, 0.337287, 0.207628],
+                 [0.886302, 0.342586, 0.202968],
+                 [0.890341, 0.347957, 0.198286],
+                 [0.894305, 0.353399, 0.193584],
+                 [0.898192, 0.358911, 0.188860],
+                 [0.902003, 0.364492, 0.184116],
+                 [0.905735, 0.370140, 0.179350],
+                 [0.909390, 0.375856, 0.174563],
+                 [0.912966, 0.381636, 0.169755],
+                 [0.916462, 0.387481, 0.164924],
+                 [0.919879, 0.393389, 0.160070],
+                 [0.923215, 0.399359, 0.155193],
+                 [0.926470, 0.405389, 0.150292],
+                 [0.929644, 0.411479, 0.145367],
+                 [0.932737, 0.417627, 0.140417],
+                 [0.935747, 0.423831, 0.135440],
+                 [0.938675, 0.430091, 0.130438],
+                 [0.941521, 0.436405, 0.125409],
+                 [0.944285, 0.442772, 0.120354],
+                 [0.946965, 0.449191, 0.115272],
+                 [0.949562, 0.455660, 0.110164],
+                 [0.952075, 0.462178, 0.105031],
+                 [0.954506, 0.468744, 0.099874],
+                 [0.956852, 0.475356, 0.094695],
+                 [0.959114, 0.482014, 0.089499],
+                 [0.961293, 0.488716, 0.084289],
+                 [0.963387, 0.495462, 0.079073],
+                 [0.965397, 0.502249, 0.073859],
+                 [0.967322, 0.509078, 0.068659],
+                 [0.969163, 0.515946, 0.063488],
+                 [0.970919, 0.522853, 0.058367],
+                 [0.972590, 0.529798, 0.053324],
+                 [0.974176, 0.536780, 0.048392],
+                 [0.975677, 0.543798, 0.043618],
+                 [0.977092, 0.550850, 0.039050],
+                 [0.978422, 0.557937, 0.034931],
+                 [0.979666, 0.565057, 0.031409],
+                 [0.980824, 0.572209, 0.028508],
+                 [0.981895, 0.579392, 0.026250],
+                 [0.982881, 0.586606, 0.024661],
+                 [0.983779, 0.593849, 0.023770],
+                 [0.984591, 0.601122, 0.023606],
+                 [0.985315, 0.608422, 0.024202],
+                 [0.985952, 0.615750, 0.025592],
+                 [0.986502, 0.623105, 0.027814],
+                 [0.986964, 0.630485, 0.030908],
+                 [0.987337, 0.637890, 0.034916],
+                 [0.987622, 0.645320, 0.039886],
+                 [0.987819, 0.652773, 0.045581],
+                 [0.987926, 0.660250, 0.051750],
+                 [0.987945, 0.667748, 0.058329],
+                 [0.987874, 0.675267, 0.065257],
+                 [0.987714, 0.682807, 0.072489],
+                 [0.987464, 0.690366, 0.079990],
+                 [0.987124, 0.697944, 0.087731],
+                 [0.986694, 0.705540, 0.095694],
+                 [0.986175, 0.713153, 0.103863],
+                 [0.985566, 0.720782, 0.112229],
+                 [0.984865, 0.728427, 0.120785],
+                 [0.984075, 0.736087, 0.129527],
+                 [0.983196, 0.743758, 0.138453],
+                 [0.982228, 0.751442, 0.147565],
+                 [0.981173, 0.759135, 0.156863],
+                 [0.980032, 0.766837, 0.166353],
+                 [0.978806, 0.774545, 0.176037],
+                 [0.977497, 0.782258, 0.185923],
+                 [0.976108, 0.789974, 0.196018],
+                 [0.974638, 0.797692, 0.206332],
+                 [0.973088, 0.805409, 0.216877],
+                 [0.971468, 0.813122, 0.227658],
+                 [0.969783, 0.820825, 0.238686],
+                 [0.968041, 0.828515, 0.249972],
+                 [0.966243, 0.836191, 0.261534],
+                 [0.964394, 0.843848, 0.273391],
+                 [0.962517, 0.851476, 0.285546],
+                 [0.960626, 0.859069, 0.298010],
+                 [0.958720, 0.866624, 0.310820],
+                 [0.956834, 0.874129, 0.323974],
+                 [0.954997, 0.881569, 0.337475],
+                 [0.953215, 0.888942, 0.351369],
+                 [0.951546, 0.896226, 0.365627],
+                 [0.950018, 0.903409, 0.380271],
+                 [0.948683, 0.910473, 0.395289],
+                 [0.947594, 0.917399, 0.410665],
+                 [0.946809, 0.924168, 0.426373],
+                 [0.946392, 0.930761, 0.442367],
+                 [0.946403, 0.937159, 0.458592],
+                 [0.946903, 0.943348, 0.474970],
+                 [0.947937, 0.949318, 0.491426],
+                 [0.949545, 0.955063, 0.507860],
+                 [0.951740, 0.960587, 0.524203],
+                 [0.954529, 0.965896, 0.540361],
+                 [0.957896, 0.971003, 0.556275],
+                 [0.961812, 0.975924, 0.571925],
+                 [0.966249, 0.980678, 0.587206],
+                 [0.971162, 0.985282, 0.602154],
+                 [0.976511, 0.989753, 0.616760],
+                 [0.982257, 0.994109, 0.631017],
+                 [0.988362, 0.998364, 0.644924]]
 
-_plasma_data = [
-    [0.050383, 0.029803, 0.527975],
-    [0.063536, 0.028426, 0.533124],
-    [0.075353, 0.027206, 0.538007],
-    [0.086222, 0.026125, 0.542658],
-    [0.096379, 0.025165, 0.547103],
-    [0.105980, 0.024309, 0.551368],
-    [0.115124, 0.023556, 0.555468],
-    [0.123903, 0.022878, 0.559423],
-    [0.132381, 0.022258, 0.563250],
-    [0.140603, 0.021687, 0.566959],
-    [0.148607, 0.021154, 0.570562],
-    [0.156421, 0.020651, 0.574065],
-    [0.164070, 0.020171, 0.577478],
-    [0.171574, 0.019706, 0.580806],
-    [0.178950, 0.019252, 0.584054],
-    [0.186213, 0.018803, 0.587228],
-    [0.193374, 0.018354, 0.590330],
-    [0.200445, 0.017902, 0.593364],
-    [0.207435, 0.017442, 0.596333],
-    [0.214350, 0.016973, 0.599239],
-    [0.221197, 0.016497, 0.602083],
-    [0.227983, 0.016007, 0.604867],
-    [0.234715, 0.015502, 0.607592],
-    [0.241396, 0.014979, 0.610259],
-    [0.248032, 0.014439, 0.612868],
-    [0.254627, 0.013882, 0.615419],
-    [0.261183, 0.013308, 0.617911],
-    [0.267703, 0.012716, 0.620346],
-    [0.274191, 0.012109, 0.622722],
-    [0.280648, 0.011488, 0.625038],
-    [0.287076, 0.010855, 0.627295],
-    [0.293478, 0.010213, 0.629490],
-    [0.299855, 0.009561, 0.631624],
-    [0.306210, 0.008902, 0.633694],
-    [0.312543, 0.008239, 0.635700],
-    [0.318856, 0.007576, 0.637640],
-    [0.325150, 0.006915, 0.639512],
-    [0.331426, 0.006261, 0.641316],
-    [0.337683, 0.005618, 0.643049],
-    [0.343925, 0.004991, 0.644710],
-    [0.350150, 0.004382, 0.646298],
-    [0.356359, 0.003798, 0.647810],
-    [0.362553, 0.003243, 0.649245],
-    [0.368733, 0.002724, 0.650601],
-    [0.374897, 0.002245, 0.651876],
-    [0.381047, 0.001814, 0.653068],
-    [0.387183, 0.001434, 0.654177],
-    [0.393304, 0.001114, 0.655199],
-    [0.399411, 0.000859, 0.656133],
-    [0.405503, 0.000678, 0.656977],
-    [0.411580, 0.000577, 0.657730],
-    [0.417642, 0.000564, 0.658390],
-    [0.423689, 0.000646, 0.658956],
-    [0.429719, 0.000831, 0.659425],
-    [0.435734, 0.001127, 0.659797],
-    [0.441732, 0.001540, 0.660069],
-    [0.447714, 0.002080, 0.660240],
-    [0.453677, 0.002755, 0.660310],
-    [0.459623, 0.003574, 0.660277],
-    [0.465550, 0.004545, 0.660139],
-    [0.471457, 0.005678, 0.659897],
-    [0.477344, 0.006980, 0.659549],
-    [0.483210, 0.008460, 0.659095],
-    [0.489055, 0.010127, 0.658534],
-    [0.494877, 0.011990, 0.657865],
-    [0.500678, 0.014055, 0.657088],
-    [0.506454, 0.016333, 0.656202],
-    [0.512206, 0.018833, 0.655209],
-    [0.517933, 0.021563, 0.654109],
-    [0.523633, 0.024532, 0.652901],
-    [0.529306, 0.027747, 0.651586],
-    [0.534952, 0.031217, 0.650165],
-    [0.540570, 0.034950, 0.648640],
-    [0.546157, 0.038954, 0.647010],
-    [0.551715, 0.043136, 0.645277],
-    [0.557243, 0.047331, 0.643443],
-    [0.562738, 0.051545, 0.641509],
-    [0.568201, 0.055778, 0.639477],
-    [0.573632, 0.060028, 0.637349],
-    [0.579029, 0.064296, 0.635126],
-    [0.584391, 0.068579, 0.632812],
-    [0.589719, 0.072878, 0.630408],
-    [0.595011, 0.077190, 0.627917],
-    [0.600266, 0.081516, 0.625342],
-    [0.605485, 0.085854, 0.622686],
-    [0.610667, 0.090204, 0.619951],
-    [0.615812, 0.094564, 0.617140],
-    [0.620919, 0.098934, 0.614257],
-    [0.625987, 0.103312, 0.611305],
-    [0.631017, 0.107699, 0.608287],
-    [0.636008, 0.112092, 0.605205],
-    [0.640959, 0.116492, 0.602065],
-    [0.645872, 0.120898, 0.598867],
-    [0.650746, 0.125309, 0.595617],
-    [0.655580, 0.129725, 0.592317],
-    [0.660374, 0.134144, 0.588971],
-    [0.665129, 0.138566, 0.585582],
-    [0.669845, 0.142992, 0.582154],
-    [0.674522, 0.147419, 0.578688],
-    [0.679160, 0.151848, 0.575189],
-    [0.683758, 0.156278, 0.571660],
-    [0.688318, 0.160709, 0.568103],
-    [0.692840, 0.165141, 0.564522],
-    [0.697324, 0.169573, 0.560919],
-    [0.701769, 0.174005, 0.557296],
-    [0.706178, 0.178437, 0.553657],
-    [0.710549, 0.182868, 0.550004],
-    [0.714883, 0.187299, 0.546338],
-    [0.719181, 0.191729, 0.542663],
-    [0.723444, 0.196158, 0.538981],
-    [0.727670, 0.200586, 0.535293],
-    [0.731862, 0.205013, 0.531601],
-    [0.736019, 0.209439, 0.527908],
-    [0.740143, 0.213864, 0.524216],
-    [0.744232, 0.218288, 0.520524],
-    [0.748289, 0.222711, 0.516834],
-    [0.752312, 0.227133, 0.513149],
-    [0.756304, 0.231555, 0.509468],
-    [0.760264, 0.235976, 0.505794],
-    [0.764193, 0.240396, 0.502126],
-    [0.768090, 0.244817, 0.498465],
-    [0.771958, 0.249237, 0.494813],
-    [0.775796, 0.253658, 0.491171],
-    [0.779604, 0.258078, 0.487539],
-    [0.783383, 0.262500, 0.483918],
-    [0.787133, 0.266922, 0.480307],
-    [0.790855, 0.271345, 0.476706],
-    [0.794549, 0.275770, 0.473117],
-    [0.798216, 0.280197, 0.469538],
-    [0.801855, 0.284626, 0.465971],
-    [0.805467, 0.289057, 0.462415],
-    [0.809052, 0.293491, 0.458870],
-    [0.812612, 0.297928, 0.455338],
-    [0.816144, 0.302368, 0.451816],
-    [0.819651, 0.306812, 0.448306],
-    [0.823132, 0.311261, 0.444806],
-    [0.826588, 0.315714, 0.441316],
-    [0.830018, 0.320172, 0.437836],
-    [0.833422, 0.324635, 0.434366],
-    [0.836801, 0.329105, 0.430905],
-    [0.840155, 0.333580, 0.427455],
-    [0.843484, 0.338062, 0.424013],
-    [0.846788, 0.342551, 0.420579],
-    [0.850066, 0.347048, 0.417153],
-    [0.853319, 0.351553, 0.413734],
-    [0.856547, 0.356066, 0.410322],
-    [0.859750, 0.360588, 0.406917],
-    [0.862927, 0.365119, 0.403519],
-    [0.866078, 0.369660, 0.400126],
-    [0.869203, 0.374212, 0.396738],
-    [0.872303, 0.378774, 0.393355],
-    [0.875376, 0.383347, 0.389976],
-    [0.878423, 0.387932, 0.386600],
-    [0.881443, 0.392529, 0.383229],
-    [0.884436, 0.397139, 0.379860],
-    [0.887402, 0.401762, 0.376494],
-    [0.890340, 0.406398, 0.373130],
-    [0.893250, 0.411048, 0.369768],
-    [0.896131, 0.415712, 0.366407],
-    [0.898984, 0.420392, 0.363047],
-    [0.901807, 0.425087, 0.359688],
-    [0.904601, 0.429797, 0.356329],
-    [0.907365, 0.434524, 0.352970],
-    [0.910098, 0.439268, 0.349610],
-    [0.912800, 0.444029, 0.346251],
-    [0.915471, 0.448807, 0.342890],
-    [0.918109, 0.453603, 0.339529],
-    [0.920714, 0.458417, 0.336166],
-    [0.923287, 0.463251, 0.332801],
-    [0.925825, 0.468103, 0.329435],
-    [0.928329, 0.472975, 0.326067],
-    [0.930798, 0.477867, 0.322697],
-    [0.933232, 0.482780, 0.319325],
-    [0.935630, 0.487712, 0.315952],
-    [0.937990, 0.492667, 0.312575],
-    [0.940313, 0.497642, 0.309197],
-    [0.942598, 0.502639, 0.305816],
-    [0.944844, 0.507658, 0.302433],
-    [0.947051, 0.512699, 0.299049],
-    [0.949217, 0.517763, 0.295662],
-    [0.951344, 0.522850, 0.292275],
-    [0.953428, 0.527960, 0.288883],
-    [0.955470, 0.533093, 0.285490],
-    [0.957469, 0.538250, 0.282096],
-    [0.959424, 0.543431, 0.278701],
-    [0.961336, 0.548636, 0.275305],
-    [0.963203, 0.553865, 0.271909],
-    [0.965024, 0.559118, 0.268513],
-    [0.966798, 0.564396, 0.265118],
-    [0.968526, 0.569700, 0.261721],
-    [0.970205, 0.575028, 0.258325],
-    [0.971835, 0.580382, 0.254931],
-    [0.973416, 0.585761, 0.251540],
-    [0.974947, 0.591165, 0.248151],
-    [0.976428, 0.596595, 0.244767],
-    [0.977856, 0.602051, 0.241387],
-    [0.979233, 0.607532, 0.238013],
-    [0.980556, 0.613039, 0.234646],
-    [0.981826, 0.618572, 0.231287],
-    [0.983041, 0.624131, 0.227937],
-    [0.984199, 0.629718, 0.224595],
-    [0.985301, 0.635330, 0.221265],
-    [0.986345, 0.640969, 0.217948],
-    [0.987332, 0.646633, 0.214648],
-    [0.988260, 0.652325, 0.211364],
-    [0.989128, 0.658043, 0.208100],
-    [0.989935, 0.663787, 0.204859],
-    [0.990681, 0.669558, 0.201642],
-    [0.991365, 0.675355, 0.198453],
-    [0.991985, 0.681179, 0.195295],
-    [0.992541, 0.687030, 0.192170],
-    [0.993032, 0.692907, 0.189084],
-    [0.993456, 0.698810, 0.186041],
-    [0.993814, 0.704741, 0.183043],
-    [0.994103, 0.710698, 0.180097],
-    [0.994324, 0.716681, 0.177208],
-    [0.994474, 0.722691, 0.174381],
-    [0.994553, 0.728728, 0.171622],
-    [0.994561, 0.734791, 0.168938],
-    [0.994495, 0.740880, 0.166335],
-    [0.994355, 0.746995, 0.163821],
-    [0.994141, 0.753137, 0.161404],
-    [0.993851, 0.759304, 0.159092],
-    [0.993482, 0.765499, 0.156891],
-    [0.993033, 0.771720, 0.154808],
-    [0.992505, 0.777967, 0.152855],
-    [0.991897, 0.784239, 0.151042],
-    [0.991209, 0.790537, 0.149377],
-    [0.990439, 0.796859, 0.147870],
-    [0.989587, 0.803205, 0.146529],
-    [0.988648, 0.809579, 0.145357],
-    [0.987621, 0.815978, 0.144363],
-    [0.986509, 0.822401, 0.143557],
-    [0.985314, 0.828846, 0.142945],
-    [0.984031, 0.835315, 0.142528],
-    [0.982653, 0.841812, 0.142303],
-    [0.981190, 0.848329, 0.142279],
-    [0.979644, 0.854866, 0.142453],
-    [0.977995, 0.861432, 0.142808],
-    [0.976265, 0.868016, 0.143351],
-    [0.974443, 0.874622, 0.144061],
-    [0.972530, 0.881250, 0.144923],
-    [0.970533, 0.887896, 0.145919],
-    [0.968443, 0.894564, 0.147014],
-    [0.966271, 0.901249, 0.148180],
-    [0.964021, 0.907950, 0.149370],
-    [0.961681, 0.914672, 0.150520],
-    [0.959276, 0.921407, 0.151566],
-    [0.956808, 0.928152, 0.152409],
-    [0.954287, 0.934908, 0.152921],
-    [0.951726, 0.941671, 0.152925],
-    [0.949151, 0.948435, 0.152178],
-    [0.946602, 0.955190, 0.150328],
-    [0.944152, 0.961916, 0.146861],
-    [0.941896, 0.968590, 0.140956],
-    [0.940015, 0.975158, 0.131326],
-]
+_plasma_data = [[0.050383, 0.029803, 0.527975],
+                [0.063536, 0.028426, 0.533124],
+                [0.075353, 0.027206, 0.538007],
+                [0.086222, 0.026125, 0.542658],
+                [0.096379, 0.025165, 0.547103],
+                [0.105980, 0.024309, 0.551368],
+                [0.115124, 0.023556, 0.555468],
+                [0.123903, 0.022878, 0.559423],
+                [0.132381, 0.022258, 0.563250],
+                [0.140603, 0.021687, 0.566959],
+                [0.148607, 0.021154, 0.570562],
+                [0.156421, 0.020651, 0.574065],
+                [0.164070, 0.020171, 0.577478],
+                [0.171574, 0.019706, 0.580806],
+                [0.178950, 0.019252, 0.584054],
+                [0.186213, 0.018803, 0.587228],
+                [0.193374, 0.018354, 0.590330],
+                [0.200445, 0.017902, 0.593364],
+                [0.207435, 0.017442, 0.596333],
+                [0.214350, 0.016973, 0.599239],
+                [0.221197, 0.016497, 0.602083],
+                [0.227983, 0.016007, 0.604867],
+                [0.234715, 0.015502, 0.607592],
+                [0.241396, 0.014979, 0.610259],
+                [0.248032, 0.014439, 0.612868],
+                [0.254627, 0.013882, 0.615419],
+                [0.261183, 0.013308, 0.617911],
+                [0.267703, 0.012716, 0.620346],
+                [0.274191, 0.012109, 0.622722],
+                [0.280648, 0.011488, 0.625038],
+                [0.287076, 0.010855, 0.627295],
+                [0.293478, 0.010213, 0.629490],
+                [0.299855, 0.009561, 0.631624],
+                [0.306210, 0.008902, 0.633694],
+                [0.312543, 0.008239, 0.635700],
+                [0.318856, 0.007576, 0.637640],
+                [0.325150, 0.006915, 0.639512],
+                [0.331426, 0.006261, 0.641316],
+                [0.337683, 0.005618, 0.643049],
+                [0.343925, 0.004991, 0.644710],
+                [0.350150, 0.004382, 0.646298],
+                [0.356359, 0.003798, 0.647810],
+                [0.362553, 0.003243, 0.649245],
+                [0.368733, 0.002724, 0.650601],
+                [0.374897, 0.002245, 0.651876],
+                [0.381047, 0.001814, 0.653068],
+                [0.387183, 0.001434, 0.654177],
+                [0.393304, 0.001114, 0.655199],
+                [0.399411, 0.000859, 0.656133],
+                [0.405503, 0.000678, 0.656977],
+                [0.411580, 0.000577, 0.657730],
+                [0.417642, 0.000564, 0.658390],
+                [0.423689, 0.000646, 0.658956],
+                [0.429719, 0.000831, 0.659425],
+                [0.435734, 0.001127, 0.659797],
+                [0.441732, 0.001540, 0.660069],
+                [0.447714, 0.002080, 0.660240],
+                [0.453677, 0.002755, 0.660310],
+                [0.459623, 0.003574, 0.660277],
+                [0.465550, 0.004545, 0.660139],
+                [0.471457, 0.005678, 0.659897],
+                [0.477344, 0.006980, 0.659549],
+                [0.483210, 0.008460, 0.659095],
+                [0.489055, 0.010127, 0.658534],
+                [0.494877, 0.011990, 0.657865],
+                [0.500678, 0.014055, 0.657088],
+                [0.506454, 0.016333, 0.656202],
+                [0.512206, 0.018833, 0.655209],
+                [0.517933, 0.021563, 0.654109],
+                [0.523633, 0.024532, 0.652901],
+                [0.529306, 0.027747, 0.651586],
+                [0.534952, 0.031217, 0.650165],
+                [0.540570, 0.034950, 0.648640],
+                [0.546157, 0.038954, 0.647010],
+                [0.551715, 0.043136, 0.645277],
+                [0.557243, 0.047331, 0.643443],
+                [0.562738, 0.051545, 0.641509],
+                [0.568201, 0.055778, 0.639477],
+                [0.573632, 0.060028, 0.637349],
+                [0.579029, 0.064296, 0.635126],
+                [0.584391, 0.068579, 0.632812],
+                [0.589719, 0.072878, 0.630408],
+                [0.595011, 0.077190, 0.627917],
+                [0.600266, 0.081516, 0.625342],
+                [0.605485, 0.085854, 0.622686],
+                [0.610667, 0.090204, 0.619951],
+                [0.615812, 0.094564, 0.617140],
+                [0.620919, 0.098934, 0.614257],
+                [0.625987, 0.103312, 0.611305],
+                [0.631017, 0.107699, 0.608287],
+                [0.636008, 0.112092, 0.605205],
+                [0.640959, 0.116492, 0.602065],
+                [0.645872, 0.120898, 0.598867],
+                [0.650746, 0.125309, 0.595617],
+                [0.655580, 0.129725, 0.592317],
+                [0.660374, 0.134144, 0.588971],
+                [0.665129, 0.138566, 0.585582],
+                [0.669845, 0.142992, 0.582154],
+                [0.674522, 0.147419, 0.578688],
+                [0.679160, 0.151848, 0.575189],
+                [0.683758, 0.156278, 0.571660],
+                [0.688318, 0.160709, 0.568103],
+                [0.692840, 0.165141, 0.564522],
+                [0.697324, 0.169573, 0.560919],
+                [0.701769, 0.174005, 0.557296],
+                [0.706178, 0.178437, 0.553657],
+                [0.710549, 0.182868, 0.550004],
+                [0.714883, 0.187299, 0.546338],
+                [0.719181, 0.191729, 0.542663],
+                [0.723444, 0.196158, 0.538981],
+                [0.727670, 0.200586, 0.535293],
+                [0.731862, 0.205013, 0.531601],
+                [0.736019, 0.209439, 0.527908],
+                [0.740143, 0.213864, 0.524216],
+                [0.744232, 0.218288, 0.520524],
+                [0.748289, 0.222711, 0.516834],
+                [0.752312, 0.227133, 0.513149],
+                [0.756304, 0.231555, 0.509468],
+                [0.760264, 0.235976, 0.505794],
+                [0.764193, 0.240396, 0.502126],
+                [0.768090, 0.244817, 0.498465],
+                [0.771958, 0.249237, 0.494813],
+                [0.775796, 0.253658, 0.491171],
+                [0.779604, 0.258078, 0.487539],
+                [0.783383, 0.262500, 0.483918],
+                [0.787133, 0.266922, 0.480307],
+                [0.790855, 0.271345, 0.476706],
+                [0.794549, 0.275770, 0.473117],
+                [0.798216, 0.280197, 0.469538],
+                [0.801855, 0.284626, 0.465971],
+                [0.805467, 0.289057, 0.462415],
+                [0.809052, 0.293491, 0.458870],
+                [0.812612, 0.297928, 0.455338],
+                [0.816144, 0.302368, 0.451816],
+                [0.819651, 0.306812, 0.448306],
+                [0.823132, 0.311261, 0.444806],
+                [0.826588, 0.315714, 0.441316],
+                [0.830018, 0.320172, 0.437836],
+                [0.833422, 0.324635, 0.434366],
+                [0.836801, 0.329105, 0.430905],
+                [0.840155, 0.333580, 0.427455],
+                [0.843484, 0.338062, 0.424013],
+                [0.846788, 0.342551, 0.420579],
+                [0.850066, 0.347048, 0.417153],
+                [0.853319, 0.351553, 0.413734],
+                [0.856547, 0.356066, 0.410322],
+                [0.859750, 0.360588, 0.406917],
+                [0.862927, 0.365119, 0.403519],
+                [0.866078, 0.369660, 0.400126],
+                [0.869203, 0.374212, 0.396738],
+                [0.872303, 0.378774, 0.393355],
+                [0.875376, 0.383347, 0.389976],
+                [0.878423, 0.387932, 0.386600],
+                [0.881443, 0.392529, 0.383229],
+                [0.884436, 0.397139, 0.379860],
+                [0.887402, 0.401762, 0.376494],
+                [0.890340, 0.406398, 0.373130],
+                [0.893250, 0.411048, 0.369768],
+                [0.896131, 0.415712, 0.366407],
+                [0.898984, 0.420392, 0.363047],
+                [0.901807, 0.425087, 0.359688],
+                [0.904601, 0.429797, 0.356329],
+                [0.907365, 0.434524, 0.352970],
+                [0.910098, 0.439268, 0.349610],
+                [0.912800, 0.444029, 0.346251],
+                [0.915471, 0.448807, 0.342890],
+                [0.918109, 0.453603, 0.339529],
+                [0.920714, 0.458417, 0.336166],
+                [0.923287, 0.463251, 0.332801],
+                [0.925825, 0.468103, 0.329435],
+                [0.928329, 0.472975, 0.326067],
+                [0.930798, 0.477867, 0.322697],
+                [0.933232, 0.482780, 0.319325],
+                [0.935630, 0.487712, 0.315952],
+                [0.937990, 0.492667, 0.312575],
+                [0.940313, 0.497642, 0.309197],
+                [0.942598, 0.502639, 0.305816],
+                [0.944844, 0.507658, 0.302433],
+                [0.947051, 0.512699, 0.299049],
+                [0.949217, 0.517763, 0.295662],
+                [0.951344, 0.522850, 0.292275],
+                [0.953428, 0.527960, 0.288883],
+                [0.955470, 0.533093, 0.285490],
+                [0.957469, 0.538250, 0.282096],
+                [0.959424, 0.543431, 0.278701],
+                [0.961336, 0.548636, 0.275305],
+                [0.963203, 0.553865, 0.271909],
+                [0.965024, 0.559118, 0.268513],
+                [0.966798, 0.564396, 0.265118],
+                [0.968526, 0.569700, 0.261721],
+                [0.970205, 0.575028, 0.258325],
+                [0.971835, 0.580382, 0.254931],
+                [0.973416, 0.585761, 0.251540],
+                [0.974947, 0.591165, 0.248151],
+                [0.976428, 0.596595, 0.244767],
+                [0.977856, 0.602051, 0.241387],
+                [0.979233, 0.607532, 0.238013],
+                [0.980556, 0.613039, 0.234646],
+                [0.981826, 0.618572, 0.231287],
+                [0.983041, 0.624131, 0.227937],
+                [0.984199, 0.629718, 0.224595],
+                [0.985301, 0.635330, 0.221265],
+                [0.986345, 0.640969, 0.217948],
+                [0.987332, 0.646633, 0.214648],
+                [0.988260, 0.652325, 0.211364],
+                [0.989128, 0.658043, 0.208100],
+                [0.989935, 0.663787, 0.204859],
+                [0.990681, 0.669558, 0.201642],
+                [0.991365, 0.675355, 0.198453],
+                [0.991985, 0.681179, 0.195295],
+                [0.992541, 0.687030, 0.192170],
+                [0.993032, 0.692907, 0.189084],
+                [0.993456, 0.698810, 0.186041],
+                [0.993814, 0.704741, 0.183043],
+                [0.994103, 0.710698, 0.180097],
+                [0.994324, 0.716681, 0.177208],
+                [0.994474, 0.722691, 0.174381],
+                [0.994553, 0.728728, 0.171622],
+                [0.994561, 0.734791, 0.168938],
+                [0.994495, 0.740880, 0.166335],
+                [0.994355, 0.746995, 0.163821],
+                [0.994141, 0.753137, 0.161404],
+                [0.993851, 0.759304, 0.159092],
+                [0.993482, 0.765499, 0.156891],
+                [0.993033, 0.771720, 0.154808],
+                [0.992505, 0.777967, 0.152855],
+                [0.991897, 0.784239, 0.151042],
+                [0.991209, 0.790537, 0.149377],
+                [0.990439, 0.796859, 0.147870],
+                [0.989587, 0.803205, 0.146529],
+                [0.988648, 0.809579, 0.145357],
+                [0.987621, 0.815978, 0.144363],
+                [0.986509, 0.822401, 0.143557],
+                [0.985314, 0.828846, 0.142945],
+                [0.984031, 0.835315, 0.142528],
+                [0.982653, 0.841812, 0.142303],
+                [0.981190, 0.848329, 0.142279],
+                [0.979644, 0.854866, 0.142453],
+                [0.977995, 0.861432, 0.142808],
+                [0.976265, 0.868016, 0.143351],
+                [0.974443, 0.874622, 0.144061],
+                [0.972530, 0.881250, 0.144923],
+                [0.970533, 0.887896, 0.145919],
+                [0.968443, 0.894564, 0.147014],
+                [0.966271, 0.901249, 0.148180],
+                [0.964021, 0.907950, 0.149370],
+                [0.961681, 0.914672, 0.150520],
+                [0.959276, 0.921407, 0.151566],
+                [0.956808, 0.928152, 0.152409],
+                [0.954287, 0.934908, 0.152921],
+                [0.951726, 0.941671, 0.152925],
+                [0.949151, 0.948435, 0.152178],
+                [0.946602, 0.955190, 0.150328],
+                [0.944152, 0.961916, 0.146861],
+                [0.941896, 0.968590, 0.140956],
+                [0.940015, 0.975158, 0.131326]]
 
-_viridis_data = [
-    [0.267004, 0.004874, 0.329415],
-    [0.268510, 0.009605, 0.335427],
-    [0.269944, 0.014625, 0.341379],
-    [0.271305, 0.019942, 0.347269],
-    [0.272594, 0.025563, 0.353093],
-    [0.273809, 0.031497, 0.358853],
-    [0.274952, 0.037752, 0.364543],
-    [0.276022, 0.044167, 0.370164],
-    [0.277018, 0.050344, 0.375715],
-    [0.277941, 0.056324, 0.381191],
-    [0.278791, 0.062145, 0.386592],
-    [0.279566, 0.067836, 0.391917],
-    [0.280267, 0.073417, 0.397163],
-    [0.280894, 0.078907, 0.402329],
-    [0.281446, 0.084320, 0.407414],
-    [0.281924, 0.089666, 0.412415],
-    [0.282327, 0.094955, 0.417331],
-    [0.282656, 0.100196, 0.422160],
-    [0.282910, 0.105393, 0.426902],
-    [0.283091, 0.110553, 0.431554],
-    [0.283197, 0.115680, 0.436115],
-    [0.283229, 0.120777, 0.440584],
-    [0.283187, 0.125848, 0.444960],
-    [0.283072, 0.130895, 0.449241],
-    [0.282884, 0.135920, 0.453427],
-    [0.282623, 0.140926, 0.457517],
-    [0.282290, 0.145912, 0.461510],
-    [0.281887, 0.150881, 0.465405],
-    [0.281412, 0.155834, 0.469201],
-    [0.280868, 0.160771, 0.472899],
-    [0.280255, 0.165693, 0.476498],
-    [0.279574, 0.170599, 0.479997],
-    [0.278826, 0.175490, 0.483397],
-    [0.278012, 0.180367, 0.486697],
-    [0.277134, 0.185228, 0.489898],
-    [0.276194, 0.190074, 0.493001],
-    [0.275191, 0.194905, 0.496005],
-    [0.274128, 0.199721, 0.498911],
-    [0.273006, 0.204520, 0.501721],
-    [0.271828, 0.209303, 0.504434],
-    [0.270595, 0.214069, 0.507052],
-    [0.269308, 0.218818, 0.509577],
-    [0.267968, 0.223549, 0.512008],
-    [0.266580, 0.228262, 0.514349],
-    [0.265145, 0.232956, 0.516599],
-    [0.263663, 0.237631, 0.518762],
-    [0.262138, 0.242286, 0.520837],
-    [0.260571, 0.246922, 0.522828],
-    [0.258965, 0.251537, 0.524736],
-    [0.257322, 0.256130, 0.526563],
-    [0.255645, 0.260703, 0.528312],
-    [0.253935, 0.265254, 0.529983],
-    [0.252194, 0.269783, 0.531579],
-    [0.250425, 0.274290, 0.533103],
-    [0.248629, 0.278775, 0.534556],
-    [0.246811, 0.283237, 0.535941],
-    [0.244972, 0.287675, 0.537260],
-    [0.243113, 0.292092, 0.538516],
-    [0.241237, 0.296485, 0.539709],
-    [0.239346, 0.300855, 0.540844],
-    [0.237441, 0.305202, 0.541921],
-    [0.235526, 0.309527, 0.542944],
-    [0.233603, 0.313828, 0.543914],
-    [0.231674, 0.318106, 0.544834],
-    [0.229739, 0.322361, 0.545706],
-    [0.227802, 0.326594, 0.546532],
-    [0.225863, 0.330805, 0.547314],
-    [0.223925, 0.334994, 0.548053],
-    [0.221989, 0.339161, 0.548752],
-    [0.220057, 0.343307, 0.549413],
-    [0.218130, 0.347432, 0.550038],
-    [0.216210, 0.351535, 0.550627],
-    [0.214298, 0.355619, 0.551184],
-    [0.212395, 0.359683, 0.551710],
-    [0.210503, 0.363727, 0.552206],
-    [0.208623, 0.367752, 0.552675],
-    [0.206756, 0.371758, 0.553117],
-    [0.204903, 0.375746, 0.553533],
-    [0.203063, 0.379716, 0.553925],
-    [0.201239, 0.383670, 0.554294],
-    [0.199430, 0.387607, 0.554642],
-    [0.197636, 0.391528, 0.554969],
-    [0.195860, 0.395433, 0.555276],
-    [0.194100, 0.399323, 0.555565],
-    [0.192357, 0.403199, 0.555836],
-    [0.190631, 0.407061, 0.556089],
-    [0.188923, 0.410910, 0.556326],
-    [0.187231, 0.414746, 0.556547],
-    [0.185556, 0.418570, 0.556753],
-    [0.183898, 0.422383, 0.556944],
-    [0.182256, 0.426184, 0.557120],
-    [0.180629, 0.429975, 0.557282],
-    [0.179019, 0.433756, 0.557430],
-    [0.177423, 0.437527, 0.557565],
-    [0.175841, 0.441290, 0.557685],
-    [0.174274, 0.445044, 0.557792],
-    [0.172719, 0.448791, 0.557885],
-    [0.171176, 0.452530, 0.557965],
-    [0.169646, 0.456262, 0.558030],
-    [0.168126, 0.459988, 0.558082],
-    [0.166617, 0.463708, 0.558119],
-    [0.165117, 0.467423, 0.558141],
-    [0.163625, 0.471133, 0.558148],
-    [0.162142, 0.474838, 0.558140],
-    [0.160665, 0.478540, 0.558115],
-    [0.159194, 0.482237, 0.558073],
-    [0.157729, 0.485932, 0.558013],
-    [0.156270, 0.489624, 0.557936],
-    [0.154815, 0.493313, 0.557840],
-    [0.153364, 0.497000, 0.557724],
-    [0.151918, 0.500685, 0.557587],
-    [0.150476, 0.504369, 0.557430],
-    [0.149039, 0.508051, 0.557250],
-    [0.147607, 0.511733, 0.557049],
-    [0.146180, 0.515413, 0.556823],
-    [0.144759, 0.519093, 0.556572],
-    [0.143343, 0.522773, 0.556295],
-    [0.141935, 0.526453, 0.555991],
-    [0.140536, 0.530132, 0.555659],
-    [0.139147, 0.533812, 0.555298],
-    [0.137770, 0.537492, 0.554906],
-    [0.136408, 0.541173, 0.554483],
-    [0.135066, 0.544853, 0.554029],
-    [0.133743, 0.548535, 0.553541],
-    [0.132444, 0.552216, 0.553018],
-    [0.131172, 0.555899, 0.552459],
-    [0.129933, 0.559582, 0.551864],
-    [0.128729, 0.563265, 0.551229],
-    [0.127568, 0.566949, 0.550556],
-    [0.126453, 0.570633, 0.549841],
-    [0.125394, 0.574318, 0.549086],
-    [0.124395, 0.578002, 0.548287],
-    [0.123463, 0.581687, 0.547445],
-    [0.122606, 0.585371, 0.546557],
-    [0.121831, 0.589055, 0.545623],
-    [0.121148, 0.592739, 0.544641],
-    [0.120565, 0.596422, 0.543611],
-    [0.120092, 0.600104, 0.542530],
-    [0.119738, 0.603785, 0.541400],
-    [0.119512, 0.607464, 0.540218],
-    [0.119423, 0.611141, 0.538982],
-    [0.119483, 0.614817, 0.537692],
-    [0.119699, 0.618490, 0.536347],
-    [0.120081, 0.622161, 0.534946],
-    [0.120638, 0.625828, 0.533488],
-    [0.121380, 0.629492, 0.531973],
-    [0.122312, 0.633153, 0.530398],
-    [0.123444, 0.636809, 0.528763],
-    [0.124780, 0.640461, 0.527068],
-    [0.126326, 0.644107, 0.525311],
-    [0.128087, 0.647749, 0.523491],
-    [0.130067, 0.651384, 0.521608],
-    [0.132268, 0.655014, 0.519661],
-    [0.134692, 0.658636, 0.517649],
-    [0.137339, 0.662252, 0.515571],
-    [0.140210, 0.665859, 0.513427],
-    [0.143303, 0.669459, 0.511215],
-    [0.146616, 0.673050, 0.508936],
-    [0.150148, 0.676631, 0.506589],
-    [0.153894, 0.680203, 0.504172],
-    [0.157851, 0.683765, 0.501686],
-    [0.162016, 0.687316, 0.499129],
-    [0.166383, 0.690856, 0.496502],
-    [0.170948, 0.694384, 0.493803],
-    [0.175707, 0.697900, 0.491033],
-    [0.180653, 0.701402, 0.488189],
-    [0.185783, 0.704891, 0.485273],
-    [0.191090, 0.708366, 0.482284],
-    [0.196571, 0.711827, 0.479221],
-    [0.202219, 0.715272, 0.476084],
-    [0.208030, 0.718701, 0.472873],
-    [0.214000, 0.722114, 0.469588],
-    [0.220124, 0.725509, 0.466226],
-    [0.226397, 0.728888, 0.462789],
-    [0.232815, 0.732247, 0.459277],
-    [0.239374, 0.735588, 0.455688],
-    [0.246070, 0.738910, 0.452024],
-    [0.252899, 0.742211, 0.448284],
-    [0.259857, 0.745492, 0.444467],
-    [0.266941, 0.748751, 0.440573],
-    [0.274149, 0.751988, 0.436601],
-    [0.281477, 0.755203, 0.432552],
-    [0.288921, 0.758394, 0.428426],
-    [0.296479, 0.761561, 0.424223],
-    [0.304148, 0.764704, 0.419943],
-    [0.311925, 0.767822, 0.415586],
-    [0.319809, 0.770914, 0.411152],
-    [0.327796, 0.773980, 0.406640],
-    [0.335885, 0.777018, 0.402049],
-    [0.344074, 0.780029, 0.397381],
-    [0.352360, 0.783011, 0.392636],
-    [0.360741, 0.785964, 0.387814],
-    [0.369214, 0.788888, 0.382914],
-    [0.377779, 0.791781, 0.377939],
-    [0.386433, 0.794644, 0.372886],
-    [0.395174, 0.797475, 0.367757],
-    [0.404001, 0.800275, 0.362552],
-    [0.412913, 0.803041, 0.357269],
-    [0.421908, 0.805774, 0.351910],
-    [0.430983, 0.808473, 0.346476],
-    [0.440137, 0.811138, 0.340967],
-    [0.449368, 0.813768, 0.335384],
-    [0.458674, 0.816363, 0.329727],
-    [0.468053, 0.818921, 0.323998],
-    [0.477504, 0.821444, 0.318195],
-    [0.487026, 0.823929, 0.312321],
-    [0.496615, 0.826376, 0.306377],
-    [0.506271, 0.828786, 0.300362],
-    [0.515992, 0.831158, 0.294279],
-    [0.525776, 0.833491, 0.288127],
-    [0.535621, 0.835785, 0.281908],
-    [0.545524, 0.838039, 0.275626],
-    [0.555484, 0.840254, 0.269281],
-    [0.565498, 0.842430, 0.262877],
-    [0.575563, 0.844566, 0.256415],
-    [0.585678, 0.846661, 0.249897],
-    [0.595839, 0.848717, 0.243329],
-    [0.606045, 0.850733, 0.236712],
-    [0.616293, 0.852709, 0.230052],
-    [0.626579, 0.854645, 0.223353],
-    [0.636902, 0.856542, 0.216620],
-    [0.647257, 0.858400, 0.209861],
-    [0.657642, 0.860219, 0.203082],
-    [0.668054, 0.861999, 0.196293],
-    [0.678489, 0.863742, 0.189503],
-    [0.688944, 0.865448, 0.182725],
-    [0.699415, 0.867117, 0.175971],
-    [0.709898, 0.868751, 0.169257],
-    [0.720391, 0.870350, 0.162603],
-    [0.730889, 0.871916, 0.156029],
-    [0.741388, 0.873449, 0.149561],
-    [0.751884, 0.874951, 0.143228],
-    [0.762373, 0.876424, 0.137064],
-    [0.772852, 0.877868, 0.131109],
-    [0.783315, 0.879285, 0.125405],
-    [0.793760, 0.880678, 0.120005],
-    [0.804182, 0.882046, 0.114965],
-    [0.814576, 0.883393, 0.110347],
-    [0.824940, 0.884720, 0.106217],
-    [0.835270, 0.886029, 0.102646],
-    [0.845561, 0.887322, 0.099702],
-    [0.855810, 0.888601, 0.097452],
-    [0.866013, 0.889868, 0.095953],
-    [0.876168, 0.891125, 0.095250],
-    [0.886271, 0.892374, 0.095374],
-    [0.896320, 0.893616, 0.096335],
-    [0.906311, 0.894855, 0.098125],
-    [0.916242, 0.896091, 0.100717],
-    [0.926106, 0.897330, 0.104071],
-    [0.935904, 0.898570, 0.108131],
-    [0.945636, 0.899815, 0.112838],
-    [0.955300, 0.901065, 0.118128],
-    [0.964894, 0.902323, 0.123941],
-    [0.974417, 0.903590, 0.130215],
-    [0.983868, 0.904867, 0.136897],
-    [0.993248, 0.906157, 0.143936],
-]
+_viridis_data = [[0.267004, 0.004874, 0.329415],
+                 [0.268510, 0.009605, 0.335427],
+                 [0.269944, 0.014625, 0.341379],
+                 [0.271305, 0.019942, 0.347269],
+                 [0.272594, 0.025563, 0.353093],
+                 [0.273809, 0.031497, 0.358853],
+                 [0.274952, 0.037752, 0.364543],
+                 [0.276022, 0.044167, 0.370164],
+                 [0.277018, 0.050344, 0.375715],
+                 [0.277941, 0.056324, 0.381191],
+                 [0.278791, 0.062145, 0.386592],
+                 [0.279566, 0.067836, 0.391917],
+                 [0.280267, 0.073417, 0.397163],
+                 [0.280894, 0.078907, 0.402329],
+                 [0.281446, 0.084320, 0.407414],
+                 [0.281924, 0.089666, 0.412415],
+                 [0.282327, 0.094955, 0.417331],
+                 [0.282656, 0.100196, 0.422160],
+                 [0.282910, 0.105393, 0.426902],
+                 [0.283091, 0.110553, 0.431554],
+                 [0.283197, 0.115680, 0.436115],
+                 [0.283229, 0.120777, 0.440584],
+                 [0.283187, 0.125848, 0.444960],
+                 [0.283072, 0.130895, 0.449241],
+                 [0.282884, 0.135920, 0.453427],
+                 [0.282623, 0.140926, 0.457517],
+                 [0.282290, 0.145912, 0.461510],
+                 [0.281887, 0.150881, 0.465405],
+                 [0.281412, 0.155834, 0.469201],
+                 [0.280868, 0.160771, 0.472899],
+                 [0.280255, 0.165693, 0.476498],
+                 [0.279574, 0.170599, 0.479997],
+                 [0.278826, 0.175490, 0.483397],
+                 [0.278012, 0.180367, 0.486697],
+                 [0.277134, 0.185228, 0.489898],
+                 [0.276194, 0.190074, 0.493001],
+                 [0.275191, 0.194905, 0.496005],
+                 [0.274128, 0.199721, 0.498911],
+                 [0.273006, 0.204520, 0.501721],
+                 [0.271828, 0.209303, 0.504434],
+                 [0.270595, 0.214069, 0.507052],
+                 [0.269308, 0.218818, 0.509577],
+                 [0.267968, 0.223549, 0.512008],
+                 [0.266580, 0.228262, 0.514349],
+                 [0.265145, 0.232956, 0.516599],
+                 [0.263663, 0.237631, 0.518762],
+                 [0.262138, 0.242286, 0.520837],
+                 [0.260571, 0.246922, 0.522828],
+                 [0.258965, 0.251537, 0.524736],
+                 [0.257322, 0.256130, 0.526563],
+                 [0.255645, 0.260703, 0.528312],
+                 [0.253935, 0.265254, 0.529983],
+                 [0.252194, 0.269783, 0.531579],
+                 [0.250425, 0.274290, 0.533103],
+                 [0.248629, 0.278775, 0.534556],
+                 [0.246811, 0.283237, 0.535941],
+                 [0.244972, 0.287675, 0.537260],
+                 [0.243113, 0.292092, 0.538516],
+                 [0.241237, 0.296485, 0.539709],
+                 [0.239346, 0.300855, 0.540844],
+                 [0.237441, 0.305202, 0.541921],
+                 [0.235526, 0.309527, 0.542944],
+                 [0.233603, 0.313828, 0.543914],
+                 [0.231674, 0.318106, 0.544834],
+                 [0.229739, 0.322361, 0.545706],
+                 [0.227802, 0.326594, 0.546532],
+                 [0.225863, 0.330805, 0.547314],
+                 [0.223925, 0.334994, 0.548053],
+                 [0.221989, 0.339161, 0.548752],
+                 [0.220057, 0.343307, 0.549413],
+                 [0.218130, 0.347432, 0.550038],
+                 [0.216210, 0.351535, 0.550627],
+                 [0.214298, 0.355619, 0.551184],
+                 [0.212395, 0.359683, 0.551710],
+                 [0.210503, 0.363727, 0.552206],
+                 [0.208623, 0.367752, 0.552675],
+                 [0.206756, 0.371758, 0.553117],
+                 [0.204903, 0.375746, 0.553533],
+                 [0.203063, 0.379716, 0.553925],
+                 [0.201239, 0.383670, 0.554294],
+                 [0.199430, 0.387607, 0.554642],
+                 [0.197636, 0.391528, 0.554969],
+                 [0.195860, 0.395433, 0.555276],
+                 [0.194100, 0.399323, 0.555565],
+                 [0.192357, 0.403199, 0.555836],
+                 [0.190631, 0.407061, 0.556089],
+                 [0.188923, 0.410910, 0.556326],
+                 [0.187231, 0.414746, 0.556547],
+                 [0.185556, 0.418570, 0.556753],
+                 [0.183898, 0.422383, 0.556944],
+                 [0.182256, 0.426184, 0.557120],
+                 [0.180629, 0.429975, 0.557282],
+                 [0.179019, 0.433756, 0.557430],
+                 [0.177423, 0.437527, 0.557565],
+                 [0.175841, 0.441290, 0.557685],
+                 [0.174274, 0.445044, 0.557792],
+                 [0.172719, 0.448791, 0.557885],
+                 [0.171176, 0.452530, 0.557965],
+                 [0.169646, 0.456262, 0.558030],
+                 [0.168126, 0.459988, 0.558082],
+                 [0.166617, 0.463708, 0.558119],
+                 [0.165117, 0.467423, 0.558141],
+                 [0.163625, 0.471133, 0.558148],
+                 [0.162142, 0.474838, 0.558140],
+                 [0.160665, 0.478540, 0.558115],
+                 [0.159194, 0.482237, 0.558073],
+                 [0.157729, 0.485932, 0.558013],
+                 [0.156270, 0.489624, 0.557936],
+                 [0.154815, 0.493313, 0.557840],
+                 [0.153364, 0.497000, 0.557724],
+                 [0.151918, 0.500685, 0.557587],
+                 [0.150476, 0.504369, 0.557430],
+                 [0.149039, 0.508051, 0.557250],
+                 [0.147607, 0.511733, 0.557049],
+                 [0.146180, 0.515413, 0.556823],
+                 [0.144759, 0.519093, 0.556572],
+                 [0.143343, 0.522773, 0.556295],
+                 [0.141935, 0.526453, 0.555991],
+                 [0.140536, 0.530132, 0.555659],
+                 [0.139147, 0.533812, 0.555298],
+                 [0.137770, 0.537492, 0.554906],
+                 [0.136408, 0.541173, 0.554483],
+                 [0.135066, 0.544853, 0.554029],
+                 [0.133743, 0.548535, 0.553541],
+                 [0.132444, 0.552216, 0.553018],
+                 [0.131172, 0.555899, 0.552459],
+                 [0.129933, 0.559582, 0.551864],
+                 [0.128729, 0.563265, 0.551229],
+                 [0.127568, 0.566949, 0.550556],
+                 [0.126453, 0.570633, 0.549841],
+                 [0.125394, 0.574318, 0.549086],
+                 [0.124395, 0.578002, 0.548287],
+                 [0.123463, 0.581687, 0.547445],
+                 [0.122606, 0.585371, 0.546557],
+                 [0.121831, 0.589055, 0.545623],
+                 [0.121148, 0.592739, 0.544641],
+                 [0.120565, 0.596422, 0.543611],
+                 [0.120092, 0.600104, 0.542530],
+                 [0.119738, 0.603785, 0.541400],
+                 [0.119512, 0.607464, 0.540218],
+                 [0.119423, 0.611141, 0.538982],
+                 [0.119483, 0.614817, 0.537692],
+                 [0.119699, 0.618490, 0.536347],
+                 [0.120081, 0.622161, 0.534946],
+                 [0.120638, 0.625828, 0.533488],
+                 [0.121380, 0.629492, 0.531973],
+                 [0.122312, 0.633153, 0.530398],
+                 [0.123444, 0.636809, 0.528763],
+                 [0.124780, 0.640461, 0.527068],
+                 [0.126326, 0.644107, 0.525311],
+                 [0.128087, 0.647749, 0.523491],
+                 [0.130067, 0.651384, 0.521608],
+                 [0.132268, 0.655014, 0.519661],
+                 [0.134692, 0.658636, 0.517649],
+                 [0.137339, 0.662252, 0.515571],
+                 [0.140210, 0.665859, 0.513427],
+                 [0.143303, 0.669459, 0.511215],
+                 [0.146616, 0.673050, 0.508936],
+                 [0.150148, 0.676631, 0.506589],
+                 [0.153894, 0.680203, 0.504172],
+                 [0.157851, 0.683765, 0.501686],
+                 [0.162016, 0.687316, 0.499129],
+                 [0.166383, 0.690856, 0.496502],
+                 [0.170948, 0.694384, 0.493803],
+                 [0.175707, 0.697900, 0.491033],
+                 [0.180653, 0.701402, 0.488189],
+                 [0.185783, 0.704891, 0.485273],
+                 [0.191090, 0.708366, 0.482284],
+                 [0.196571, 0.711827, 0.479221],
+                 [0.202219, 0.715272, 0.476084],
+                 [0.208030, 0.718701, 0.472873],
+                 [0.214000, 0.722114, 0.469588],
+                 [0.220124, 0.725509, 0.466226],
+                 [0.226397, 0.728888, 0.462789],
+                 [0.232815, 0.732247, 0.459277],
+                 [0.239374, 0.735588, 0.455688],
+                 [0.246070, 0.738910, 0.452024],
+                 [0.252899, 0.742211, 0.448284],
+                 [0.259857, 0.745492, 0.444467],
+                 [0.266941, 0.748751, 0.440573],
+                 [0.274149, 0.751988, 0.436601],
+                 [0.281477, 0.755203, 0.432552],
+                 [0.288921, 0.758394, 0.428426],
+                 [0.296479, 0.761561, 0.424223],
+                 [0.304148, 0.764704, 0.419943],
+                 [0.311925, 0.767822, 0.415586],
+                 [0.319809, 0.770914, 0.411152],
+                 [0.327796, 0.773980, 0.406640],
+                 [0.335885, 0.777018, 0.402049],
+                 [0.344074, 0.780029, 0.397381],
+                 [0.352360, 0.783011, 0.392636],
+                 [0.360741, 0.785964, 0.387814],
+                 [0.369214, 0.788888, 0.382914],
+                 [0.377779, 0.791781, 0.377939],
+                 [0.386433, 0.794644, 0.372886],
+                 [0.395174, 0.797475, 0.367757],
+                 [0.404001, 0.800275, 0.362552],
+                 [0.412913, 0.803041, 0.357269],
+                 [0.421908, 0.805774, 0.351910],
+                 [0.430983, 0.808473, 0.346476],
+                 [0.440137, 0.811138, 0.340967],
+                 [0.449368, 0.813768, 0.335384],
+                 [0.458674, 0.816363, 0.329727],
+                 [0.468053, 0.818921, 0.323998],
+                 [0.477504, 0.821444, 0.318195],
+                 [0.487026, 0.823929, 0.312321],
+                 [0.496615, 0.826376, 0.306377],
+                 [0.506271, 0.828786, 0.300362],
+                 [0.515992, 0.831158, 0.294279],
+                 [0.525776, 0.833491, 0.288127],
+                 [0.535621, 0.835785, 0.281908],
+                 [0.545524, 0.838039, 0.275626],
+                 [0.555484, 0.840254, 0.269281],
+                 [0.565498, 0.842430, 0.262877],
+                 [0.575563, 0.844566, 0.256415],
+                 [0.585678, 0.846661, 0.249897],
+                 [0.595839, 0.848717, 0.243329],
+                 [0.606045, 0.850733, 0.236712],
+                 [0.616293, 0.852709, 0.230052],
+                 [0.626579, 0.854645, 0.223353],
+                 [0.636902, 0.856542, 0.216620],
+                 [0.647257, 0.858400, 0.209861],
+                 [0.657642, 0.860219, 0.203082],
+                 [0.668054, 0.861999, 0.196293],
+                 [0.678489, 0.863742, 0.189503],
+                 [0.688944, 0.865448, 0.182725],
+                 [0.699415, 0.867117, 0.175971],
+                 [0.709898, 0.868751, 0.169257],
+                 [0.720391, 0.870350, 0.162603],
+                 [0.730889, 0.871916, 0.156029],
+                 [0.741388, 0.873449, 0.149561],
+                 [0.751884, 0.874951, 0.143228],
+                 [0.762373, 0.876424, 0.137064],
+                 [0.772852, 0.877868, 0.131109],
+                 [0.783315, 0.879285, 0.125405],
+                 [0.793760, 0.880678, 0.120005],
+                 [0.804182, 0.882046, 0.114965],
+                 [0.814576, 0.883393, 0.110347],
+                 [0.824940, 0.884720, 0.106217],
+                 [0.835270, 0.886029, 0.102646],
+                 [0.845561, 0.887322, 0.099702],
+                 [0.855810, 0.888601, 0.097452],
+                 [0.866013, 0.889868, 0.095953],
+                 [0.876168, 0.891125, 0.095250],
+                 [0.886271, 0.892374, 0.095374],
+                 [0.896320, 0.893616, 0.096335],
+                 [0.906311, 0.894855, 0.098125],
+                 [0.916242, 0.896091, 0.100717],
+                 [0.926106, 0.897330, 0.104071],
+                 [0.935904, 0.898570, 0.108131],
+                 [0.945636, 0.899815, 0.112838],
+                 [0.955300, 0.901065, 0.118128],
+                 [0.964894, 0.902323, 0.123941],
+                 [0.974417, 0.903590, 0.130215],
+                 [0.983868, 0.904867, 0.136897],
+                 [0.993248, 0.906157, 0.143936]]
 
-_cividis_data = [
-    [0.000000, 0.135112, 0.304751],
-    [0.000000, 0.138068, 0.311105],
-    [0.000000, 0.141013, 0.317579],
-    [0.000000, 0.143951, 0.323982],
-    [0.000000, 0.146877, 0.330479],
-    [0.000000, 0.149791, 0.337065],
-    [0.000000, 0.152673, 0.343704],
-    [0.000000, 0.155377, 0.350500],
-    [0.000000, 0.157932, 0.357521],
-    [0.000000, 0.160495, 0.364534],
-    [0.000000, 0.163058, 0.371608],
-    [0.000000, 0.165621, 0.378769],
-    [0.000000, 0.168204, 0.385902],
-    [0.000000, 0.170800, 0.393100],
-    [0.000000, 0.173420, 0.400353],
-    [0.000000, 0.176082, 0.407577],
-    [0.000000, 0.178802, 0.414764],
-    [0.000000, 0.181610, 0.421859],
-    [0.000000, 0.184550, 0.428802],
-    [0.000000, 0.186915, 0.435532],
-    [0.000000, 0.188769, 0.439563],
-    [0.000000, 0.190950, 0.441085],
-    [0.000000, 0.193366, 0.441561],
-    [0.003602, 0.195911, 0.441564],
-    [0.017852, 0.198528, 0.441248],
-    [0.032110, 0.201199, 0.440785],
-    [0.046205, 0.203903, 0.440196],
-    [0.058378, 0.206629, 0.439531],
-    [0.068968, 0.209372, 0.438863],
-    [0.078624, 0.212122, 0.438105],
-    [0.087465, 0.214879, 0.437342],
-    [0.095645, 0.217643, 0.436593],
-    [0.103401, 0.220406, 0.435790],
-    [0.110658, 0.223170, 0.435067],
-    [0.117612, 0.225935, 0.434308],
-    [0.124291, 0.228697, 0.433547],
-    [0.130669, 0.231458, 0.432840],
-    [0.136830, 0.234216, 0.432148],
-    [0.142852, 0.236972, 0.431404],
-    [0.148638, 0.239724, 0.430752],
-    [0.154261, 0.242475, 0.430120],
-    [0.159733, 0.245221, 0.429528],
-    [0.165113, 0.247965, 0.428908],
-    [0.170362, 0.250707, 0.428325],
-    [0.175490, 0.253444, 0.427790],
-    [0.180503, 0.256180, 0.427299],
-    [0.185453, 0.258914, 0.426788],
-    [0.190303, 0.261644, 0.426329],
-    [0.195057, 0.264372, 0.425924],
-    [0.199764, 0.267099, 0.425497],
-    [0.204385, 0.269823, 0.425126],
-    [0.208926, 0.272546, 0.424809],
-    [0.213431, 0.275266, 0.424480],
-    [0.217863, 0.277985, 0.424206],
-    [0.222264, 0.280702, 0.423914],
-    [0.226598, 0.283419, 0.423678],
-    [0.230871, 0.286134, 0.423498],
-    [0.235120, 0.288848, 0.423304],
-    [0.239312, 0.291562, 0.423167],
-    [0.243485, 0.294274, 0.423014],
-    [0.247605, 0.296986, 0.422917],
-    [0.251675, 0.299698, 0.422873],
-    [0.255731, 0.302409, 0.422814],
-    [0.259740, 0.305120, 0.422810],
-    [0.263738, 0.307831, 0.422789],
-    [0.267693, 0.310542, 0.422821],
-    [0.271639, 0.313253, 0.422837],
-    [0.275513, 0.315965, 0.422979],
-    [0.279411, 0.318677, 0.423031],
-    [0.283240, 0.321390, 0.423211],
-    [0.287065, 0.324103, 0.423373],
-    [0.290884, 0.326816, 0.423517],
-    [0.294669, 0.329531, 0.423716],
-    [0.298421, 0.332247, 0.423973],
-    [0.302169, 0.334963, 0.424213],
-    [0.305886, 0.337681, 0.424512],
-    [0.309601, 0.340399, 0.424790],
-    [0.313287, 0.343120, 0.425120],
-    [0.316941, 0.345842, 0.425512],
-    [0.320595, 0.348565, 0.425889],
-    [0.324250, 0.351289, 0.426250],
-    [0.327875, 0.354016, 0.426670],
-    [0.331474, 0.356744, 0.427144],
-    [0.335073, 0.359474, 0.427605],
-    [0.338673, 0.362206, 0.428053],
-    [0.342246, 0.364939, 0.428559],
-    [0.345793, 0.367676, 0.429127],
-    [0.349341, 0.370414, 0.429685],
-    [0.352892, 0.373153, 0.430226],
-    [0.356418, 0.375896, 0.430823],
-    [0.359916, 0.378641, 0.431501],
-    [0.363446, 0.381388, 0.432075],
-    [0.366923, 0.384139, 0.432796],
-    [0.370430, 0.386890, 0.433428],
-    [0.373884, 0.389646, 0.434209],
-    [0.377371, 0.392404, 0.434890],
-    [0.380830, 0.395164, 0.435653],
-    [0.384268, 0.397928, 0.436475],
-    [0.387705, 0.400694, 0.437305],
-    [0.391151, 0.403464, 0.438096],
-    [0.394568, 0.406236, 0.438986],
-    [0.397991, 0.409011, 0.439848],
-    [0.401418, 0.411790, 0.440708],
-    [0.404820, 0.414572, 0.441642],
-    [0.408226, 0.417357, 0.442570],
-    [0.411607, 0.420145, 0.443577],
-    [0.414992, 0.422937, 0.444578],
-    [0.418383, 0.425733, 0.445560],
-    [0.421748, 0.428531, 0.446640],
-    [0.425120, 0.431334, 0.447692],
-    [0.428462, 0.434140, 0.448864],
-    [0.431817, 0.436950, 0.449982],
-    [0.435168, 0.439763, 0.451134],
-    [0.438504, 0.442580, 0.452341],
-    [0.441810, 0.445402, 0.453659],
-    [0.445148, 0.448226, 0.454885],
-    [0.448447, 0.451053, 0.456264],
-    [0.451759, 0.453887, 0.457582],
-    [0.455072, 0.456718, 0.458976],
-    [0.458366, 0.459552, 0.460457],
-    [0.461616, 0.462405, 0.461969],
-    [0.464947, 0.465241, 0.463395],
-    [0.468254, 0.468083, 0.464908],
-    [0.471501, 0.470960, 0.466357],
-    [0.474812, 0.473832, 0.467681],
-    [0.478186, 0.476699, 0.468845],
-    [0.481622, 0.479573, 0.469767],
-    [0.485141, 0.482451, 0.470384],
-    [0.488697, 0.485318, 0.471008],
-    [0.492278, 0.488198, 0.471453],
-    [0.495913, 0.491076, 0.471751],
-    [0.499552, 0.493960, 0.472032],
-    [0.503185, 0.496851, 0.472305],
-    [0.506866, 0.499743, 0.472432],
-    [0.510540, 0.502643, 0.472550],
-    [0.514226, 0.505546, 0.472640],
-    [0.517920, 0.508454, 0.472707],
-    [0.521643, 0.511367, 0.472639],
-    [0.525348, 0.514285, 0.472660],
-    [0.529086, 0.517207, 0.472543],
-    [0.532829, 0.520135, 0.472401],
-    [0.536553, 0.523067, 0.472352],
-    [0.540307, 0.526005, 0.472163],
-    [0.544069, 0.528948, 0.471947],
-    [0.547840, 0.531895, 0.471704],
-    [0.551612, 0.534849, 0.471439],
-    [0.555393, 0.537807, 0.471147],
-    [0.559181, 0.540771, 0.470829],
-    [0.562972, 0.543741, 0.470488],
-    [0.566802, 0.546715, 0.469988],
-    [0.570607, 0.549695, 0.469593],
-    [0.574417, 0.552682, 0.469172],
-    [0.578236, 0.555673, 0.468724],
-    [0.582087, 0.558670, 0.468118],
-    [0.585916, 0.561674, 0.467618],
-    [0.589753, 0.564682, 0.467090],
-    [0.593622, 0.567697, 0.466401],
-    [0.597469, 0.570718, 0.465821],
-    [0.601354, 0.573743, 0.465074],
-    [0.605211, 0.576777, 0.464441],
-    [0.609105, 0.579816, 0.463638],
-    [0.612977, 0.582861, 0.462950],
-    [0.616852, 0.585913, 0.462237],
-    [0.620765, 0.588970, 0.461351],
-    [0.624654, 0.592034, 0.460583],
-    [0.628576, 0.595104, 0.459641],
-    [0.632506, 0.598180, 0.458668],
-    [0.636412, 0.601264, 0.457818],
-    [0.640352, 0.604354, 0.456791],
-    [0.644270, 0.607450, 0.455886],
-    [0.648222, 0.610553, 0.454801],
-    [0.652178, 0.613664, 0.453689],
-    [0.656114, 0.616780, 0.452702],
-    [0.660082, 0.619904, 0.451534],
-    [0.664055, 0.623034, 0.450338],
-    [0.668008, 0.626171, 0.449270],
-    [0.671991, 0.629316, 0.448018],
-    [0.675981, 0.632468, 0.446736],
-    [0.679979, 0.635626, 0.445424],
-    [0.683950, 0.638793, 0.444251],
-    [0.687957, 0.641966, 0.442886],
-    [0.691971, 0.645145, 0.441491],
-    [0.695985, 0.648334, 0.440072],
-    [0.700008, 0.651529, 0.438624],
-    [0.704037, 0.654731, 0.437147],
-    [0.708067, 0.657942, 0.435647],
-    [0.712105, 0.661160, 0.434117],
-    [0.716177, 0.664384, 0.432386],
-    [0.720222, 0.667618, 0.430805],
-    [0.724274, 0.670859, 0.429194],
-    [0.728334, 0.674107, 0.427554],
-    [0.732422, 0.677364, 0.425717],
-    [0.736488, 0.680629, 0.424028],
-    [0.740589, 0.683900, 0.422131],
-    [0.744664, 0.687181, 0.420393],
-    [0.748772, 0.690470, 0.418448],
-    [0.752886, 0.693766, 0.416472],
-    [0.756975, 0.697071, 0.414659],
-    [0.761096, 0.700384, 0.412638],
-    [0.765223, 0.703705, 0.410587],
-    [0.769353, 0.707035, 0.408516],
-    [0.773486, 0.710373, 0.406422],
-    [0.777651, 0.713719, 0.404112],
-    [0.781795, 0.717074, 0.401966],
-    [0.785965, 0.720438, 0.399613],
-    [0.790116, 0.723810, 0.397423],
-    [0.794298, 0.727190, 0.395016],
-    [0.798480, 0.730580, 0.392597],
-    [0.802667, 0.733978, 0.390153],
-    [0.806859, 0.737385, 0.387684],
-    [0.811054, 0.740801, 0.385198],
-    [0.815274, 0.744226, 0.382504],
-    [0.819499, 0.747659, 0.379785],
-    [0.823729, 0.751101, 0.377043],
-    [0.827959, 0.754553, 0.374292],
-    [0.832192, 0.758014, 0.371529],
-    [0.836429, 0.761483, 0.368747],
-    [0.840693, 0.764962, 0.365746],
-    [0.844957, 0.768450, 0.362741],
-    [0.849223, 0.771947, 0.359729],
-    [0.853515, 0.775454, 0.356500],
-    [0.857809, 0.778969, 0.353259],
-    [0.862105, 0.782494, 0.350011],
-    [0.866421, 0.786028, 0.346571],
-    [0.870717, 0.789572, 0.343333],
-    [0.875057, 0.793125, 0.339685],
-    [0.879378, 0.796687, 0.336241],
-    [0.883720, 0.800258, 0.332599],
-    [0.888081, 0.803839, 0.328770],
-    [0.892440, 0.807430, 0.324968],
-    [0.896818, 0.811030, 0.320982],
-    [0.901195, 0.814639, 0.317021],
-    [0.905589, 0.818257, 0.312889],
-    [0.910000, 0.821885, 0.308594],
-    [0.914407, 0.825522, 0.304348],
-    [0.918828, 0.829168, 0.299960],
-    [0.923279, 0.832822, 0.295244],
-    [0.927724, 0.836486, 0.290611],
-    [0.932180, 0.840159, 0.285880],
-    [0.936660, 0.843841, 0.280876],
-    [0.941147, 0.847530, 0.275815],
-    [0.945654, 0.851228, 0.270532],
-    [0.950178, 0.854933, 0.265085],
-    [0.954725, 0.858646, 0.259365],
-    [0.959284, 0.862365, 0.253563],
-    [0.963872, 0.866089, 0.247445],
-    [0.968469, 0.869819, 0.241310],
-    [0.973114, 0.873550, 0.234677],
-    [0.977780, 0.877281, 0.227954],
-    [0.982497, 0.881008, 0.220878],
-    [0.987293, 0.884718, 0.213336],
-    [0.992218, 0.888385, 0.205468],
-    [0.994847, 0.892954, 0.203445],
-    [0.995249, 0.898384, 0.207561],
-    [0.995503, 0.903866, 0.212370],
-    [0.995737, 0.909344, 0.217772],
-]
+_cividis_data = [[0.000000, 0.135112, 0.304751],
+                 [0.000000, 0.138068, 0.311105],
+                 [0.000000, 0.141013, 0.317579],
+                 [0.000000, 0.143951, 0.323982],
+                 [0.000000, 0.146877, 0.330479],
+                 [0.000000, 0.149791, 0.337065],
+                 [0.000000, 0.152673, 0.343704],
+                 [0.000000, 0.155377, 0.350500],
+                 [0.000000, 0.157932, 0.357521],
+                 [0.000000, 0.160495, 0.364534],
+                 [0.000000, 0.163058, 0.371608],
+                 [0.000000, 0.165621, 0.378769],
+                 [0.000000, 0.168204, 0.385902],
+                 [0.000000, 0.170800, 0.393100],
+                 [0.000000, 0.173420, 0.400353],
+                 [0.000000, 0.176082, 0.407577],
+                 [0.000000, 0.178802, 0.414764],
+                 [0.000000, 0.181610, 0.421859],
+                 [0.000000, 0.184550, 0.428802],
+                 [0.000000, 0.186915, 0.435532],
+                 [0.000000, 0.188769, 0.439563],
+                 [0.000000, 0.190950, 0.441085],
+                 [0.000000, 0.193366, 0.441561],
+                 [0.003602, 0.195911, 0.441564],
+                 [0.017852, 0.198528, 0.441248],
+                 [0.032110, 0.201199, 0.440785],
+                 [0.046205, 0.203903, 0.440196],
+                 [0.058378, 0.206629, 0.439531],
+                 [0.068968, 0.209372, 0.438863],
+                 [0.078624, 0.212122, 0.438105],
+                 [0.087465, 0.214879, 0.437342],
+                 [0.095645, 0.217643, 0.436593],
+                 [0.103401, 0.220406, 0.435790],
+                 [0.110658, 0.223170, 0.435067],
+                 [0.117612, 0.225935, 0.434308],
+                 [0.124291, 0.228697, 0.433547],
+                 [0.130669, 0.231458, 0.432840],
+                 [0.136830, 0.234216, 0.432148],
+                 [0.142852, 0.236972, 0.431404],
+                 [0.148638, 0.239724, 0.430752],
+                 [0.154261, 0.242475, 0.430120],
+                 [0.159733, 0.245221, 0.429528],
+                 [0.165113, 0.247965, 0.428908],
+                 [0.170362, 0.250707, 0.428325],
+                 [0.175490, 0.253444, 0.427790],
+                 [0.180503, 0.256180, 0.427299],
+                 [0.185453, 0.258914, 0.426788],
+                 [0.190303, 0.261644, 0.426329],
+                 [0.195057, 0.264372, 0.425924],
+                 [0.199764, 0.267099, 0.425497],
+                 [0.204385, 0.269823, 0.425126],
+                 [0.208926, 0.272546, 0.424809],
+                 [0.213431, 0.275266, 0.424480],
+                 [0.217863, 0.277985, 0.424206],
+                 [0.222264, 0.280702, 0.423914],
+                 [0.226598, 0.283419, 0.423678],
+                 [0.230871, 0.286134, 0.423498],
+                 [0.235120, 0.288848, 0.423304],
+                 [0.239312, 0.291562, 0.423167],
+                 [0.243485, 0.294274, 0.423014],
+                 [0.247605, 0.296986, 0.422917],
+                 [0.251675, 0.299698, 0.422873],
+                 [0.255731, 0.302409, 0.422814],
+                 [0.259740, 0.305120, 0.422810],
+                 [0.263738, 0.307831, 0.422789],
+                 [0.267693, 0.310542, 0.422821],
+                 [0.271639, 0.313253, 0.422837],
+                 [0.275513, 0.315965, 0.422979],
+                 [0.279411, 0.318677, 0.423031],
+                 [0.283240, 0.321390, 0.423211],
+                 [0.287065, 0.324103, 0.423373],
+                 [0.290884, 0.326816, 0.423517],
+                 [0.294669, 0.329531, 0.423716],
+                 [0.298421, 0.332247, 0.423973],
+                 [0.302169, 0.334963, 0.424213],
+                 [0.305886, 0.337681, 0.424512],
+                 [0.309601, 0.340399, 0.424790],
+                 [0.313287, 0.343120, 0.425120],
+                 [0.316941, 0.345842, 0.425512],
+                 [0.320595, 0.348565, 0.425889],
+                 [0.324250, 0.351289, 0.426250],
+                 [0.327875, 0.354016, 0.426670],
+                 [0.331474, 0.356744, 0.427144],
+                 [0.335073, 0.359474, 0.427605],
+                 [0.338673, 0.362206, 0.428053],
+                 [0.342246, 0.364939, 0.428559],
+                 [0.345793, 0.367676, 0.429127],
+                 [0.349341, 0.370414, 0.429685],
+                 [0.352892, 0.373153, 0.430226],
+                 [0.356418, 0.375896, 0.430823],
+                 [0.359916, 0.378641, 0.431501],
+                 [0.363446, 0.381388, 0.432075],
+                 [0.366923, 0.384139, 0.432796],
+                 [0.370430, 0.386890, 0.433428],
+                 [0.373884, 0.389646, 0.434209],
+                 [0.377371, 0.392404, 0.434890],
+                 [0.380830, 0.395164, 0.435653],
+                 [0.384268, 0.397928, 0.436475],
+                 [0.387705, 0.400694, 0.437305],
+                 [0.391151, 0.403464, 0.438096],
+                 [0.394568, 0.406236, 0.438986],
+                 [0.397991, 0.409011, 0.439848],
+                 [0.401418, 0.411790, 0.440708],
+                 [0.404820, 0.414572, 0.441642],
+                 [0.408226, 0.417357, 0.442570],
+                 [0.411607, 0.420145, 0.443577],
+                 [0.414992, 0.422937, 0.444578],
+                 [0.418383, 0.425733, 0.445560],
+                 [0.421748, 0.428531, 0.446640],
+                 [0.425120, 0.431334, 0.447692],
+                 [0.428462, 0.434140, 0.448864],
+                 [0.431817, 0.436950, 0.449982],
+                 [0.435168, 0.439763, 0.451134],
+                 [0.438504, 0.442580, 0.452341],
+                 [0.441810, 0.445402, 0.453659],
+                 [0.445148, 0.448226, 0.454885],
+                 [0.448447, 0.451053, 0.456264],
+                 [0.451759, 0.453887, 0.457582],
+                 [0.455072, 0.456718, 0.458976],
+                 [0.458366, 0.459552, 0.460457],
+                 [0.461616, 0.462405, 0.461969],
+                 [0.464947, 0.465241, 0.463395],
+                 [0.468254, 0.468083, 0.464908],
+                 [0.471501, 0.470960, 0.466357],
+                 [0.474812, 0.473832, 0.467681],
+                 [0.478186, 0.476699, 0.468845],
+                 [0.481622, 0.479573, 0.469767],
+                 [0.485141, 0.482451, 0.470384],
+                 [0.488697, 0.485318, 0.471008],
+                 [0.492278, 0.488198, 0.471453],
+                 [0.495913, 0.491076, 0.471751],
+                 [0.499552, 0.493960, 0.472032],
+                 [0.503185, 0.496851, 0.472305],
+                 [0.506866, 0.499743, 0.472432],
+                 [0.510540, 0.502643, 0.472550],
+                 [0.514226, 0.505546, 0.472640],
+                 [0.517920, 0.508454, 0.472707],
+                 [0.521643, 0.511367, 0.472639],
+                 [0.525348, 0.514285, 0.472660],
+                 [0.529086, 0.517207, 0.472543],
+                 [0.532829, 0.520135, 0.472401],
+                 [0.536553, 0.523067, 0.472352],
+                 [0.540307, 0.526005, 0.472163],
+                 [0.544069, 0.528948, 0.471947],
+                 [0.547840, 0.531895, 0.471704],
+                 [0.551612, 0.534849, 0.471439],
+                 [0.555393, 0.537807, 0.471147],
+                 [0.559181, 0.540771, 0.470829],
+                 [0.562972, 0.543741, 0.470488],
+                 [0.566802, 0.546715, 0.469988],
+                 [0.570607, 0.549695, 0.469593],
+                 [0.574417, 0.552682, 0.469172],
+                 [0.578236, 0.555673, 0.468724],
+                 [0.582087, 0.558670, 0.468118],
+                 [0.585916, 0.561674, 0.467618],
+                 [0.589753, 0.564682, 0.467090],
+                 [0.593622, 0.567697, 0.466401],
+                 [0.597469, 0.570718, 0.465821],
+                 [0.601354, 0.573743, 0.465074],
+                 [0.605211, 0.576777, 0.464441],
+                 [0.609105, 0.579816, 0.463638],
+                 [0.612977, 0.582861, 0.462950],
+                 [0.616852, 0.585913, 0.462237],
+                 [0.620765, 0.588970, 0.461351],
+                 [0.624654, 0.592034, 0.460583],
+                 [0.628576, 0.595104, 0.459641],
+                 [0.632506, 0.598180, 0.458668],
+                 [0.636412, 0.601264, 0.457818],
+                 [0.640352, 0.604354, 0.456791],
+                 [0.644270, 0.607450, 0.455886],
+                 [0.648222, 0.610553, 0.454801],
+                 [0.652178, 0.613664, 0.453689],
+                 [0.656114, 0.616780, 0.452702],
+                 [0.660082, 0.619904, 0.451534],
+                 [0.664055, 0.623034, 0.450338],
+                 [0.668008, 0.626171, 0.449270],
+                 [0.671991, 0.629316, 0.448018],
+                 [0.675981, 0.632468, 0.446736],
+                 [0.679979, 0.635626, 0.445424],
+                 [0.683950, 0.638793, 0.444251],
+                 [0.687957, 0.641966, 0.442886],
+                 [0.691971, 0.645145, 0.441491],
+                 [0.695985, 0.648334, 0.440072],
+                 [0.700008, 0.651529, 0.438624],
+                 [0.704037, 0.654731, 0.437147],
+                 [0.708067, 0.657942, 0.435647],
+                 [0.712105, 0.661160, 0.434117],
+                 [0.716177, 0.664384, 0.432386],
+                 [0.720222, 0.667618, 0.430805],
+                 [0.724274, 0.670859, 0.429194],
+                 [0.728334, 0.674107, 0.427554],
+                 [0.732422, 0.677364, 0.425717],
+                 [0.736488, 0.680629, 0.424028],
+                 [0.740589, 0.683900, 0.422131],
+                 [0.744664, 0.687181, 0.420393],
+                 [0.748772, 0.690470, 0.418448],
+                 [0.752886, 0.693766, 0.416472],
+                 [0.756975, 0.697071, 0.414659],
+                 [0.761096, 0.700384, 0.412638],
+                 [0.765223, 0.703705, 0.410587],
+                 [0.769353, 0.707035, 0.408516],
+                 [0.773486, 0.710373, 0.406422],
+                 [0.777651, 0.713719, 0.404112],
+                 [0.781795, 0.717074, 0.401966],
+                 [0.785965, 0.720438, 0.399613],
+                 [0.790116, 0.723810, 0.397423],
+                 [0.794298, 0.727190, 0.395016],
+                 [0.798480, 0.730580, 0.392597],
+                 [0.802667, 0.733978, 0.390153],
+                 [0.806859, 0.737385, 0.387684],
+                 [0.811054, 0.740801, 0.385198],
+                 [0.815274, 0.744226, 0.382504],
+                 [0.819499, 0.747659, 0.379785],
+                 [0.823729, 0.751101, 0.377043],
+                 [0.827959, 0.754553, 0.374292],
+                 [0.832192, 0.758014, 0.371529],
+                 [0.836429, 0.761483, 0.368747],
+                 [0.840693, 0.764962, 0.365746],
+                 [0.844957, 0.768450, 0.362741],
+                 [0.849223, 0.771947, 0.359729],
+                 [0.853515, 0.775454, 0.356500],
+                 [0.857809, 0.778969, 0.353259],
+                 [0.862105, 0.782494, 0.350011],
+                 [0.866421, 0.786028, 0.346571],
+                 [0.870717, 0.789572, 0.343333],
+                 [0.875057, 0.793125, 0.339685],
+                 [0.879378, 0.796687, 0.336241],
+                 [0.883720, 0.800258, 0.332599],
+                 [0.888081, 0.803839, 0.328770],
+                 [0.892440, 0.807430, 0.324968],
+                 [0.896818, 0.811030, 0.320982],
+                 [0.901195, 0.814639, 0.317021],
+                 [0.905589, 0.818257, 0.312889],
+                 [0.910000, 0.821885, 0.308594],
+                 [0.914407, 0.825522, 0.304348],
+                 [0.918828, 0.829168, 0.299960],
+                 [0.923279, 0.832822, 0.295244],
+                 [0.927724, 0.836486, 0.290611],
+                 [0.932180, 0.840159, 0.285880],
+                 [0.936660, 0.843841, 0.280876],
+                 [0.941147, 0.847530, 0.275815],
+                 [0.945654, 0.851228, 0.270532],
+                 [0.950178, 0.854933, 0.265085],
+                 [0.954725, 0.858646, 0.259365],
+                 [0.959284, 0.862365, 0.253563],
+                 [0.963872, 0.866089, 0.247445],
+                 [0.968469, 0.869819, 0.241310],
+                 [0.973114, 0.873550, 0.234677],
+                 [0.977780, 0.877281, 0.227954],
+                 [0.982497, 0.881008, 0.220878],
+                 [0.987293, 0.884718, 0.213336],
+                 [0.992218, 0.888385, 0.205468],
+                 [0.994847, 0.892954, 0.203445],
+                 [0.995249, 0.898384, 0.207561],
+                 [0.995503, 0.903866, 0.212370],
+                 [0.995737, 0.909344, 0.217772]]
 
 _twilight_data = [
-    [0.88575015840754434, 0.85000924943067835, 0.8879736506427196],
-    [0.88378520195539056, 0.85072940540310626, 0.88723222096949894],
-    [0.88172231059285788, 0.85127594077653468, 0.88638056925514819],
-    [0.8795410528270573, 0.85165675407495722, 0.8854143767924102],
-    [0.87724880858965482, 0.85187028338870274, 0.88434120381311432],
-    [0.87485347508575972, 0.85191526123023187, 0.88316926967613829],
-    [0.87233134085124076, 0.85180165478080894, 0.88189704355001619],
-    [0.86970474853509816, 0.85152403004797894, 0.88053883390003362],
-    [0.86696015505333579, 0.8510896085314068, 0.87909766977173343],
-    [0.86408985081463996, 0.85050391167507788, 0.87757925784892632],
-    [0.86110245436899846, 0.84976754857001258, 0.87599242923439569],
-    [0.85798259245670372, 0.84888934810281835, 0.87434038553446281],
-    [0.85472593189256985, 0.84787488124672816, 0.8726282980930582],
-    [0.85133714570857189, 0.84672735796116472, 0.87086081657350445],
-    [0.84780710702577922, 0.8454546229209523, 0.86904036783694438],
-    [0.8441261828674842, 0.84406482711037389, 0.86716973322690072],
-    [0.84030420805957784, 0.8425605950855084, 0.865250882410458],
-    [0.83634031809191178, 0.84094796518951942, 0.86328528001070159],
-    [0.83222705712934408, 0.83923490627754482, 0.86127563500427884],
-    [0.82796894316013536, 0.83742600751395202, 0.85922399451306786],
-    [0.82357429680252847, 0.83552487764795436, 0.85713191328514948],
-    [0.81904654677937527, 0.8335364929949034, 0.85500206287010105],
-    [0.81438982121143089, 0.83146558694197847, 0.85283759062147024],
-    [0.8095999819094809, 0.82931896673505456, 0.85064441601050367],
-    [0.80469164429814577, 0.82709838780560663, 0.84842449296974021],
-    [0.79967075421267997, 0.82480781812080928, 0.84618210029578533],
-    [0.79454305089231114, 0.82245116226304615, 0.84392184786827984],
-    [0.78931445564608915, 0.82003213188702007, 0.8416486380471222],
-    [0.78399101042764918, 0.81755426400533426, 0.83936747464036732],
-    [0.77857892008227592, 0.81502089378742548, 0.8370834463093898],
-    [0.77308416590170936, 0.81243524735466011, 0.83480172950579679],
-    [0.76751108504417864, 0.8098007598713145, 0.83252816638059668],
-    [0.76186907937980286, 0.80711949387647486, 0.830266486168872],
-    [0.75616443584381976, 0.80439408733477935, 0.82802138994719998],
-    [0.75040346765406696, 0.80162699008965321, 0.82579737851082424],
-    [0.74459247771890169, 0.79882047719583249, 0.82359867586156521],
-    [0.73873771700494939, 0.79597665735031009, 0.82142922780433014],
-    [0.73284543645523459, 0.79309746468844067, 0.81929263384230377],
-    [0.72692177512829703, 0.7901846863592763, 0.81719217466726379],
-    [0.72097280665536778, 0.78723995923452639, 0.81513073920879264],
-    [0.71500403076252128, 0.78426487091581187, 0.81311116559949914],
-    [0.70902078134539304, 0.78126088716070907, 0.81113591855117928],
-    [0.7030297722540817, 0.77822904973358131, 0.80920618848056969],
-    [0.6970365443886174, 0.77517050008066057, 0.80732335380063447],
-    [0.69104641009309098, 0.77208629460678091, 0.80548841690679074],
-    [0.68506446154395928, 0.7689774029354699, 0.80370206267176914],
-    [0.67909554499882152, 0.76584472131395898, 0.8019646617300199],
-    [0.67314422559426212, 0.76268908733890484, 0.80027628545809526],
-    [0.66721479803752815, 0.7595112803730375, 0.79863674654537764],
-    [0.6613112930078745, 0.75631202708719025, 0.7970456043491897],
-    [0.65543692326454717, 0.75309208756768431, 0.79550271129031047],
-    [0.64959573004253479, 0.74985201221941766, 0.79400674021499107],
-    [0.6437910831099849, 0.7465923800833657, 0.79255653201306053],
-    [0.63802586828545982, 0.74331376714033193, 0.79115100459573173],
-    [0.6323027138710603, 0.74001672160131404, 0.78978892762640429],
-    [0.62662402022604591, 0.73670175403699445, 0.78846901316334561],
-    [0.62099193064817548, 0.73336934798923203, 0.78718994624696581],
-    [0.61540846411770478, 0.73001995232739691, 0.78595022706750484],
-    [0.60987543176093062, 0.72665398759758293, 0.78474835732694714],
-    [0.60439434200274855, 0.7232718614323369, 0.78358295593535587],
-    [0.5989665814482068, 0.71987394892246725, 0.78245259899346642],
-    [0.59359335696837223, 0.7164606049658685, 0.78135588237640097],
-    [0.58827579780555495, 0.71303214646458135, 0.78029141405636515],
-    [0.58301487036932409, 0.70958887676997473, 0.77925781820476592],
-    [0.5778116438998202, 0.70613106157153982, 0.77825345121025524],
-    [0.5726668948158774, 0.7026589535425779, 0.77727702680911992],
-    [0.56758117853861967, 0.69917279302646274, 0.77632748534275298],
-    [0.56255515357219343, 0.69567278381629649, 0.77540359142309845],
-    [0.55758940419605174, 0.69215911458254054, 0.7745041337932782],
-    [0.55268450589347129, 0.68863194515166382, 0.7736279426902245],
-    [0.54784098153018634, 0.68509142218509878, 0.77277386473440868],
-    [0.54305932424018233, 0.68153767253065878, 0.77194079697835083],
-    [0.53834015575176275, 0.67797081129095405, 0.77112734439057717],
-    [0.53368389147728401, 0.67439093705212727, 0.7703325054879735],
-    [0.529090861832473, 0.67079812302806219, 0.76955552292313134],
-    [0.52456151470593582, 0.66719242996142225, 0.76879541714230948],
-    [0.52009627392235558, 0.66357391434030388, 0.76805119403344102],
-    [0.5156955988596057, 0.65994260812897998, 0.76732191489596169],
-    [0.51135992541601927, 0.65629853981831865, 0.76660663780645333],
-    [0.50708969576451657, 0.65264172403146448, 0.76590445660835849],
-    [0.5028853540415561, 0.64897216734095264, 0.76521446718174913],
-    [0.49874733661356069, 0.6452898684900934, 0.76453578734180083],
-    [0.4946761847863938, 0.64159484119504429, 0.76386719002130909],
-    [0.49067224938561221, 0.63788704858847078, 0.76320812763163837],
-    [0.4867359599430568, 0.63416646251100506, 0.76255780085924041],
-    [0.4828677867260272, 0.6304330455306234, 0.76191537149895305],
-    [0.47906816236197386, 0.62668676251860134, 0.76128000375662419],
-    [0.47533752394906287, 0.62292757283835809, 0.76065085571817748],
-    [0.47167629518877091, 0.61915543242884641, 0.76002709227883047],
-    [0.46808490970531597, 0.61537028695790286, 0.75940789891092741],
-    [0.46456376716303932, 0.61157208822864151, 0.75879242623025811],
-    [0.46111326647023881, 0.607760777169989, 0.75817986436807139],
-    [0.45773377230160567, 0.60393630046586455, 0.75756936901859162],
-    [0.45442563977552913, 0.60009859503858665, 0.75696013660606487],
-    [0.45118918687617743, 0.59624762051353541, 0.75635120643246645],
-    [0.44802470933589172, 0.59238331452146575, 0.75574176474107924],
-    [0.44493246854215379, 0.5885055998308617, 0.7551311041857901],
-    [0.44191271766696399, 0.58461441100175571, 0.75451838884410671],
-    [0.43896563958048396, 0.58070969241098491, 0.75390276208285945],
-    [0.43609138958356369, 0.57679137998186081, 0.7532834105961016],
-    [0.43329008867358393, 0.57285941625606673, 0.75265946532566674],
-    [0.43056179073057571, 0.56891374572457176, 0.75203008099312696],
-    [0.42790652284925834, 0.5649543060909209, 0.75139443521914839],
-    [0.42532423665011354, 0.56098104959950301, 0.75075164989005116],
-    [0.42281485675772662, 0.55699392126996583, 0.75010086988227642],
-    [0.42037822361396326, 0.55299287158108168, 0.7494412559451894],
-    [0.41801414079233629, 0.54897785421888889, 0.74877193167001121],
-    [0.4157223260454232, 0.54494882715350401, 0.74809204459000522],
-    [0.41350245743314729, 0.54090574771098476, 0.74740073297543086],
-    [0.41135414697304568, 0.53684857765005933, 0.74669712855065784],
-    [0.4092768899914751, 0.53277730177130322, 0.74598030635707824],
-    [0.40727018694219069, 0.52869188011057411, 0.74524942637581271],
-    [0.40533343789303178, 0.52459228174983119, 0.74450365836708132],
-    [0.40346600333905397, 0.52047847653840029, 0.74374215223567086],
-    [0.40166714010896104, 0.51635044969688759, 0.7429640345324835],
-    [0.39993606933454834, 0.51220818143218516, 0.74216844571317986],
-    [0.3982719152586337, 0.50805166539276136, 0.74135450918099721],
-    [0.39667374905665609, 0.50388089053847973, 0.74052138580516735],
-    [0.39514058808207631, 0.49969585326377758, 0.73966820211715711],
-    [0.39367135736822567, 0.49549655777451179, 0.738794102296364],
-    [0.39226494876209317, 0.49128300332899261, 0.73789824784475078],
-    [0.39092017571994903, 0.48705520251223039, 0.73697977133881254],
-    [0.38963580160340855, 0.48281316715123496, 0.73603782546932739],
-    [0.38841053300842432, 0.47855691131792805, 0.73507157641157261],
-    [0.38724301459330251, 0.47428645933635388, 0.73408016787854391],
-    [0.38613184178892102, 0.4700018340988123, 0.7330627749243106],
-    [0.38507556793651387, 0.46570306719930193, 0.73201854033690505],
-    [0.38407269378943537, 0.46139018782416635, 0.73094665432902683],
-    [0.38312168084402748, 0.45706323581407199, 0.72984626791353258],
-    [0.38222094988570376, 0.45272225034283325, 0.72871656144003782],
-    [0.38136887930454161, 0.44836727669277859, 0.72755671317141346],
-    [0.38056380696565623, 0.44399837208633719, 0.72636587045135315],
-    [0.37980403744848751, 0.43961558821222629, 0.72514323778761092],
-    [0.37908789283110761, 0.43521897612544935, 0.72388798691323131],
-    [0.378413635091359, 0.43080859411413064, 0.72259931993061044],
-    [0.37777949753513729, 0.4263845142616835, 0.72127639993530235],
-    [0.37718371844251231, 0.42194680223454828, 0.71991841524475775],
-    [0.37662448930806297, 0.41749553747893614, 0.71852454736176108],
-    [0.37610001286385814, 0.41303079952477062, 0.71709396919920232],
-    [0.37560846919442398, 0.40855267638072096, 0.71562585091587549],
-    [0.37514802505380473, 0.4040612609993941, 0.7141193695725726],
-    [0.37471686019302231, 0.3995566498711684, 0.71257368516500463],
-    [0.37431313199312338, 0.39503894828283309, 0.71098796522377461],
-    [0.37393499330475782, 0.39050827529375831, 0.70936134293478448],
-    [0.3735806215098284, 0.38596474386057539, 0.70769297607310577],
-    [0.37324816143326384, 0.38140848555753937, 0.70598200974806036],
-    [0.37293578646665032, 0.37683963835219841, 0.70422755780589941],
-    [0.37264166757849604, 0.37225835004836849, 0.7024287314570723],
-    [0.37236397858465387, 0.36766477862108266, 0.70058463496520773],
-    [0.37210089702443822, 0.36305909736982378, 0.69869434615073722],
-    [0.3718506155898596, 0.35844148285875221, 0.69675695810256544],
-    [0.37161133234400479, 0.3538121372967869, 0.69477149919380887],
-    [0.37138124223736607, 0.34917126878479027, 0.69273703471928827],
-    [0.37115856636209105, 0.34451911410230168, 0.69065253586464992],
-    [0.37094151551337329, 0.33985591488818123, 0.68851703379505125],
-    [0.37072833279422668, 0.33518193808489577, 0.68632948169606767],
-    [0.37051738634484427, 0.33049741244307851, 0.68408888788857214],
-    [0.37030682071842685, 0.32580269697872455, 0.68179411684486679],
-    [0.37009487130772695, 0.3210981375964933, 0.67944405399056851],
-    [0.36987980329025361, 0.31638410101153364, 0.67703755438090574],
-    [0.36965987626565955, 0.31166098762951971, 0.67457344743419545],
-    [0.36943334591276228, 0.30692923551862339, 0.67205052849120617],
-    [0.36919847837592484, 0.30218932176507068, 0.66946754331614522],
-    [0.36895355306596778, 0.29744175492366276, 0.66682322089824264],
-    [0.36869682231895268, 0.29268709856150099, 0.66411625298236909],
-    [0.36842655638020444, 0.28792596437778462, 0.66134526910944602],
-    [0.36814101479899719, 0.28315901221182987, 0.65850888806972308],
-    [0.36783843696531082, 0.27838697181297761, 0.65560566838453704],
-    [0.36751707094367697, 0.27361063317090978, 0.65263411711618635],
-    [0.36717513650699446, 0.26883085667326956, 0.64959272297892245],
-    [0.36681085540107988, 0.26404857724525643, 0.64647991652908243],
-    [0.36642243251550632, 0.25926481158628106, 0.64329409140765537],
-    [0.36600853966739794, 0.25448043878086224, 0.64003361803368586],
-    [0.36556698373538982, 0.24969683475296395, 0.63669675187488584],
-    [0.36509579845886808, 0.24491536803550484, 0.63328173520055586],
-    [0.36459308890125008, 0.24013747024823828, 0.62978680155026101],
-    [0.36405693022088509, 0.23536470386204195, 0.62621013451953023],
-    [0.36348537610385145, 0.23059876218396419, 0.62254988622392882],
-    [0.36287643560041027, 0.22584149293287031, 0.61880417410823019],
-    [0.36222809558295926, 0.22109488427338303, 0.61497112346096128],
-    [0.36153829010998356, 0.21636111429594002, 0.61104880679640927],
-    [0.36080493826624654, 0.21164251793458128, 0.60703532172064711],
-    [0.36002681809096376, 0.20694122817889948, 0.60292845431916875],
-    [0.35920088560930186, 0.20226037920758122, 0.5987265295935138],
-    [0.35832489966617809, 0.197602942459778, 0.59442768517501066],
-    [0.35739663292915563, 0.19297208197842461, 0.59003011251063131],
-    [0.35641381143126327, 0.18837119869242164, 0.5855320765920552],
-    [0.35537415306906722, 0.18380392577704466, 0.58093191431832802],
-    [0.35427534960663759, 0.17927413271618647, 0.57622809660668717],
-    [0.35311574421123737, 0.17478570377561287, 0.57141871523555288],
-    [0.35189248608873791, 0.17034320478524959, 0.56650284911216653],
-    [0.35060304441931012, 0.16595129984720861, 0.56147964703993225],
-    [0.34924513554955644, 0.16161477763045118, 0.55634837474163779],
-    [0.34781653238777782, 0.15733863511152979, 0.55110853452703257],
-    [0.34631507175793091, 0.15312802296627787, 0.5457599924248665],
-    [0.34473901574536375, 0.14898820589826409, 0.54030245920406539],
-    [0.34308600291572294, 0.14492465359918028, 0.53473704282067103],
-    [0.34135411074506483, 0.1409427920655632, 0.52906500940336754],
-    [0.33954168752669694, 0.13704801896718169, 0.52328797535085236],
-    [0.33764732090671112, 0.13324562282438077, 0.51740807573979475],
-    [0.33566978565015315, 0.12954074251271822, 0.51142807215168951],
-    [0.33360804901486002, 0.12593818301005921, 0.50535164796654897],
-    [0.33146154891145124, 0.12244245263391232, 0.49918274588431072],
-    [0.32923005203231409, 0.11905764321981127, 0.49292595612342666],
-    [0.3269137124539796, 0.1157873496841953, 0.48658646495697461],
-    [0.32451307931207785, 0.11263459791730848, 0.48017007211645196],
-    [0.32202882276069322, 0.10960114111258401, 0.47368494725726878],
-    [0.31946262395497965, 0.10668879882392659, 0.46713728801395243],
-    [0.31681648089023501, 0.10389861387653518, 0.46053414662739794],
-    [0.31409278414755532, 0.10123077676403242, 0.45388335612058467],
+    [0.88575015840754434, 0.85000924943067835,  0.8879736506427196],
+    [0.88378520195539056, 0.85072940540310626,  0.88723222096949894],
+    [0.88172231059285788, 0.85127594077653468,  0.88638056925514819],
+    [0.8795410528270573,  0.85165675407495722,  0.8854143767924102],
+    [0.87724880858965482, 0.85187028338870274,  0.88434120381311432],
+    [0.87485347508575972, 0.85191526123023187,  0.88316926967613829],
+    [0.87233134085124076, 0.85180165478080894,  0.88189704355001619],
+    [0.86970474853509816, 0.85152403004797894,  0.88053883390003362],
+    [0.86696015505333579, 0.8510896085314068,   0.87909766977173343],
+    [0.86408985081463996, 0.85050391167507788,  0.87757925784892632],
+    [0.86110245436899846, 0.84976754857001258,  0.87599242923439569],
+    [0.85798259245670372, 0.84888934810281835,  0.87434038553446281],
+    [0.85472593189256985, 0.84787488124672816,  0.8726282980930582],
+    [0.85133714570857189, 0.84672735796116472,  0.87086081657350445],
+    [0.84780710702577922, 0.8454546229209523,   0.86904036783694438],
+    [0.8441261828674842,  0.84406482711037389,  0.86716973322690072],
+    [0.84030420805957784, 0.8425605950855084,   0.865250882410458],
+    [0.83634031809191178, 0.84094796518951942,  0.86328528001070159],
+    [0.83222705712934408, 0.83923490627754482,  0.86127563500427884],
+    [0.82796894316013536, 0.83742600751395202,  0.85922399451306786],
+    [0.82357429680252847, 0.83552487764795436,  0.85713191328514948],
+    [0.81904654677937527, 0.8335364929949034,   0.85500206287010105],
+    [0.81438982121143089, 0.83146558694197847,  0.85283759062147024],
+    [0.8095999819094809,  0.82931896673505456,  0.85064441601050367],
+    [0.80469164429814577, 0.82709838780560663,  0.84842449296974021],
+    [0.79967075421267997, 0.82480781812080928,  0.84618210029578533],
+    [0.79454305089231114, 0.82245116226304615,  0.84392184786827984],
+    [0.78931445564608915, 0.82003213188702007,  0.8416486380471222],
+    [0.78399101042764918, 0.81755426400533426,  0.83936747464036732],
+    [0.77857892008227592, 0.81502089378742548,  0.8370834463093898],
+    [0.77308416590170936, 0.81243524735466011,  0.83480172950579679],
+    [0.76751108504417864, 0.8098007598713145,   0.83252816638059668],
+    [0.76186907937980286, 0.80711949387647486,  0.830266486168872],
+    [0.75616443584381976, 0.80439408733477935,  0.82802138994719998],
+    [0.75040346765406696, 0.80162699008965321,  0.82579737851082424],
+    [0.74459247771890169, 0.79882047719583249,  0.82359867586156521],
+    [0.73873771700494939, 0.79597665735031009,  0.82142922780433014],
+    [0.73284543645523459, 0.79309746468844067,  0.81929263384230377],
+    [0.72692177512829703, 0.7901846863592763,   0.81719217466726379],
+    [0.72097280665536778, 0.78723995923452639,  0.81513073920879264],
+    [0.71500403076252128, 0.78426487091581187,  0.81311116559949914],
+    [0.70902078134539304, 0.78126088716070907,  0.81113591855117928],
+    [0.7030297722540817,  0.77822904973358131,  0.80920618848056969],
+    [0.6970365443886174,  0.77517050008066057,  0.80732335380063447],
+    [0.69104641009309098, 0.77208629460678091,  0.80548841690679074],
+    [0.68506446154395928, 0.7689774029354699,   0.80370206267176914],
+    [0.67909554499882152, 0.76584472131395898,  0.8019646617300199],
+    [0.67314422559426212, 0.76268908733890484,  0.80027628545809526],
+    [0.66721479803752815, 0.7595112803730375,   0.79863674654537764],
+    [0.6613112930078745,  0.75631202708719025,  0.7970456043491897],
+    [0.65543692326454717, 0.75309208756768431,  0.79550271129031047],
+    [0.64959573004253479, 0.74985201221941766,  0.79400674021499107],
+    [0.6437910831099849,  0.7465923800833657,   0.79255653201306053],
+    [0.63802586828545982, 0.74331376714033193,  0.79115100459573173],
+    [0.6323027138710603,  0.74001672160131404,  0.78978892762640429],
+    [0.62662402022604591, 0.73670175403699445,  0.78846901316334561],
+    [0.62099193064817548, 0.73336934798923203,  0.78718994624696581],
+    [0.61540846411770478, 0.73001995232739691,  0.78595022706750484],
+    [0.60987543176093062, 0.72665398759758293,  0.78474835732694714],
+    [0.60439434200274855, 0.7232718614323369,   0.78358295593535587],
+    [0.5989665814482068,  0.71987394892246725,  0.78245259899346642],
+    [0.59359335696837223, 0.7164606049658685,   0.78135588237640097],
+    [0.58827579780555495, 0.71303214646458135,  0.78029141405636515],
+    [0.58301487036932409, 0.70958887676997473,  0.77925781820476592],
+    [0.5778116438998202,  0.70613106157153982,  0.77825345121025524],
+    [0.5726668948158774,  0.7026589535425779,   0.77727702680911992],
+    [0.56758117853861967, 0.69917279302646274,  0.77632748534275298],
+    [0.56255515357219343, 0.69567278381629649,  0.77540359142309845],
+    [0.55758940419605174, 0.69215911458254054,  0.7745041337932782],
+    [0.55268450589347129, 0.68863194515166382,  0.7736279426902245],
+    [0.54784098153018634, 0.68509142218509878,  0.77277386473440868],
+    [0.54305932424018233, 0.68153767253065878,  0.77194079697835083],
+    [0.53834015575176275, 0.67797081129095405,  0.77112734439057717],
+    [0.53368389147728401, 0.67439093705212727,  0.7703325054879735],
+    [0.529090861832473,   0.67079812302806219,  0.76955552292313134],
+    [0.52456151470593582, 0.66719242996142225,  0.76879541714230948],
+    [0.52009627392235558, 0.66357391434030388,  0.76805119403344102],
+    [0.5156955988596057,  0.65994260812897998,  0.76732191489596169],
+    [0.51135992541601927, 0.65629853981831865,  0.76660663780645333],
+    [0.50708969576451657, 0.65264172403146448,  0.76590445660835849],
+    [0.5028853540415561,  0.64897216734095264,  0.76521446718174913],
+    [0.49874733661356069, 0.6452898684900934,   0.76453578734180083],
+    [0.4946761847863938,  0.64159484119504429,  0.76386719002130909],
+    [0.49067224938561221, 0.63788704858847078,  0.76320812763163837],
+    [0.4867359599430568,  0.63416646251100506,  0.76255780085924041],
+    [0.4828677867260272,  0.6304330455306234,   0.76191537149895305],
+    [0.47906816236197386, 0.62668676251860134,  0.76128000375662419],
+    [0.47533752394906287, 0.62292757283835809,  0.76065085571817748],
+    [0.47167629518877091, 0.61915543242884641,  0.76002709227883047],
+    [0.46808490970531597, 0.61537028695790286,  0.75940789891092741],
+    [0.46456376716303932, 0.61157208822864151,  0.75879242623025811],
+    [0.46111326647023881, 0.607760777169989,    0.75817986436807139],
+    [0.45773377230160567, 0.60393630046586455,  0.75756936901859162],
+    [0.45442563977552913, 0.60009859503858665,  0.75696013660606487],
+    [0.45118918687617743, 0.59624762051353541,  0.75635120643246645],
+    [0.44802470933589172, 0.59238331452146575,  0.75574176474107924],
+    [0.44493246854215379, 0.5885055998308617,   0.7551311041857901],
+    [0.44191271766696399, 0.58461441100175571,  0.75451838884410671],
+    [0.43896563958048396, 0.58070969241098491,  0.75390276208285945],
+    [0.43609138958356369, 0.57679137998186081,  0.7532834105961016],
+    [0.43329008867358393, 0.57285941625606673,  0.75265946532566674],
+    [0.43056179073057571, 0.56891374572457176,  0.75203008099312696],
+    [0.42790652284925834, 0.5649543060909209,   0.75139443521914839],
+    [0.42532423665011354, 0.56098104959950301,  0.75075164989005116],
+    [0.42281485675772662, 0.55699392126996583,  0.75010086988227642],
+    [0.42037822361396326, 0.55299287158108168,  0.7494412559451894],
+    [0.41801414079233629, 0.54897785421888889,  0.74877193167001121],
+    [0.4157223260454232,  0.54494882715350401,  0.74809204459000522],
+    [0.41350245743314729, 0.54090574771098476,  0.74740073297543086],
+    [0.41135414697304568, 0.53684857765005933,  0.74669712855065784],
+    [0.4092768899914751,  0.53277730177130322,  0.74598030635707824],
+    [0.40727018694219069, 0.52869188011057411,  0.74524942637581271],
+    [0.40533343789303178, 0.52459228174983119,  0.74450365836708132],
+    [0.40346600333905397, 0.52047847653840029,  0.74374215223567086],
+    [0.40166714010896104, 0.51635044969688759,  0.7429640345324835],
+    [0.39993606933454834, 0.51220818143218516,  0.74216844571317986],
+    [0.3982719152586337,  0.50805166539276136,  0.74135450918099721],
+    [0.39667374905665609, 0.50388089053847973,  0.74052138580516735],
+    [0.39514058808207631, 0.49969585326377758,  0.73966820211715711],
+    [0.39367135736822567, 0.49549655777451179,  0.738794102296364],
+    [0.39226494876209317, 0.49128300332899261,  0.73789824784475078],
+    [0.39092017571994903, 0.48705520251223039,  0.73697977133881254],
+    [0.38963580160340855, 0.48281316715123496,  0.73603782546932739],
+    [0.38841053300842432, 0.47855691131792805,  0.73507157641157261],
+    [0.38724301459330251, 0.47428645933635388,  0.73408016787854391],
+    [0.38613184178892102, 0.4700018340988123,   0.7330627749243106],
+    [0.38507556793651387, 0.46570306719930193,  0.73201854033690505],
+    [0.38407269378943537, 0.46139018782416635,  0.73094665432902683],
+    [0.38312168084402748, 0.45706323581407199,  0.72984626791353258],
+    [0.38222094988570376, 0.45272225034283325,  0.72871656144003782],
+    [0.38136887930454161, 0.44836727669277859,  0.72755671317141346],
+    [0.38056380696565623, 0.44399837208633719,  0.72636587045135315],
+    [0.37980403744848751, 0.43961558821222629,  0.72514323778761092],
+    [0.37908789283110761, 0.43521897612544935,  0.72388798691323131],
+    [0.378413635091359,   0.43080859411413064,  0.72259931993061044],
+    [0.37777949753513729, 0.4263845142616835,   0.72127639993530235],
+    [0.37718371844251231, 0.42194680223454828,  0.71991841524475775],
+    [0.37662448930806297, 0.41749553747893614,  0.71852454736176108],
+    [0.37610001286385814, 0.41303079952477062,  0.71709396919920232],
+    [0.37560846919442398, 0.40855267638072096,  0.71562585091587549],
+    [0.37514802505380473, 0.4040612609993941,   0.7141193695725726],
+    [0.37471686019302231, 0.3995566498711684,   0.71257368516500463],
+    [0.37431313199312338, 0.39503894828283309,  0.71098796522377461],
+    [0.37393499330475782, 0.39050827529375831,  0.70936134293478448],
+    [0.3735806215098284,  0.38596474386057539,  0.70769297607310577],
+    [0.37324816143326384, 0.38140848555753937,  0.70598200974806036],
+    [0.37293578646665032, 0.37683963835219841,  0.70422755780589941],
+    [0.37264166757849604, 0.37225835004836849,  0.7024287314570723],
+    [0.37236397858465387, 0.36766477862108266,  0.70058463496520773],
+    [0.37210089702443822, 0.36305909736982378,  0.69869434615073722],
+    [0.3718506155898596,  0.35844148285875221,  0.69675695810256544],
+    [0.37161133234400479, 0.3538121372967869,   0.69477149919380887],
+    [0.37138124223736607, 0.34917126878479027,  0.69273703471928827],
+    [0.37115856636209105, 0.34451911410230168,  0.69065253586464992],
+    [0.37094151551337329, 0.33985591488818123,  0.68851703379505125],
+    [0.37072833279422668, 0.33518193808489577,  0.68632948169606767],
+    [0.37051738634484427, 0.33049741244307851,  0.68408888788857214],
+    [0.37030682071842685, 0.32580269697872455,  0.68179411684486679],
+    [0.37009487130772695, 0.3210981375964933,   0.67944405399056851],
+    [0.36987980329025361, 0.31638410101153364,  0.67703755438090574],
+    [0.36965987626565955, 0.31166098762951971,  0.67457344743419545],
+    [0.36943334591276228, 0.30692923551862339,  0.67205052849120617],
+    [0.36919847837592484, 0.30218932176507068,  0.66946754331614522],
+    [0.36895355306596778, 0.29744175492366276,  0.66682322089824264],
+    [0.36869682231895268, 0.29268709856150099,  0.66411625298236909],
+    [0.36842655638020444, 0.28792596437778462,  0.66134526910944602],
+    [0.36814101479899719, 0.28315901221182987,  0.65850888806972308],
+    [0.36783843696531082, 0.27838697181297761,  0.65560566838453704],
+    [0.36751707094367697, 0.27361063317090978,  0.65263411711618635],
+    [0.36717513650699446, 0.26883085667326956,  0.64959272297892245],
+    [0.36681085540107988, 0.26404857724525643,  0.64647991652908243],
+    [0.36642243251550632, 0.25926481158628106,  0.64329409140765537],
+    [0.36600853966739794, 0.25448043878086224,  0.64003361803368586],
+    [0.36556698373538982, 0.24969683475296395,  0.63669675187488584],
+    [0.36509579845886808, 0.24491536803550484,  0.63328173520055586],
+    [0.36459308890125008, 0.24013747024823828,  0.62978680155026101],
+    [0.36405693022088509, 0.23536470386204195,  0.62621013451953023],
+    [0.36348537610385145, 0.23059876218396419,  0.62254988622392882],
+    [0.36287643560041027, 0.22584149293287031,  0.61880417410823019],
+    [0.36222809558295926, 0.22109488427338303,  0.61497112346096128],
+    [0.36153829010998356, 0.21636111429594002,  0.61104880679640927],
+    [0.36080493826624654, 0.21164251793458128,  0.60703532172064711],
+    [0.36002681809096376, 0.20694122817889948,  0.60292845431916875],
+    [0.35920088560930186, 0.20226037920758122,  0.5987265295935138],
+    [0.35832489966617809, 0.197602942459778,    0.59442768517501066],
+    [0.35739663292915563, 0.19297208197842461,  0.59003011251063131],
+    [0.35641381143126327, 0.18837119869242164,  0.5855320765920552],
+    [0.35537415306906722, 0.18380392577704466,  0.58093191431832802],
+    [0.35427534960663759, 0.17927413271618647,  0.57622809660668717],
+    [0.35311574421123737, 0.17478570377561287,  0.57141871523555288],
+    [0.35189248608873791, 0.17034320478524959,  0.56650284911216653],
+    [0.35060304441931012, 0.16595129984720861,  0.56147964703993225],
+    [0.34924513554955644, 0.16161477763045118,  0.55634837474163779],
+    [0.34781653238777782, 0.15733863511152979,  0.55110853452703257],
+    [0.34631507175793091, 0.15312802296627787,  0.5457599924248665],
+    [0.34473901574536375, 0.14898820589826409,  0.54030245920406539],
+    [0.34308600291572294, 0.14492465359918028,  0.53473704282067103],
+    [0.34135411074506483, 0.1409427920655632,   0.52906500940336754],
+    [0.33954168752669694, 0.13704801896718169,  0.52328797535085236],
+    [0.33764732090671112, 0.13324562282438077,  0.51740807573979475],
+    [0.33566978565015315, 0.12954074251271822,  0.51142807215168951],
+    [0.33360804901486002, 0.12593818301005921,  0.50535164796654897],
+    [0.33146154891145124, 0.12244245263391232,  0.49918274588431072],
+    [0.32923005203231409, 0.11905764321981127,  0.49292595612342666],
+    [0.3269137124539796,  0.1157873496841953,   0.48658646495697461],
+    [0.32451307931207785, 0.11263459791730848,  0.48017007211645196],
+    [0.32202882276069322, 0.10960114111258401,  0.47368494725726878],
+    [0.31946262395497965, 0.10668879882392659,  0.46713728801395243],
+    [0.31681648089023501, 0.10389861387653518,  0.46053414662739794],
+    [0.31409278414755532, 0.10123077676403242,  0.45388335612058467],
     [0.31129434479712365, 0.098684771934052201, 0.44719313715161618],
     [0.30842444457210105, 0.096259385340577736, 0.44047194882050544],
     [0.30548675819945936, 0.093952764840823738, 0.43372849999361113],
@@ -1513,7 +1503,7 @@
     [0.29942483960214772, 0.089682253716750038, 0.42021619665853854],
     [0.29631000388905288, 0.087713250960463951, 0.41346259134143476],
     [0.29314593096985248, 0.085850656889620708, 0.40672178082365834],
-    [0.28993792445176608, 0.08409078829085731, 0.40000214725256295],
+    [0.28993792445176608, 0.08409078829085731,  0.40000214725256295],
     [0.28669151388283165, 0.082429873848480689, 0.39331182532243375],
     [0.28341239797185225, 0.080864153365499375, 0.38665868550105914],
     [0.28010638576975472, 0.079389994802261526, 0.38005028528138707],
@@ -1532,7 +1522,7 @@
     [0.23739062429054825, 0.067365888050555517, 0.30108922184065873],
     [0.23433055727563878, 0.066935599661639394, 0.29574009929867601],
     [0.23132955273021344, 0.066576186939090592, 0.29051361067988485],
-    [0.2283917709422868, 0.06628997924139618, 0.28541074411068496],
+    [0.2283917709422868,  0.06628997924139618,  0.28541074411068496],
     [0.22552164337737857, 0.066078173119395595, 0.28043398847505197],
     [0.22272706739121817, 0.065933790675651943, 0.27559714652053702],
     [0.22001251100779617, 0.065857918918907604, 0.27090279994325861],
@@ -1540,13 +1530,13 @@
     [0.21482843531473683, 0.065940385613778491, 0.26191675992376573],
     [0.21237411048541005, 0.066085024661758446, 0.25765165093569542],
     [0.21001214221188125, 0.066308573918947178, 0.2535289048041211],
-    [0.2077442377448806, 0.06661453200418091, 0.24954644291943817],
+    [0.2077442377448806,  0.06661453200418091,  0.24954644291943817],
     [0.20558051999470117, 0.066990462397868739, 0.24572497420147632],
     [0.20352007949514977, 0.067444179612424215, 0.24205576625191821],
     [0.20156133764129841, 0.067983271026200248, 0.23852974228695395],
     [0.19971571438603364, 0.068592710553704722, 0.23517094067076993],
     [0.19794834061899208, 0.069314066071660657, 0.23194647381302336],
-    [0.1960826032659409, 0.070321227242423623, 0.22874673279569585],
+    [0.1960826032659409,  0.070321227242423623, 0.22874673279569585],
     [0.19410351363791453, 0.071608304856891569, 0.22558727307410353],
     [0.19199449184606268, 0.073182830649273306, 0.22243385243433622],
     [0.18975853639094634, 0.075019861862143766, 0.2193005075652994],
@@ -1554,27 +1544,27 @@
     [0.18488035509396164, 0.079425730279723883, 0.21307651648984993],
     [0.18774482037046955, 0.077251588468039312, 0.21387448578597812],
     [0.19049578401722037, 0.075311278416787641, 0.2146562337112265],
-    [0.1931548636579131, 0.073606819040117955, 0.21542362939081539],
+    [0.1931548636579131,  0.073606819040117955, 0.21542362939081539],
     [0.19571853588267552, 0.072157781039602742, 0.21617499187076789],
     [0.19819343656336558, 0.070974625252738788, 0.21690975060032436],
     [0.20058760685133747, 0.070064576149984209, 0.21762721310371608],
     [0.20290365333558247, 0.069435248580458964, 0.21833167885096033],
     [0.20531725273301316, 0.068919592266397572, 0.21911516689288835],
     [0.20785704662965598, 0.068484398797025281, 0.22000133917653536],
-    [0.21052882914958676, 0.06812195249816172, 0.22098759107715404],
-    [0.2133313859647627, 0.067830148426026665, 0.22207043213024291],
+    [0.21052882914958676, 0.06812195249816172,  0.22098759107715404],
+    [0.2133313859647627,  0.067830148426026665, 0.22207043213024291],
     [0.21625279838647882, 0.067616330270516389, 0.22324568672294431],
     [0.21930503925136402, 0.067465786362940039, 0.22451023616807558],
     [0.22247308588973624, 0.067388214053092838, 0.22585960379408354],
-    [0.2257539681670791, 0.067382132300147474, 0.22728984778098055],
+    [0.2257539681670791,  0.067382132300147474, 0.22728984778098055],
     [0.22915620278592841, 0.067434730871152565, 0.22879681433956656],
     [0.23266299920501882, 0.067557104388479783, 0.23037617493752832],
-    [0.23627495835774248, 0.06774359820987802, 0.23202360805926608],
+    [0.23627495835774248, 0.06774359820987802,  0.23202360805926608],
     [0.23999586188690308, 0.067985029964779953, 0.23373434258507808],
     [0.24381149720247919, 0.068289851529011875, 0.23550427698321885],
     [0.24772092990501099, 0.068653337909486523, 0.2373288009471749],
     [0.25172899728289466, 0.069064630826035506, 0.23920260612763083],
-    [0.25582135547481771, 0.06953231029187984, 0.24112190491594204],
+    [0.25582135547481771, 0.06953231029187984,  0.24112190491594204],
     [0.25999463887892144, 0.070053855603861875, 0.24308218808684579],
     [0.26425512207060942, 0.070616595622995437, 0.24507758869355967],
     [0.26859095948172862, 0.071226716277922458, 0.24710443563450618],
@@ -1583,509 +1573,1275 @@
     [0.28201746297366942, 0.073315693214040967, 0.25332800295084507],
     [0.28662309235899847, 0.074088460826808866, 0.25543478673717029],
     [0.29128515387578635, 0.074899049847466703, 0.25755101595750435],
-    [0.2960004726065818, 0.075745336000958424, 0.25967245030364566],
+    [0.2960004726065818,  0.075745336000958424, 0.25967245030364566],
     [0.30077276812918691, 0.076617824336164764, 0.26179294097819672],
     [0.30559226007249934, 0.077521963107537312, 0.26391006692119662],
     [0.31045520848595526, 0.078456871676182177, 0.2660200572779356],
     [0.31535870009205808, 0.079420997315243186, 0.26811904076941961],
     [0.32029986557994061, 0.080412994737554838, 0.27020322893039511],
     [0.32527888860401261, 0.081428390076546092, 0.27226772884656186],
-    [0.33029174471181438, 0.08246763389003825, 0.27430929404579435],
+    [0.33029174471181438, 0.08246763389003825,  0.27430929404579435],
     [0.33533353224455448, 0.083532434119003962, 0.27632534356790039],
     [0.34040164359597463, 0.084622236191702671, 0.27831254595259397],
     [0.34549355713871799, 0.085736654965126335, 0.28026769921081435],
-    [0.35060678246032478, 0.08687555176033529, 0.28218770540182386],
+    [0.35060678246032478, 0.08687555176033529,  0.28218770540182386],
     [0.35573889947341125, 0.088038974350243354, 0.2840695897279818],
     [0.36088752387578377, 0.089227194362745205, 0.28591050458531014],
     [0.36605031412464006, 0.090440685427697898, 0.2877077458811747],
     [0.37122508431309342, 0.091679997480262732, 0.28945865397633169],
-    [0.3764103053221462, 0.092945198093777909, 0.29116024157313919],
+    [0.3764103053221462,  0.092945198093777909, 0.29116024157313919],
     [0.38160247377467543, 0.094238731263712183, 0.29281107506269488],
-    [0.38679939079544168, 0.09556181960083443, 0.29440901248173756],
-    [0.39199887556812907, 0.09691583650296684, 0.29595212005509081],
+    [0.38679939079544168, 0.09556181960083443,  0.29440901248173756],
+    [0.39199887556812907, 0.09691583650296684,  0.29595212005509081],
     [0.39719876876325577, 0.098302320968278623, 0.29743856476285779],
     [0.40239692379737496, 0.099722930314950553, 0.29886674369733968],
-    [0.40759120392688708, 0.10117945586419633, 0.30023519507728602],
-    [0.41277985630360303, 0.1026734006932461, 0.30154226437468967],
-    [0.41796105205173684, 0.10420644885760968, 0.30278652039631843],
-    [0.42313214269556043, 0.10578120994917611, 0.3039675809469457],
-    [0.42829101315789753, 0.1073997763055258, 0.30508479060294547],
-    [0.4334355841041439, 0.1090642347484701, 0.30613767928289148],
-    [0.43856378187931538, 0.11077667828375456, 0.30712600062348083],
-    [0.44367358645071275, 0.11253912421257944, 0.30804973095465449],
-    [0.44876299173174822, 0.11435355574622549, 0.30890905921943196],
-    [0.45383005086999889, 0.11622183788331528, 0.30970441249844921],
-    [0.45887288947308297, 0.11814571137706886, 0.31043636979038808],
-    [0.46389102840284874, 0.12012561256850712, 0.31110343446582983],
-    [0.46888111384598413, 0.12216445576414045, 0.31170911458932665],
-    [0.473841437035254, 0.12426354237989065, 0.31225470169927194],
-    [0.47877034239726296, 0.12642401401409453, 0.31274172735821959],
-    [0.48366628618847957, 0.12864679022013889, 0.31317188565991266],
-    [0.48852847371852987, 0.13093210934893723, 0.31354553695453014],
-    [0.49335504375145617, 0.13328091630401023, 0.31386561956734976],
-    [0.49814435462074153, 0.13569380302451714, 0.314135190862664],
-    [0.50289524974970612, 0.13817086581280427, 0.31435662153833671],
-    [0.50760681181053691, 0.14071192654913128, 0.31453200120082569],
-    [0.51227835105321762, 0.14331656120063752, 0.3146630922831542],
-    [0.51690848800544464, 0.14598463068714407, 0.31475407592280041],
-    [0.52149652863229956, 0.14871544765633712, 0.31480767954534428],
-    [0.52604189625477482, 0.15150818660835483, 0.31482653406646727],
-    [0.53054420489856446, 0.15436183633886777, 0.31481299789187128],
-    [0.5350027976174474, 0.15727540775107324, 0.31477085207396532],
-    [0.53941736649199057, 0.16024769309971934, 0.31470295028655965],
-    [0.54378771313608565, 0.16327738551419116, 0.31461204226295625],
-    [0.54811370033467621, 0.1663630904279047, 0.31450102990914708],
-    [0.55239521572711914, 0.16950338809328983, 0.31437291554615371],
-    [0.55663229034969341, 0.17269677158182117, 0.31423043195101424],
-    [0.56082499039117173, 0.17594170887918095, 0.31407639883970623],
-    [0.56497343529017696, 0.17923664950367169, 0.3139136046337036],
-    [0.56907784784011428, 0.18258004462335425, 0.31374440956796529],
-    [0.57313845754107873, 0.18597036007065024, 0.31357126868520002],
-    [0.57715550812992045, 0.18940601489760422, 0.31339704333572083],
-    [0.58112932761586555, 0.19288548904692518, 0.31322399394183942],
-    [0.58506024396466882, 0.19640737049066315, 0.31305401163732732],
-    [0.58894861935544707, 0.19997020971775276, 0.31288922211590126],
-    [0.59279480536520257, 0.20357251410079796, 0.31273234839304942],
-    [0.59659918109122367, 0.207212956082026, 0.31258523031121233],
-    [0.60036213010411577, 0.21089030138947745, 0.31244934410414688],
-    [0.60408401696732739, 0.21460331490206347, 0.31232652641170694],
-    [0.60776523994818654, 0.21835070166659282, 0.31221903291870201],
-    [0.6114062072731884, 0.22213124697023234, 0.31212881396435238],
-    [0.61500723236391375, 0.22594402043981826, 0.31205680685765741],
-    [0.61856865258877192, 0.22978799249179921, 0.31200463838728931],
-    [0.62209079821082613, 0.2336621873300741, 0.31197383273627388],
-    [0.62557416500434959, 0.23756535071152696, 0.31196698314912269],
-    [0.62901892016985872, 0.24149689191922535, 0.31198447195645718],
-    [0.63242534854210275, 0.24545598775548677, 0.31202765974624452],
-    [0.6357937104834237, 0.24944185818822678, 0.31209793953300591],
-    [0.6391243387840212, 0.25345365461983138, 0.31219689612063978],
-    [0.642417577481186, 0.257490519876798, 0.31232631707560987],
-    [0.64567349382645434, 0.26155203161615281, 0.31248673753935263],
-    [0.64889230169458245, 0.26563755336209077, 0.31267941819570189],
-    [0.65207417290277303, 0.26974650525236699, 0.31290560605819168],
-    [0.65521932609327127, 0.27387826652410152, 0.3131666792687211],
-    [0.6583280801134499, 0.27803210957665631, 0.3134643447952643],
-    [0.66140037532601781, 0.28220778870555907, 0.31379912926498488],
-    [0.66443632469878844, 0.28640483614256179, 0.31417223403606975],
-    [0.66743603766369131, 0.29062280081258873, 0.31458483752056837],
-    [0.67039959547676198, 0.29486126309253047, 0.31503813956872212],
-    [0.67332725564817331, 0.29911962764489264, 0.31553372323982209],
-    [0.67621897924409746, 0.30339762792450425, 0.3160724937230589],
-    [0.67907474028157344, 0.30769497879760166, 0.31665545668946665],
-    [0.68189457150944521, 0.31201133280550686, 0.31728380489244951],
-    [0.68467850942494535, 0.31634634821222207, 0.31795870784057567],
-    [0.68742656435169625, 0.32069970535138104, 0.31868137622277692],
-    [0.6901389321505248, 0.32507091815606004, 0.31945332332898302],
-    [0.69281544846764931, 0.32945984647042675, 0.3202754315314667],
-    [0.69545608346891119, 0.33386622163232865, 0.32114884306985791],
-    [0.6980608153581771, 0.33828976326048621, 0.32207478855218091],
-    [0.70062962477242097, 0.34273019305341756, 0.32305449047765694],
-    [0.70316249458814151, 0.34718723719597999, 0.32408913679491225],
-    [0.70565951122610093, 0.35166052978120937, 0.32518014084085567],
-    [0.70812059568420482, 0.35614985523380299, 0.32632861885644465],
-    [0.7105456546582587, 0.36065500290840113, 0.32753574162788762],
-    [0.71293466839773467, 0.36517570519856757, 0.3288027427038317],
-    [0.71528760614847287, 0.36971170225223449, 0.3301308728723546],
-    [0.71760444908133847, 0.37426272710686193, 0.33152138620958932],
-    [0.71988521490549851, 0.37882848839337313, 0.33297555200245399],
-    [0.7221299918421461, 0.38340864508963057, 0.33449469983585844],
-    [0.72433865647781592, 0.38800301593162145, 0.33607995965691828],
-    [0.72651122900227549, 0.3926113126792577, 0.3377325942005665],
-    [0.72864773856716547, 0.39723324476747235, 0.33945384341064017],
-    [0.73074820754845171, 0.401868526884681, 0.3412449533046818],
-    [0.73281270506268747, 0.4065168468778026, 0.34310715173410822],
-    [0.73484133598564938, 0.41117787004519513, 0.34504169470809071],
-    [0.73683422173585866, 0.41585125850290111, 0.34704978520758401],
-    [0.73879140024599266, 0.42053672992315327, 0.34913260148542435],
-    [0.74071301619506091, 0.4252339389526239, 0.35129130890802607],
-    [0.7425992159973317, 0.42994254036133867, 0.35352709245374592],
-    [0.74445018676570673, 0.43466217184617112, 0.35584108091122535],
-    [0.74626615789163442, 0.43939245044973502, 0.35823439142300639],
-    [0.74804739275559562, 0.44413297780351974, 0.36070813602540136],
-    [0.74979420547170472, 0.44888333481548809, 0.36326337558360278],
-    [0.75150685045891663, 0.45364314496866825, 0.36590112443835765],
-    [0.75318566369046569, 0.45841199172949604, 0.36862236642234769],
-    [0.75483105066959544, 0.46318942799460555, 0.3714280448394211],
-    [0.75644341577140706, 0.46797501437948458, 0.37431909037543515],
-    [0.75802325538455839, 0.4727682731566229, 0.37729635531096678],
-    [0.75957111105340058, 0.47756871222057079, 0.380360657784311],
-    [0.7610876378057071, 0.48237579130289127, 0.38351275723852291],
-    [0.76257333554052609, 0.48718906673415824, 0.38675335037837993],
-    [0.76402885609288662, 0.49200802533379656, 0.39008308392311997],
-    [0.76545492593330511, 0.49683212909727231, 0.39350254000115381],
-    [0.76685228950643891, 0.5016608471009063, 0.39701221751773474],
-    [0.76822176599735303, 0.50649362371287909, 0.40061257089416885],
-    [0.7695642334401418, 0.5113298901696085, 0.40430398069682483],
-    [0.77088091962302474, 0.51616892643469103, 0.40808667584648967],
-    [0.77217257229605551, 0.5210102658711383, 0.41196089987122869],
-    [0.77344021829889886, 0.52585332093451564, 0.41592679539764366],
-    [0.77468494746063199, 0.53069749384776732, 0.41998440356963762],
-    [0.77590790730685699, 0.53554217882461186, 0.42413367909988375],
-    [0.7771103295521099, 0.54038674910561235, 0.42837450371258479],
-    [0.77829345807633121, 0.54523059488426595, 0.432706647838971],
-    [0.77945862731506643, 0.55007308413977274, 0.43712979856444761],
-    [0.78060774749483774, 0.55491335744890613, 0.44164332426364639],
-    [0.78174180478981836, 0.55975098052594863, 0.44624687186865436],
-    [0.78286225264440912, 0.56458533111166875, 0.45093985823706345],
-    [0.78397060836414478, 0.56941578326710418, 0.45572154742892063],
-    [0.78506845019606841, 0.5742417003617839, 0.46059116206904965],
-    [0.78615737132332963, 0.5790624629815756, 0.46554778281918402],
-    [0.78723904108188347, 0.58387743744557208, 0.47059039582133383],
-    [0.78831514045623963, 0.58868600173562435, 0.47571791879076081],
-    [0.78938737766251943, 0.5934875421745599, 0.48092913815357724],
-    [0.79045776847727878, 0.59828134277062461, 0.48622257801969754],
-    [0.79152832843475607, 0.60306670593147205, 0.49159667021646397],
-    [0.79260034304237448, 0.60784322087037024, 0.49705020621532009],
-    [0.79367559698664958, 0.61261029334072192, 0.50258161291269432],
-    [0.79475585972654039, 0.61736734400220705, 0.50818921213102985],
-    [0.79584292379583765, 0.62211378808451145, 0.51387124091909786],
-    [0.79693854719951607, 0.62684905679296699, 0.5196258425240281],
-    [0.79804447815136637, 0.63157258225089552, 0.52545108144834785],
-    [0.7991624518501963, 0.63628379372029187, 0.53134495942561433],
-    [0.80029415389753977, 0.64098213306749863, 0.53730535185141037],
-    [0.80144124292560048, 0.64566703459218766, 0.5433300863249918],
-    [0.80260531146112946, 0.65033793748103852, 0.54941691584603647],
-    [0.80378792531077625, 0.65499426549472628, 0.55556350867083815],
-    [0.80499054790810298, 0.65963545027564163, 0.56176745110546977],
-    [0.80621460526927058, 0.66426089585282289, 0.56802629178649788],
-    [0.8074614045096935, 0.6688700095398864, 0.57433746373459582],
-    [0.80873219170089694, 0.67346216702194517, 0.58069834805576737],
-    [0.81002809466520687, 0.67803672673971815, 0.58710626908082753],
-    [0.81135014011763329, 0.68259301546243389, 0.59355848909050757],
-    [0.81269922039881493, 0.68713033714618876, 0.60005214820435104],
-    [0.81407611046993344, 0.69164794791482131, 0.6065843782630862],
-    [0.81548146627279483, 0.69614505508308089, 0.61315221209322646],
-    [0.81691575775055891, 0.70062083014783982, 0.61975260637257923],
-    [0.81837931164498223, 0.70507438189635097, 0.62638245478933297],
-    [0.81987230650455289, 0.70950474978787481, 0.63303857040067113],
-    [0.8213947205565636, 0.7139109141951604, 0.63971766697672761],
-    [0.82294635110428427, 0.71829177331290062, 0.6464164243818421],
-    [0.8245268129450285, 0.72264614312088882, 0.65313137915422603],
-    [0.82613549710580259, 0.72697275518238258, 0.65985900156216504],
-    [0.8277716072353446, 0.73127023324078089, 0.66659570204682972],
-    [0.82943407816481474, 0.7355371221572935, 0.67333772009301907],
-    [0.83112163529096306, 0.73977184647638616, 0.68008125203631464],
-    [0.83283277185777982, 0.74397271817459876, 0.68682235874648545],
-    [0.8345656905566583, 0.7481379479992134, 0.69355697649863846],
-    [0.83631898844737929, 0.75226548952875261, 0.70027999028864962],
-    [0.83809123476131964, 0.75635314860808633, 0.70698561390212977],
-    [0.83987839884120874, 0.76039907199779677, 0.71367147811129228],
-    [0.84167750766845151, 0.76440101200982946, 0.72033299387284622],
-    [0.84348529222933699, 0.76835660399870176, 0.72696536998972039],
-    [0.84529810731955113, 0.77226338601044719, 0.73356368240541492],
-    [0.84711195507965098, 0.77611880236047159, 0.74012275762807056],
-    [0.84892245563117641, 0.77992021407650147, 0.74663719293664366],
-    [0.85072697023178789, 0.78366457342383888, 0.7530974636118285],
-    [0.85251907207708444, 0.78734936133548439, 0.7594994148789691],
-    [0.85429219611470464, 0.79097196777091994, 0.76583801477914104],
-    [0.85604022314725403, 0.79452963601550608, 0.77210610037674143],
-    [0.85775662943504905, 0.79801963142713928, 0.77829571667247499],
-    [0.8594346370300241, 0.8014392309950078, 0.78439788751383921],
-    [0.86107117027565516, 0.80478517909812231, 0.79039529663736285],
-    [0.86265601051127572, 0.80805523804261525, 0.796282666437655],
-    [0.86418343723941027, 0.81124644224653542, 0.80204612696863953],
-    [0.86564934325605325, 0.81435544067514909, 0.80766972324164554],
-    [0.86705314907048503, 0.81737804041911244, 0.81313419626911398],
-    [0.86839954695818633, 0.82030875512181523, 0.81841638963128993],
-    [0.86969131502613806, 0.82314158859569164, 0.82350476683173168],
-    [0.87093846717297507, 0.82586857889438514, 0.82838497261149613],
-    [0.87215331978454325, 0.82848052823709672, 0.8330486712880828],
-    [0.87335171360916275, 0.83096715251272624, 0.83748851001197089],
-    [0.87453793320260187, 0.83331972948645461, 0.84171925358069011],
-    [0.87571458709961403, 0.8355302318472394, 0.84575537519027078],
-    [0.87687848451614692, 0.83759238071186537, 0.84961373549150254],
-    [0.87802298436649007, 0.83950165618540074, 0.85330645352458923],
-    [0.87913244240792765, 0.84125554884475906, 0.85685572291039636],
-    [0.88019293315695812, 0.84285224824778615, 0.86027399927156634],
-    [0.88119169871341951, 0.84429066717717349, 0.86356595168669881],
-    [0.88211542489401606, 0.84557007254559347, 0.86673765046233331],
-    [0.88295168595448525, 0.84668970275699273, 0.86979617048190971],
-    [0.88369127145898041, 0.84764891761519268, 0.87274147101441557],
-    [0.88432713054113543, 0.84844741572055415, 0.87556785228242973],
-    [0.88485138159908572, 0.84908426422893801, 0.87828235285372469],
-    [0.88525897972630474, 0.84955892810989209, 0.88088414794024839],
-    [0.88554714811952384, 0.84987174283631584, 0.88336206121170946],
-    [0.88571155122845646, 0.85002186115856315, 0.88572538990087124],
-]
+    [0.40759120392688708, 0.10117945586419633,  0.30023519507728602],
+    [0.41277985630360303, 0.1026734006932461,   0.30154226437468967],
+    [0.41796105205173684, 0.10420644885760968,  0.30278652039631843],
+    [0.42313214269556043, 0.10578120994917611,  0.3039675809469457],
+    [0.42829101315789753, 0.1073997763055258,   0.30508479060294547],
+    [0.4334355841041439,  0.1090642347484701,   0.30613767928289148],
+    [0.43856378187931538, 0.11077667828375456,  0.30712600062348083],
+    [0.44367358645071275, 0.11253912421257944,  0.30804973095465449],
+    [0.44876299173174822, 0.11435355574622549,  0.30890905921943196],
+    [0.45383005086999889, 0.11622183788331528,  0.30970441249844921],
+    [0.45887288947308297, 0.11814571137706886,  0.31043636979038808],
+    [0.46389102840284874, 0.12012561256850712,  0.31110343446582983],
+    [0.46888111384598413, 0.12216445576414045,  0.31170911458932665],
+    [0.473841437035254,   0.12426354237989065,  0.31225470169927194],
+    [0.47877034239726296, 0.12642401401409453,  0.31274172735821959],
+    [0.48366628618847957, 0.12864679022013889,  0.31317188565991266],
+    [0.48852847371852987, 0.13093210934893723,  0.31354553695453014],
+    [0.49335504375145617, 0.13328091630401023,  0.31386561956734976],
+    [0.49814435462074153, 0.13569380302451714,  0.314135190862664],
+    [0.50289524974970612, 0.13817086581280427,  0.31435662153833671],
+    [0.50760681181053691, 0.14071192654913128,  0.31453200120082569],
+    [0.51227835105321762, 0.14331656120063752,  0.3146630922831542],
+    [0.51690848800544464, 0.14598463068714407,  0.31475407592280041],
+    [0.52149652863229956, 0.14871544765633712,  0.31480767954534428],
+    [0.52604189625477482, 0.15150818660835483,  0.31482653406646727],
+    [0.53054420489856446, 0.15436183633886777,  0.31481299789187128],
+    [0.5350027976174474,  0.15727540775107324,  0.31477085207396532],
+    [0.53941736649199057, 0.16024769309971934,  0.31470295028655965],
+    [0.54378771313608565, 0.16327738551419116,  0.31461204226295625],
+    [0.54811370033467621, 0.1663630904279047,   0.31450102990914708],
+    [0.55239521572711914, 0.16950338809328983,  0.31437291554615371],
+    [0.55663229034969341, 0.17269677158182117,  0.31423043195101424],
+    [0.56082499039117173, 0.17594170887918095,  0.31407639883970623],
+    [0.56497343529017696, 0.17923664950367169,  0.3139136046337036],
+    [0.56907784784011428, 0.18258004462335425,  0.31374440956796529],
+    [0.57313845754107873, 0.18597036007065024,  0.31357126868520002],
+    [0.57715550812992045, 0.18940601489760422,  0.31339704333572083],
+    [0.58112932761586555, 0.19288548904692518,  0.31322399394183942],
+    [0.58506024396466882, 0.19640737049066315,  0.31305401163732732],
+    [0.58894861935544707, 0.19997020971775276,  0.31288922211590126],
+    [0.59279480536520257, 0.20357251410079796,  0.31273234839304942],
+    [0.59659918109122367, 0.207212956082026,    0.31258523031121233],
+    [0.60036213010411577, 0.21089030138947745,  0.31244934410414688],
+    [0.60408401696732739, 0.21460331490206347,  0.31232652641170694],
+    [0.60776523994818654, 0.21835070166659282,  0.31221903291870201],
+    [0.6114062072731884,  0.22213124697023234,  0.31212881396435238],
+    [0.61500723236391375, 0.22594402043981826,  0.31205680685765741],
+    [0.61856865258877192, 0.22978799249179921,  0.31200463838728931],
+    [0.62209079821082613, 0.2336621873300741,   0.31197383273627388],
+    [0.62557416500434959, 0.23756535071152696,  0.31196698314912269],
+    [0.62901892016985872, 0.24149689191922535,  0.31198447195645718],
+    [0.63242534854210275, 0.24545598775548677,  0.31202765974624452],
+    [0.6357937104834237,  0.24944185818822678,  0.31209793953300591],
+    [0.6391243387840212,  0.25345365461983138,  0.31219689612063978],
+    [0.642417577481186,   0.257490519876798,    0.31232631707560987],
+    [0.64567349382645434, 0.26155203161615281,  0.31248673753935263],
+    [0.64889230169458245, 0.26563755336209077,  0.31267941819570189],
+    [0.65207417290277303, 0.26974650525236699,  0.31290560605819168],
+    [0.65521932609327127, 0.27387826652410152,  0.3131666792687211],
+    [0.6583280801134499,  0.27803210957665631,  0.3134643447952643],
+    [0.66140037532601781, 0.28220778870555907,  0.31379912926498488],
+    [0.66443632469878844, 0.28640483614256179,  0.31417223403606975],
+    [0.66743603766369131, 0.29062280081258873,  0.31458483752056837],
+    [0.67039959547676198, 0.29486126309253047,  0.31503813956872212],
+    [0.67332725564817331, 0.29911962764489264,  0.31553372323982209],
+    [0.67621897924409746, 0.30339762792450425,  0.3160724937230589],
+    [0.67907474028157344, 0.30769497879760166,  0.31665545668946665],
+    [0.68189457150944521, 0.31201133280550686,  0.31728380489244951],
+    [0.68467850942494535, 0.31634634821222207,  0.31795870784057567],
+    [0.68742656435169625, 0.32069970535138104,  0.31868137622277692],
+    [0.6901389321505248,  0.32507091815606004,  0.31945332332898302],
+    [0.69281544846764931, 0.32945984647042675,  0.3202754315314667],
+    [0.69545608346891119, 0.33386622163232865,  0.32114884306985791],
+    [0.6980608153581771,  0.33828976326048621,  0.32207478855218091],
+    [0.70062962477242097, 0.34273019305341756,  0.32305449047765694],
+    [0.70316249458814151, 0.34718723719597999,  0.32408913679491225],
+    [0.70565951122610093, 0.35166052978120937,  0.32518014084085567],
+    [0.70812059568420482, 0.35614985523380299,  0.32632861885644465],
+    [0.7105456546582587,  0.36065500290840113,  0.32753574162788762],
+    [0.71293466839773467, 0.36517570519856757,  0.3288027427038317],
+    [0.71528760614847287, 0.36971170225223449,  0.3301308728723546],
+    [0.71760444908133847, 0.37426272710686193,  0.33152138620958932],
+    [0.71988521490549851, 0.37882848839337313,  0.33297555200245399],
+    [0.7221299918421461,  0.38340864508963057,  0.33449469983585844],
+    [0.72433865647781592, 0.38800301593162145,  0.33607995965691828],
+    [0.72651122900227549, 0.3926113126792577,   0.3377325942005665],
+    [0.72864773856716547, 0.39723324476747235,  0.33945384341064017],
+    [0.73074820754845171, 0.401868526884681,    0.3412449533046818],
+    [0.73281270506268747, 0.4065168468778026,   0.34310715173410822],
+    [0.73484133598564938, 0.41117787004519513,  0.34504169470809071],
+    [0.73683422173585866, 0.41585125850290111,  0.34704978520758401],
+    [0.73879140024599266, 0.42053672992315327,  0.34913260148542435],
+    [0.74071301619506091, 0.4252339389526239,   0.35129130890802607],
+    [0.7425992159973317,  0.42994254036133867,  0.35352709245374592],
+    [0.74445018676570673, 0.43466217184617112,  0.35584108091122535],
+    [0.74626615789163442, 0.43939245044973502,  0.35823439142300639],
+    [0.74804739275559562, 0.44413297780351974,  0.36070813602540136],
+    [0.74979420547170472, 0.44888333481548809,  0.36326337558360278],
+    [0.75150685045891663, 0.45364314496866825,  0.36590112443835765],
+    [0.75318566369046569, 0.45841199172949604,  0.36862236642234769],
+    [0.75483105066959544, 0.46318942799460555,  0.3714280448394211],
+    [0.75644341577140706, 0.46797501437948458,  0.37431909037543515],
+    [0.75802325538455839, 0.4727682731566229,   0.37729635531096678],
+    [0.75957111105340058, 0.47756871222057079,  0.380360657784311],
+    [0.7610876378057071,  0.48237579130289127,  0.38351275723852291],
+    [0.76257333554052609, 0.48718906673415824,  0.38675335037837993],
+    [0.76402885609288662, 0.49200802533379656,  0.39008308392311997],
+    [0.76545492593330511, 0.49683212909727231,  0.39350254000115381],
+    [0.76685228950643891, 0.5016608471009063,   0.39701221751773474],
+    [0.76822176599735303, 0.50649362371287909,  0.40061257089416885],
+    [0.7695642334401418,  0.5113298901696085,   0.40430398069682483],
+    [0.77088091962302474, 0.51616892643469103,  0.40808667584648967],
+    [0.77217257229605551, 0.5210102658711383,   0.41196089987122869],
+    [0.77344021829889886, 0.52585332093451564,  0.41592679539764366],
+    [0.77468494746063199, 0.53069749384776732,  0.41998440356963762],
+    [0.77590790730685699, 0.53554217882461186,  0.42413367909988375],
+    [0.7771103295521099,  0.54038674910561235,  0.42837450371258479],
+    [0.77829345807633121, 0.54523059488426595,  0.432706647838971],
+    [0.77945862731506643, 0.55007308413977274,  0.43712979856444761],
+    [0.78060774749483774, 0.55491335744890613,  0.44164332426364639],
+    [0.78174180478981836, 0.55975098052594863,  0.44624687186865436],
+    [0.78286225264440912, 0.56458533111166875,  0.45093985823706345],
+    [0.78397060836414478, 0.56941578326710418,  0.45572154742892063],
+    [0.78506845019606841, 0.5742417003617839,   0.46059116206904965],
+    [0.78615737132332963, 0.5790624629815756,   0.46554778281918402],
+    [0.78723904108188347, 0.58387743744557208,  0.47059039582133383],
+    [0.78831514045623963, 0.58868600173562435,  0.47571791879076081],
+    [0.78938737766251943, 0.5934875421745599,   0.48092913815357724],
+    [0.79045776847727878, 0.59828134277062461,  0.48622257801969754],
+    [0.79152832843475607, 0.60306670593147205,  0.49159667021646397],
+    [0.79260034304237448, 0.60784322087037024,  0.49705020621532009],
+    [0.79367559698664958, 0.61261029334072192,  0.50258161291269432],
+    [0.79475585972654039, 0.61736734400220705,  0.50818921213102985],
+    [0.79584292379583765, 0.62211378808451145,  0.51387124091909786],
+    [0.79693854719951607, 0.62684905679296699,  0.5196258425240281],
+    [0.79804447815136637, 0.63157258225089552,  0.52545108144834785],
+    [0.7991624518501963,  0.63628379372029187,  0.53134495942561433],
+    [0.80029415389753977, 0.64098213306749863,  0.53730535185141037],
+    [0.80144124292560048, 0.64566703459218766,  0.5433300863249918],
+    [0.80260531146112946, 0.65033793748103852,  0.54941691584603647],
+    [0.80378792531077625, 0.65499426549472628,  0.55556350867083815],
+    [0.80499054790810298, 0.65963545027564163,  0.56176745110546977],
+    [0.80621460526927058, 0.66426089585282289,  0.56802629178649788],
+    [0.8074614045096935,  0.6688700095398864,   0.57433746373459582],
+    [0.80873219170089694, 0.67346216702194517,  0.58069834805576737],
+    [0.81002809466520687, 0.67803672673971815,  0.58710626908082753],
+    [0.81135014011763329, 0.68259301546243389,  0.59355848909050757],
+    [0.81269922039881493, 0.68713033714618876,  0.60005214820435104],
+    [0.81407611046993344, 0.69164794791482131,  0.6065843782630862],
+    [0.81548146627279483, 0.69614505508308089,  0.61315221209322646],
+    [0.81691575775055891, 0.70062083014783982,  0.61975260637257923],
+    [0.81837931164498223, 0.70507438189635097,  0.62638245478933297],
+    [0.81987230650455289, 0.70950474978787481,  0.63303857040067113],
+    [0.8213947205565636,  0.7139109141951604,   0.63971766697672761],
+    [0.82294635110428427, 0.71829177331290062,  0.6464164243818421],
+    [0.8245268129450285,  0.72264614312088882,  0.65313137915422603],
+    [0.82613549710580259, 0.72697275518238258,  0.65985900156216504],
+    [0.8277716072353446,  0.73127023324078089,  0.66659570204682972],
+    [0.82943407816481474, 0.7355371221572935,   0.67333772009301907],
+    [0.83112163529096306, 0.73977184647638616,  0.68008125203631464],
+    [0.83283277185777982, 0.74397271817459876,  0.68682235874648545],
+    [0.8345656905566583,  0.7481379479992134,   0.69355697649863846],
+    [0.83631898844737929, 0.75226548952875261,  0.70027999028864962],
+    [0.83809123476131964, 0.75635314860808633,  0.70698561390212977],
+    [0.83987839884120874, 0.76039907199779677,  0.71367147811129228],
+    [0.84167750766845151, 0.76440101200982946,  0.72033299387284622],
+    [0.84348529222933699, 0.76835660399870176,  0.72696536998972039],
+    [0.84529810731955113, 0.77226338601044719,  0.73356368240541492],
+    [0.84711195507965098, 0.77611880236047159,  0.74012275762807056],
+    [0.84892245563117641, 0.77992021407650147,  0.74663719293664366],
+    [0.85072697023178789, 0.78366457342383888,  0.7530974636118285],
+    [0.85251907207708444, 0.78734936133548439,  0.7594994148789691],
+    [0.85429219611470464, 0.79097196777091994,  0.76583801477914104],
+    [0.85604022314725403, 0.79452963601550608,  0.77210610037674143],
+    [0.85775662943504905, 0.79801963142713928,  0.77829571667247499],
+    [0.8594346370300241,  0.8014392309950078,   0.78439788751383921],
+    [0.86107117027565516, 0.80478517909812231,  0.79039529663736285],
+    [0.86265601051127572, 0.80805523804261525,  0.796282666437655],
+    [0.86418343723941027, 0.81124644224653542,  0.80204612696863953],
+    [0.86564934325605325, 0.81435544067514909,  0.80766972324164554],
+    [0.86705314907048503, 0.81737804041911244,  0.81313419626911398],
+    [0.86839954695818633, 0.82030875512181523,  0.81841638963128993],
+    [0.86969131502613806, 0.82314158859569164,  0.82350476683173168],
+    [0.87093846717297507, 0.82586857889438514,  0.82838497261149613],
+    [0.87215331978454325, 0.82848052823709672,  0.8330486712880828],
+    [0.87335171360916275, 0.83096715251272624,  0.83748851001197089],
+    [0.87453793320260187, 0.83331972948645461,  0.84171925358069011],
+    [0.87571458709961403, 0.8355302318472394,   0.84575537519027078],
+    [0.87687848451614692, 0.83759238071186537,  0.84961373549150254],
+    [0.87802298436649007, 0.83950165618540074,  0.85330645352458923],
+    [0.87913244240792765, 0.84125554884475906,  0.85685572291039636],
+    [0.88019293315695812, 0.84285224824778615,  0.86027399927156634],
+    [0.88119169871341951, 0.84429066717717349,  0.86356595168669881],
+    [0.88211542489401606, 0.84557007254559347,  0.86673765046233331],
+    [0.88295168595448525, 0.84668970275699273,  0.86979617048190971],
+    [0.88369127145898041, 0.84764891761519268,  0.87274147101441557],
+    [0.88432713054113543, 0.84844741572055415,  0.87556785228242973],
+    [0.88485138159908572, 0.84908426422893801,  0.87828235285372469],
+    [0.88525897972630474, 0.84955892810989209,  0.88088414794024839],
+    [0.88554714811952384, 0.84987174283631584,  0.88336206121170946],
+    [0.88571155122845646, 0.85002186115856315,  0.88572538990087124]]
 
-_turbo_data = [
-    [0.18995, 0.07176, 0.23217],
-    [0.19483, 0.08339, 0.26149],
-    [0.19956, 0.09498, 0.29024],
-    [0.20415, 0.10652, 0.31844],
-    [0.20860, 0.11802, 0.34607],
-    [0.21291, 0.12947, 0.37314],
-    [0.21708, 0.14087, 0.39964],
-    [0.22111, 0.15223, 0.42558],
-    [0.22500, 0.16354, 0.45096],
-    [0.22875, 0.17481, 0.47578],
-    [0.23236, 0.18603, 0.50004],
-    [0.23582, 0.19720, 0.52373],
-    [0.23915, 0.20833, 0.54686],
-    [0.24234, 0.21941, 0.56942],
-    [0.24539, 0.23044, 0.59142],
-    [0.24830, 0.24143, 0.61286],
-    [0.25107, 0.25237, 0.63374],
-    [0.25369, 0.26327, 0.65406],
-    [0.25618, 0.27412, 0.67381],
-    [0.25853, 0.28492, 0.69300],
-    [0.26074, 0.29568, 0.71162],
-    [0.26280, 0.30639, 0.72968],
-    [0.26473, 0.31706, 0.74718],
-    [0.26652, 0.32768, 0.76412],
-    [0.26816, 0.33825, 0.78050],
-    [0.26967, 0.34878, 0.79631],
-    [0.27103, 0.35926, 0.81156],
-    [0.27226, 0.36970, 0.82624],
-    [0.27334, 0.38008, 0.84037],
-    [0.27429, 0.39043, 0.85393],
-    [0.27509, 0.40072, 0.86692],
-    [0.27576, 0.41097, 0.87936],
-    [0.27628, 0.42118, 0.89123],
-    [0.27667, 0.43134, 0.90254],
-    [0.27691, 0.44145, 0.91328],
-    [0.27701, 0.45152, 0.92347],
-    [0.27698, 0.46153, 0.93309],
-    [0.27680, 0.47151, 0.94214],
-    [0.27648, 0.48144, 0.95064],
-    [0.27603, 0.49132, 0.95857],
-    [0.27543, 0.50115, 0.96594],
-    [0.27469, 0.51094, 0.97275],
-    [0.27381, 0.52069, 0.97899],
-    [0.27273, 0.53040, 0.98461],
-    [0.27106, 0.54015, 0.98930],
-    [0.26878, 0.54995, 0.99303],
-    [0.26592, 0.55979, 0.99583],
-    [0.26252, 0.56967, 0.99773],
-    [0.25862, 0.57958, 0.99876],
-    [0.25425, 0.58950, 0.99896],
-    [0.24946, 0.59943, 0.99835],
-    [0.24427, 0.60937, 0.99697],
-    [0.23874, 0.61931, 0.99485],
-    [0.23288, 0.62923, 0.99202],
-    [0.22676, 0.63913, 0.98851],
-    [0.22039, 0.64901, 0.98436],
-    [0.21382, 0.65886, 0.97959],
-    [0.20708, 0.66866, 0.97423],
-    [0.20021, 0.67842, 0.96833],
-    [0.19326, 0.68812, 0.96190],
-    [0.18625, 0.69775, 0.95498],
-    [0.17923, 0.70732, 0.94761],
-    [0.17223, 0.71680, 0.93981],
-    [0.16529, 0.72620, 0.93161],
-    [0.15844, 0.73551, 0.92305],
-    [0.15173, 0.74472, 0.91416],
-    [0.14519, 0.75381, 0.90496],
-    [0.13886, 0.76279, 0.89550],
-    [0.13278, 0.77165, 0.88580],
-    [0.12698, 0.78037, 0.87590],
-    [0.12151, 0.78896, 0.86581],
-    [0.11639, 0.79740, 0.85559],
-    [0.11167, 0.80569, 0.84525],
-    [0.10738, 0.81381, 0.83484],
-    [0.10357, 0.82177, 0.82437],
-    [0.10026, 0.82955, 0.81389],
-    [0.09750, 0.83714, 0.80342],
-    [0.09532, 0.84455, 0.79299],
-    [0.09377, 0.85175, 0.78264],
-    [0.09287, 0.85875, 0.77240],
-    [0.09267, 0.86554, 0.76230],
-    [0.09320, 0.87211, 0.75237],
-    [0.09451, 0.87844, 0.74265],
-    [0.09662, 0.88454, 0.73316],
-    [0.09958, 0.89040, 0.72393],
-    [0.10342, 0.89600, 0.71500],
-    [0.10815, 0.90142, 0.70599],
-    [0.11374, 0.90673, 0.69651],
-    [0.12014, 0.91193, 0.68660],
-    [0.12733, 0.91701, 0.67627],
-    [0.13526, 0.92197, 0.66556],
-    [0.14391, 0.92680, 0.65448],
-    [0.15323, 0.93151, 0.64308],
-    [0.16319, 0.93609, 0.63137],
-    [0.17377, 0.94053, 0.61938],
-    [0.18491, 0.94484, 0.60713],
-    [0.19659, 0.94901, 0.59466],
-    [0.20877, 0.95304, 0.58199],
-    [0.22142, 0.95692, 0.56914],
-    [0.23449, 0.96065, 0.55614],
-    [0.24797, 0.96423, 0.54303],
-    [0.26180, 0.96765, 0.52981],
-    [0.27597, 0.97092, 0.51653],
-    [0.29042, 0.97403, 0.50321],
-    [0.30513, 0.97697, 0.48987],
-    [0.32006, 0.97974, 0.47654],
-    [0.33517, 0.98234, 0.46325],
-    [0.35043, 0.98477, 0.45002],
-    [0.36581, 0.98702, 0.43688],
-    [0.38127, 0.98909, 0.42386],
-    [0.39678, 0.99098, 0.41098],
-    [0.41229, 0.99268, 0.39826],
-    [0.42778, 0.99419, 0.38575],
-    [0.44321, 0.99551, 0.37345],
-    [0.45854, 0.99663, 0.36140],
-    [0.47375, 0.99755, 0.34963],
-    [0.48879, 0.99828, 0.33816],
-    [0.50362, 0.99879, 0.32701],
-    [0.51822, 0.99910, 0.31622],
-    [0.53255, 0.99919, 0.30581],
-    [0.54658, 0.99907, 0.29581],
-    [0.56026, 0.99873, 0.28623],
-    [0.57357, 0.99817, 0.27712],
-    [0.58646, 0.99739, 0.26849],
-    [0.59891, 0.99638, 0.26038],
-    [0.61088, 0.99514, 0.25280],
-    [0.62233, 0.99366, 0.24579],
-    [0.63323, 0.99195, 0.23937],
-    [0.64362, 0.98999, 0.23356],
-    [0.65394, 0.98775, 0.22835],
-    [0.66428, 0.98524, 0.22370],
-    [0.67462, 0.98246, 0.21960],
-    [0.68494, 0.97941, 0.21602],
-    [0.69525, 0.97610, 0.21294],
-    [0.70553, 0.97255, 0.21032],
-    [0.71577, 0.96875, 0.20815],
-    [0.72596, 0.96470, 0.20640],
-    [0.73610, 0.96043, 0.20504],
-    [0.74617, 0.95593, 0.20406],
-    [0.75617, 0.95121, 0.20343],
-    [0.76608, 0.94627, 0.20311],
-    [0.77591, 0.94113, 0.20310],
-    [0.78563, 0.93579, 0.20336],
-    [0.79524, 0.93025, 0.20386],
-    [0.80473, 0.92452, 0.20459],
-    [0.81410, 0.91861, 0.20552],
-    [0.82333, 0.91253, 0.20663],
-    [0.83241, 0.90627, 0.20788],
-    [0.84133, 0.89986, 0.20926],
-    [0.85010, 0.89328, 0.21074],
-    [0.85868, 0.88655, 0.21230],
-    [0.86709, 0.87968, 0.21391],
-    [0.87530, 0.87267, 0.21555],
-    [0.88331, 0.86553, 0.21719],
-    [0.89112, 0.85826, 0.21880],
-    [0.89870, 0.85087, 0.22038],
-    [0.90605, 0.84337, 0.22188],
-    [0.91317, 0.83576, 0.22328],
-    [0.92004, 0.82806, 0.22456],
-    [0.92666, 0.82025, 0.22570],
-    [0.93301, 0.81236, 0.22667],
-    [0.93909, 0.80439, 0.22744],
-    [0.94489, 0.79634, 0.22800],
-    [0.95039, 0.78823, 0.22831],
-    [0.95560, 0.78005, 0.22836],
-    [0.96049, 0.77181, 0.22811],
-    [0.96507, 0.76352, 0.22754],
-    [0.96931, 0.75519, 0.22663],
-    [0.97323, 0.74682, 0.22536],
-    [0.97679, 0.73842, 0.22369],
-    [0.98000, 0.73000, 0.22161],
-    [0.98289, 0.72140, 0.21918],
-    [0.98549, 0.71250, 0.21650],
-    [0.98781, 0.70330, 0.21358],
-    [0.98986, 0.69382, 0.21043],
-    [0.99163, 0.68408, 0.20706],
-    [0.99314, 0.67408, 0.20348],
-    [0.99438, 0.66386, 0.19971],
-    [0.99535, 0.65341, 0.19577],
-    [0.99607, 0.64277, 0.19165],
-    [0.99654, 0.63193, 0.18738],
-    [0.99675, 0.62093, 0.18297],
-    [0.99672, 0.60977, 0.17842],
-    [0.99644, 0.59846, 0.17376],
-    [0.99593, 0.58703, 0.16899],
-    [0.99517, 0.57549, 0.16412],
-    [0.99419, 0.56386, 0.15918],
-    [0.99297, 0.55214, 0.15417],
-    [0.99153, 0.54036, 0.14910],
-    [0.98987, 0.52854, 0.14398],
-    [0.98799, 0.51667, 0.13883],
-    [0.98590, 0.50479, 0.13367],
-    [0.98360, 0.49291, 0.12849],
-    [0.98108, 0.48104, 0.12332],
-    [0.97837, 0.46920, 0.11817],
-    [0.97545, 0.45740, 0.11305],
-    [0.97234, 0.44565, 0.10797],
-    [0.96904, 0.43399, 0.10294],
-    [0.96555, 0.42241, 0.09798],
-    [0.96187, 0.41093, 0.09310],
-    [0.95801, 0.39958, 0.08831],
-    [0.95398, 0.38836, 0.08362],
-    [0.94977, 0.37729, 0.07905],
-    [0.94538, 0.36638, 0.07461],
-    [0.94084, 0.35566, 0.07031],
-    [0.93612, 0.34513, 0.06616],
-    [0.93125, 0.33482, 0.06218],
-    [0.92623, 0.32473, 0.05837],
-    [0.92105, 0.31489, 0.05475],
-    [0.91572, 0.30530, 0.05134],
-    [0.91024, 0.29599, 0.04814],
-    [0.90463, 0.28696, 0.04516],
-    [0.89888, 0.27824, 0.04243],
-    [0.89298, 0.26981, 0.03993],
-    [0.88691, 0.26152, 0.03753],
-    [0.88066, 0.25334, 0.03521],
-    [0.87422, 0.24526, 0.03297],
-    [0.86760, 0.23730, 0.03082],
-    [0.86079, 0.22945, 0.02875],
-    [0.85380, 0.22170, 0.02677],
-    [0.84662, 0.21407, 0.02487],
-    [0.83926, 0.20654, 0.02305],
-    [0.83172, 0.19912, 0.02131],
-    [0.82399, 0.19182, 0.01966],
-    [0.81608, 0.18462, 0.01809],
-    [0.80799, 0.17753, 0.01660],
-    [0.79971, 0.17055, 0.01520],
-    [0.79125, 0.16368, 0.01387],
-    [0.78260, 0.15693, 0.01264],
-    [0.77377, 0.15028, 0.01148],
-    [0.76476, 0.14374, 0.01041],
-    [0.75556, 0.13731, 0.00942],
-    [0.74617, 0.13098, 0.00851],
-    [0.73661, 0.12477, 0.00769],
-    [0.72686, 0.11867, 0.00695],
-    [0.71692, 0.11268, 0.00629],
-    [0.70680, 0.10680, 0.00571],
-    [0.69650, 0.10102, 0.00522],
-    [0.68602, 0.09536, 0.00481],
-    [0.67535, 0.08980, 0.00449],
-    [0.66449, 0.08436, 0.00424],
-    [0.65345, 0.07902, 0.00408],
-    [0.64223, 0.07380, 0.00401],
-    [0.63082, 0.06868, 0.00401],
-    [0.61923, 0.06367, 0.00410],
-    [0.60746, 0.05878, 0.00427],
-    [0.59550, 0.05399, 0.00453],
-    [0.58336, 0.04931, 0.00486],
-    [0.57103, 0.04474, 0.00529],
-    [0.55852, 0.04028, 0.00579],
-    [0.54583, 0.03593, 0.00638],
-    [0.53295, 0.03169, 0.00705],
-    [0.51989, 0.02756, 0.00780],
-    [0.50664, 0.02354, 0.00863],
-    [0.49321, 0.01963, 0.00955],
-    [0.47960, 0.01583, 0.01055],
-]
-
-_twilight_shifted_data = (
-    _twilight_data[len(_twilight_data) // 2 :]
-    + _twilight_data[: len(_twilight_data) // 2]
-)
+_twilight_shifted_data = (_twilight_data[len(_twilight_data)//2:] +
+                          _twilight_data[:len(_twilight_data)//2])
 _twilight_shifted_data.reverse()
+_turbo_data = [[0.18995, 0.07176, 0.23217],
+               [0.19483, 0.08339, 0.26149],
+               [0.19956, 0.09498, 0.29024],
+               [0.20415, 0.10652, 0.31844],
+               [0.20860, 0.11802, 0.34607],
+               [0.21291, 0.12947, 0.37314],
+               [0.21708, 0.14087, 0.39964],
+               [0.22111, 0.15223, 0.42558],
+               [0.22500, 0.16354, 0.45096],
+               [0.22875, 0.17481, 0.47578],
+               [0.23236, 0.18603, 0.50004],
+               [0.23582, 0.19720, 0.52373],
+               [0.23915, 0.20833, 0.54686],
+               [0.24234, 0.21941, 0.56942],
+               [0.24539, 0.23044, 0.59142],
+               [0.24830, 0.24143, 0.61286],
+               [0.25107, 0.25237, 0.63374],
+               [0.25369, 0.26327, 0.65406],
+               [0.25618, 0.27412, 0.67381],
+               [0.25853, 0.28492, 0.69300],
+               [0.26074, 0.29568, 0.71162],
+               [0.26280, 0.30639, 0.72968],
+               [0.26473, 0.31706, 0.74718],
+               [0.26652, 0.32768, 0.76412],
+               [0.26816, 0.33825, 0.78050],
+               [0.26967, 0.34878, 0.79631],
+               [0.27103, 0.35926, 0.81156],
+               [0.27226, 0.36970, 0.82624],
+               [0.27334, 0.38008, 0.84037],
+               [0.27429, 0.39043, 0.85393],
+               [0.27509, 0.40072, 0.86692],
+               [0.27576, 0.41097, 0.87936],
+               [0.27628, 0.42118, 0.89123],
+               [0.27667, 0.43134, 0.90254],
+               [0.27691, 0.44145, 0.91328],
+               [0.27701, 0.45152, 0.92347],
+               [0.27698, 0.46153, 0.93309],
+               [0.27680, 0.47151, 0.94214],
+               [0.27648, 0.48144, 0.95064],
+               [0.27603, 0.49132, 0.95857],
+               [0.27543, 0.50115, 0.96594],
+               [0.27469, 0.51094, 0.97275],
+               [0.27381, 0.52069, 0.97899],
+               [0.27273, 0.53040, 0.98461],
+               [0.27106, 0.54015, 0.98930],
+               [0.26878, 0.54995, 0.99303],
+               [0.26592, 0.55979, 0.99583],
+               [0.26252, 0.56967, 0.99773],
+               [0.25862, 0.57958, 0.99876],
+               [0.25425, 0.58950, 0.99896],
+               [0.24946, 0.59943, 0.99835],
+               [0.24427, 0.60937, 0.99697],
+               [0.23874, 0.61931, 0.99485],
+               [0.23288, 0.62923, 0.99202],
+               [0.22676, 0.63913, 0.98851],
+               [0.22039, 0.64901, 0.98436],
+               [0.21382, 0.65886, 0.97959],
+               [0.20708, 0.66866, 0.97423],
+               [0.20021, 0.67842, 0.96833],
+               [0.19326, 0.68812, 0.96190],
+               [0.18625, 0.69775, 0.95498],
+               [0.17923, 0.70732, 0.94761],
+               [0.17223, 0.71680, 0.93981],
+               [0.16529, 0.72620, 0.93161],
+               [0.15844, 0.73551, 0.92305],
+               [0.15173, 0.74472, 0.91416],
+               [0.14519, 0.75381, 0.90496],
+               [0.13886, 0.76279, 0.89550],
+               [0.13278, 0.77165, 0.88580],
+               [0.12698, 0.78037, 0.87590],
+               [0.12151, 0.78896, 0.86581],
+               [0.11639, 0.79740, 0.85559],
+               [0.11167, 0.80569, 0.84525],
+               [0.10738, 0.81381, 0.83484],
+               [0.10357, 0.82177, 0.82437],
+               [0.10026, 0.82955, 0.81389],
+               [0.09750, 0.83714, 0.80342],
+               [0.09532, 0.84455, 0.79299],
+               [0.09377, 0.85175, 0.78264],
+               [0.09287, 0.85875, 0.77240],
+               [0.09267, 0.86554, 0.76230],
+               [0.09320, 0.87211, 0.75237],
+               [0.09451, 0.87844, 0.74265],
+               [0.09662, 0.88454, 0.73316],
+               [0.09958, 0.89040, 0.72393],
+               [0.10342, 0.89600, 0.71500],
+               [0.10815, 0.90142, 0.70599],
+               [0.11374, 0.90673, 0.69651],
+               [0.12014, 0.91193, 0.68660],
+               [0.12733, 0.91701, 0.67627],
+               [0.13526, 0.92197, 0.66556],
+               [0.14391, 0.92680, 0.65448],
+               [0.15323, 0.93151, 0.64308],
+               [0.16319, 0.93609, 0.63137],
+               [0.17377, 0.94053, 0.61938],
+               [0.18491, 0.94484, 0.60713],
+               [0.19659, 0.94901, 0.59466],
+               [0.20877, 0.95304, 0.58199],
+               [0.22142, 0.95692, 0.56914],
+               [0.23449, 0.96065, 0.55614],
+               [0.24797, 0.96423, 0.54303],
+               [0.26180, 0.96765, 0.52981],
+               [0.27597, 0.97092, 0.51653],
+               [0.29042, 0.97403, 0.50321],
+               [0.30513, 0.97697, 0.48987],
+               [0.32006, 0.97974, 0.47654],
+               [0.33517, 0.98234, 0.46325],
+               [0.35043, 0.98477, 0.45002],
+               [0.36581, 0.98702, 0.43688],
+               [0.38127, 0.98909, 0.42386],
+               [0.39678, 0.99098, 0.41098],
+               [0.41229, 0.99268, 0.39826],
+               [0.42778, 0.99419, 0.38575],
+               [0.44321, 0.99551, 0.37345],
+               [0.45854, 0.99663, 0.36140],
+               [0.47375, 0.99755, 0.34963],
+               [0.48879, 0.99828, 0.33816],
+               [0.50362, 0.99879, 0.32701],
+               [0.51822, 0.99910, 0.31622],
+               [0.53255, 0.99919, 0.30581],
+               [0.54658, 0.99907, 0.29581],
+               [0.56026, 0.99873, 0.28623],
+               [0.57357, 0.99817, 0.27712],
+               [0.58646, 0.99739, 0.26849],
+               [0.59891, 0.99638, 0.26038],
+               [0.61088, 0.99514, 0.25280],
+               [0.62233, 0.99366, 0.24579],
+               [0.63323, 0.99195, 0.23937],
+               [0.64362, 0.98999, 0.23356],
+               [0.65394, 0.98775, 0.22835],
+               [0.66428, 0.98524, 0.22370],
+               [0.67462, 0.98246, 0.21960],
+               [0.68494, 0.97941, 0.21602],
+               [0.69525, 0.97610, 0.21294],
+               [0.70553, 0.97255, 0.21032],
+               [0.71577, 0.96875, 0.20815],
+               [0.72596, 0.96470, 0.20640],
+               [0.73610, 0.96043, 0.20504],
+               [0.74617, 0.95593, 0.20406],
+               [0.75617, 0.95121, 0.20343],
+               [0.76608, 0.94627, 0.20311],
+               [0.77591, 0.94113, 0.20310],
+               [0.78563, 0.93579, 0.20336],
+               [0.79524, 0.93025, 0.20386],
+               [0.80473, 0.92452, 0.20459],
+               [0.81410, 0.91861, 0.20552],
+               [0.82333, 0.91253, 0.20663],
+               [0.83241, 0.90627, 0.20788],
+               [0.84133, 0.89986, 0.20926],
+               [0.85010, 0.89328, 0.21074],
+               [0.85868, 0.88655, 0.21230],
+               [0.86709, 0.87968, 0.21391],
+               [0.87530, 0.87267, 0.21555],
+               [0.88331, 0.86553, 0.21719],
+               [0.89112, 0.85826, 0.21880],
+               [0.89870, 0.85087, 0.22038],
+               [0.90605, 0.84337, 0.22188],
+               [0.91317, 0.83576, 0.22328],
+               [0.92004, 0.82806, 0.22456],
+               [0.92666, 0.82025, 0.22570],
+               [0.93301, 0.81236, 0.22667],
+               [0.93909, 0.80439, 0.22744],
+               [0.94489, 0.79634, 0.22800],
+               [0.95039, 0.78823, 0.22831],
+               [0.95560, 0.78005, 0.22836],
+               [0.96049, 0.77181, 0.22811],
+               [0.96507, 0.76352, 0.22754],
+               [0.96931, 0.75519, 0.22663],
+               [0.97323, 0.74682, 0.22536],
+               [0.97679, 0.73842, 0.22369],
+               [0.98000, 0.73000, 0.22161],
+               [0.98289, 0.72140, 0.21918],
+               [0.98549, 0.71250, 0.21650],
+               [0.98781, 0.70330, 0.21358],
+               [0.98986, 0.69382, 0.21043],
+               [0.99163, 0.68408, 0.20706],
+               [0.99314, 0.67408, 0.20348],
+               [0.99438, 0.66386, 0.19971],
+               [0.99535, 0.65341, 0.19577],
+               [0.99607, 0.64277, 0.19165],
+               [0.99654, 0.63193, 0.18738],
+               [0.99675, 0.62093, 0.18297],
+               [0.99672, 0.60977, 0.17842],
+               [0.99644, 0.59846, 0.17376],
+               [0.99593, 0.58703, 0.16899],
+               [0.99517, 0.57549, 0.16412],
+               [0.99419, 0.56386, 0.15918],
+               [0.99297, 0.55214, 0.15417],
+               [0.99153, 0.54036, 0.14910],
+               [0.98987, 0.52854, 0.14398],
+               [0.98799, 0.51667, 0.13883],
+               [0.98590, 0.50479, 0.13367],
+               [0.98360, 0.49291, 0.12849],
+               [0.98108, 0.48104, 0.12332],
+               [0.97837, 0.46920, 0.11817],
+               [0.97545, 0.45740, 0.11305],
+               [0.97234, 0.44565, 0.10797],
+               [0.96904, 0.43399, 0.10294],
+               [0.96555, 0.42241, 0.09798],
+               [0.96187, 0.41093, 0.09310],
+               [0.95801, 0.39958, 0.08831],
+               [0.95398, 0.38836, 0.08362],
+               [0.94977, 0.37729, 0.07905],
+               [0.94538, 0.36638, 0.07461],
+               [0.94084, 0.35566, 0.07031],
+               [0.93612, 0.34513, 0.06616],
+               [0.93125, 0.33482, 0.06218],
+               [0.92623, 0.32473, 0.05837],
+               [0.92105, 0.31489, 0.05475],
+               [0.91572, 0.30530, 0.05134],
+               [0.91024, 0.29599, 0.04814],
+               [0.90463, 0.28696, 0.04516],
+               [0.89888, 0.27824, 0.04243],
+               [0.89298, 0.26981, 0.03993],
+               [0.88691, 0.26152, 0.03753],
+               [0.88066, 0.25334, 0.03521],
+               [0.87422, 0.24526, 0.03297],
+               [0.86760, 0.23730, 0.03082],
+               [0.86079, 0.22945, 0.02875],
+               [0.85380, 0.22170, 0.02677],
+               [0.84662, 0.21407, 0.02487],
+               [0.83926, 0.20654, 0.02305],
+               [0.83172, 0.19912, 0.02131],
+               [0.82399, 0.19182, 0.01966],
+               [0.81608, 0.18462, 0.01809],
+               [0.80799, 0.17753, 0.01660],
+               [0.79971, 0.17055, 0.01520],
+               [0.79125, 0.16368, 0.01387],
+               [0.78260, 0.15693, 0.01264],
+               [0.77377, 0.15028, 0.01148],
+               [0.76476, 0.14374, 0.01041],
+               [0.75556, 0.13731, 0.00942],
+               [0.74617, 0.13098, 0.00851],
+               [0.73661, 0.12477, 0.00769],
+               [0.72686, 0.11867, 0.00695],
+               [0.71692, 0.11268, 0.00629],
+               [0.70680, 0.10680, 0.00571],
+               [0.69650, 0.10102, 0.00522],
+               [0.68602, 0.09536, 0.00481],
+               [0.67535, 0.08980, 0.00449],
+               [0.66449, 0.08436, 0.00424],
+               [0.65345, 0.07902, 0.00408],
+               [0.64223, 0.07380, 0.00401],
+               [0.63082, 0.06868, 0.00401],
+               [0.61923, 0.06367, 0.00410],
+               [0.60746, 0.05878, 0.00427],
+               [0.59550, 0.05399, 0.00453],
+               [0.58336, 0.04931, 0.00486],
+               [0.57103, 0.04474, 0.00529],
+               [0.55852, 0.04028, 0.00579],
+               [0.54583, 0.03593, 0.00638],
+               [0.53295, 0.03169, 0.00705],
+               [0.51989, 0.02756, 0.00780],
+               [0.50664, 0.02354, 0.00863],
+               [0.49321, 0.01963, 0.00955],
+               [0.47960, 0.01583, 0.01055]]
+
+_berlin_data = [
+    [0.62108, 0.69018, 0.99951],
+    [0.61216, 0.68923, 0.99537],
+    [0.6032, 0.68825, 0.99124],
+    [0.5942, 0.68726, 0.98709],
+    [0.58517, 0.68625, 0.98292],
+    [0.57609, 0.68522, 0.97873],
+    [0.56696, 0.68417, 0.97452],
+    [0.55779, 0.6831, 0.97029],
+    [0.54859, 0.68199, 0.96602],
+    [0.53933, 0.68086, 0.9617],
+    [0.53003, 0.67969, 0.95735],
+    [0.52069, 0.67848, 0.95294],
+    [0.51129, 0.67723, 0.94847],
+    [0.50186, 0.67591, 0.94392],
+    [0.49237, 0.67453, 0.9393],
+    [0.48283, 0.67308, 0.93457],
+    [0.47324, 0.67153, 0.92975],
+    [0.46361, 0.6699, 0.92481],
+    [0.45393, 0.66815, 0.91974],
+    [0.44421, 0.66628, 0.91452],
+    [0.43444, 0.66427, 0.90914],
+    [0.42465, 0.66212, 0.90359],
+    [0.41482, 0.65979, 0.89785],
+    [0.40498, 0.65729, 0.89191],
+    [0.39514, 0.65458, 0.88575],
+    [0.3853, 0.65167, 0.87937],
+    [0.37549, 0.64854, 0.87276],
+    [0.36574, 0.64516, 0.8659],
+    [0.35606, 0.64155, 0.8588],
+    [0.34645, 0.63769, 0.85145],
+    [0.33698, 0.63357, 0.84386],
+    [0.32764, 0.62919, 0.83602],
+    [0.31849, 0.62455, 0.82794],
+    [0.30954, 0.61966, 0.81963],
+    [0.30078, 0.6145, 0.81111],
+    [0.29231, 0.60911, 0.80238],
+    [0.2841, 0.60348, 0.79347],
+    [0.27621, 0.59763, 0.78439],
+    [0.26859, 0.59158, 0.77514],
+    [0.26131, 0.58534, 0.76578],
+    [0.25437, 0.57891, 0.7563],
+    [0.24775, 0.57233, 0.74672],
+    [0.24146, 0.5656, 0.73707],
+    [0.23552, 0.55875, 0.72735],
+    [0.22984, 0.5518, 0.7176],
+    [0.2245, 0.54475, 0.7078],
+    [0.21948, 0.53763, 0.698],
+    [0.21469, 0.53043, 0.68819],
+    [0.21017, 0.52319, 0.67838],
+    [0.20589, 0.5159, 0.66858],
+    [0.20177, 0.5086, 0.65879],
+    [0.19788, 0.50126, 0.64903],
+    [0.19417, 0.4939, 0.63929],
+    [0.19056, 0.48654, 0.62957],
+    [0.18711, 0.47918, 0.6199],
+    [0.18375, 0.47183, 0.61024],
+    [0.1805, 0.46447, 0.60062],
+    [0.17737, 0.45712, 0.59104],
+    [0.17426, 0.44979, 0.58148],
+    [0.17122, 0.44247, 0.57197],
+    [0.16824, 0.43517, 0.56249],
+    [0.16529, 0.42788, 0.55302],
+    [0.16244, 0.42061, 0.5436],
+    [0.15954, 0.41337, 0.53421],
+    [0.15674, 0.40615, 0.52486],
+    [0.15391, 0.39893, 0.51552],
+    [0.15112, 0.39176, 0.50623],
+    [0.14835, 0.38459, 0.49697],
+    [0.14564, 0.37746, 0.48775],
+    [0.14288, 0.37034, 0.47854],
+    [0.14014, 0.36326, 0.46939],
+    [0.13747, 0.3562, 0.46024],
+    [0.13478, 0.34916, 0.45115],
+    [0.13208, 0.34215, 0.44209],
+    [0.1294, 0.33517, 0.43304],
+    [0.12674, 0.3282, 0.42404],
+    [0.12409, 0.32126, 0.41507],
+    [0.12146, 0.31435, 0.40614],
+    [0.1189, 0.30746, 0.39723],
+    [0.11632, 0.30061, 0.38838],
+    [0.11373, 0.29378, 0.37955],
+    [0.11119, 0.28698, 0.37075],
+    [0.10861, 0.28022, 0.362],
+    [0.10616, 0.2735, 0.35328],
+    [0.10367, 0.26678, 0.34459],
+    [0.10118, 0.26011, 0.33595],
+    [0.098776, 0.25347, 0.32734],
+    [0.096347, 0.24685, 0.31878],
+    [0.094059, 0.24026, 0.31027],
+    [0.091788, 0.23373, 0.30176],
+    [0.089506, 0.22725, 0.29332],
+    [0.087341, 0.2208, 0.28491],
+    [0.085142, 0.21436, 0.27658],
+    [0.083069, 0.20798, 0.26825],
+    [0.081098, 0.20163, 0.25999],
+    [0.07913, 0.19536, 0.25178],
+    [0.077286, 0.18914, 0.24359],
+    [0.075571, 0.18294, 0.2355],
+    [0.073993, 0.17683, 0.22743],
+    [0.07241, 0.17079, 0.21943],
+    [0.071045, 0.1648, 0.2115],
+    [0.069767, 0.1589, 0.20363],
+    [0.068618, 0.15304, 0.19582],
+    [0.06756, 0.14732, 0.18812],
+    [0.066665, 0.14167, 0.18045],
+    [0.065923, 0.13608, 0.17292],
+    [0.065339, 0.1307, 0.16546],
+    [0.064911, 0.12535, 0.15817],
+    [0.064636, 0.12013, 0.15095],
+    [0.064517, 0.11507, 0.14389],
+    [0.064554, 0.11022, 0.13696],
+    [0.064749, 0.10543, 0.13023],
+    [0.0651, 0.10085, 0.12357],
+    [0.065383, 0.096469, 0.11717],
+    [0.065574, 0.092338, 0.11101],
+    [0.065892, 0.088201, 0.10498],
+    [0.066388, 0.084134, 0.099288],
+    [0.067108, 0.080051, 0.093829],
+    [0.068193, 0.076099, 0.08847],
+    [0.06972, 0.072283, 0.083025],
+    [0.071639, 0.068654, 0.077544],
+    [0.073978, 0.065058, 0.07211],
+    [0.076596, 0.061657, 0.066651],
+    [0.079637, 0.05855, 0.061133],
+    [0.082963, 0.055666, 0.055745],
+    [0.086537, 0.052997, 0.050336],
+    [0.090315, 0.050699, 0.04504],
+    [0.09426, 0.048753, 0.039773],
+    [0.098319, 0.047041, 0.034683],
+    [0.10246, 0.045624, 0.030074],
+    [0.10673, 0.044705, 0.026012],
+    [0.11099, 0.043972, 0.022379],
+    [0.11524, 0.043596, 0.01915],
+    [0.11955, 0.043567, 0.016299],
+    [0.12381, 0.043861, 0.013797],
+    [0.1281, 0.044459, 0.011588],
+    [0.13232, 0.045229, 0.0095315],
+    [0.13645, 0.046164, 0.0078947],
+    [0.14063, 0.047374, 0.006502],
+    [0.14488, 0.048634, 0.0053266],
+    [0.14923, 0.049836, 0.0043455],
+    [0.15369, 0.050997, 0.0035374],
+    [0.15831, 0.05213, 0.0028824],
+    [0.16301, 0.053218, 0.0023628],
+    [0.16781, 0.05424, 0.0019629],
+    [0.17274, 0.055172, 0.001669],
+    [0.1778, 0.056018, 0.0014692],
+    [0.18286, 0.05682, 0.0013401],
+    [0.18806, 0.057574, 0.0012617],
+    [0.19323, 0.058514, 0.0012261],
+    [0.19846, 0.05955, 0.0012271],
+    [0.20378, 0.060501, 0.0012601],
+    [0.20909, 0.061486, 0.0013221],
+    [0.21447, 0.06271, 0.0014116],
+    [0.2199, 0.063823, 0.0015287],
+    [0.22535, 0.065027, 0.0016748],
+    [0.23086, 0.066297, 0.0018529],
+    [0.23642, 0.067645, 0.0020675],
+    [0.24202, 0.069092, 0.0023247],
+    [0.24768, 0.070458, 0.0026319],
+    [0.25339, 0.071986, 0.0029984],
+    [0.25918, 0.07364, 0.003435],
+    [0.265, 0.075237, 0.0039545],
+    [0.27093, 0.076965, 0.004571],
+    [0.27693, 0.078822, 0.0053006],
+    [0.28302, 0.080819, 0.0061608],
+    [0.2892, 0.082879, 0.0071713],
+    [0.29547, 0.085075, 0.0083494],
+    [0.30186, 0.08746, 0.0097258],
+    [0.30839, 0.089912, 0.011455],
+    [0.31502, 0.09253, 0.013324],
+    [0.32181, 0.095392, 0.015413],
+    [0.32874, 0.098396, 0.01778],
+    [0.3358, 0.10158, 0.020449],
+    [0.34304, 0.10498, 0.02344],
+    [0.35041, 0.10864, 0.026771],
+    [0.35795, 0.11256, 0.030456],
+    [0.36563, 0.11666, 0.034571],
+    [0.37347, 0.12097, 0.039115],
+    [0.38146, 0.12561, 0.043693],
+    [0.38958, 0.13046, 0.048471],
+    [0.39785, 0.13547, 0.053136],
+    [0.40622, 0.1408, 0.057848],
+    [0.41469, 0.14627, 0.062715],
+    [0.42323, 0.15198, 0.067685],
+    [0.43184, 0.15791, 0.073044],
+    [0.44044, 0.16403, 0.07862],
+    [0.44909, 0.17027, 0.084644],
+    [0.4577, 0.17667, 0.090869],
+    [0.46631, 0.18321, 0.097335],
+    [0.4749, 0.18989, 0.10406],
+    [0.48342, 0.19668, 0.11104],
+    [0.49191, 0.20352, 0.11819],
+    [0.50032, 0.21043, 0.1255],
+    [0.50869, 0.21742, 0.13298],
+    [0.51698, 0.22443, 0.14062],
+    [0.5252, 0.23154, 0.14835],
+    [0.53335, 0.23862, 0.15626],
+    [0.54144, 0.24575, 0.16423],
+    [0.54948, 0.25292, 0.17226],
+    [0.55746, 0.26009, 0.1804],
+    [0.56538, 0.26726, 0.18864],
+    [0.57327, 0.27446, 0.19692],
+    [0.58111, 0.28167, 0.20524],
+    [0.58892, 0.28889, 0.21362],
+    [0.59672, 0.29611, 0.22205],
+    [0.60448, 0.30335, 0.23053],
+    [0.61223, 0.31062, 0.23905],
+    [0.61998, 0.31787, 0.24762],
+    [0.62771, 0.32513, 0.25619],
+    [0.63544, 0.33244, 0.26481],
+    [0.64317, 0.33975, 0.27349],
+    [0.65092, 0.34706, 0.28218],
+    [0.65866, 0.3544, 0.29089],
+    [0.66642, 0.36175, 0.29964],
+    [0.67419, 0.36912, 0.30842],
+    [0.68198, 0.37652, 0.31722],
+    [0.68978, 0.38392, 0.32604],
+    [0.6976, 0.39135, 0.33493],
+    [0.70543, 0.39879, 0.3438],
+    [0.71329, 0.40627, 0.35272],
+    [0.72116, 0.41376, 0.36166],
+    [0.72905, 0.42126, 0.37062],
+    [0.73697, 0.4288, 0.37962],
+    [0.7449, 0.43635, 0.38864],
+    [0.75285, 0.44392, 0.39768],
+    [0.76083, 0.45151, 0.40675],
+    [0.76882, 0.45912, 0.41584],
+    [0.77684, 0.46676, 0.42496],
+    [0.78488, 0.47441, 0.43409],
+    [0.79293, 0.48208, 0.44327],
+    [0.80101, 0.48976, 0.45246],
+    [0.80911, 0.49749, 0.46167],
+    [0.81722, 0.50521, 0.47091],
+    [0.82536, 0.51296, 0.48017],
+    [0.83352, 0.52073, 0.48945],
+    [0.84169, 0.52853, 0.49876],
+    [0.84988, 0.53634, 0.5081],
+    [0.85809, 0.54416, 0.51745],
+    [0.86632, 0.55201, 0.52683],
+    [0.87457, 0.55988, 0.53622],
+    [0.88283, 0.56776, 0.54564],
+    [0.89111, 0.57567, 0.55508],
+    [0.89941, 0.58358, 0.56455],
+    [0.90772, 0.59153, 0.57404],
+    [0.91603, 0.59949, 0.58355],
+    [0.92437, 0.60747, 0.59309],
+    [0.93271, 0.61546, 0.60265],
+    [0.94108, 0.62348, 0.61223],
+    [0.94945, 0.63151, 0.62183],
+    [0.95783, 0.63956, 0.63147],
+    [0.96622, 0.64763, 0.64111],
+    [0.97462, 0.65572, 0.65079],
+    [0.98303, 0.66382, 0.66049],
+    [0.99145, 0.67194, 0.67022],
+    [0.99987, 0.68007, 0.67995]]
+
+_managua_data = [
+    [1, 0.81263, 0.40424],
+    [0.99516, 0.80455, 0.40155],
+    [0.99024, 0.79649, 0.39888],
+    [0.98532, 0.78848, 0.39622],
+    [0.98041, 0.7805, 0.39356],
+    [0.97551, 0.77257, 0.39093],
+    [0.97062, 0.76468, 0.3883],
+    [0.96573, 0.75684, 0.38568],
+    [0.96087, 0.74904, 0.3831],
+    [0.95601, 0.74129, 0.38052],
+    [0.95116, 0.7336, 0.37795],
+    [0.94631, 0.72595, 0.37539],
+    [0.94149, 0.71835, 0.37286],
+    [0.93667, 0.7108, 0.37034],
+    [0.93186, 0.7033, 0.36784],
+    [0.92706, 0.69585, 0.36536],
+    [0.92228, 0.68845, 0.36289],
+    [0.9175, 0.68109, 0.36042],
+    [0.91273, 0.67379, 0.358],
+    [0.90797, 0.66653, 0.35558],
+    [0.90321, 0.65932, 0.35316],
+    [0.89846, 0.65216, 0.35078],
+    [0.89372, 0.64503, 0.34839],
+    [0.88899, 0.63796, 0.34601],
+    [0.88426, 0.63093, 0.34367],
+    [0.87953, 0.62395, 0.34134],
+    [0.87481, 0.617, 0.33902],
+    [0.87009, 0.61009, 0.3367],
+    [0.86538, 0.60323, 0.33442],
+    [0.86067, 0.59641, 0.33213],
+    [0.85597, 0.58963, 0.32987],
+    [0.85125, 0.5829, 0.3276],
+    [0.84655, 0.57621, 0.32536],
+    [0.84185, 0.56954, 0.32315],
+    [0.83714, 0.56294, 0.32094],
+    [0.83243, 0.55635, 0.31874],
+    [0.82772, 0.54983, 0.31656],
+    [0.82301, 0.54333, 0.31438],
+    [0.81829, 0.53688, 0.31222],
+    [0.81357, 0.53046, 0.3101],
+    [0.80886, 0.52408, 0.30796],
+    [0.80413, 0.51775, 0.30587],
+    [0.7994, 0.51145, 0.30375],
+    [0.79466, 0.50519, 0.30167],
+    [0.78991, 0.49898, 0.29962],
+    [0.78516, 0.4928, 0.29757],
+    [0.7804, 0.48668, 0.29553],
+    [0.77564, 0.48058, 0.29351],
+    [0.77086, 0.47454, 0.29153],
+    [0.76608, 0.46853, 0.28954],
+    [0.76128, 0.46255, 0.28756],
+    [0.75647, 0.45663, 0.28561],
+    [0.75166, 0.45075, 0.28369],
+    [0.74682, 0.44491, 0.28178],
+    [0.74197, 0.4391, 0.27988],
+    [0.73711, 0.43333, 0.27801],
+    [0.73223, 0.42762, 0.27616],
+    [0.72732, 0.42192, 0.2743],
+    [0.72239, 0.41628, 0.27247],
+    [0.71746, 0.41067, 0.27069],
+    [0.71247, 0.40508, 0.26891],
+    [0.70747, 0.39952, 0.26712],
+    [0.70244, 0.39401, 0.26538],
+    [0.69737, 0.38852, 0.26367],
+    [0.69227, 0.38306, 0.26194],
+    [0.68712, 0.37761, 0.26025],
+    [0.68193, 0.37219, 0.25857],
+    [0.67671, 0.3668, 0.25692],
+    [0.67143, 0.36142, 0.25529],
+    [0.6661, 0.35607, 0.25367],
+    [0.66071, 0.35073, 0.25208],
+    [0.65528, 0.34539, 0.25049],
+    [0.6498, 0.34009, 0.24895],
+    [0.64425, 0.3348, 0.24742],
+    [0.63866, 0.32953, 0.2459],
+    [0.633, 0.32425, 0.24442],
+    [0.62729, 0.31901, 0.24298],
+    [0.62152, 0.3138, 0.24157],
+    [0.6157, 0.3086, 0.24017],
+    [0.60983, 0.30341, 0.23881],
+    [0.60391, 0.29826, 0.23752],
+    [0.59793, 0.29314, 0.23623],
+    [0.59191, 0.28805, 0.235],
+    [0.58585, 0.28302, 0.23377],
+    [0.57974, 0.27799, 0.23263],
+    [0.57359, 0.27302, 0.23155],
+    [0.56741, 0.26808, 0.23047],
+    [0.5612, 0.26321, 0.22948],
+    [0.55496, 0.25837, 0.22857],
+    [0.54871, 0.25361, 0.22769],
+    [0.54243, 0.24891, 0.22689],
+    [0.53614, 0.24424, 0.22616],
+    [0.52984, 0.23968, 0.22548],
+    [0.52354, 0.2352, 0.22487],
+    [0.51724, 0.23076, 0.22436],
+    [0.51094, 0.22643, 0.22395],
+    [0.50467, 0.22217, 0.22363],
+    [0.49841, 0.21802, 0.22339],
+    [0.49217, 0.21397, 0.22325],
+    [0.48595, 0.21, 0.22321],
+    [0.47979, 0.20618, 0.22328],
+    [0.47364, 0.20242, 0.22345],
+    [0.46756, 0.1988, 0.22373],
+    [0.46152, 0.19532, 0.22413],
+    [0.45554, 0.19195, 0.22465],
+    [0.44962, 0.18873, 0.22534],
+    [0.44377, 0.18566, 0.22616],
+    [0.43799, 0.18266, 0.22708],
+    [0.43229, 0.17987, 0.22817],
+    [0.42665, 0.17723, 0.22938],
+    [0.42111, 0.17474, 0.23077],
+    [0.41567, 0.17238, 0.23232],
+    [0.41033, 0.17023, 0.23401],
+    [0.40507, 0.16822, 0.2359],
+    [0.39992, 0.1664, 0.23794],
+    [0.39489, 0.16475, 0.24014],
+    [0.38996, 0.16331, 0.24254],
+    [0.38515, 0.16203, 0.24512],
+    [0.38046, 0.16093, 0.24792],
+    [0.37589, 0.16, 0.25087],
+    [0.37143, 0.15932, 0.25403],
+    [0.36711, 0.15883, 0.25738],
+    [0.36292, 0.15853, 0.26092],
+    [0.35885, 0.15843, 0.26466],
+    [0.35494, 0.15853, 0.26862],
+    [0.35114, 0.15882, 0.27276],
+    [0.34748, 0.15931, 0.27711],
+    [0.34394, 0.15999, 0.28164],
+    [0.34056, 0.16094, 0.28636],
+    [0.33731, 0.16207, 0.29131],
+    [0.3342, 0.16338, 0.29642],
+    [0.33121, 0.16486, 0.3017],
+    [0.32837, 0.16658, 0.30719],
+    [0.32565, 0.16847, 0.31284],
+    [0.3231, 0.17056, 0.31867],
+    [0.32066, 0.17283, 0.32465],
+    [0.31834, 0.1753, 0.33079],
+    [0.31616, 0.17797, 0.3371],
+    [0.3141, 0.18074, 0.34354],
+    [0.31216, 0.18373, 0.35011],
+    [0.31038, 0.1869, 0.35682],
+    [0.3087, 0.19021, 0.36363],
+    [0.30712, 0.1937, 0.37056],
+    [0.3057, 0.19732, 0.3776],
+    [0.30435, 0.20106, 0.38473],
+    [0.30314, 0.205, 0.39195],
+    [0.30204, 0.20905, 0.39924],
+    [0.30106, 0.21323, 0.40661],
+    [0.30019, 0.21756, 0.41404],
+    [0.29944, 0.22198, 0.42151],
+    [0.29878, 0.22656, 0.42904],
+    [0.29822, 0.23122, 0.4366],
+    [0.29778, 0.23599, 0.44419],
+    [0.29745, 0.24085, 0.45179],
+    [0.29721, 0.24582, 0.45941],
+    [0.29708, 0.2509, 0.46703],
+    [0.29704, 0.25603, 0.47465],
+    [0.2971, 0.26127, 0.48225],
+    [0.29726, 0.26658, 0.48983],
+    [0.2975, 0.27194, 0.4974],
+    [0.29784, 0.27741, 0.50493],
+    [0.29828, 0.28292, 0.51242],
+    [0.29881, 0.28847, 0.51987],
+    [0.29943, 0.29408, 0.52728],
+    [0.30012, 0.29976, 0.53463],
+    [0.3009, 0.30548, 0.54191],
+    [0.30176, 0.31122, 0.54915],
+    [0.30271, 0.317, 0.5563],
+    [0.30373, 0.32283, 0.56339],
+    [0.30483, 0.32866, 0.5704],
+    [0.30601, 0.33454, 0.57733],
+    [0.30722, 0.34042, 0.58418],
+    [0.30853, 0.34631, 0.59095],
+    [0.30989, 0.35224, 0.59763],
+    [0.3113, 0.35817, 0.60423],
+    [0.31277, 0.3641, 0.61073],
+    [0.31431, 0.37005, 0.61715],
+    [0.3159, 0.376, 0.62347],
+    [0.31752, 0.38195, 0.62969],
+    [0.3192, 0.3879, 0.63583],
+    [0.32092, 0.39385, 0.64188],
+    [0.32268, 0.39979, 0.64783],
+    [0.32446, 0.40575, 0.6537],
+    [0.3263, 0.41168, 0.65948],
+    [0.32817, 0.41763, 0.66517],
+    [0.33008, 0.42355, 0.67079],
+    [0.33201, 0.4295, 0.67632],
+    [0.33398, 0.43544, 0.68176],
+    [0.33596, 0.44137, 0.68715],
+    [0.33798, 0.44731, 0.69246],
+    [0.34003, 0.45327, 0.69769],
+    [0.3421, 0.45923, 0.70288],
+    [0.34419, 0.4652, 0.70799],
+    [0.34631, 0.4712, 0.71306],
+    [0.34847, 0.4772, 0.71808],
+    [0.35064, 0.48323, 0.72305],
+    [0.35283, 0.48928, 0.72798],
+    [0.35506, 0.49537, 0.73288],
+    [0.3573, 0.50149, 0.73773],
+    [0.35955, 0.50763, 0.74256],
+    [0.36185, 0.51381, 0.74736],
+    [0.36414, 0.52001, 0.75213],
+    [0.36649, 0.52627, 0.75689],
+    [0.36884, 0.53256, 0.76162],
+    [0.37119, 0.53889, 0.76633],
+    [0.37359, 0.54525, 0.77103],
+    [0.376, 0.55166, 0.77571],
+    [0.37842, 0.55809, 0.78037],
+    [0.38087, 0.56458, 0.78503],
+    [0.38333, 0.5711, 0.78966],
+    [0.38579, 0.57766, 0.79429],
+    [0.38828, 0.58426, 0.7989],
+    [0.39078, 0.59088, 0.8035],
+    [0.39329, 0.59755, 0.8081],
+    [0.39582, 0.60426, 0.81268],
+    [0.39835, 0.61099, 0.81725],
+    [0.4009, 0.61774, 0.82182],
+    [0.40344, 0.62454, 0.82637],
+    [0.406, 0.63137, 0.83092],
+    [0.40856, 0.63822, 0.83546],
+    [0.41114, 0.6451, 0.83999],
+    [0.41372, 0.65202, 0.84451],
+    [0.41631, 0.65896, 0.84903],
+    [0.4189, 0.66593, 0.85354],
+    [0.42149, 0.67294, 0.85805],
+    [0.4241, 0.67996, 0.86256],
+    [0.42671, 0.68702, 0.86705],
+    [0.42932, 0.69411, 0.87156],
+    [0.43195, 0.70123, 0.87606],
+    [0.43457, 0.70839, 0.88056],
+    [0.4372, 0.71557, 0.88506],
+    [0.43983, 0.72278, 0.88956],
+    [0.44248, 0.73004, 0.89407],
+    [0.44512, 0.73732, 0.89858],
+    [0.44776, 0.74464, 0.9031],
+    [0.45042, 0.752, 0.90763],
+    [0.45308, 0.75939, 0.91216],
+    [0.45574, 0.76682, 0.9167],
+    [0.45841, 0.77429, 0.92124],
+    [0.46109, 0.78181, 0.9258],
+    [0.46377, 0.78936, 0.93036],
+    [0.46645, 0.79694, 0.93494],
+    [0.46914, 0.80458, 0.93952],
+    [0.47183, 0.81224, 0.94412],
+    [0.47453, 0.81995, 0.94872],
+    [0.47721, 0.8277, 0.95334],
+    [0.47992, 0.83549, 0.95796],
+    [0.48261, 0.84331, 0.96259],
+    [0.4853, 0.85117, 0.96722],
+    [0.48801, 0.85906, 0.97186],
+    [0.49071, 0.86699, 0.97651],
+    [0.49339, 0.87495, 0.98116],
+    [0.49607, 0.88294, 0.98581],
+    [0.49877, 0.89096, 0.99047],
+    [0.50144, 0.89901, 0.99512],
+    [0.50411, 0.90708, 0.99978]]
 
-cmaps = {}
-for (name, data) in (
-    ('magma', _magma_data),
-    ('inferno', _inferno_data),
-    ('plasma', _plasma_data),
-    ('viridis', _viridis_data),
-    ('cividis', _cividis_data),
-    ('twilight', _twilight_data),
-    ('twilight_shifted', _twilight_shifted_data),
-    ('turbo', _turbo_data),
-):
+_vanimo_data = [
+    [1, 0.80346, 0.99215],
+    [0.99397, 0.79197, 0.98374],
+    [0.98791, 0.78052, 0.97535],
+    [0.98185, 0.7691, 0.96699],
+    [0.97578, 0.75774, 0.95867],
+    [0.96971, 0.74643, 0.95037],
+    [0.96363, 0.73517, 0.94211],
+    [0.95755, 0.72397, 0.93389],
+    [0.95147, 0.71284, 0.9257],
+    [0.94539, 0.70177, 0.91756],
+    [0.93931, 0.69077, 0.90945],
+    [0.93322, 0.67984, 0.90137],
+    [0.92713, 0.66899, 0.89334],
+    [0.92104, 0.65821, 0.88534],
+    [0.91495, 0.64751, 0.87738],
+    [0.90886, 0.63689, 0.86946],
+    [0.90276, 0.62634, 0.86158],
+    [0.89666, 0.61588, 0.85372],
+    [0.89055, 0.60551, 0.84591],
+    [0.88444, 0.59522, 0.83813],
+    [0.87831, 0.58503, 0.83039],
+    [0.87219, 0.57491, 0.82268],
+    [0.86605, 0.5649, 0.815],
+    [0.8599, 0.55499, 0.80736],
+    [0.85373, 0.54517, 0.79974],
+    [0.84756, 0.53544, 0.79216],
+    [0.84138, 0.52583, 0.78461],
+    [0.83517, 0.5163, 0.77709],
+    [0.82896, 0.5069, 0.76959],
+    [0.82272, 0.49761, 0.76212],
+    [0.81647, 0.48841, 0.75469],
+    [0.81018, 0.47934, 0.74728],
+    [0.80389, 0.47038, 0.7399],
+    [0.79757, 0.46154, 0.73255],
+    [0.79123, 0.45283, 0.72522],
+    [0.78487, 0.44424, 0.71792],
+    [0.77847, 0.43578, 0.71064],
+    [0.77206, 0.42745, 0.70339],
+    [0.76562, 0.41925, 0.69617],
+    [0.75914, 0.41118, 0.68897],
+    [0.75264, 0.40327, 0.68179],
+    [0.74612, 0.39549, 0.67465],
+    [0.73957, 0.38783, 0.66752],
+    [0.73297, 0.38034, 0.66041],
+    [0.72634, 0.37297, 0.65331],
+    [0.71967, 0.36575, 0.64623],
+    [0.71293, 0.35864, 0.63915],
+    [0.70615, 0.35166, 0.63206],
+    [0.69929, 0.34481, 0.62496],
+    [0.69236, 0.33804, 0.61782],
+    [0.68532, 0.33137, 0.61064],
+    [0.67817, 0.32479, 0.6034],
+    [0.67091, 0.3183, 0.59609],
+    [0.66351, 0.31184, 0.5887],
+    [0.65598, 0.30549, 0.58123],
+    [0.64828, 0.29917, 0.57366],
+    [0.64045, 0.29289, 0.56599],
+    [0.63245, 0.28667, 0.55822],
+    [0.6243, 0.28051, 0.55035],
+    [0.61598, 0.27442, 0.54237],
+    [0.60752, 0.26838, 0.53428],
+    [0.59889, 0.2624, 0.5261],
+    [0.59012, 0.25648, 0.51782],
+    [0.5812, 0.25063, 0.50944],
+    [0.57214, 0.24483, 0.50097],
+    [0.56294, 0.23914, 0.4924],
+    [0.55359, 0.23348, 0.48376],
+    [0.54413, 0.22795, 0.47505],
+    [0.53454, 0.22245, 0.46623],
+    [0.52483, 0.21706, 0.45736],
+    [0.51501, 0.21174, 0.44843],
+    [0.50508, 0.20651, 0.43942],
+    [0.49507, 0.20131, 0.43036],
+    [0.48495, 0.19628, 0.42125],
+    [0.47476, 0.19128, 0.4121],
+    [0.4645, 0.18639, 0.4029],
+    [0.45415, 0.18157, 0.39367],
+    [0.44376, 0.17688, 0.38441],
+    [0.43331, 0.17225, 0.37513],
+    [0.42282, 0.16773, 0.36585],
+    [0.41232, 0.16332, 0.35655],
+    [0.40178, 0.15897, 0.34726],
+    [0.39125, 0.15471, 0.33796],
+    [0.38071, 0.15058, 0.32869],
+    [0.37017, 0.14651, 0.31945],
+    [0.35969, 0.14258, 0.31025],
+    [0.34923, 0.13872, 0.30106],
+    [0.33883, 0.13499, 0.29196],
+    [0.32849, 0.13133, 0.28293],
+    [0.31824, 0.12778, 0.27396],
+    [0.30808, 0.12431, 0.26508],
+    [0.29805, 0.12097, 0.25631],
+    [0.28815, 0.11778, 0.24768],
+    [0.27841, 0.11462, 0.23916],
+    [0.26885, 0.11169, 0.23079],
+    [0.25946, 0.10877, 0.22259],
+    [0.25025, 0.10605, 0.21455],
+    [0.24131, 0.10341, 0.20673],
+    [0.23258, 0.10086, 0.19905],
+    [0.2241, 0.098494, 0.19163],
+    [0.21593, 0.096182, 0.18443],
+    [0.20799, 0.094098, 0.17748],
+    [0.20032, 0.092102, 0.17072],
+    [0.19299, 0.09021, 0.16425],
+    [0.18596, 0.088461, 0.15799],
+    [0.17918, 0.086861, 0.15197],
+    [0.17272, 0.08531, 0.14623],
+    [0.16658, 0.084017, 0.14075],
+    [0.1607, 0.082745, 0.13546],
+    [0.15515, 0.081683, 0.13049],
+    [0.1499, 0.080653, 0.1257],
+    [0.14493, 0.07978, 0.12112],
+    [0.1402, 0.079037, 0.11685],
+    [0.13578, 0.078426, 0.11282],
+    [0.13168, 0.077944, 0.10894],
+    [0.12782, 0.077586, 0.10529],
+    [0.12422, 0.077332, 0.1019],
+    [0.12091, 0.077161, 0.098724],
+    [0.11793, 0.077088, 0.095739],
+    [0.11512, 0.077124, 0.092921],
+    [0.11267, 0.077278, 0.090344],
+    [0.11042, 0.077557, 0.087858],
+    [0.10835, 0.077968, 0.085431],
+    [0.10665, 0.078516, 0.083233],
+    [0.105, 0.079207, 0.081185],
+    [0.10368, 0.080048, 0.079202],
+    [0.10245, 0.081036, 0.077408],
+    [0.10143, 0.082173, 0.075793],
+    [0.1006, 0.083343, 0.074344],
+    [0.099957, 0.084733, 0.073021],
+    [0.099492, 0.086174, 0.071799],
+    [0.099204, 0.087868, 0.070716],
+    [0.099092, 0.089631, 0.069813],
+    [0.099154, 0.091582, 0.069047],
+    [0.099384, 0.093597, 0.068337],
+    [0.099759, 0.095871, 0.067776],
+    [0.10029, 0.098368, 0.067351],
+    [0.10099, 0.101, 0.067056],
+    [0.10185, 0.1039, 0.066891],
+    [0.1029, 0.10702, 0.066853],
+    [0.10407, 0.11031, 0.066942],
+    [0.10543, 0.1138, 0.067155],
+    [0.10701, 0.1175, 0.067485],
+    [0.10866, 0.12142, 0.067929],
+    [0.11059, 0.12561, 0.06849],
+    [0.11265, 0.12998, 0.069162],
+    [0.11483, 0.13453, 0.069842],
+    [0.11725, 0.13923, 0.07061],
+    [0.11985, 0.14422, 0.071528],
+    [0.12259, 0.14937, 0.072403],
+    [0.12558, 0.15467, 0.073463],
+    [0.12867, 0.16015, 0.074429],
+    [0.13196, 0.16584, 0.075451],
+    [0.1354, 0.17169, 0.076499],
+    [0.13898, 0.17771, 0.077615],
+    [0.14273, 0.18382, 0.078814],
+    [0.14658, 0.1901, 0.080098],
+    [0.15058, 0.19654, 0.081473],
+    [0.15468, 0.20304, 0.08282],
+    [0.15891, 0.20968, 0.084315],
+    [0.16324, 0.21644, 0.085726],
+    [0.16764, 0.22326, 0.087378],
+    [0.17214, 0.23015, 0.088955],
+    [0.17673, 0.23717, 0.090617],
+    [0.18139, 0.24418, 0.092314],
+    [0.18615, 0.25132, 0.094071],
+    [0.19092, 0.25846, 0.095839],
+    [0.19578, 0.26567, 0.097702],
+    [0.20067, 0.2729, 0.099539],
+    [0.20564, 0.28016, 0.10144],
+    [0.21062, 0.28744, 0.10342],
+    [0.21565, 0.29475, 0.10534],
+    [0.22072, 0.30207, 0.10737],
+    [0.22579, 0.30942, 0.10942],
+    [0.23087, 0.31675, 0.11146],
+    [0.236, 0.32407, 0.11354],
+    [0.24112, 0.3314, 0.11563],
+    [0.24625, 0.33874, 0.11774],
+    [0.25142, 0.34605, 0.11988],
+    [0.25656, 0.35337, 0.12202],
+    [0.26171, 0.36065, 0.12422],
+    [0.26686, 0.36793, 0.12645],
+    [0.272, 0.37519, 0.12865],
+    [0.27717, 0.38242, 0.13092],
+    [0.28231, 0.38964, 0.13316],
+    [0.28741, 0.39682, 0.13541],
+    [0.29253, 0.40398, 0.13773],
+    [0.29763, 0.41111, 0.13998],
+    [0.30271, 0.4182, 0.14232],
+    [0.30778, 0.42527, 0.14466],
+    [0.31283, 0.43231, 0.14699],
+    [0.31787, 0.43929, 0.14937],
+    [0.32289, 0.44625, 0.15173],
+    [0.32787, 0.45318, 0.15414],
+    [0.33286, 0.46006, 0.1566],
+    [0.33781, 0.46693, 0.15904],
+    [0.34276, 0.47374, 0.16155],
+    [0.34769, 0.48054, 0.16407],
+    [0.3526, 0.48733, 0.16661],
+    [0.35753, 0.4941, 0.16923],
+    [0.36245, 0.50086, 0.17185],
+    [0.36738, 0.50764, 0.17458],
+    [0.37234, 0.51443, 0.17738],
+    [0.37735, 0.52125, 0.18022],
+    [0.38238, 0.52812, 0.18318],
+    [0.38746, 0.53505, 0.18626],
+    [0.39261, 0.54204, 0.18942],
+    [0.39783, 0.54911, 0.19272],
+    [0.40311, 0.55624, 0.19616],
+    [0.40846, 0.56348, 0.1997],
+    [0.4139, 0.57078, 0.20345],
+    [0.41942, 0.57819, 0.20734],
+    [0.42503, 0.5857, 0.2114],
+    [0.43071, 0.59329, 0.21565],
+    [0.43649, 0.60098, 0.22009],
+    [0.44237, 0.60878, 0.2247],
+    [0.44833, 0.61667, 0.22956],
+    [0.45439, 0.62465, 0.23468],
+    [0.46053, 0.63274, 0.23997],
+    [0.46679, 0.64092, 0.24553],
+    [0.47313, 0.64921, 0.25138],
+    [0.47959, 0.6576, 0.25745],
+    [0.48612, 0.66608, 0.26382],
+    [0.49277, 0.67466, 0.27047],
+    [0.49951, 0.68335, 0.2774],
+    [0.50636, 0.69213, 0.28464],
+    [0.51331, 0.70101, 0.2922],
+    [0.52035, 0.70998, 0.30008],
+    [0.5275, 0.71905, 0.30828],
+    [0.53474, 0.72821, 0.31682],
+    [0.54207, 0.73747, 0.32567],
+    [0.5495, 0.74682, 0.33491],
+    [0.55702, 0.75625, 0.34443],
+    [0.56461, 0.76577, 0.35434],
+    [0.5723, 0.77537, 0.36457],
+    [0.58006, 0.78506, 0.37515],
+    [0.58789, 0.79482, 0.38607],
+    [0.59581, 0.80465, 0.39734],
+    [0.60379, 0.81455, 0.40894],
+    [0.61182, 0.82453, 0.42086],
+    [0.61991, 0.83457, 0.43311],
+    [0.62805, 0.84467, 0.44566],
+    [0.63623, 0.85482, 0.45852],
+    [0.64445, 0.86503, 0.47168],
+    [0.6527, 0.8753, 0.48511],
+    [0.66099, 0.88562, 0.49882],
+    [0.6693, 0.89599, 0.51278],
+    [0.67763, 0.90641, 0.52699],
+    [0.68597, 0.91687, 0.54141],
+    [0.69432, 0.92738, 0.55605],
+    [0.70269, 0.93794, 0.5709],
+    [0.71107, 0.94855, 0.58593],
+    [0.71945, 0.9592, 0.60112],
+    [0.72782, 0.96989, 0.61646],
+    [0.7362, 0.98063, 0.63191],
+    [0.74458, 0.99141, 0.64748]]
 
-    cmaps[name] = ListedColormap(data, name=name)
-    # generate reversed colormap
-    name = name + '_r'
-    cmaps[name] = ListedColormap(list(reversed(data)), name=name)
+cmaps = {
+    name: ListedColormap(data, name=name) for name, data in [
+        ('magma', _magma_data),
+        ('inferno', _inferno_data),
+        ('plasma', _plasma_data),
+        ('viridis', _viridis_data),
+        ('cividis', _cividis_data),
+        ('twilight', _twilight_data),
+        ('twilight_shifted', _twilight_shifted_data),
+        ('turbo', _turbo_data),
+        ('berlin', _berlin_data),
+        ('managua', _managua_data),
+        ('vanimo', _vanimo_data),
+    ]}
diff --git a/napari/utils/colormaps/vendored/cm.py b/napari/utils/colormaps/vendored/cm.py
index 6b523a78696..aef9027744a 100644
--- a/napari/utils/colormaps/vendored/cm.py
+++ b/napari/utils/colormaps/vendored/cm.py
@@ -39,7 +39,7 @@
 def _reverser(f, x=None):
     """Helper such that ``_reverser(f)(x) == f(1 - x)``."""
     if x is None:
-        # Returning a partial object keeps it picklable.
+        # Returning a partial object keeps it pickleable.
         return functools.partial(_reverser, f)
     return f(1 - x)
 
diff --git a/napari/utils/events/__init__.py b/napari/utils/events/__init__.py
index 8771ab02d4b..825d4010cbf 100644
--- a/napari/utils/events/__init__.py
+++ b/napari/utils/events/__init__.py
@@ -18,18 +18,18 @@
 from napari.utils.events.types import SupportsEvents
 
 __all__ = [
-    'disconnect_events',
     'EmitterGroup',
     'Event',
+    'EventEmitter',
     'EventedDict',
     'EventedList',
     'EventedModel',
     'EventedSet',
-    'EventEmitter',
     'NestableEventedList',
     'SelectableEventedList',
     'Selection',
     'SupportsEvents',
     'TypedMutableSequence',
+    'disconnect_events',
     'set_event_tracing_enabled',
 ]
diff --git a/napari/utils/events/_tests/test_event_emitter.py b/napari/utils/events/_tests/test_event_emitter.py
index aebf64dc94b..4b028e8c147 100644
--- a/napari/utils/events/_tests/test_event_emitter.py
+++ b/napari/utils/events/_tests/test_event_emitter.py
@@ -182,19 +182,19 @@ def fun6(val):
     res_li = []
     e.connect(fun3)
     e()
-    assert res_li == [3, 1, 2]
+    assert res_li == [1, 3, 2]
     res_li = []
     e.connect(fun4)
     e()
-    assert res_li == [3, 1, 4, 2]
+    assert res_li == [1, 3, 2, 4]
     res_li = []
-    e.connect(partial(fun5, val=5), position='last')
+    e.connect(partial(fun5, val=5), position='first')
     e()
-    assert res_li == [3, 1, 5, 4, 2]
+    assert res_li == [5, 1, 3, 2, 4]
     res_li = []
-    e.connect(partial(fun6, val=6), position='last')
+    e.connect(partial(fun6, val=6), position='first')
     e()
-    assert res_li == [3, 1, 5, 4, 2, 6]
+    assert res_li == [5, 1, 3, 6, 2, 4]
 
 
 def test_event_order_methods():
@@ -228,7 +228,7 @@ def fun4(self):
     e.connect(t1.fun2)
     e.connect(t2.fun4)
     e()
-    assert res_li == [2, 1, 4, 3]
+    assert res_li == [1, 2, 3, 4]
 
 
 def test_no_event_arg():
diff --git a/napari/utils/events/_tests/test_evented_dict.py b/napari/utils/events/_tests/test_evented_dict.py
index c5e3a3b3499..c5abe06b558 100644
--- a/napari/utils/events/_tests/test_evented_dict.py
+++ b/napari/utils/events/_tests/test_evented_dict.py
@@ -6,7 +6,7 @@
 from napari.utils.events.containers import EventedDict
 
 
-@pytest.fixture()
+@pytest.fixture
 def regular_dict():
     return {'A': 0, 'B': 1, 'C': 2, False: '3', 4: 5}
 
diff --git a/napari/utils/events/_tests/test_evented_list.py b/napari/utils/events/_tests/test_evented_list.py
index c8ea5e76b7a..4c28aefc098 100644
--- a/napari/utils/events/_tests/test_evented_list.py
+++ b/napari/utils/events/_tests/test_evented_list.py
@@ -7,7 +7,7 @@
 from napari.utils.events import EmitterGroup, EventedList, NestableEventedList
 
 
-@pytest.fixture()
+@pytest.fixture
 def regular_list():
     return list(range(5))
 
diff --git a/napari/utils/events/_tests/test_evented_model.py b/napari/utils/events/_tests/test_evented_model.py
index d80cdf4899f..f29d12ac316 100644
--- a/napari/utils/events/_tests/test_evented_model.py
+++ b/napari/utils/events/_tests/test_evented_model.py
@@ -484,7 +484,7 @@ def test_evented_model_with_property_setters():
         t.e = 100
 
 
-@pytest.fixture()
+@pytest.fixture
 def mocked_object():
     t = T()
     t.events.a = Mock(t.events.a)
diff --git a/napari/utils/events/_tests/test_evented_set.py b/napari/utils/events/_tests/test_evented_set.py
index e3419989a18..c4bcce7f28a 100644
--- a/napari/utils/events/_tests/test_evented_set.py
+++ b/napari/utils/events/_tests/test_evented_set.py
@@ -5,12 +5,12 @@
 from napari.utils.events import EventedSet
 
 
-@pytest.fixture()
+@pytest.fixture
 def regular_set():
     return set(range(5))
 
 
-@pytest.fixture()
+@pytest.fixture
 def test_set(request, regular_set):
     test_set = EventedSet(regular_set)
     test_set.events = Mock(wraps=test_set.events)
diff --git a/napari/utils/events/containers/__init__.py b/napari/utils/events/containers/__init__.py
index 5b378313b38..f8f1bff729f 100644
--- a/napari/utils/events/containers/__init__.py
+++ b/napari/utils/events/containers/__init__.py
@@ -11,14 +11,14 @@
 from napari.utils.events.containers._typed import TypedMutableSequence
 
 __all__ = [
+    'EventedDict',
     'EventedList',
     'EventedSet',
     'NestableEventedList',
-    'EventedDict',
     'Selectable',
     'SelectableEventedList',
     'SelectableNestableEventedList',
     'Selection',
-    'TypedMutableSequence',
     'TypedMutableMapping',
+    'TypedMutableSequence',
 ]
diff --git a/napari/utils/events/containers/_evented_list.py b/napari/utils/events/containers/_evented_list.py
index 48e8c8298e9..e9c5fba734c 100644
--- a/napari/utils/events/containers/_evented_list.py
+++ b/napari/utils/events/containers/_evented_list.py
@@ -362,7 +362,7 @@ def _move_plan(
                 yield src, dest_index + d_inc
 
             popped.append(src)
-            # if the item moved up, icrement the destination index
+            # if the item moved up, increment the destination index
             if dest_index <= src:
                 d_inc += 1
 
diff --git a/napari/utils/events/containers/_nested_list.py b/napari/utils/events/containers/_nested_list.py
index 6804ea57eb0..bed8ca6813f 100644
--- a/napari/utils/events/containers/_nested_list.py
+++ b/napari/utils/events/containers/_nested_list.py
@@ -249,7 +249,7 @@ def _reemit_child_event(self, event: Event) -> None:
                 if hasattr(event, attr):
                     setattr(event, attr, ei)
 
-        # if the starting event was from a nestable envented list, we can
+        # if the starting event was from a nestable evented list, we can
         # use the same event type here (e.g: removed, inserted)
         if isinstance(event.source, NestableEventedList):
             emitter = getattr(self.events, event.type, self.events)
diff --git a/napari/utils/events/containers/_typed.py b/napari/utils/events/containers/_typed.py
index 4e3e47f188b..87fb6a478fc 100644
--- a/napari/utils/events/containers/_typed.py
+++ b/napari/utils/events/containers/_typed.py
@@ -20,7 +20,7 @@
 Index = Union[int, slice]
 
 _T = TypeVar('_T')
-_L = TypeVar('_L')
+_L = TypeVar('_L', bound=Any)
 
 
 class TypedMutableSequence(MutableSequence[_T]):
@@ -170,7 +170,7 @@ def __newlike__(
         self, iterable: Iterable[_T]
     ) -> 'TypedMutableSequence[_T]':
         new = self.__class__()
-        # seperating this allows subclasses to omit these from their `__init__`
+        # separating this allows subclasses to omit these from their `__init__`
         new._basetypes = self._basetypes
         new._lookup = self._lookup.copy()
         new.extend(iterable)
diff --git a/napari/utils/events/debugging.py b/napari/utils/events/debugging.py
index 12fcb0bbc4b..fa4a36b915c 100644
--- a/napari/utils/events/debugging.py
+++ b/napari/utils/events/debugging.py
@@ -9,7 +9,7 @@
 from napari.utils.translations import trans
 
 try:
-    from rich import print
+    from rich import print  # noqa: A004
 except ModuleNotFoundError:
     print(
         trans._(
@@ -113,7 +113,7 @@ def log_event_stack(event: 'Event', cfg: EventDebugSettings = _SETTINGS):
                 lines.insert(1, f'  was triggered by {trigger}, via:')
                 break
 
-    # seperate groups of events
+    # separate groups of events
     if not cfg._cur_depth:
         lines = ['─' * 79, '', *lines]
     elif not cfg.nesting_allowance:
diff --git a/napari/utils/events/event.py b/napari/utils/events/event.py
index 6c0a5f3fff1..5ccc2a63999 100644
--- a/napari/utils/events/event.py
+++ b/napari/utils/events/event.py
@@ -138,12 +138,12 @@ def _pop_source(self):
 
     @property
     def type(self) -> str:
-        # No docstring; documeted in class docstring
+        # No docstring; documented in class docstring
         return self._type
 
     @property
     def native(self) -> Any:
-        # No docstring; documeted in class docstring
+        # No docstring; documented in class docstring
         return self._native
 
     @property
@@ -410,7 +410,7 @@ def connect(
         self,
         callback: Union[Callback, CallbackRef, CallbackStr, 'EventEmitter'],
         ref: Union[bool, str] = False,
-        position: Literal['first', 'last'] = 'first',
+        position: Literal['first', 'last'] = 'last',
         before: Union[str, Callback, list[Union[str, Callback]], None] = None,
         after: Union[str, Callback, list[Union[str, Callback]], None] = None,
         until: Optional['EventEmitter'] = None,
diff --git a/napari/utils/events/evented_model.py b/napari/utils/events/evented_model.py
index e7741197e45..122eca8e09e 100644
--- a/napari/utils/events/evented_model.py
+++ b/napari/utils/events/evented_model.py
@@ -320,7 +320,7 @@ def _check_if_values_changed_and_emit_if_needed(self):
             return
         to_emit = []
         for name in self._primary_changes:
-            # primary changes should contains only fields that are changed directly by assigment
+            # primary changes should contains only fields that are changed directly by assignment
             if name not in self._changes_queue:
                 continue
             old_value = self._changes_queue[name]
@@ -381,7 +381,7 @@ def _setattr_impl(self, name: str, value: Any) -> None:
         # set value using original setter
         self._super_setattr_(name, value)
 
-    # expose the private EmitterGroup publically
+    # expose the private EmitterGroup publicly
     @property
     def events(self) -> EmitterGroup:
         return self._events
diff --git a/napari/utils/geometry.py b/napari/utils/geometry.py
index 67645d67db4..c4b526bfe86 100644
--- a/napari/utils/geometry.py
+++ b/napari/utils/geometry.py
@@ -187,9 +187,9 @@ def point_in_bounding_box(point: np.ndarray, bounding_box: np.ndarray) -> bool:
         (2, n) array containing the min and max of the nD bounding box.
         As returned by `Layer._extent_data`.
     """
-    if np.all(point >= bounding_box[0]) and np.all(point <= bounding_box[1]):
-        return True
-    return False
+    return bool(
+        np.all(point >= bounding_box[0]) and np.all(point <= bounding_box[1])
+    )
 
 
 def clamp_point_to_bounding_box(point: np.ndarray, bounding_box: np.ndarray):
diff --git a/napari/utils/info.py b/napari/utils/info.py
index 028538d10e4..22d967994bd 100644
--- a/napari/utils/info.py
+++ b/napari/utils/info.py
@@ -118,7 +118,9 @@ def sys_info(as_html: bool = False) -> str:
         ('superqt', 'superqt'),
         ('in_n_out', 'in-n-out'),
         ('app_model', 'app-model'),
+        ('psygnal', 'psygnal'),
         ('npe2', 'npe2'),
+        ('pydantic', 'pydantic'),
     )
 
     loaded = {}
@@ -166,6 +168,7 @@ def sys_info(as_html: bool = False) -> str:
     optional_modules = (
         ('numba', 'numba'),
         ('triangle', 'triangle'),
+        ('napari_plugin_manager', 'napari-plugin-manager'),
     )
 
     for module, name in optional_modules:
diff --git a/napari/utils/interactions.py b/napari/utils/interactions.py
index c630b81b712..6cd9edb0b78 100644
--- a/napari/utils/interactions.py
+++ b/napari/utils/interactions.py
@@ -6,8 +6,8 @@
 from numpydoc.docscrape import FunctionDoc
 
 from napari.utils.key_bindings import (
-    KeyBinding,
     KeyBindingLike,
+    KeyCode,
     coerce_keybinding,
 )
 from napari.utils.translations import trans
@@ -49,7 +49,7 @@ def hello_world(layer, event):
         if inspect.isgenerator(gen):
             try:
                 next(gen)
-                # now store iterated genenerator
+                # now store iterated generator
                 obj._mouse_wheel_gen[mouse_wheel_func] = gen
                 # and now store event that initially triggered the press
                 obj._persisted_mouse_event[gen] = event
@@ -127,7 +127,7 @@ def hello_world(layer, event):
         if inspect.isgenerator(gen):
             try:
                 next(gen)
-                # now store iterated genenerator
+                # now store iterated generator
                 obj._mouse_drag_gen[mouse_drag_func] = gen
                 # and now store event that initially triggered the press
                 obj._persisted_mouse_event[gen] = event
@@ -220,55 +220,27 @@ def hello_world(layer, event):
 
 
 KEY_SYMBOLS = {
-    'Ctrl': 'Ctrl',
-    'Shift': '⇧',
-    'Alt': 'Alt',
-    'Meta': '⊞',
-    'Left': '←',
-    'Right': '→',
-    'Up': '↑',
-    'Down': '↓',
-    'Backspace': '⌫',
-    'Delete': '⌦',
-    'Tab': '↹',
-    'Escape': 'Esc',
-    'Return': '⏎',
-    'Enter': '↵',
-    'Space': '␣',
+    'Ctrl': KeyCode.from_string('Ctrl').os_symbol(),
+    'Shift': KeyCode.from_string('Shift').os_symbol(),
+    'Alt': KeyCode.from_string('Alt').os_symbol(),
+    'Meta': KeyCode.from_string('Meta').os_symbol(),
+    'Left': KeyCode.from_string('Left').os_symbol(),
+    'Right': KeyCode.from_string('Right').os_symbol(),
+    'Up': KeyCode.from_string('Up').os_symbol(),
+    'Down': KeyCode.from_string('Down').os_symbol(),
+    'Backspace': KeyCode.from_string('Backspace').os_symbol(),
+    'Delete': KeyCode.from_string('Delete').os_symbol(),
+    'Tab': KeyCode.from_string('Tab').os_symbol(),
+    'Escape': KeyCode.from_string('Escape').os_symbol(),
+    'Return': KeyCode.from_string('Return').os_symbol(),
+    'Enter': KeyCode.from_string('Enter').os_symbol(),
+    'Space': KeyCode.from_string('Space').os_symbol(),
 }
 
 
 JOINCHAR = '+'
 if sys.platform.startswith('darwin'):
-    KEY_SYMBOLS.update({'Ctrl': '⌃', 'Alt': '⌥', 'Meta': '⌘'})
     JOINCHAR = ''
-elif sys.platform.startswith('linux'):
-    KEY_SYMBOLS.update({'Meta': 'Super'})
-
-
-def _kb2mods(key_bind: KeyBinding) -> list[str]:
-    """Extract list of modifiers from a key binding.
-
-    Parameters
-    ----------
-    key_bind : KeyBinding
-        The key binding whose mods are to be extracted.
-
-    Returns
-    -------
-    list of str
-        The key modifiers used by the key binding.
-    """
-    mods = []
-    if key_bind.ctrl:
-        mods.append('Ctrl')
-    if key_bind.shift:
-        mods.append('Shift')
-    if key_bind.alt:
-        mods.append('Alt')
-    if key_bind.meta:
-        mods.append('Meta')
-    return mods
 
 
 class Shortcut:
@@ -316,7 +288,7 @@ def parse_platform(text: str) -> str:
 
         This replace platform specific symbols, like ↵ by Enter,  ⌘ by Command on MacOS....
         """
-        # edge case, shortcut combinaison where `+` is a key.
+        # edge case, shortcut combination where `+` is a key.
         # this should be rare as on english keyboard + is Shift-Minus.
         # but not unheard of. In those case `+` is always at the end with `++`
         # as you can't get two non-modifier keys,  or alone.
@@ -357,13 +329,7 @@ def platform(self) -> str:
         string
             Shortcut formatted to be displayed on current paltform.
         """
-        return ' '.join(
-            JOINCHAR.join(
-                KEY_SYMBOLS.get(x, x)
-                for x in ([*_kb2mods(part), str(part.key)])
-            )
-            for part in self._kb.parts
-        )
+        return self._kb.to_text(use_symbols=True, joinchar=JOINCHAR)
 
     def __str__(self):
         return self.platform
diff --git a/napari/utils/io.py b/napari/utils/io.py
index 3780d9e7d21..9e2074e7f25 100644
--- a/napari/utils/io.py
+++ b/napari/utils/io.py
@@ -1,4 +1,5 @@
 import os
+import struct
 import warnings
 
 import numpy as np
@@ -103,7 +104,15 @@ def imsave_tiff(filename, data):
         # 'compression' kwarg since 2021.6.6; we depend on more recent versions
         # now. See:
         # https://forum.image.sc/t/problem-saving-generated-labels-in-cellpose-napari/54892/8
-        tifffile.imwrite(filename, data, compression=('zlib', 1))
+        try:
+            tifffile.imwrite(filename, data, compression=('zlib', 1))
+        except struct.error:  # compressed data >4GB
+            tifffile.imwrite(
+                filename,
+                data,
+                compression=('zlib', 1),
+                bigtiff=True,
+            )
 
 
 def __getattr__(name: str):
diff --git a/napari/utils/misc.py b/napari/utils/misc.py
index 02b8e888777..25e1a954ea9 100644
--- a/napari/utils/misc.py
+++ b/napari/utils/misc.py
@@ -149,7 +149,7 @@ def ensure_iterable(
         )
     if is_iterable(
         arg, color=color
-    ):  # argumnet color is to be removed in 0.6.0
+    ):  # argument color is to be removed in 0.6.0
         return arg
 
     return itertools.repeat(arg)
@@ -180,7 +180,7 @@ def is_iterable(
     if isinstance(arg, (str, Enum)) or np.isscalar(arg):
         return False
 
-    # this is to be removed in 0.6.0, coloer is never set True
+    # this is to be removed in 0.6.0, color is never set True
     if color is True and isinstance(arg, (list, np.ndarray)):
         return np.array(arg).ndim != 1 or len(arg) not in [3, 4]
 
@@ -193,15 +193,15 @@ def is_sequence(arg: Any) -> bool:
     return True:
         list
         tuple
-    return False
+    return False:
         string
         numbers
         dict
         set
     """
-    if isinstance(arg, collections.abc.Sequence) and not isinstance(arg, str):
-        return True
-    return False
+    return bool(
+        isinstance(arg, collections.abc.Sequence) and not isinstance(arg, str)
+    )
 
 
 def ensure_sequence_of_iterables(
@@ -499,7 +499,7 @@ def ensure_layer_data_tuple(val: tuple) -> tuple:
     if not isinstance(val, tuple) and val:
         raise TypeError(msg)
     if len(val) > 1:
-        if not isinstance(val[1], dict):
+        if not isinstance(val[1], collections.abc.Mapping):
             raise TypeError(msg)
         if len(val) > 2 and not isinstance(val[2], str):
             raise TypeError(msg)
diff --git a/napari/utils/notebook_display.py b/napari/utils/notebook_display.py
index 5727bf50fb7..45a5c15b954 100644
--- a/napari/utils/notebook_display.py
+++ b/napari/utils/notebook_display.py
@@ -14,7 +14,7 @@
 
 from napari.utils.io import imsave_png
 
-__all__ = ['nbscreenshot', 'NotebookScreenshot']
+__all__ = ['NotebookScreenshot', 'nbscreenshot']
 
 
 class NotebookScreenshot:
@@ -108,9 +108,9 @@ def _repr_png_(self):
         -------
         In memory binary stream containing PNG screenshot image.
         """
-        from napari._qt.qt_event_loop import get_app
+        from napari._qt.qt_event_loop import get_qapp
 
-        get_app().processEvents()
+        get_qapp().processEvents()
         self.image = self.viewer.screenshot(
             canvas_only=self.canvas_only, flash=False
         )
diff --git a/napari/utils/notifications.py b/napari/utils/notifications.py
index 4091e67a5e2..da07c68b333 100644
--- a/napari/utils/notifications.py
+++ b/napari/utils/notifications.py
@@ -23,16 +23,16 @@
 }
 
 __all__ = [
-    'NotificationSeverity',
-    'Notification',
     'ErrorNotification',
-    'WarningNotification',
+    'Notification',
     'NotificationManager',
+    'NotificationSeverity',
+    'WarningNotification',
+    'show_console_notification',
     'show_debug',
+    'show_error',
     'show_info',
     'show_warning',
-    'show_error',
-    'show_console_notification',
 ]
 
 
diff --git a/napari/utils/perf/__init__.py b/napari/utils/perf/__init__.py
index a4d3119be0c..d4928c56ee5 100644
--- a/napari/utils/perf/__init__.py
+++ b/napari/utils/perf/__init__.py
@@ -65,12 +65,12 @@
 
 
 __all__ = [
-    'perf_config',
     'USE_PERFMON',
+    'PerfEvent',
     'add_counter_event',
     'add_instant_event',
     'block_timer',
+    'perf_config',
     'perf_timer',
     'timers',
-    'PerfEvent',
 ]
diff --git a/napari/utils/perf/_patcher.py b/napari/utils/perf/_patcher.py
index 1a57f37f742..912f8940571 100644
--- a/napari/utils/perf/_patcher.py
+++ b/napari/utils/perf/_patcher.py
@@ -142,7 +142,7 @@ def _import_module(
             # We successfully imported part of the target_str but then
             # we got a failure. Usually this is because we tried
             # importing a class or function. Return the inner-most
-            # module we did successfuly import. And return the rest of
+            # module we did successfully import. And return the rest of
             # the module_path we didn't use.
             attribute_str = '.'.join(parts[i - 1 :])
             return module, attribute_str
diff --git a/napari/utils/progress.py b/napari/utils/progress.py
index 6eff172daad..f7fe5e2d4b6 100644
--- a/napari/utils/progress.py
+++ b/napari/utils/progress.py
@@ -8,7 +8,7 @@
 from napari.utils.events.event import EmitterGroup, Event
 from napari.utils.translations import trans
 
-__all__ = ['progress', 'progrange', 'cancelable_progress']
+__all__ = ['cancelable_progress', 'progrange', 'progress']
 
 
 class progress(tqdm):
diff --git a/napari/utils/shortcuts.py b/napari/utils/shortcuts.py
index 2f3e2771655..86c078fcd6b 100644
--- a/napari/utils/shortcuts.py
+++ b/napari/utils/shortcuts.py
@@ -17,6 +17,7 @@
     'napari:focus_axes_down': [KeyMod.Alt | KeyCode.DownArrow],
     'napari:roll_axes': [KeyMod.CtrlCmd | KeyCode.KeyE],
     'napari:transpose_axes': [KeyMod.CtrlCmd | KeyCode.KeyT],
+    'napari:rotate_layers': [KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyT],
     'napari:toggle_grid': [KeyMod.CtrlCmd | KeyCode.KeyG],
     'napari:toggle_selected_visibility': [KeyCode.KeyV],
     'napari:toggle_unselected_visibility': [KeyMod.Shift | KeyCode.KeyV],
@@ -82,8 +83,12 @@
         KeyCode.Delete,
         KeyCode.Backspace,
     ],
-    'napari:finish_drawing_shape': [KeyCode.Escape],
+    'napari:finish_drawing_shape': [KeyCode.Enter, KeyCode.Escape],
     # image
+    'napari:orient_plane_normal_along_x': [KeyCode.KeyX],
+    'napari:orient_plane_normal_along_y': [KeyCode.KeyY],
+    'napari:orient_plane_normal_along_z': [KeyCode.KeyZ],
+    'napari:orient_plane_normal_along_view_direction': [KeyCode.KeyO],
     'napari:activate_image_pan_zoom_mode': [KeyCode.Digit1],
     'napari:activate_image_transform_mode': [KeyCode.Digit2],
     # vectors
diff --git a/napari/utils/theme.py b/napari/utils/theme.py
index ee5aa0cce5b..dea8aec9be4 100644
--- a/napari/utils/theme.py
+++ b/napari/utils/theme.py
@@ -11,7 +11,6 @@
 import npe2
 
 from napari._pydantic_compat import Color, validator
-from napari._vendor import darkdetect
 from napari.resources._icons import (
     PLUGIN_FILE_NAME,
     _theme_path,
@@ -223,6 +222,10 @@ def gradient_match(matchobj):
 
 def get_system_theme() -> str:
     """Return the system default theme, either 'dark', or 'light'."""
+    try:
+        from napari._vendor import darkdetect
+    except ImportError:
+        return 'dark'
     try:
         id_ = darkdetect.theme().lower()
     except AttributeError:
diff --git a/napari/utils/transforms/__init__.py b/napari/utils/transforms/__init__.py
index deea0c5bb27..193f41ba580 100644
--- a/napari/utils/transforms/__init__.py
+++ b/napari/utils/transforms/__init__.py
@@ -8,10 +8,10 @@
 )
 
 __all__ = [
-    'shear_matrix_from_angle',
     'Affine',
     'CompositeAffine',
     'ScaleTranslate',
     'Transform',
     'TransformChain',
+    'shear_matrix_from_angle',
 ]
diff --git a/napari/utils/transforms/_tests/test_transforms.py b/napari/utils/transforms/_tests/test_transforms.py
index ddefd3244a5..69cad837c9d 100644
--- a/napari/utils/transforms/_tests/test_transforms.py
+++ b/napari/utils/transforms/_tests/test_transforms.py
@@ -248,7 +248,7 @@ def test_affine_matrix_compose(dimensionality):
     np.testing.assert_almost_equal(transform_A.affine_matrix, A)
     np.testing.assert_almost_equal(transform_B.affine_matrix, B)
 
-    # Compose tranform and directly matrix multiply
+    # Compose transform and directly matrix multiply
     transform_C = transform_B.compose(transform_A)
     C = B @ A
     np.testing.assert_almost_equal(transform_C.affine_matrix, C)
diff --git a/napari/utils/transforms/_units.py b/napari/utils/transforms/_units.py
index a9d4efa5a40..b52997838fa 100644
--- a/napari/utils/transforms/_units.py
+++ b/napari/utils/transforms/_units.py
@@ -13,9 +13,9 @@
 
 
 __all__ = (
-    'get_units_from_name',
-    'UnitsLike',
     'UnitsInfo',
+    'UnitsLike',
+    'get_units_from_name',
 )
 
 
diff --git a/napari/utils/translations.py b/napari/utils/translations.py
index 68e861c79ce..a60b9dae702 100644
--- a/napari/utils/translations.py
+++ b/napari/utils/translations.py
@@ -171,13 +171,13 @@ class TranslationString(str):
     """
 
     __slots__ = (
+        '_deferred',
         '_domain',
+        '_kwargs',
         '_msgctxt',
         '_msgid',
         '_msgid_plural',
         '_n',
-        '_deferred',
-        '_kwargs',
     )
 
     def __deepcopy__(self, memo):
diff --git a/napari/utils/tree/__init__.py b/napari/utils/tree/__init__.py
index ca4f6bbfb65..1b514adbd13 100644
--- a/napari/utils/tree/__init__.py
+++ b/napari/utils/tree/__init__.py
@@ -1,4 +1,4 @@
 from napari.utils.tree.group import Group
 from napari.utils.tree.node import Node
 
-__all__ = ['Node', 'Group']
+__all__ = ['Group', 'Node']
diff --git a/napari/utils/tree/_tests/test_tree_model.py b/napari/utils/tree/_tests/test_tree_model.py
index d73f0afddf6..4446e436058 100644
--- a/napari/utils/tree/_tests/test_tree_model.py
+++ b/napari/utils/tree/_tests/test_tree_model.py
@@ -5,7 +5,7 @@
 from napari.utils.tree import Group, Node
 
 
-@pytest.fixture()
+@pytest.fixture
 def tree():
     return Group(
         [
diff --git a/napari/view_layers.py b/napari/view_layers.py
index 40c1ad2927d..d8e1f1fc704 100644
--- a/napari/view_layers.py
+++ b/napari/view_layers.py
@@ -23,6 +23,7 @@ def view_<layer_type>(*args, **kwargs):
 from napari.viewer import Viewer
 
 __all__ = [
+    'imshow',
     'view_image',
     'view_labels',
     'view_path',
@@ -31,7 +32,6 @@ def view_<layer_type>(*args, **kwargs):
     'view_surface',
     'view_tracks',
     'view_vectors',
-    'imshow',
 ]
 
 _doc_template = """Create a viewer and add a{n} {layer_string} layer.
@@ -108,7 +108,7 @@ def _merge_layer_viewer_sigs_docs(func):
         'return': Viewer,
     }
 
-    # _forwardrefns_ is used by stubgen.py to populate the globalns
+    # _forwardrefns_ is used by stubgen.py to populate the globals
     # when evaluate forward references with get_type_hints
     func._forwardrefns_ = {**add_method.__globals__}
     return func
diff --git a/napari/viewer.py b/napari/viewer.py
index fb92478f50a..f7b5fc49091 100644
--- a/napari/viewer.py
+++ b/napari/viewer.py
@@ -7,6 +7,7 @@
 
 from napari.components.viewer_model import ViewerModel
 from napari.utils import _magicgui
+from napari.utils.events.event_utils import disconnect_events
 
 if TYPE_CHECKING:
     # helpful for IDE support
@@ -66,7 +67,7 @@ def __init__(
         self._window = Window(self, show=show)
         self._instances.add(self)
 
-    # Expose private window publically. This is needed to keep window off pydantic model
+    # Expose private window publicly. This is needed to keep window off pydantic model
     @property
     def window(self) -> 'Window':
         return self._window
@@ -194,6 +195,9 @@ def close(self):
         """Close the viewer window."""
         # Shutdown the slicer first to avoid processing any more tasks.
         self._layer_slicer.shutdown()
+        # Disconnect changes to dims before removing layers one-by-one
+        # to avoid any unnecessary slicing.
+        disconnect_events(self.dims.events, self)
         # Remove all the layers from the viewer
         self.layers.clear()
         # Close the main window
@@ -204,7 +208,7 @@ def close(self):
     @classmethod
     def close_all(cls) -> int:
         """
-        Class metod, Close all existing viewer instances.
+        Class method, Close all existing viewer instances.
 
         This is mostly exposed to avoid leaking of viewers when running tests.
         As having many non-closed viewer can adversely affect performances.
diff --git a/napari_builtins/_tests/conftest.py b/napari_builtins/_tests/conftest.py
index 7f9d7716b26..f89b7919402 100644
--- a/napari_builtins/_tests/conftest.py
+++ b/napari_builtins/_tests/conftest.py
@@ -54,6 +54,6 @@ def some_layer(request):
     return request.param
 
 
-@pytest.fixture()
+@pytest.fixture
 def layers_list():
     return LAYERS
diff --git a/napari_builtins/_tests/test_io.py b/napari_builtins/_tests/test_io.py
index 8a042736f84..fa7da1f6e07 100644
--- a/napari_builtins/_tests/test_io.py
+++ b/napari_builtins/_tests/test_io.py
@@ -1,7 +1,7 @@
 import csv
 import os
+from importlib.metadata import version
 from pathlib import Path
-from tempfile import TemporaryDirectory
 from typing import NamedTuple
 from uuid import uuid4
 
@@ -12,6 +12,7 @@
 import pytest
 import tifffile
 import zarr
+from packaging.version import parse as parse_version
 
 from napari_builtins.io._read import (
     _guess_layer_type_from_column_names,
@@ -38,7 +39,7 @@ class ImageSpec(NamedTuple):
 ZARR1 = ImageSpec((10, 20, 20), 'uint8', '.zarr')
 
 
-@pytest.fixture()
+@pytest.fixture
 def write_spec(tmp_path: Path):
     def writer(spec: ImageSpec):
         image = np.random.random(spec.shape).astype(spec.dtype)
@@ -47,7 +48,7 @@ def writer(spec: ImageSpec):
             tifffile.imwrite(str(fname), image)
         elif spec.ext == '.zarr':
             fname.mkdir()
-            z = zarr.open(str(fname), 'a', shape=image.shape)
+            z = zarr.open(store=str(fname), mode='a', shape=image.shape)
             z[:] = image
         else:
             imageio.imwrite(str(fname), image)
@@ -68,23 +69,21 @@ def test_guess_zarr_path():
     assert not _guess_zarr_path('no_zarr_suffix/data.png')
 
 
-def test_zarr():
+def test_zarr(tmp_path):
     image = np.random.random((10, 20, 20))
-    with TemporaryDirectory(suffix='.zarr') as fout:
-        z = zarr.open(fout, 'a', shape=image.shape)
-        z[:] = image
-        image_in = magic_imread([fout])
-        # Note: due to lazy loading, the next line needs to happen within
-        # the context manager. Alternatively, we could convert to NumPy here.
-        np.testing.assert_array_equal(image, image_in)
+    data_path = str(tmp_path / 'data.zarr')
+    z = zarr.open(store=data_path, mode='a', shape=image.shape)
+    z[:] = image
+    image_in = magic_imread([data_path])
+    np.testing.assert_array_equal(image, image_in)
 
 
 def test_zarr_nested(tmp_path):
     image = np.random.random((10, 20, 20))
     image_name = 'my_image'
     root_path = tmp_path / 'dataset.zarr'
-    grp = zarr.open(str(root_path), mode='a')
-    grp.create_dataset(image_name, data=image)
+    grp = zarr.open(store=str(root_path), mode='a')
+    grp.create_dataset(image_name, data=image, shape=image.shape)
 
     image_in = magic_imread([str(root_path / image_name)])
     np.testing.assert_array_equal(image, image_in)
@@ -94,8 +93,8 @@ def test_zarr_with_unrelated_file(tmp_path):
     image = np.random.random((10, 20, 20))
     image_name = 'my_image'
     root_path = tmp_path / 'dataset.zarr'
-    grp = zarr.open(str(root_path), mode='a')
-    grp.create_dataset(image_name, data=image)
+    grp = zarr.open(store=str(root_path), mode='a')
+    grp.create_dataset(image_name, data=image, shape=image.shape)
 
     txt_file_path = root_path / 'unrelated.txt'
     txt_file_path.touch()
@@ -104,24 +103,23 @@ def test_zarr_with_unrelated_file(tmp_path):
     np.testing.assert_array_equal(image, image_in[0])
 
 
-def test_zarr_multiscale():
+def test_zarr_multiscale(tmp_path):
     multiscale = [
         np.random.random((20, 20)),
         np.random.random((10, 10)),
         np.random.random((5, 5)),
     ]
-    with TemporaryDirectory(suffix='.zarr') as fout:
-        root = zarr.open_group(fout, 'a')
-        for i in range(len(multiscale)):
-            shape = 20 // 2**i
-            z = root.create_dataset(str(i), shape=(shape,) * 2)
-            z[:] = multiscale[i]
-        multiscale_in = magic_imread([fout])
-        assert len(multiscale) == len(multiscale_in)
-        # Note: due to lazy loading, the next line needs to happen within
-        # the context manager. Alternatively, we could convert to NumPy here.
-        for images, images_in in zip(multiscale, multiscale_in):
-            np.testing.assert_array_equal(images, images_in)
+    fout = str(tmp_path / 'multiscale.zarr')
+
+    root = zarr.open_group(fout, mode='a')
+    for i in range(len(multiscale)):
+        shape = 20 // 2**i
+        z = root.create_dataset(str(i), shape=(shape,) * 2)
+        z[:] = multiscale[i]
+    multiscale_in = magic_imread([fout])
+    assert len(multiscale) == len(multiscale_in)
+    for images, images_in in zip(multiscale, multiscale_in):
+        np.testing.assert_array_equal(images, images_in)
 
 
 def test_write_csv(tmpdir):
@@ -312,28 +310,37 @@ def test_add_zarr(write_spec):
     assert out[0].shape == ZARR1.shape  # type: ignore
 
 
-def test_add_zarr_1d_array_is_ignored():
+def test_add_zarr_1d_array_is_ignored(tmp_path):
+    zarr_dir = str(tmp_path / 'data.zarr')
     # For more details: https://github.com/napari/napari/issues/1471
-    with TemporaryDirectory(suffix='.zarr') as zarr_dir:
-        z = zarr.open(zarr_dir, 'w')
-        z['1d'] = np.zeros(3)
 
-        image_path = os.path.join(zarr_dir, '1d')
-        assert npe2.read([image_path], stack=False) == [(None,)]
+    z = zarr.open(store=zarr_dir, mode='w')
+    z.zeros(name='1d', shape=(3,), chunks=(3,), dtype='float32')
+
+    image_path = os.path.join(zarr_dir, '1d')
+    assert npe2.read([image_path], stack=False) == [(None,)]
 
 
-def test_add_many_zarr_1d_array_is_ignored():
+@pytest.mark.xfail(
+    parse_version(version('zarr')) >= parse_version('3.0.0a0')
+    and os.name == 'nt',
+    reason='zarr 3 return incorrect key in windows',
+    strict=True,
+)
+def test_add_many_zarr_1d_array_is_ignored(tmp_path):
     # For more details: https://github.com/napari/napari/issues/1471
-    with TemporaryDirectory(suffix='.zarr') as zarr_dir:
-        z = zarr.open(zarr_dir, 'w')
-        z['1d'] = np.zeros(3)
-        z['2d'] = np.zeros((3, 4))
-        z['3d'] = np.zeros((3, 4, 5))
-
-        for name in z.array_keys():
-            [out] = npe2.read([os.path.join(zarr_dir, name)], stack=False)
-            if name == '1d':
-                assert out == (None,)
-            else:
-                assert isinstance(out[0], da.Array)
-                assert out[0].ndim == int(name[0])
+    zarr_dir = str(tmp_path / 'data.zarr')
+
+    z = zarr.open(store=zarr_dir, mode='w')
+
+    z.zeros(name='1d', shape=(3,), chunks=(3,), dtype='float32')
+    z.zeros(name='2d', shape=(3, 4), chunks=(3, 4), dtype='float32')
+    z.zeros(name='3d', shape=(3, 4, 5), chunks=(3, 4, 5), dtype='float32')
+
+    for name in z.array_keys():
+        [out] = npe2.read([os.path.join(zarr_dir, name)], stack=False)
+        if name.endswith('1d'):
+            assert out == (None,)
+        else:
+            assert isinstance(out[0], da.Array), name
+            assert out[0].ndim == int(name[0])
diff --git a/napari_builtins/_tests/test_reader.py b/napari_builtins/_tests/test_reader.py
index 7d961fe44b1..8c218a478f4 100644
--- a/napari_builtins/_tests/test_reader.py
+++ b/napari_builtins/_tests/test_reader.py
@@ -10,7 +10,7 @@
 from napari_builtins.io._write import write_csv
 
 
-@pytest.fixture()
+@pytest.fixture
 def save_image(tmp_path: Path):
     """Create a temporary file."""
 
@@ -51,6 +51,7 @@ def test_animated_gif_reader(save_image):
     assert layer_data[0][0].shape == (5, 20, 20, 3)
 
 
+@pytest.mark.slow
 def test_reader_plugin_url():
     layer_data = npe2.read(
         ['https://samples.fiji.sc/FakeTracks.tif'], stack=False
diff --git a/napari_builtins/io/_read.py b/napari_builtins/io/_read.py
index 4d0e0fd022d..fe26a95be78 100644
--- a/napari_builtins/io/_read.py
+++ b/napari_builtins/io/_read.py
@@ -133,6 +133,18 @@ def read_zarr_dataset(path: str):
         ]
         assert image, 'No arrays found in zarr group'
         shape = image[0].shape
+    elif (path / 'zarr.json').exists():
+        # zarr v3
+        import zarr
+
+        data = zarr.open(store=path)
+        if isinstance(data, zarr.Array):
+            image = da.from_zarr(data)
+            shape = image.shape
+        else:
+            image = [data[k] for k in sorted(data)]
+            assert image, 'No arrays found in zarr group'
+            shape = image[0].shape
     else:  # pragma: no cover
         raise ValueError(
             trans._(
diff --git a/pyproject.toml b/pyproject.toml
index fb114e130ae..9c34dbc0f6e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -37,7 +37,7 @@ classifiers = [
 requires-python = ">=3.9"
 dependencies = [
     "appdirs>=1.4.4",
-    "app-model>=0.2.5,<0.3.0",
+    "app-model>=0.3.0,<0.4.0",
     "cachey>=0.2.1",
     "certifi>=2018.1.18",
     "dask[array]>=2021.10.0",
@@ -45,7 +45,7 @@ dependencies = [
     "jsonschema>=3.2.0",
     "lazy_loader>=0.2",
     "magicgui>=0.7.0",
-    "napari-console>=0.0.9",
+    "napari-console>=0.1.1",
     "napari-plugin-engine>=0.1.9",
     "napari-svg>=0.1.8",
     "npe2>=0.7.6",
@@ -60,6 +60,7 @@ dependencies = [
     "pydantic>=1.9.0",
     "pygments>=2.6.0",
     "PyOpenGL>=3.1.0",
+    "pywin32 ; platform_system == 'Windows'",
     "PyYAML>=5.1",
     "qtpy>=1.10.0",
     "scikit-image[data]>=0.19.1",
@@ -105,7 +106,7 @@ pyside = [
     "napari[pyside2]"
 ]
 pyqt5 = [
-    "PyQt5>=5.12.3,!=5.15.0",
+    "PyQt5>=5.13.2,!=5.15.0",
 ]
 pyqt = [
     "napari[pyqt5]"
@@ -115,7 +116,7 @@ qt = [
 ]
 all = [
     "napari[pyqt,optional]",
-    "napari-plugin-manager >=0.1.0a1, <0.2.0",
+    "napari-plugin-manager >=0.1.3, <0.2.0",
 ]
 optional = [
     "triangle ; platform_machine != 'arm64'",
@@ -159,7 +160,8 @@ dev = [
     "ruff",
     "check-manifest>=0.42",
     "pre-commit>=2.9.0",
-    "pydantic[dotenv]",
+    "pydantic",
+    "python-dotenv",
     "napari[testing]",
 ]
 build = [
@@ -251,7 +253,7 @@ select = [
     "UP", # pyupgrade
     "I", # isort
     "YTT", #flake8-2020
-    "TCH", # flake8-type-checing
+    "TC", # flake8-type-checing
     "BLE", # flake8-blind-exception
     "B", # flake8-bugbear
     "A", # flake8-builtins
@@ -280,7 +282,7 @@ select = [
     "T20", # flake8-print
 ]
 ignore = [
-    "E501", "TCH001", "TCH002", "TCH003",
+    "E501", "TC001", "TC002", "TC003",
     "A003", # flake8-builtins - we have class attributes violating these rule
     "COM812", # flake8-commas - we don't like adding comma on single line of arguments
     "COM819", # conflicts with ruff-format
@@ -361,6 +363,9 @@ filterwarnings = [
   # https://github.com/pydata/xarray/blame/b1f3fea467f9387ed35c221205a70524f4caa18b/pyproject.toml#L333-L334
   # https://github.com/pydata/xarray/pull/8939
   "ignore:__array__ implementation doesn't accept a copy keyword, so passing copy=False failed.",
+  "ignore:pkg_resources is deprecated",
+  "ignore:Deprecated call to `pkg_resources.declare_namespace",
+  "ignore:Use Group.create_array instead."
 ]
 markers = [
     "examples: Test of examples",
@@ -369,6 +374,9 @@ markers = [
     "disable_qtimer_start: Disable timer start in this Test",
     "disable_qanimation_start: Disable animation start in this Test",
     "enable_console: Don't mock the IPython console (in QtConsole) in this Test",
+    # mark slow tests, so they can be skipped using: pytest -m "not slow"
+    "slow: mark a test as slow",
+    "key_bindings: Test of keybindings",
 ]
 
 [tool.mypy]
@@ -549,7 +557,7 @@ module = [
     "napari._vispy.visuals.surface",
     "napari.layers.shapes._shapes_models.path",
     "napari.layers.shapes._shapes_models.polygon",
-    "napari.layers.shapes._shapes_models._polgyon_base",
+    "napari.layers.shapes._shapes_models._polygon_base",
     "napari.layers.shapes._shapes_models.ellipse",
     "napari.layers.shapes._shapes_models.line",
     "napari.layers.shapes._shapes_models.rectangle",
@@ -607,7 +615,6 @@ module = [
     "napari._vispy.overlays.scale_bar",
     "napari._vispy.overlays.text",
     "napari.layers.labels._labels_key_bindings",
-    "napari.layers.tracks._tracks_key_bindings",
     "napari.layers.utils._slice_input",
     "napari.utils._register",
     "napari.utils.colormaps.categorical_colormap",
@@ -688,7 +695,6 @@ module = [
     "napari.conftest",
     "napari.layers.labels._labels_utils",
     "napari.layers.points._points_mouse_bindings",
-    "napari.layers.tracks._track_utils",
     "napari.utils.colormaps.colormap",
     "napari.utils.notifications",
 ]
@@ -762,7 +768,6 @@ exclude_lines = [
 ]
 
 [tool.coverage.run]
-parallel = true
 omit = [
     "*/_vendor/*",
     "*/_version.py",
diff --git a/resources/constraints/benchmark.txt b/resources/constraints/benchmark.txt
index 0abf6df1a1b..09fda90fb7a 100644
--- a/resources/constraints/benchmark.txt
+++ b/resources/constraints/benchmark.txt
@@ -36,3 +36,5 @@ virtualenv==20.26.3
     # via asv
 zipp==3.19.2
     # via importlib-metadata
+
+flexparser!=0.4
diff --git a/resources/constraints/constraints_py3.10.txt b/resources/constraints/constraints_py3.10.txt
index b5c24f71087..06b05f6d619 100644
--- a/resources/constraints/constraints_py3.10.txt
+++ b/resources/constraints/constraints_py3.10.txt
@@ -1,62 +1,61 @@
 # This file was autogenerated by uv via the following command:
-#    uv pip compile --python-version 3.10 --output-file napari_repo/resources/constraints/constraints_py3.10.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt --extra pyqt5 --extra pyqt6_experimental --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
-alabaster==0.7.16
+#    uv pip compile --python-version 3.10 --output-file napari_repo/resources/constraints/constraints_py3.10.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt --extra pyqt5 --extra pyqt6 --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
+alabaster==1.0.0
     # via sphinx
 annotated-types==0.7.0
     # via pydantic
-app-model==0.2.7
+app-model==0.3.0
     # via napari (napari_repo/pyproject.toml)
 appdirs==1.4.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
-    #   pint
 asciitree==0.3.3
     # via zarr
 asttokens==2.4.1
     # via stack-data
-attrs==23.2.0
+attrs==24.2.0
     # via
     #   hypothesis
     #   jsonschema
     #   referencing
-babel==2.15.0
+babel==2.16.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   sphinx
-build==1.2.1
+build==1.2.2.post1
     # via npe2
 cachey==0.2.1
     # via napari (napari_repo/pyproject.toml)
-certifi==2024.7.4
+certifi==2024.8.30
     # via
     #   napari (napari_repo/pyproject.toml)
     #   requests
-charset-normalizer==3.3.2
+charset-normalizer==3.4.0
     # via requests
 click==8.1.7
     # via
     #   dask
     #   typer
-cloudpickle==3.0.0
+cloudpickle==3.1.0
     # via dask
 comm==0.2.2
     # via ipykernel
-contourpy==1.2.1
+contourpy==1.3.1
     # via matplotlib
-coverage==7.5.4
+coverage==7.6.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
 cycler==0.12.1
     # via matplotlib
-dask==2024.6.2
+dask==2024.11.2
     # via napari (napari_repo/pyproject.toml)
-debugpy==1.8.1
+debugpy==1.8.8
     # via ipykernel
 decorator==5.1.1
     # via ipython
-distlib==0.3.8
+distlib==0.3.9
     # via virtualenv
 docstring-parser==0.16
     # via
@@ -64,29 +63,29 @@ docstring-parser==0.16
     #   magicgui
 docutils==0.21.2
     # via sphinx
-exceptiongroup==1.2.1
+exceptiongroup==1.2.2
     # via
     #   hypothesis
     #   ipython
     #   pytest
-executing==2.0.1
+executing==2.1.0
     # via stack-data
 fasteners==0.19
     # via zarr
-filelock==3.15.4
+filelock==3.16.1
     # via
     #   torch
     #   triton
     #   virtualenv
 flexcache==0.3
     # via pint
-flexparser==0.3.1
+flexparser==0.4
     # via pint
-fonttools==4.53.0
+fonttools==4.55.0
     # via matplotlib
-freetype-py==2.4.0
+freetype-py==2.5.1
     # via vispy
-fsspec==2024.6.0
+fsspec==2024.10.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
@@ -95,18 +94,18 @@ heapdict==1.0.1
     # via cachey
 hsluv==5.0.4
     # via vispy
-hypothesis==6.103.4
+hypothesis==6.119.3
     # via napari (napari_repo/pyproject.toml)
-idna==3.7
+idna==3.10
     # via requests
-imageio==2.34.1
+imageio==2.36.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-svg
     #   scikit-image
 imagesize==1.4.1
     # via sphinx
-importlib-metadata==7.2.1
+importlib-metadata==8.5.0
     # via
     #   build
     #   dask
@@ -114,26 +113,27 @@ in-n-out==0.2.1
     # via app-model
 iniconfig==2.0.0
     # via pytest
-ipykernel==6.29.4
+ipykernel==6.29.5
     # via
+    #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari-console
     #   qtconsole
-ipython==8.25.0
+ipython==8.29.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
     #   napari-console
-jedi==0.19.1
+jedi==0.19.2
     # via ipython
 jinja2==3.1.4
     # via
     #   sphinx
     #   torch
-jsonschema==4.22.0
+jsonschema==4.23.0
     # via napari (napari_repo/pyproject.toml)
-jsonschema-specifications==2023.12.1
+jsonschema-specifications==2024.10.1
     # via jsonschema
-jupyter-client==8.6.2
+jupyter-client==8.6.3
     # via
     #   ipykernel
     #   qtconsole
@@ -142,7 +142,7 @@ jupyter-core==5.7.2
     #   ipykernel
     #   jupyter-client
     #   qtconsole
-kiwisolver==1.4.5
+kiwisolver==1.4.7
     # via
     #   matplotlib
     #   vispy
@@ -154,19 +154,19 @@ llvmlite==0.43.0
     # via numba
 locket==1.0.0
     # via partd
-lxml==5.2.2
+lxml==5.3.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   lxml-html-clean
-lxml-html-clean==0.1.1
+lxml-html-clean==0.4.1
     # via lxml
-magicgui==0.8.3
+magicgui==0.9.1
     # via napari (napari_repo/pyproject.toml)
 markdown-it-py==3.0.0
     # via rich
-markupsafe==2.1.5
+markupsafe==3.0.2
     # via jinja2
-matplotlib==3.9.0
+matplotlib==3.9.2
     # via napari (napari_repo/pyproject.toml)
 matplotlib-inline==0.1.7
     # via
@@ -174,37 +174,35 @@ matplotlib-inline==0.1.7
     #   ipython
 mdurl==0.1.2
     # via markdown-it-py
-ml-dtypes==0.4.0
+ml-dtypes==0.5.0
     # via tensorstore
 mouseinfo==0.1.3
     # via pyautogui
 mpmath==1.3.0
     # via sympy
-napari-console==0.0.9
+napari-console==0.1.1
     # via napari (napari_repo/pyproject.toml)
 napari-plugin-engine==0.2.0
-    # via
-    #   napari (napari_repo/pyproject.toml)
-    #   napari-svg
-napari-plugin-manager==0.1.0a2
     # via napari (napari_repo/pyproject.toml)
-napari-svg==0.1.10
+napari-plugin-manager==0.1.3
+    # via napari (napari_repo/pyproject.toml)
+napari-svg==0.2.0
     # via napari (napari_repo/pyproject.toml)
 nest-asyncio==1.6.0
     # via ipykernel
-networkx==3.3
+networkx==3.4.2
     # via
     #   scikit-image
     #   torch
-npe2==0.7.6
+npe2==0.7.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-plugin-manager
 numba==0.60.0
     # via napari (napari_repo/pyproject.toml)
-numcodecs==0.12.1
+numcodecs==0.13.1
     # via zarr
-numpy==2.0.0
+numpy==2.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   contourpy
@@ -224,40 +222,41 @@ numpy==2.0.0
     #   vispy
     #   xarray
     #   zarr
-numpydoc==1.7.0
+numpydoc==1.8.0
     # via napari (napari_repo/pyproject.toml)
-nvidia-cublas-cu12==12.1.3.1
+nvidia-cublas-cu12==12.4.5.8
     # via
     #   nvidia-cudnn-cu12
     #   nvidia-cusolver-cu12
     #   torch
-nvidia-cuda-cupti-cu12==12.1.105
+nvidia-cuda-cupti-cu12==12.4.127
     # via torch
-nvidia-cuda-nvrtc-cu12==12.1.105
+nvidia-cuda-nvrtc-cu12==12.4.127
     # via torch
-nvidia-cuda-runtime-cu12==12.1.105
+nvidia-cuda-runtime-cu12==12.4.127
     # via torch
-nvidia-cudnn-cu12==8.9.2.26
+nvidia-cudnn-cu12==9.1.0.70
     # via torch
-nvidia-cufft-cu12==11.0.2.54
+nvidia-cufft-cu12==11.2.1.3
     # via torch
-nvidia-curand-cu12==10.3.2.106
+nvidia-curand-cu12==10.3.5.147
     # via torch
-nvidia-cusolver-cu12==11.4.5.107
+nvidia-cusolver-cu12==11.6.1.9
     # via torch
-nvidia-cusparse-cu12==12.1.0.106
+nvidia-cusparse-cu12==12.3.1.170
     # via
     #   nvidia-cusolver-cu12
     #   torch
-nvidia-nccl-cu12==2.20.5
+nvidia-nccl-cu12==2.21.5
     # via torch
-nvidia-nvjitlink-cu12==12.5.40
+nvidia-nvjitlink-cu12==12.4.127
     # via
     #   nvidia-cusolver-cu12
     #   nvidia-cusparse-cu12
-nvidia-nvtx-cu12==12.1.105
+    #   torch
+nvidia-nvtx-cu12==12.4.127
     # via torch
-packaging==24.1
+packaging==24.2
     # via
     #   build
     #   dask
@@ -272,7 +271,7 @@ packaging==24.1
     #   sphinx
     #   vispy
     #   xarray
-pandas==2.2.2
+pandas==2.2.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   xarray
@@ -282,20 +281,21 @@ partd==1.4.2
     # via dask
 pexpect==4.9.0
     # via ipython
-pillow==10.3.0
+pillow==11.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   imageio
     #   matplotlib
     #   pyscreeze
     #   scikit-image
-pint==0.24
+pint==0.24.4
     # via napari (napari_repo/pyproject.toml)
-pip==24.1
+pip==24.3.1
     # via napari-plugin-manager
-platformdirs==4.2.2
+platformdirs==4.3.6
     # via
     #   jupyter-core
+    #   pint
     #   pooch
     #   virtualenv
 pluggy==1.5.0
@@ -308,9 +308,9 @@ pooch==1.8.2
     #   scikit-image
 pretend==1.0.9
     # via napari (napari_repo/pyproject.toml)
-prompt-toolkit==3.0.47
+prompt-toolkit==3.0.48
     # via ipython
-psutil==6.0.0
+psutil==6.1.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
@@ -322,13 +322,13 @@ psygnal==0.11.1
     #   npe2
 ptyprocess==0.7.0
     # via pexpect
-pure-eval==0.2.2
+pure-eval==0.2.3
     # via stack-data
 pyautogui==0.9.54
     # via napari (napari_repo/pyproject.toml)
 pyconify==0.1.6
     # via superqt
-pydantic==2.7.4
+pydantic==2.9.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   app-model
@@ -336,7 +336,7 @@ pydantic==2.7.4
     #   pydantic-compat
 pydantic-compat==0.1.2
     # via app-model
-pydantic-core==2.18.4
+pydantic-core==2.23.4
     # via pydantic
 pygetwindow==0.0.9
     # via pyautogui
@@ -354,27 +354,27 @@ pyopengl==3.1.6
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-pyparsing==3.1.2
+pyparsing==3.2.0
     # via matplotlib
 pyperclip==1.9.0
     # via mouseinfo
-pyproject-hooks==1.1.0
+pyproject-hooks==1.2.0
     # via build
-pyqt5==5.15.10
+pyqt5==5.15.11
     # via napari (napari_repo/pyproject.toml)
-pyqt5-qt5==5.15.2
+pyqt5-qt5==5.15.15
     # via pyqt5
-pyqt5-sip==12.13.0
+pyqt5-sip==12.15.0
     # via pyqt5
-pyqt6==6.7.0
+pyqt6==6.7.1
     # via napari (napari_repo/pyproject.toml)
-pyqt6-qt6==6.7.2
+pyqt6-qt6==6.7.3
     # via pyqt6
-pyqt6-sip==13.6.0
+pyqt6-sip==13.8.0
     # via pyqt6
 pyrect==0.2.0
     # via pygetwindow
-pyscreeze==0.1.30
+pyscreeze==1.0.1
     # via pyautogui
 pyside2==5.15.2.1
     # via napari (napari_repo/pyproject.toml)
@@ -388,7 +388,7 @@ pyside6-essentials==6.4.2
     # via
     #   pyside6
     #   pyside6-addons
-pytest==8.2.2
+pytest==8.3.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
@@ -396,7 +396,7 @@ pytest==8.2.2
     #   pytest-metadata
     #   pytest-pretty
     #   pytest-qt
-pytest-cov==5.0.0
+pytest-cov==6.0.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
 pytest-json-report==1.5.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
@@ -417,23 +417,22 @@ python3-xlib==0.15
     #   pyautogui
 pytweening==1.2.0
     # via pyautogui
-pytz==2024.1
+pytz==2024.2
     # via pandas
-pyyaml==6.0.1
+pyyaml==6.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   npe2
-pyzmq==26.0.3
+pyzmq==26.2.0
     # via
     #   ipykernel
     #   jupyter-client
-    #   qtconsole
-qtconsole==5.5.2
+qtconsole==5.6.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-console
-qtpy==2.4.1
+qtpy==2.4.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
@@ -450,19 +449,19 @@ requests==2.32.3
     #   pooch
     #   pyconify
     #   sphinx
-rich==13.7.1
+rich==13.9.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
     #   pytest-pretty
     #   typer
-rpds-py==0.18.1
+rpds-py==0.21.0
     # via
     #   jsonschema
     #   referencing
 scikit-image==0.24.0
     # via napari (napari_repo/pyproject.toml)
-scipy==1.13.1
+scipy==1.14.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
@@ -483,19 +482,19 @@ snowballstemmer==2.2.0
     # via sphinx
 sortedcontainers==2.4.0
     # via hypothesis
-sphinx==7.3.7
+sphinx==8.1.3
     # via numpydoc
-sphinxcontrib-applehelp==1.0.8
+sphinxcontrib-applehelp==2.0.0
     # via sphinx
-sphinxcontrib-devhelp==1.0.6
+sphinxcontrib-devhelp==2.0.0
     # via sphinx
-sphinxcontrib-htmlhelp==2.0.5
+sphinxcontrib-htmlhelp==2.1.0
     # via sphinx
 sphinxcontrib-jsmath==1.0.1
     # via sphinx
-sphinxcontrib-qthelp==1.0.7
+sphinxcontrib-qthelp==2.0.0
     # via sphinx
-sphinxcontrib-serializinghtml==1.1.10
+sphinxcontrib-serializinghtml==2.0.0
     # via sphinx
 stack-data==0.6.3
     # via ipython
@@ -504,19 +503,19 @@ superqt==0.6.7
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
     #   napari-plugin-manager
-sympy==1.12.1
+sympy==1.13.1
     # via torch
 tabulate==0.9.0
     # via numpydoc
-tensorstore==0.1.63
+tensorstore==0.1.68
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-tifffile==2024.6.18
+tifffile==2024.9.20
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
-tomli==2.0.1
+tomli==2.1.0
     # via
     #   build
     #   coverage
@@ -524,20 +523,20 @@ tomli==2.0.1
     #   numpydoc
     #   pytest
     #   sphinx
-tomli-w==1.0.0
+tomli-w==1.1.0
     # via npe2
-toolz==0.12.1
+toolz==1.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   partd
-torch==2.3.1
+torch==2.5.1
     # via napari (napari_repo/pyproject.toml)
 tornado==6.4.1
     # via
     #   ipykernel
     #   jupyter-client
-tqdm==4.66.4
+tqdm==4.67.0
     # via napari (napari_repo/pyproject.toml)
 traitlets==5.14.3
     # via
@@ -550,9 +549,9 @@ traitlets==5.14.3
     #   qtconsole
 triangle==20230923
     # via napari (napari_repo/pyproject.toml)
-triton==2.3.1
+triton==3.1.0
     # via torch
-typer==0.12.3
+typer==0.13.0
     # via npe2
 typing-extensions==4.12.2
     # via
@@ -565,14 +564,15 @@ typing-extensions==4.12.2
     #   pint
     #   pydantic
     #   pydantic-core
+    #   rich
     #   superqt
     #   torch
     #   typer
-tzdata==2024.1
+tzdata==2024.2
     # via pandas
-urllib3==2.2.2
+urllib3==2.2.3
     # via requests
-virtualenv==20.26.3
+virtualenv==20.27.1
     # via napari (napari_repo/pyproject.toml)
 vispy==0.14.3
     # via
@@ -582,9 +582,9 @@ wcwidth==0.2.13
     # via prompt-toolkit
 wrapt==1.16.0
     # via napari (napari_repo/pyproject.toml)
-xarray==2024.6.0
+xarray==2024.10.0
     # via napari (napari_repo/pyproject.toml)
-zarr==2.18.2
+zarr==2.18.3
     # via napari (napari_repo/pyproject.toml)
-zipp==3.19.2
+zipp==3.21.0
     # via importlib-metadata
diff --git a/resources/constraints/constraints_py3.10_docs.txt b/resources/constraints/constraints_py3.10_docs.txt
index 5b82dfb95e1..6bf4cb16392 100644
--- a/resources/constraints/constraints_py3.10_docs.txt
+++ b/resources/constraints/constraints_py3.10_docs.txt
@@ -1,46 +1,45 @@
 # This file was autogenerated by uv via the following command:
-#    uv pip compile --python-version 3.10 --output-file napari_repo/resources/constraints/constraints_py3.10_docs.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt resources/constraints/version_denylist_examples.txt docs/requirements.txt napari_repo/resources/constraints/pydantic_le_2.txt --extra pyqt5 --extra pyqt6_experimental --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
+#    uv pip compile --python-version 3.10 --output-file napari_repo/resources/constraints/constraints_py3.10_docs.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt resources/constraints/version_denylist_examples.txt docs/requirements.txt napari_repo/resources/constraints/pydantic_le_2.txt --extra pyqt5 --extra pyqt6 --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
 accessible-pygments==0.0.5
     # via pydata-sphinx-theme
 alabaster==0.7.16
     # via sphinx
-anyio==4.4.0
+anyio==4.6.2.post1
     # via
     #   starlette
     #   watchfiles
-app-model==0.2.7
+app-model==0.3.0
     # via napari (napari_repo/pyproject.toml)
 appdirs==1.4.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
-    #   pint
 asciitree==0.3.3
     # via zarr
 asttokens==2.4.1
     # via stack-data
-attrs==23.2.0
+attrs==24.2.0
     # via
     #   hypothesis
     #   jsonschema
     #   jupyter-cache
     #   referencing
-babel==2.15.0
+babel==2.16.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pydata-sphinx-theme
     #   sphinx
 beautifulsoup4==4.12.3
     # via pydata-sphinx-theme
-build==1.2.1
+build==1.2.2.post1
     # via npe2
 cachey==0.2.1
     # via napari (napari_repo/pyproject.toml)
-certifi==2024.7.4
+certifi==2024.8.30
     # via
     #   napari (napari_repo/pyproject.toml)
     #   requests
-charset-normalizer==3.3.2
+charset-normalizer==3.4.0
     # via requests
 click==8.1.7
     # via
@@ -49,27 +48,27 @@ click==8.1.7
     #   sphinx-external-toc
     #   typer
     #   uvicorn
-cloudpickle==3.0.0
+cloudpickle==3.1.0
     # via dask
 colorama==0.4.6
     # via sphinx-autobuild
 comm==0.2.2
     # via ipykernel
-contourpy==1.2.1
+contourpy==1.3.1
     # via matplotlib
-coverage==7.5.4
+coverage==7.6.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
 cycler==0.12.1
     # via matplotlib
-dask==2024.6.2
+dask==2024.8.1
     # via napari (napari_repo/pyproject.toml)
-debugpy==1.8.1
+debugpy==1.8.8
     # via ipykernel
 decorator==5.1.1
     # via ipython
-distlib==0.3.8
+distlib==0.3.9
     # via virtualenv
 docstring-parser==0.16
     # via
@@ -81,37 +80,37 @@ docutils==0.21.2
     #   pydata-sphinx-theme
     #   sphinx
     #   sphinx-tabs
-exceptiongroup==1.2.1
+exceptiongroup==1.2.2
     # via
     #   anyio
     #   hypothesis
     #   ipython
     #   pytest
-executing==2.0.1
+executing==2.1.0
     # via stack-data
 fasteners==0.19
     # via zarr
 fastjsonschema==2.20.0
     # via nbformat
-filelock==3.15.4
+filelock==3.16.1
     # via
     #   torch
     #   triton
     #   virtualenv
 flexcache==0.3
     # via pint
-flexparser==0.3.1
+flexparser==0.4
     # via pint
-fonttools==4.53.0
+fonttools==4.55.0
     # via matplotlib
-freetype-py==2.4.0
+freetype-py==2.5.1
     # via vispy
-fsspec==2024.6.0
+fsspec==2024.10.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   torch
-greenlet==3.0.3
+greenlet==3.1.1
     # via sqlalchemy
 h11==0.14.0
     # via uvicorn
@@ -119,13 +118,13 @@ heapdict==1.0.1
     # via cachey
 hsluv==5.0.4
     # via vispy
-hypothesis==6.103.4
+hypothesis==6.119.3
     # via napari (napari_repo/pyproject.toml)
-idna==3.7
+idna==3.10
     # via
     #   anyio
     #   requests
-imageio==2.34.1
+imageio==2.36.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-svg
@@ -134,28 +133,31 @@ imageio-ffmpeg==0.5.1
     # via -r docs/requirements.txt
 imagesize==1.4.1
     # via sphinx
-importlib-metadata==7.2.1
+importlib-metadata==8.5.0
     # via
     #   build
     #   dask
     #   jupyter-cache
     #   myst-nb
+importlib-resources==6.4.5
+    # via nibabel
 in-n-out==0.2.1
     # via app-model
 iniconfig==2.0.0
     # via pytest
-ipykernel==6.29.4
+ipykernel==6.29.5
     # via
+    #   -r napari_repo/resources/constraints/version_denylist.txt
     #   myst-nb
     #   napari-console
     #   qtconsole
-ipython==8.25.0
+ipython==8.29.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
     #   myst-nb
     #   napari-console
-jedi==0.19.1
+jedi==0.19.2
     # via ipython
 jinja2==3.1.4
     # via
@@ -166,15 +168,15 @@ joblib==1.4.2
     # via
     #   nilearn
     #   scikit-learn
-jsonschema==4.22.0
+jsonschema==4.23.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   nbformat
-jsonschema-specifications==2023.12.1
+jsonschema-specifications==2024.10.1
     # via jsonschema
-jupyter-cache==1.0.0
+jupyter-cache==1.0.1
     # via myst-nb
-jupyter-client==8.6.2
+jupyter-client==8.6.3
     # via
     #   ipykernel
     #   nbclient
@@ -186,7 +188,7 @@ jupyter-core==5.7.2
     #   nbclient
     #   nbformat
     #   qtconsole
-kiwisolver==1.4.5
+kiwisolver==1.4.7
     # via
     #   matplotlib
     #   vispy
@@ -198,25 +200,25 @@ llvmlite==0.43.0
     # via numba
 locket==1.0.0
     # via partd
-lxml==5.2.2
+lxml==5.3.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   lxml-html-clean
     #   nilearn
-lxml-html-clean==0.1.1
+lxml-html-clean==0.4.1
     # via
     #   -r docs/requirements.txt
     #   lxml
-magicgui==0.8.3
+magicgui==0.9.1
     # via napari (napari_repo/pyproject.toml)
 markdown-it-py==3.0.0
     # via
     #   mdit-py-plugins
     #   myst-parser
     #   rich
-markupsafe==2.1.5
+markupsafe==3.0.2
     # via jinja2
-matplotlib==3.9.0
+matplotlib==3.9.2
     # via
     #   -r docs/requirements.txt
     #   napari (napari_repo/pyproject.toml)
@@ -224,31 +226,29 @@ matplotlib-inline==0.1.7
     # via
     #   ipykernel
     #   ipython
-mdit-py-plugins==0.4.1
+mdit-py-plugins==0.4.2
     # via myst-parser
 mdurl==0.1.2
     # via markdown-it-py
-ml-dtypes==0.4.0
+ml-dtypes==0.5.0
     # via tensorstore
 mouseinfo==0.1.3
     # via pyautogui
 mpmath==1.3.0
     # via sympy
-myst-nb==1.1.0
+myst-nb==1.1.2
     # via -r docs/requirements.txt
-myst-parser==3.0.1
+myst-parser==4.0.0
     # via myst-nb
-napari-console==0.0.9
+napari-console==0.1.1
     # via napari (napari_repo/pyproject.toml)
 napari-plugin-engine==0.2.0
-    # via
-    #   napari (napari_repo/pyproject.toml)
-    #   napari-svg
-napari-plugin-manager==0.1.0a2
     # via napari (napari_repo/pyproject.toml)
-napari-sphinx-theme==0.4.0
+napari-plugin-manager==0.1.3
+    # via napari (napari_repo/pyproject.toml)
+napari-sphinx-theme==0.5.0
     # via -r docs/requirements.txt
-napari-svg==0.1.10
+napari-svg==0.2.0
     # via napari (napari_repo/pyproject.toml)
 nbclient==0.10.0
     # via
@@ -261,21 +261,21 @@ nbformat==5.10.4
     #   nbclient
 nest-asyncio==1.6.0
     # via ipykernel
-networkx==3.3
+networkx==3.4.2
     # via
     #   scikit-image
     #   torch
-nibabel==5.2.1
+nibabel==5.3.2
     # via nilearn
 nilearn==0.10.4
     # via -r resources/constraints/version_denylist_examples.txt
-npe2==0.7.6
+npe2==0.7.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-plugin-manager
 numba==0.60.0
     # via napari (napari_repo/pyproject.toml)
-numcodecs==0.12.1
+numcodecs==0.13.1
     # via zarr
 numpy==1.23.5
     # via
@@ -301,40 +301,41 @@ numpy==1.23.5
     #   vispy
     #   xarray
     #   zarr
-numpydoc==1.7.0
+numpydoc==1.8.0
     # via napari (napari_repo/pyproject.toml)
-nvidia-cublas-cu12==12.1.3.1
+nvidia-cublas-cu12==12.4.5.8
     # via
     #   nvidia-cudnn-cu12
     #   nvidia-cusolver-cu12
     #   torch
-nvidia-cuda-cupti-cu12==12.1.105
+nvidia-cuda-cupti-cu12==12.4.127
     # via torch
-nvidia-cuda-nvrtc-cu12==12.1.105
+nvidia-cuda-nvrtc-cu12==12.4.127
     # via torch
-nvidia-cuda-runtime-cu12==12.1.105
+nvidia-cuda-runtime-cu12==12.4.127
     # via torch
-nvidia-cudnn-cu12==8.9.2.26
+nvidia-cudnn-cu12==9.1.0.70
     # via torch
-nvidia-cufft-cu12==11.0.2.54
+nvidia-cufft-cu12==11.2.1.3
     # via torch
-nvidia-curand-cu12==10.3.2.106
+nvidia-curand-cu12==10.3.5.147
     # via torch
-nvidia-cusolver-cu12==11.4.5.107
+nvidia-cusolver-cu12==11.6.1.9
     # via torch
-nvidia-cusparse-cu12==12.1.0.106
+nvidia-cusparse-cu12==12.3.1.170
     # via
     #   nvidia-cusolver-cu12
     #   torch
-nvidia-nccl-cu12==2.20.5
+nvidia-nccl-cu12==2.21.5
     # via torch
-nvidia-nvjitlink-cu12==12.5.40
+nvidia-nvjitlink-cu12==12.4.127
     # via
     #   nvidia-cusolver-cu12
     #   nvidia-cusparse-cu12
-nvidia-nvtx-cu12==12.1.105
+    #   torch
+nvidia-nvtx-cu12==12.4.127
     # via torch
-packaging==24.1
+packaging==24.2
     # via
     #   -r resources/constraints/version_denylist_examples.txt
     #   build
@@ -346,7 +347,6 @@ packaging==24.1
     #   nibabel
     #   nilearn
     #   pooch
-    #   pydata-sphinx-theme
     #   pytest
     #   qtconsole
     #   qtpy
@@ -354,7 +354,7 @@ packaging==24.1
     #   sphinx
     #   vispy
     #   xarray
-pandas==2.2.2
+pandas==2.2.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   nilearn
@@ -365,7 +365,7 @@ partd==1.4.2
     # via dask
 pexpect==4.9.0
     # via ipython
-pillow==10.3.0
+pillow==11.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   imageio
@@ -373,13 +373,14 @@ pillow==10.3.0
     #   pyscreeze
     #   scikit-image
     #   sphinx-gallery
-pint==0.24
+pint==0.24.4
     # via napari (napari_repo/pyproject.toml)
-pip==24.1
+pip==24.3.1
     # via napari-plugin-manager
-platformdirs==4.2.2
+platformdirs==4.3.6
     # via
     #   jupyter-core
+    #   pint
     #   pooch
     #   virtualenv
 pluggy==1.5.0
@@ -392,9 +393,9 @@ pooch==1.8.2
     #   scikit-image
 pretend==1.0.9
     # via napari (napari_repo/pyproject.toml)
-prompt-toolkit==3.0.47
+prompt-toolkit==3.0.48
     # via ipython
-psutil==6.0.0
+psutil==6.1.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
@@ -406,13 +407,13 @@ psygnal==0.11.1
     #   npe2
 ptyprocess==0.7.0
     # via pexpect
-pure-eval==0.2.2
+pure-eval==0.2.3
     # via stack-data
 pyautogui==0.9.54
     # via napari (napari_repo/pyproject.toml)
 pyconify==0.1.6
     # via superqt
-pydantic==1.10.17
+pydantic==1.10.19
     # via
     #   -r napari_repo/resources/constraints/pydantic_le_2.txt
     #   napari (napari_repo/pyproject.toml)
@@ -421,7 +422,7 @@ pydantic==1.10.17
     #   pydantic-compat
 pydantic-compat==0.1.2
     # via app-model
-pydata-sphinx-theme==0.15.3
+pydata-sphinx-theme==0.16.0
     # via napari-sphinx-theme
 pygetwindow==0.0.9
     # via pyautogui
@@ -442,27 +443,27 @@ pyopengl==3.1.6
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-pyparsing==3.1.2
+pyparsing==3.2.0
     # via matplotlib
 pyperclip==1.9.0
     # via mouseinfo
-pyproject-hooks==1.1.0
+pyproject-hooks==1.2.0
     # via build
-pyqt5==5.15.10
+pyqt5==5.15.11
     # via napari (napari_repo/pyproject.toml)
-pyqt5-qt5==5.15.2
+pyqt5-qt5==5.15.15
     # via pyqt5
-pyqt5-sip==12.13.0
+pyqt5-sip==12.15.0
     # via pyqt5
-pyqt6==6.7.0
+pyqt6==6.7.1
     # via napari (napari_repo/pyproject.toml)
-pyqt6-qt6==6.7.2
+pyqt6-qt6==6.7.3
     # via pyqt6
-pyqt6-sip==13.6.0
+pyqt6-sip==13.8.0
     # via pyqt6
 pyrect==0.2.0
     # via pygetwindow
-pyscreeze==0.1.30
+pyscreeze==1.0.1
     # via pyautogui
 pyside2==5.15.2.1
     # via napari (napari_repo/pyproject.toml)
@@ -476,15 +477,16 @@ pyside6-essentials==6.4.2
     # via
     #   pyside6
     #   pyside6-addons
-pytest==8.2.2
+pytest==8.3.3
     # via
+    #   -r docs/requirements.txt
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
     #   pytest-json-report
     #   pytest-metadata
     #   pytest-pretty
     #   pytest-qt
-pytest-cov==5.0.0
+pytest-cov==6.0.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
 pytest-json-report==1.5.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
@@ -505,9 +507,9 @@ python3-xlib==0.15
     #   pyautogui
 pytweening==1.2.0
     # via pyautogui
-pytz==2024.1
+pytz==2024.2
     # via pandas
-pyyaml==6.0.1
+pyyaml==6.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
@@ -516,16 +518,15 @@ pyyaml==6.0.1
     #   myst-parser
     #   npe2
     #   sphinx-external-toc
-pyzmq==26.0.3
+pyzmq==26.2.0
     # via
     #   ipykernel
     #   jupyter-client
-    #   qtconsole
-qtconsole==5.5.2
+qtconsole==5.6.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-console
-qtpy==2.4.1
+qtpy==2.4.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
@@ -543,27 +544,27 @@ requests==2.32.3
     #   pooch
     #   pyconify
     #   sphinx
-rich==13.7.1
+rich==13.9.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
     #   pytest-pretty
     #   typer
-rpds-py==0.18.1
+rpds-py==0.21.0
     # via
     #   jsonschema
     #   referencing
 scikit-image==0.24.0
     # via napari (napari_repo/pyproject.toml)
-scikit-learn==1.5.0
+scikit-learn==1.5.2
     # via nilearn
-scipy==1.13.1
+scipy==1.14.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   nilearn
     #   scikit-image
     #   scikit-learn
-setuptools==70.1.0
+setuptools==75.5.0
     # via imageio-ffmpeg
 shellingham==1.5.4
     # via typer
@@ -584,10 +585,11 @@ snowballstemmer==2.2.0
     # via sphinx
 sortedcontainers==2.4.0
     # via hypothesis
-soupsieve==2.5
+soupsieve==2.6
     # via beautifulsoup4
-sphinx==7.3.7
+sphinx==7.4.7
     # via
+    #   -r docs/requirements.txt
     #   myst-nb
     #   myst-parser
     #   numpydoc
@@ -601,64 +603,64 @@ sphinx==7.3.7
     #   sphinx-gallery
     #   sphinx-tabs
     #   sphinx-tags
-sphinx-autobuild==2024.4.16
+sphinx-autobuild==2024.10.3
     # via -r docs/requirements.txt
 sphinx-autodoc-typehints==1.12.0
     # via -r docs/requirements.txt
 sphinx-copybutton==0.5.2
     # via -r docs/requirements.txt
-sphinx-design==0.6.0
+sphinx-design==0.6.1
     # via -r docs/requirements.txt
 sphinx-external-toc==1.0.1
     # via -r docs/requirements.txt
 sphinx-favicon==1.0.1
     # via -r docs/requirements.txt
-sphinx-gallery==0.16.0
+sphinx-gallery==0.18.0
     # via -r docs/requirements.txt
-sphinx-tabs==3.4.5
+sphinx-tabs==3.4.7
     # via -r docs/requirements.txt
-sphinx-tags==0.3.1
+sphinx-tags==0.4
     # via -r docs/requirements.txt
-sphinxcontrib-applehelp==1.0.8
+sphinxcontrib-applehelp==2.0.0
     # via sphinx
-sphinxcontrib-devhelp==1.0.6
+sphinxcontrib-devhelp==2.0.0
     # via sphinx
-sphinxcontrib-htmlhelp==2.0.5
+sphinxcontrib-htmlhelp==2.1.0
     # via sphinx
 sphinxcontrib-jsmath==1.0.1
     # via sphinx
-sphinxcontrib-qthelp==1.0.7
+sphinxcontrib-qthelp==2.0.0
     # via sphinx
-sphinxcontrib-serializinghtml==1.1.10
+sphinxcontrib-serializinghtml==2.0.0
     # via sphinx
-sqlalchemy==2.0.31
+sqlalchemy==2.0.36
     # via jupyter-cache
 stack-data==0.6.3
     # via ipython
-starlette==0.37.2
+starlette==0.41.2
     # via sphinx-autobuild
 superqt==0.6.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
     #   napari-plugin-manager
-sympy==1.12.1
+sympy==1.13.1
     # via torch
 tabulate==0.9.0
     # via
     #   jupyter-cache
     #   numpydoc
-tensorstore==0.1.63
+tensorstore==0.1.68
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
 threadpoolctl==3.5.0
     # via scikit-learn
-tifffile==2024.6.18
+tifffile==2024.9.20
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
-tomli==2.0.1
+tomli==2.1.0
     # via
     #   build
     #   coverage
@@ -666,20 +668,20 @@ tomli==2.0.1
     #   numpydoc
     #   pytest
     #   sphinx
-tomli-w==1.0.0
+tomli-w==1.1.0
     # via npe2
-toolz==0.12.1
+toolz==1.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   partd
-torch==2.3.1
+torch==2.5.1
     # via napari (napari_repo/pyproject.toml)
 tornado==6.4.1
     # via
     #   ipykernel
     #   jupyter-client
-tqdm==4.66.4
+tqdm==4.67.0
     # via napari (napari_repo/pyproject.toml)
 traitlets==5.14.3
     # via
@@ -694,9 +696,9 @@ traitlets==5.14.3
     #   qtconsole
 triangle==20230923
     # via napari (napari_repo/pyproject.toml)
-triton==2.3.1
+triton==3.1.0
     # via torch
-typer==0.12.3
+typer==0.13.0
     # via npe2
 typing-extensions==4.12.2
     # via
@@ -708,37 +710,39 @@ typing-extensions==4.12.2
     #   ipython
     #   magicgui
     #   myst-nb
+    #   nibabel
     #   pint
     #   pydantic
     #   pydata-sphinx-theme
+    #   rich
     #   sqlalchemy
     #   superqt
     #   torch
     #   typer
     #   uvicorn
-tzdata==2024.1
+tzdata==2024.2
     # via pandas
-urllib3==2.2.2
+urllib3==2.2.3
     # via requests
-uvicorn==0.30.1
+uvicorn==0.32.0
     # via sphinx-autobuild
-virtualenv==20.26.3
+virtualenv==20.27.1
     # via napari (napari_repo/pyproject.toml)
 vispy==0.14.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-svg
-watchfiles==0.22.0
+watchfiles==0.24.0
     # via sphinx-autobuild
 wcwidth==0.2.13
     # via prompt-toolkit
-websockets==12.0
+websockets==14.1
     # via sphinx-autobuild
 wrapt==1.16.0
     # via napari (napari_repo/pyproject.toml)
-xarray==2024.6.0
+xarray==2024.7.0
     # via napari (napari_repo/pyproject.toml)
 zarr==2.18.2
     # via napari (napari_repo/pyproject.toml)
-zipp==3.19.2
+zipp==3.21.0
     # via importlib-metadata
diff --git a/resources/constraints/constraints_py3.10_pydantic_1.txt b/resources/constraints/constraints_py3.10_pydantic_1.txt
index f1adf961f98..9b84baf0261 100644
--- a/resources/constraints/constraints_py3.10_pydantic_1.txt
+++ b/resources/constraints/constraints_py3.10_pydantic_1.txt
@@ -1,60 +1,59 @@
 # This file was autogenerated by uv via the following command:
-#    uv pip compile --python-version 3.10 --output-file napari_repo/resources/constraints/constraints_py3.10_pydantic_1.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt napari_repo/resources/constraints/pydantic_le_2.txt --extra pyqt5 --extra pyqt6_experimental --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
-alabaster==0.7.16
+#    uv pip compile --python-version 3.10 --output-file napari_repo/resources/constraints/constraints_py3.10_pydantic_1.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt napari_repo/resources/constraints/pydantic_le_2.txt --extra pyqt5 --extra pyqt6 --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
+alabaster==1.0.0
     # via sphinx
-app-model==0.2.7
+app-model==0.3.0
     # via napari (napari_repo/pyproject.toml)
 appdirs==1.4.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
-    #   pint
 asciitree==0.3.3
     # via zarr
 asttokens==2.4.1
     # via stack-data
-attrs==23.2.0
+attrs==24.2.0
     # via
     #   hypothesis
     #   jsonschema
     #   referencing
-babel==2.15.0
+babel==2.16.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   sphinx
-build==1.2.1
+build==1.2.2.post1
     # via npe2
 cachey==0.2.1
     # via napari (napari_repo/pyproject.toml)
-certifi==2024.7.4
+certifi==2024.8.30
     # via
     #   napari (napari_repo/pyproject.toml)
     #   requests
-charset-normalizer==3.3.2
+charset-normalizer==3.4.0
     # via requests
 click==8.1.7
     # via
     #   dask
     #   typer
-cloudpickle==3.0.0
+cloudpickle==3.1.0
     # via dask
 comm==0.2.2
     # via ipykernel
-contourpy==1.2.1
+contourpy==1.3.1
     # via matplotlib
-coverage==7.5.4
+coverage==7.6.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
 cycler==0.12.1
     # via matplotlib
-dask==2024.6.2
+dask==2024.11.2
     # via napari (napari_repo/pyproject.toml)
-debugpy==1.8.1
+debugpy==1.8.8
     # via ipykernel
 decorator==5.1.1
     # via ipython
-distlib==0.3.8
+distlib==0.3.9
     # via virtualenv
 docstring-parser==0.16
     # via
@@ -62,29 +61,29 @@ docstring-parser==0.16
     #   magicgui
 docutils==0.21.2
     # via sphinx
-exceptiongroup==1.2.1
+exceptiongroup==1.2.2
     # via
     #   hypothesis
     #   ipython
     #   pytest
-executing==2.0.1
+executing==2.1.0
     # via stack-data
 fasteners==0.19
     # via zarr
-filelock==3.15.4
+filelock==3.16.1
     # via
     #   torch
     #   triton
     #   virtualenv
 flexcache==0.3
     # via pint
-flexparser==0.3.1
+flexparser==0.4
     # via pint
-fonttools==4.53.0
+fonttools==4.55.0
     # via matplotlib
-freetype-py==2.4.0
+freetype-py==2.5.1
     # via vispy
-fsspec==2024.6.0
+fsspec==2024.10.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
@@ -93,18 +92,18 @@ heapdict==1.0.1
     # via cachey
 hsluv==5.0.4
     # via vispy
-hypothesis==6.103.4
+hypothesis==6.119.3
     # via napari (napari_repo/pyproject.toml)
-idna==3.7
+idna==3.10
     # via requests
-imageio==2.34.1
+imageio==2.36.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-svg
     #   scikit-image
 imagesize==1.4.1
     # via sphinx
-importlib-metadata==7.2.1
+importlib-metadata==8.5.0
     # via
     #   build
     #   dask
@@ -112,26 +111,27 @@ in-n-out==0.2.1
     # via app-model
 iniconfig==2.0.0
     # via pytest
-ipykernel==6.29.4
+ipykernel==6.29.5
     # via
+    #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari-console
     #   qtconsole
-ipython==8.25.0
+ipython==8.29.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
     #   napari-console
-jedi==0.19.1
+jedi==0.19.2
     # via ipython
 jinja2==3.1.4
     # via
     #   sphinx
     #   torch
-jsonschema==4.22.0
+jsonschema==4.23.0
     # via napari (napari_repo/pyproject.toml)
-jsonschema-specifications==2023.12.1
+jsonschema-specifications==2024.10.1
     # via jsonschema
-jupyter-client==8.6.2
+jupyter-client==8.6.3
     # via
     #   ipykernel
     #   qtconsole
@@ -140,7 +140,7 @@ jupyter-core==5.7.2
     #   ipykernel
     #   jupyter-client
     #   qtconsole
-kiwisolver==1.4.5
+kiwisolver==1.4.7
     # via
     #   matplotlib
     #   vispy
@@ -152,19 +152,19 @@ llvmlite==0.43.0
     # via numba
 locket==1.0.0
     # via partd
-lxml==5.2.2
+lxml==5.3.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   lxml-html-clean
-lxml-html-clean==0.1.1
+lxml-html-clean==0.4.1
     # via lxml
-magicgui==0.8.3
+magicgui==0.9.1
     # via napari (napari_repo/pyproject.toml)
 markdown-it-py==3.0.0
     # via rich
-markupsafe==2.1.5
+markupsafe==3.0.2
     # via jinja2
-matplotlib==3.9.0
+matplotlib==3.9.2
     # via napari (napari_repo/pyproject.toml)
 matplotlib-inline==0.1.7
     # via
@@ -172,37 +172,35 @@ matplotlib-inline==0.1.7
     #   ipython
 mdurl==0.1.2
     # via markdown-it-py
-ml-dtypes==0.4.0
+ml-dtypes==0.5.0
     # via tensorstore
 mouseinfo==0.1.3
     # via pyautogui
 mpmath==1.3.0
     # via sympy
-napari-console==0.0.9
+napari-console==0.1.1
     # via napari (napari_repo/pyproject.toml)
 napari-plugin-engine==0.2.0
-    # via
-    #   napari (napari_repo/pyproject.toml)
-    #   napari-svg
-napari-plugin-manager==0.1.0a2
     # via napari (napari_repo/pyproject.toml)
-napari-svg==0.1.10
+napari-plugin-manager==0.1.3
+    # via napari (napari_repo/pyproject.toml)
+napari-svg==0.2.0
     # via napari (napari_repo/pyproject.toml)
 nest-asyncio==1.6.0
     # via ipykernel
-networkx==3.3
+networkx==3.4.2
     # via
     #   scikit-image
     #   torch
-npe2==0.7.6
+npe2==0.7.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-plugin-manager
 numba==0.60.0
     # via napari (napari_repo/pyproject.toml)
-numcodecs==0.12.1
+numcodecs==0.13.1
     # via zarr
-numpy==2.0.0
+numpy==2.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   contourpy
@@ -222,40 +220,41 @@ numpy==2.0.0
     #   vispy
     #   xarray
     #   zarr
-numpydoc==1.7.0
+numpydoc==1.8.0
     # via napari (napari_repo/pyproject.toml)
-nvidia-cublas-cu12==12.1.3.1
+nvidia-cublas-cu12==12.4.5.8
     # via
     #   nvidia-cudnn-cu12
     #   nvidia-cusolver-cu12
     #   torch
-nvidia-cuda-cupti-cu12==12.1.105
+nvidia-cuda-cupti-cu12==12.4.127
     # via torch
-nvidia-cuda-nvrtc-cu12==12.1.105
+nvidia-cuda-nvrtc-cu12==12.4.127
     # via torch
-nvidia-cuda-runtime-cu12==12.1.105
+nvidia-cuda-runtime-cu12==12.4.127
     # via torch
-nvidia-cudnn-cu12==8.9.2.26
+nvidia-cudnn-cu12==9.1.0.70
     # via torch
-nvidia-cufft-cu12==11.0.2.54
+nvidia-cufft-cu12==11.2.1.3
     # via torch
-nvidia-curand-cu12==10.3.2.106
+nvidia-curand-cu12==10.3.5.147
     # via torch
-nvidia-cusolver-cu12==11.4.5.107
+nvidia-cusolver-cu12==11.6.1.9
     # via torch
-nvidia-cusparse-cu12==12.1.0.106
+nvidia-cusparse-cu12==12.3.1.170
     # via
     #   nvidia-cusolver-cu12
     #   torch
-nvidia-nccl-cu12==2.20.5
+nvidia-nccl-cu12==2.21.5
     # via torch
-nvidia-nvjitlink-cu12==12.5.40
+nvidia-nvjitlink-cu12==12.4.127
     # via
     #   nvidia-cusolver-cu12
     #   nvidia-cusparse-cu12
-nvidia-nvtx-cu12==12.1.105
+    #   torch
+nvidia-nvtx-cu12==12.4.127
     # via torch
-packaging==24.1
+packaging==24.2
     # via
     #   build
     #   dask
@@ -270,7 +269,7 @@ packaging==24.1
     #   sphinx
     #   vispy
     #   xarray
-pandas==2.2.2
+pandas==2.2.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   xarray
@@ -280,20 +279,21 @@ partd==1.4.2
     # via dask
 pexpect==4.9.0
     # via ipython
-pillow==10.3.0
+pillow==11.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   imageio
     #   matplotlib
     #   pyscreeze
     #   scikit-image
-pint==0.24
+pint==0.24.4
     # via napari (napari_repo/pyproject.toml)
-pip==24.1
+pip==24.3.1
     # via napari-plugin-manager
-platformdirs==4.2.2
+platformdirs==4.3.6
     # via
     #   jupyter-core
+    #   pint
     #   pooch
     #   virtualenv
 pluggy==1.5.0
@@ -306,9 +306,9 @@ pooch==1.8.2
     #   scikit-image
 pretend==1.0.9
     # via napari (napari_repo/pyproject.toml)
-prompt-toolkit==3.0.47
+prompt-toolkit==3.0.48
     # via ipython
-psutil==6.0.0
+psutil==6.1.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
@@ -320,13 +320,13 @@ psygnal==0.11.1
     #   npe2
 ptyprocess==0.7.0
     # via pexpect
-pure-eval==0.2.2
+pure-eval==0.2.3
     # via stack-data
 pyautogui==0.9.54
     # via napari (napari_repo/pyproject.toml)
 pyconify==0.1.6
     # via superqt
-pydantic==1.10.17
+pydantic==1.10.19
     # via
     #   -r napari_repo/resources/constraints/pydantic_le_2.txt
     #   napari (napari_repo/pyproject.toml)
@@ -351,27 +351,27 @@ pyopengl==3.1.6
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-pyparsing==3.1.2
+pyparsing==3.2.0
     # via matplotlib
 pyperclip==1.9.0
     # via mouseinfo
-pyproject-hooks==1.1.0
+pyproject-hooks==1.2.0
     # via build
-pyqt5==5.15.10
+pyqt5==5.15.11
     # via napari (napari_repo/pyproject.toml)
-pyqt5-qt5==5.15.2
+pyqt5-qt5==5.15.15
     # via pyqt5
-pyqt5-sip==12.13.0
+pyqt5-sip==12.15.0
     # via pyqt5
-pyqt6==6.7.0
+pyqt6==6.7.1
     # via napari (napari_repo/pyproject.toml)
-pyqt6-qt6==6.7.2
+pyqt6-qt6==6.7.3
     # via pyqt6
-pyqt6-sip==13.6.0
+pyqt6-sip==13.8.0
     # via pyqt6
 pyrect==0.2.0
     # via pygetwindow
-pyscreeze==0.1.30
+pyscreeze==1.0.1
     # via pyautogui
 pyside2==5.15.2.1
     # via napari (napari_repo/pyproject.toml)
@@ -385,7 +385,7 @@ pyside6-essentials==6.4.2
     # via
     #   pyside6
     #   pyside6-addons
-pytest==8.2.2
+pytest==8.3.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
@@ -393,7 +393,7 @@ pytest==8.2.2
     #   pytest-metadata
     #   pytest-pretty
     #   pytest-qt
-pytest-cov==5.0.0
+pytest-cov==6.0.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
 pytest-json-report==1.5.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
@@ -414,23 +414,22 @@ python3-xlib==0.15
     #   pyautogui
 pytweening==1.2.0
     # via pyautogui
-pytz==2024.1
+pytz==2024.2
     # via pandas
-pyyaml==6.0.1
+pyyaml==6.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   npe2
-pyzmq==26.0.3
+pyzmq==26.2.0
     # via
     #   ipykernel
     #   jupyter-client
-    #   qtconsole
-qtconsole==5.5.2
+qtconsole==5.6.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-console
-qtpy==2.4.1
+qtpy==2.4.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
@@ -447,19 +446,19 @@ requests==2.32.3
     #   pooch
     #   pyconify
     #   sphinx
-rich==13.7.1
+rich==13.9.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
     #   pytest-pretty
     #   typer
-rpds-py==0.18.1
+rpds-py==0.21.0
     # via
     #   jsonschema
     #   referencing
 scikit-image==0.24.0
     # via napari (napari_repo/pyproject.toml)
-scipy==1.13.1
+scipy==1.14.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
@@ -480,19 +479,19 @@ snowballstemmer==2.2.0
     # via sphinx
 sortedcontainers==2.4.0
     # via hypothesis
-sphinx==7.3.7
+sphinx==8.1.3
     # via numpydoc
-sphinxcontrib-applehelp==1.0.8
+sphinxcontrib-applehelp==2.0.0
     # via sphinx
-sphinxcontrib-devhelp==1.0.6
+sphinxcontrib-devhelp==2.0.0
     # via sphinx
-sphinxcontrib-htmlhelp==2.0.5
+sphinxcontrib-htmlhelp==2.1.0
     # via sphinx
 sphinxcontrib-jsmath==1.0.1
     # via sphinx
-sphinxcontrib-qthelp==1.0.7
+sphinxcontrib-qthelp==2.0.0
     # via sphinx
-sphinxcontrib-serializinghtml==1.1.10
+sphinxcontrib-serializinghtml==2.0.0
     # via sphinx
 stack-data==0.6.3
     # via ipython
@@ -501,19 +500,19 @@ superqt==0.6.7
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
     #   napari-plugin-manager
-sympy==1.12.1
+sympy==1.13.1
     # via torch
 tabulate==0.9.0
     # via numpydoc
-tensorstore==0.1.63
+tensorstore==0.1.68
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-tifffile==2024.6.18
+tifffile==2024.9.20
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
-tomli==2.0.1
+tomli==2.1.0
     # via
     #   build
     #   coverage
@@ -521,20 +520,20 @@ tomli==2.0.1
     #   numpydoc
     #   pytest
     #   sphinx
-tomli-w==1.0.0
+tomli-w==1.1.0
     # via npe2
-toolz==0.12.1
+toolz==1.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   partd
-torch==2.3.1
+torch==2.5.1
     # via napari (napari_repo/pyproject.toml)
 tornado==6.4.1
     # via
     #   ipykernel
     #   jupyter-client
-tqdm==4.66.4
+tqdm==4.67.0
     # via napari (napari_repo/pyproject.toml)
 traitlets==5.14.3
     # via
@@ -547,9 +546,9 @@ traitlets==5.14.3
     #   qtconsole
 triangle==20230923
     # via napari (napari_repo/pyproject.toml)
-triton==2.3.1
+triton==3.1.0
     # via torch
-typer==0.12.3
+typer==0.13.0
     # via npe2
 typing-extensions==4.12.2
     # via
@@ -561,14 +560,15 @@ typing-extensions==4.12.2
     #   magicgui
     #   pint
     #   pydantic
+    #   rich
     #   superqt
     #   torch
     #   typer
-tzdata==2024.1
+tzdata==2024.2
     # via pandas
-urllib3==2.2.2
+urllib3==2.2.3
     # via requests
-virtualenv==20.26.3
+virtualenv==20.27.1
     # via napari (napari_repo/pyproject.toml)
 vispy==0.14.3
     # via
@@ -578,9 +578,9 @@ wcwidth==0.2.13
     # via prompt-toolkit
 wrapt==1.16.0
     # via napari (napari_repo/pyproject.toml)
-xarray==2024.6.0
+xarray==2024.10.0
     # via napari (napari_repo/pyproject.toml)
-zarr==2.18.2
+zarr==2.18.3
     # via napari (napari_repo/pyproject.toml)
-zipp==3.19.2
+zipp==3.21.0
     # via importlib-metadata
diff --git a/resources/constraints/constraints_py3.10_macos_arm.txt b/resources/constraints/constraints_py3.10_windows.txt
similarity index 78%
rename from resources/constraints/constraints_py3.10_macos_arm.txt
rename to resources/constraints/constraints_py3.10_windows.txt
index 921e01eaf70..04e410f1619 100644
--- a/resources/constraints/constraints_py3.10_macos_arm.txt
+++ b/resources/constraints/constraints_py3.10_windows.txt
@@ -1,64 +1,69 @@
 # This file was autogenerated by uv via the following command:
-#    uv pip compile --python-platform aarch64-apple-darwin --python-version 3.10 --output-file napari_repo/resources/constraints/constraints_py3.10_macos_arm.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt --extra pyqt5 --extra pyqt6_experimental --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
-alabaster==0.7.16
+#    uv pip compile --python-platform windows --python-version 3.10 --output-file napari_repo/resources/constraints/constraints_py3.10_windows.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt --extra pyqt5 --extra pyqt6 --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
+alabaster==1.0.0
     # via sphinx
 annotated-types==0.7.0
     # via pydantic
-app-model==0.2.7
+app-model==0.3.0
     # via napari (napari_repo/pyproject.toml)
 appdirs==1.4.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
-    #   pint
-appnope==0.1.4
-    # via ipykernel
 asciitree==0.3.3
     # via zarr
 asttokens==2.4.1
     # via stack-data
-attrs==23.2.0
+attrs==24.2.0
     # via
     #   hypothesis
     #   jsonschema
     #   referencing
-babel==2.15.0
+babel==2.16.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   sphinx
-build==1.2.1
+build==1.2.2.post1
     # via npe2
 cachey==0.2.1
     # via napari (napari_repo/pyproject.toml)
-certifi==2024.7.4
+certifi==2024.8.30
     # via
     #   napari (napari_repo/pyproject.toml)
     #   requests
-charset-normalizer==3.3.2
+charset-normalizer==3.4.0
     # via requests
 click==8.1.7
     # via
     #   dask
     #   typer
-cloudpickle==3.0.0
+cloudpickle==3.1.0
     # via dask
+colorama==0.4.6
+    # via
+    #   build
+    #   click
+    #   ipython
+    #   pytest
+    #   sphinx
+    #   tqdm
 comm==0.2.2
     # via ipykernel
-contourpy==1.2.1
+contourpy==1.3.1
     # via matplotlib
-coverage==7.5.4
+coverage==7.6.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
 cycler==0.12.1
     # via matplotlib
-dask==2024.6.2
+dask==2024.11.2
     # via napari (napari_repo/pyproject.toml)
-debugpy==1.8.1
+debugpy==1.8.8
     # via ipykernel
 decorator==5.1.1
     # via ipython
-distlib==0.3.8
+distlib==0.3.9
     # via virtualenv
 docstring-parser==0.16
     # via
@@ -66,28 +71,28 @@ docstring-parser==0.16
     #   magicgui
 docutils==0.21.2
     # via sphinx
-exceptiongroup==1.2.1
+exceptiongroup==1.2.2
     # via
     #   hypothesis
     #   ipython
     #   pytest
-executing==2.0.1
+executing==2.1.0
     # via stack-data
 fasteners==0.19
     # via zarr
-filelock==3.15.4
+filelock==3.16.1
     # via
     #   torch
     #   virtualenv
 flexcache==0.3
     # via pint
-flexparser==0.3.1
+flexparser==0.4
     # via pint
-fonttools==4.53.0
+fonttools==4.55.0
     # via matplotlib
-freetype-py==2.4.0
+freetype-py==2.5.1
     # via vispy
-fsspec==2024.6.0
+fsspec==2024.10.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
@@ -96,18 +101,18 @@ heapdict==1.0.1
     # via cachey
 hsluv==5.0.4
     # via vispy
-hypothesis==6.103.4
+hypothesis==6.119.3
     # via napari (napari_repo/pyproject.toml)
-idna==3.7
+idna==3.10
     # via requests
-imageio==2.34.1
+imageio==2.36.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-svg
     #   scikit-image
 imagesize==1.4.1
     # via sphinx
-importlib-metadata==7.2.1
+importlib-metadata==8.5.0
     # via
     #   build
     #   dask
@@ -115,26 +120,27 @@ in-n-out==0.2.1
     # via app-model
 iniconfig==2.0.0
     # via pytest
-ipykernel==6.29.4
+ipykernel==6.29.5
     # via
+    #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari-console
     #   qtconsole
-ipython==8.25.0
+ipython==8.29.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
     #   napari-console
-jedi==0.19.1
+jedi==0.19.2
     # via ipython
 jinja2==3.1.4
     # via
     #   sphinx
     #   torch
-jsonschema==4.22.0
+jsonschema==4.23.0
     # via napari (napari_repo/pyproject.toml)
-jsonschema-specifications==2023.12.1
+jsonschema-specifications==2024.10.1
     # via jsonschema
-jupyter-client==8.6.2
+jupyter-client==8.6.3
     # via
     #   ipykernel
     #   qtconsole
@@ -143,7 +149,7 @@ jupyter-core==5.7.2
     #   ipykernel
     #   jupyter-client
     #   qtconsole
-kiwisolver==1.4.5
+kiwisolver==1.4.7
     # via
     #   matplotlib
     #   vispy
@@ -155,19 +161,19 @@ llvmlite==0.43.0
     # via numba
 locket==1.0.0
     # via partd
-lxml==5.2.2
+lxml==5.3.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   lxml-html-clean
-lxml-html-clean==0.1.1
+lxml-html-clean==0.4.1
     # via lxml
-magicgui==0.8.3
+magicgui==0.9.1
     # via napari (napari_repo/pyproject.toml)
 markdown-it-py==3.0.0
     # via rich
-markupsafe==2.1.5
+markupsafe==3.0.2
     # via jinja2
-matplotlib==3.9.0
+matplotlib==3.9.2
     # via napari (napari_repo/pyproject.toml)
 matplotlib-inline==0.1.7
     # via
@@ -175,37 +181,35 @@ matplotlib-inline==0.1.7
     #   ipython
 mdurl==0.1.2
     # via markdown-it-py
-ml-dtypes==0.4.0
+ml-dtypes==0.5.0
     # via tensorstore
 mouseinfo==0.1.3
     # via pyautogui
 mpmath==1.3.0
     # via sympy
-napari-console==0.0.9
+napari-console==0.1.1
     # via napari (napari_repo/pyproject.toml)
 napari-plugin-engine==0.2.0
-    # via
-    #   napari (napari_repo/pyproject.toml)
-    #   napari-svg
-napari-plugin-manager==0.1.0a2
     # via napari (napari_repo/pyproject.toml)
-napari-svg==0.1.10
+napari-plugin-manager==0.1.3
+    # via napari (napari_repo/pyproject.toml)
+napari-svg==0.2.0
     # via napari (napari_repo/pyproject.toml)
 nest-asyncio==1.6.0
     # via ipykernel
-networkx==3.3
+networkx==3.4.2
     # via
     #   scikit-image
     #   torch
-npe2==0.7.6
+npe2==0.7.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-plugin-manager
 numba==0.60.0
     # via napari (napari_repo/pyproject.toml)
-numcodecs==0.12.1
+numcodecs==0.13.1
     # via zarr
-numpy==2.0.0
+numpy==2.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   contourpy
@@ -221,12 +225,13 @@ numpy==2.0.0
     #   scipy
     #   tensorstore
     #   tifffile
+    #   triangle
     #   vispy
     #   xarray
     #   zarr
-numpydoc==1.7.0
+numpydoc==1.8.0
     # via napari (napari_repo/pyproject.toml)
-packaging==24.1
+packaging==24.2
     # via
     #   build
     #   dask
@@ -241,7 +246,7 @@ packaging==24.1
     #   sphinx
     #   vispy
     #   xarray
-pandas==2.2.2
+pandas==2.2.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   xarray
@@ -249,22 +254,21 @@ parso==0.8.4
     # via jedi
 partd==1.4.2
     # via dask
-pexpect==4.9.0
-    # via ipython
-pillow==10.3.0
+pillow==11.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   imageio
     #   matplotlib
     #   pyscreeze
     #   scikit-image
-pint==0.24
+pint==0.24.4
     # via napari (napari_repo/pyproject.toml)
-pip==24.1
+pip==24.3.1
     # via napari-plugin-manager
-platformdirs==4.2.2
+platformdirs==4.3.6
     # via
     #   jupyter-core
+    #   pint
     #   pooch
     #   virtualenv
 pluggy==1.5.0
@@ -277,9 +281,9 @@ pooch==1.8.2
     #   scikit-image
 pretend==1.0.9
     # via napari (napari_repo/pyproject.toml)
-prompt-toolkit==3.0.47
+prompt-toolkit==3.0.48
     # via ipython
-psutil==6.0.0
+psutil==6.1.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
@@ -289,15 +293,13 @@ psygnal==0.11.1
     #   app-model
     #   magicgui
     #   npe2
-ptyprocess==0.7.0
-    # via pexpect
-pure-eval==0.2.2
+pure-eval==0.2.3
     # via stack-data
 pyautogui==0.9.54
     # via napari (napari_repo/pyproject.toml)
 pyconify==0.1.6
     # via superqt
-pydantic==2.7.4
+pydantic==2.9.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   app-model
@@ -305,7 +307,7 @@ pydantic==2.7.4
     #   pydantic-compat
 pydantic-compat==0.1.2
     # via app-model
-pydantic-core==2.18.4
+pydantic-core==2.23.4
     # via pydantic
 pygetwindow==0.0.9
     # via pyautogui
@@ -319,41 +321,34 @@ pygments==2.18.0
     #   superqt
 pymsgbox==1.0.9
     # via pyautogui
-pyobjc-core==10.3.1
-    # via
-    #   pyautogui
-    #   pyobjc-framework-cocoa
-    #   pyobjc-framework-quartz
-pyobjc-framework-cocoa==10.3.1
-    # via pyobjc-framework-quartz
-pyobjc-framework-quartz==10.3.1
-    # via pyautogui
 pyopengl==3.1.6
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-pyparsing==3.1.2
+pyparsing==3.2.0
     # via matplotlib
 pyperclip==1.9.0
     # via mouseinfo
-pyproject-hooks==1.1.0
+pyproject-hooks==1.2.0
     # via build
-pyqt5==5.15.10
+pyqt5==5.15.11
     # via napari (napari_repo/pyproject.toml)
-pyqt5-qt5==5.15.14
+pyqt5-qt5==5.15.2
     # via pyqt5
-pyqt5-sip==12.13.0
+pyqt5-sip==12.15.0
     # via pyqt5
-pyqt6==6.7.0
+pyqt6==6.7.1
     # via napari (napari_repo/pyproject.toml)
-pyqt6-qt6==6.7.2
+pyqt6-qt6==6.7.3
     # via pyqt6
-pyqt6-sip==13.6.0
+pyqt6-sip==13.8.0
     # via pyqt6
 pyrect==0.2.0
     # via pygetwindow
-pyscreeze==0.1.30
+pyscreeze==1.0.1
     # via pyautogui
+pyside2==5.15.2.1
+    # via napari (napari_repo/pyproject.toml)
 pyside6==6.4.2
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
@@ -364,7 +359,7 @@ pyside6-essentials==6.4.2
     # via
     #   pyside6
     #   pyside6-addons
-pytest==8.2.2
+pytest==8.3.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
@@ -372,7 +367,7 @@ pytest==8.2.2
     #   pytest-metadata
     #   pytest-pretty
     #   pytest-qt
-pytest-cov==5.0.0
+pytest-cov==6.0.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
 pytest-json-report==1.5.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
@@ -389,23 +384,26 @@ python-dateutil==2.9.0.post0
     #   pandas
 pytweening==1.2.0
     # via pyautogui
-pytz==2024.1
+pytz==2024.2
     # via pandas
-pyyaml==6.0.1
+pywin32==308
+    # via
+    #   napari (napari_repo/pyproject.toml)
+    #   jupyter-core
+pyyaml==6.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   npe2
-pyzmq==26.0.3
+pyzmq==26.2.0
     # via
     #   ipykernel
     #   jupyter-client
-    #   qtconsole
-qtconsole==5.5.2
+qtconsole==5.6.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-console
-qtpy==2.4.1
+qtpy==2.4.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
@@ -422,26 +420,26 @@ requests==2.32.3
     #   pooch
     #   pyconify
     #   sphinx
-rich==13.7.1
+rich==13.9.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
     #   pytest-pretty
     #   typer
-rpds-py==0.18.1
+rpds-py==0.21.0
     # via
     #   jsonschema
     #   referencing
-rubicon-objc==0.4.9
-    # via mouseinfo
 scikit-image==0.24.0
     # via napari (napari_repo/pyproject.toml)
-scipy==1.13.1
+scipy==1.14.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
 shellingham==1.5.4
     # via typer
+shiboken2==5.15.2.1
+    # via pyside2
 shiboken6==6.4.2
     # via
     #   pyside6
@@ -455,19 +453,19 @@ snowballstemmer==2.2.0
     # via sphinx
 sortedcontainers==2.4.0
     # via hypothesis
-sphinx==7.3.7
+sphinx==8.1.3
     # via numpydoc
-sphinxcontrib-applehelp==1.0.8
+sphinxcontrib-applehelp==2.0.0
     # via sphinx
-sphinxcontrib-devhelp==1.0.6
+sphinxcontrib-devhelp==2.0.0
     # via sphinx
-sphinxcontrib-htmlhelp==2.0.5
+sphinxcontrib-htmlhelp==2.1.0
     # via sphinx
 sphinxcontrib-jsmath==1.0.1
     # via sphinx
-sphinxcontrib-qthelp==1.0.7
+sphinxcontrib-qthelp==2.0.0
     # via sphinx
-sphinxcontrib-serializinghtml==1.1.10
+sphinxcontrib-serializinghtml==2.0.0
     # via sphinx
 stack-data==0.6.3
     # via ipython
@@ -476,19 +474,19 @@ superqt==0.6.7
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
     #   napari-plugin-manager
-sympy==1.12.1
+sympy==1.13.1
     # via torch
 tabulate==0.9.0
     # via numpydoc
-tensorstore==0.1.63
+tensorstore==0.1.68
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-tifffile==2024.6.18
+tifffile==2024.9.20
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
-tomli==2.0.1
+tomli==2.1.0
     # via
     #   build
     #   coverage
@@ -496,20 +494,20 @@ tomli==2.0.1
     #   numpydoc
     #   pytest
     #   sphinx
-tomli-w==1.0.0
+tomli-w==1.1.0
     # via npe2
-toolz==0.12.1
+toolz==1.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   partd
-torch==2.3.1
+torch==2.5.1
     # via napari (napari_repo/pyproject.toml)
 tornado==6.4.1
     # via
     #   ipykernel
     #   jupyter-client
-tqdm==4.66.4
+tqdm==4.67.0
     # via napari (napari_repo/pyproject.toml)
 traitlets==5.14.3
     # via
@@ -520,7 +518,9 @@ traitlets==5.14.3
     #   jupyter-core
     #   matplotlib-inline
     #   qtconsole
-typer==0.12.3
+triangle==20230923
+    # via napari (napari_repo/pyproject.toml)
+typer==0.13.0
     # via npe2
 typing-extensions==4.12.2
     # via
@@ -533,14 +533,15 @@ typing-extensions==4.12.2
     #   pint
     #   pydantic
     #   pydantic-core
+    #   rich
     #   superqt
     #   torch
     #   typer
-tzdata==2024.1
+tzdata==2024.2
     # via pandas
-urllib3==2.2.2
+urllib3==2.2.3
     # via requests
-virtualenv==20.26.3
+virtualenv==20.27.1
     # via napari (napari_repo/pyproject.toml)
 vispy==0.14.3
     # via
@@ -550,9 +551,9 @@ wcwidth==0.2.13
     # via prompt-toolkit
 wrapt==1.16.0
     # via napari (napari_repo/pyproject.toml)
-xarray==2024.6.0
+xarray==2024.10.0
     # via napari (napari_repo/pyproject.toml)
-zarr==2.18.2
+zarr==2.18.3
     # via napari (napari_repo/pyproject.toml)
-zipp==3.19.2
+zipp==3.21.0
     # via importlib-metadata
diff --git a/resources/constraints/constraints_py3.11.txt b/resources/constraints/constraints_py3.11.txt
index 2c3fbba5050..65da823ad08 100644
--- a/resources/constraints/constraints_py3.11.txt
+++ b/resources/constraints/constraints_py3.11.txt
@@ -1,62 +1,61 @@
 # This file was autogenerated by uv via the following command:
-#    uv pip compile --python-version 3.11 --output-file napari_repo/resources/constraints/constraints_py3.11.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt --extra pyqt5 --extra pyqt6_experimental --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
-alabaster==0.7.16
+#    uv pip compile --python-version 3.11 --output-file napari_repo/resources/constraints/constraints_py3.11.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt --extra pyqt5 --extra pyqt6 --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
+alabaster==1.0.0
     # via sphinx
 annotated-types==0.7.0
     # via pydantic
-app-model==0.2.7
+app-model==0.3.0
     # via napari (napari_repo/pyproject.toml)
 appdirs==1.4.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
-    #   pint
 asciitree==0.3.3
     # via zarr
 asttokens==2.4.1
     # via stack-data
-attrs==23.2.0
+attrs==24.2.0
     # via
     #   hypothesis
     #   jsonschema
     #   referencing
-babel==2.15.0
+babel==2.16.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   sphinx
-build==1.2.1
+build==1.2.2.post1
     # via npe2
 cachey==0.2.1
     # via napari (napari_repo/pyproject.toml)
-certifi==2024.7.4
+certifi==2024.8.30
     # via
     #   napari (napari_repo/pyproject.toml)
     #   requests
-charset-normalizer==3.3.2
+charset-normalizer==3.4.0
     # via requests
 click==8.1.7
     # via
     #   dask
     #   typer
-cloudpickle==3.0.0
+cloudpickle==3.1.0
     # via dask
 comm==0.2.2
     # via ipykernel
-contourpy==1.2.1
+contourpy==1.3.1
     # via matplotlib
-coverage==7.5.4
+coverage==7.6.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
 cycler==0.12.1
     # via matplotlib
-dask==2024.6.2
+dask==2024.11.2
     # via napari (napari_repo/pyproject.toml)
-debugpy==1.8.1
+debugpy==1.8.8
     # via ipykernel
 decorator==5.1.1
     # via ipython
-distlib==0.3.8
+distlib==0.3.9
     # via virtualenv
 docstring-parser==0.16
     # via
@@ -64,24 +63,24 @@ docstring-parser==0.16
     #   magicgui
 docutils==0.21.2
     # via sphinx
-executing==2.0.1
+executing==2.1.0
     # via stack-data
 fasteners==0.19
     # via zarr
-filelock==3.15.4
+filelock==3.16.1
     # via
     #   torch
     #   triton
     #   virtualenv
 flexcache==0.3
     # via pint
-flexparser==0.3.1
+flexparser==0.4
     # via pint
-fonttools==4.53.0
+fonttools==4.55.0
     # via matplotlib
-freetype-py==2.4.0
+freetype-py==2.5.1
     # via vispy
-fsspec==2024.6.0
+fsspec==2024.10.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
@@ -90,43 +89,44 @@ heapdict==1.0.1
     # via cachey
 hsluv==5.0.4
     # via vispy
-hypothesis==6.103.4
+hypothesis==6.119.3
     # via napari (napari_repo/pyproject.toml)
-idna==3.7
+idna==3.10
     # via requests
-imageio==2.34.1
+imageio==2.36.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-svg
     #   scikit-image
 imagesize==1.4.1
     # via sphinx
-importlib-metadata==7.2.1
+importlib-metadata==8.5.0
     # via dask
 in-n-out==0.2.1
     # via app-model
 iniconfig==2.0.0
     # via pytest
-ipykernel==6.29.4
+ipykernel==6.29.5
     # via
+    #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari-console
     #   qtconsole
-ipython==8.25.0
+ipython==8.29.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
     #   napari-console
-jedi==0.19.1
+jedi==0.19.2
     # via ipython
 jinja2==3.1.4
     # via
     #   sphinx
     #   torch
-jsonschema==4.22.0
+jsonschema==4.23.0
     # via napari (napari_repo/pyproject.toml)
-jsonschema-specifications==2023.12.1
+jsonschema-specifications==2024.10.1
     # via jsonschema
-jupyter-client==8.6.2
+jupyter-client==8.6.3
     # via
     #   ipykernel
     #   qtconsole
@@ -135,7 +135,7 @@ jupyter-core==5.7.2
     #   ipykernel
     #   jupyter-client
     #   qtconsole
-kiwisolver==1.4.5
+kiwisolver==1.4.7
     # via
     #   matplotlib
     #   vispy
@@ -147,19 +147,19 @@ llvmlite==0.43.0
     # via numba
 locket==1.0.0
     # via partd
-lxml==5.2.2
+lxml==5.3.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   lxml-html-clean
-lxml-html-clean==0.1.1
+lxml-html-clean==0.4.1
     # via lxml
-magicgui==0.8.3
+magicgui==0.9.1
     # via napari (napari_repo/pyproject.toml)
 markdown-it-py==3.0.0
     # via rich
-markupsafe==2.1.5
+markupsafe==3.0.2
     # via jinja2
-matplotlib==3.9.0
+matplotlib==3.9.2
     # via napari (napari_repo/pyproject.toml)
 matplotlib-inline==0.1.7
     # via
@@ -167,37 +167,35 @@ matplotlib-inline==0.1.7
     #   ipython
 mdurl==0.1.2
     # via markdown-it-py
-ml-dtypes==0.4.0
+ml-dtypes==0.5.0
     # via tensorstore
 mouseinfo==0.1.3
     # via pyautogui
 mpmath==1.3.0
     # via sympy
-napari-console==0.0.9
+napari-console==0.1.1
     # via napari (napari_repo/pyproject.toml)
 napari-plugin-engine==0.2.0
-    # via
-    #   napari (napari_repo/pyproject.toml)
-    #   napari-svg
-napari-plugin-manager==0.1.0a2
     # via napari (napari_repo/pyproject.toml)
-napari-svg==0.1.10
+napari-plugin-manager==0.1.3
+    # via napari (napari_repo/pyproject.toml)
+napari-svg==0.2.0
     # via napari (napari_repo/pyproject.toml)
 nest-asyncio==1.6.0
     # via ipykernel
-networkx==3.3
+networkx==3.4.2
     # via
     #   scikit-image
     #   torch
-npe2==0.7.6
+npe2==0.7.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-plugin-manager
 numba==0.60.0
     # via napari (napari_repo/pyproject.toml)
-numcodecs==0.12.1
+numcodecs==0.14.0
     # via zarr
-numpy==2.0.0
+numpy==2.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   contourpy
@@ -217,40 +215,41 @@ numpy==2.0.0
     #   vispy
     #   xarray
     #   zarr
-numpydoc==1.7.0
+numpydoc==1.8.0
     # via napari (napari_repo/pyproject.toml)
-nvidia-cublas-cu12==12.1.3.1
+nvidia-cublas-cu12==12.4.5.8
     # via
     #   nvidia-cudnn-cu12
     #   nvidia-cusolver-cu12
     #   torch
-nvidia-cuda-cupti-cu12==12.1.105
+nvidia-cuda-cupti-cu12==12.4.127
     # via torch
-nvidia-cuda-nvrtc-cu12==12.1.105
+nvidia-cuda-nvrtc-cu12==12.4.127
     # via torch
-nvidia-cuda-runtime-cu12==12.1.105
+nvidia-cuda-runtime-cu12==12.4.127
     # via torch
-nvidia-cudnn-cu12==8.9.2.26
+nvidia-cudnn-cu12==9.1.0.70
     # via torch
-nvidia-cufft-cu12==11.0.2.54
+nvidia-cufft-cu12==11.2.1.3
     # via torch
-nvidia-curand-cu12==10.3.2.106
+nvidia-curand-cu12==10.3.5.147
     # via torch
-nvidia-cusolver-cu12==11.4.5.107
+nvidia-cusolver-cu12==11.6.1.9
     # via torch
-nvidia-cusparse-cu12==12.1.0.106
+nvidia-cusparse-cu12==12.3.1.170
     # via
     #   nvidia-cusolver-cu12
     #   torch
-nvidia-nccl-cu12==2.20.5
+nvidia-nccl-cu12==2.21.5
     # via torch
-nvidia-nvjitlink-cu12==12.5.40
+nvidia-nvjitlink-cu12==12.4.127
     # via
     #   nvidia-cusolver-cu12
     #   nvidia-cusparse-cu12
-nvidia-nvtx-cu12==12.1.105
+    #   torch
+nvidia-nvtx-cu12==12.4.127
     # via torch
-packaging==24.1
+packaging==24.2
     # via
     #   build
     #   dask
@@ -265,7 +264,7 @@ packaging==24.1
     #   sphinx
     #   vispy
     #   xarray
-pandas==2.2.2
+pandas==2.2.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   xarray
@@ -275,20 +274,21 @@ partd==1.4.2
     # via dask
 pexpect==4.9.0
     # via ipython
-pillow==10.3.0
+pillow==11.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   imageio
     #   matplotlib
     #   pyscreeze
     #   scikit-image
-pint==0.24
+pint==0.24.4
     # via napari (napari_repo/pyproject.toml)
-pip==24.1
+pip==24.3.1
     # via napari-plugin-manager
-platformdirs==4.2.2
+platformdirs==4.3.6
     # via
     #   jupyter-core
+    #   pint
     #   pooch
     #   virtualenv
 pluggy==1.5.0
@@ -301,9 +301,9 @@ pooch==1.8.2
     #   scikit-image
 pretend==1.0.9
     # via napari (napari_repo/pyproject.toml)
-prompt-toolkit==3.0.47
+prompt-toolkit==3.0.48
     # via ipython
-psutil==6.0.0
+psutil==6.1.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
@@ -315,13 +315,13 @@ psygnal==0.11.1
     #   npe2
 ptyprocess==0.7.0
     # via pexpect
-pure-eval==0.2.2
+pure-eval==0.2.3
     # via stack-data
 pyautogui==0.9.54
     # via napari (napari_repo/pyproject.toml)
 pyconify==0.1.6
     # via superqt
-pydantic==2.7.4
+pydantic==2.9.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   app-model
@@ -329,7 +329,7 @@ pydantic==2.7.4
     #   pydantic-compat
 pydantic-compat==0.1.2
     # via app-model
-pydantic-core==2.18.4
+pydantic-core==2.23.4
     # via pydantic
 pygetwindow==0.0.9
     # via pyautogui
@@ -347,27 +347,27 @@ pyopengl==3.1.6
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-pyparsing==3.1.2
+pyparsing==3.2.0
     # via matplotlib
 pyperclip==1.9.0
     # via mouseinfo
-pyproject-hooks==1.1.0
+pyproject-hooks==1.2.0
     # via build
-pyqt5==5.15.10
+pyqt5==5.15.11
     # via napari (napari_repo/pyproject.toml)
-pyqt5-qt5==5.15.2
+pyqt5-qt5==5.15.15
     # via pyqt5
-pyqt5-sip==12.13.0
+pyqt5-sip==12.15.0
     # via pyqt5
-pyqt6==6.7.0
+pyqt6==6.7.1
     # via napari (napari_repo/pyproject.toml)
-pyqt6-qt6==6.7.2
+pyqt6-qt6==6.7.3
     # via pyqt6
-pyqt6-sip==13.6.0
+pyqt6-sip==13.8.0
     # via pyqt6
 pyrect==0.2.0
     # via pygetwindow
-pyscreeze==0.1.30
+pyscreeze==1.0.1
     # via pyautogui
 pyside6==6.4.2
     # via
@@ -379,7 +379,7 @@ pyside6-essentials==6.4.2
     # via
     #   pyside6
     #   pyside6-addons
-pytest==8.2.2
+pytest==8.3.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
@@ -387,7 +387,7 @@ pytest==8.2.2
     #   pytest-metadata
     #   pytest-pretty
     #   pytest-qt
-pytest-cov==5.0.0
+pytest-cov==6.0.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
 pytest-json-report==1.5.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
@@ -408,23 +408,22 @@ python3-xlib==0.15
     #   pyautogui
 pytweening==1.2.0
     # via pyautogui
-pytz==2024.1
+pytz==2024.2
     # via pandas
-pyyaml==6.0.1
+pyyaml==6.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   npe2
-pyzmq==26.0.3
+pyzmq==26.2.0
     # via
     #   ipykernel
     #   jupyter-client
-    #   qtconsole
-qtconsole==5.5.2
+qtconsole==5.6.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-console
-qtpy==2.4.1
+qtpy==2.4.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
@@ -441,19 +440,19 @@ requests==2.32.3
     #   pooch
     #   pyconify
     #   sphinx
-rich==13.7.1
+rich==13.9.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
     #   pytest-pretty
     #   typer
-rpds-py==0.18.1
+rpds-py==0.21.0
     # via
     #   jsonschema
     #   referencing
 scikit-image==0.24.0
     # via napari (napari_repo/pyproject.toml)
-scipy==1.13.1
+scipy==1.14.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
@@ -472,19 +471,19 @@ snowballstemmer==2.2.0
     # via sphinx
 sortedcontainers==2.4.0
     # via hypothesis
-sphinx==7.3.7
+sphinx==8.1.3
     # via numpydoc
-sphinxcontrib-applehelp==1.0.8
+sphinxcontrib-applehelp==2.0.0
     # via sphinx
-sphinxcontrib-devhelp==1.0.6
+sphinxcontrib-devhelp==2.0.0
     # via sphinx
-sphinxcontrib-htmlhelp==2.0.5
+sphinxcontrib-htmlhelp==2.1.0
     # via sphinx
 sphinxcontrib-jsmath==1.0.1
     # via sphinx
-sphinxcontrib-qthelp==1.0.7
+sphinxcontrib-qthelp==2.0.0
     # via sphinx
-sphinxcontrib-serializinghtml==1.1.10
+sphinxcontrib-serializinghtml==2.0.0
     # via sphinx
 stack-data==0.6.3
     # via ipython
@@ -493,32 +492,34 @@ superqt==0.6.7
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
     #   napari-plugin-manager
-sympy==1.12.1
+sympy==1.13.1
     # via torch
 tabulate==0.9.0
     # via numpydoc
-tensorstore==0.1.63
+tensorstore==0.1.68
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-tifffile==2024.6.18
+tifffile==2024.9.20
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
-tomli-w==1.0.0
+tomli==2.1.0
+    # via coverage
+tomli-w==1.1.0
     # via npe2
-toolz==0.12.1
+toolz==1.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   partd
-torch==2.3.1
+torch==2.5.1
     # via napari (napari_repo/pyproject.toml)
 tornado==6.4.1
     # via
     #   ipykernel
     #   jupyter-client
-tqdm==4.66.4
+tqdm==4.67.0
     # via napari (napari_repo/pyproject.toml)
 traitlets==5.14.3
     # via
@@ -531,9 +532,9 @@ traitlets==5.14.3
     #   qtconsole
 triangle==20230923
     # via napari (napari_repo/pyproject.toml)
-triton==2.3.1
+triton==3.1.0
     # via torch
-typer==0.12.3
+typer==0.13.0
     # via npe2
 typing-extensions==4.12.2
     # via
@@ -549,11 +550,11 @@ typing-extensions==4.12.2
     #   superqt
     #   torch
     #   typer
-tzdata==2024.1
+tzdata==2024.2
     # via pandas
-urllib3==2.2.2
+urllib3==2.2.3
     # via requests
-virtualenv==20.26.3
+virtualenv==20.27.1
     # via napari (napari_repo/pyproject.toml)
 vispy==0.14.3
     # via
@@ -563,9 +564,9 @@ wcwidth==0.2.13
     # via prompt-toolkit
 wrapt==1.16.0
     # via napari (napari_repo/pyproject.toml)
-xarray==2024.6.0
+xarray==2024.10.0
     # via napari (napari_repo/pyproject.toml)
-zarr==2.18.2
+zarr==2.18.3
     # via napari (napari_repo/pyproject.toml)
-zipp==3.19.2
+zipp==3.21.0
     # via importlib-metadata
diff --git a/resources/constraints/constraints_py3.11_docs.txt b/resources/constraints/constraints_py3.11_docs.txt
new file mode 100644
index 00000000000..2ec41cd708c
--- /dev/null
+++ b/resources/constraints/constraints_py3.11_docs.txt
@@ -0,0 +1,723 @@
+# This file was autogenerated by uv via the following command:
+#    uv pip compile --python-version 3.11 --output-file resources/constraints/constraints_py3.11_docs.txt pyproject.toml resources/constraints/version_denylist.txt resources/constraints/version_denylist_examples.txt ../napari-docs/requirements.txt resources/constraints/pydantic_le_2.txt --extra pyqt5 --extra pyqt6 --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
+accessible-pygments==0.0.5
+    # via pydata-sphinx-theme
+alabaster==0.7.16
+    # via sphinx
+anyio==4.4.0
+    # via
+    #   starlette
+    #   watchfiles
+app-model==0.3.0
+    # via napari (pyproject.toml)
+appdirs==1.4.4
+    # via
+    #   napari (pyproject.toml)
+    #   npe2
+    #   pint
+asciitree==0.3.3
+    # via zarr
+asttokens==2.4.1
+    # via stack-data
+attrs==23.2.0
+    # via
+    #   hypothesis
+    #   jsonschema
+    #   jupyter-cache
+    #   referencing
+babel==2.15.0
+    # via
+    #   napari (pyproject.toml)
+    #   pydata-sphinx-theme
+    #   sphinx
+beautifulsoup4==4.12.3
+    # via pydata-sphinx-theme
+build==1.2.1
+    # via npe2
+cachey==0.2.1
+    # via napari (pyproject.toml)
+certifi==2024.7.4
+    # via
+    #   napari (pyproject.toml)
+    #   requests
+charset-normalizer==3.3.2
+    # via requests
+click==8.1.7
+    # via
+    #   dask
+    #   jupyter-cache
+    #   sphinx-external-toc
+    #   typer
+    #   uvicorn
+cloudpickle==3.0.0
+    # via dask
+colorama==0.4.6
+    # via sphinx-autobuild
+comm==0.2.2
+    # via ipykernel
+contourpy==1.2.1
+    # via matplotlib
+coverage==7.6.0
+    # via
+    #   napari (pyproject.toml)
+    #   pytest-cov
+cycler==0.12.1
+    # via matplotlib
+dask==2024.7.0
+    # via napari (pyproject.toml)
+debugpy==1.8.2
+    # via ipykernel
+decorator==5.1.1
+    # via ipython
+distlib==0.3.8
+    # via virtualenv
+docstring-parser==0.16
+    # via
+    #   napari (pyproject.toml)
+    #   magicgui
+docutils==0.21.2
+    # via
+    #   myst-parser
+    #   pydata-sphinx-theme
+    #   sphinx
+    #   sphinx-tabs
+executing==2.0.1
+    # via stack-data
+fasteners==0.19
+    # via zarr
+fastjsonschema==2.20.0
+    # via nbformat
+filelock==3.15.4
+    # via
+    #   torch
+    #   triton
+    #   virtualenv
+flexcache==0.3
+    # via pint
+flexparser==0.3.1
+    # via pint
+fonttools==4.53.1
+    # via matplotlib
+freetype-py==2.4.0
+    # via vispy
+fsspec==2024.6.1
+    # via
+    #   napari (pyproject.toml)
+    #   dask
+    #   torch
+greenlet==3.0.3
+    # via sqlalchemy
+h11==0.14.0
+    # via uvicorn
+heapdict==1.0.1
+    # via cachey
+hsluv==5.0.4
+    # via vispy
+hypothesis==6.108.2
+    # via napari (pyproject.toml)
+idna==3.7
+    # via
+    #   anyio
+    #   requests
+imageio==2.34.2
+    # via
+    #   napari (pyproject.toml)
+    #   napari-svg
+    #   scikit-image
+imageio-ffmpeg==0.5.1
+    # via -r ../napari-docs/requirements.txt
+imagesize==1.4.1
+    # via sphinx
+importlib-metadata==8.0.0
+    # via
+    #   dask
+    #   jupyter-cache
+    #   myst-nb
+in-n-out==0.2.1
+    # via app-model
+iniconfig==2.0.0
+    # via pytest
+ipykernel==6.29.5
+    # via
+    #   myst-nb
+    #   napari-console
+    #   qtconsole
+ipython==8.26.0
+    # via
+    #   napari (pyproject.toml)
+    #   ipykernel
+    #   myst-nb
+    #   napari-console
+jedi==0.19.1
+    # via ipython
+jinja2==3.1.4
+    # via
+    #   myst-parser
+    #   sphinx
+    #   torch
+joblib==1.4.2
+    # via
+    #   nilearn
+    #   scikit-learn
+jsonschema==4.23.0
+    # via
+    #   napari (pyproject.toml)
+    #   nbformat
+jsonschema-specifications==2023.12.1
+    # via jsonschema
+jupyter-cache==1.0.0
+    # via myst-nb
+jupyter-client==8.6.2
+    # via
+    #   ipykernel
+    #   nbclient
+    #   qtconsole
+jupyter-core==5.7.2
+    # via
+    #   ipykernel
+    #   jupyter-client
+    #   nbclient
+    #   nbformat
+    #   qtconsole
+kiwisolver==1.4.5
+    # via
+    #   matplotlib
+    #   vispy
+lazy-loader==0.4
+    # via
+    #   napari (pyproject.toml)
+    #   scikit-image
+llvmlite==0.43.0
+    # via numba
+locket==1.0.0
+    # via partd
+lxml==5.2.2
+    # via
+    #   napari (pyproject.toml)
+    #   lxml-html-clean
+    #   nilearn
+lxml-html-clean==0.1.1
+    # via
+    #   -r ../napari-docs/requirements.txt
+    #   lxml
+magicgui==0.8.3
+    # via napari (pyproject.toml)
+markdown-it-py==3.0.0
+    # via
+    #   mdit-py-plugins
+    #   myst-parser
+    #   rich
+markupsafe==2.1.5
+    # via jinja2
+matplotlib==3.9.1
+    # via
+    #   -r ../napari-docs/requirements.txt
+    #   napari (pyproject.toml)
+matplotlib-inline==0.1.7
+    # via
+    #   ipykernel
+    #   ipython
+mdit-py-plugins==0.4.1
+    # via myst-parser
+mdurl==0.1.2
+    # via markdown-it-py
+ml-dtypes==0.4.0
+    # via tensorstore
+mouseinfo==0.1.3
+    # via pyautogui
+mpmath==1.3.0
+    # via sympy
+myst-nb==1.1.1
+    # via -r ../napari-docs/requirements.txt
+myst-parser==3.0.1
+    # via myst-nb
+napari-console==0.0.9
+    # via napari (pyproject.toml)
+napari-plugin-engine==0.2.0
+    # via napari (pyproject.toml)
+napari-plugin-manager==0.1.0a2
+    # via napari (pyproject.toml)
+napari-sphinx-theme==0.5.0
+    # via -r ../napari-docs/requirements.txt
+napari-svg==0.2.0
+    # via napari (pyproject.toml)
+nbclient==0.10.0
+    # via
+    #   jupyter-cache
+    #   myst-nb
+nbformat==5.10.4
+    # via
+    #   jupyter-cache
+    #   myst-nb
+    #   nbclient
+nest-asyncio==1.6.0
+    # via ipykernel
+networkx==3.3
+    # via
+    #   scikit-image
+    #   torch
+nibabel==5.2.1
+    # via nilearn
+nilearn==0.10.4
+    # via -r resources/constraints/version_denylist_examples.txt
+npe2==0.7.6
+    # via
+    #   napari (pyproject.toml)
+    #   napari-plugin-manager
+numba==0.60.0
+    # via napari (pyproject.toml)
+numcodecs==0.13.0
+    # via zarr
+numpy==1.23.5
+    # via
+    #   -r resources/constraints/version_denylist_examples.txt
+    #   napari (pyproject.toml)
+    #   contourpy
+    #   dask
+    #   imageio
+    #   matplotlib
+    #   ml-dtypes
+    #   napari-svg
+    #   nibabel
+    #   nilearn
+    #   numba
+    #   numcodecs
+    #   pandas
+    #   scikit-image
+    #   scikit-learn
+    #   scipy
+    #   tensorstore
+    #   tifffile
+    #   triangle
+    #   vispy
+    #   xarray
+    #   zarr
+numpydoc==1.7.0
+    # via napari (pyproject.toml)
+nvidia-cublas-cu12==12.1.3.1
+    # via
+    #   nvidia-cudnn-cu12
+    #   nvidia-cusolver-cu12
+    #   torch
+nvidia-cuda-cupti-cu12==12.1.105
+    # via torch
+nvidia-cuda-nvrtc-cu12==12.1.105
+    # via torch
+nvidia-cuda-runtime-cu12==12.1.105
+    # via torch
+nvidia-cudnn-cu12==8.9.2.26
+    # via torch
+nvidia-cufft-cu12==11.0.2.54
+    # via torch
+nvidia-curand-cu12==10.3.2.106
+    # via torch
+nvidia-cusolver-cu12==11.4.5.107
+    # via torch
+nvidia-cusparse-cu12==12.1.0.106
+    # via
+    #   nvidia-cusolver-cu12
+    #   torch
+nvidia-nccl-cu12==2.20.5
+    # via torch
+nvidia-nvjitlink-cu12==12.5.82
+    # via
+    #   nvidia-cusolver-cu12
+    #   nvidia-cusparse-cu12
+nvidia-nvtx-cu12==12.1.105
+    # via torch
+packaging==24.1
+    # via
+    #   -r resources/constraints/version_denylist_examples.txt
+    #   build
+    #   dask
+    #   ipykernel
+    #   lazy-loader
+    #   matplotlib
+    #   napari-sphinx-theme
+    #   nibabel
+    #   nilearn
+    #   pooch
+    #   pydata-sphinx-theme
+    #   pytest
+    #   qtconsole
+    #   qtpy
+    #   scikit-image
+    #   sphinx
+    #   vispy
+    #   xarray
+pandas==2.2.2
+    # via
+    #   napari (pyproject.toml)
+    #   nilearn
+    #   xarray
+parso==0.8.4
+    # via jedi
+partd==1.4.2
+    # via dask
+pexpect==4.9.0
+    # via ipython
+pillow==10.4.0
+    # via
+    #   napari (pyproject.toml)
+    #   imageio
+    #   matplotlib
+    #   pyscreeze
+    #   scikit-image
+    #   sphinx-gallery
+pint==0.24.3
+    # via napari (pyproject.toml)
+pip==24.1.2
+    # via napari-plugin-manager
+platformdirs==4.2.2
+    # via
+    #   jupyter-core
+    #   pooch
+    #   virtualenv
+pluggy==1.5.0
+    # via
+    #   pytest
+    #   pytest-qt
+pooch==1.8.2
+    # via
+    #   napari (pyproject.toml)
+    #   scikit-image
+pretend==1.0.9
+    # via napari (pyproject.toml)
+prompt-toolkit==3.0.47
+    # via ipython
+psutil==6.0.0
+    # via
+    #   napari (pyproject.toml)
+    #   ipykernel
+psygnal==0.11.1
+    # via
+    #   napari (pyproject.toml)
+    #   app-model
+    #   magicgui
+    #   npe2
+ptyprocess==0.7.0
+    # via pexpect
+pure-eval==0.2.2
+    # via stack-data
+pyautogui==0.9.54
+    # via napari (pyproject.toml)
+pyconify==0.1.6
+    # via superqt
+pydantic==1.10.17
+    # via
+    #   -r resources/constraints/pydantic_le_2.txt
+    #   napari (pyproject.toml)
+    #   app-model
+    #   npe2
+    #   pydantic-compat
+pydantic-compat==0.1.2
+    # via app-model
+pydata-sphinx-theme==0.15.4
+    # via napari-sphinx-theme
+pygetwindow==0.0.9
+    # via pyautogui
+pygments==2.18.0
+    # via
+    #   napari (pyproject.toml)
+    #   accessible-pygments
+    #   ipython
+    #   pydata-sphinx-theme
+    #   qtconsole
+    #   rich
+    #   sphinx
+    #   sphinx-tabs
+    #   superqt
+pymsgbox==1.0.9
+    # via pyautogui
+pyopengl==3.1.6
+    # via
+    #   -r resources/constraints/version_denylist.txt
+    #   napari (pyproject.toml)
+pyparsing==3.1.2
+    # via matplotlib
+pyperclip==1.9.0
+    # via mouseinfo
+pyproject-hooks==1.1.0
+    # via build
+pyqt5==5.15.11
+    # via napari (pyproject.toml)
+pyqt5-qt5==5.15.14
+    # via pyqt5
+pyqt5-sip==12.15.0
+    # via pyqt5
+pyqt6==6.7.1
+    # via napari (pyproject.toml)
+pyqt6-qt6==6.7.2
+    # via pyqt6
+pyqt6-sip==13.8.0
+    # via pyqt6
+pyrect==0.2.0
+    # via pygetwindow
+pyscreeze==0.1.30
+    # via pyautogui
+pyside6==6.4.2
+    # via
+    #   -r resources/constraints/version_denylist.txt
+    #   napari (pyproject.toml)
+pyside6-addons==6.4.2
+    # via pyside6
+pyside6-essentials==6.4.2
+    # via
+    #   pyside6
+    #   pyside6-addons
+pytest==8.2.2
+    # via
+    #   napari (pyproject.toml)
+    #   pytest-cov
+    #   pytest-json-report
+    #   pytest-metadata
+    #   pytest-pretty
+    #   pytest-qt
+pytest-cov==5.0.0
+    # via -r resources/constraints/version_denylist.txt
+pytest-json-report==1.5.0
+    # via -r resources/constraints/version_denylist.txt
+pytest-metadata==3.1.1
+    # via pytest-json-report
+pytest-pretty==1.2.0
+    # via napari (pyproject.toml)
+pytest-qt==4.4.0
+    # via napari (pyproject.toml)
+python-dateutil==2.9.0.post0
+    # via
+    #   jupyter-client
+    #   matplotlib
+    #   pandas
+python3-xlib==0.15
+    # via
+    #   mouseinfo
+    #   pyautogui
+pytweening==1.2.0
+    # via pyautogui
+pytz==2024.1
+    # via pandas
+pyyaml==6.0.1
+    # via
+    #   napari (pyproject.toml)
+    #   dask
+    #   jupyter-cache
+    #   myst-nb
+    #   myst-parser
+    #   npe2
+    #   sphinx-external-toc
+pyzmq==26.0.3
+    # via
+    #   ipykernel
+    #   jupyter-client
+    #   qtconsole
+qtconsole==5.5.2
+    # via
+    #   napari (pyproject.toml)
+    #   napari-console
+qtpy==2.4.1
+    # via
+    #   napari (pyproject.toml)
+    #   magicgui
+    #   napari-console
+    #   napari-plugin-manager
+    #   qtconsole
+    #   superqt
+referencing==0.35.1
+    # via
+    #   jsonschema
+    #   jsonschema-specifications
+requests==2.32.3
+    # via
+    #   nilearn
+    #   pooch
+    #   pyconify
+    #   sphinx
+rich==13.7.1
+    # via
+    #   napari (pyproject.toml)
+    #   npe2
+    #   pytest-pretty
+    #   typer
+rpds-py==0.19.0
+    # via
+    #   jsonschema
+    #   referencing
+scikit-image==0.24.0
+    # via napari (pyproject.toml)
+scikit-learn==1.5.1
+    # via nilearn
+scipy==1.14.0
+    # via
+    #   napari (pyproject.toml)
+    #   nilearn
+    #   scikit-image
+    #   scikit-learn
+setuptools==71.0.3
+    # via imageio-ffmpeg
+shellingham==1.5.4
+    # via typer
+shiboken6==6.4.2
+    # via
+    #   pyside6
+    #   pyside6-addons
+    #   pyside6-essentials
+six==1.16.0
+    # via
+    #   asttokens
+    #   python-dateutil
+sniffio==1.3.1
+    # via anyio
+snowballstemmer==2.2.0
+    # via sphinx
+sortedcontainers==2.4.0
+    # via hypothesis
+soupsieve==2.5
+    # via beautifulsoup4
+sphinx==7.4.6
+    # via
+    #   myst-nb
+    #   myst-parser
+    #   numpydoc
+    #   pydata-sphinx-theme
+    #   sphinx-autobuild
+    #   sphinx-autodoc-typehints
+    #   sphinx-copybutton
+    #   sphinx-design
+    #   sphinx-external-toc
+    #   sphinx-favicon
+    #   sphinx-gallery
+    #   sphinx-tabs
+    #   sphinx-tags
+sphinx-autobuild==2024.4.16
+    # via -r ../napari-docs/requirements.txt
+sphinx-autodoc-typehints==1.12.0
+    # via -r ../napari-docs/requirements.txt
+sphinx-copybutton==0.5.2
+    # via -r ../napari-docs/requirements.txt
+sphinx-design==0.6.0
+    # via -r ../napari-docs/requirements.txt
+sphinx-external-toc==1.0.1
+    # via -r ../napari-docs/requirements.txt
+sphinx-favicon==1.0.1
+    # via -r ../napari-docs/requirements.txt
+sphinx-gallery==0.16.0
+    # via -r ../napari-docs/requirements.txt
+sphinx-tabs==3.4.5
+    # via -r ../napari-docs/requirements.txt
+sphinx-tags==0.4
+    # via -r ../napari-docs/requirements.txt
+sphinxcontrib-applehelp==1.0.8
+    # via sphinx
+sphinxcontrib-devhelp==1.0.6
+    # via sphinx
+sphinxcontrib-htmlhelp==2.0.5
+    # via sphinx
+sphinxcontrib-jsmath==1.0.1
+    # via sphinx
+sphinxcontrib-qthelp==1.0.7
+    # via sphinx
+sphinxcontrib-serializinghtml==1.1.10
+    # via sphinx
+sqlalchemy==2.0.31
+    # via jupyter-cache
+stack-data==0.6.3
+    # via ipython
+starlette==0.37.2
+    # via sphinx-autobuild
+superqt==0.6.7
+    # via
+    #   napari (pyproject.toml)
+    #   magicgui
+    #   napari-plugin-manager
+sympy==1.13.1
+    # via torch
+tabulate==0.9.0
+    # via
+    #   jupyter-cache
+    #   numpydoc
+tensorstore==0.1.63
+    # via
+    #   -r resources/constraints/version_denylist.txt
+    #   napari (pyproject.toml)
+threadpoolctl==3.5.0
+    # via scikit-learn
+tifffile==2024.7.2
+    # via
+    #   napari (pyproject.toml)
+    #   scikit-image
+tomli-w==1.0.0
+    # via npe2
+toolz==0.12.1
+    # via
+    #   napari (pyproject.toml)
+    #   dask
+    #   partd
+torch==2.3.1
+    # via napari (pyproject.toml)
+tornado==6.4.1
+    # via
+    #   ipykernel
+    #   jupyter-client
+tqdm==4.66.4
+    # via napari (pyproject.toml)
+traitlets==5.14.3
+    # via
+    #   comm
+    #   ipykernel
+    #   ipython
+    #   jupyter-client
+    #   jupyter-core
+    #   matplotlib-inline
+    #   nbclient
+    #   nbformat
+    #   qtconsole
+triangle==20230923
+    # via napari (pyproject.toml)
+triton==2.3.1
+    # via torch
+typer==0.12.3
+    # via npe2
+typing-extensions==4.12.2
+    # via
+    #   napari (pyproject.toml)
+    #   app-model
+    #   flexcache
+    #   flexparser
+    #   ipython
+    #   magicgui
+    #   myst-nb
+    #   pint
+    #   pydantic
+    #   pydata-sphinx-theme
+    #   sqlalchemy
+    #   superqt
+    #   torch
+    #   typer
+tzdata==2024.1
+    # via pandas
+urllib3==2.2.2
+    # via requests
+uvicorn==0.30.1
+    # via sphinx-autobuild
+virtualenv==20.26.3
+    # via napari (pyproject.toml)
+vispy==0.14.3
+    # via
+    #   napari (pyproject.toml)
+    #   napari-svg
+watchfiles==0.22.0
+    # via sphinx-autobuild
+wcwidth==0.2.13
+    # via prompt-toolkit
+websockets==12.0
+    # via sphinx-autobuild
+wrapt==1.16.0
+    # via napari (pyproject.toml)
+xarray==2024.6.0
+    # via napari (pyproject.toml)
+zarr==2.18.2
+    # via
+    #   -r resources/constraints/version_denylist.txt
+    #   napari (pyproject.toml)
+zipp==3.19.2
+    # via importlib-metadata
diff --git a/resources/constraints/constraints_py3.11_pydantic_1.txt b/resources/constraints/constraints_py3.11_pydantic_1.txt
index 7d98f178877..712b86a71c5 100644
--- a/resources/constraints/constraints_py3.11_pydantic_1.txt
+++ b/resources/constraints/constraints_py3.11_pydantic_1.txt
@@ -1,60 +1,59 @@
 # This file was autogenerated by uv via the following command:
-#    uv pip compile --python-version 3.11 --output-file napari_repo/resources/constraints/constraints_py3.11_pydantic_1.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt napari_repo/resources/constraints/pydantic_le_2.txt --extra pyqt5 --extra pyqt6_experimental --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
-alabaster==0.7.16
+#    uv pip compile --python-version 3.11 --output-file napari_repo/resources/constraints/constraints_py3.11_pydantic_1.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt napari_repo/resources/constraints/pydantic_le_2.txt --extra pyqt5 --extra pyqt6 --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
+alabaster==1.0.0
     # via sphinx
-app-model==0.2.7
+app-model==0.3.0
     # via napari (napari_repo/pyproject.toml)
 appdirs==1.4.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
-    #   pint
 asciitree==0.3.3
     # via zarr
 asttokens==2.4.1
     # via stack-data
-attrs==23.2.0
+attrs==24.2.0
     # via
     #   hypothesis
     #   jsonschema
     #   referencing
-babel==2.15.0
+babel==2.16.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   sphinx
-build==1.2.1
+build==1.2.2.post1
     # via npe2
 cachey==0.2.1
     # via napari (napari_repo/pyproject.toml)
-certifi==2024.7.4
+certifi==2024.8.30
     # via
     #   napari (napari_repo/pyproject.toml)
     #   requests
-charset-normalizer==3.3.2
+charset-normalizer==3.4.0
     # via requests
 click==8.1.7
     # via
     #   dask
     #   typer
-cloudpickle==3.0.0
+cloudpickle==3.1.0
     # via dask
 comm==0.2.2
     # via ipykernel
-contourpy==1.2.1
+contourpy==1.3.1
     # via matplotlib
-coverage==7.5.4
+coverage==7.6.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
 cycler==0.12.1
     # via matplotlib
-dask==2024.6.2
+dask==2024.11.2
     # via napari (napari_repo/pyproject.toml)
-debugpy==1.8.1
+debugpy==1.8.8
     # via ipykernel
 decorator==5.1.1
     # via ipython
-distlib==0.3.8
+distlib==0.3.9
     # via virtualenv
 docstring-parser==0.16
     # via
@@ -62,24 +61,24 @@ docstring-parser==0.16
     #   magicgui
 docutils==0.21.2
     # via sphinx
-executing==2.0.1
+executing==2.1.0
     # via stack-data
 fasteners==0.19
     # via zarr
-filelock==3.15.4
+filelock==3.16.1
     # via
     #   torch
     #   triton
     #   virtualenv
 flexcache==0.3
     # via pint
-flexparser==0.3.1
+flexparser==0.4
     # via pint
-fonttools==4.53.0
+fonttools==4.55.0
     # via matplotlib
-freetype-py==2.4.0
+freetype-py==2.5.1
     # via vispy
-fsspec==2024.6.0
+fsspec==2024.10.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
@@ -88,43 +87,44 @@ heapdict==1.0.1
     # via cachey
 hsluv==5.0.4
     # via vispy
-hypothesis==6.103.4
+hypothesis==6.119.3
     # via napari (napari_repo/pyproject.toml)
-idna==3.7
+idna==3.10
     # via requests
-imageio==2.34.1
+imageio==2.36.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-svg
     #   scikit-image
 imagesize==1.4.1
     # via sphinx
-importlib-metadata==7.2.1
+importlib-metadata==8.5.0
     # via dask
 in-n-out==0.2.1
     # via app-model
 iniconfig==2.0.0
     # via pytest
-ipykernel==6.29.4
+ipykernel==6.29.5
     # via
+    #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari-console
     #   qtconsole
-ipython==8.25.0
+ipython==8.29.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
     #   napari-console
-jedi==0.19.1
+jedi==0.19.2
     # via ipython
 jinja2==3.1.4
     # via
     #   sphinx
     #   torch
-jsonschema==4.22.0
+jsonschema==4.23.0
     # via napari (napari_repo/pyproject.toml)
-jsonschema-specifications==2023.12.1
+jsonschema-specifications==2024.10.1
     # via jsonschema
-jupyter-client==8.6.2
+jupyter-client==8.6.3
     # via
     #   ipykernel
     #   qtconsole
@@ -133,7 +133,7 @@ jupyter-core==5.7.2
     #   ipykernel
     #   jupyter-client
     #   qtconsole
-kiwisolver==1.4.5
+kiwisolver==1.4.7
     # via
     #   matplotlib
     #   vispy
@@ -145,19 +145,19 @@ llvmlite==0.43.0
     # via numba
 locket==1.0.0
     # via partd
-lxml==5.2.2
+lxml==5.3.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   lxml-html-clean
-lxml-html-clean==0.1.1
+lxml-html-clean==0.4.1
     # via lxml
-magicgui==0.8.3
+magicgui==0.9.1
     # via napari (napari_repo/pyproject.toml)
 markdown-it-py==3.0.0
     # via rich
-markupsafe==2.1.5
+markupsafe==3.0.2
     # via jinja2
-matplotlib==3.9.0
+matplotlib==3.9.2
     # via napari (napari_repo/pyproject.toml)
 matplotlib-inline==0.1.7
     # via
@@ -165,37 +165,35 @@ matplotlib-inline==0.1.7
     #   ipython
 mdurl==0.1.2
     # via markdown-it-py
-ml-dtypes==0.4.0
+ml-dtypes==0.5.0
     # via tensorstore
 mouseinfo==0.1.3
     # via pyautogui
 mpmath==1.3.0
     # via sympy
-napari-console==0.0.9
+napari-console==0.1.1
     # via napari (napari_repo/pyproject.toml)
 napari-plugin-engine==0.2.0
-    # via
-    #   napari (napari_repo/pyproject.toml)
-    #   napari-svg
-napari-plugin-manager==0.1.0a2
     # via napari (napari_repo/pyproject.toml)
-napari-svg==0.1.10
+napari-plugin-manager==0.1.3
+    # via napari (napari_repo/pyproject.toml)
+napari-svg==0.2.0
     # via napari (napari_repo/pyproject.toml)
 nest-asyncio==1.6.0
     # via ipykernel
-networkx==3.3
+networkx==3.4.2
     # via
     #   scikit-image
     #   torch
-npe2==0.7.6
+npe2==0.7.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-plugin-manager
 numba==0.60.0
     # via napari (napari_repo/pyproject.toml)
-numcodecs==0.12.1
+numcodecs==0.14.0
     # via zarr
-numpy==2.0.0
+numpy==2.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   contourpy
@@ -215,40 +213,41 @@ numpy==2.0.0
     #   vispy
     #   xarray
     #   zarr
-numpydoc==1.7.0
+numpydoc==1.8.0
     # via napari (napari_repo/pyproject.toml)
-nvidia-cublas-cu12==12.1.3.1
+nvidia-cublas-cu12==12.4.5.8
     # via
     #   nvidia-cudnn-cu12
     #   nvidia-cusolver-cu12
     #   torch
-nvidia-cuda-cupti-cu12==12.1.105
+nvidia-cuda-cupti-cu12==12.4.127
     # via torch
-nvidia-cuda-nvrtc-cu12==12.1.105
+nvidia-cuda-nvrtc-cu12==12.4.127
     # via torch
-nvidia-cuda-runtime-cu12==12.1.105
+nvidia-cuda-runtime-cu12==12.4.127
     # via torch
-nvidia-cudnn-cu12==8.9.2.26
+nvidia-cudnn-cu12==9.1.0.70
     # via torch
-nvidia-cufft-cu12==11.0.2.54
+nvidia-cufft-cu12==11.2.1.3
     # via torch
-nvidia-curand-cu12==10.3.2.106
+nvidia-curand-cu12==10.3.5.147
     # via torch
-nvidia-cusolver-cu12==11.4.5.107
+nvidia-cusolver-cu12==11.6.1.9
     # via torch
-nvidia-cusparse-cu12==12.1.0.106
+nvidia-cusparse-cu12==12.3.1.170
     # via
     #   nvidia-cusolver-cu12
     #   torch
-nvidia-nccl-cu12==2.20.5
+nvidia-nccl-cu12==2.21.5
     # via torch
-nvidia-nvjitlink-cu12==12.5.40
+nvidia-nvjitlink-cu12==12.4.127
     # via
     #   nvidia-cusolver-cu12
     #   nvidia-cusparse-cu12
-nvidia-nvtx-cu12==12.1.105
+    #   torch
+nvidia-nvtx-cu12==12.4.127
     # via torch
-packaging==24.1
+packaging==24.2
     # via
     #   build
     #   dask
@@ -263,7 +262,7 @@ packaging==24.1
     #   sphinx
     #   vispy
     #   xarray
-pandas==2.2.2
+pandas==2.2.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   xarray
@@ -273,20 +272,21 @@ partd==1.4.2
     # via dask
 pexpect==4.9.0
     # via ipython
-pillow==10.3.0
+pillow==11.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   imageio
     #   matplotlib
     #   pyscreeze
     #   scikit-image
-pint==0.24
+pint==0.24.4
     # via napari (napari_repo/pyproject.toml)
-pip==24.1
+pip==24.3.1
     # via napari-plugin-manager
-platformdirs==4.2.2
+platformdirs==4.3.6
     # via
     #   jupyter-core
+    #   pint
     #   pooch
     #   virtualenv
 pluggy==1.5.0
@@ -299,9 +299,9 @@ pooch==1.8.2
     #   scikit-image
 pretend==1.0.9
     # via napari (napari_repo/pyproject.toml)
-prompt-toolkit==3.0.47
+prompt-toolkit==3.0.48
     # via ipython
-psutil==6.0.0
+psutil==6.1.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
@@ -313,13 +313,13 @@ psygnal==0.11.1
     #   npe2
 ptyprocess==0.7.0
     # via pexpect
-pure-eval==0.2.2
+pure-eval==0.2.3
     # via stack-data
 pyautogui==0.9.54
     # via napari (napari_repo/pyproject.toml)
 pyconify==0.1.6
     # via superqt
-pydantic==1.10.17
+pydantic==1.10.19
     # via
     #   -r napari_repo/resources/constraints/pydantic_le_2.txt
     #   napari (napari_repo/pyproject.toml)
@@ -344,27 +344,27 @@ pyopengl==3.1.6
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-pyparsing==3.1.2
+pyparsing==3.2.0
     # via matplotlib
 pyperclip==1.9.0
     # via mouseinfo
-pyproject-hooks==1.1.0
+pyproject-hooks==1.2.0
     # via build
-pyqt5==5.15.10
+pyqt5==5.15.11
     # via napari (napari_repo/pyproject.toml)
-pyqt5-qt5==5.15.2
+pyqt5-qt5==5.15.15
     # via pyqt5
-pyqt5-sip==12.13.0
+pyqt5-sip==12.15.0
     # via pyqt5
-pyqt6==6.7.0
+pyqt6==6.7.1
     # via napari (napari_repo/pyproject.toml)
-pyqt6-qt6==6.7.2
+pyqt6-qt6==6.7.3
     # via pyqt6
-pyqt6-sip==13.6.0
+pyqt6-sip==13.8.0
     # via pyqt6
 pyrect==0.2.0
     # via pygetwindow
-pyscreeze==0.1.30
+pyscreeze==1.0.1
     # via pyautogui
 pyside6==6.4.2
     # via
@@ -376,7 +376,7 @@ pyside6-essentials==6.4.2
     # via
     #   pyside6
     #   pyside6-addons
-pytest==8.2.2
+pytest==8.3.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
@@ -384,7 +384,7 @@ pytest==8.2.2
     #   pytest-metadata
     #   pytest-pretty
     #   pytest-qt
-pytest-cov==5.0.0
+pytest-cov==6.0.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
 pytest-json-report==1.5.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
@@ -405,23 +405,22 @@ python3-xlib==0.15
     #   pyautogui
 pytweening==1.2.0
     # via pyautogui
-pytz==2024.1
+pytz==2024.2
     # via pandas
-pyyaml==6.0.1
+pyyaml==6.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   npe2
-pyzmq==26.0.3
+pyzmq==26.2.0
     # via
     #   ipykernel
     #   jupyter-client
-    #   qtconsole
-qtconsole==5.5.2
+qtconsole==5.6.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-console
-qtpy==2.4.1
+qtpy==2.4.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
@@ -438,19 +437,19 @@ requests==2.32.3
     #   pooch
     #   pyconify
     #   sphinx
-rich==13.7.1
+rich==13.9.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
     #   pytest-pretty
     #   typer
-rpds-py==0.18.1
+rpds-py==0.21.0
     # via
     #   jsonschema
     #   referencing
 scikit-image==0.24.0
     # via napari (napari_repo/pyproject.toml)
-scipy==1.13.1
+scipy==1.14.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
@@ -469,19 +468,19 @@ snowballstemmer==2.2.0
     # via sphinx
 sortedcontainers==2.4.0
     # via hypothesis
-sphinx==7.3.7
+sphinx==8.1.3
     # via numpydoc
-sphinxcontrib-applehelp==1.0.8
+sphinxcontrib-applehelp==2.0.0
     # via sphinx
-sphinxcontrib-devhelp==1.0.6
+sphinxcontrib-devhelp==2.0.0
     # via sphinx
-sphinxcontrib-htmlhelp==2.0.5
+sphinxcontrib-htmlhelp==2.1.0
     # via sphinx
 sphinxcontrib-jsmath==1.0.1
     # via sphinx
-sphinxcontrib-qthelp==1.0.7
+sphinxcontrib-qthelp==2.0.0
     # via sphinx
-sphinxcontrib-serializinghtml==1.1.10
+sphinxcontrib-serializinghtml==2.0.0
     # via sphinx
 stack-data==0.6.3
     # via ipython
@@ -490,32 +489,34 @@ superqt==0.6.7
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
     #   napari-plugin-manager
-sympy==1.12.1
+sympy==1.13.1
     # via torch
 tabulate==0.9.0
     # via numpydoc
-tensorstore==0.1.63
+tensorstore==0.1.68
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-tifffile==2024.6.18
+tifffile==2024.9.20
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
-tomli-w==1.0.0
+tomli==2.1.0
+    # via coverage
+tomli-w==1.1.0
     # via npe2
-toolz==0.12.1
+toolz==1.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   partd
-torch==2.3.1
+torch==2.5.1
     # via napari (napari_repo/pyproject.toml)
 tornado==6.4.1
     # via
     #   ipykernel
     #   jupyter-client
-tqdm==4.66.4
+tqdm==4.67.0
     # via napari (napari_repo/pyproject.toml)
 traitlets==5.14.3
     # via
@@ -528,9 +529,9 @@ traitlets==5.14.3
     #   qtconsole
 triangle==20230923
     # via napari (napari_repo/pyproject.toml)
-triton==2.3.1
+triton==3.1.0
     # via torch
-typer==0.12.3
+typer==0.13.0
     # via npe2
 typing-extensions==4.12.2
     # via
@@ -545,11 +546,11 @@ typing-extensions==4.12.2
     #   superqt
     #   torch
     #   typer
-tzdata==2024.1
+tzdata==2024.2
     # via pandas
-urllib3==2.2.2
+urllib3==2.2.3
     # via requests
-virtualenv==20.26.3
+virtualenv==20.27.1
     # via napari (napari_repo/pyproject.toml)
 vispy==0.14.3
     # via
@@ -559,9 +560,9 @@ wcwidth==0.2.13
     # via prompt-toolkit
 wrapt==1.16.0
     # via napari (napari_repo/pyproject.toml)
-xarray==2024.6.0
+xarray==2024.10.0
     # via napari (napari_repo/pyproject.toml)
-zarr==2.18.2
+zarr==2.18.3
     # via napari (napari_repo/pyproject.toml)
-zipp==3.19.2
+zipp==3.21.0
     # via importlib-metadata
diff --git a/resources/constraints/constraints_py3.11_macos_arm.txt b/resources/constraints/constraints_py3.11_windows.txt
similarity index 79%
rename from resources/constraints/constraints_py3.11_macos_arm.txt
rename to resources/constraints/constraints_py3.11_windows.txt
index d3237c8addb..0a7719b9656 100644
--- a/resources/constraints/constraints_py3.11_macos_arm.txt
+++ b/resources/constraints/constraints_py3.11_windows.txt
@@ -1,64 +1,69 @@
 # This file was autogenerated by uv via the following command:
-#    uv pip compile --python-platform aarch64-apple-darwin --python-version 3.11 --output-file napari_repo/resources/constraints/constraints_py3.11_macos_arm.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt --extra pyqt5 --extra pyqt6_experimental --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
-alabaster==0.7.16
+#    uv pip compile --python-platform windows --python-version 3.11 --output-file napari_repo/resources/constraints/constraints_py3.11_windows.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt --extra pyqt5 --extra pyqt6 --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
+alabaster==1.0.0
     # via sphinx
 annotated-types==0.7.0
     # via pydantic
-app-model==0.2.7
+app-model==0.3.0
     # via napari (napari_repo/pyproject.toml)
 appdirs==1.4.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
-    #   pint
-appnope==0.1.4
-    # via ipykernel
 asciitree==0.3.3
     # via zarr
 asttokens==2.4.1
     # via stack-data
-attrs==23.2.0
+attrs==24.2.0
     # via
     #   hypothesis
     #   jsonschema
     #   referencing
-babel==2.15.0
+babel==2.16.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   sphinx
-build==1.2.1
+build==1.2.2.post1
     # via npe2
 cachey==0.2.1
     # via napari (napari_repo/pyproject.toml)
-certifi==2024.7.4
+certifi==2024.8.30
     # via
     #   napari (napari_repo/pyproject.toml)
     #   requests
-charset-normalizer==3.3.2
+charset-normalizer==3.4.0
     # via requests
 click==8.1.7
     # via
     #   dask
     #   typer
-cloudpickle==3.0.0
+cloudpickle==3.1.0
     # via dask
+colorama==0.4.6
+    # via
+    #   build
+    #   click
+    #   ipython
+    #   pytest
+    #   sphinx
+    #   tqdm
 comm==0.2.2
     # via ipykernel
-contourpy==1.2.1
+contourpy==1.3.1
     # via matplotlib
-coverage==7.5.4
+coverage==7.6.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
 cycler==0.12.1
     # via matplotlib
-dask==2024.6.2
+dask==2024.11.2
     # via napari (napari_repo/pyproject.toml)
-debugpy==1.8.1
+debugpy==1.8.8
     # via ipykernel
 decorator==5.1.1
     # via ipython
-distlib==0.3.8
+distlib==0.3.9
     # via virtualenv
 docstring-parser==0.16
     # via
@@ -66,23 +71,23 @@ docstring-parser==0.16
     #   magicgui
 docutils==0.21.2
     # via sphinx
-executing==2.0.1
+executing==2.1.0
     # via stack-data
 fasteners==0.19
     # via zarr
-filelock==3.15.4
+filelock==3.16.1
     # via
     #   torch
     #   virtualenv
 flexcache==0.3
     # via pint
-flexparser==0.3.1
+flexparser==0.4
     # via pint
-fonttools==4.53.0
+fonttools==4.55.0
     # via matplotlib
-freetype-py==2.4.0
+freetype-py==2.5.1
     # via vispy
-fsspec==2024.6.0
+fsspec==2024.10.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
@@ -91,43 +96,44 @@ heapdict==1.0.1
     # via cachey
 hsluv==5.0.4
     # via vispy
-hypothesis==6.103.4
+hypothesis==6.119.3
     # via napari (napari_repo/pyproject.toml)
-idna==3.7
+idna==3.10
     # via requests
-imageio==2.34.1
+imageio==2.36.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-svg
     #   scikit-image
 imagesize==1.4.1
     # via sphinx
-importlib-metadata==7.2.1
+importlib-metadata==8.5.0
     # via dask
 in-n-out==0.2.1
     # via app-model
 iniconfig==2.0.0
     # via pytest
-ipykernel==6.29.4
+ipykernel==6.29.5
     # via
+    #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari-console
     #   qtconsole
-ipython==8.25.0
+ipython==8.29.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
     #   napari-console
-jedi==0.19.1
+jedi==0.19.2
     # via ipython
 jinja2==3.1.4
     # via
     #   sphinx
     #   torch
-jsonschema==4.22.0
+jsonschema==4.23.0
     # via napari (napari_repo/pyproject.toml)
-jsonschema-specifications==2023.12.1
+jsonschema-specifications==2024.10.1
     # via jsonschema
-jupyter-client==8.6.2
+jupyter-client==8.6.3
     # via
     #   ipykernel
     #   qtconsole
@@ -136,7 +142,7 @@ jupyter-core==5.7.2
     #   ipykernel
     #   jupyter-client
     #   qtconsole
-kiwisolver==1.4.5
+kiwisolver==1.4.7
     # via
     #   matplotlib
     #   vispy
@@ -148,19 +154,19 @@ llvmlite==0.43.0
     # via numba
 locket==1.0.0
     # via partd
-lxml==5.2.2
+lxml==5.3.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   lxml-html-clean
-lxml-html-clean==0.1.1
+lxml-html-clean==0.4.1
     # via lxml
-magicgui==0.8.3
+magicgui==0.9.1
     # via napari (napari_repo/pyproject.toml)
 markdown-it-py==3.0.0
     # via rich
-markupsafe==2.1.5
+markupsafe==3.0.2
     # via jinja2
-matplotlib==3.9.0
+matplotlib==3.9.2
     # via napari (napari_repo/pyproject.toml)
 matplotlib-inline==0.1.7
     # via
@@ -168,37 +174,35 @@ matplotlib-inline==0.1.7
     #   ipython
 mdurl==0.1.2
     # via markdown-it-py
-ml-dtypes==0.4.0
+ml-dtypes==0.5.0
     # via tensorstore
 mouseinfo==0.1.3
     # via pyautogui
 mpmath==1.3.0
     # via sympy
-napari-console==0.0.9
+napari-console==0.1.1
     # via napari (napari_repo/pyproject.toml)
 napari-plugin-engine==0.2.0
-    # via
-    #   napari (napari_repo/pyproject.toml)
-    #   napari-svg
-napari-plugin-manager==0.1.0a2
     # via napari (napari_repo/pyproject.toml)
-napari-svg==0.1.10
+napari-plugin-manager==0.1.3
+    # via napari (napari_repo/pyproject.toml)
+napari-svg==0.2.0
     # via napari (napari_repo/pyproject.toml)
 nest-asyncio==1.6.0
     # via ipykernel
-networkx==3.3
+networkx==3.4.2
     # via
     #   scikit-image
     #   torch
-npe2==0.7.6
+npe2==0.7.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-plugin-manager
 numba==0.60.0
     # via napari (napari_repo/pyproject.toml)
-numcodecs==0.12.1
+numcodecs==0.14.0
     # via zarr
-numpy==2.0.0
+numpy==2.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   contourpy
@@ -214,12 +218,13 @@ numpy==2.0.0
     #   scipy
     #   tensorstore
     #   tifffile
+    #   triangle
     #   vispy
     #   xarray
     #   zarr
-numpydoc==1.7.0
+numpydoc==1.8.0
     # via napari (napari_repo/pyproject.toml)
-packaging==24.1
+packaging==24.2
     # via
     #   build
     #   dask
@@ -234,7 +239,7 @@ packaging==24.1
     #   sphinx
     #   vispy
     #   xarray
-pandas==2.2.2
+pandas==2.2.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   xarray
@@ -242,22 +247,21 @@ parso==0.8.4
     # via jedi
 partd==1.4.2
     # via dask
-pexpect==4.9.0
-    # via ipython
-pillow==10.3.0
+pillow==11.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   imageio
     #   matplotlib
     #   pyscreeze
     #   scikit-image
-pint==0.24
+pint==0.24.4
     # via napari (napari_repo/pyproject.toml)
-pip==24.1
+pip==24.3.1
     # via napari-plugin-manager
-platformdirs==4.2.2
+platformdirs==4.3.6
     # via
     #   jupyter-core
+    #   pint
     #   pooch
     #   virtualenv
 pluggy==1.5.0
@@ -270,9 +274,9 @@ pooch==1.8.2
     #   scikit-image
 pretend==1.0.9
     # via napari (napari_repo/pyproject.toml)
-prompt-toolkit==3.0.47
+prompt-toolkit==3.0.48
     # via ipython
-psutil==6.0.0
+psutil==6.1.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
@@ -282,15 +286,13 @@ psygnal==0.11.1
     #   app-model
     #   magicgui
     #   npe2
-ptyprocess==0.7.0
-    # via pexpect
-pure-eval==0.2.2
+pure-eval==0.2.3
     # via stack-data
 pyautogui==0.9.54
     # via napari (napari_repo/pyproject.toml)
 pyconify==0.1.6
     # via superqt
-pydantic==2.7.4
+pydantic==2.9.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   app-model
@@ -298,7 +300,7 @@ pydantic==2.7.4
     #   pydantic-compat
 pydantic-compat==0.1.2
     # via app-model
-pydantic-core==2.18.4
+pydantic-core==2.23.4
     # via pydantic
 pygetwindow==0.0.9
     # via pyautogui
@@ -312,40 +314,31 @@ pygments==2.18.0
     #   superqt
 pymsgbox==1.0.9
     # via pyautogui
-pyobjc-core==10.3.1
-    # via
-    #   pyautogui
-    #   pyobjc-framework-cocoa
-    #   pyobjc-framework-quartz
-pyobjc-framework-cocoa==10.3.1
-    # via pyobjc-framework-quartz
-pyobjc-framework-quartz==10.3.1
-    # via pyautogui
 pyopengl==3.1.6
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-pyparsing==3.1.2
+pyparsing==3.2.0
     # via matplotlib
 pyperclip==1.9.0
     # via mouseinfo
-pyproject-hooks==1.1.0
+pyproject-hooks==1.2.0
     # via build
-pyqt5==5.15.10
+pyqt5==5.15.11
     # via napari (napari_repo/pyproject.toml)
-pyqt5-qt5==5.15.14
+pyqt5-qt5==5.15.2
     # via pyqt5
-pyqt5-sip==12.13.0
+pyqt5-sip==12.15.0
     # via pyqt5
-pyqt6==6.7.0
+pyqt6==6.7.1
     # via napari (napari_repo/pyproject.toml)
-pyqt6-qt6==6.7.2
+pyqt6-qt6==6.7.3
     # via pyqt6
-pyqt6-sip==13.6.0
+pyqt6-sip==13.8.0
     # via pyqt6
 pyrect==0.2.0
     # via pygetwindow
-pyscreeze==0.1.30
+pyscreeze==1.0.1
     # via pyautogui
 pyside6==6.4.2
     # via
@@ -357,7 +350,7 @@ pyside6-essentials==6.4.2
     # via
     #   pyside6
     #   pyside6-addons
-pytest==8.2.2
+pytest==8.3.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
@@ -365,7 +358,7 @@ pytest==8.2.2
     #   pytest-metadata
     #   pytest-pretty
     #   pytest-qt
-pytest-cov==5.0.0
+pytest-cov==6.0.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
 pytest-json-report==1.5.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
@@ -382,23 +375,26 @@ python-dateutil==2.9.0.post0
     #   pandas
 pytweening==1.2.0
     # via pyautogui
-pytz==2024.1
+pytz==2024.2
     # via pandas
-pyyaml==6.0.1
+pywin32==308
+    # via
+    #   napari (napari_repo/pyproject.toml)
+    #   jupyter-core
+pyyaml==6.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   npe2
-pyzmq==26.0.3
+pyzmq==26.2.0
     # via
     #   ipykernel
     #   jupyter-client
-    #   qtconsole
-qtconsole==5.5.2
+qtconsole==5.6.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-console
-qtpy==2.4.1
+qtpy==2.4.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
@@ -415,21 +411,19 @@ requests==2.32.3
     #   pooch
     #   pyconify
     #   sphinx
-rich==13.7.1
+rich==13.9.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
     #   pytest-pretty
     #   typer
-rpds-py==0.18.1
+rpds-py==0.21.0
     # via
     #   jsonschema
     #   referencing
-rubicon-objc==0.4.9
-    # via mouseinfo
 scikit-image==0.24.0
     # via napari (napari_repo/pyproject.toml)
-scipy==1.13.1
+scipy==1.14.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
@@ -448,19 +442,19 @@ snowballstemmer==2.2.0
     # via sphinx
 sortedcontainers==2.4.0
     # via hypothesis
-sphinx==7.3.7
+sphinx==8.1.3
     # via numpydoc
-sphinxcontrib-applehelp==1.0.8
+sphinxcontrib-applehelp==2.0.0
     # via sphinx
-sphinxcontrib-devhelp==1.0.6
+sphinxcontrib-devhelp==2.0.0
     # via sphinx
-sphinxcontrib-htmlhelp==2.0.5
+sphinxcontrib-htmlhelp==2.1.0
     # via sphinx
 sphinxcontrib-jsmath==1.0.1
     # via sphinx
-sphinxcontrib-qthelp==1.0.7
+sphinxcontrib-qthelp==2.0.0
     # via sphinx
-sphinxcontrib-serializinghtml==1.1.10
+sphinxcontrib-serializinghtml==2.0.0
     # via sphinx
 stack-data==0.6.3
     # via ipython
@@ -469,32 +463,34 @@ superqt==0.6.7
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
     #   napari-plugin-manager
-sympy==1.12.1
+sympy==1.13.1
     # via torch
 tabulate==0.9.0
     # via numpydoc
-tensorstore==0.1.63
+tensorstore==0.1.68
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-tifffile==2024.6.18
+tifffile==2024.9.20
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
-tomli-w==1.0.0
+tomli==2.1.0
+    # via coverage
+tomli-w==1.1.0
     # via npe2
-toolz==0.12.1
+toolz==1.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   partd
-torch==2.3.1
+torch==2.5.1
     # via napari (napari_repo/pyproject.toml)
 tornado==6.4.1
     # via
     #   ipykernel
     #   jupyter-client
-tqdm==4.66.4
+tqdm==4.67.0
     # via napari (napari_repo/pyproject.toml)
 traitlets==5.14.3
     # via
@@ -505,7 +501,9 @@ traitlets==5.14.3
     #   jupyter-core
     #   matplotlib-inline
     #   qtconsole
-typer==0.12.3
+triangle==20230923
+    # via napari (napari_repo/pyproject.toml)
+typer==0.13.0
     # via npe2
 typing-extensions==4.12.2
     # via
@@ -521,11 +519,11 @@ typing-extensions==4.12.2
     #   superqt
     #   torch
     #   typer
-tzdata==2024.1
+tzdata==2024.2
     # via pandas
-urllib3==2.2.2
+urllib3==2.2.3
     # via requests
-virtualenv==20.26.3
+virtualenv==20.27.1
     # via napari (napari_repo/pyproject.toml)
 vispy==0.14.3
     # via
@@ -535,9 +533,9 @@ wcwidth==0.2.13
     # via prompt-toolkit
 wrapt==1.16.0
     # via napari (napari_repo/pyproject.toml)
-xarray==2024.6.0
+xarray==2024.10.0
     # via napari (napari_repo/pyproject.toml)
-zarr==2.18.2
+zarr==2.18.3
     # via napari (napari_repo/pyproject.toml)
-zipp==3.19.2
+zipp==3.21.0
     # via importlib-metadata
diff --git a/resources/constraints/constraints_py3.12.txt b/resources/constraints/constraints_py3.12.txt
index d179b8dfd58..9df92bb9fad 100644
--- a/resources/constraints/constraints_py3.12.txt
+++ b/resources/constraints/constraints_py3.12.txt
@@ -1,62 +1,61 @@
 # This file was autogenerated by uv via the following command:
-#    uv pip compile --python-version 3.12 --output-file napari_repo/resources/constraints/constraints_py3.12.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt --extra pyqt5 --extra pyqt6_experimental --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
-alabaster==0.7.16
+#    uv pip compile --python-version 3.12 --output-file napari_repo/resources/constraints/constraints_py3.12.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt --extra pyqt5 --extra pyqt6 --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
+alabaster==1.0.0
     # via sphinx
 annotated-types==0.7.0
     # via pydantic
-app-model==0.2.7
+app-model==0.3.0
     # via napari (napari_repo/pyproject.toml)
 appdirs==1.4.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
-    #   pint
 asciitree==0.3.3
     # via zarr
 asttokens==2.4.1
     # via stack-data
-attrs==23.2.0
+attrs==24.2.0
     # via
     #   hypothesis
     #   jsonschema
     #   referencing
-babel==2.15.0
+babel==2.16.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   sphinx
-build==1.2.1
+build==1.2.2.post1
     # via npe2
 cachey==0.2.1
     # via napari (napari_repo/pyproject.toml)
-certifi==2024.7.4
+certifi==2024.8.30
     # via
     #   napari (napari_repo/pyproject.toml)
     #   requests
-charset-normalizer==3.3.2
+charset-normalizer==3.4.0
     # via requests
 click==8.1.7
     # via
     #   dask
     #   typer
-cloudpickle==3.0.0
+cloudpickle==3.1.0
     # via dask
 comm==0.2.2
     # via ipykernel
-contourpy==1.2.1
+contourpy==1.3.1
     # via matplotlib
-coverage==7.5.4
+coverage==7.6.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
 cycler==0.12.1
     # via matplotlib
-dask==2024.6.2
+dask==2024.11.2
     # via napari (napari_repo/pyproject.toml)
-debugpy==1.8.1
+debugpy==1.8.8
     # via ipykernel
 decorator==5.1.1
     # via ipython
-distlib==0.3.8
+distlib==0.3.9
     # via virtualenv
 docstring-parser==0.16
     # via
@@ -64,23 +63,24 @@ docstring-parser==0.16
     #   magicgui
 docutils==0.21.2
     # via sphinx
-executing==2.0.1
+executing==2.1.0
     # via stack-data
 fasteners==0.19
     # via zarr
-filelock==3.15.4
+filelock==3.16.1
     # via
     #   torch
+    #   triton
     #   virtualenv
 flexcache==0.3
     # via pint
-flexparser==0.3.1
+flexparser==0.4
     # via pint
-fonttools==4.53.0
+fonttools==4.55.0
     # via matplotlib
-freetype-py==2.4.0
+freetype-py==2.5.1
     # via vispy
-fsspec==2024.6.0
+fsspec==2024.10.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
@@ -89,11 +89,11 @@ heapdict==1.0.1
     # via cachey
 hsluv==5.0.4
     # via vispy
-hypothesis==6.103.4
+hypothesis==6.119.3
     # via napari (napari_repo/pyproject.toml)
-idna==3.7
+idna==3.10
     # via requests
-imageio==2.34.1
+imageio==2.36.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-svg
@@ -104,26 +104,27 @@ in-n-out==0.2.1
     # via app-model
 iniconfig==2.0.0
     # via pytest
-ipykernel==6.29.4
+ipykernel==6.29.5
     # via
+    #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari-console
     #   qtconsole
-ipython==8.25.0
+ipython==8.29.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
     #   napari-console
-jedi==0.19.1
+jedi==0.19.2
     # via ipython
 jinja2==3.1.4
     # via
     #   sphinx
     #   torch
-jsonschema==4.22.0
+jsonschema==4.23.0
     # via napari (napari_repo/pyproject.toml)
-jsonschema-specifications==2023.12.1
+jsonschema-specifications==2024.10.1
     # via jsonschema
-jupyter-client==8.6.2
+jupyter-client==8.6.3
     # via
     #   ipykernel
     #   qtconsole
@@ -132,7 +133,7 @@ jupyter-core==5.7.2
     #   ipykernel
     #   jupyter-client
     #   qtconsole
-kiwisolver==1.4.5
+kiwisolver==1.4.7
     # via
     #   matplotlib
     #   vispy
@@ -144,19 +145,19 @@ llvmlite==0.43.0
     # via numba
 locket==1.0.0
     # via partd
-lxml==5.2.2
+lxml==5.3.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   lxml-html-clean
-lxml-html-clean==0.1.1
+lxml-html-clean==0.4.1
     # via lxml
-magicgui==0.8.3
+magicgui==0.9.1
     # via napari (napari_repo/pyproject.toml)
 markdown-it-py==3.0.0
     # via rich
-markupsafe==2.1.5
+markupsafe==3.0.2
     # via jinja2
-matplotlib==3.9.0
+matplotlib==3.9.2
     # via napari (napari_repo/pyproject.toml)
 matplotlib-inline==0.1.7
     # via
@@ -164,37 +165,35 @@ matplotlib-inline==0.1.7
     #   ipython
 mdurl==0.1.2
     # via markdown-it-py
-ml-dtypes==0.4.0
+ml-dtypes==0.5.0
     # via tensorstore
 mouseinfo==0.1.3
     # via pyautogui
 mpmath==1.3.0
     # via sympy
-napari-console==0.0.9
+napari-console==0.1.1
     # via napari (napari_repo/pyproject.toml)
 napari-plugin-engine==0.2.0
-    # via
-    #   napari (napari_repo/pyproject.toml)
-    #   napari-svg
-napari-plugin-manager==0.1.0a2
     # via napari (napari_repo/pyproject.toml)
-napari-svg==0.1.10
+napari-plugin-manager==0.1.3
+    # via napari (napari_repo/pyproject.toml)
+napari-svg==0.2.0
     # via napari (napari_repo/pyproject.toml)
 nest-asyncio==1.6.0
     # via ipykernel
-networkx==3.3
+networkx==3.4.2
     # via
     #   scikit-image
     #   torch
-npe2==0.7.6
+npe2==0.7.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-plugin-manager
 numba==0.60.0
     # via napari (napari_repo/pyproject.toml)
-numcodecs==0.12.1
+numcodecs==0.14.0
     # via zarr
-numpy==2.0.0
+numpy==2.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   contourpy
@@ -214,40 +213,41 @@ numpy==2.0.0
     #   vispy
     #   xarray
     #   zarr
-numpydoc==1.7.0
+numpydoc==1.8.0
     # via napari (napari_repo/pyproject.toml)
-nvidia-cublas-cu12==12.1.3.1
+nvidia-cublas-cu12==12.4.5.8
     # via
     #   nvidia-cudnn-cu12
     #   nvidia-cusolver-cu12
     #   torch
-nvidia-cuda-cupti-cu12==12.1.105
+nvidia-cuda-cupti-cu12==12.4.127
     # via torch
-nvidia-cuda-nvrtc-cu12==12.1.105
+nvidia-cuda-nvrtc-cu12==12.4.127
     # via torch
-nvidia-cuda-runtime-cu12==12.1.105
+nvidia-cuda-runtime-cu12==12.4.127
     # via torch
-nvidia-cudnn-cu12==8.9.2.26
+nvidia-cudnn-cu12==9.1.0.70
     # via torch
-nvidia-cufft-cu12==11.0.2.54
+nvidia-cufft-cu12==11.2.1.3
     # via torch
-nvidia-curand-cu12==10.3.2.106
+nvidia-curand-cu12==10.3.5.147
     # via torch
-nvidia-cusolver-cu12==11.4.5.107
+nvidia-cusolver-cu12==11.6.1.9
     # via torch
-nvidia-cusparse-cu12==12.1.0.106
+nvidia-cusparse-cu12==12.3.1.170
     # via
     #   nvidia-cusolver-cu12
     #   torch
-nvidia-nccl-cu12==2.20.5
+nvidia-nccl-cu12==2.21.5
     # via torch
-nvidia-nvjitlink-cu12==12.5.40
+nvidia-nvjitlink-cu12==12.4.127
     # via
     #   nvidia-cusolver-cu12
     #   nvidia-cusparse-cu12
-nvidia-nvtx-cu12==12.1.105
+    #   torch
+nvidia-nvtx-cu12==12.4.127
     # via torch
-packaging==24.1
+packaging==24.2
     # via
     #   build
     #   dask
@@ -262,7 +262,7 @@ packaging==24.1
     #   sphinx
     #   vispy
     #   xarray
-pandas==2.2.2
+pandas==2.2.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   xarray
@@ -272,19 +272,20 @@ partd==1.4.2
     # via dask
 pexpect==4.9.0
     # via ipython
-pillow==10.3.0
+pillow==11.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   imageio
     #   matplotlib
     #   scikit-image
-pint==0.24
+pint==0.24.4
     # via napari (napari_repo/pyproject.toml)
-pip==24.1
+pip==24.3.1
     # via napari-plugin-manager
-platformdirs==4.2.2
+platformdirs==4.3.6
     # via
     #   jupyter-core
+    #   pint
     #   pooch
     #   virtualenv
 pluggy==1.5.0
@@ -297,9 +298,9 @@ pooch==1.8.2
     #   scikit-image
 pretend==1.0.9
     # via napari (napari_repo/pyproject.toml)
-prompt-toolkit==3.0.47
+prompt-toolkit==3.0.48
     # via ipython
-psutil==6.0.0
+psutil==6.1.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
@@ -311,13 +312,13 @@ psygnal==0.11.1
     #   npe2
 ptyprocess==0.7.0
     # via pexpect
-pure-eval==0.2.2
+pure-eval==0.2.3
     # via stack-data
 pyautogui==0.9.54
     # via napari (napari_repo/pyproject.toml)
 pyconify==0.1.6
     # via superqt
-pydantic==2.7.4
+pydantic==2.9.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   app-model
@@ -325,7 +326,7 @@ pydantic==2.7.4
     #   pydantic-compat
 pydantic-compat==0.1.2
     # via app-model
-pydantic-core==2.18.4
+pydantic-core==2.23.4
     # via pydantic
 pygetwindow==0.0.9
     # via pyautogui
@@ -343,29 +344,29 @@ pyopengl==3.1.6
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-pyparsing==3.1.2
+pyparsing==3.2.0
     # via matplotlib
 pyperclip==1.9.0
     # via mouseinfo
-pyproject-hooks==1.1.0
+pyproject-hooks==1.2.0
     # via build
-pyqt5==5.15.10
+pyqt5==5.15.11
     # via napari (napari_repo/pyproject.toml)
-pyqt5-qt5==5.15.2
+pyqt5-qt5==5.15.15
     # via pyqt5
-pyqt5-sip==12.13.0
+pyqt5-sip==12.15.0
     # via pyqt5
-pyqt6==6.7.0
+pyqt6==6.7.1
     # via napari (napari_repo/pyproject.toml)
-pyqt6-qt6==6.7.2
+pyqt6-qt6==6.7.3
     # via pyqt6
-pyqt6-sip==13.6.0
+pyqt6-sip==13.8.0
     # via pyqt6
 pyrect==0.2.0
     # via pygetwindow
-pyscreeze==0.1.30
+pyscreeze==1.0.1
     # via pyautogui
-pytest==8.2.2
+pytest==8.3.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
@@ -373,7 +374,7 @@ pytest==8.2.2
     #   pytest-metadata
     #   pytest-pretty
     #   pytest-qt
-pytest-cov==5.0.0
+pytest-cov==6.0.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
 pytest-json-report==1.5.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
@@ -394,23 +395,22 @@ python3-xlib==0.15
     #   pyautogui
 pytweening==1.2.0
     # via pyautogui
-pytz==2024.1
+pytz==2024.2
     # via pandas
-pyyaml==6.0.1
+pyyaml==6.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   npe2
-pyzmq==26.0.3
+pyzmq==26.2.0
     # via
     #   ipykernel
     #   jupyter-client
-    #   qtconsole
-qtconsole==5.5.2
+qtconsole==5.6.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-console
-qtpy==2.4.1
+qtpy==2.4.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
@@ -427,22 +427,24 @@ requests==2.32.3
     #   pooch
     #   pyconify
     #   sphinx
-rich==13.7.1
+rich==13.9.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
     #   pytest-pretty
     #   typer
-rpds-py==0.18.1
+rpds-py==0.21.0
     # via
     #   jsonschema
     #   referencing
 scikit-image==0.24.0
     # via napari (napari_repo/pyproject.toml)
-scipy==1.13.1
+scipy==1.14.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
+setuptools==75.5.0
+    # via torch
 shellingham==1.5.4
     # via typer
 six==1.16.0
@@ -453,19 +455,19 @@ snowballstemmer==2.2.0
     # via sphinx
 sortedcontainers==2.4.0
     # via hypothesis
-sphinx==7.3.7
+sphinx==8.1.3
     # via numpydoc
-sphinxcontrib-applehelp==1.0.8
+sphinxcontrib-applehelp==2.0.0
     # via sphinx
-sphinxcontrib-devhelp==1.0.6
+sphinxcontrib-devhelp==2.0.0
     # via sphinx
-sphinxcontrib-htmlhelp==2.0.5
+sphinxcontrib-htmlhelp==2.1.0
     # via sphinx
 sphinxcontrib-jsmath==1.0.1
     # via sphinx
-sphinxcontrib-qthelp==1.0.7
+sphinxcontrib-qthelp==2.0.0
     # via sphinx
-sphinxcontrib-serializinghtml==1.1.10
+sphinxcontrib-serializinghtml==2.0.0
     # via sphinx
 stack-data==0.6.3
     # via ipython
@@ -474,32 +476,32 @@ superqt==0.6.7
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
     #   napari-plugin-manager
-sympy==1.12.1
+sympy==1.13.1
     # via torch
 tabulate==0.9.0
     # via numpydoc
-tensorstore==0.1.63
+tensorstore==0.1.68
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-tifffile==2024.6.18
+tifffile==2024.9.20
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
-tomli-w==1.0.0
+tomli-w==1.1.0
     # via npe2
-toolz==0.12.1
+toolz==1.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   partd
-torch==2.3.1
+torch==2.5.1
     # via napari (napari_repo/pyproject.toml)
 tornado==6.4.1
     # via
     #   ipykernel
     #   jupyter-client
-tqdm==4.66.4
+tqdm==4.67.0
     # via napari (napari_repo/pyproject.toml)
 traitlets==5.14.3
     # via
@@ -512,7 +514,9 @@ traitlets==5.14.3
     #   qtconsole
 triangle==20230923
     # via napari (napari_repo/pyproject.toml)
-typer==0.12.3
+triton==3.1.0
+    # via torch
+typer==0.13.0
     # via npe2
 typing-extensions==4.12.2
     # via
@@ -527,11 +531,11 @@ typing-extensions==4.12.2
     #   superqt
     #   torch
     #   typer
-tzdata==2024.1
+tzdata==2024.2
     # via pandas
-urllib3==2.2.2
+urllib3==2.2.3
     # via requests
-virtualenv==20.26.3
+virtualenv==20.27.1
     # via napari (napari_repo/pyproject.toml)
 vispy==0.14.3
     # via
@@ -541,7 +545,7 @@ wcwidth==0.2.13
     # via prompt-toolkit
 wrapt==1.16.0
     # via napari (napari_repo/pyproject.toml)
-xarray==2024.6.0
+xarray==2024.10.0
     # via napari (napari_repo/pyproject.toml)
-zarr==2.18.2
+zarr==2.18.3
     # via napari (napari_repo/pyproject.toml)
diff --git a/resources/constraints/constraints_py3.12_pydantic_1.txt b/resources/constraints/constraints_py3.12_pydantic_1.txt
index ba989077028..abdf5deff36 100644
--- a/resources/constraints/constraints_py3.12_pydantic_1.txt
+++ b/resources/constraints/constraints_py3.12_pydantic_1.txt
@@ -1,60 +1,59 @@
 # This file was autogenerated by uv via the following command:
-#    uv pip compile --python-version 3.12 --output-file napari_repo/resources/constraints/constraints_py3.12_pydantic_1.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt napari_repo/resources/constraints/pydantic_le_2.txt --extra pyqt5 --extra pyqt6_experimental --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
-alabaster==0.7.16
+#    uv pip compile --python-version 3.12 --output-file napari_repo/resources/constraints/constraints_py3.12_pydantic_1.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt napari_repo/resources/constraints/pydantic_le_2.txt --extra pyqt5 --extra pyqt6 --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
+alabaster==1.0.0
     # via sphinx
-app-model==0.2.7
+app-model==0.3.0
     # via napari (napari_repo/pyproject.toml)
 appdirs==1.4.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
-    #   pint
 asciitree==0.3.3
     # via zarr
 asttokens==2.4.1
     # via stack-data
-attrs==23.2.0
+attrs==24.2.0
     # via
     #   hypothesis
     #   jsonschema
     #   referencing
-babel==2.15.0
+babel==2.16.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   sphinx
-build==1.2.1
+build==1.2.2.post1
     # via npe2
 cachey==0.2.1
     # via napari (napari_repo/pyproject.toml)
-certifi==2024.7.4
+certifi==2024.8.30
     # via
     #   napari (napari_repo/pyproject.toml)
     #   requests
-charset-normalizer==3.3.2
+charset-normalizer==3.4.0
     # via requests
 click==8.1.7
     # via
     #   dask
     #   typer
-cloudpickle==3.0.0
+cloudpickle==3.1.0
     # via dask
 comm==0.2.2
     # via ipykernel
-contourpy==1.2.1
+contourpy==1.3.1
     # via matplotlib
-coverage==7.5.4
+coverage==7.6.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
 cycler==0.12.1
     # via matplotlib
-dask==2024.6.2
+dask==2024.11.2
     # via napari (napari_repo/pyproject.toml)
-debugpy==1.8.1
+debugpy==1.8.8
     # via ipykernel
 decorator==5.1.1
     # via ipython
-distlib==0.3.8
+distlib==0.3.9
     # via virtualenv
 docstring-parser==0.16
     # via
@@ -62,23 +61,24 @@ docstring-parser==0.16
     #   magicgui
 docutils==0.21.2
     # via sphinx
-executing==2.0.1
+executing==2.1.0
     # via stack-data
 fasteners==0.19
     # via zarr
-filelock==3.15.4
+filelock==3.16.1
     # via
     #   torch
+    #   triton
     #   virtualenv
 flexcache==0.3
     # via pint
-flexparser==0.3.1
+flexparser==0.4
     # via pint
-fonttools==4.53.0
+fonttools==4.55.0
     # via matplotlib
-freetype-py==2.4.0
+freetype-py==2.5.1
     # via vispy
-fsspec==2024.6.0
+fsspec==2024.10.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
@@ -87,11 +87,11 @@ heapdict==1.0.1
     # via cachey
 hsluv==5.0.4
     # via vispy
-hypothesis==6.103.4
+hypothesis==6.119.3
     # via napari (napari_repo/pyproject.toml)
-idna==3.7
+idna==3.10
     # via requests
-imageio==2.34.1
+imageio==2.36.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-svg
@@ -102,26 +102,27 @@ in-n-out==0.2.1
     # via app-model
 iniconfig==2.0.0
     # via pytest
-ipykernel==6.29.4
+ipykernel==6.29.5
     # via
+    #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari-console
     #   qtconsole
-ipython==8.25.0
+ipython==8.29.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
     #   napari-console
-jedi==0.19.1
+jedi==0.19.2
     # via ipython
 jinja2==3.1.4
     # via
     #   sphinx
     #   torch
-jsonschema==4.22.0
+jsonschema==4.23.0
     # via napari (napari_repo/pyproject.toml)
-jsonschema-specifications==2023.12.1
+jsonschema-specifications==2024.10.1
     # via jsonschema
-jupyter-client==8.6.2
+jupyter-client==8.6.3
     # via
     #   ipykernel
     #   qtconsole
@@ -130,7 +131,7 @@ jupyter-core==5.7.2
     #   ipykernel
     #   jupyter-client
     #   qtconsole
-kiwisolver==1.4.5
+kiwisolver==1.4.7
     # via
     #   matplotlib
     #   vispy
@@ -142,19 +143,19 @@ llvmlite==0.43.0
     # via numba
 locket==1.0.0
     # via partd
-lxml==5.2.2
+lxml==5.3.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   lxml-html-clean
-lxml-html-clean==0.1.1
+lxml-html-clean==0.4.1
     # via lxml
-magicgui==0.8.3
+magicgui==0.9.1
     # via napari (napari_repo/pyproject.toml)
 markdown-it-py==3.0.0
     # via rich
-markupsafe==2.1.5
+markupsafe==3.0.2
     # via jinja2
-matplotlib==3.9.0
+matplotlib==3.9.2
     # via napari (napari_repo/pyproject.toml)
 matplotlib-inline==0.1.7
     # via
@@ -162,37 +163,35 @@ matplotlib-inline==0.1.7
     #   ipython
 mdurl==0.1.2
     # via markdown-it-py
-ml-dtypes==0.4.0
+ml-dtypes==0.5.0
     # via tensorstore
 mouseinfo==0.1.3
     # via pyautogui
 mpmath==1.3.0
     # via sympy
-napari-console==0.0.9
+napari-console==0.1.1
     # via napari (napari_repo/pyproject.toml)
 napari-plugin-engine==0.2.0
-    # via
-    #   napari (napari_repo/pyproject.toml)
-    #   napari-svg
-napari-plugin-manager==0.1.0a2
     # via napari (napari_repo/pyproject.toml)
-napari-svg==0.1.10
+napari-plugin-manager==0.1.3
+    # via napari (napari_repo/pyproject.toml)
+napari-svg==0.2.0
     # via napari (napari_repo/pyproject.toml)
 nest-asyncio==1.6.0
     # via ipykernel
-networkx==3.3
+networkx==3.4.2
     # via
     #   scikit-image
     #   torch
-npe2==0.7.6
+npe2==0.7.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-plugin-manager
 numba==0.60.0
     # via napari (napari_repo/pyproject.toml)
-numcodecs==0.12.1
+numcodecs==0.14.0
     # via zarr
-numpy==2.0.0
+numpy==2.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   contourpy
@@ -212,40 +211,41 @@ numpy==2.0.0
     #   vispy
     #   xarray
     #   zarr
-numpydoc==1.7.0
+numpydoc==1.8.0
     # via napari (napari_repo/pyproject.toml)
-nvidia-cublas-cu12==12.1.3.1
+nvidia-cublas-cu12==12.4.5.8
     # via
     #   nvidia-cudnn-cu12
     #   nvidia-cusolver-cu12
     #   torch
-nvidia-cuda-cupti-cu12==12.1.105
+nvidia-cuda-cupti-cu12==12.4.127
     # via torch
-nvidia-cuda-nvrtc-cu12==12.1.105
+nvidia-cuda-nvrtc-cu12==12.4.127
     # via torch
-nvidia-cuda-runtime-cu12==12.1.105
+nvidia-cuda-runtime-cu12==12.4.127
     # via torch
-nvidia-cudnn-cu12==8.9.2.26
+nvidia-cudnn-cu12==9.1.0.70
     # via torch
-nvidia-cufft-cu12==11.0.2.54
+nvidia-cufft-cu12==11.2.1.3
     # via torch
-nvidia-curand-cu12==10.3.2.106
+nvidia-curand-cu12==10.3.5.147
     # via torch
-nvidia-cusolver-cu12==11.4.5.107
+nvidia-cusolver-cu12==11.6.1.9
     # via torch
-nvidia-cusparse-cu12==12.1.0.106
+nvidia-cusparse-cu12==12.3.1.170
     # via
     #   nvidia-cusolver-cu12
     #   torch
-nvidia-nccl-cu12==2.20.5
+nvidia-nccl-cu12==2.21.5
     # via torch
-nvidia-nvjitlink-cu12==12.5.40
+nvidia-nvjitlink-cu12==12.4.127
     # via
     #   nvidia-cusolver-cu12
     #   nvidia-cusparse-cu12
-nvidia-nvtx-cu12==12.1.105
+    #   torch
+nvidia-nvtx-cu12==12.4.127
     # via torch
-packaging==24.1
+packaging==24.2
     # via
     #   build
     #   dask
@@ -260,7 +260,7 @@ packaging==24.1
     #   sphinx
     #   vispy
     #   xarray
-pandas==2.2.2
+pandas==2.2.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   xarray
@@ -270,19 +270,20 @@ partd==1.4.2
     # via dask
 pexpect==4.9.0
     # via ipython
-pillow==10.3.0
+pillow==11.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   imageio
     #   matplotlib
     #   scikit-image
-pint==0.24
+pint==0.24.4
     # via napari (napari_repo/pyproject.toml)
-pip==24.1
+pip==24.3.1
     # via napari-plugin-manager
-platformdirs==4.2.2
+platformdirs==4.3.6
     # via
     #   jupyter-core
+    #   pint
     #   pooch
     #   virtualenv
 pluggy==1.5.0
@@ -295,9 +296,9 @@ pooch==1.8.2
     #   scikit-image
 pretend==1.0.9
     # via napari (napari_repo/pyproject.toml)
-prompt-toolkit==3.0.47
+prompt-toolkit==3.0.48
     # via ipython
-psutil==6.0.0
+psutil==6.1.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
@@ -309,13 +310,13 @@ psygnal==0.11.1
     #   npe2
 ptyprocess==0.7.0
     # via pexpect
-pure-eval==0.2.2
+pure-eval==0.2.3
     # via stack-data
 pyautogui==0.9.54
     # via napari (napari_repo/pyproject.toml)
 pyconify==0.1.6
     # via superqt
-pydantic==1.10.17
+pydantic==1.10.19
     # via
     #   -r napari_repo/resources/constraints/pydantic_le_2.txt
     #   napari (napari_repo/pyproject.toml)
@@ -340,29 +341,29 @@ pyopengl==3.1.6
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-pyparsing==3.1.2
+pyparsing==3.2.0
     # via matplotlib
 pyperclip==1.9.0
     # via mouseinfo
-pyproject-hooks==1.1.0
+pyproject-hooks==1.2.0
     # via build
-pyqt5==5.15.10
+pyqt5==5.15.11
     # via napari (napari_repo/pyproject.toml)
-pyqt5-qt5==5.15.2
+pyqt5-qt5==5.15.15
     # via pyqt5
-pyqt5-sip==12.13.0
+pyqt5-sip==12.15.0
     # via pyqt5
-pyqt6==6.7.0
+pyqt6==6.7.1
     # via napari (napari_repo/pyproject.toml)
-pyqt6-qt6==6.7.2
+pyqt6-qt6==6.7.3
     # via pyqt6
-pyqt6-sip==13.6.0
+pyqt6-sip==13.8.0
     # via pyqt6
 pyrect==0.2.0
     # via pygetwindow
-pyscreeze==0.1.30
+pyscreeze==1.0.1
     # via pyautogui
-pytest==8.2.2
+pytest==8.3.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
@@ -370,7 +371,7 @@ pytest==8.2.2
     #   pytest-metadata
     #   pytest-pretty
     #   pytest-qt
-pytest-cov==5.0.0
+pytest-cov==6.0.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
 pytest-json-report==1.5.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
@@ -391,23 +392,22 @@ python3-xlib==0.15
     #   pyautogui
 pytweening==1.2.0
     # via pyautogui
-pytz==2024.1
+pytz==2024.2
     # via pandas
-pyyaml==6.0.1
+pyyaml==6.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   npe2
-pyzmq==26.0.3
+pyzmq==26.2.0
     # via
     #   ipykernel
     #   jupyter-client
-    #   qtconsole
-qtconsole==5.5.2
+qtconsole==5.6.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-console
-qtpy==2.4.1
+qtpy==2.4.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
@@ -424,22 +424,24 @@ requests==2.32.3
     #   pooch
     #   pyconify
     #   sphinx
-rich==13.7.1
+rich==13.9.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
     #   pytest-pretty
     #   typer
-rpds-py==0.18.1
+rpds-py==0.21.0
     # via
     #   jsonschema
     #   referencing
 scikit-image==0.24.0
     # via napari (napari_repo/pyproject.toml)
-scipy==1.13.1
+scipy==1.14.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
+setuptools==75.5.0
+    # via torch
 shellingham==1.5.4
     # via typer
 six==1.16.0
@@ -450,19 +452,19 @@ snowballstemmer==2.2.0
     # via sphinx
 sortedcontainers==2.4.0
     # via hypothesis
-sphinx==7.3.7
+sphinx==8.1.3
     # via numpydoc
-sphinxcontrib-applehelp==1.0.8
+sphinxcontrib-applehelp==2.0.0
     # via sphinx
-sphinxcontrib-devhelp==1.0.6
+sphinxcontrib-devhelp==2.0.0
     # via sphinx
-sphinxcontrib-htmlhelp==2.0.5
+sphinxcontrib-htmlhelp==2.1.0
     # via sphinx
 sphinxcontrib-jsmath==1.0.1
     # via sphinx
-sphinxcontrib-qthelp==1.0.7
+sphinxcontrib-qthelp==2.0.0
     # via sphinx
-sphinxcontrib-serializinghtml==1.1.10
+sphinxcontrib-serializinghtml==2.0.0
     # via sphinx
 stack-data==0.6.3
     # via ipython
@@ -471,32 +473,32 @@ superqt==0.6.7
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
     #   napari-plugin-manager
-sympy==1.12.1
+sympy==1.13.1
     # via torch
 tabulate==0.9.0
     # via numpydoc
-tensorstore==0.1.63
+tensorstore==0.1.68
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-tifffile==2024.6.18
+tifffile==2024.9.20
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
-tomli-w==1.0.0
+tomli-w==1.1.0
     # via npe2
-toolz==0.12.1
+toolz==1.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   partd
-torch==2.3.1
+torch==2.5.1
     # via napari (napari_repo/pyproject.toml)
 tornado==6.4.1
     # via
     #   ipykernel
     #   jupyter-client
-tqdm==4.66.4
+tqdm==4.67.0
     # via napari (napari_repo/pyproject.toml)
 traitlets==5.14.3
     # via
@@ -509,7 +511,9 @@ traitlets==5.14.3
     #   qtconsole
 triangle==20230923
     # via napari (napari_repo/pyproject.toml)
-typer==0.12.3
+triton==3.1.0
+    # via torch
+typer==0.13.0
     # via npe2
 typing-extensions==4.12.2
     # via
@@ -523,11 +527,11 @@ typing-extensions==4.12.2
     #   superqt
     #   torch
     #   typer
-tzdata==2024.1
+tzdata==2024.2
     # via pandas
-urllib3==2.2.2
+urllib3==2.2.3
     # via requests
-virtualenv==20.26.3
+virtualenv==20.27.1
     # via napari (napari_repo/pyproject.toml)
 vispy==0.14.3
     # via
@@ -537,7 +541,7 @@ wcwidth==0.2.13
     # via prompt-toolkit
 wrapt==1.16.0
     # via napari (napari_repo/pyproject.toml)
-xarray==2024.6.0
+xarray==2024.10.0
     # via napari (napari_repo/pyproject.toml)
-zarr==2.18.2
+zarr==2.18.3
     # via napari (napari_repo/pyproject.toml)
diff --git a/resources/constraints/constraints_py3.12_macos_arm.txt b/resources/constraints/constraints_py3.12_windows.txt
similarity index 78%
rename from resources/constraints/constraints_py3.12_macos_arm.txt
rename to resources/constraints/constraints_py3.12_windows.txt
index 40548a1e331..d1d2a972b6d 100644
--- a/resources/constraints/constraints_py3.12_macos_arm.txt
+++ b/resources/constraints/constraints_py3.12_windows.txt
@@ -1,64 +1,69 @@
 # This file was autogenerated by uv via the following command:
-#    uv pip compile --python-platform aarch64-apple-darwin --python-version 3.12 --output-file napari_repo/resources/constraints/constraints_py3.12_macos_arm.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt --extra pyqt5 --extra pyqt6_experimental --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
-alabaster==0.7.16
+#    uv pip compile --python-platform windows --python-version 3.12 --output-file napari_repo/resources/constraints/constraints_py3.12_windows.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt --extra pyqt5 --extra pyqt6 --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
+alabaster==1.0.0
     # via sphinx
 annotated-types==0.7.0
     # via pydantic
-app-model==0.2.7
+app-model==0.3.0
     # via napari (napari_repo/pyproject.toml)
 appdirs==1.4.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
-    #   pint
-appnope==0.1.4
-    # via ipykernel
 asciitree==0.3.3
     # via zarr
 asttokens==2.4.1
     # via stack-data
-attrs==23.2.0
+attrs==24.2.0
     # via
     #   hypothesis
     #   jsonschema
     #   referencing
-babel==2.15.0
+babel==2.16.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   sphinx
-build==1.2.1
+build==1.2.2.post1
     # via npe2
 cachey==0.2.1
     # via napari (napari_repo/pyproject.toml)
-certifi==2024.7.4
+certifi==2024.8.30
     # via
     #   napari (napari_repo/pyproject.toml)
     #   requests
-charset-normalizer==3.3.2
+charset-normalizer==3.4.0
     # via requests
 click==8.1.7
     # via
     #   dask
     #   typer
-cloudpickle==3.0.0
+cloudpickle==3.1.0
     # via dask
+colorama==0.4.6
+    # via
+    #   build
+    #   click
+    #   ipython
+    #   pytest
+    #   sphinx
+    #   tqdm
 comm==0.2.2
     # via ipykernel
-contourpy==1.2.1
+contourpy==1.3.1
     # via matplotlib
-coverage==7.5.4
+coverage==7.6.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
 cycler==0.12.1
     # via matplotlib
-dask==2024.6.2
+dask==2024.11.2
     # via napari (napari_repo/pyproject.toml)
-debugpy==1.8.1
+debugpy==1.8.8
     # via ipykernel
 decorator==5.1.1
     # via ipython
-distlib==0.3.8
+distlib==0.3.9
     # via virtualenv
 docstring-parser==0.16
     # via
@@ -66,23 +71,23 @@ docstring-parser==0.16
     #   magicgui
 docutils==0.21.2
     # via sphinx
-executing==2.0.1
+executing==2.1.0
     # via stack-data
 fasteners==0.19
     # via zarr
-filelock==3.15.4
+filelock==3.16.1
     # via
     #   torch
     #   virtualenv
 flexcache==0.3
     # via pint
-flexparser==0.3.1
+flexparser==0.4
     # via pint
-fonttools==4.53.0
+fonttools==4.55.0
     # via matplotlib
-freetype-py==2.4.0
+freetype-py==2.5.1
     # via vispy
-fsspec==2024.6.0
+fsspec==2024.10.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
@@ -91,11 +96,11 @@ heapdict==1.0.1
     # via cachey
 hsluv==5.0.4
     # via vispy
-hypothesis==6.103.4
+hypothesis==6.119.3
     # via napari (napari_repo/pyproject.toml)
-idna==3.7
+idna==3.10
     # via requests
-imageio==2.34.1
+imageio==2.36.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-svg
@@ -106,26 +111,27 @@ in-n-out==0.2.1
     # via app-model
 iniconfig==2.0.0
     # via pytest
-ipykernel==6.29.4
+ipykernel==6.29.5
     # via
+    #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari-console
     #   qtconsole
-ipython==8.25.0
+ipython==8.29.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
     #   napari-console
-jedi==0.19.1
+jedi==0.19.2
     # via ipython
 jinja2==3.1.4
     # via
     #   sphinx
     #   torch
-jsonschema==4.22.0
+jsonschema==4.23.0
     # via napari (napari_repo/pyproject.toml)
-jsonschema-specifications==2023.12.1
+jsonschema-specifications==2024.10.1
     # via jsonschema
-jupyter-client==8.6.2
+jupyter-client==8.6.3
     # via
     #   ipykernel
     #   qtconsole
@@ -134,7 +140,7 @@ jupyter-core==5.7.2
     #   ipykernel
     #   jupyter-client
     #   qtconsole
-kiwisolver==1.4.5
+kiwisolver==1.4.7
     # via
     #   matplotlib
     #   vispy
@@ -146,19 +152,19 @@ llvmlite==0.43.0
     # via numba
 locket==1.0.0
     # via partd
-lxml==5.2.2
+lxml==5.3.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   lxml-html-clean
-lxml-html-clean==0.1.1
+lxml-html-clean==0.4.1
     # via lxml
-magicgui==0.8.3
+magicgui==0.9.1
     # via napari (napari_repo/pyproject.toml)
 markdown-it-py==3.0.0
     # via rich
-markupsafe==2.1.5
+markupsafe==3.0.2
     # via jinja2
-matplotlib==3.9.0
+matplotlib==3.9.2
     # via napari (napari_repo/pyproject.toml)
 matplotlib-inline==0.1.7
     # via
@@ -166,37 +172,35 @@ matplotlib-inline==0.1.7
     #   ipython
 mdurl==0.1.2
     # via markdown-it-py
-ml-dtypes==0.4.0
+ml-dtypes==0.5.0
     # via tensorstore
 mouseinfo==0.1.3
     # via pyautogui
 mpmath==1.3.0
     # via sympy
-napari-console==0.0.9
+napari-console==0.1.1
     # via napari (napari_repo/pyproject.toml)
 napari-plugin-engine==0.2.0
-    # via
-    #   napari (napari_repo/pyproject.toml)
-    #   napari-svg
-napari-plugin-manager==0.1.0a2
     # via napari (napari_repo/pyproject.toml)
-napari-svg==0.1.10
+napari-plugin-manager==0.1.3
+    # via napari (napari_repo/pyproject.toml)
+napari-svg==0.2.0
     # via napari (napari_repo/pyproject.toml)
 nest-asyncio==1.6.0
     # via ipykernel
-networkx==3.3
+networkx==3.4.2
     # via
     #   scikit-image
     #   torch
-npe2==0.7.6
+npe2==0.7.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-plugin-manager
 numba==0.60.0
     # via napari (napari_repo/pyproject.toml)
-numcodecs==0.12.1
+numcodecs==0.14.0
     # via zarr
-numpy==2.0.0
+numpy==2.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   contourpy
@@ -212,12 +216,13 @@ numpy==2.0.0
     #   scipy
     #   tensorstore
     #   tifffile
+    #   triangle
     #   vispy
     #   xarray
     #   zarr
-numpydoc==1.7.0
+numpydoc==1.8.0
     # via napari (napari_repo/pyproject.toml)
-packaging==24.1
+packaging==24.2
     # via
     #   build
     #   dask
@@ -232,7 +237,7 @@ packaging==24.1
     #   sphinx
     #   vispy
     #   xarray
-pandas==2.2.2
+pandas==2.2.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   xarray
@@ -240,21 +245,20 @@ parso==0.8.4
     # via jedi
 partd==1.4.2
     # via dask
-pexpect==4.9.0
-    # via ipython
-pillow==10.3.0
+pillow==11.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   imageio
     #   matplotlib
     #   scikit-image
-pint==0.24
+pint==0.24.4
     # via napari (napari_repo/pyproject.toml)
-pip==24.1
+pip==24.3.1
     # via napari-plugin-manager
-platformdirs==4.2.2
+platformdirs==4.3.6
     # via
     #   jupyter-core
+    #   pint
     #   pooch
     #   virtualenv
 pluggy==1.5.0
@@ -267,9 +271,9 @@ pooch==1.8.2
     #   scikit-image
 pretend==1.0.9
     # via napari (napari_repo/pyproject.toml)
-prompt-toolkit==3.0.47
+prompt-toolkit==3.0.48
     # via ipython
-psutil==6.0.0
+psutil==6.1.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
@@ -279,15 +283,13 @@ psygnal==0.11.1
     #   app-model
     #   magicgui
     #   npe2
-ptyprocess==0.7.0
-    # via pexpect
-pure-eval==0.2.2
+pure-eval==0.2.3
     # via stack-data
 pyautogui==0.9.54
     # via napari (napari_repo/pyproject.toml)
 pyconify==0.1.6
     # via superqt
-pydantic==2.7.4
+pydantic==2.9.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   app-model
@@ -295,7 +297,7 @@ pydantic==2.7.4
     #   pydantic-compat
 pydantic-compat==0.1.2
     # via app-model
-pydantic-core==2.18.4
+pydantic-core==2.23.4
     # via pydantic
 pygetwindow==0.0.9
     # via pyautogui
@@ -309,42 +311,33 @@ pygments==2.18.0
     #   superqt
 pymsgbox==1.0.9
     # via pyautogui
-pyobjc-core==10.3.1
-    # via
-    #   pyautogui
-    #   pyobjc-framework-cocoa
-    #   pyobjc-framework-quartz
-pyobjc-framework-cocoa==10.3.1
-    # via pyobjc-framework-quartz
-pyobjc-framework-quartz==10.3.1
-    # via pyautogui
 pyopengl==3.1.6
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-pyparsing==3.1.2
+pyparsing==3.2.0
     # via matplotlib
 pyperclip==1.9.0
     # via mouseinfo
-pyproject-hooks==1.1.0
+pyproject-hooks==1.2.0
     # via build
-pyqt5==5.15.10
+pyqt5==5.15.11
     # via napari (napari_repo/pyproject.toml)
-pyqt5-qt5==5.15.14
+pyqt5-qt5==5.15.2
     # via pyqt5
-pyqt5-sip==12.13.0
+pyqt5-sip==12.15.0
     # via pyqt5
-pyqt6==6.7.0
+pyqt6==6.7.1
     # via napari (napari_repo/pyproject.toml)
-pyqt6-qt6==6.7.2
+pyqt6-qt6==6.7.3
     # via pyqt6
-pyqt6-sip==13.6.0
+pyqt6-sip==13.8.0
     # via pyqt6
 pyrect==0.2.0
     # via pygetwindow
-pyscreeze==0.1.30
+pyscreeze==1.0.1
     # via pyautogui
-pytest==8.2.2
+pytest==8.3.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
@@ -352,7 +345,7 @@ pytest==8.2.2
     #   pytest-metadata
     #   pytest-pretty
     #   pytest-qt
-pytest-cov==5.0.0
+pytest-cov==6.0.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
 pytest-json-report==1.5.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
@@ -369,23 +362,26 @@ python-dateutil==2.9.0.post0
     #   pandas
 pytweening==1.2.0
     # via pyautogui
-pytz==2024.1
+pytz==2024.2
     # via pandas
-pyyaml==6.0.1
+pywin32==308
+    # via
+    #   napari (napari_repo/pyproject.toml)
+    #   jupyter-core
+pyyaml==6.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   npe2
-pyzmq==26.0.3
+pyzmq==26.2.0
     # via
     #   ipykernel
     #   jupyter-client
-    #   qtconsole
-qtconsole==5.5.2
+qtconsole==5.6.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-console
-qtpy==2.4.1
+qtpy==2.4.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
@@ -402,24 +398,24 @@ requests==2.32.3
     #   pooch
     #   pyconify
     #   sphinx
-rich==13.7.1
+rich==13.9.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
     #   pytest-pretty
     #   typer
-rpds-py==0.18.1
+rpds-py==0.21.0
     # via
     #   jsonschema
     #   referencing
-rubicon-objc==0.4.9
-    # via mouseinfo
 scikit-image==0.24.0
     # via napari (napari_repo/pyproject.toml)
-scipy==1.13.1
+scipy==1.14.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
+setuptools==75.5.0
+    # via torch
 shellingham==1.5.4
     # via typer
 six==1.16.0
@@ -430,19 +426,19 @@ snowballstemmer==2.2.0
     # via sphinx
 sortedcontainers==2.4.0
     # via hypothesis
-sphinx==7.3.7
+sphinx==8.1.3
     # via numpydoc
-sphinxcontrib-applehelp==1.0.8
+sphinxcontrib-applehelp==2.0.0
     # via sphinx
-sphinxcontrib-devhelp==1.0.6
+sphinxcontrib-devhelp==2.0.0
     # via sphinx
-sphinxcontrib-htmlhelp==2.0.5
+sphinxcontrib-htmlhelp==2.1.0
     # via sphinx
 sphinxcontrib-jsmath==1.0.1
     # via sphinx
-sphinxcontrib-qthelp==1.0.7
+sphinxcontrib-qthelp==2.0.0
     # via sphinx
-sphinxcontrib-serializinghtml==1.1.10
+sphinxcontrib-serializinghtml==2.0.0
     # via sphinx
 stack-data==0.6.3
     # via ipython
@@ -451,32 +447,32 @@ superqt==0.6.7
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
     #   napari-plugin-manager
-sympy==1.12.1
+sympy==1.13.1
     # via torch
 tabulate==0.9.0
     # via numpydoc
-tensorstore==0.1.63
+tensorstore==0.1.68
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-tifffile==2024.6.18
+tifffile==2024.9.20
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
-tomli-w==1.0.0
+tomli-w==1.1.0
     # via npe2
-toolz==0.12.1
+toolz==1.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   partd
-torch==2.3.1
+torch==2.5.1
     # via napari (napari_repo/pyproject.toml)
 tornado==6.4.1
     # via
     #   ipykernel
     #   jupyter-client
-tqdm==4.66.4
+tqdm==4.67.0
     # via napari (napari_repo/pyproject.toml)
 traitlets==5.14.3
     # via
@@ -487,7 +483,9 @@ traitlets==5.14.3
     #   jupyter-core
     #   matplotlib-inline
     #   qtconsole
-typer==0.12.3
+triangle==20230923
+    # via napari (napari_repo/pyproject.toml)
+typer==0.13.0
     # via npe2
 typing-extensions==4.12.2
     # via
@@ -502,11 +500,11 @@ typing-extensions==4.12.2
     #   superqt
     #   torch
     #   typer
-tzdata==2024.1
+tzdata==2024.2
     # via pandas
-urllib3==2.2.2
+urllib3==2.2.3
     # via requests
-virtualenv==20.26.3
+virtualenv==20.27.1
     # via napari (napari_repo/pyproject.toml)
 vispy==0.14.3
     # via
@@ -516,7 +514,7 @@ wcwidth==0.2.13
     # via prompt-toolkit
 wrapt==1.16.0
     # via napari (napari_repo/pyproject.toml)
-xarray==2024.6.0
+xarray==2024.10.0
     # via napari (napari_repo/pyproject.toml)
-zarr==2.18.2
+zarr==2.18.3
     # via napari (napari_repo/pyproject.toml)
diff --git a/resources/constraints/constraints_py3.9.txt b/resources/constraints/constraints_py3.9.txt
index 430d513d96c..a51ffa36076 100644
--- a/resources/constraints/constraints_py3.9.txt
+++ b/resources/constraints/constraints_py3.9.txt
@@ -1,10 +1,10 @@
 # This file was autogenerated by uv via the following command:
-#    uv pip compile --python-version 3.9 --output-file napari_repo/resources/constraints/constraints_py3.9.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt --extra pyqt5 --extra pyqt6_experimental --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
+#    uv pip compile --python-version 3.9 --output-file napari_repo/resources/constraints/constraints_py3.9.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt --extra pyqt5 --extra pyqt6 --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
 alabaster==0.7.16
     # via sphinx
 annotated-types==0.7.0
     # via pydantic
-app-model==0.2.7
+app-model==0.3.0
     # via napari (napari_repo/pyproject.toml)
 appdirs==1.4.4
     # via
@@ -14,48 +14,48 @@ asciitree==0.3.3
     # via zarr
 asttokens==2.4.1
     # via stack-data
-attrs==23.2.0
+attrs==24.2.0
     # via
     #   hypothesis
     #   jsonschema
     #   referencing
-babel==2.15.0
+babel==2.16.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   sphinx
-build==1.2.1
+build==1.2.2.post1
     # via npe2
 cachey==0.2.1
     # via napari (napari_repo/pyproject.toml)
-certifi==2024.7.4
+certifi==2024.8.30
     # via
     #   napari (napari_repo/pyproject.toml)
     #   requests
-charset-normalizer==3.3.2
+charset-normalizer==3.4.0
     # via requests
 click==8.1.7
     # via
     #   dask
     #   typer
-cloudpickle==3.0.0
+cloudpickle==3.1.0
     # via dask
 comm==0.2.2
     # via ipykernel
-contourpy==1.2.1
+contourpy==1.3.0
     # via matplotlib
-coverage==7.5.4
+coverage==7.6.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
 cycler==0.12.1
     # via matplotlib
-dask==2024.6.2
+dask==2024.8.0
     # via napari (napari_repo/pyproject.toml)
-debugpy==1.8.1
+debugpy==1.8.8
     # via ipykernel
 decorator==5.1.1
     # via ipython
-distlib==0.3.8
+distlib==0.3.9
     # via virtualenv
 docstring-parser==0.16
     # via
@@ -63,25 +63,29 @@ docstring-parser==0.16
     #   magicgui
 docutils==0.21.2
     # via sphinx
-exceptiongroup==1.2.1
+exceptiongroup==1.2.2
     # via
     #   hypothesis
     #   ipython
     #   pytest
-executing==2.0.1
+executing==2.1.0
     # via stack-data
 fasteners==0.19
     # via zarr
-filelock==3.15.4
+filelock==3.16.1
     # via
     #   torch
     #   triton
     #   virtualenv
-fonttools==4.53.0
+flexcache==0.3
+    # via pint
+flexparser==0.4
+    # via pint
+fonttools==4.55.0
     # via matplotlib
-freetype-py==2.4.0
+freetype-py==2.5.1
     # via vispy
-fsspec==2024.6.0
+fsspec==2024.10.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
@@ -90,31 +94,32 @@ heapdict==1.0.1
     # via cachey
 hsluv==5.0.4
     # via vispy
-hypothesis==6.103.4
+hypothesis==6.119.3
     # via napari (napari_repo/pyproject.toml)
-idna==3.7
+idna==3.10
     # via requests
-imageio==2.34.1
+imageio==2.36.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-svg
     #   scikit-image
 imagesize==1.4.1
     # via sphinx
-importlib-metadata==7.2.1
+importlib-metadata==8.5.0
     # via
     #   build
     #   dask
     #   jupyter-client
     #   sphinx
-importlib-resources==6.4.0
+importlib-resources==6.4.5
     # via matplotlib
 in-n-out==0.2.1
     # via app-model
 iniconfig==2.0.0
     # via pytest
-ipykernel==6.29.4
+ipykernel==6.29.5
     # via
+    #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari-console
     #   qtconsole
 ipython==8.18.1
@@ -122,17 +127,17 @@ ipython==8.18.1
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
     #   napari-console
-jedi==0.19.1
+jedi==0.19.2
     # via ipython
 jinja2==3.1.4
     # via
     #   sphinx
     #   torch
-jsonschema==4.22.0
+jsonschema==4.23.0
     # via napari (napari_repo/pyproject.toml)
-jsonschema-specifications==2023.12.1
+jsonschema-specifications==2024.10.1
     # via jsonschema
-jupyter-client==8.6.2
+jupyter-client==8.6.3
     # via
     #   ipykernel
     #   qtconsole
@@ -141,7 +146,7 @@ jupyter-core==5.7.2
     #   ipykernel
     #   jupyter-client
     #   qtconsole
-kiwisolver==1.4.5
+kiwisolver==1.4.7
     # via
     #   matplotlib
     #   vispy
@@ -153,19 +158,19 @@ llvmlite==0.43.0
     # via numba
 locket==1.0.0
     # via partd
-lxml==5.2.2
+lxml==5.3.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   lxml-html-clean
-lxml-html-clean==0.1.1
+lxml-html-clean==0.4.1
     # via lxml
-magicgui==0.8.3
+magicgui==0.9.1
     # via napari (napari_repo/pyproject.toml)
 markdown-it-py==3.0.0
     # via rich
-markupsafe==2.1.5
+markupsafe==3.0.2
     # via jinja2
-matplotlib==3.9.0
+matplotlib==3.9.2
     # via napari (napari_repo/pyproject.toml)
 matplotlib-inline==0.1.7
     # via
@@ -173,21 +178,19 @@ matplotlib-inline==0.1.7
     #   ipython
 mdurl==0.1.2
     # via markdown-it-py
-ml-dtypes==0.4.0
+ml-dtypes==0.5.0
     # via tensorstore
 mouseinfo==0.1.3
     # via pyautogui
 mpmath==1.3.0
     # via sympy
-napari-console==0.0.9
+napari-console==0.1.1
     # via napari (napari_repo/pyproject.toml)
 napari-plugin-engine==0.2.0
-    # via
-    #   napari (napari_repo/pyproject.toml)
-    #   napari-svg
-napari-plugin-manager==0.1.0a2
     # via napari (napari_repo/pyproject.toml)
-napari-svg==0.1.10
+napari-plugin-manager==0.1.3
+    # via napari (napari_repo/pyproject.toml)
+napari-svg==0.2.0
     # via napari (napari_repo/pyproject.toml)
 nest-asyncio==1.6.0
     # via ipykernel
@@ -195,7 +198,7 @@ networkx==3.2.1
     # via
     #   scikit-image
     #   torch
-npe2==0.7.6
+npe2==0.7.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-plugin-manager
@@ -223,40 +226,41 @@ numpy==1.26.4
     #   vispy
     #   xarray
     #   zarr
-numpydoc==1.7.0
+numpydoc==1.8.0
     # via napari (napari_repo/pyproject.toml)
-nvidia-cublas-cu12==12.1.3.1
+nvidia-cublas-cu12==12.4.5.8
     # via
     #   nvidia-cudnn-cu12
     #   nvidia-cusolver-cu12
     #   torch
-nvidia-cuda-cupti-cu12==12.1.105
+nvidia-cuda-cupti-cu12==12.4.127
     # via torch
-nvidia-cuda-nvrtc-cu12==12.1.105
+nvidia-cuda-nvrtc-cu12==12.4.127
     # via torch
-nvidia-cuda-runtime-cu12==12.1.105
+nvidia-cuda-runtime-cu12==12.4.127
     # via torch
-nvidia-cudnn-cu12==8.9.2.26
+nvidia-cudnn-cu12==9.1.0.70
     # via torch
-nvidia-cufft-cu12==11.0.2.54
+nvidia-cufft-cu12==11.2.1.3
     # via torch
-nvidia-curand-cu12==10.3.2.106
+nvidia-curand-cu12==10.3.5.147
     # via torch
-nvidia-cusolver-cu12==11.4.5.107
+nvidia-cusolver-cu12==11.6.1.9
     # via torch
-nvidia-cusparse-cu12==12.1.0.106
+nvidia-cusparse-cu12==12.3.1.170
     # via
     #   nvidia-cusolver-cu12
     #   torch
-nvidia-nccl-cu12==2.20.5
+nvidia-nccl-cu12==2.21.5
     # via torch
-nvidia-nvjitlink-cu12==12.5.40
+nvidia-nvjitlink-cu12==12.4.127
     # via
     #   nvidia-cusolver-cu12
     #   nvidia-cusparse-cu12
-nvidia-nvtx-cu12==12.1.105
+    #   torch
+nvidia-nvtx-cu12==12.4.127
     # via torch
-packaging==24.1
+packaging==24.2
     # via
     #   build
     #   dask
@@ -271,7 +275,7 @@ packaging==24.1
     #   sphinx
     #   vispy
     #   xarray
-pandas==2.2.2
+pandas==2.2.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   xarray
@@ -281,20 +285,21 @@ partd==1.4.2
     # via dask
 pexpect==4.9.0
     # via ipython
-pillow==10.3.0
+pillow==11.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   imageio
     #   matplotlib
     #   pyscreeze
     #   scikit-image
-pint==0.23
+pint==0.24.4
     # via napari (napari_repo/pyproject.toml)
-pip==24.1
+pip==24.3.1
     # via napari-plugin-manager
-platformdirs==4.2.2
+platformdirs==4.3.6
     # via
     #   jupyter-core
+    #   pint
     #   pooch
     #   virtualenv
 pluggy==1.5.0
@@ -307,9 +312,9 @@ pooch==1.8.2
     #   scikit-image
 pretend==1.0.9
     # via napari (napari_repo/pyproject.toml)
-prompt-toolkit==3.0.47
+prompt-toolkit==3.0.48
     # via ipython
-psutil==6.0.0
+psutil==6.1.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
@@ -321,13 +326,13 @@ psygnal==0.11.1
     #   npe2
 ptyprocess==0.7.0
     # via pexpect
-pure-eval==0.2.2
+pure-eval==0.2.3
     # via stack-data
 pyautogui==0.9.54
     # via napari (napari_repo/pyproject.toml)
 pyconify==0.1.6
     # via superqt
-pydantic==2.7.4
+pydantic==2.9.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   app-model
@@ -335,7 +340,7 @@ pydantic==2.7.4
     #   pydantic-compat
 pydantic-compat==0.1.2
     # via app-model
-pydantic-core==2.18.4
+pydantic-core==2.23.4
     # via pydantic
 pygetwindow==0.0.9
     # via pyautogui
@@ -353,27 +358,27 @@ pyopengl==3.1.6
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-pyparsing==3.1.2
+pyparsing==3.2.0
     # via matplotlib
 pyperclip==1.9.0
     # via mouseinfo
-pyproject-hooks==1.1.0
+pyproject-hooks==1.2.0
     # via build
-pyqt5==5.15.10
+pyqt5==5.15.11
     # via napari (napari_repo/pyproject.toml)
-pyqt5-qt5==5.15.2
+pyqt5-qt5==5.15.15
     # via pyqt5
-pyqt5-sip==12.13.0
+pyqt5-sip==12.15.0
     # via pyqt5
-pyqt6==6.7.0
+pyqt6==6.7.1
     # via napari (napari_repo/pyproject.toml)
-pyqt6-qt6==6.7.2
+pyqt6-qt6==6.7.3
     # via pyqt6
-pyqt6-sip==13.6.0
+pyqt6-sip==13.8.0
     # via pyqt6
 pyrect==0.2.0
     # via pygetwindow
-pyscreeze==0.1.30
+pyscreeze==1.0.1
     # via pyautogui
 pyside2==5.15.2.1
     # via napari (napari_repo/pyproject.toml)
@@ -387,7 +392,7 @@ pyside6-essentials==6.3.1
     # via
     #   pyside6
     #   pyside6-addons
-pytest==8.2.2
+pytest==8.3.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
@@ -395,7 +400,7 @@ pytest==8.2.2
     #   pytest-metadata
     #   pytest-pretty
     #   pytest-qt
-pytest-cov==5.0.0
+pytest-cov==6.0.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
 pytest-json-report==1.5.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
@@ -416,23 +421,22 @@ python3-xlib==0.15
     #   pyautogui
 pytweening==1.2.0
     # via pyautogui
-pytz==2024.1
+pytz==2024.2
     # via pandas
-pyyaml==6.0.1
+pyyaml==6.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   npe2
-pyzmq==26.0.3
+pyzmq==26.2.0
     # via
     #   ipykernel
     #   jupyter-client
-    #   qtconsole
-qtconsole==5.5.2
+qtconsole==5.6.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-console
-qtpy==2.4.1
+qtpy==2.4.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
@@ -449,13 +453,13 @@ requests==2.32.3
     #   pooch
     #   pyconify
     #   sphinx
-rich==13.7.1
+rich==13.9.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
     #   pytest-pretty
     #   typer
-rpds-py==0.18.1
+rpds-py==0.21.0
     # via
     #   jsonschema
     #   referencing
@@ -482,19 +486,19 @@ snowballstemmer==2.2.0
     # via sphinx
 sortedcontainers==2.4.0
     # via hypothesis
-sphinx==7.3.7
+sphinx==7.4.7
     # via numpydoc
-sphinxcontrib-applehelp==1.0.8
+sphinxcontrib-applehelp==2.0.0
     # via sphinx
-sphinxcontrib-devhelp==1.0.6
+sphinxcontrib-devhelp==2.0.0
     # via sphinx
-sphinxcontrib-htmlhelp==2.0.5
+sphinxcontrib-htmlhelp==2.1.0
     # via sphinx
 sphinxcontrib-jsmath==1.0.1
     # via sphinx
-sphinxcontrib-qthelp==1.0.7
+sphinxcontrib-qthelp==2.0.0
     # via sphinx
-sphinxcontrib-serializinghtml==1.1.10
+sphinxcontrib-serializinghtml==2.0.0
     # via sphinx
 stack-data==0.6.3
     # via ipython
@@ -503,19 +507,19 @@ superqt==0.6.7
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
     #   napari-plugin-manager
-sympy==1.12.1
+sympy==1.13.1
     # via torch
 tabulate==0.9.0
     # via numpydoc
-tensorstore==0.1.63
+tensorstore==0.1.68
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-tifffile==2024.6.18
+tifffile==2024.8.30
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
-tomli==2.0.1
+tomli==2.1.0
     # via
     #   build
     #   coverage
@@ -523,20 +527,20 @@ tomli==2.0.1
     #   numpydoc
     #   pytest
     #   sphinx
-tomli-w==1.0.0
+tomli-w==1.1.0
     # via npe2
-toolz==0.12.1
+toolz==1.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   partd
-torch==2.3.1
+torch==2.5.1
     # via napari (napari_repo/pyproject.toml)
 tornado==6.4.1
     # via
     #   ipykernel
     #   jupyter-client
-tqdm==4.66.4
+tqdm==4.67.0
     # via napari (napari_repo/pyproject.toml)
 traitlets==5.14.3
     # via
@@ -549,27 +553,30 @@ traitlets==5.14.3
     #   qtconsole
 triangle==20230923
     # via napari (napari_repo/pyproject.toml)
-triton==2.3.1
+triton==3.1.0
     # via torch
-typer==0.12.3
+typer==0.13.0
     # via npe2
 typing-extensions==4.12.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   app-model
+    #   flexcache
+    #   flexparser
     #   ipython
     #   magicgui
     #   pint
     #   pydantic
     #   pydantic-core
+    #   rich
     #   superqt
     #   torch
     #   typer
-tzdata==2024.1
+tzdata==2024.2
     # via pandas
-urllib3==2.2.2
+urllib3==2.2.3
     # via requests
-virtualenv==20.26.3
+virtualenv==20.27.1
     # via napari (napari_repo/pyproject.toml)
 vispy==0.14.3
     # via
@@ -579,11 +586,11 @@ wcwidth==0.2.13
     # via prompt-toolkit
 wrapt==1.16.0
     # via napari (napari_repo/pyproject.toml)
-xarray==2024.6.0
+xarray==2024.7.0
     # via napari (napari_repo/pyproject.toml)
 zarr==2.18.2
     # via napari (napari_repo/pyproject.toml)
-zipp==3.19.2
+zipp==3.21.0
     # via
     #   importlib-metadata
     #   importlib-resources
diff --git a/resources/constraints/constraints_py3.9_examples.txt b/resources/constraints/constraints_py3.9_examples.txt
index 497eedcf12c..91175907d0a 100644
--- a/resources/constraints/constraints_py3.9_examples.txt
+++ b/resources/constraints/constraints_py3.9_examples.txt
@@ -1,10 +1,10 @@
 # This file was autogenerated by uv via the following command:
-#    uv pip compile --python-version 3.9 --output-file napari_repo/resources/constraints/constraints_py3.9_examples.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt resources/constraints/version_denylist_examples.txt --extra pyqt5 --extra pyqt6_experimental --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
+#    uv pip compile --python-version 3.9 --output-file napari_repo/resources/constraints/constraints_py3.9_examples.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt resources/constraints/version_denylist_examples.txt --extra pyqt5 --extra pyqt6 --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
 alabaster==0.7.16
     # via sphinx
 annotated-types==0.7.0
     # via pydantic
-app-model==0.2.7
+app-model==0.3.0
     # via napari (napari_repo/pyproject.toml)
 appdirs==1.4.4
     # via
@@ -14,48 +14,48 @@ asciitree==0.3.3
     # via zarr
 asttokens==2.4.1
     # via stack-data
-attrs==23.2.0
+attrs==24.2.0
     # via
     #   hypothesis
     #   jsonschema
     #   referencing
-babel==2.15.0
+babel==2.16.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   sphinx
-build==1.2.1
+build==1.2.2.post1
     # via npe2
 cachey==0.2.1
     # via napari (napari_repo/pyproject.toml)
-certifi==2024.7.4
+certifi==2024.8.30
     # via
     #   napari (napari_repo/pyproject.toml)
     #   requests
-charset-normalizer==3.3.2
+charset-normalizer==3.4.0
     # via requests
 click==8.1.7
     # via
     #   dask
     #   typer
-cloudpickle==3.0.0
+cloudpickle==3.1.0
     # via dask
 comm==0.2.2
     # via ipykernel
-contourpy==1.2.1
+contourpy==1.3.0
     # via matplotlib
-coverage==7.5.4
+coverage==7.6.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
 cycler==0.12.1
     # via matplotlib
-dask==2024.6.2
+dask==2024.8.0
     # via napari (napari_repo/pyproject.toml)
-debugpy==1.8.1
+debugpy==1.8.8
     # via ipykernel
 decorator==5.1.1
     # via ipython
-distlib==0.3.8
+distlib==0.3.9
     # via virtualenv
 docstring-parser==0.16
     # via
@@ -63,25 +63,29 @@ docstring-parser==0.16
     #   magicgui
 docutils==0.21.2
     # via sphinx
-exceptiongroup==1.2.1
+exceptiongroup==1.2.2
     # via
     #   hypothesis
     #   ipython
     #   pytest
-executing==2.0.1
+executing==2.1.0
     # via stack-data
 fasteners==0.19
     # via zarr
-filelock==3.15.4
+filelock==3.16.1
     # via
     #   torch
     #   triton
     #   virtualenv
-fonttools==4.53.0
+flexcache==0.3
+    # via pint
+flexparser==0.4
+    # via pint
+fonttools==4.55.0
     # via matplotlib
-freetype-py==2.4.0
+freetype-py==2.5.1
     # via vispy
-fsspec==2024.6.0
+fsspec==2024.10.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
@@ -90,31 +94,34 @@ heapdict==1.0.1
     # via cachey
 hsluv==5.0.4
     # via vispy
-hypothesis==6.103.4
+hypothesis==6.119.3
     # via napari (napari_repo/pyproject.toml)
-idna==3.7
+idna==3.10
     # via requests
-imageio==2.34.1
+imageio==2.36.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-svg
     #   scikit-image
 imagesize==1.4.1
     # via sphinx
-importlib-metadata==7.2.1
+importlib-metadata==8.5.0
     # via
     #   build
     #   dask
     #   jupyter-client
     #   sphinx
-importlib-resources==6.4.0
-    # via matplotlib
+importlib-resources==6.4.5
+    # via
+    #   matplotlib
+    #   nibabel
 in-n-out==0.2.1
     # via app-model
 iniconfig==2.0.0
     # via pytest
-ipykernel==6.29.4
+ipykernel==6.29.5
     # via
+    #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari-console
     #   qtconsole
 ipython==8.18.1
@@ -122,7 +129,7 @@ ipython==8.18.1
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
     #   napari-console
-jedi==0.19.1
+jedi==0.19.2
     # via ipython
 jinja2==3.1.4
     # via
@@ -132,11 +139,11 @@ joblib==1.4.2
     # via
     #   nilearn
     #   scikit-learn
-jsonschema==4.22.0
+jsonschema==4.23.0
     # via napari (napari_repo/pyproject.toml)
-jsonschema-specifications==2023.12.1
+jsonschema-specifications==2024.10.1
     # via jsonschema
-jupyter-client==8.6.2
+jupyter-client==8.6.3
     # via
     #   ipykernel
     #   qtconsole
@@ -145,7 +152,7 @@ jupyter-core==5.7.2
     #   ipykernel
     #   jupyter-client
     #   qtconsole
-kiwisolver==1.4.5
+kiwisolver==1.4.7
     # via
     #   matplotlib
     #   vispy
@@ -157,20 +164,20 @@ llvmlite==0.43.0
     # via numba
 locket==1.0.0
     # via partd
-lxml==5.2.2
+lxml==5.3.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   lxml-html-clean
     #   nilearn
-lxml-html-clean==0.1.1
+lxml-html-clean==0.4.1
     # via lxml
-magicgui==0.8.3
+magicgui==0.9.1
     # via napari (napari_repo/pyproject.toml)
 markdown-it-py==3.0.0
     # via rich
-markupsafe==2.1.5
+markupsafe==3.0.2
     # via jinja2
-matplotlib==3.9.0
+matplotlib==3.9.2
     # via napari (napari_repo/pyproject.toml)
 matplotlib-inline==0.1.7
     # via
@@ -178,21 +185,19 @@ matplotlib-inline==0.1.7
     #   ipython
 mdurl==0.1.2
     # via markdown-it-py
-ml-dtypes==0.4.0
+ml-dtypes==0.5.0
     # via tensorstore
 mouseinfo==0.1.3
     # via pyautogui
 mpmath==1.3.0
     # via sympy
-napari-console==0.0.9
+napari-console==0.1.1
     # via napari (napari_repo/pyproject.toml)
 napari-plugin-engine==0.2.0
-    # via
-    #   napari (napari_repo/pyproject.toml)
-    #   napari-svg
-napari-plugin-manager==0.1.0a2
     # via napari (napari_repo/pyproject.toml)
-napari-svg==0.1.10
+napari-plugin-manager==0.1.3
+    # via napari (napari_repo/pyproject.toml)
+napari-svg==0.2.0
     # via napari (napari_repo/pyproject.toml)
 nest-asyncio==1.6.0
     # via ipykernel
@@ -200,11 +205,11 @@ networkx==3.2.1
     # via
     #   scikit-image
     #   torch
-nibabel==5.2.1
+nibabel==5.3.2
     # via nilearn
 nilearn==0.10.4
     # via -r resources/constraints/version_denylist_examples.txt
-npe2==0.7.6
+npe2==0.7.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-plugin-manager
@@ -236,40 +241,41 @@ numpy==1.23.5
     #   vispy
     #   xarray
     #   zarr
-numpydoc==1.7.0
+numpydoc==1.8.0
     # via napari (napari_repo/pyproject.toml)
-nvidia-cublas-cu12==12.1.3.1
+nvidia-cublas-cu12==12.4.5.8
     # via
     #   nvidia-cudnn-cu12
     #   nvidia-cusolver-cu12
     #   torch
-nvidia-cuda-cupti-cu12==12.1.105
+nvidia-cuda-cupti-cu12==12.4.127
     # via torch
-nvidia-cuda-nvrtc-cu12==12.1.105
+nvidia-cuda-nvrtc-cu12==12.4.127
     # via torch
-nvidia-cuda-runtime-cu12==12.1.105
+nvidia-cuda-runtime-cu12==12.4.127
     # via torch
-nvidia-cudnn-cu12==8.9.2.26
+nvidia-cudnn-cu12==9.1.0.70
     # via torch
-nvidia-cufft-cu12==11.0.2.54
+nvidia-cufft-cu12==11.2.1.3
     # via torch
-nvidia-curand-cu12==10.3.2.106
+nvidia-curand-cu12==10.3.5.147
     # via torch
-nvidia-cusolver-cu12==11.4.5.107
+nvidia-cusolver-cu12==11.6.1.9
     # via torch
-nvidia-cusparse-cu12==12.1.0.106
+nvidia-cusparse-cu12==12.3.1.170
     # via
     #   nvidia-cusolver-cu12
     #   torch
-nvidia-nccl-cu12==2.20.5
+nvidia-nccl-cu12==2.21.5
     # via torch
-nvidia-nvjitlink-cu12==12.5.40
+nvidia-nvjitlink-cu12==12.4.127
     # via
     #   nvidia-cusolver-cu12
     #   nvidia-cusparse-cu12
-nvidia-nvtx-cu12==12.1.105
+    #   torch
+nvidia-nvtx-cu12==12.4.127
     # via torch
-packaging==24.1
+packaging==24.2
     # via
     #   -r resources/constraints/version_denylist_examples.txt
     #   build
@@ -287,7 +293,7 @@ packaging==24.1
     #   sphinx
     #   vispy
     #   xarray
-pandas==2.2.2
+pandas==2.2.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   nilearn
@@ -298,20 +304,21 @@ partd==1.4.2
     # via dask
 pexpect==4.9.0
     # via ipython
-pillow==10.3.0
+pillow==11.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   imageio
     #   matplotlib
     #   pyscreeze
     #   scikit-image
-pint==0.23
+pint==0.24.4
     # via napari (napari_repo/pyproject.toml)
-pip==24.1
+pip==24.3.1
     # via napari-plugin-manager
-platformdirs==4.2.2
+platformdirs==4.3.6
     # via
     #   jupyter-core
+    #   pint
     #   pooch
     #   virtualenv
 pluggy==1.5.0
@@ -324,9 +331,9 @@ pooch==1.8.2
     #   scikit-image
 pretend==1.0.9
     # via napari (napari_repo/pyproject.toml)
-prompt-toolkit==3.0.47
+prompt-toolkit==3.0.48
     # via ipython
-psutil==6.0.0
+psutil==6.1.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
@@ -338,13 +345,13 @@ psygnal==0.11.1
     #   npe2
 ptyprocess==0.7.0
     # via pexpect
-pure-eval==0.2.2
+pure-eval==0.2.3
     # via stack-data
 pyautogui==0.9.54
     # via napari (napari_repo/pyproject.toml)
 pyconify==0.1.6
     # via superqt
-pydantic==2.7.4
+pydantic==2.9.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   app-model
@@ -352,7 +359,7 @@ pydantic==2.7.4
     #   pydantic-compat
 pydantic-compat==0.1.2
     # via app-model
-pydantic-core==2.18.4
+pydantic-core==2.23.4
     # via pydantic
 pygetwindow==0.0.9
     # via pyautogui
@@ -370,27 +377,27 @@ pyopengl==3.1.6
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-pyparsing==3.1.2
+pyparsing==3.2.0
     # via matplotlib
 pyperclip==1.9.0
     # via mouseinfo
-pyproject-hooks==1.1.0
+pyproject-hooks==1.2.0
     # via build
-pyqt5==5.15.10
+pyqt5==5.15.11
     # via napari (napari_repo/pyproject.toml)
-pyqt5-qt5==5.15.2
+pyqt5-qt5==5.15.15
     # via pyqt5
-pyqt5-sip==12.13.0
+pyqt5-sip==12.15.0
     # via pyqt5
-pyqt6==6.7.0
+pyqt6==6.7.1
     # via napari (napari_repo/pyproject.toml)
-pyqt6-qt6==6.7.2
+pyqt6-qt6==6.7.3
     # via pyqt6
-pyqt6-sip==13.6.0
+pyqt6-sip==13.8.0
     # via pyqt6
 pyrect==0.2.0
     # via pygetwindow
-pyscreeze==0.1.30
+pyscreeze==1.0.1
     # via pyautogui
 pyside2==5.15.2.1
     # via napari (napari_repo/pyproject.toml)
@@ -404,7 +411,7 @@ pyside6-essentials==6.3.1
     # via
     #   pyside6
     #   pyside6-addons
-pytest==8.2.2
+pytest==8.3.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
@@ -412,7 +419,7 @@ pytest==8.2.2
     #   pytest-metadata
     #   pytest-pretty
     #   pytest-qt
-pytest-cov==5.0.0
+pytest-cov==6.0.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
 pytest-json-report==1.5.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
@@ -433,23 +440,22 @@ python3-xlib==0.15
     #   pyautogui
 pytweening==1.2.0
     # via pyautogui
-pytz==2024.1
+pytz==2024.2
     # via pandas
-pyyaml==6.0.1
+pyyaml==6.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   npe2
-pyzmq==26.0.3
+pyzmq==26.2.0
     # via
     #   ipykernel
     #   jupyter-client
-    #   qtconsole
-qtconsole==5.5.2
+qtconsole==5.6.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-console
-qtpy==2.4.1
+qtpy==2.4.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
@@ -467,19 +473,19 @@ requests==2.32.3
     #   pooch
     #   pyconify
     #   sphinx
-rich==13.7.1
+rich==13.9.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
     #   pytest-pretty
     #   typer
-rpds-py==0.18.1
+rpds-py==0.21.0
     # via
     #   jsonschema
     #   referencing
 scikit-image==0.24.0
     # via napari (napari_repo/pyproject.toml)
-scikit-learn==1.5.0
+scikit-learn==1.5.2
     # via nilearn
 scipy==1.13.1
     # via
@@ -504,19 +510,19 @@ snowballstemmer==2.2.0
     # via sphinx
 sortedcontainers==2.4.0
     # via hypothesis
-sphinx==7.3.7
+sphinx==7.4.7
     # via numpydoc
-sphinxcontrib-applehelp==1.0.8
+sphinxcontrib-applehelp==2.0.0
     # via sphinx
-sphinxcontrib-devhelp==1.0.6
+sphinxcontrib-devhelp==2.0.0
     # via sphinx
-sphinxcontrib-htmlhelp==2.0.5
+sphinxcontrib-htmlhelp==2.1.0
     # via sphinx
 sphinxcontrib-jsmath==1.0.1
     # via sphinx
-sphinxcontrib-qthelp==1.0.7
+sphinxcontrib-qthelp==2.0.0
     # via sphinx
-sphinxcontrib-serializinghtml==1.1.10
+sphinxcontrib-serializinghtml==2.0.0
     # via sphinx
 stack-data==0.6.3
     # via ipython
@@ -525,21 +531,21 @@ superqt==0.6.7
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
     #   napari-plugin-manager
-sympy==1.12.1
+sympy==1.13.1
     # via torch
 tabulate==0.9.0
     # via numpydoc
-tensorstore==0.1.63
+tensorstore==0.1.68
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
 threadpoolctl==3.5.0
     # via scikit-learn
-tifffile==2024.6.18
+tifffile==2024.8.30
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
-tomli==2.0.1
+tomli==2.1.0
     # via
     #   build
     #   coverage
@@ -547,20 +553,20 @@ tomli==2.0.1
     #   numpydoc
     #   pytest
     #   sphinx
-tomli-w==1.0.0
+tomli-w==1.1.0
     # via npe2
-toolz==0.12.1
+toolz==1.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   partd
-torch==2.3.1
+torch==2.5.1
     # via napari (napari_repo/pyproject.toml)
 tornado==6.4.1
     # via
     #   ipykernel
     #   jupyter-client
-tqdm==4.66.4
+tqdm==4.67.0
     # via napari (napari_repo/pyproject.toml)
 traitlets==5.14.3
     # via
@@ -573,27 +579,31 @@ traitlets==5.14.3
     #   qtconsole
 triangle==20230923
     # via napari (napari_repo/pyproject.toml)
-triton==2.3.1
+triton==3.1.0
     # via torch
-typer==0.12.3
+typer==0.13.0
     # via npe2
 typing-extensions==4.12.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   app-model
+    #   flexcache
+    #   flexparser
     #   ipython
     #   magicgui
+    #   nibabel
     #   pint
     #   pydantic
     #   pydantic-core
+    #   rich
     #   superqt
     #   torch
     #   typer
-tzdata==2024.1
+tzdata==2024.2
     # via pandas
-urllib3==2.2.2
+urllib3==2.2.3
     # via requests
-virtualenv==20.26.3
+virtualenv==20.27.1
     # via napari (napari_repo/pyproject.toml)
 vispy==0.14.3
     # via
@@ -603,11 +613,11 @@ wcwidth==0.2.13
     # via prompt-toolkit
 wrapt==1.16.0
     # via napari (napari_repo/pyproject.toml)
-xarray==2024.6.0
+xarray==2024.7.0
     # via napari (napari_repo/pyproject.toml)
 zarr==2.18.2
     # via napari (napari_repo/pyproject.toml)
-zipp==3.19.2
+zipp==3.21.0
     # via
     #   importlib-metadata
     #   importlib-resources
diff --git a/resources/constraints/constraints_py3.9_min_req.txt b/resources/constraints/constraints_py3.9_min_req.txt
index e69de29bb2d..21e0607e25c 100644
--- a/resources/constraints/constraints_py3.9_min_req.txt
+++ b/resources/constraints/constraints_py3.9_min_req.txt
@@ -0,0 +1 @@
+setuptools<70
diff --git a/resources/constraints/constraints_py3.9_pydantic_1.txt b/resources/constraints/constraints_py3.9_pydantic_1.txt
index 2ca22e0faf0..267600122dd 100644
--- a/resources/constraints/constraints_py3.9_pydantic_1.txt
+++ b/resources/constraints/constraints_py3.9_pydantic_1.txt
@@ -1,8 +1,8 @@
 # This file was autogenerated by uv via the following command:
-#    uv pip compile --python-version 3.9 --output-file napari_repo/resources/constraints/constraints_py3.9_pydantic_1.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt napari_repo/resources/constraints/pydantic_le_2.txt --extra pyqt5 --extra pyqt6_experimental --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
+#    uv pip compile --python-version 3.9 --output-file napari_repo/resources/constraints/constraints_py3.9_pydantic_1.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt napari_repo/resources/constraints/pydantic_le_2.txt --extra pyqt5 --extra pyqt6 --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
 alabaster==0.7.16
     # via sphinx
-app-model==0.2.7
+app-model==0.3.0
     # via napari (napari_repo/pyproject.toml)
 appdirs==1.4.4
     # via
@@ -12,48 +12,48 @@ asciitree==0.3.3
     # via zarr
 asttokens==2.4.1
     # via stack-data
-attrs==23.2.0
+attrs==24.2.0
     # via
     #   hypothesis
     #   jsonschema
     #   referencing
-babel==2.15.0
+babel==2.16.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   sphinx
-build==1.2.1
+build==1.2.2.post1
     # via npe2
 cachey==0.2.1
     # via napari (napari_repo/pyproject.toml)
-certifi==2024.7.4
+certifi==2024.8.30
     # via
     #   napari (napari_repo/pyproject.toml)
     #   requests
-charset-normalizer==3.3.2
+charset-normalizer==3.4.0
     # via requests
 click==8.1.7
     # via
     #   dask
     #   typer
-cloudpickle==3.0.0
+cloudpickle==3.1.0
     # via dask
 comm==0.2.2
     # via ipykernel
-contourpy==1.2.1
+contourpy==1.3.0
     # via matplotlib
-coverage==7.5.4
+coverage==7.6.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
 cycler==0.12.1
     # via matplotlib
-dask==2024.6.2
+dask==2024.8.0
     # via napari (napari_repo/pyproject.toml)
-debugpy==1.8.1
+debugpy==1.8.8
     # via ipykernel
 decorator==5.1.1
     # via ipython
-distlib==0.3.8
+distlib==0.3.9
     # via virtualenv
 docstring-parser==0.16
     # via
@@ -61,25 +61,29 @@ docstring-parser==0.16
     #   magicgui
 docutils==0.21.2
     # via sphinx
-exceptiongroup==1.2.1
+exceptiongroup==1.2.2
     # via
     #   hypothesis
     #   ipython
     #   pytest
-executing==2.0.1
+executing==2.1.0
     # via stack-data
 fasteners==0.19
     # via zarr
-filelock==3.15.4
+filelock==3.16.1
     # via
     #   torch
     #   triton
     #   virtualenv
-fonttools==4.53.0
+flexcache==0.3
+    # via pint
+flexparser==0.4
+    # via pint
+fonttools==4.55.0
     # via matplotlib
-freetype-py==2.4.0
+freetype-py==2.5.1
     # via vispy
-fsspec==2024.6.0
+fsspec==2024.10.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
@@ -88,31 +92,32 @@ heapdict==1.0.1
     # via cachey
 hsluv==5.0.4
     # via vispy
-hypothesis==6.103.4
+hypothesis==6.119.3
     # via napari (napari_repo/pyproject.toml)
-idna==3.7
+idna==3.10
     # via requests
-imageio==2.34.1
+imageio==2.36.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-svg
     #   scikit-image
 imagesize==1.4.1
     # via sphinx
-importlib-metadata==7.2.1
+importlib-metadata==8.5.0
     # via
     #   build
     #   dask
     #   jupyter-client
     #   sphinx
-importlib-resources==6.4.0
+importlib-resources==6.4.5
     # via matplotlib
 in-n-out==0.2.1
     # via app-model
 iniconfig==2.0.0
     # via pytest
-ipykernel==6.29.4
+ipykernel==6.29.5
     # via
+    #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari-console
     #   qtconsole
 ipython==8.18.1
@@ -120,17 +125,17 @@ ipython==8.18.1
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
     #   napari-console
-jedi==0.19.1
+jedi==0.19.2
     # via ipython
 jinja2==3.1.4
     # via
     #   sphinx
     #   torch
-jsonschema==4.22.0
+jsonschema==4.23.0
     # via napari (napari_repo/pyproject.toml)
-jsonschema-specifications==2023.12.1
+jsonschema-specifications==2024.10.1
     # via jsonschema
-jupyter-client==8.6.2
+jupyter-client==8.6.3
     # via
     #   ipykernel
     #   qtconsole
@@ -139,7 +144,7 @@ jupyter-core==5.7.2
     #   ipykernel
     #   jupyter-client
     #   qtconsole
-kiwisolver==1.4.5
+kiwisolver==1.4.7
     # via
     #   matplotlib
     #   vispy
@@ -151,19 +156,19 @@ llvmlite==0.43.0
     # via numba
 locket==1.0.0
     # via partd
-lxml==5.2.2
+lxml==5.3.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   lxml-html-clean
-lxml-html-clean==0.1.1
+lxml-html-clean==0.4.1
     # via lxml
-magicgui==0.8.3
+magicgui==0.9.1
     # via napari (napari_repo/pyproject.toml)
 markdown-it-py==3.0.0
     # via rich
-markupsafe==2.1.5
+markupsafe==3.0.2
     # via jinja2
-matplotlib==3.9.0
+matplotlib==3.9.2
     # via napari (napari_repo/pyproject.toml)
 matplotlib-inline==0.1.7
     # via
@@ -171,21 +176,19 @@ matplotlib-inline==0.1.7
     #   ipython
 mdurl==0.1.2
     # via markdown-it-py
-ml-dtypes==0.4.0
+ml-dtypes==0.5.0
     # via tensorstore
 mouseinfo==0.1.3
     # via pyautogui
 mpmath==1.3.0
     # via sympy
-napari-console==0.0.9
+napari-console==0.1.1
     # via napari (napari_repo/pyproject.toml)
 napari-plugin-engine==0.2.0
-    # via
-    #   napari (napari_repo/pyproject.toml)
-    #   napari-svg
-napari-plugin-manager==0.1.0a2
     # via napari (napari_repo/pyproject.toml)
-napari-svg==0.1.10
+napari-plugin-manager==0.1.3
+    # via napari (napari_repo/pyproject.toml)
+napari-svg==0.2.0
     # via napari (napari_repo/pyproject.toml)
 nest-asyncio==1.6.0
     # via ipykernel
@@ -193,7 +196,7 @@ networkx==3.2.1
     # via
     #   scikit-image
     #   torch
-npe2==0.7.6
+npe2==0.7.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-plugin-manager
@@ -221,40 +224,41 @@ numpy==1.26.4
     #   vispy
     #   xarray
     #   zarr
-numpydoc==1.7.0
+numpydoc==1.8.0
     # via napari (napari_repo/pyproject.toml)
-nvidia-cublas-cu12==12.1.3.1
+nvidia-cublas-cu12==12.4.5.8
     # via
     #   nvidia-cudnn-cu12
     #   nvidia-cusolver-cu12
     #   torch
-nvidia-cuda-cupti-cu12==12.1.105
+nvidia-cuda-cupti-cu12==12.4.127
     # via torch
-nvidia-cuda-nvrtc-cu12==12.1.105
+nvidia-cuda-nvrtc-cu12==12.4.127
     # via torch
-nvidia-cuda-runtime-cu12==12.1.105
+nvidia-cuda-runtime-cu12==12.4.127
     # via torch
-nvidia-cudnn-cu12==8.9.2.26
+nvidia-cudnn-cu12==9.1.0.70
     # via torch
-nvidia-cufft-cu12==11.0.2.54
+nvidia-cufft-cu12==11.2.1.3
     # via torch
-nvidia-curand-cu12==10.3.2.106
+nvidia-curand-cu12==10.3.5.147
     # via torch
-nvidia-cusolver-cu12==11.4.5.107
+nvidia-cusolver-cu12==11.6.1.9
     # via torch
-nvidia-cusparse-cu12==12.1.0.106
+nvidia-cusparse-cu12==12.3.1.170
     # via
     #   nvidia-cusolver-cu12
     #   torch
-nvidia-nccl-cu12==2.20.5
+nvidia-nccl-cu12==2.21.5
     # via torch
-nvidia-nvjitlink-cu12==12.5.40
+nvidia-nvjitlink-cu12==12.4.127
     # via
     #   nvidia-cusolver-cu12
     #   nvidia-cusparse-cu12
-nvidia-nvtx-cu12==12.1.105
+    #   torch
+nvidia-nvtx-cu12==12.4.127
     # via torch
-packaging==24.1
+packaging==24.2
     # via
     #   build
     #   dask
@@ -269,7 +273,7 @@ packaging==24.1
     #   sphinx
     #   vispy
     #   xarray
-pandas==2.2.2
+pandas==2.2.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   xarray
@@ -279,20 +283,21 @@ partd==1.4.2
     # via dask
 pexpect==4.9.0
     # via ipython
-pillow==10.3.0
+pillow==11.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   imageio
     #   matplotlib
     #   pyscreeze
     #   scikit-image
-pint==0.23
+pint==0.24.4
     # via napari (napari_repo/pyproject.toml)
-pip==24.1
+pip==24.3.1
     # via napari-plugin-manager
-platformdirs==4.2.2
+platformdirs==4.3.6
     # via
     #   jupyter-core
+    #   pint
     #   pooch
     #   virtualenv
 pluggy==1.5.0
@@ -305,9 +310,9 @@ pooch==1.8.2
     #   scikit-image
 pretend==1.0.9
     # via napari (napari_repo/pyproject.toml)
-prompt-toolkit==3.0.47
+prompt-toolkit==3.0.48
     # via ipython
-psutil==6.0.0
+psutil==6.1.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
@@ -319,13 +324,13 @@ psygnal==0.11.1
     #   npe2
 ptyprocess==0.7.0
     # via pexpect
-pure-eval==0.2.2
+pure-eval==0.2.3
     # via stack-data
 pyautogui==0.9.54
     # via napari (napari_repo/pyproject.toml)
 pyconify==0.1.6
     # via superqt
-pydantic==1.10.17
+pydantic==1.10.19
     # via
     #   -r napari_repo/resources/constraints/pydantic_le_2.txt
     #   napari (napari_repo/pyproject.toml)
@@ -350,27 +355,27 @@ pyopengl==3.1.6
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-pyparsing==3.1.2
+pyparsing==3.2.0
     # via matplotlib
 pyperclip==1.9.0
     # via mouseinfo
-pyproject-hooks==1.1.0
+pyproject-hooks==1.2.0
     # via build
-pyqt5==5.15.10
+pyqt5==5.15.11
     # via napari (napari_repo/pyproject.toml)
-pyqt5-qt5==5.15.2
+pyqt5-qt5==5.15.15
     # via pyqt5
-pyqt5-sip==12.13.0
+pyqt5-sip==12.15.0
     # via pyqt5
-pyqt6==6.7.0
+pyqt6==6.7.1
     # via napari (napari_repo/pyproject.toml)
-pyqt6-qt6==6.7.2
+pyqt6-qt6==6.7.3
     # via pyqt6
-pyqt6-sip==13.6.0
+pyqt6-sip==13.8.0
     # via pyqt6
 pyrect==0.2.0
     # via pygetwindow
-pyscreeze==0.1.30
+pyscreeze==1.0.1
     # via pyautogui
 pyside2==5.15.2.1
     # via napari (napari_repo/pyproject.toml)
@@ -384,7 +389,7 @@ pyside6-essentials==6.3.1
     # via
     #   pyside6
     #   pyside6-addons
-pytest==8.2.2
+pytest==8.3.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
@@ -392,7 +397,7 @@ pytest==8.2.2
     #   pytest-metadata
     #   pytest-pretty
     #   pytest-qt
-pytest-cov==5.0.0
+pytest-cov==6.0.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
 pytest-json-report==1.5.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
@@ -413,23 +418,22 @@ python3-xlib==0.15
     #   pyautogui
 pytweening==1.2.0
     # via pyautogui
-pytz==2024.1
+pytz==2024.2
     # via pandas
-pyyaml==6.0.1
+pyyaml==6.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   npe2
-pyzmq==26.0.3
+pyzmq==26.2.0
     # via
     #   ipykernel
     #   jupyter-client
-    #   qtconsole
-qtconsole==5.5.2
+qtconsole==5.6.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-console
-qtpy==2.4.1
+qtpy==2.4.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
@@ -446,13 +450,13 @@ requests==2.32.3
     #   pooch
     #   pyconify
     #   sphinx
-rich==13.7.1
+rich==13.9.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
     #   pytest-pretty
     #   typer
-rpds-py==0.18.1
+rpds-py==0.21.0
     # via
     #   jsonschema
     #   referencing
@@ -479,19 +483,19 @@ snowballstemmer==2.2.0
     # via sphinx
 sortedcontainers==2.4.0
     # via hypothesis
-sphinx==7.3.7
+sphinx==7.4.7
     # via numpydoc
-sphinxcontrib-applehelp==1.0.8
+sphinxcontrib-applehelp==2.0.0
     # via sphinx
-sphinxcontrib-devhelp==1.0.6
+sphinxcontrib-devhelp==2.0.0
     # via sphinx
-sphinxcontrib-htmlhelp==2.0.5
+sphinxcontrib-htmlhelp==2.1.0
     # via sphinx
 sphinxcontrib-jsmath==1.0.1
     # via sphinx
-sphinxcontrib-qthelp==1.0.7
+sphinxcontrib-qthelp==2.0.0
     # via sphinx
-sphinxcontrib-serializinghtml==1.1.10
+sphinxcontrib-serializinghtml==2.0.0
     # via sphinx
 stack-data==0.6.3
     # via ipython
@@ -500,19 +504,19 @@ superqt==0.6.7
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
     #   napari-plugin-manager
-sympy==1.12.1
+sympy==1.13.1
     # via torch
 tabulate==0.9.0
     # via numpydoc
-tensorstore==0.1.63
+tensorstore==0.1.68
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-tifffile==2024.6.18
+tifffile==2024.8.30
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
-tomli==2.0.1
+tomli==2.1.0
     # via
     #   build
     #   coverage
@@ -520,20 +524,20 @@ tomli==2.0.1
     #   numpydoc
     #   pytest
     #   sphinx
-tomli-w==1.0.0
+tomli-w==1.1.0
     # via npe2
-toolz==0.12.1
+toolz==1.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   partd
-torch==2.3.1
+torch==2.5.1
     # via napari (napari_repo/pyproject.toml)
 tornado==6.4.1
     # via
     #   ipykernel
     #   jupyter-client
-tqdm==4.66.4
+tqdm==4.67.0
     # via napari (napari_repo/pyproject.toml)
 traitlets==5.14.3
     # via
@@ -546,26 +550,29 @@ traitlets==5.14.3
     #   qtconsole
 triangle==20230923
     # via napari (napari_repo/pyproject.toml)
-triton==2.3.1
+triton==3.1.0
     # via torch
-typer==0.12.3
+typer==0.13.0
     # via npe2
 typing-extensions==4.12.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   app-model
+    #   flexcache
+    #   flexparser
     #   ipython
     #   magicgui
     #   pint
     #   pydantic
+    #   rich
     #   superqt
     #   torch
     #   typer
-tzdata==2024.1
+tzdata==2024.2
     # via pandas
-urllib3==2.2.2
+urllib3==2.2.3
     # via requests
-virtualenv==20.26.3
+virtualenv==20.27.1
     # via napari (napari_repo/pyproject.toml)
 vispy==0.14.3
     # via
@@ -575,11 +582,11 @@ wcwidth==0.2.13
     # via prompt-toolkit
 wrapt==1.16.0
     # via napari (napari_repo/pyproject.toml)
-xarray==2024.6.0
+xarray==2024.7.0
     # via napari (napari_repo/pyproject.toml)
 zarr==2.18.2
     # via napari (napari_repo/pyproject.toml)
-zipp==3.19.2
+zipp==3.21.0
     # via
     #   importlib-metadata
     #   importlib-resources
diff --git a/resources/constraints/constraints_py3.9_macos_arm.txt b/resources/constraints/constraints_py3.9_windows.txt
similarity index 79%
rename from resources/constraints/constraints_py3.9_macos_arm.txt
rename to resources/constraints/constraints_py3.9_windows.txt
index 68ddf2ebb7f..20efd49bfa8 100644
--- a/resources/constraints/constraints_py3.9_macos_arm.txt
+++ b/resources/constraints/constraints_py3.9_windows.txt
@@ -1,63 +1,69 @@
 # This file was autogenerated by uv via the following command:
-#    uv pip compile --python-platform aarch64-apple-darwin --python-version 3.9 --output-file napari_repo/resources/constraints/constraints_py3.9_macos_arm.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt --extra pyqt5 --extra pyqt6_experimental --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
+#    uv pip compile --python-platform windows --python-version 3.9 --output-file napari_repo/resources/constraints/constraints_py3.9_windows.txt napari_repo/pyproject.toml napari_repo/resources/constraints/version_denylist.txt --extra pyqt5 --extra pyqt6 --extra pyside2 --extra pyside6_experimental --extra testing --extra testing_extra --extra optional
 alabaster==0.7.16
     # via sphinx
 annotated-types==0.7.0
     # via pydantic
-app-model==0.2.7
+app-model==0.3.0
     # via napari (napari_repo/pyproject.toml)
 appdirs==1.4.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
-appnope==0.1.4
-    # via ipykernel
 asciitree==0.3.3
     # via zarr
 asttokens==2.4.1
     # via stack-data
-attrs==23.2.0
+attrs==24.2.0
     # via
     #   hypothesis
     #   jsonschema
     #   referencing
-babel==2.15.0
+babel==2.16.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   sphinx
-build==1.2.1
+build==1.2.2.post1
     # via npe2
 cachey==0.2.1
     # via napari (napari_repo/pyproject.toml)
-certifi==2024.7.4
+certifi==2024.8.30
     # via
     #   napari (napari_repo/pyproject.toml)
     #   requests
-charset-normalizer==3.3.2
+charset-normalizer==3.4.0
     # via requests
 click==8.1.7
     # via
     #   dask
     #   typer
-cloudpickle==3.0.0
+cloudpickle==3.1.0
     # via dask
+colorama==0.4.6
+    # via
+    #   build
+    #   click
+    #   ipython
+    #   pytest
+    #   sphinx
+    #   tqdm
 comm==0.2.2
     # via ipykernel
-contourpy==1.2.1
+contourpy==1.3.0
     # via matplotlib
-coverage==7.5.4
+coverage==7.6.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
 cycler==0.12.1
     # via matplotlib
-dask==2024.6.2
+dask==2024.8.0
     # via napari (napari_repo/pyproject.toml)
-debugpy==1.8.1
+debugpy==1.8.8
     # via ipykernel
 decorator==5.1.1
     # via ipython
-distlib==0.3.8
+distlib==0.3.9
     # via virtualenv
 docstring-parser==0.16
     # via
@@ -65,24 +71,28 @@ docstring-parser==0.16
     #   magicgui
 docutils==0.21.2
     # via sphinx
-exceptiongroup==1.2.1
+exceptiongroup==1.2.2
     # via
     #   hypothesis
     #   ipython
     #   pytest
-executing==2.0.1
+executing==2.1.0
     # via stack-data
 fasteners==0.19
     # via zarr
-filelock==3.15.4
+filelock==3.16.1
     # via
     #   torch
     #   virtualenv
-fonttools==4.53.0
+flexcache==0.3
+    # via pint
+flexparser==0.4
+    # via pint
+fonttools==4.55.0
     # via matplotlib
-freetype-py==2.4.0
+freetype-py==2.5.1
     # via vispy
-fsspec==2024.6.0
+fsspec==2024.10.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
@@ -91,31 +101,32 @@ heapdict==1.0.1
     # via cachey
 hsluv==5.0.4
     # via vispy
-hypothesis==6.103.4
+hypothesis==6.119.3
     # via napari (napari_repo/pyproject.toml)
-idna==3.7
+idna==3.10
     # via requests
-imageio==2.34.1
+imageio==2.36.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-svg
     #   scikit-image
 imagesize==1.4.1
     # via sphinx
-importlib-metadata==7.2.1
+importlib-metadata==8.5.0
     # via
     #   build
     #   dask
     #   jupyter-client
     #   sphinx
-importlib-resources==6.4.0
+importlib-resources==6.4.5
     # via matplotlib
 in-n-out==0.2.1
     # via app-model
 iniconfig==2.0.0
     # via pytest
-ipykernel==6.29.4
+ipykernel==6.29.5
     # via
+    #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari-console
     #   qtconsole
 ipython==8.18.1
@@ -123,17 +134,17 @@ ipython==8.18.1
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
     #   napari-console
-jedi==0.19.1
+jedi==0.19.2
     # via ipython
 jinja2==3.1.4
     # via
     #   sphinx
     #   torch
-jsonschema==4.22.0
+jsonschema==4.23.0
     # via napari (napari_repo/pyproject.toml)
-jsonschema-specifications==2023.12.1
+jsonschema-specifications==2024.10.1
     # via jsonschema
-jupyter-client==8.6.2
+jupyter-client==8.6.3
     # via
     #   ipykernel
     #   qtconsole
@@ -142,7 +153,7 @@ jupyter-core==5.7.2
     #   ipykernel
     #   jupyter-client
     #   qtconsole
-kiwisolver==1.4.5
+kiwisolver==1.4.7
     # via
     #   matplotlib
     #   vispy
@@ -154,19 +165,19 @@ llvmlite==0.43.0
     # via numba
 locket==1.0.0
     # via partd
-lxml==5.2.2
+lxml==5.3.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   lxml-html-clean
-lxml-html-clean==0.1.1
+lxml-html-clean==0.4.1
     # via lxml
-magicgui==0.8.3
+magicgui==0.9.1
     # via napari (napari_repo/pyproject.toml)
 markdown-it-py==3.0.0
     # via rich
-markupsafe==2.1.5
+markupsafe==3.0.2
     # via jinja2
-matplotlib==3.9.0
+matplotlib==3.9.2
     # via napari (napari_repo/pyproject.toml)
 matplotlib-inline==0.1.7
     # via
@@ -174,21 +185,19 @@ matplotlib-inline==0.1.7
     #   ipython
 mdurl==0.1.2
     # via markdown-it-py
-ml-dtypes==0.4.0
+ml-dtypes==0.5.0
     # via tensorstore
 mouseinfo==0.1.3
     # via pyautogui
 mpmath==1.3.0
     # via sympy
-napari-console==0.0.9
+napari-console==0.1.1
     # via napari (napari_repo/pyproject.toml)
 napari-plugin-engine==0.2.0
-    # via
-    #   napari (napari_repo/pyproject.toml)
-    #   napari-svg
-napari-plugin-manager==0.1.0a2
     # via napari (napari_repo/pyproject.toml)
-napari-svg==0.1.10
+napari-plugin-manager==0.1.3
+    # via napari (napari_repo/pyproject.toml)
+napari-svg==0.2.0
     # via napari (napari_repo/pyproject.toml)
 nest-asyncio==1.6.0
     # via ipykernel
@@ -196,7 +205,7 @@ networkx==3.2.1
     # via
     #   scikit-image
     #   torch
-npe2==0.7.6
+npe2==0.7.7
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-plugin-manager
@@ -220,12 +229,13 @@ numpy==1.26.4
     #   scipy
     #   tensorstore
     #   tifffile
+    #   triangle
     #   vispy
     #   xarray
     #   zarr
-numpydoc==1.7.0
+numpydoc==1.8.0
     # via napari (napari_repo/pyproject.toml)
-packaging==24.1
+packaging==24.2
     # via
     #   build
     #   dask
@@ -240,7 +250,7 @@ packaging==24.1
     #   sphinx
     #   vispy
     #   xarray
-pandas==2.2.2
+pandas==2.2.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   xarray
@@ -248,22 +258,21 @@ parso==0.8.4
     # via jedi
 partd==1.4.2
     # via dask
-pexpect==4.9.0
-    # via ipython
-pillow==10.3.0
+pillow==11.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   imageio
     #   matplotlib
     #   pyscreeze
     #   scikit-image
-pint==0.23
+pint==0.24.4
     # via napari (napari_repo/pyproject.toml)
-pip==24.1
+pip==24.3.1
     # via napari-plugin-manager
-platformdirs==4.2.2
+platformdirs==4.3.6
     # via
     #   jupyter-core
+    #   pint
     #   pooch
     #   virtualenv
 pluggy==1.5.0
@@ -276,9 +285,9 @@ pooch==1.8.2
     #   scikit-image
 pretend==1.0.9
     # via napari (napari_repo/pyproject.toml)
-prompt-toolkit==3.0.47
+prompt-toolkit==3.0.48
     # via ipython
-psutil==6.0.0
+psutil==6.1.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   ipykernel
@@ -288,15 +297,13 @@ psygnal==0.11.1
     #   app-model
     #   magicgui
     #   npe2
-ptyprocess==0.7.0
-    # via pexpect
-pure-eval==0.2.2
+pure-eval==0.2.3
     # via stack-data
 pyautogui==0.9.54
     # via napari (napari_repo/pyproject.toml)
 pyconify==0.1.6
     # via superqt
-pydantic==2.7.4
+pydantic==2.9.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   app-model
@@ -304,7 +311,7 @@ pydantic==2.7.4
     #   pydantic-compat
 pydantic-compat==0.1.2
     # via app-model
-pydantic-core==2.18.4
+pydantic-core==2.23.4
     # via pydantic
 pygetwindow==0.0.9
     # via pyautogui
@@ -318,41 +325,34 @@ pygments==2.18.0
     #   superqt
 pymsgbox==1.0.9
     # via pyautogui
-pyobjc-core==10.3.1
-    # via
-    #   pyautogui
-    #   pyobjc-framework-cocoa
-    #   pyobjc-framework-quartz
-pyobjc-framework-cocoa==10.3.1
-    # via pyobjc-framework-quartz
-pyobjc-framework-quartz==10.3.1
-    # via pyautogui
 pyopengl==3.1.6
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-pyparsing==3.1.2
+pyparsing==3.2.0
     # via matplotlib
 pyperclip==1.9.0
     # via mouseinfo
-pyproject-hooks==1.1.0
+pyproject-hooks==1.2.0
     # via build
-pyqt5==5.15.10
+pyqt5==5.15.11
     # via napari (napari_repo/pyproject.toml)
-pyqt5-qt5==5.15.14
+pyqt5-qt5==5.15.2
     # via pyqt5
-pyqt5-sip==12.13.0
+pyqt5-sip==12.15.0
     # via pyqt5
-pyqt6==6.7.0
+pyqt6==6.7.1
     # via napari (napari_repo/pyproject.toml)
-pyqt6-qt6==6.7.2
+pyqt6-qt6==6.7.3
     # via pyqt6
-pyqt6-sip==13.6.0
+pyqt6-sip==13.8.0
     # via pyqt6
 pyrect==0.2.0
     # via pygetwindow
-pyscreeze==0.1.30
+pyscreeze==1.0.1
     # via pyautogui
+pyside2==5.15.2.1
+    # via napari (napari_repo/pyproject.toml)
 pyside6==6.3.1
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
@@ -363,7 +363,7 @@ pyside6-essentials==6.3.1
     # via
     #   pyside6
     #   pyside6-addons
-pytest==8.2.2
+pytest==8.3.3
     # via
     #   napari (napari_repo/pyproject.toml)
     #   pytest-cov
@@ -371,7 +371,7 @@ pytest==8.2.2
     #   pytest-metadata
     #   pytest-pretty
     #   pytest-qt
-pytest-cov==5.0.0
+pytest-cov==6.0.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
 pytest-json-report==1.5.0
     # via -r napari_repo/resources/constraints/version_denylist.txt
@@ -388,23 +388,26 @@ python-dateutil==2.9.0.post0
     #   pandas
 pytweening==1.2.0
     # via pyautogui
-pytz==2024.1
+pytz==2024.2
     # via pandas
-pyyaml==6.0.1
+pywin32==308
+    # via
+    #   napari (napari_repo/pyproject.toml)
+    #   jupyter-core
+pyyaml==6.0.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   npe2
-pyzmq==26.0.3
+pyzmq==26.2.0
     # via
     #   ipykernel
     #   jupyter-client
-    #   qtconsole
-qtconsole==5.5.2
+qtconsole==5.6.1
     # via
     #   napari (napari_repo/pyproject.toml)
     #   napari-console
-qtpy==2.4.1
+qtpy==2.4.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
@@ -421,18 +424,16 @@ requests==2.32.3
     #   pooch
     #   pyconify
     #   sphinx
-rich==13.7.1
+rich==13.9.4
     # via
     #   napari (napari_repo/pyproject.toml)
     #   npe2
     #   pytest-pretty
     #   typer
-rpds-py==0.18.1
+rpds-py==0.21.0
     # via
     #   jsonschema
     #   referencing
-rubicon-objc==0.4.9
-    # via mouseinfo
 scikit-image==0.24.0
     # via napari (napari_repo/pyproject.toml)
 scipy==1.13.1
@@ -441,6 +442,8 @@ scipy==1.13.1
     #   scikit-image
 shellingham==1.5.4
     # via typer
+shiboken2==5.15.2.1
+    # via pyside2
 shiboken6==6.3.1
     # via
     #   pyside6
@@ -454,19 +457,19 @@ snowballstemmer==2.2.0
     # via sphinx
 sortedcontainers==2.4.0
     # via hypothesis
-sphinx==7.3.7
+sphinx==7.4.7
     # via numpydoc
-sphinxcontrib-applehelp==1.0.8
+sphinxcontrib-applehelp==2.0.0
     # via sphinx
-sphinxcontrib-devhelp==1.0.6
+sphinxcontrib-devhelp==2.0.0
     # via sphinx
-sphinxcontrib-htmlhelp==2.0.5
+sphinxcontrib-htmlhelp==2.1.0
     # via sphinx
 sphinxcontrib-jsmath==1.0.1
     # via sphinx
-sphinxcontrib-qthelp==1.0.7
+sphinxcontrib-qthelp==2.0.0
     # via sphinx
-sphinxcontrib-serializinghtml==1.1.10
+sphinxcontrib-serializinghtml==2.0.0
     # via sphinx
 stack-data==0.6.3
     # via ipython
@@ -475,19 +478,19 @@ superqt==0.6.7
     #   napari (napari_repo/pyproject.toml)
     #   magicgui
     #   napari-plugin-manager
-sympy==1.12.1
+sympy==1.13.1
     # via torch
 tabulate==0.9.0
     # via numpydoc
-tensorstore==0.1.63
+tensorstore==0.1.68
     # via
     #   -r napari_repo/resources/constraints/version_denylist.txt
     #   napari (napari_repo/pyproject.toml)
-tifffile==2024.6.18
+tifffile==2024.8.30
     # via
     #   napari (napari_repo/pyproject.toml)
     #   scikit-image
-tomli==2.0.1
+tomli==2.1.0
     # via
     #   build
     #   coverage
@@ -495,20 +498,20 @@ tomli==2.0.1
     #   numpydoc
     #   pytest
     #   sphinx
-tomli-w==1.0.0
+tomli-w==1.1.0
     # via npe2
-toolz==0.12.1
+toolz==1.0.0
     # via
     #   napari (napari_repo/pyproject.toml)
     #   dask
     #   partd
-torch==2.3.1
+torch==2.5.1
     # via napari (napari_repo/pyproject.toml)
 tornado==6.4.1
     # via
     #   ipykernel
     #   jupyter-client
-tqdm==4.66.4
+tqdm==4.67.0
     # via napari (napari_repo/pyproject.toml)
 traitlets==5.14.3
     # via
@@ -519,25 +522,30 @@ traitlets==5.14.3
     #   jupyter-core
     #   matplotlib-inline
     #   qtconsole
-typer==0.12.3
+triangle==20230923
+    # via napari (napari_repo/pyproject.toml)
+typer==0.13.0
     # via npe2
 typing-extensions==4.12.2
     # via
     #   napari (napari_repo/pyproject.toml)
     #   app-model
+    #   flexcache
+    #   flexparser
     #   ipython
     #   magicgui
     #   pint
     #   pydantic
     #   pydantic-core
+    #   rich
     #   superqt
     #   torch
     #   typer
-tzdata==2024.1
+tzdata==2024.2
     # via pandas
-urllib3==2.2.2
+urllib3==2.2.3
     # via requests
-virtualenv==20.26.3
+virtualenv==20.27.1
     # via napari (napari_repo/pyproject.toml)
 vispy==0.14.3
     # via
@@ -547,11 +555,11 @@ wcwidth==0.2.13
     # via prompt-toolkit
 wrapt==1.16.0
     # via napari (napari_repo/pyproject.toml)
-xarray==2024.6.0
+xarray==2024.7.0
     # via napari (napari_repo/pyproject.toml)
 zarr==2.18.2
     # via napari (napari_repo/pyproject.toml)
-zipp==3.19.2
+zipp==3.21.0
     # via
     #   importlib-metadata
     #   importlib-resources
diff --git a/resources/constraints/version_denylist.txt b/resources/constraints/version_denylist.txt
index f689ca503ec..50b4969d68f 100644
--- a/resources/constraints/version_denylist.txt
+++ b/resources/constraints/version_denylist.txt
@@ -4,4 +4,4 @@ PySide6 != 6.4.3, !=6.5.0, !=6.5.1, !=6.5.1.1, !=6.5.2, != 6.5.3, != 6.6.0, != 6
 pytest-json-report
 pyopengl!=3.1.7
 tensorstore!=0.1.38
-zarr!=3.0.0a0
+ipykernel!=7.0.0a0
diff --git a/resources/requirements_mypy.txt b/resources/requirements_mypy.txt
index 4e988603b84..5a2a81f7d2b 100644
--- a/resources/requirements_mypy.txt
+++ b/resources/requirements_mypy.txt
@@ -4,33 +4,33 @@ annotated-types==0.7.0
     # via pydantic
 appdirs==1.4.4
     # via npe2
-build==1.2.1
+build==1.2.2.post1
     # via npe2
-certifi==2024.6.2
+certifi==2024.8.30
     # via requests
-charset-normalizer==3.3.2
+charset-normalizer==3.4.0
     # via requests
 click==8.1.7
     # via typer
 docstring-parser==0.16
     # via magicgui
-idna==3.7
+idna==3.10
     # via requests
-magicgui==0.8.3
+magicgui==0.9.1
     # via -r napari_repo/resources/requirements_mypy.in
 markdown-it-py==3.0.0
     # via rich
 mdurl==0.1.2
     # via markdown-it-py
-mypy==1.10.0
+mypy==1.13.0
     # via -r napari_repo/resources/requirements_mypy.in
 mypy-extensions==1.0.0
     # via mypy
-npe2==0.7.6
+npe2==0.7.7
     # via -r napari_repo/resources/requirements_mypy.in
-numpy==2.0.0
+numpy==2.1.3
     # via -r napari_repo/resources/requirements_mypy.in
-packaging==24.1
+packaging==24.2
     # via
     #   build
     #   qtpy
@@ -40,34 +40,34 @@ psygnal==0.11.1
     #   npe2
 pyconify==0.1.6
     # via superqt
-pydantic==2.7.4
+pydantic==2.9.2
     # via
     #   -r napari_repo/resources/requirements_mypy.in
     #   npe2
-pydantic-core==2.18.4
+pydantic-core==2.23.4
     # via pydantic
 pygments==2.18.0
     # via
     #   rich
     #   superqt
-pyproject-hooks==1.1.0
+pyproject-hooks==1.2.0
     # via build
-pyqt6==6.7.0
+pyqt6==6.7.1
     # via -r napari_repo/resources/requirements_mypy.in
-pyqt6-qt6==6.7.2
+pyqt6-qt6==6.7.3
     # via pyqt6
-pyqt6-sip==13.6.0
+pyqt6-sip==13.8.0
     # via pyqt6
-pyyaml==6.0.1
+pyyaml==6.0.2
     # via npe2
-qtpy==2.4.1
+qtpy==2.4.2
     # via
     #   -r napari_repo/resources/requirements_mypy.in
     #   magicgui
     #   superqt
 requests==2.32.3
     # via pyconify
-rich==13.7.1
+rich==13.9.4
     # via
     #   npe2
     #   typer
@@ -75,15 +75,15 @@ shellingham==1.5.4
     # via typer
 superqt==0.6.7
     # via magicgui
-tomli-w==1.0.0
+tomli-w==1.1.0
     # via npe2
-typer==0.12.3
+typer==0.13.0
     # via npe2
-types-pyyaml==6.0.12.20240311
+types-pyyaml==6.0.12.20240917
     # via -r napari_repo/resources/requirements_mypy.in
-types-requests==2.32.0.20240622
+types-requests==2.32.0.20241016
     # via -r napari_repo/resources/requirements_mypy.in
-types-setuptools==70.0.0.20240524
+types-setuptools==75.5.0.20241116
     # via -r napari_repo/resources/requirements_mypy.in
 typing-extensions==4.12.2
     # via
@@ -93,7 +93,7 @@ typing-extensions==4.12.2
     #   pydantic-core
     #   superqt
     #   typer
-urllib3==2.2.2
+urllib3==2.2.3
     # via
     #   requests
     #   types-requests
diff --git a/tools/check_vendored_modules.py b/tools/check_vendored_modules.py
index 0edc0425419..3697951e02d 100644
--- a/tools/check_vendored_modules.py
+++ b/tools/check_vendored_modules.py
@@ -6,6 +6,7 @@
 import sys
 from pathlib import Path
 from subprocess import check_output
+from typing import List
 
 
 TOOLS_PATH = Path(__file__).parent
@@ -36,7 +37,7 @@ def _clone(org, reponame, tag):
 
 
 def check_vendored_files(
-    org: str, reponame: str, tag: str, source_paths: Path, target_path: Path
+    org: str, reponame: str, tag: str, source_paths: List[Path], target_path: Path
 ) -> str:
     repo_path = _clone(org, reponame, tag)
     vendor_path = REPO_ROOT_PATH / NAPARI_FOLDER / target_path
@@ -80,12 +81,13 @@ def check_vendored_module(org: str, reponame: str, tag: str) -> str:
 def main():
     CI = '--ci' in sys.argv
     print("\n\nChecking vendored modules\n")
+    vendored_modules = []
     for org, reponame, tag, source, target in [
         ("albertosottile", "darkdetect", "master", None, None),
         (
             "matplotlib",
             "matplotlib",
-            "v3.2.1",
+            "main",
             [
                 # this file seem to be post 3.0.3 but pre 3.1
                 # plus there may have been custom changes.
@@ -94,7 +96,7 @@ def main():
                 # this file seem much more recent, but is touched much more rarely.
                 # it is at least from 3.2.1 as the turbo colormap is present and
                 # was added in matplotlib in 3.2.1
-                #'lib/matplotlib/_cm_listed.py'
+                'lib/matplotlib/_cm_listed.py'
             ],
             'utils/colormaps/vendored/',
         ),
@@ -107,15 +109,18 @@ def main():
                 org, reponame, tag, [Path(s) for s in source], Path(target)
             )
 
-        if CI:
-            print(f"::set-output name=vendored::{org}/{reponame}")
-            sys.exit(0)
         if diff:
-            print(diff)
-            print(
-                f"\n * '{org}/{reponame}' vendor code seems to not be up to date!!!\n"
-            )
-            sys.exit(1)
+            vendored_modules.append((org, reponame, diff))
+
+    if CI:
+        with open(TOOLS_PATH / "vendored_modules.txt", "w") as f:
+            f.write(" ".join(f"{org}/{reponame}" for org, reponame, _ in vendored_modules))
+        sys.exit(0)
+    if vendored_modules:
+        print("\n\nThe following vendored modules are not up to date:\n")
+        for org, reponame, _diff in vendored_modules:
+            print(f"\n * {org}/{reponame}\n")
+        sys,exit(1)
 
 
 if __name__ == "__main__":
diff --git a/tools/create_pr_or_update_existing_one.py b/tools/create_pr_or_update_existing_one.py
index 9d8d12903ba..1890b6da7f1 100644
--- a/tools/create_pr_or_update_existing_one.py
+++ b/tools/create_pr_or_update_existing_one.py
@@ -159,6 +159,22 @@ def update_own_pr(pr_number: int, access_token: str, base_branch: str, repo):
     response = requests.post(url, headers=headers, json=payload)
     response.raise_for_status()
 
+    url_labels = f'{BASE_URL}/repos/{repo}/issues/{pr_number}/labels'
+    response = requests.get(url_labels, headers=headers)
+    response.raise_for_status()
+
+    remove_label_url = (
+        f'{BASE_URL}/repos/{repo}/issues/{pr_number}/labels/ready%20to%20merge'
+    )
+
+    # following lines is to check if "ready to merge" label is added to PR,
+    # if it is present, then remove it to point that PR was changed
+    for label in response.json():
+        if label['name'] == 'ready to merge':
+            response = requests.delete(remove_label_url, headers=headers)
+            response.raise_for_status()
+            break
+
 
 def list_pr_for_branch(branch_name: str, access_token: str, repo=''):
     """
diff --git a/tools/string_list.json b/tools/string_list.json
index 1d5532ea055..d8fb8e9da78 100644
--- a/tools/string_list.json
+++ b/tools/string_list.json
@@ -903,6 +903,7 @@
       "viewer",
       "napari:roll_axes",
       "napari:transpose_axes",
+      "napari:rotate_layers",
       "napari:reset_view",
       "napari:toggle_grid",
       "napari:toggle_ndisplay",
@@ -1885,7 +1886,7 @@
     ],
     "napari/layers/shapes/_shapes_key_bindings.py": ["mode"],
     "napari/layers/shapes/_shapes_models/__init__.py": [],
-    "napari/layers/shapes/_shapes_models/_polgyon_base.py": ["int", "polygon"],
+    "napari/layers/shapes/_shapes_models/_polygon_base.py": ["int", "polygon"],
     "napari/layers/shapes/_shapes_models/ellipse.py": ["ellipse", "int"],
     "napari/layers/shapes/_shapes_models/line.py": ["int", "line"],
     "napari/layers/shapes/_shapes_models/path.py": ["path"],
diff --git a/tox.ini b/tox.ini
index 2441bea759c..f62d0a1ba6a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -101,7 +101,7 @@ indexserver =
     extra = https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
 commands =
     echo "COVERAGE: {env:COVERAGE:}"
-    cov: coverage run \
+    cov: coverage run --parallel-mode \
     !cov: python \
         -m pytest {env:PYTEST_PATH:} --color=yes --basetemp={envtmpdir} \
         --ignore tools --maxfail=5 --json-report \
@@ -116,7 +116,7 @@ commands_pre =
     pip uninstall -y pyautogui pytest-qt pyqt5 pyside2 pyside6 pyqt6
 
 commands =
-    cov: coverage run \
+    cov: coverage run --parallel-mode \
     !cov: python \
         -m pytest -v --color=yes --basetemp={envtmpdir} --ignore napari/_vispy \
         --ignore napari/_qt --ignore napari/_tests --ignore tools \
@@ -131,7 +131,7 @@ deps =
     numpy < 1.24
     packaging
 commands =
-    cov: coverage run \
+    cov: coverage run --parallel-mode \
     !cov: python \
         -m pytest napari/_tests/test_examples.py -v --color=yes --basetemp={envtmpdir} {posargs}
 
@@ -169,5 +169,5 @@ commands =
 deps =
     -r resources/requirements_mypy.txt
 commands =
-    mypy --config-file pyproject.toml
+    mypy --config-file pyproject.toml  --pretty --show-error-codes napari
 skip_install = true