Skip to content

[Components]: Add debug canvas utilities for R3F scene inspection#101

Open
justkahdri wants to merge 10 commits intomainfrom
claude/r3f-debug-canvas-u5EhE
Open

[Components]: Add debug canvas utilities for R3F scene inspection#101
justkahdri wants to merge 10 commits intomainfrom
claude/r3f-debug-canvas-u5EhE

Conversation

@justkahdri
Copy link
Contributor

@justkahdri justkahdri commented Mar 18, 2026

Summary

Introduces a comprehensive set of debug utilities for React Three Fiber scenes, providing interactive controls and monitoring tools for development and debugging workflows.

Key Changes

  • Debug Fly Controls (debug-fly-controls.tsx): Implements first-person camera movement with pointer lock, keyboard input (WASD for movement, Space for up, Shift for sprint), and mouse look with pitch/yaw rotation. Toggleable via Alt+F hotkey.

  • Debug Orbit Controls (debug-orbit-controls.tsx): Wraps @react-three/drei's OrbitControls for intuitive camera orbiting around a target point, with automatic target detection from camera userData or scene direction.

  • Debug Grid Helper (debug-grid-helper.tsx): Simple grid visualization component for spatial reference in the scene.

  • Debug Camera Monitor (debug-camera-monitor.tsx): Real-time display of camera position and rotation (in degrees) in the debug panel, updating each frame with formatted values.

  • Performance Monitor (debug-perf-monitor.tsx): GPU performance tracking using stats-gl, displaying triangle count and draw call metrics with historical graphs. Positioned as a fixed overlay in the bottom-right corner.

  • Index Export (index.ts): Barrel export for all debug components.

  • Registry Entry: Added debug-canvas component to registry with dependencies on stats-gl and @react-three/drei, plus registry dependency on @joyco/debug.

Notable Implementation Details

  • All controls integrate with a shared tweakpane-based debug store (useDebugBindings/useDebugState) for centralized toggling
  • Fly controls use YXZ Euler rotation order to prevent gimbal lock and maintain proper camera orientation
  • Performance monitor uses useFrame with priority ordering (-Infinity for pre-render, Infinity for post-render) to accurately measure per-frame GPU metrics
  • Camera monitor automatically refreshes bindings each frame to reflect live values
  • Pointer lock is properly managed with cleanup to restore normal pointer events when controls are disabled

https://claude.ai/code/session_01HkhaTW7bjR4kZHyehXgW1z

Greptile Summary

This PR introduces a comprehensive set of R3F debug utilities (DebugCanvas, fly/orbit controls, grid helper, camera monitor, perf monitor) all wired through the existing @joyco/debug tweakpane store, plus the plumbing to surface a new Canvas sub-section in the sidebar.

The implementation is solid overall. Previous review feedback has been incorporated: the camera monitor correctly uses 'YXZ' Euler decomposition to match fly controls, PerfMonitorGL now carries the DebugPerfMonitor export name, the Space key calls e.preventDefault(), and stats.dispose() is deferred inside .then() rather than called synchronously. The mutual exclusion mechanism between orbit and fly controls works correctly because useDebugBindings wraps the target in a Proxy that propagates mutations back through both the tweakpane UI and the Zustand store.

Key findings:

  • Missing .catch() on async perf-monitor cleanup — if stats.init(gl) rejects (e.g., WebGL context loss) the .then() branch is skipped and the rejection is unhandled, logging a console error.
  • Redundant targetRef in ActiveOrbitControlstargetRef is only used inside the useState initializer and is the same object as the returned target state; the ref can be replaced with a local variable in the initialiser for clarity.

Confidence Score: 4/5

  • This PR is safe to merge; the two remaining notes are minor style/robustness improvements that don't affect normal operation.
  • All previous review feedback has been addressed. The only new findings are a missing .catch() on an edge-case async cleanup (WebGL context loss) and a redundant ref in the orbit controls initialiser — neither affects the happy path.
  • registry/components/debug-canvas/debug-perf-monitor.tsx (missing .catch()), registry/components/debug-canvas/debug-controls.tsx (redundant targetRef)

Important Files Changed

Filename Overview
registry/components/debug-canvas/debug-controls.tsx Implements DebugControls, ActiveOrbitControls, and ActiveFlyControls. Mutual exclusion via store subscription is correct (proxy in useDebugBindings propagates mutations back through Zustand). Fly controls correctly initialize pitch/yaw from the camera quaternion on mount and use YXZ Euler order matching the camera monitor. Minor: redundant targetRef in ActiveOrbitControls (same object as target state).
registry/components/debug-canvas/debug-perf-monitor.tsx GPU perf overlay using stats-gl. Correctly defers DOM cleanup until the async init resolves via .then(). Missing .catch() means a rejected stats.init(gl) (e.g., context loss) results in an unhandled promise rejection. Pre/post-frame triangle delta logic is functionally correct given Three.js's autoReset behaviour.
registry/components/debug-canvas/debug-camera-monitor.tsx Reads camera position and quaternion each frame, decomposing with 'YXZ' order to match fly controls. Tweakpane bindings are created/disposed correctly via useDebugFolder. No issues found.
registry/components/debug-canvas/debug-canvas.tsx Thin wrapper around R3F Canvas that composes all four debug helpers. Clean and straightforward; no issues.
registry/components/debug-canvas/debug-grid-helper.tsx Simple toggle for a <gridHelper> primitive. Correctly uses useDebugBindings with the shared 'Canvas' folder key. No issues.
registry/components/debug-canvas/index.ts Barrel export for all five public components. All names carry the Debug prefix consistently. No issues.
components/layout/sidebar/section.tsx Adds a canvasSlugs prop and a CollapsibleSubSection for Canvas pages, positioned between UI and Effects in the sidebar. Logic mirrors the existing game/effect patterns exactly. No issues.
lib/source.ts Adds getCanvasSlugs() following the exact same pattern as getGameSlugs() and getEffectSlugs(). No issues.
demos/debug-canvas-demo.tsx Demo wraps DebugCanvas in a DebugProvider with enabled hardcoded to true, appropriate for a demo context. Scene contains basic meshes for visual reference. No issues.
registry.json New debug-canvas registry entry lists all six component files, declares stats-gl and @react-three/drei as dependencies, and @joyco/debug as a registry dependency. Correctly structured.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: registry/components/debug-canvas/debug-perf-monitor.tsx
Line: 83-87

