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

scene-controls-kit

v3.0.1

Published

Pluggable Three.js modules: SceneControls (WASD fly, orbit on Q, height/FOV trackbars, cursor raycasting) and TransformGizmo (click to select an object, Enter to open a move/rotate gizmo, clipboard copy). Work in plain Three.js and in React Three Fiber vi

Readme

scene-controls-kit

Two pluggable modules for Three.js scenes. The core is written against the plain Three.js API, doesn't create a renderer/scene/camera, and doesn't run its own render loop — it works in vanilla Three.js as well as in React Three Fiber (via the hooks in r3f.js).

SceneControls — camera:

  • WASD — horizontal movement relative to the look direction
  • RMB + mouse — look around (Pointer Lock); left click stays free for object picking
  • Q — toggle orbit around a point in front of the camera
  • Height (Y) trackbar and FOV trackbar, built into the UI overlay
  • O / P — height (Y) from the keyboard
  • Hover — computes the world point under the cursor (X/Y/Z) in real time
  • C — copy the cursor point to the clipboard

TransformGizmo — object picking and a reduced-scope TransformControls/PivotControls equivalent:

  • Click an object — selects it (white outline)
  • Enter — opens a gizmo: X/Y/Z arrows (translate) + X/Y/Z rings (rotate); Enter/Escape again — closes it
  • Position/Rotation panel with clipboard copy (button or the C key)
  • Automatically pauses SceneControls while the gizmo is open

view

Install

npm install three
npm install react @react-three/fiber   # only if you need r3f.js

This package isn't published to npm — just copy the src folder into your project.

Quick start (both modules in one call)

import \* as THREE from 'three';
import { createControls } from 'scene-controls-kit';

const renderer = new THREE.WebGLRenderer({ antialias: true });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(65, innerWidth / innerHeight, 0.1, 200);
camera.position.set(0, 2.2, 8);

const material = new  THREE.MeshNormalMaterial({wireframe: true});
const mesh1 = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), material);
scene.add(mesh1);

const controls = createControls(scene, camera, renderer.domElement, {
  gizmo: { objects: \[mesh1] }, // what can be picked by clicking
});

const clock = new THREE.Clock();
function animate() {
  requestAnimationFrame(animate);
  controls.update(clock.getDelta());
  renderer.render(scene, camera);
}
animate();

controls.dispose(); // when tearing down the scene

controls.camera is the SceneControls instance, controls.gizmo is the TransformGizmo instance. Only one of the two modules:

createControls(scene, camera, dom, { gizmo: false });                       // camera only
createControls(scene, camera, dom, { camera: false, gizmo: { objects } });  // gizmo only

React Three Fiber

Combined hook:

import { useRef, useEffect } from 'react';
import { useControls } from 'scene-controls-kit/r3f';

function ControlsBridge({ meshRef1, meshRef2 }) {
  const { gizmoRef } = useControls({ gizmo: { objects: \[] } });

  useEffect(() => {
    // reading .current inside useEffect — by this point React has already
    // attached the refs
    gizmoRef.current?.setObjects(\[meshRef1.current, meshRef2.current].filter(Boolean));
  }, \[]);

  return null;
}

export default function App() {
  const meshRef1 = useRef(null);
  const meshRef2 = useRef(null);

  return (
    <Canvas camera={{ position: \[0, 2.2, 8], fov: 65 }}>
      <ControlsBridge meshRef1={meshRef1} meshRef2={meshRef2} />
      <mesh ref={meshRef1} position={\[-2, 0.5, 0]}>
        <boxGeometry />
        <meshStandardMaterial color="orange" />
      </mesh>
      <mesh ref={meshRef2} position={\[2, 0.5, 0]}>
        <sphereGeometry />
        <meshStandardMaterial color="hotpink" />
      </mesh>
    </Canvas>
  );
}

If the object list doesn't change dynamically, you can pass it straight into the options (useControls({ gizmo: { objects: \[...] } })), but .current still needs to be read inside useEffect/a callback, not in the component body (it's still null there).

For finer-grained control, use the hooks separately: useSceneControls(options) and useTransformGizmo(options) — their signatures match the class constructors.

SceneControls options

new SceneControls(camera, domElement, {
  moveSpeed: 6,
  sprintMultiplier: 1.8,   // Shift
  heightMin: 0.6,
  heightMax: 20,
  heightKeySpeed: 4,       // O/P
  fovMin: 28,
  fovMax: 115,
  orbitDistance: 9,
  lookTrigger: 'right',    // 'right' — look on RMB (left click free for picking); 'left' — look on any click
  ui: true,                // built-in panel (hints + trackbars)
  raycast: { enabled: true, planeY: 0, objects: null, showUI: true }, // cursor-point coordinates
  theme: { accent: '#5eead4', accent2: '#ffb454', /\* ... \*/ },
  labels: { hintMove: 'move', modeFly: 'Fly', /\* ... \*/ },
});

SceneControls API

|Method|Description| |-|-| |update(delta)|Call every frame — moves the camera in fly mode| |toggleMode()|Fly ↔ orbit (same as Q)| |getMode()|'fly' | 'orbit'| |getHeight() / setHeight(v)|Height Y| |getFov() / setFov(v)|FOV| |getCursorPoint() / getCursorObject()|World point/object under the cursor| |setRaycastPlaneY(y) / setRaycastObjects(objs) / setRaycastEnabled(bool)|Configure the cursor-point raycast| |copyCursorPoint()|Copy the cursor point to the clipboard as "x, y, z"| |pause() / resume() / isPaused()|Pause the controls (used by TransformGizmo)| |on(event, cb) / off(event, cb)|'modechange', 'heightchange', 'fovchange', 'cursorpoint', 'cursorcopy'| |dispose()|Removes listeners, removes the UI, releases Pointer Lock|

TransformGizmo options

new TransformGizmo(scene, camera, domElement, {
  objects: \[],                  // THREE.Object3D\[] — what can be picked by clicking (null entries are safe)
  cameraControls: null,         // a SceneControls instance for auto pause()/resume()
  clickThreshold: 6,            // px — "click, not drag" threshold
  translateSensitivity: 0.0025,
  rotateSensitivity: 0.008,
  ui: true,                     // Position/Rotation panel + copy button
  theme: { /\* same keys as SceneControls, plus magenta \*/ },
  labels: { title: 'Selected object', copy: 'Copy', /\* ... \*/ },
});

TransformGizmo API

|Method|Description| |-|-| |update()|Call every frame — syncs the gizmo/outline/panel| |setObjects(objects)|Change the list of pickable objects| |select(object) / deselect()|Select/deselect programmatically| |getSelected() / isActive()|Current object / whether the gizmo is open| |enterTransform() / exitTransform() / toggleTransform()|Same as Enter| |copyTransform()|Copy position/rotation to the clipboard, returns the text| |on(event, cb) / off(event, cb)|'select', 'deselect', 'transformenter', 'transformexit', 'change', 'copy'| |dispose()|Removes listeners, removes the gizmo/outline/panel|

Why camera look is on RMB, not on any click

With lookTrigger: 'left' (look on any click), a miss-click outside all objects would engage Pointer Lock — the cursor gets captured for camera look, and clicking to pick objects stops working until Escape. With the default (lookTrigger: 'right') this can't happen: left click is entirely reserved for TransformGizmo. Only switch to 'left' if TransformGizmo isn't used at all.

If clicking an object ever seems to "do nothing" with no console errors — check Escape (Pointer Lock may have engaged anyway) and make sure .current is read inside useEffect, not in the component body.

License

MIT — see LICENSE.