MikaFil commited on
Commit
dd7950b
·
verified ·
1 Parent(s): 8d41429

Update orbit-camera.js

Browse files
Files changed (1) hide show
  1. orbit-camera.js +509 -14
orbit-camera.js CHANGED
@@ -1,12 +1,13 @@
1
  // orbit-camera.js
2
  // ==============================
3
- console.log('[OrbitFILE] loaded orbit-camera.js'); // preuve que le fichier est bien chargé
4
 
5
  /*
6
  * PlayCanvas Orbit Camera for GSplat/SOGS/GLB viewers.
7
- * - Handles min/max zoom, pitch, yaw, minY (don't go below ground)
8
- * - Mouse, touch & keyboard controls (orbit, pan, zoom)
9
- * - Attributes are auto-injected by viewer.js
 
 
10
  */
11
 
12
  var OrbitCamera = pc.createScript('orbitCamera');
@@ -62,7 +63,7 @@ OrbitCamera.prototype.resetAndLookAtPoint = function (resetPoint, lookAtPoint) {
62
  this.distance = distance.length();
63
  this.pivotPoint.copy(lookAtPoint);
64
  var cameraQuat = this.entity.getRotation();
65
- this.yaw = this._calcYaw(cameraQuat);
66
  this.pitch = this._calcPitch(cameraQuat, this.yaw);
67
  this._removeInertia();
68
  this._updatePosition();
@@ -88,7 +89,7 @@ OrbitCamera.prototype.resetToPosition = function (position, lookAtPoint) {
88
  distanceVec.sub2(position, lookAtPoint);
89
  this._targetDistance = this._distance = distanceVec.length();
90
  var cameraQuat = this.entity.getRotation();
91
- this._targetYaw = this._yaw = this._calcYaw(cameraQuat);
92
  this._targetPitch = this._pitch = this._calcPitch(cameraQuat, this._yaw);
93
  this._removeInertia();
94
  this._updatePosition();
@@ -112,13 +113,14 @@ OrbitCamera.prototype.initialize = function () {
112
 
113
  this._modelsAabb = new pc.BoundingBox();
114
  this._buildAabb(this.focusEntity || this.app.root);
 
115
  this.entity.lookAt(this._modelsAabb.center);
116
 
117
  this._pivotPoint = new pc.Vec3();
118
  this._pivotPoint.copy(this._modelsAabb.center);
119
 
120
  var cameraQuat = this.entity.getRotation();
121
- this._yaw = this._calcYaw(cameraQuat); // DEGRÉS
122
  this._pitch = this._clampPitchAngle(this._calcPitch(cameraQuat, this._yaw));
123
  this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
124
 
@@ -147,28 +149,31 @@ OrbitCamera.prototype.initialize = function () {
147
  if (value) { this.focus(this.focusEntity || this.app.root); }
148
  });
149
 
150
- this.on('destroy', function () { window.removeEventListener('resize', onWindowResize, false); });
151
-
152
- console.log('[OrbitCam] init yaw=%s° pitch=%s° dist=%s',
153
- this._yaw.toFixed(2), this._pitch.toFixed(2), this._distance.toFixed(3));
154
  };
155
 
156
  OrbitCamera.prototype.update = function (dt) {
157
  var t = this.inertiaFactor === 0 ? 1 : Math.min(dt / this.inertiaFactor, 1);
158
  this._distance = pc.math.lerp(this._distance, this._targetDistance, t);
159
- this._yaw = pc.math.lerp(this._yaw, this._targetYaw, t);
160
- this._pitch = pc.math.lerp(this._pitch, this._targetPitch, t);
161
  this._updatePosition();
162
  };
163
 
164
  OrbitCamera.prototype._updatePosition = function () {
165
  this.entity.setLocalPosition(0, 0, 0);
166
  this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
 
167
  var position = this.entity.getPosition();
168
  position.copy(this.entity.forward);
169
  position.mulScalar(-this._distance);
170
  position.add(this.pivotPoint);
 
 
171
  position.y = Math.max(position.y, this.minY);
 
172
  this.entity.setPosition(position);
173
  };
174
 
@@ -185,4 +190,494 @@ OrbitCamera.prototype._checkAspectRatio = function () {
185
  };
186
 
187
  OrbitCamera.prototype._buildAabb = function (entity) {
188
- var i, m, mes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  // orbit-camera.js
2
  // ==============================
 
3
 
4
  /*
5
  * PlayCanvas Orbit Camera for GSplat/SOGS/GLB viewers.
6
+ * - Min/Max zoom, pitch, yaw, minY clamping
7
+ * - Mouse, Touch & Keyboard controls (orbit, pan, zoom)
8
+ * - Robust keyboard capture (works even if canvas has no focus)
9
+ * - Yaw conversion fixed (degrees everywhere)
10
+ * - Optional debug logs
11
  */
12
 
13
  var OrbitCamera = pc.createScript('orbitCamera');
 
63
  this.distance = distance.length();
64
  this.pivotPoint.copy(lookAtPoint);
65
  var cameraQuat = this.entity.getRotation();
66
+ this.yaw = this._calcYaw(cameraQuat); // degrees
67
  this.pitch = this._calcPitch(cameraQuat, this.yaw);
68
  this._removeInertia();
69
  this._updatePosition();
 
89
  distanceVec.sub2(position, lookAtPoint);
90
  this._targetDistance = this._distance = distanceVec.length();
91
  var cameraQuat = this.entity.getRotation();
92
+ this._targetYaw = this._yaw = this._calcYaw(cameraQuat); // degrees
93
  this._targetPitch = this._pitch = this._calcPitch(cameraQuat, this._yaw);
94
  this._removeInertia();
95
  this._updatePosition();
 
113
 
114
  this._modelsAabb = new pc.BoundingBox();
115
  this._buildAabb(this.focusEntity || this.app.root);
116
+
117
  this.entity.lookAt(this._modelsAabb.center);
118
 
119
  this._pivotPoint = new pc.Vec3();
120
  this._pivotPoint.copy(this._modelsAabb.center);
121
 
122
  var cameraQuat = this.entity.getRotation();
123
+ this._yaw = this._calcYaw(cameraQuat); // degrees
124
  this._pitch = this._clampPitchAngle(this._calcPitch(cameraQuat, this._yaw));
125
  this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
126
 
 
149
  if (value) { this.focus(this.focusEntity || this.app.root); }
150
  });
