cad-camera-controls
v0.2.0
Published
CAD-style camera controls for Three.js with fixed-pivot rotate, modifier pan, and cursor-anchored zoom. Optional React Three Fiber wrapper included.
Downloads
561
Maintainers
Readme
cad-camera-controls
CAD-style camera controls for Three.js and React Three Fiber.
This is built for the same navigation family as tools like Onshape, SolidWorks, Fusion 360, and Blender: orbit around a stable point, pan the view without silently moving that point, and zoom toward the cursor. It is not intended to clone any one application exactly. The main opinion is that the pivot is explicit and deterministic, so applications can keep it at the scene origin, move it to a selected part, or set it from their own picking logic.
- Fixed-pivot orbit, modifier-key pan, cursor-anchored zoom
- Explicit pivot control for selection and part-picking workflows
- Perspective and orthographic camera support
- Dolly, FOV, and auto zoom modes
- Inertial damping
- Configurable mouse, touch, and keyboard bindings
- React Three Fiber wrapper included
Examples
- Settings playground — tune camera type, bindings, speeds, limits, fit helpers, and saved views.
- Pivot picking — click scene parts to move the fixed pivot to an object center, or click empty space to reset it to the scene origin.
Installation
Vanilla Three.js
npm install cad-camera-controls threeimport { CADCameraControls } from 'cad-camera-controls';React Three Fiber
npm install cad-camera-controls three react react-dom @react-three/fiberimport { CADCameraControls } from 'cad-camera-controls/react';React, react-dom, and @react-three/fiber are optional peer dependencies — vanilla JS users don't need them.
Quick Start — Vanilla Three.js
import * as THREE from 'three';
import { CADCameraControls } from 'cad-camera-controls';
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 10000);
camera.position.set(0, 500, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const controls = new CADCameraControls(camera, renderer.domElement);
controls.listenToKeyEvents(document.body);
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
controls.update(clock.getDelta());
renderer.render(scene, camera);
}
animate();Quick Start — React Three Fiber
import { Canvas } from '@react-three/fiber';
import { CADCameraControls } from 'cad-camera-controls/react';
function App() {
return (
<Canvas camera={{ position: [0, 500, 1000], fov: 50 }}>
<CADCameraControls />
<mesh>
<boxGeometry />
<meshNormalMaterial />
</mesh>
</Canvas>
);
}Orthographic Cameras
Both PerspectiveCamera and OrthographicCamera are supported. With an orthographic camera, scroll/pinch adjusts camera.zoom (clamped by minZoom/maxZoom). The zoomMode, minDistance, maxDistance, minFov, and maxFov properties only apply to perspective cameras.
const camera = new THREE.OrthographicCamera(-500, 500, 500, -500, 0.1, 10000);
const controls = new CADCameraControls(camera, renderer.domElement);
controls.minZoom = 0.1;
controls.maxZoom = 50;API
Constructor
new CADCameraControls( camera : PerspectiveCamera | OrthographicCamera, domElement? : HTMLElement )
camera — The camera to control.
domElement — The DOM element for event listeners. If provided, connect() is called automatically.
Properties
.enabled : boolean
Enable or disable all interaction. Default is true.
.enableDamping : boolean
Smooth inertial deceleration after releasing a drag or scroll. When enabled, you must call .update() in your animation loop. Default is true.
.dampingFactor : number
Damping friction in the range (0, 1). Higher values stop the camera faster. Each frame, velocity is multiplied by (1 - dampingFactor). Controls the smoothness of acceleration and deceleration without affecting zoom or pan magnitude. Default is 0.2.
.pivot : Vector3
Fixed orbit center. Rotation orbits around this point. Pan moves the camera without changing the pivot. Default is (0, 0, 0).
.orbitStyle : 'turntable' | 'free'
How drag deltas map to rotation axes. Both styles orbit around pivot. Default is 'turntable'.
'turntable'— Horizontal drag yaws about the world up axis; vertical drag pitches about the camera's right axis. The horizon never rolls, and circular mouse motion returns the view to where it started.'free'— Horizontal drag yaws about the camera's own up axis instead. Yaw and pitch no longer commute, so circular mouse motion accumulates roll about the view axis — counter-clockwise circles visibly rotate the scene clockwise, matching the default rotate in CAD tools like Onshape. UselevelHorizon()to remove accumulated roll.
.inputBindings : InputBindings
Mouse button and modifier key mapping. Button values: 0 = left, 1 = middle, 2 = right. An optional modifier ('ctrl', 'meta', 'alt', 'shift') disambiguates when both actions share the same button.
Default is { rotate: { button: 0 }, pan: { button: 2 } } (left-click to rotate, right-click to pan).
// Middle-click to pan
controls.inputBindings = { rotate: { button: 0 }, pan: { button: 1 } };
// Same button with modifier: shift+left to pan, left to rotate
controls.inputBindings = { rotate: { button: 0 }, pan: { button: 0, modifier: 'shift' } };.touchBindings : TouchBindings
Touch gesture mapping. one and two control one-finger and two-finger drag. pinch enables pinch-to-zoom.
Default is { one: 'rotate', two: 'pan', pinch: true }.
.rotateSpeed : number
Orbit rotation sensitivity in radians per pixel of mouse/touch drag. With damping enabled, releasing the drag produces a smooth inertial tail that decays by dampingFactor per frame. Default is 0.005.
.panSpeed : number
Pan sensitivity in world-units per pixel per unit-distance from the pivot. The actual pan per pixel of drag is panSpeed × distance-to-pivot. With damping enabled, releasing the drag produces a smooth inertial tail. Default is 0.0016.
.zoomSpeed : number
Scroll and pinch zoom speed (exponent multiplier). Zoom per scroll tick is Math.pow(0.99, zoomSpeed). With damping enabled, each tick produces a smooth inertial tail. Default is 1.
.zoomMode : 'dolly' | 'fov' | 'auto'
Zoom strategy for perspective cameras. Has no effect on orthographic cameras. Default is 'dolly'.
'dolly'— Moves the camera toward or away from the pivot. Clamped byminDistance/maxDistance.'fov'— Narrows or widens the field of view, keeping the camera stationary. Clamped byminFov/maxFov.'auto'— Dolly until the camera reachesminDistance, then seamlessly switches to FOV narrowing. Zooming back out reverses: FOV widens to its original value first, then dolly resumes.
controls.zoomMode = 'auto';.autoFovAnchorScale : number
Scales the cursor-anchored position shift during FOV zoom in 'auto' mode. At 1, full cursor tracking (camera shifts to keep the zoom centered on the cursor). At 0, no cursor tracking (camera stays stationary during FOV zoom). Only applies when zoomMode is 'auto'. Default is 0.1.
.minDistance : number
Minimum camera distance from the pivot in world units. Perspective cameras only. Default is 50.
.maxDistance : number
Maximum camera distance from the pivot in world units. Perspective cameras only. Default is 100000.
.minZoom : number
Minimum camera.zoom value. Orthographic cameras only. Default is 0.01.
.maxZoom : number
Maximum camera.zoom value. Orthographic cameras only. Default is 1000.
.minFov : number
Minimum field of view in degrees. Used by 'fov' and 'auto' zoom modes. Default is 1.
.maxFov : number
Maximum field of view in degrees. Used by 'fov' and 'auto' zoom modes. Default is 120.
.preventContextMenu : boolean
Suppress the browser right-click context menu on the DOM element. Default is true.
.enableKeyboard : boolean
Enable or disable keyboard controls. Keyboard listeners must be attached separately with .listenToKeyEvents(). Default is true.
.keyboardBindings : KeyboardBindings
Keyboard action mapping. Each of rotate, pan, and zoom is either { modifier: ModifierKey } (active with modifier), {} (active bare — no modifier), or false (disabled). At most one action can be bare, all active modifiers must be unique, and at least one action must be active. These constraints are validated at runtime.
Default is { rotate: { modifier: 'shift' }, pan: {}, zoom: false } (shift+arrows to rotate, bare arrows to pan, zoom disabled).
// Bare arrows rotate, ctrl+arrows pan, zoom disabled
controls.keyboardBindings = { rotate: {}, pan: { modifier: 'ctrl' }, zoom: false };
// All three with modifiers
controls.keyboardBindings = { rotate: { modifier: 'shift' }, pan: { modifier: 'ctrl' }, zoom: { modifier: 'alt' } };
// Bare UP/DOWN to zoom (only valid when both rotate and pan have modifiers)
controls.keyboardBindings = { rotate: { modifier: 'ctrl' }, pan: { modifier: 'shift' }, zoom: {} };.keyPanSpeed : number
Screen pixels of simulated mouse drag per arrow key press for panning. The actual world-space movement depends on panSpeed and camera distance from the pivot. With damping enabled, each keypress produces a smooth ease-in/ease-out motion. Default is 7.
.keyRotateSpeed : number
Rotation speed for arrow key rotation. The rotation angle per keypress is 2π × keyRotateSpeed / domElement.clientHeight radians. With damping enabled, each keypress produces a smooth ease-in/ease-out motion. Default is 1.
.keyZoomSpeed : number
Zoom speed for keyboard zoom (UP/DOWN). Uses the same formula as scroll zoom: Math.pow(0.99, keyZoomSpeed) per keypress. With damping enabled, each keypress produces a smooth ease-in/ease-out motion. Only applies when keyboardBindings.zoom is not false. Default is 1.
.keys : KeyboardKeys
Key code mapping for arrow controls. Default is { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' }.
// Remap to WASD
controls.keys = { LEFT: 'KeyA', UP: 'KeyW', RIGHT: 'KeyD', BOTTOM: 'KeyS' };Methods
.connect( domElement? : HTMLElement ) : void
Attach event listeners. If a different element was previously connected, it is disconnected first.
.disconnect() : void
Remove all event listeners from the current DOM element.
.dispose() : void
Disconnect and release all resources.
.update( deltaSeconds? : number ) : boolean
Advance damping and apply camera transforms. Required every frame when enableDamping is true. Returns true if the camera moved.
deltaSeconds — Time since last frame in seconds. Default is 1/60.
.resetBaseFov() : void
Recapture the camera's current FOV as the base for 'auto' zoom mode. Call this after changing camera.fov programmatically.
.fitToBox( box : Box3, enableTransition? : boolean, padding? : number ) : Promise<void>
Move the camera to frame the given axis-aligned bounding box. The camera keeps its current orientation and the pivot is not modified. Returns a Promise that resolves when the transition completes.
box — The Box3 to frame. Empty or degenerate boxes are ignored.
enableTransition — Animate over 0.5 s with ease-in-out. Default is true. Pass false to snap instantly.
padding — Fractional padding around the object. 0.1 means the object occupies roughly 90 % of the viewport with ~5 % margin on each side. Default is 0. Negative values are clamped to 0.
For perspective cameras, the camera dollies along its forward axis so the box fills the viewport. Distance is clamped to minDistance / maxDistance. For orthographic cameras, camera.zoom is adjusted and clamped to minZoom / maxZoom.
Any active transition is cancelled by user interaction (pointer, wheel, keyboard) or by calling another fit method.
const box = new THREE.Box3().setFromObject(mesh);
await controls.fitToBox(box); // animated, no padding
await controls.fitToBox(box, true, 0.1); // animated, 10% padding
controls.fitToBox(box, false, 0.2); // instant snap, 20% padding.fitToSphere( sphere : Sphere, enableTransition? : boolean, padding? : number ) : Promise<void>
Move the camera to frame the given bounding sphere. Behaves like fitToBox but uses the sphere's radius for distance calculations.
sphere — The Sphere to frame. Zero or negative radius is ignored.
enableTransition — Animate over 0.5 s with ease-in-out. Default is true. Pass false to snap instantly.
padding — Fractional padding around the object. Default is 0. Negative values are clamped to 0.
const sphere = new THREE.Sphere();
new THREE.Box3().setFromObject(mesh).getBoundingSphere(sphere);
await controls.fitToSphere(sphere); // no padding
await controls.fitToSphere(sphere, true, 0.1); // 10% padding.setView( position : Vector3, quaternion : Quaternion, enableTransition? : boolean, options? : { zoom?, fov? } ) : Promise<void>
Move the camera to a specific position and orientation. Returns a Promise that resolves when the transition completes. Useful for view-cube snaps, saved views, and any programmatic camera placement.
position — Target camera position.
quaternion — Target camera orientation.
enableTransition — Animate over 0.5 s with ease-in-out. Default is true. Pass false to snap instantly.
options.zoom — Target camera.zoom for orthographic cameras. If omitted, current zoom is kept.
options.fov — Target camera.fov for perspective cameras. If omitted, the base FOV (set at construction) is restored.
The pivot is not modified. Orientation is interpolated via quaternion slerp (shortest-path). Any active transition is cancelled by user interaction or by calling another transition method.
// View-cube "Front" snap
const frontPos = new THREE.Vector3(0, 0, 500);
const frontQuat = new THREE.Quaternion(); // identity = looking along -Z
await controls.setView(frontPos, frontQuat);
// Restore a saved view with zoom
await controls.setView(savedPos, savedQuat, true, { zoom: 2.5 });
// Instant snap, no animation
controls.setView(pos, quat, false);.levelHorizon( enableTransition? : boolean ) : Promise<void>
Rotate the camera about its view axis to the nearest orientation with a level horizon. Position and view direction are unchanged. Useful as a recovery affordance with orbitStyle: 'free', where roll accumulates. Returns a Promise that resolves when the transition completes.
enableTransition — Animate over 0.5 s with ease-in-out. Default is true. Pass false to snap instantly.
Resolves immediately without moving the camera when looking straight up or down, where no level orientation exists.
controls.orbitStyle = 'free';
// ... after the user accumulates some roll
await controls.levelHorizon();.listenToKeyEvents( domElement : HTMLElement ) : void
Attach keyboard listeners to the given DOM element. Which modifier triggers rotate vs. pan is controlled by .keyboardBindings.
controls.listenToKeyEvents(document.body);.stopListenToKeyEvents() : void
Remove keyboard listeners. Called automatically by .dispose().
Events
change
Fired when the camera position or orientation changes.
start
Fired when the user begins a drag interaction.
end
Fired when the user ends a drag interaction.
controls.addEventListener('change', () => {
renderer.render(scene, camera);
});Contributing
git clone https://github.com/alankalb/cad-camera-controls.git
cd cad-camera-controls
npm install| Command | Description |
|---|---|
| npm test | Run tests |
| npm run check | Run lint, typecheck, and tests |
| npm run lint | Lint with ESLint |
| npm run lint:fix | Lint and auto-fix |
| npm run typecheck | TypeScript type check |
| npm run build | Build with tsup |
| npm run build:example | Build the example playground |
| npm run smoke:exports | Verify package exports import successfully |
| npm run release:verify | Run the full release verification gate |
| npm run dev:example | Start the example playground |
CI runs lint, typecheck, tests, library build, example build, package export smoke checks, and npm pack --dry-run on every pull request. See RELEASE.md for the release checklist.