Comment:
**Missing `.catch()` on async cleanup chain**

`cleanupP.then(...)` is called but there is no `.catch()` handler. If `stats.init(gl)` rejects (e.g., WebGL context loss, driver issue), the `.then()` callback never runs and the rejected promise goes unhandled — producing a browser console error and potentially leaving the `stats-gl` instance in a partially-initialized state without calling `dispose()`.

```suggestion
      cleanupP.then((applyFlexLayout) => {
        if (applyFlexLayout) window.removeEventListener('resize', applyFlexLayout)
        stats.dom.remove()
        stats.dispose()
      }).catch(() => {
        // init failed — nothing was appended, nothing to clean up
      })
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: registry/components/debug-canvas/debug-controls.tsx
Line: 12-21

Comment:
**Redundant `targetRef` in `ActiveOrbitControls`**

`targetRef` is only ever referenced inside the `useState` initializer, and it is returned from that initializer — making `target` and `targetRef.current` the exact same object reference. The ref is never read or written elsewhere, so it adds noise without benefit. A local variable in the initializer is cleaner:

```suggestion
  const [target] = useState(() => {
    const t = new Vector3()
    if (camera.userData.target instanceof Vector3) {
      return t.copy(camera.userData.target)
    }
    const dir = new Vector3()
    camera.getWorldDirection(dir)
    return t.copy(camera.position).add(dir.multiplyScalar(20))
  })
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "Fix review issues in..."

@vercel
Copy link
Contributor

vercel bot commented Mar 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
joyco-hub Ready Ready Preview, Comment Mar 20, 2026 11:41am

@justkahdri justkahdri changed the title Add debug canvas utilities for R3F scene inspection [Components]: Add debug canvas utilities for R3F scene inspection Mar 18, 2026
@justkahdri
Copy link
Contributor Author

@greptile

claude and others added 10 commits March 20, 2026 08:40
Extract R3F debug helpers from sazabi into a reusable registry component:
- DebugOrbitControls: drei OrbitControls with pointer event management
- DebugFlyControls: FPS-style WASD + mouse look with Alt+F toggle
- DebugGridHelper: toggleable 100x100 grid
- DebugCameraMonitor: real-time camera position/rotation display
- DebugPerfMonitor: stats-gl overlay with FPS, TRI, and CALL panels

All helpers share a "Canvas" tweakpane folder via useDebugBindings.

https://claude.ai/code/session_01HkhaTW7bjR4kZHyehXgW1z
- Demo with a simple 3-object scene showcasing all debug helpers
- MDX docs with installation, usage, and component API descriptions

https://claude.ai/code/session_01HkhaTW7bjR4kZHyehXgW1z
- Add 'canvas' frontmatter type to source config
- Add getCanvasSlugs() helper in lib/source.ts
- Thread canvasSlugs through layout → sidebar → section
- Render Canvas as a new collapsible subsection with Frame icon
- Set debug-canvas.mdx type to 'canvas'

https://claude.ai/code/session_01HkhaTW7bjR4kZHyehXgW1z
- Enclose description in quotes for consistency in debug-canvas.mdx
- Add "debug-canvas" entry to the components list in meta.json
- Create DebugCanvas as a drop-in R3F Canvas replacement with all debug
  helpers included (orbit/fly controls, grid, camera monitor, perf stats)
- Combine orbit and fly controls into a single DebugControls component
  with mutual exclusion (enabling one disables the other)
- Use default drei OrbitControls
- Update demo to use simplified single-import API
- Update docs and registry.json

https://claude.ai/code/session_01HkhaTW7bjR4kZHyehXgW1z
- Move stats.dispose() and dom.remove() inside .then() to avoid race
  with async stats.init() on early unmount
- Use 'YXZ' euler order in camera monitor to match fly controls convention
- Rename PerfMonitor to DebugPerfMonitor for consistent naming
- Add preventDefault() on Space key in fly controls to prevent page scroll

https://claude.ai/code/session_01HkhaTW7bjR4kZHyehXgW1z
- Add .catch() on cleanupP.then() in perf monitor to handle rejected
  stats.init() without unhandled promise errors
- Replace unnecessary targetRef with local variable in ActiveOrbitControls

https://claude.ai/code/session_01HkhaTW7bjR4kZHyehXgW1z
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