151
 
152
+ this.on('destroy', function () {
153
+ window.removeEventListener('resize', onWindowResize, false);
154
+ });
 
155
  };
156
 
157
  OrbitCamera.prototype.update = function (dt) {
158
  var t = this.inertiaFactor === 0 ? 1 : Math.min(dt / this.inertiaFactor, 1);
159
  this._distance = pc.math.lerp(this._distance, this._targetDistance, t);
160
+ this._yaw = pc.math.lerp(this._yaw, this._targetYaw, t);
161
+ this._pitch = pc.math.lerp(this._pitch, this._targetPitch, t);
162
  this._updatePosition();
163
  };
164
 
165
  OrbitCamera.prototype._updatePosition = function () {
166
  this.entity.setLocalPosition(0, 0, 0);
167
  this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
168
+
169
  var position = this.entity.getPosition();
170
  position.copy(this.entity.forward);
171
  position.mulScalar(-this._distance);
172
  position.add(this.pivotPoint);
173
+
174
+ // Never below ground
175
  position.y = Math.max(position.y, this.minY);
176
+
177
  this.entity.setPosition(position);
178
  };
179
 
 
190
  };
191
 
192
  OrbitCamera.prototype._buildAabb = function (entity) {
193
+ var i, m, meshInstances = [];
194
+
195
+ var renders = entity.findComponents('render');
196
+ for (i = 0; i < renders.length; i++) {
197
+ var render = renders[i];
198
+ for (m = 0; m < render.meshInstances.length; m++) {
199
+ meshInstances.push(render.meshInstances[m]);
200
+ }
201
+ }
202
+
203
+ var models = entity.findComponents('model');
204
+ for (i = 0; i < models.length; i++) {
205
+ var model = models[i];
206
+ for (m = 0; m < model.meshInstances.length; m++) {
207
+ meshInstances.push(model.meshInstances[m]);
208
+ }
209
+ }
210
+
211
+ var gsplats = entity.findComponents('gsplat');
212
+ for (i = 0; i < gsplats.length; i++) {
213
+ var gsplat = gsplats[i];
214
+ var instance = gsplat.instance;
215
+ if (instance && instance.meshInstance) {
216
+ meshInstances.push(instance.meshInstance);
217
+ }
218
+ }
219
+
220
+ for (i = 0; i < meshInstances.length; i++) {
221
+ if (i === 0) {
222
+ this._modelsAabb.copy(meshInstances[i].aabb);
223
+ } else {
224
+ this._modelsAabb.add(meshInstances[i].aabb);
225
+ }
226
+ }
227
+ };
228
+
229
+ OrbitCamera.prototype._calcYaw = function (quat) {
230
+ // return DEGREES
231
+ var f = new pc.Vec3();
232
+ quat.transformVector(pc.Vec3.FORWARD, f);
233
+ return Math.atan2(-f.x, -f.z) * pc.math.RAD_TO_DEG;
234
+ };
235
+
236
+ OrbitCamera.prototype._clampDistance = function (distance) {
237
+ if (this.distanceMax > 0) {
238
+ return pc.math.clamp(distance, this.distanceMin, this.distanceMax);
239
+ }
240
+ return Math.max(distance, this.distanceMin);
241
+ };
242
+
243
+ OrbitCamera.prototype._clampPitchAngle = function (pitch) {
244
+ return pc.math.clamp(pitch, this.pitchAngleMin, this.pitchAngleMax);
245
+ };
246
+
247
+ OrbitCamera.prototype._clampYawAngle = function (yaw) {
248
+ return pc.math.clamp(yaw, this.yawAngleMin, this.yawAngleMax);
249
+ };
250
+
251
+ OrbitCamera.quatWithoutYaw = new pc.Quat();
252
+ OrbitCamera.yawOffset = new pc.Quat();
253
+
254
+ OrbitCamera.prototype._calcPitch = function (quat, yaw) {
255
+ var quatWithoutYaw = OrbitCamera.quatWithoutYaw;
256
+ var yawOffset = OrbitCamera.yawOffset;
257
+ yawOffset.setFromEulerAngles(0, -yaw, 0);
258
+ quatWithoutYaw.mul2(yawOffset, quat);
259
+ var f = new pc.Vec3();
260
+ quatWithoutYaw.transformVector(pc.Vec3.FORWARD, f);
261
+ return Math.atan2(-f.y, -f.z) * pc.math.RAD_TO_DEG;
262
+ };
263
+
264
+ // =================== Orbit Camera Input Mouse Script ========================
265
+
266
+ var OrbitCameraInputMouse = pc.createScript('orbitCameraInputMouse');
267
+ OrbitCameraInputMouse.attributes.add('orbitSensitivity', { type: 'number', default: 0.3, title: 'Orbit Sensitivity' });
268
+ OrbitCameraInputMouse.attributes.add('distanceSensitivity', { type: 'number', default: 0.4, title: 'Distance Sensitivity' });
269
+
270
+ OrbitCameraInputMouse.prototype.initialize = function () {
271
+ this.orbitCamera = this.entity.script.orbitCamera;
272
+ if (this.orbitCamera) {
273
+ var self = this;
274
+ var onMouseOut = function () { self.onMouseOut(); };
275
+
276
+ this.app.mouse.on(pc.EVENT_MOUSEDOWN, this.onMouseDown, this);
277
+ this.app.mouse.on(pc.EVENT_MOUSEUP, this.onMouseUp, this);
278
+ this.app.mouse.on(pc.EVENT_MOUSEMOVE, this.onMouseMove, this);
279
+ this.app.mouse.on(pc.EVENT_MOUSEWHEEL,this.onMouseWheel,this);
280
+
281
+ window.addEventListener('mouseout', onMouseOut, false);
282
+
283
+ this.on('destroy', function () {
284
+ this.app.mouse.off(pc.EVENT_MOUSEDOWN, this.onMouseDown, this);
285
+ this.app.mouse.off(pc.EVENT_MOUSEUP, this.onMouseUp, this);
286
+ this.app.mouse.off(pc.EVENT_MOUSEMOVE, this.onMouseMove, this);
287
+ this.app.mouse.off(pc.EVENT_MOUSEWHEEL,this.onMouseWheel,this);
288
+ window.removeEventListener('mouseout', onMouseOut, false);
289
+ });
290
+ }
291
+ this.app.mouse.disableContextMenu();
292
+
293
+ this.lookButtonDown = false;
294
+ this.panButtonDown = false;
295
+ this.lastPoint = new pc.Vec2();
296
+ };
297
+ OrbitCameraInputMouse.fromWorldPoint = new pc.Vec3();
298
+ OrbitCameraInputMouse.toWorldPoint = new pc.Vec3();
299
+ OrbitCameraInputMouse.worldDiff = new pc.Vec3();
300
+
301
+ OrbitCameraInputMouse.prototype.pan = function (screenPoint) {
302
+ var fromWorldPoint = OrbitCameraInputMouse.fromWorldPoint;
303
+ var toWorldPoint = OrbitCameraInputMouse.toWorldPoint;
304
+ var worldDiff = OrbitCameraInputMouse.worldDiff;
305
+
306
+ var camera = this.entity.camera;
307
+ var distance = this.orbitCamera.distance;
308
+
309
+ camera.screenToWorld(screenPoint.x, this.lastPoint.y, distance, fromWorldPoint);
310
+ camera.screenToWorld(this.lastPoint.x, this.lastPoint.y, distance, toWorldPoint);
311
+ camera.screenToWorld(screenPoint.x, screenPoint.y, distance, fromWorldPoint);
312
+ worldDiff.sub2(toWorldPoint, fromWorldPoint);
313
+
314
+ var proposedPivot = this.orbitCamera.pivotPoint.clone().add(worldDiff);
315
+ var minY = this.orbitCamera.minY;
316
+ var resultingY = this.orbitCamera.worldCameraYForPivot(proposedPivot);
317
+
318
+ if (resultingY >= minY - 1e-4) {
319
+ this.orbitCamera.pivotPoint.add(worldDiff);
320
+ } else {
321
+ worldDiff.y = 0;
322
+ proposedPivot = this.orbitCamera.pivotPoint.clone().add(worldDiff);
323
+ resultingY = this.orbitCamera.worldCameraYForPivot(proposedPivot);
324
+ if (resultingY >= minY - 1e-4) {
325
+ this.orbitCamera.pivotPoint.add(worldDiff);
326
+ }
327
+ }
328
+ };
329
+
330
+ OrbitCameraInputMouse.prototype.onMouseDown = function (event) {
331
+ switch (event.button) {
332
+ case pc.MOUSEBUTTON_LEFT: this.panButtonDown = true; break;
333
+ case pc.MOUSEBUTTON_MIDDLE:
334
+ case pc.MOUSEBUTTON_RIGHT: this.lookButtonDown = true; break;
335
+ }
336
+ };
337
+ OrbitCameraInputMouse.prototype.onMouseUp = function (event) {
338
+ switch (event.button) {
339
+ case pc.MOUSEBUTTON_LEFT: this.panButtonDown = false; break;
340
+ case pc.MOUSEBUTTON_MIDDLE:
341
+ case pc.MOUSEBUTTON_RIGHT: this.lookButtonDown = false; break;
342
+ }
343
+ };
344
+ OrbitCameraInputMouse.prototype.onMouseMove = function (event) {
345
+ if (this.lookButtonDown) {
346
+ var sens = this.orbitSensitivity;
347
+ var deltaPitch = event.dy * sens;
348
+ var deltaYaw = event.dx * sens;
349
+
350
+ var currPitch = this.orbitCamera.pitch;
351
+ var currYaw = this.orbitCamera.yaw;
352
+ var currDist = this.orbitCamera.distance;
353
+ var currPivot = this.orbitCamera.pivotPoint.clone();
354
+
355
+ var camQuat = new pc.Quat().setFromEulerAngles(currPitch, currYaw, 0);
356
+ var forward = new pc.Vec3(); camQuat.transformVector(pc.Vec3.FORWARD, forward);
357
+ var preY = currPivot.y + (-forward.y) * currDist;
358
+
359
+ var proposedPitch = currPitch - deltaPitch;
360
+ var testQuat = new pc.Quat().setFromEulerAngles(proposedPitch, currYaw, 0);
361
+ var testForward = new pc.Vec3(); testQuat.transformVector(pc.Vec3.FORWARD, testForward);
362
+ var proposedY = currPivot.y + (-testForward.y) * currDist;
363
+
364
+ var minY = this.orbitCamera.minY;
365
+ var wouldGoBelow = proposedY < minY - 1e-4;
366
+
367
+ if (wouldGoBelow && (proposedY < preY)) {
368
+ this.orbitCamera.yaw = currYaw - deltaYaw;
369
+ } else {
370
+ this.orbitCamera.pitch = proposedPitch;
371
+ this.orbitCamera.yaw = currYaw - deltaYaw;
372
+ }
373
+ } else if (this.panButtonDown) {
374
+ this.pan(new pc.Vec2(event.x, event.y));
375
+ }
376
+
377
+ this.lastPoint.set(event.x, event.y);
378
+ };
379
+
380
+ OrbitCameraInputMouse.prototype.onMouseWheel = function (event) {
381
+ if (this.entity.camera.projection === pc.PROJECTION_PERSPECTIVE) {
382
+ this.orbitCamera.distance -= event.wheelDelta * this.distanceSensitivity * (this.orbitCamera.distance * 0.1);
383
+ } else {
384
+ this.orbitCamera.orthoHeight -= event.wheelDelta * this.distanceSensitivity * (this.orbitCamera.orthoHeight * 0.1);
385
+ }
386
+ event.event.preventDefault();
387
+ };
388
+ OrbitCameraInputMouse.prototype.onMouseOut = function () {
389
+ this.lookButtonDown = false;
390
+ this.panButtonDown = false;
391
+ };
392
+
393
+ // =================== Orbit Camera Input Touch Script ========================
394
+
395
+ var OrbitCameraInputTouch = pc.createScript('orbitCameraInputTouch');
396
+ OrbitCameraInputTouch.attributes.add('orbitSensitivity', { type: 'number', default: 0.6, title: 'Orbit Sensitivity' });
397
+ OrbitCameraInputTouch.attributes.add('distanceSensitivity', { type: 'number', default: 0.5, title: 'Distance Sensitivity' });
398
+
399
+ OrbitCameraInputTouch.prototype.initialize = function () {
400
+ this.orbitCamera = this.entity.script.orbitCamera;
401
+ this.lastTouchPoint = new pc.Vec2();
402
+ this.lastPinchMidPoint = new pc.Vec2();
403
+ this.lastPinchDistance = 0;
404
+
405
+ if (this.orbitCamera && this.app.touch) {
406
+ this.app.touch.on(pc.EVENT_TOUCHSTART, this.onTouchStartEndCancel, this);
407
+ this.app.touch.on(pc.EVENT_TOUCHEND, this.onTouchStartEndCancel, this);
408
+ this.app.touch.on(pc.EVENT_TOUCHCANCEL, this.onTouchStartEndCancel, this);
409
+ this.app.touch.on(pc.EVENT_TOUCHMOVE, this.onTouchMove, this);
410
+
411
+ this.on('destroy', function () {
412
+ this.app.touch.off(pc.EVENT_TOUCHSTART, this.onTouchStartEndCancel, this);
413
+ this.app.touch.off(pc.EVENT_TOUCHEND, this.onTouchStartEndCancel, this);
414
+ this.app.touch.off(pc.EVENT_TOUCHCANCEL, this.onTouchStartEndCancel, this);
415
+ this.app.touch.off(pc.EVENT_TOUCHMOVE, this.onTouchMove, this);
416
+ });
417
+ }
418
+ };
419
+ OrbitCameraInputTouch.prototype.getPinchDistance = function (pointA, pointB) {
420
+ var dx = pointA.x - pointB.x;
421
+ var dy = pointA.y - pointB.y;
422
+ return Math.sqrt((dx * dx) + (dy * dy));
423
+ };
424
+ OrbitCameraInputTouch.prototype.calcMidPoint = function (pointA, pointB, result) {
425
+ result.set(pointB.x - pointA.x, pointB.y - pointA.y);
426
+ result.mulScalar(0.5);
427
+ result.x += pointA.x;
428
+ result.y += pointA.y;
429
+ };
430
+ OrbitCameraInputTouch.prototype.onTouchStartEndCancel = function (event) {
431
+ var touches = event.touches;
432
+ if (touches.length === 1) {
433
+ this.lastTouchPoint.set(touches[0].x, touches[0].y);
434
+ } else if (touches.length === 2) {
435
+ this.lastPinchDistance = this.getPinchDistance(touches[0], touches[1]);
436
+ this.calcMidPoint(touches[0], touches[1], this.lastPinchMidPoint);
437
+ }
438
+ };
439
+ OrbitCameraInputTouch.fromWorldPoint = new pc.Vec3();
440
+ OrbitCameraInputTouch.toWorldPoint = new pc.Vec3();
441
+ OrbitCameraInputTouch.worldDiff = new pc.Vec3();
442
+
443
+ OrbitCameraInputTouch.prototype.pan = function (midPoint) {
444
+ var fromWorldPoint = OrbitCameraInputTouch.fromWorldPoint;
445
+ var toWorldPoint = OrbitCameraInputTouch.toWorldPoint;
446
+ var worldDiff = OrbitCameraInputTouch.worldDiff;
447
+
448
+ var camera = this.entity.camera;
449
+ var distance = this.orbitCamera.distance;
450
+
451
+ camera.screenToWorld(midPoint.x, midPoint.y, distance, fromWorldPoint);
452
+ camera.screenToWorld(this.lastPinchMidPoint.x, this.lastPinchMidPoint.y, distance, toWorldPoint);
453
+ worldDiff.sub2(toWorldPoint, fromWorldPoint);
454
+
455
+ var proposedPivot = this.orbitCamera.pivotPoint.clone().add(worldDiff);
456
+ var minY = this.orbitCamera.minY;
457
+ var resultingY = this.orbitCamera.worldCameraYForPivot(proposedPivot);
458
+
459
+ if (resultingY >= minY - 1e-4) {
460
+ this.orbitCamera.pivotPoint.add(worldDiff);
461
+ } else {
462
+ worldDiff.y = 0;
463
+ proposedPivot = this.orbitCamera.pivotPoint.clone().add(worldDiff);
464
+ resultingY = this.orbitCamera.worldCameraYForPivot(proposedPivot);
465
+ if (resultingY >= minY - 1e-4) {
466
+ this.orbitCamera.pivotPoint.add(worldDiff);
467
+ }
468
+ }
469
+ };
470
+
471
+ OrbitCameraInputTouch.pinchMidPoint = new pc.Vec2();
472
+
473
+ OrbitCameraInputTouch.prototype.onTouchMove = function (event) {
474
+ var pinchMidPoint = OrbitCameraInputTouch.pinchMidPoint;
475
+ var touches = event.touches;
476
+
477
+ if (touches.length === 1) {
478
+ var touch = touches[0];
479
+
480
+ var sens = this.orbitSensitivity;
481
+ var deltaPitch = (touch.y - this.lastTouchPoint.y) * sens;
482
+ var deltaYaw = (touch.x - this.lastTouchPoint.x) * sens;
483
+
484
+ var currPitch = this.orbitCamera.pitch;
485
+ var currYaw = this.orbitCamera.yaw;
486
+ var currDist = this.orbitCamera.distance;
487
+ var currPivot = this.orbitCamera.pivotPoint.clone();
488
+
489
+ var camQuat = new pc.Quat().setFromEulerAngles(currPitch, currYaw, 0);
490
+ var forward = new pc.Vec3(); camQuat.transformVector(pc.Vec3.FORWARD, forward);
491
+ var preY = currPivot.y + (-forward.y) * currDist;
492
+
493
+ var proposedPitch = currPitch - deltaPitch;
494
+ var testQuat = new pc.Quat().setFromEulerAngles(proposedPitch, currYaw, 0);
495
+ var testForward = new pc.Vec3(); testQuat.transformVector(pc.Vec3.FORWARD, testForward);
496
+ var proposedY = currPivot.y + (-testForward.y) * currDist;
497
+
498
+ var minY = this.orbitCamera.minY;
499
+ var wouldGoBelow = proposedY < minY - 1e-4;
500
+
501
+ if (wouldGoBelow && (proposedY < preY)) {
502
+ this.orbitCamera.yaw = currYaw - deltaYaw;
503
+ } else {
504
+ this.orbitCamera.pitch = proposedPitch;
505
+ this.orbitCamera.yaw = currYaw - deltaYaw;
506
+ }
507
+
508
+ this.lastTouchPoint.set(touch.x, touch.y);
509
+ } else if (touches.length === 2) {
510
+ var currentPinchDistance = this.getPinchDistance(touches[0], touches[1]);
511
+ var diffInPinchDistance = currentPinchDistance - this.lastPinchDistance;
512
+ this.lastPinchDistance = currentPinchDistance;
513
+
514
+ this.orbitCamera.distance -= (diffInPinchDistance * this.distanceSensitivity * 0.1) * (this.orbitCamera.distance * 0.1);
515
+
516
+ this.calcMidPoint(touches[0], touches[1], pinchMidPoint);
517
+ this.pan(pinchMidPoint);
518
+ this.lastPinchMidPoint.copy(pinchMidPoint);
519
+ }
520
+ };
521
+
522
+ // =================== Orbit Camera Input Keyboard Script ========================
523
+
524
+ var OrbitCameraInputKeyboard = pc.createScript('orbitCameraInputKeyboard');
525
+
526
+ /*
527
+ * Keyboard:
528
+ * - ↑ / ↓ : orbit pitch (north/south)
529
+ * - ← / → : orbit yaw (left/right)
530
+ * - Shift + ↑ / ↓ : zoom in / out (persp: distance, ortho: orthoHeight)
531
+ * - Shift + ← / → : pan sideways
532
+ *
533
+ * Robust capture: uses both pc.Keyboard and window key events
534
+ * (works even if the canvas isn’t focused). Optionally focus canvas on pointer down.
535
+ */
536
+
537
+ OrbitCameraInputKeyboard.attributes.add('orbitDegreesPerSecond', { type: 'number', default: 180, title: 'Orbit Degrees / s' });
538
+ OrbitCameraInputKeyboard.attributes.add('zoomFactorPerSecond', { type: 'number', default: 2.0, title: 'Zoom Factor / s' });
539
+ OrbitCameraInputKeyboard.attributes.add('panUnitsPerSecond', { type: 'number', default: 1.0, title: 'Pan (distance frac) / s' });
540
+ OrbitCameraInputKeyboard.attributes.add('autoFocusCanvas', { type: 'boolean', default: true, title: 'Auto-focus Canvas on Click' });
541
+ OrbitCameraInputKeyboard.attributes.add('debugLogs', { type: 'boolean', default: false, title: 'Enable Debug Logs' });
542
+
543
+ OrbitCameraInputKeyboard.prototype._log = function () {
544
+ if (!this.debugLogs) return;
545
+ console.log.apply(console, arguments); // eslint-disable-line no-console
546
+ };
547
+
548
+ OrbitCameraInputKeyboard.prototype.initialize = function () {
549
+ this.orbitCamera = this.entity.script.orbitCamera;
550
+
551
+ // Ensure PlayCanvas keyboard exists
552
+ if (!this.app.keyboard) {
553
+ this.app.keyboard = new pc.Keyboard(window);
554
+ this._log('[OrbitKB] created pc.Keyboard(window)');
555
+ }
556
+
557
+ // Window-level key state (works regardless of focus target)
558
+ this._keyState = Object.create(null);
559
+ this._onWinKeyDown = (e) => {
560
+ this._keyState[e.key] = true;
561
+ // prevent page scroll on arrows
562
+ if (e.key && e.key.indexOf('Arrow') === 0) e.preventDefault();
563
+ this._log('[OrbitKB] down', e.key);
564
+ };
565
+ this._onWinKeyUp = (e) => {
566
+ this._keyState[e.key] = false;
567
+ this._log('[OrbitKB] up', e.key);
568
+ };
569
+ window.addEventListener('keydown', this._onWinKeyDown, { passive: false });
570
+ window.addEventListener('keyup', this._onWinKeyUp);
571
+
572
+ // Optional: make canvas focusable and auto-focus on click
573
+ this.canvas = this.app.graphicsDevice && this.app.graphicsDevice.canvas;
574
+ if (this.canvas && this.autoFocusCanvas) {
575
+ if (!this.canvas.hasAttribute('tabindex')) this.canvas.setAttribute('tabindex', '0');
576
+ this._onPointerDown = () => { try { this.canvas.focus(); } catch (e) {} };
577
+ this.canvas.addEventListener('pointerdown', this._onPointerDown);
578
+ }
579
+
580
+ this.on('destroy', function () {
581
+ window.removeEventListener('keydown', this._onWinKeyDown, { passive: false });
582
+ window.removeEventListener('keyup', this._onWinKeyUp);
583
+ if (this.canvas && this._onPointerDown) this.canvas.removeEventListener('pointerdown', this._onPointerDown);
584
+ });
585
+ };
586
+
587
+ OrbitCameraInputKeyboard.prototype._pressed = function (pcKey, keyString) {
588
+ // true if either PlayCanvas reports it or our window listener does
589
+ var pcPressed = this.app.keyboard && this.app.keyboard.isPressed ? this.app.keyboard.isPressed(pcKey) : false;
590
+ var winPressed = !!this._keyState[keyString];
591
+ return pcPressed || winPressed;
592
+ };
593
+
594
+ OrbitCameraInputKeyboard.prototype._wouldGoBelowMinYWithPitch = function (proposedPitch) {
595
+ var cam = this.orbitCamera;
596
+ var currPitch = cam.pitch;
597
+ var currYaw = cam.yaw;
598
+ var currDist = cam.distance;
599
+ var pivot = cam.pivotPoint.clone();
600
+
601
+ var camQuat = new pc.Quat().setFromEulerAngles(currPitch, currYaw, 0);
602
+ var forward = new pc.Vec3(); camQuat.transformVector(pc.Vec3.FORWARD, forward);
603
+ var preY = pivot.y + (-forward.y) * currDist;
604
+
605
+ var testQuat = new pc.Quat().setFromEulerAngles(proposedPitch, currYaw, 0);
606
+ var testForward = new pc.Vec3(); testQuat.transformVector(pc.Vec3.FORWARD, testForward);
607
+ var proposedY = pivot.y + (-testForward.y) * currDist;
608
+
609
+ var minY = cam.minY;
610
+ return { wouldGoBelow: proposedY < minY - 1e-4, preY: preY, proposedY: proposedY };
611
+ };
612
+
613
+ OrbitCameraInputKeyboard.prototype._panSideways = function (delta) {
614
+ var cam = this.orbitCamera;
615
+
616
+ var quat = new pc.Quat().setFromEulerAngles(cam.pitch, cam.yaw, 0);
617
+ var right = new pc.Vec3(); quat.transformVector(pc.Vec3.RIGHT, right);
618
+ var move = right.scale(delta);
619
+
620
+ var minY = cam.minY;
621
+ var proposedPivot = cam.pivotPoint.clone().add(move);
622
+ var resultingY = cam.worldCameraYForPivot(proposedPivot);
623
+
624
+ if (resultingY >= minY - 1e-4) {
625
+ cam.pivotPoint.add(move);
626
+ } else {
627
+ move.y = 0;
628
+ proposedPivot = cam.pivotPoint.clone().add(move);
629
+ resultingY = cam.worldCameraYForPivot(proposedPivot);
630
+ if (resultingY >= minY - 1e-4) cam.pivotPoint.add(move);
631
+ }
632
+ };
633
+
634
+ OrbitCameraInputKeyboard.prototype.update = function (dt) {
635
+ if (!this.orbitCamera) return;
636
+
637
+ var cam = this.orbitCamera;
638
+
639
+ var shift = this._pressed(pc.KEY_SHIFT, 'Shift') || this._pressed(pc.KEY_SHIFT, 'ShiftLeft') || this._pressed(pc.KEY_SHIFT, 'ShiftRight');
640
+ var up = this._pressed(pc.KEY_UP, 'ArrowUp');
641
+ var down = this._pressed(pc.KEY_DOWN, 'ArrowDown');
642
+ var left = this._pressed(pc.KEY_LEFT, 'ArrowLeft');
643
+ var right = this._pressed(pc.KEY_RIGHT, 'ArrowRight');
644
+
645
+ if (!(up || down || left || right)) return;
646
+
647
+ if (shift) {
648
+ // Zoom (Shift + Up/Down)
649
+ if (up || down) {
650
+ var k = this.zoomFactorPerSecond;
651
+ if (this.entity.camera.projection === pc.PROJECTION_PERSPECTIVE) {
652
+ var dz = k * dt * (cam.distance * 0.8);
653
+ cam.distance = cam.distance + (down ? +dz : -dz);
654
+ } else {
655
+ var dh = k * dt * (this.entity.camera.orthoHeight * 0.8);
656
+ this.entity.camera.orthoHeight += (down ? +dh : -dh);
657
+ }
658
+ }
659
+ // Pan lateral (Shift + Left/Right)
660
+ if (left || right) {
661
+ var speed = this.panUnitsPerSecond * cam.distance; // world units / s
662
+ var delta = speed * dt * (left ? -1 : +1);
663
+ this._panSideways(delta);
664
+ }
665
+ return;
666
+ }
667
+
668
+ // Orbit (no Shift)
669
+ var orbitRate = this.orbitDegreesPerSecond * dt;
670
+
671
+ if (up || down) {
672
+ var dir = up ? -1 : +1; // Up: decrease pitch
673
+ var currPitch = cam.pitch;
674
+ var proposedPitch = currPitch + dir * orbitRate;
675
+ var test = this._wouldGoBelowMinYWithPitch(proposedPitch);
676
+ if (!(test.wouldGoBelow && (test.proposedY < test.preY))) {
677
+ cam.pitch = proposedPitch;
678
+ }
679
+ }
680
+
681
+ if (left) cam.yaw = cam.yaw + orbitRate;
682
+ if (right) cam.yaw = cam.yaw - orbitRate;
683
+ };