File size: 12,273 Bytes
352fb85
 
 
 
 
 
1486770
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352fb85
1486770
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352fb85
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
import { Camera } from "../cameras/Camera";
import { Matrix3 } from "../math/Matrix3";
import { Quaternion } from "../math/Quaternion";
import { Vector3 } from "../math/Vector3";

class OrbitControls {
  // Vertical (polar) limits: by default, free rotation.
  minAngle: number = -Infinity;
  maxAngle: number = Infinity;
  // Horizontal (azimuth) limits: by default, free rotation.
  minAzimuth: number = -Infinity;
  maxAzimuth: number = Infinity;
  minZoom: number = 0.1;
  maxZoom: number = 30;
  orbitSpeed: number = 1;
  panSpeed: number = 1;
  zoomSpeed: number = 1;
  dampening: number = 0.12;
  setCameraTarget: (newTarget: Vector3) => void = () => {};
  update: () => void;
  dispose: () => void;

  /**
   * @param camera the camera to control.
   * @param canvas the element to attach events.
   * @param alpha initial horizontal angle (in radians).
   * @param beta initial vertical angle (in radians).
   * @param radius initial distance from the target.
   * @param enableKeyboardControls whether to listen to keyboard events.
   * @param inputTarget the point to orbit around.
   */
  constructor(
    camera: Camera,
    canvas: HTMLElement,
    alpha: number = 0.5,
    beta: number = 0.5,
    radius: number = 5,
    enableKeyboardControls: boolean = true,
    inputTarget: Vector3 = new Vector3(),
    // New optional parameters to be set from JSON:
    initAlpha?: number,
    initBeta?: number,
    initRadius?: number,
  ) {
    // Use the JSON-provided initial values if given.
    const defaultAlpha = initAlpha !== undefined ? initAlpha : alpha;
    const defaultBeta = initBeta !== undefined ? initBeta : beta;
    const defaultRadius = initRadius !== undefined ? initRadius : radius;

    // The target (the point the camera orbits around)
    let target = inputTarget.clone();

    // Set both the current and desired orbit parameters to the defaults.
    let desiredTarget = target.clone();
    let desiredAlpha = defaultAlpha;
    let desiredBeta = defaultBeta;
    let desiredRadius = defaultRadius;

    // We now use separate “current” state variables so that update() can lerp toward desired values.
    let currentAlpha = defaultAlpha;
    let currentBeta = defaultBeta;
    let currentRadius = defaultRadius;

    let dragging = false;
    let panning = false;
    let lastDist = 0;
    let lastX = 0;
    let lastY = 0;

    const keys: { [key: string]: boolean } = {};

    let isUpdatingCamera = false;

    // (Optionally you could keep an onCameraChange listener to update the current state when the camera changes,
    // but that may override your JSON settings. In this example, we remove it.)
    // camera.addEventListener("objectChanged", onCameraChange);

    this.setCameraTarget = (newTarget: Vector3) => {
      const dx = newTarget.x - camera.position.x;
      const dy = newTarget.y - camera.position.y;
      const dz = newTarget.z - camera.position.z;
      desiredRadius = Math.sqrt(dx * dx + dy * dy + dz * dz);
      desiredBeta = Math.atan2(dy, Math.sqrt(dx * dx + dz * dz));
      desiredAlpha = -Math.atan2(dx, dz);
      desiredTarget = new Vector3(newTarget.x, newTarget.y, newTarget.z);
    };

    const computeZoomNorm = () => {
      return 0.1 + (0.9 * (desiredRadius - this.minZoom)) / (this.maxZoom - this.minZoom);
    };

    const onKeyDown = (e: KeyboardEvent) => {
      keys[e.code] = true;
      // Map arrow keys to WASD keys
      if (e.code === "ArrowUp") keys["KeyW"] = true;
      if (e.code === "ArrowDown") keys["KeyS"] = true;
      if (e.code === "ArrowLeft") keys["KeyA"] = true;
      if (e.code === "ArrowRight") keys["KeyD"] = true;
    };

    const onKeyUp = (e: KeyboardEvent) => {
      keys[e.code] = false;
      if (e.code === "ArrowUp") keys["KeyW"] = false;
      if (e.code === "ArrowDown") keys["KeyS"] = false;
      if (e.code === "ArrowLeft") keys["KeyA"] = false;
      if (e.code === "ArrowRight") keys["KeyD"] = false;
    };

    const onMouseDown = (e: MouseEvent) => {
      preventDefault(e);
      dragging = true;
      panning = e.button === 2;
      lastX = e.clientX;
      lastY = e.clientY;
      window.addEventListener("mouseup", onMouseUp);
    };

    const onMouseUp = (e: MouseEvent) => {
      preventDefault(e);
      dragging = false;
      panning = false;
      window.removeEventListener("mouseup", onMouseUp);
    };

    const onMouseMove = (e: MouseEvent) => {
      preventDefault(e);
      if (!dragging || !camera) return;

      const dx = e.clientX - lastX;
      const dy = e.clientY - lastY;

      if (panning) {
        const zoomNorm = computeZoomNorm();
        const panX = -dx * this.panSpeed * 0.01 * zoomNorm;
        const panY = -dy * this.panSpeed * 0.01 * zoomNorm;
        const R = Matrix3.RotationFromQuaternion(camera.rotation).buffer;
        const right = new Vector3(R[0], R[3], R[6]);
        const up = new Vector3(R[1], R[4], R[7]);
        desiredTarget = desiredTarget.add(right.multiply(panX));
        desiredTarget = desiredTarget.add(up.multiply(panY));
      } else {
        desiredAlpha -= dx * this.orbitSpeed * 0.003;
        desiredBeta += dy * this.orbitSpeed * 0.003;
        // Clamp vertical angle (beta) if limits are finite
        desiredBeta = Math.min(
          Math.max(desiredBeta, (this.minAngle * Math.PI) / 180),
          (this.maxAngle * Math.PI) / 180,
        );
      }

      lastX = e.clientX;
      lastY = e.clientY;
    };

    const onWheel = (e: WheelEvent) => {
      preventDefault(e);
      const zoomNorm = computeZoomNorm();
      desiredRadius += e.deltaY * this.zoomSpeed * 0.025 * zoomNorm;
      desiredRadius = Math.min(Math.max(desiredRadius, this.minZoom), this.maxZoom);
    };

    const onTouchStart = (e: TouchEvent) => {
      preventDefault(e);
      if (e.touches.length === 1) {
        dragging = true;
        panning = false;
        lastX = e.touches[0].clientX;
        lastY = e.touches[0].clientY;
        lastDist = 0;
      } else if (e.touches.length === 2) {
        dragging = true;
        panning = true;
        lastX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
        lastY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
        const distX = e.touches[0].clientX - e.touches[1].clientX;
        const distY = e.touches[0].clientY - e.touches[1].clientY;
        lastDist = Math.sqrt(distX * distX + distY * distY);
      }
    };

    const onTouchEnd = (e: TouchEvent) => {
      preventDefault(e);
      dragging = false;
      panning = false;
    };

    const onTouchMove = (e: TouchEvent) => {
      preventDefault(e);
      if (!dragging || !camera) return;

      if (panning) {
        const zoomNorm = computeZoomNorm();
        const distX = e.touches[0].clientX - e.touches[1].clientX;
        const distY = e.touches[0].clientY - e.touches[1].clientY;
        const dist = Math.sqrt(distX * distX + distY * distY);
        const delta = lastDist - dist;
        desiredRadius += delta * this.zoomSpeed * 0.1 * zoomNorm;
        desiredRadius = Math.min(Math.max(desiredRadius, this.minZoom), this.maxZoom);
        lastDist = dist;

        const touchX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
        const touchY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
        const dx = touchX - lastX;
        const dy = touchY - lastY;
        const R = Matrix3.RotationFromQuaternion(camera.rotation).buffer;
        const right = new Vector3(R[0], R[3], R[6]);
        const up = new Vector3(R[1], R[4], R[7]);
        desiredTarget = desiredTarget.add(right.multiply(-dx * this.panSpeed * 0.025 * zoomNorm));
        desiredTarget = desiredTarget.add(up.multiply(-dy * this.panSpeed * 0.025 * zoomNorm));
        lastX = touchX;
        lastY = touchY;
      } else {
        const dx = e.touches[0].clientX - lastX;
        const dy = e.touches[0].clientY - lastY;

        desiredAlpha -= dx * this.orbitSpeed * 0.003;
        desiredBeta += dy * this.orbitSpeed * 0.003;
        desiredBeta = Math.min(
          Math.max(desiredBeta, (this.minAngle * Math.PI) / 180),
          (this.maxAngle * Math.PI) / 180,
        );

        lastX = e.touches[0].clientX;
        lastY = e.touches[0].clientY;
      }
    };

    const lerp = (a: number, b: number, t: number) => (1 - t) * a + t * b;

    this.update = () => {
      isUpdatingCamera = true;

      // Lerp the current state toward the desired state.
      currentAlpha = lerp(currentAlpha, desiredAlpha, this.dampening);
      currentBeta = lerp(currentBeta, desiredBeta, this.dampening);
      currentRadius = lerp(currentRadius, desiredRadius, this.dampening);
      target = target.lerp(desiredTarget, this.dampening);

      // Recompute the camera position from currentAlpha/currentBeta/currentRadius.
      const x = target.x + currentRadius * Math.sin(currentAlpha) * Math.cos(currentBeta);
      const y = target.y - currentRadius * Math.sin(currentBeta);
      const z = target.z - currentRadius * Math.cos(currentAlpha) * Math.cos(currentBeta);
      camera.position = new Vector3(x, y, z);

      // Compute new rotation so that the camera looks at the target.
      const direction = target.subtract(camera.position).normalize();
      const rx = Math.asin(-direction.y);
      const ry = Math.atan2(direction.x, direction.z);
      camera.rotation = Quaternion.FromEuler(new Vector3(rx, ry, 0));

      const moveSpeed = 0.025;
      const rotateSpeed = 0.01;
      const R = Matrix3.RotationFromQuaternion(camera.rotation).buffer;
      const forward = new Vector3(-R[2], -R[5], -R[8]);
      const right = new Vector3(R[0], R[3], R[6]);

      if (keys["KeyS"]) desiredTarget = desiredTarget.add(forward.multiply(moveSpeed));
      if (keys["KeyW"]) desiredTarget = desiredTarget.subtract(forward.multiply(moveSpeed));
      if (keys["KeyA"]) desiredTarget = desiredTarget.subtract(right.multiply(moveSpeed));
      if (keys["KeyD"]) desiredTarget = desiredTarget.add(right.multiply(moveSpeed));

      // Horizontal (azimuth) rotation with 'e' and 'q'
      if (keys["KeyE"]) desiredAlpha += rotateSpeed;
      if (keys["KeyQ"]) desiredAlpha -= rotateSpeed;
      desiredAlpha = Math.min(
        Math.max(desiredAlpha, (this.minAzimuth * Math.PI) / 180),
        (this.maxAzimuth * Math.PI) / 180,
      );

      // Vertical (polar) rotation with 'r' and 'f'
      if (keys["KeyR"]) desiredBeta += rotateSpeed;
      if (keys["KeyF"]) desiredBeta -= rotateSpeed;
      desiredBeta = Math.min(
        Math.max(desiredBeta, (this.minAngle * Math.PI) / 180),
        (this.maxAngle * Math.PI) / 180,
      );

      isUpdatingCamera = false;
    };

    const preventDefault = (e: Event) => {
      e.preventDefault();
      e.stopPropagation();
    };

    this.dispose = () => {
      canvas.removeEventListener("dragenter", preventDefault);
      canvas.removeEventListener("dragover", preventDefault);
      canvas.removeEventListener("dragleave", preventDefault);
      canvas.removeEventListener("contextmenu", preventDefault);

      canvas.removeEventListener("mousedown", onMouseDown);
      canvas.removeEventListener("mousemove", onMouseMove);
      canvas.removeEventListener("wheel", onWheel);

      canvas.removeEventListener("touchstart", onTouchStart);
      canvas.removeEventListener("touchend", onTouchEnd);
      canvas.removeEventListener("touchmove", onTouchMove);

      if (enableKeyboardControls) {
        window.removeEventListener("keydown", onKeyDown);
        window.removeEventListener("keyup", onKeyUp);
      }
    };

    if (enableKeyboardControls) {
      window.addEventListener("keydown", onKeyDown);
      window.addEventListener("keyup", onKeyUp);
    }

    canvas.addEventListener("dragenter", preventDefault);
    canvas.addEventListener("dragover", preventDefault);
    canvas.addEventListener("dragleave", preventDefault);
    canvas.addEventListener("contextmenu", preventDefault);

    canvas.addEventListener("mousedown", onMouseDown);
    canvas.addEventListener("mousemove", onMouseMove);
    canvas.addEventListener("wheel", onWheel);

    canvas.addEventListener("touchstart", onTouchStart);
    canvas.addEventListener("touchend", onTouchEnd);
    canvas.addEventListener("touchmove", onTouchMove);

    this.update();
  }
}

export { OrbitControls };