Skip to content

Fix/plane drag snapback#20

Merged
CSSFrancis merged 5 commits into
mainfrom
fix/plane-drag-snapback
Jun 17, 2026
Merged

Fix/plane drag snapback#20
CSSFrancis merged 5 commits into
mainfrom
fix/plane-drag-snapback

Conversation

@CSSFrancis

Copy link
Copy Markdown
Owner

This pull request introduces several improvements and bug fixes to the 3D voxel explorer and the underlying rendering engine, especially around the GPU/canvas rendering paths and interactive slice controls. The most important changes include a complete rewrite of how 3D voxels are selected and rendered in the explorer, robust handling and recovery from mid-draw GPU failures, fixes for canvas layering and background handling, and enhanced support for per-voxel colors. Additional tests are added to cover these behaviors.

These changes collectively make the 3D voxel explorer more robust, visually accurate, and user-friendly, while also addressing subtle rendering bugs and improving test coverage.

Two issues made the voxel explorer's slice selector feel broken:

1. Snap-back (underlying code): Plot3D.to_state_dict() returned dict(_state)
   without refreshing overlay_widgets from the live widgets, so any view-only
   push that goes through the targeted-fields path (set_highlight / set_view)
   re-serialised a stale plane position and overwrote the in-progress drag in
   JS. The drag would jump back to the last Python-known integer position.
   Fix: to_state_dict() now always rebuilds overlay_widgets from the live
   _widgets, so every push path carries the current plane positions.

2. Faulty / jumpy highlight (example): the explorer snapped the highlight to
   integer voxel indices, so the marker jumped while the plane glided. Now it
   tracks smooth float positions (fx,fy,fz) for the highlight and the planes
   that follow, keeping integer indices only for slicing the 2D images.

Adds 3 regression tests (TestPlaneDragNoSnapBack) asserting set_highlight /
set_view preserve a live plane position. 3D suite + example execution green.
The voxel highlight appeared to land on random voxels in large (256³)
volumes because the example rendered a sparse random subsample of the
whole volume — the highlight projected to the correct (ix,iy,iz) but
almost never coincided with a displayed cube, so it floated in empty
space.

Render the voxels lying ON the three slice planes instead, re-cut live
on each drag. The marker is now always anchored on a real cube at the
slice intersection, the volume shows actual slice contents, and the
on-plane count is ~3·(N/step)² regardless of N, so it stays fast at 256³.

- Plot3D.set_point_colors: accept voxels panels, not just scatter, so
  slabs can be recoloured live each drag.
- Add the voxel grain explorer to the example smoke tests.
- Add a set_point_colors-on-voxels regression test.
A 256³ grain explorer produced ~8112 slab voxels, which crossed the
GPU_VOXEL_THRESHOLD (8000) and switched the panel to the Phase-1 WebGPU
voxel path. That path is not hardware-verified in CI — headless Chromium
exposes no WebGPU adapter, so the canvas fallback is what every test
exercised — and on real GPUs it rendered a sparse, see-through volume
(cubes appearing to float / vanish) instead of solid slice slabs.

- Raise GPU_VOXEL_THRESHOLD 8000 -> 20000 so mid-size volumes stay on the
  depth-sorted, visual-regression-tested Canvas2D renderer. The grain
  explorer (~8k cubes) now renders its full slabs (verified at N=256).
- GPU voxel pipeline: cullMode 'none'. The MVP swaps rows (r0,r2,r1) and
  negates depth, so cube winding can't be relied on for back-face culling;
  culling dropped the wrong faces. Translucent cubes need all faces anyway.
- GPU geometry cache now keys on point_colors_b64 too, so set_point_colors
  recolours voxels live instead of reusing a stale colour buffer.
- Add a browser regression test that a ~8k-voxel volume renders dense slabs
  on the canvas path (gpuCanvas hidden).
Correct the diagnosis from the previous commit. Large voxel volumes rendered 'empty' (only plane widgets + highlight, no cubes) in WebGPU-enabled browsers (PyCharm JCEF = Chrome 137). It was NOT the shader or back-face culling.

Real cause: the 3D panel stacks gpuCanvas (z-index 0, WebGPU voxels) under plotCanvas (z-index 1, decorations). Activating the GPU path cleared the plotCanvas bitmap but left its opaque CSS background, so the element painted over every GPU-drawn voxel.

- plotCanvas background -> transparent while GPU active; restored on fallback, device loss, and state-no-longer-wants-GPU.
- Revert earlier workarounds now shown unnecessary: GPU_VOXEL_THRESHOLD back to 8000, cullMode back to 'back' for voxels.
- Verified the voxel WGSL + _gpuMatrix on real hardware (NVIDIA TITAN X via native wgpu): three solid slabs; cull 'back' == 'none'.
- Retarget regression test to the DOM layering invariant; the active-GPU transparent swap is hardware-verified separately (no WebGPU adapter in CI).
Safari's experimental WebGPU can activate, render correctly for a while, then throw mid-draw or lose the device. The GPU path makes the decoration plotCanvas transparent and takes GPU-only branches (proj==null etc.), so a mid-draw throw left the frame half-built: voxels AND axes vanished, and only a window resize (forcing a full redraw) brought them back.

The catch block now disposes the GPU panel, restores the opaque plotCanvas background, and re-renders the whole panel once on the canvas path in the same frame (guarded against re-entry via p._gpuFellBack, reset per draw3d call). Verified by simulating an adapter+device that activates then throws on createCommandEncoder: no page errors, gpuCanvas hidden, plotCanvas opaque, voxels + axes rendered. Note: the kernel 'Task was destroyed but it is pending' warnings are ipykernel shutdown noise, unrelated to this.
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.63%. Comparing base (0f8badb) to head (5a6a252).

Files with missing lines Patch % Lines
anyplotlib/plot3d/_plot3d.py 83.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #20      +/-   ##
==========================================
+ Coverage   90.62%   90.63%   +0.01%     
==========================================
  Files          33       33              
  Lines        2741     2744       +3     
==========================================
+ Hits         2484     2487       +3     
  Misses        257      257              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@CSSFrancis CSSFrancis merged commit ae47ee8 into main Jun 17, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants