← ALL NOTES
METHOD·2026.09·6 MIN READ
Adapting a 3D Gizmo for Desktop Interaction
3d-visualization3d-design

This article covers the desktop adaptation of the cube-gizmo prototype. The touch interaction has been documented separately, so the focus here is on mouse and keyboard input.

The same Gizmo geometry and manipulation model are shared across devices. On desktop, hover, multiple mouse buttons, the wheel, and the keyboard provide additional input channels, so several touch-specific mechanisms can be simplified.

Desktop Input Mapping

The desktop version uses the following controls:

InputBehavior
Left press + dragManipulate the active Gizmo handle
Mouse hoverPreview the handle currently under the pointer
Right-button dragOrbit the camera
Middle-button dragPan the camera
WheelZoom
EscapeCancel the active Gizmo operation and restore the previous state

The touch version uses a 260ms long press to confirm drag intent. Desktop interaction starts directly on pointerdown. A precise pointer and stable hover state already provide enough target confirmation before the drag begins.

The object model stays the same across devices: users still manipulate the visible axis, plane, or rotation handle directly. Only the input mechanics change.

Hover: Separate Available from Active

Hover adds a useful pre-action state on desktop.

The current visual states are:

  • Default: original red / green / blue axis colors;
  • Hover: #FFD060 warm yellow;
  • Active: #FF8A00 orange.

Warm yellow indicates the handle currently available under the pointer. Orange is reserved for an operation that has already started. The cursor changes to grab when a draggable handle is detected.

Hover detection runs on pointermove. A stable hitKey prevents repeated state updates while the pointer remains over the same target:

JAVASCRIPT
const hit = intersectHandle(event)
const hitKey = hit ? `${hit.type}-${hit.axis ?? hit.plane}` : null
const prevKey = state.hover ? `${state.hover.type}-${state.hover.axis ?? state.hover.plane}` : null

if (hitKey !== prevKey) {
  state.hover = hit
  renderer.domElement.style.cursor = hit ? 'grab' : 'default'
}

This matters for a Gizmo because pointer-move events are frequent while the actual target changes much less often.

Routing Input When Labels Overlap the Gizmo

Dimension labels can overlap Gizmo handles in screen space. Labels are clickable for numeric editing, while Gizmo handles are draggable.

The desktop rule gives the Gizmo hit priority.

On pointerdown, the scene first checks whether a handle is hit:

JAVASCRIPT
if (state.variant === 4) {
  if (intersectHandle(event)) {
    pcDidRoute = true
    onPointerDown(event)
  } else {
    pcDidRoute = false
  }
}

The later click event uses pcDidRoute before opening the dimension editor:

JAVASCRIPT
if (state.variant === 4 && !pcDidRoute) {
  openDimensionEditor()
}

pcDidRoute is kept separately because click fires after pointerup. Temporary drag-routing state may already be cleared by then.

The useful rule here is to decide ownership at the beginning of the input sequence. Overlapping interactions become much easier to reason about when one target owns the pointer from pointerdown onward.

Mouse Button Roles

Desktop input can separate object manipulation and camera navigation across different buttons:

  • left button: select and drag Gizmo handles;
  • right button: orbit camera;
  • middle button: pan camera;
  • wheel: zoom.

The browser context menu is disabled on the renderer so the right button can remain dedicated to camera rotation:

JAVASCRIPT
renderer.domElement.addEventListener('contextmenu', event => {
  event.preventDefault()
})

This keeps left-button interaction consistently tied to scene objects and Gizmo manipulation.

Escape to Cancel an Operation

Escape provides a natural desktop cancel action.

At the beginning of a drag, the scene stores an operationStart snapshot containing the cube position and rotation. Each frame calculates the current result from the initial state plus the latest delta.

This keeps manipulation stable across frame-rate changes and also makes cancellation straightforward:

JAVASCRIPT
function onKeyDown(event) {
  if (event.key === 'Escape' && state.variant === 4) {
    if (state.operationStart) {
      cubeGroup.position.copy(state.operationStart.cubePosition)
      cubeGroup.quaternion.copy(state.operationStart.cubeQuaternion)
    }

    state.active = null
    state.operationStart = null
    state.draggingCamera = false
    state.panningCamera = false
  }
}

Because the listener is attached to window, it is also removed when the scene is disposed to avoid duplicate listeners and memory leaks.

Detecting Input Capability

Desktop / touch behavior is selected through pointer capability rather than user-agent strings:

JAVASCRIPT
window.matchMedia('(pointer: fine)').matches

pointer: fine usually represents a mouse or trackpad, while coarse is closer to finger input.

JAVASCRIPT
React.useEffect(() => {
  if (window.matchMedia('(pointer: fine)').matches) {
    setVariant(4)
  }
}, [])

This avoids several UA-related edge cases, including tablets that report desktop-like user agents and browser device emulation.

Hybrid devices still need attention. An iPad with an external mouse may enter desktop mode, and convertible devices can move between touch and precise-pointer input. A production version could listen for media-query changes instead of checking only once at startup.

The demo does not expose a manual desktop / touch mode switch. It enters the matching interaction mode automatically so viewers can use the prototype immediately.

Shared 3D Picking Infrastructure

Desktop adaptation continues to use the same Raycaster and handle data as the touch version.

The basic picking path remains:

  1. convert screen coordinates to NDC;
  2. cast a ray from the camera;
  3. call Raycaster.intersectObjects();
  4. resolve the final target using distance and handle priority.

The visible Gizmo geometry stays thin. Larger invisible hit geometries are used for picking, which keeps visual size separate from interactive target size.

Axis-end arrows receive a higher priority than axis bodies, while planes and rotation arcs use their own priorities. This keeps target selection stable when hit areas overlap.

Engineering Notes

Input events. Desktop interaction mainly uses: pointerdown / pointermove / pointerup / wheel / contextmenu / keydown. Pointer Events continue to unify mouse and touch instead of maintaining two unrelated event systems.

Core state. createScene() keeps a small set of explicit interaction states: active (handle currently being manipulated), selected (selected object / handle), hover (current mouse hover target), operationStart (snapshot taken at the start of an operation), pointers (active pointers), draggingCamera (camera orbit state), panningCamera (camera pan state).

React / Three.js boundary. React manages declarative UI state such as variants, panels, and editors. Three.js manages the renderer, camera, Gizmo, Raycaster, and continuous manipulation. createScene() receives the DOM container and callbacks, then returns imperative methods such as setVariant, reset, and setDimension. React stores this API through useRef and receives scene changes through callbacks.

Desktop Regression Checklist
  • Hover target and cursor remain stable across every handle;
  • Hover and Active states are visually distinct;
  • dragging a Gizmo with the left button does not move the camera;
  • right / middle camera controls do not trigger object interaction;
  • overlapping labels and handles follow the expected priority;
  • Escape restores position and rotation correctly;
  • scene rebuilds do not leave duplicated keyboard or mouse listeners;
  • mouse, trackpad, touch, and hybrid devices enter a reasonable input mode.

The desktop version reuses the same manipulation semantics and picking layer as touch. Hover, mouse-button roles, and keyboard cancellation are added on top of that shared scene logic, so each input type can use the controls that fit it best without maintaining a separate Gizmo system.

CONTINUE READING
Translating Figma Component Properties into Code Props2026.09Engineering a Figma Design System into Production Assets2026.09