Skip to content

Enhancement: Add API to enable/disable keyboard controls independently in HybridCameraController #5734

@ujseah

Description

@ujseah

What package are you referring to?

@speckle/viewer - specifically the HybridCameraController and CameraController classes in the viewer package.

Is your feature request related to a problem? Please describe.

When embedding the Speckle Viewer in applications that have their own keyboard shortcuts (e.g., a collaborative canvas application using tldraw), the HybridCameraController's WASD/Q/E keyboard navigation causes conflicts.

The core issues are:

  1. Keyboard listeners are registered at the window/document level, not scoped to the viewer container element. This means keyboard events are captured regardless of whether the viewer has focus.

  2. No API to disable keyboard controls independently. Setting cameraController.enabled = false only disables pointer interactions (orbit, pan, zoom via enableInteraction()/disableInteraction()), but the keyboard handlers (onKeyDown/onKeyUp in HybridCameraController) remain active.

  3. Focus-based control is not possible. When a user clicks outside the Speckle viewer but remains on the same page, pressing WASD/Q/E continues to move the 3D camera unexpectedly.

Reproduction scenario:

  1. Embed a Speckle viewer in a page with other interactive elements
  2. Load a model and use WASD to navigate
  3. Click outside the viewer container (on another part of the page)
  4. Press WASD keys - the Speckle camera still moves, even though the viewer doesn't have focus

Describe the solution you'd like

Add a keyboardEnabled property or method to HybridCameraController (and/or CameraController) that allows developers to enable/disable keyboard controls independently of pointer controls:

Option A: Simple property

cameraController.keyboardEnabled = false;

Option B: Granular options object

cameraController.options = {
  enableKeyboard: false,
  enableOrbit: true,
  enableZoom: true,
  enablePan: true,
};

Option C: Dedicated method

cameraController.setKeyboardEnabled(false);

Alternative approach: Scope the keyboard listeners to the viewer container element rather than window/document, so they only fire when the container has DOM focus. This would make the viewer behave like a standard focusable component.

Describe alternatives you've considered

Current workaround: We've implemented a window-level event listener in the capture phase that intercepts WASD/Q/E keys when our viewer component isn't focused, calling stopImmediatePropagation() to prevent the events from reaching Speckle's keyboard handlers.

useEffect(() => {
    const BLOCKED_KEYS = new Set(['w', 'a', 's', 'd', 'q', 'e', 'W', 'A', 'S', 'D', 'Q', 'E']);

    const handleKeyDown = (e: KeyboardEvent) => {
        if (!isFocusedRef.current && BLOCKED_KEYS.has(e.key)) {
            e.stopImmediatePropagation();
        }
    };

    window.addEventListener('keydown', handleKeyDown, true); // capture phase
    return () => window.removeEventListener('keydown', handleKeyDown, true);
}, []);

Why this isn't ideal:

  • Requires understanding Speckle's internal implementation
  • Fragile if Speckle's event registration changes
  • Adds complexity to every embedding application
  • Race condition potential between listener registration order

Other alternatives considered:

  • Using CameraController instead of HybridCameraController - removes WASD support entirely, which is a regression

Additional context

Environment:

  • @speckle/viewer version: 2.26.8
  • Framework: React/Next.js with tldraw canvas

Use case: We're building a collaborative canvas application (similar to Figma/Miro) that embeds Speckle model viewers as interactive shapes. The parent canvas has keyboard shortcuts (e.g., for tools, undo/redo) that conflict with Speckle's WASD navigation. We need:

  • WASD navigation enabled when users click into and focus on the Speckle viewer
  • WASD navigation disabled when users click outside to interact with the canvas

Technical details from investigation:

  • CameraController.enabled setter calls enableInteraction()/disableInteraction() which only manages pointer events
  • HybridCameraController extends CameraController and adds its own onKeyDown/onKeyUp methods
  • These keyboard methods are protected and not controlled by the enabled property
  • The keyboard listeners appear to be registered on window during controller initialization

Similar patterns in other libraries:

  • Three.js OrbitControls has enableKeys property
  • Babylon.js cameras have keysUp/keysDown arrays that can be cleared
  • Most 3D viewer libraries scope keyboard input to the canvas element's focus state

Prerequisites

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions