-
Notifications
You must be signed in to change notification settings - Fork 224
Description
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:
-
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.
-
No API to disable keyboard controls independently. Setting
cameraController.enabled = falseonly disables pointer interactions (orbit, pan, zoom viaenableInteraction()/disableInteraction()), but the keyboard handlers (onKeyDown/onKeyUpinHybridCameraController) remain active. -
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:
- Embed a Speckle viewer in a page with other interactive elements
- Load a model and use WASD to navigate
- Click outside the viewer container (on another part of the page)
- 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
CameraControllerinstead ofHybridCameraController- removes WASD support entirely, which is a regression
Additional context
Environment:
@speckle/viewerversion: 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.enabledsetter callsenableInteraction()/disableInteraction()which only manages pointer eventsHybridCameraControllerextendsCameraControllerand adds its ownonKeyDown/onKeyUpmethods- These keyboard methods are
protectedand not controlled by theenabledproperty - The keyboard listeners appear to be registered on
windowduring controller initialization
Similar patterns in other libraries:
- Three.js
OrbitControlshasenableKeysproperty - Babylon.js cameras have
keysUp/keysDownarrays that can be cleared - Most 3D viewer libraries scope keyboard input to the canvas element's focus state
Prerequisites
- I read the contribution guidelines
- I checked the documentation and found no answer.
- I checked existing issues and found no similar issue.
- I checked the community forum for related discussions and found no answer.
- I'm requesting the feature to the correct repository