npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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 three
import { CADCameraControls } from 'cad-camera-controls';

React Three Fiber

npm install cad-camera-controls three react react-dom @react-three/fiber
import { 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. Use levelHorizon() 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 by minDistance / maxDistance.
  • 'fov' — Narrows or widens the field of view, keeping the camera stationary. Clamped by minFov / maxFov.
  • 'auto' — Dolly until the camera reaches minDistance, 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.

License

MIT