# Rotate camera and Update orbitControl to rotate around Z axis

**URL:** <https://discourse.threejs.org/t/rotate-camera-and-update-orbitcontrol-to-rotate-around-z-axis/86359>\
**Category:** Questions\
**Created:** [August 19, 2025, 5:58am UTC](https://discourse.threejs.org/t/rotate-camera-and-update-orbitcontrol-to-rotate-around-z-axis/86359 "2025-08-19T05:58:33Z")\
**Posts on this page:** 12\
**Page:** 1

<div class="post-metadata">

**Author:** ![Bretto](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.threejs.org/bretto/32/30614_2.png) [@Bretto](https://discourse.threejs.org/u/Bretto)\
**Post date:** [August 19, 2025, 5:58am UTC](https://discourse.threejs.org/t/rotate-camera-and-update-orbitcontrol-to-rotate-around-z-axis/86359/1 "2025-08-19T05:58:33Z")

</div>

I am using [GitHub - unconed/mathbox: Presentation-quality WebGL math graphing](https://github.com/unconed/mathbox) so not the latest threejs I have tried all I could find and think about but I cant do it… can someone show me how to make the orbitControl rotate around a different axis any solution advice would greatly be appreciated. Thanks

---

<div class="post-metadata">

**Author:** ![seanwasere](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.threejs.org/seanwasere/32/20864_2.png) [@seanwasere](https://discourse.threejs.org/u/seanwasere)\
**Post date:** [August 19, 2025, 8:46am UTC](https://discourse.threejs.org/t/rotate-camera-and-update-orbitcontrol-to-rotate-around-z-axis/86359/2 "2025-08-19T08:46:40Z")

</div>

presuming you have something like this,

`const controls = new OrbitControls(camera, renderer.domElement)`

you can change the orbit controls target at anytime using something like this.

`controls.target.set(1,2,3)`

and then

‘controls.update()’ _\<---- This can be done in your render loop, some onchange event, or directly after you manually change the target._

The sudden change of target can be very abrupt, so it’s common to use some animation technique.

Here is an example,

> **[JEasing OrbitControls](https://editor.sbcode.net/dd10836c49f14f9a58c75dd11660b9f6355ed22b)**
>
> Three.js TypeScript Tutorials by Sean Bradley : https://sbcode.net/threejs

The [target](https://threejs.org/docs/?q=orbitcontrols#examples/en/controls/OrbitControls.target) is a `Vector3`. For more ways to change a `Vector3` see [Vector3](https://threejs.org/docs/#api/en/math/Vector3)

---

<div class="post-metadata">

**Author:** ![Bretto](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.threejs.org/bretto/32/30614_2.png) [@Bretto](https://discourse.threejs.org/u/Bretto)\
**Post date:** [August 19, 2025, 9:05am UTC](https://discourse.threejs.org/t/rotate-camera-and-update-orbitcontrol-to-rotate-around-z-axis/86359/3 "2025-08-19T09:05:15Z")

</div>

Thank you for the feed back but the orbitcontrol is still only rotating around the Y axis….  
this.three = this.mb.three;  
this.three.camera.position.set(0,3,0);  
this.three.controls.target.set(0, 0, 0);  
this.three.controls.update();

I need my camera (0,3,0); and to rotate on Z…

---

<div class="post-metadata">

**Author:** ![seanwasere](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.threejs.org/seanwasere/32/20864_2.png) [@seanwasere](https://discourse.threejs.org/u/seanwasere)\
**Post date:** [August 19, 2025, 9:10am UTC](https://discourse.threejs.org/t/rotate-camera-and-update-orbitcontrol-to-rotate-around-z-axis/86359/4 "2025-08-19T09:10:18Z")

</div>

with [OrbitControls](https://threejs.org/docs/?q=orbitcontrols#examples/en/controls/OrbitControls) you can set [minAzimuthAngle](https://threejs.org/docs/?q=orbitcontrols#examples/en/controls/OrbitControls.minAzimuthAngle) and [maxAzimuthAngle](https://threejs.org/docs/?q=orbitcontrols#examples/en/controls/OrbitControls.maxAzimuthAngle).

Example, rotates only on z axis by setting min & max azimuth to `Math.PI / 2`

> **[JEasing OrbitControls - sliding](https://editor.sbcode.net/744d5ab472f53d9facfc7a5947cd0cd7831335c5)**
>
> Three.js TypeScript Tutorials by Sean Bradley : https://sbcode.net/threejs

or another way

> **[MapControls with Select Object to Zoom in](https://editor.sbcode.net/275bc6b9c2206d0bf997e292d0d621e62d163bee)**
>
> Press ESC or click floor to reset

---

<div class="post-metadata">

**Author:** ![Bretto](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.threejs.org/bretto/32/30614_2.png) [@Bretto](https://discourse.threejs.org/u/Bretto)\
**Post date:** [August 19, 2025, 9:52am UTC](https://discourse.threejs.org/t/rotate-camera-and-update-orbitcontrol-to-rotate-around-z-axis/86359/5 "2025-08-19T09:52:48Z")

</div>

this. is my set up from camera (0,0,3)  
I can do a 360 around it with orbitcontrol

 ![image](https://canada1.discourse-cdn.com/flex035/uploads/threejs/original/3X/e/e/ee036fc7864eabb570d4ced68a2fbe897afb0244.png)

 ![image](https://canada1.discourse-cdn.com/flex035/uploads/threejs/original/3X/b/4/b41e614e1ae02cb54d38b6019688a3c8e8ef0629.png)  
my issue is when I put the camera on (0,3,0)

 ![image](https://canada1.discourse-cdn.com/flex035/uploads/threejs/original/3X/7/6/764ef97fcd25accba946d931a3c753143cd4ba08.png)  
I want to be able to rotate around it 360.  
its true that your example rotates around the z axis but in my setup its not working

 ![image](https://canada1.discourse-cdn.com/flex035/uploads/threejs/original/3X/8/0/800b4678044a13d23359eb741edcd162dc1dcbc2.png)  
when I am using the mouse from left to right its not rotating the shapes around 360.

I am sorry if I am confusing you I got to say I am not good with all this rotations in 3D space I think I am still confused… Basicly this is what I am re-building…

 ![image](https://canada1.discourse-cdn.com/flex035/uploads/threejs/original/3X/0/f/0ff2730fccfbd7e48eed3a8d656f482dc52ac26a.png)

> **[Bitcoin Vision](https://dewmap.com/#bitcoin-vortex)**
>
> Interactive Visualization of the Bitcoin Power-Law.

click on the menu on the left to view the vortex interactive

![image](https://canada1.discourse-cdn.com/flex035/uploads/threejs/original/3X/3/1/318143c94bbad70494d7f03b69ae13a40ce6cb1b.png)

Thank you for your help

---

<div class="post-metadata">

**Author:** ![seanwasere](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.threejs.org/seanwasere/32/20864_2.png) [@seanwasere](https://discourse.threejs.org/u/seanwasere)\
**Post date:** [August 19, 2025, 10:23am UTC](https://discourse.threejs.org/t/rotate-camera-and-update-orbitcontrol-to-rotate-around-z-axis/86359/6 "2025-08-19T10:23:07Z")

</div>

I am not able to help.  
but here is another exmple that doesn’t use orbit controls, but you can still orbit around the object to whatever angle you want and still toggle pitch and yaw rotations to be enabled or not.

> **[Orbit and Zooming without using OrbitControls - with settings](https://editor.sbcode.net/d8f965ec824aabc14e3f71c2a2d23a99295a3e77)**
>
> Orbiting and Zooming a Vector3 without using OrbitControls or any trigonometry

or create an orbitControls that rotates on only 1 axis, then put your object in a group, and then set the groups lookAt to be one of  
1,0,0  
0,1,0  
0,0,1  
-1,0,0  
0,-1,0  
0,0,-1

a group is an [Object3D](https://threejs.org/docs/#api/en/core/Object3D)

---

<div class="post-metadata">

**Author:** ![Bretto](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.threejs.org/bretto/32/30614_2.png) [@Bretto](https://discourse.threejs.org/u/Bretto)\
**Post date:** [August 19, 2025, 10:50am UTC](https://discourse.threejs.org/t/rotate-camera-and-update-orbitcontrol-to-rotate-around-z-axis/86359/7 "2025-08-19T10:50:02Z")

</div>

I will work on it some more tomorrow, Thanks a lot for your help 👍

---

<div class="post-metadata">

**Author:** ![PavelBoytchev](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.threejs.org/pavelboytchev/32/38639_2.png) [@PavelBoytchev](https://discourse.threejs.org/u/PavelBoytchev)\
**Post date:** [August 19, 2025, 11:47am UTC](https://discourse.threejs.org/t/rotate-camera-and-update-orbitcontrol-to-rotate-around-z-axis/86359/8 "2025-08-19T11:47:13Z")

</div>

> [@Bretto](#):
>
> my issue is when I put the camera on (0,3,0)

I’m not sure I understand what you want to do, however, try this:

```javascript
camera.position.set( 0, 3, 0 ); // camera position
camera.up.set( 0, 0, 1 ); // camera rotation axis

```

---

<div class="post-metadata">

**Author:** ![Bretto](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.threejs.org/bretto/32/30614_2.png) [@Bretto](https://discourse.threejs.org/u/Bretto)\
**Post date:** [August 19, 2025, 4:22pm UTC](https://discourse.threejs.org/t/rotate-camera-and-update-orbitcontrol-to-rotate-around-z-axis/86359/9 "2025-08-19T16:22:59Z")

</div>

I did it doesn’t work…

---

<div class="post-metadata">

**Author:** ![PavelBoytchev](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.threejs.org/pavelboytchev/32/38639_2.png) [@PavelBoytchev](https://discourse.threejs.org/u/PavelBoytchev)\
**Post date:** [August 19, 2025, 4:32pm UTC](https://discourse.threejs.org/t/rotate-camera-and-update-orbitcontrol-to-rotate-around-z-axis/86359/10 "2025-08-19T16:32:57Z")

</div>

So, either I failed to understand the issue, or you did something strange in your code. When I tried it, it worked fine – the blue axis is Z, and orbiting is around Z:

---

<div class="post-metadata">

**Author:** ![Bretto](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.threejs.org/bretto/32/30614_2.png) [@Bretto](https://discourse.threejs.org/u/Bretto)\
**Post date:** [August 20, 2025, 2:27am UTC](https://discourse.threejs.org/t/rotate-camera-and-update-orbitcontrol-to-rotate-around-z-axis/86359/11 "2025-08-20T02:27:52Z")

</div>

it could be that [GitHub - unconed/mathbox: Presentation-quality WebGL math graphing](https://github.com/unconed/mathbox) OrbitControls didn’t implement that feature correctly… did you use the latest version of three in your example ? thanks for you feedback anyway but yes I did try that at first as apparently the OrbitControls was supposed to work from the up set of the camera

---

<div class="post-metadata">

**Author:** ![Bretto](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.threejs.org/bretto/32/30614_2.png) [@Bretto](https://discourse.threejs.org/u/Bretto)\
**Post date:** [August 20, 2025, 7:02am UTC](https://discourse.threejs.org/t/rotate-camera-and-update-orbitcontrol-to-rotate-around-z-axis/86359/12 "2025-08-20T07:02:43Z")

</div>

```javascript
import {
  EventDispatcher,
  MOUSE,
  Quaternion,
  Spherical,
  TOUCH,
  Vector2,
  Vector3,
  Camera,
  Matrix4,
} from "three";

export class OrbitControls2 extends EventDispatcher {
  object: Camera;
  domElement: HTMLElement;

  enabled = true;
  target = new Vector3();
  minDistance = 0;
  maxDistance = Infinity;
  minZoom = 0;
  maxZoom = Infinity;
  minPolarAngle = 0;
  maxPolarAngle = Math.PI;
  minAzimuthAngle = -Infinity;
  maxAzimuthAngle = Infinity;
  enableDamping = false;
  dampingFactor = 0.05;
  enableZoom = true;
  zoomSpeed = 1.0;
  enableRotate = true;
  rotateSpeed = 1.0;
  enablePan = true;
  panSpeed = 1.0;
  screenSpacePanning = true;
  keyPanSpeed = 7.0;
  autoRotate = false;
  autoRotateSpeed = 2.0;
  enableKeys = true;
  keys = { LEFT: "ArrowLeft", UP: "ArrowUp", RIGHT: "ArrowRight", BOTTOM: "ArrowDown" };
  mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN };
  touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN };

  // internals
  private _spherical = new Spherical();
  private _sphericalDelta = new Spherical();
  private _scale = 1;
  private _panOffset = new Vector3();
  private _zoomChanged = false;

  private _rotateStart = new Vector2();
  private _rotateEnd = new Vector2();
  private _rotateDelta = new Vector2();

  private _panStart = new Vector2();
  private _panEnd = new Vector2();
  private _panDelta = new Vector2();

  private _dollyStart = new Vector2();
  private _dollyEnd = new Vector2();
  private _dollyDelta = new Vector2();

  private _state: "NONE" | "ROTATE" | "DOLLY" | "PAN" | "TOUCH_ROTATE" | "TOUCH_PAN" | "TOUCH_DOLLY_PAN" = "NONE";

  // Store current rotation angles for different coordinate systems
  private _currentTheta = 0;
  private _currentPhi = 0;

  constructor(object: Camera, domElement: HTMLElement) {
    super();
    this.object = object;
    this.domElement = domElement;

    // Initialize current angles based on camera position
    this._initializeAngles();

    this.domElement.addEventListener("contextmenu", this.onContextMenu);
    this.domElement.addEventListener("mousedown", this.onMouseDown);
    this.domElement.addEventListener("wheel", this.onMouseWheel, { passive: false });
    this.domElement.addEventListener("touchstart", this.onTouchStart, { passive: false });
    this.domElement.addEventListener("touchmove", this.onTouchMove, { passive: false });
    this.domElement.addEventListener("touchend", this.onTouchEnd);
    window.addEventListener("keydown", this.onKeyDown);
  }

  private _initializeAngles() {
    const offset = new Vector3().subVectors(this.object.position, this.target);
    const upVector = this.object.up.clone().normalize();

    if (upVector.equals(new Vector3(0, 0, 1))) {
      // Z-up system
      this._currentTheta = Math.atan2(offset.y, offset.x);
      this._currentPhi = Math.atan2(Math.sqrt(offset.x * offset.x + offset.y * offset.y), offset.z);
    } else {
      // Y-up system (default)
      this._spherical.setFromVector3(offset);
      this._currentTheta = this._spherical.theta;
      this._currentPhi = this._spherical.phi;
    }
  }

  // === CORE UPDATE ===
  update = () => {
    const position = this.object.position;
    const upVector = this.object.up.clone().normalize();
    const offset = new Vector3().subVectors(position, this.target);
    const radius = offset.length();

    // Apply auto-rotate
    if (this.autoRotate && this._state === "NONE") {
      this._sphericalDelta.theta -= (2 * Math.PI) / 60 / 60 * this.autoRotateSpeed;
    }

    // Update current angles with deltas
    this._currentTheta += this._sphericalDelta.theta;
    this._currentPhi += this._sphericalDelta.phi;

    // Clamp angles - prevent flipping by avoiding poles
    this._currentTheta = Math.max(this.minAzimuthAngle, Math.min(this.maxAzimuthAngle, this._currentTheta));

    // Prevent flip by ensuring phi never reaches 0 or PI (the poles)
    const epsilon = 0.000001;
    const clampedMinPolar = Math.max(this.minPolarAngle, epsilon);
    const clampedMaxPolar = Math.min(this.maxPolarAngle, Math.PI - epsilon);
    this._currentPhi = Math.max(clampedMinPolar, Math.min(clampedMaxPolar, this._currentPhi));

    // Apply zoom
    const newRadius = Math.max(this.minDistance, Math.min(this.maxDistance, radius * this._scale));

    if (upVector.equals(new Vector3(0, 0, 1))) {
      // Z-up coordinate system: theta rotates around Z, phi is angle from Z-axis
      const sinPhi = Math.sin(this._currentPhi);
      const cosPhi = Math.cos(this._currentPhi);

      position.x = this.target.x + newRadius * sinPhi * Math.cos(this._currentTheta);
      position.y = this.target.y + newRadius * sinPhi * Math.sin(this._currentTheta);
      position.z = this.target.z + newRadius * cosPhi;
    } else if (upVector.equals(new Vector3(0, 1, 0))) {
      // Y-up coordinate system (standard)
      const sinPhi = Math.sin(this._currentPhi);
      const cosPhi = Math.cos(this._currentPhi);

      position.x = this.target.x + newRadius * sinPhi * Math.sin(this._currentTheta);
      position.y = this.target.y + newRadius * cosPhi;
      position.z = this.target.z + newRadius * sinPhi * Math.cos(this._currentTheta);
    } else {
      // Other coordinate systems - use quaternion approach
      this._spherical.theta = this._currentTheta;
      this._spherical.phi = this._currentPhi;
      this._spherical.radius = newRadius;
      this._spherical.makeSafe();

      const quat = new Quaternion().setFromUnitVectors(new Vector3(0, 1, 0), upVector);
      const quatInverse = quat.clone().invert();

      offset.setFromSpherical(this._spherical);
      offset.applyQuaternion(quat);
      position.copy(this.target).add(offset);
    }

    this.object.lookAt(this.target);

    // Apply damping or reset deltas
    if (this.enableDamping) {
      this._sphericalDelta.theta *= 1 - this.dampingFactor;
      this._sphericalDelta.phi *= 1 - this.dampingFactor;
    } else {
      this._sphericalDelta.set(0, 0, 0);
    }

    // Reset scale and pan offset
    this._scale = 1;
    this._panOffset.set(0, 0, 0);

    this.dispatchEvent({ type: "change" });
    return true;
  };

  // === Event Handlers ===
  private onContextMenu = (event: MouseEvent) => { event.preventDefault(); };

  private onMouseDown = (event: MouseEvent) => {
    if (!this.enabled) return;
    event.preventDefault();

    switch (event.button) {
      case 0: if (this.mouseButtons.LEFT === MOUSE.ROTATE) this.handleMouseDownRotate(event); break;
      case 1: if (this.mouseButtons.MIDDLE === MOUSE.DOLLY) this.handleMouseDownDolly(event); break;
      case 2: if (this.mouseButtons.RIGHT === MOUSE.PAN) this.handleMouseDownPan(event); break;
    }
    document.addEventListener("mousemove", this.onMouseMove);
    document.addEventListener("mouseup", this.onMouseUp);
  };

  private onMouseMove = (event: MouseEvent) => {
    if (!this.enabled) return;
    event.preventDefault();
    switch (this._state) {
      case "ROTATE": this.handleMouseMoveRotate(event); break;
      case "DOLLY": this.handleMouseMoveDolly(event); break;
      case "PAN": this.handleMouseMovePan(event); break;
    }
  };

  private onMouseUp = () => {
    document.removeEventListener("mousemove", this.onMouseMove);
    document.removeEventListener("mouseup", this.onMouseUp);
    this._state = "NONE";
  };

  private onMouseWheel = (event: WheelEvent) => {
    if (!this.enabled || !this.enableZoom || this._state !== "NONE") return;
    event.preventDefault();
    event.stopPropagation();
    if (event.deltaY < 0) this.dollyOut(this.getZoomScale());
    else if (event.deltaY > 0) this.dollyIn(this.getZoomScale());
    this.update();
  };

  private onKeyDown = (event: KeyboardEvent) => {
    if (!this.enabled || !this.enableKeys) return;
    switch (event.code) {
      case this.keys.UP: this.pan(0, this.keyPanSpeed); break;
      case this.keys.BOTTOM: this.pan(0, -this.keyPanSpeed); break;
      case this.keys.LEFT: this.pan(this.keyPanSpeed, 0); break;
      case this.keys.RIGHT: this.pan(-this.keyPanSpeed, 0); break;
    }
    this.update();
  };

  private onTouchStart = (event: TouchEvent) => {
    if (!this.enabled) return;
    switch (event.touches.length) {
      case 1: this._state = "TOUCH_ROTATE"; this._rotateStart.set(event.touches[0].pageX, event.touches[0].pageY); break;
      case 2: this._state = "TOUCH_DOLLY_PAN"; this.handleTouchStartDollyPan(event); break;
    }
  };

  private onTouchMove = (event: TouchEvent) => {
    if (!this.enabled) return;
    event.preventDefault();
    switch (this._state) {
      case "TOUCH_ROTATE": this.handleTouchMoveRotate(event); break;
      case "TOUCH_DOLLY_PAN": this.handleTouchMoveDollyPan(event); break;
    }
  };

  private onTouchEnd = () => { this._state = "NONE"; };

  // === Helpers ===
  private getZoomScale() { return Math.pow(0.95, this.zoomSpeed); }

  private dollyIn(dollyScale: number) { this._scale /= dollyScale; }
  private dollyOut(dollyScale: number) { this._scale *= dollyScale; }

  private pan(deltaX: number, deltaY: number) {
    const offset = new Vector3();
    const element = this.domElement;
    const position = this.object.position;
    offset.copy(position).sub(this.target);
    let targetDistance = offset.length();
    targetDistance *= Math.tan(((this.object as any).fov / 2) * Math.PI / 180.0);

    // Get the camera's right and up vectors for proper panning
    const cameraUp = this.object.up.clone().normalize();
    const cameraRight = new Vector3().crossVectors(
      new Vector3().subVectors(this.target, position).normalize(),
      cameraUp
    ).normalize();

    const panOffsetRight = cameraRight.clone().multiplyScalar(
      -2 * deltaX * targetDistance / element.clientHeight
    );
    const panOffsetUp = cameraUp.clone().multiplyScalar(
      2 * deltaY * targetDistance / element.clientHeight
    );

    this._panOffset.copy(panOffsetRight).add(panOffsetUp);
    this.target.add(this._panOffset);
  }

  private handleMouseDownRotate(event: MouseEvent) {
    this._rotateStart.set(event.clientX, event.clientY);
    this._state = "ROTATE";
  }
  private handleMouseDownDolly(event: MouseEvent) {
    this._dollyStart.set(event.clientX, event.clientY);
    this._state = "DOLLY";
  }
  private handleMouseDownPan(event: MouseEvent) {
    this._panStart.set(event.clientX, event.clientY);
    this._state = "PAN";
  }

  private handleMouseMoveRotate(event: MouseEvent) {
    this._rotateEnd.set(event.clientX, event.clientY);
    this._rotateDelta.subVectors(this._rotateEnd, this._rotateStart).multiplyScalar(this.rotateSpeed);
    const element = this.domElement;

    // Use consistent scaling for both coordinate systems
    this._sphericalDelta.theta -= (2 * Math.PI * this._rotateDelta.x / element.clientHeight);
    this._sphericalDelta.phi -= (2 * Math.PI * this._rotateDelta.y / element.clientHeight);

    this._rotateStart.copy(this._rotateEnd);
    this.update();
  }

  private handleMouseMoveDolly(event: MouseEvent) {
    this._dollyEnd.set(event.clientX, event.clientY);
    this._dollyDelta.subVectors(this._dollyEnd, this._dollyStart);
    if (this._dollyDelta.y > 0) this.dollyIn(this.getZoomScale());
    else if (this._dollyDelta.y < 0) this.dollyOut(this.getZoomScale());
    this._dollyStart.copy(this._dollyEnd);
    this.update();
  }

  private handleMouseMovePan(event: MouseEvent) {
    this._panEnd.set(event.clientX, event.clientY);
    this._panDelta.subVectors(this._panEnd, this._panStart).multiplyScalar(this.panSpeed);
    this.pan(this._panDelta.x, this._panDelta.y);
    this._panStart.copy(this._panEnd);
    this.update();
  }

  private handleTouchStartDollyPan(event: TouchEvent) {
    const dx = event.touches[0].pageX - event.touches[1].pageX;
    const dy = event.touches[0].pageY - event.touches[1].pageY;
    const distance = Math.sqrt(dx * dx + dy * dy);
    this._dollyStart.set(0, distance);
    const x = (event.touches[0].pageX + event.touches[1].pageX) / 2;
    const y = (event.touches[0].pageY + event.touches[1].pageY) / 2;
    this._panStart.set(x, y);
  }

  private handleTouchMoveRotate(event: TouchEvent) {
    this._rotateEnd.set(event.touches[0].pageX, event.touches[0].pageY);
    this._rotateDelta.subVectors(this._rotateEnd, this._rotateStart).multiplyScalar(this.rotateSpeed);
    const element = this.domElement;

    // Use consistent scaling for both coordinate systems
    this._sphericalDelta.theta -= (2 * Math.PI * this._rotateDelta.x / element.clientHeight);
    this._sphericalDelta.phi -= (2 * Math.PI * this._rotateDelta.y / element.clientHeight);

    this._rotateStart.copy(this._rotateEnd);
    this.update();
  }

  private handleTouchMoveDollyPan(event: TouchEvent) {
    const dx = event.touches[0].pageX - event.touches[1].pageX;
    const dy = event.touches[0].pageY - event.touches[1].pageY;
    const distance = Math.sqrt(dx * dx + dy * dy);
    this._dollyEnd.set(0, distance);
    this._dollyDelta.subVectors(this._dollyEnd, this._dollyStart);
    if (this._dollyDelta.y > 0) this.dollyOut(this.getZoomScale());
    else if (this._dollyDelta.y < 0) this.dollyIn(this.getZoomScale());
    this._dollyStart.copy(this._dollyEnd);

    const x = (event.touches[0].pageX + event.touches[1].pageX) / 2;
    const y = (event.touches[0].pageY + event.touches[1].pageY) / 2;
    this._panEnd.set(x, y);
    this._panDelta.subVectors(this._panEnd, this._panStart);
    this.pan(this._panDelta.x, this._panDelta.y);
    this._panStart.copy(this._panEnd);

    this.update();
  }

  dispose() {
    this.domElement.removeEventListener("contextmenu", this.onContextMenu);
    this.domElement.removeEventListener("mousedown", this.onMouseDown);
    this.domElement.removeEventListener("wheel", this.onMouseWheel);
    this.domElement.removeEventListener("touchstart", this.onTouchStart);
    this.domElement.removeEventListener("touchmove", this.onTouchMove);
    this.domElement.removeEventListener("touchend", this.onTouchEnd);
    window.removeEventListener("keydown", this.onKeyDown);
  }
}

```
