MikaFil commited on
Commit
742d619
·
verified ·
1 Parent(s): 419a820

Update orbit-camera.js

Browse files
Files changed (1) hide show
  1. orbit-camera.js +480 -50
orbit-camera.js CHANGED
@@ -1,5 +1,5 @@
1
  ///////////////////////////////////////////////////////////////////////////////
2
- // Orbit Camera Script //
3
  ////////////////////////////////////////////////////////////////////////////////
4
 
5
  var OrbitCamera = pc.createScript('orbitCamera');
@@ -7,36 +7,317 @@ var OrbitCamera = pc.createScript('orbitCamera');
7
  OrbitCamera.attributes.add('distanceMax', { type: 'number', default: 20, title: 'Distance Max', description: 'Setting this at 0 will give an infinite distance limit' });
8
  OrbitCamera.attributes.add('distanceMin', { type: 'number', default: 1, title: 'Distance Min' });
9
  OrbitCamera.attributes.add('pitchAngleMax', { type: 'number', default: 90, title: 'Pitch Angle Max (degrees)' });
10
- // Will be overridden by config; e.g. -90
11
  OrbitCamera.attributes.add('pitchAngleMin', { type: 'number', default: 0, title: 'Pitch Angle Min (degrees)' });
12
  OrbitCamera.attributes.add('yawAngleMax', { type: 'number', default: 360, title: 'Yaw Angle Max (degrees)' });
13
  OrbitCamera.attributes.add('yawAngleMin', { type: 'number', default: -360, title: 'Yaw Angle Min (degrees)' });
14
- // Minimum world Y
15
  OrbitCamera.attributes.add('minY', { type: 'number', default: 0, title: 'Minimum Y', description: 'Minimum Y value for the camera during orbiting or translation' });
16
 
17
  OrbitCamera.attributes.add('inertiaFactor', {
18
- type: 'number', default: 0.2, title: 'Inertia Factor',
 
 
19
  description: 'Higher value means that the camera will continue moving after the user has stopped dragging. 0 is fully responsive.'
20
  });
21
- OrbitCamera.attributes.add('focusEntity', { type: 'entity', title: 'Focus Entity' });
22
- OrbitCamera.attributes.add('frameOnStart', { type: 'boolean', default: true, title: 'Frame on Start' });
23
 
24
- // distance, orthoHeight, pitch, yaw, pivotPoint props as before...
25
- // ... (unchanged) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
- // _clampPitchAngle, _clampYawAngle, _calcPitch, _calcYaw, etc.
28
- // ... (unchanged) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  OrbitCamera.prototype._updatePosition = function () {
31
  this.entity.setLocalPosition(0, 0, 0);
32
  this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
33
- var position = this.entity.getPosition().clone().mulScalar(0); // scratch
34
- position.copy(this.entity.forward).mulScalar(-this._distance).add(this.pivotPoint);
 
 
 
35
  position.y = Math.max(position.y, this.minY);
36
  this.entity.setPosition(position);
37
  };
38
 
39
- // rest of OrbitCamera is unchanged …
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
 
42
  ////////////////////////////////////////////////////////////////////////////////
@@ -45,67 +326,209 @@ OrbitCamera.prototype._updatePosition = function () {
45
  var OrbitCameraInputMouse = pc.createScript('orbitCameraInputMouse');
46
 
47
  OrbitCameraInputMouse.attributes.add('orbitSensitivity', {
48
- type: 'number', default: 0.3, title: 'Orbit Sensitivity'
 
 
 
49
  });
50
  OrbitCameraInputMouse.attributes.add('distanceSensitivity', {
51
- type: 'number', default: 0.4, title: 'Distance Sensitivity'
 
 
 
52
  });
53
-
54
  OrbitCameraInputMouse.prototype.initialize = function () {
55
  this.orbitCamera = this.entity.script.orbitCamera;
56
- // event hookup unchanged …
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  this.lookButtonDown = false;
58
  this.panButtonDown = false;
59
  this.lastPoint = new pc.Vec2();
60
  };
61
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  OrbitCameraInputMouse.prototype.onMouseMove = function (event) {
63
  if (this.lookButtonDown) {
64
  var sens = this.orbitSensitivity;
65
  var currentPitch = this.orbitCamera.pitch;
66
  var currentYaw = this.orbitCamera.yaw;
67
- var deltaPitch = -event.dy * sens;
68
- var deltaYaw = -event.dx * sens;
69
 
70
- var proposedPitch = currentPitch + deltaPitch;
 
 
 
71
 
72
- // DEBUG:
73
  console.log(
74
- `[OrbitMouse] pitch=${currentPitch.toFixed(2)}, dy=${event.dy.toFixed(2)}, ` +
75
- `Δpitch=${deltaPitch.toFixed(2)}, proposed=${proposedPitch.toFixed(2)}`
 
 
 
76
  );
77
 
78
- // If already at min and we're trying to go below it, suppress:
79
- if (currentPitch <= this.orbitCamera.pitchAngleMin && proposedPitch < currentPitch) {
80
- console.log('[OrbitMouse] suppressing upward drag past minPitch');
81
  proposedPitch = currentPitch;
 
 
 
 
 
82
  }
83
 
84
- // Apply:
85
  this.orbitCamera.pitch = proposedPitch;
86
- this.orbitCamera.yaw = currentYaw + deltaYaw;
 
87
  } else if (this.panButtonDown) {
88
  this.pan(new pc.Vec2(event.x, event.y));
89
  }
90
  this.lastPoint.set(event.x, event.y);
91
  };
92
-
93
- // rest of Mouse Input (pan, wheel, etc.) unchanged…
94
-
 
 
 
 
 
 
 
 
 
95
 
96
 
97
  ////////////////////////////////////////////////////////////////////////////////
98
- // Orbit Camera Touch Input Script //
99
  ////////////////////////////////////////////////////////////////////////////////
100
  var OrbitCameraInputTouch = pc.createScript('orbitCameraInputTouch');
101
 
102
  OrbitCameraInputTouch.attributes.add('orbitSensitivity', {
103
- type: 'number', default: 0.6, title: 'Orbit Sensitivity'
 
 
 
104
  });
105
  OrbitCameraInputTouch.attributes.add('distanceSensitivity', {
106
- type: 'number', default: 0.5, title: 'Distance Sensitivity'
 
 
 
107
  });
108
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  OrbitCameraInputTouch.prototype.onTouchMove = function (event) {
110
  var touches = event.touches;
111
  if (touches.length === 1) {
@@ -113,30 +536,37 @@ OrbitCameraInputTouch.prototype.onTouchMove = function (event) {
113
  var sens = this.orbitSensitivity;
114
  var currentPitch = this.orbitCamera.pitch;
115
  var currentYaw = this.orbitCamera.yaw;
 
 
116
  var deltaY = touch.y - this.lastTouchPoint.y;
117
- var deltaX = touch.x - this.lastTouchPoint.x;
118
- var deltaPitch = -deltaY * sens;
119
- var deltaYaw = -deltaX * sens;
120
- var proposedPitch = currentPitch + deltaPitch;
121
 
122
- // DEBUG:
123
  console.log(
124
- `[OrbitTouch] pitch=${currentPitch.toFixed(2)}, Δy=${deltaY.toFixed(2)}, ` +
125
- `Δpitch=${deltaPitch.toFixed(2)}, proposed=${proposedPitch.toFixed(2)}`
 
 
 
126
  );
127
 
128
- if (currentPitch <= this.orbitCamera.pitchAngleMin && proposedPitch < currentPitch) {
129
- console.log('[OrbitTouch] suppressing upward drag past minPitch');
130
  proposedPitch = currentPitch;
 
 
 
 
131
  }
132
 
133
  this.orbitCamera.pitch = proposedPitch;
134
- this.orbitCamera.yaw = currentYaw + deltaYaw;
 
 
 
135
  this.lastTouchPoint.set(touch.x, touch.y);
 
136
  } else if (touches.length === 2) {
137
- // pinch zoom + pan unchanged
138
- // …
139
  }
140
  };
141
-
142
- // rest of Touch Input unchanged…
 
1
  ///////////////////////////////////////////////////////////////////////////////
2
+ // Orbit Camera Script qui marche presque //
3
  ////////////////////////////////////////////////////////////////////////////////
4
 
5
  var OrbitCamera = pc.createScript('orbitCamera');
 
7
  OrbitCamera.attributes.add('distanceMax', { type: 'number', default: 20, title: 'Distance Max', description: 'Setting this at 0 will give an infinite distance limit' });
8
  OrbitCamera.attributes.add('distanceMin', { type: 'number', default: 1, title: 'Distance Min' });
9
  OrbitCamera.attributes.add('pitchAngleMax', { type: 'number', default: 90, title: 'Pitch Angle Max (degrees)' });
10
+ // Note: This default will be overridden by your JSON config. In your JSON you set minAngle to -90.
11
  OrbitCamera.attributes.add('pitchAngleMin', { type: 'number', default: 0, title: 'Pitch Angle Min (degrees)' });
12
  OrbitCamera.attributes.add('yawAngleMax', { type: 'number', default: 360, title: 'Yaw Angle Max (degrees)' });
13
  OrbitCamera.attributes.add('yawAngleMin', { type: 'number', default: -360, title: 'Yaw Angle Min (degrees)' });
14
+ // NEW: Added minY attribute to define the minimum allowed Y value for the camera.
15
  OrbitCamera.attributes.add('minY', { type: 'number', default: 0, title: 'Minimum Y', description: 'Minimum Y value for the camera during orbiting or translation' });
16
 
17
  OrbitCamera.attributes.add('inertiaFactor', {
18
+ type: 'number',
19
+ default: 0.2,
20
+ title: 'Inertia Factor',
21
  description: 'Higher value means that the camera will continue moving after the user has stopped dragging. 0 is fully responsive.'
22
  });
 
 
23
 
24
+ OrbitCamera.attributes.add('focusEntity', {
25
+ type: 'entity',
26
+ title: 'Focus Entity',
27
+ description: 'Entity for the camera to focus on. If blank, then the camera will use the whole scene'
28
+ });
29
+
30
+ OrbitCamera.attributes.add('frameOnStart', {
31
+ type: 'boolean',
32
+ default: true,
33
+ title: 'Frame on Start',
34
+ description: 'Frames the entity or scene at the start of the application."'
35
+ });
36
+
37
+
38
+ // Property to get and set the distance between the pivot point and camera
39
+ // Clamped between this.distanceMin and this.distanceMax
40
+ Object.defineProperty(OrbitCamera.prototype, 'distance', {
41
+ get: function () {
42
+ return this._targetDistance;
43
+ },
44
+ set: function (value) {
45
+ this._targetDistance = this._clampDistance(value);
46
+ }
47
+ });
48
+
49
+ // Property to get and set the camera orthoHeight (clamped above 0)
50
+ Object.defineProperty(OrbitCamera.prototype, 'orthoHeight', {
51
+ get: function () {
52
+ return this.entity.camera.orthoHeight;
53
+ },
54
+ set: function (value) {
55
+ this.entity.camera.orthoHeight = Math.max(0, value);
56
+ }
57
+ });
58
+
59
+ // Property to get and set the pitch (in degrees) of the camera around the pivot.
60
+ // The pitch value is clamped between pitchAngleMin and pitchAngleMax.
61
+ Object.defineProperty(OrbitCamera.prototype, 'pitch', {
62
+ get: function () {
63
+ return this._targetPitch;
64
+ },
65
+ set: function (value) {
66
+ this._targetPitch = this._clampPitchAngle(value);
67
+ }
68
+ });
69
+
70
+ // Property to get and set the yaw (in degrees) of the camera around the pivot.
71
+ Object.defineProperty(OrbitCamera.prototype, 'yaw', {
72
+ get: function () {
73
+ return this._targetYaw;
74
+ },
75
+ set: function (value) {
76
+ this._targetYaw = this._clampYawAngle(value);
77
+ }
78
+ });
79
+
80
+ // Property to get and set the world position of the pivot point that the camera orbits around.
81
+ Object.defineProperty(OrbitCamera.prototype, 'pivotPoint', {
82
+ get: function () {
83
+ return this._pivotPoint;
84
+ },
85
+ set: function (value) {
86
+ this._pivotPoint.copy(value);
87
+ }
88
+ });
89
+
90
+
91
+ // Moves the camera to look at an entity and all its children so they are all in view.
92
+ OrbitCamera.prototype.focus = function (focusEntity) {
93
+ // Calculate a bounding box that encompasses all models to frame in the camera view.
94
+ this._buildAabb(focusEntity);
95
+ var halfExtents = this._modelsAabb.halfExtents;
96
+ var radius = Math.max(halfExtents.x, Math.max(halfExtents.y, halfExtents.z));
97
+ this.distance = (radius * 1.5) / Math.sin(0.5 * this.entity.camera.fov * pc.math.DEG_TO_RAD);
98
+ this._removeInertia();
99
+ this._pivotPoint.copy(this._modelsAabb.center);
100
+ };
101
+
102
+ OrbitCamera.distanceBetween = new pc.Vec3();
103
+
104
+ // Set the camera position to a world position and look at a world position.
105
+ OrbitCamera.prototype.resetAndLookAtPoint = function (resetPoint, lookAtPoint) {
106
+ this.pivotPoint.copy(lookAtPoint);
107
+ this.entity.setPosition(resetPoint);
108
+ this.entity.lookAt(lookAtPoint);
109
+ var distance = OrbitCamera.distanceBetween;
110
+ distance.sub2(lookAtPoint, resetPoint);
111
+ this.distance = distance.length();
112
+ this.pivotPoint.copy(lookAtPoint);
113
+ var cameraQuat = this.entity.getRotation();
114
+ this.yaw = this._calcYaw(cameraQuat);
115
+ this.pitch = this._calcPitch(cameraQuat, this.yaw);
116
+ this._removeInertia();
117
+ this._updatePosition();
118
+ };
119
 
120
+ OrbitCamera.prototype.resetAndLookAtEntity = function (resetPoint, entity) {
121
+ this._buildAabb(entity);
122
+ this.resetAndLookAtPoint(resetPoint, this._modelsAabb.center);
123
+ };
124
+
125
+ OrbitCamera.prototype.reset = function (yaw, pitch, distance) {
126
+ this.pitch = pitch;
127
+ this.yaw = yaw;
128
+ this.distance = distance;
129
+ this._removeInertia();
130
+ };
131
+
132
+ OrbitCamera.prototype.resetToPosition = function (position, lookAtPoint) {
133
+ this.entity.setPosition(position);
134
+ this.entity.lookAt(lookAtPoint);
135
+ this._pivotPoint.copy(lookAtPoint);
136
+ var distanceVec = new pc.Vec3();
137
+ distanceVec.sub2(position, lookAtPoint);
138
+ this._targetDistance = this._distance = distanceVec.length();
139
+ var cameraQuat = this.entity.getRotation();
140
+ this._targetYaw = this._yaw = this._calcYaw(cameraQuat);
141
+ this._targetPitch = this._pitch = this._calcPitch(cameraQuat, this._yaw);
142
+ this._removeInertia();
143
+ this._updatePosition();
144
+ };
145
+
146
+
147
+ ////////////////////////////////////////////////////////////////////////////////
148
+ // Private Methods //
149
+ ////////////////////////////////////////////////////////////////////////////////
150
+
151
+ OrbitCamera.prototype.initialize = function () {
152
+ var self = this;
153
+ var onWindowResize = function () {
154
+ self._checkAspectRatio();
155
+ };
156
+
157
+ window.addEventListener('resize', onWindowResize, false);
158
+ this._checkAspectRatio();
159
+ this._modelsAabb = new pc.BoundingBox();
160
+ this._buildAabb(this.focusEntity || this.app.root);
161
+ this.entity.lookAt(this._modelsAabb.center);
162
+ this._pivotPoint = new pc.Vec3();
163
+ this._pivotPoint.copy(this._modelsAabb.center);
164
+ var cameraQuat = this.entity.getRotation();
165
+ this._yaw = this._calcYaw(cameraQuat);
166
+ // Compute pitch and clamp it immediately.
167
+ this._pitch = this._clampPitchAngle(this._calcPitch(cameraQuat, this._yaw));
168
+ this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
169
+ this._distance = 0;
170
+ this._targetYaw = this._yaw;
171
+ this._targetPitch = this._pitch;
172
+
173
+ if (this.frameOnStart) {
174
+ this.focus(this.focusEntity || this.app.root);
175
+ } else {
176
+ var distanceBetween = new pc.Vec3();
177
+ distanceBetween.sub2(this.entity.getPosition(), this._pivotPoint);
178
+ this._distance = this._clampDistance(distanceBetween.length());
179
+ }
180
+
181
+ this._targetDistance = this._distance;
182
+
183
+ this.on('attr:distanceMin', function (value, prev) {
184
+ this._distance = this._clampDistance(this._distance);
185
+ });
186
+ this.on('attr:distanceMax', function (value, prev) {
187
+ this._distance = this._clampDistance(this._distance);
188
+ });
189
+ this.on('attr:pitchAngleMin', function (value, prev) {
190
+ this._pitch = this._clampPitchAngle(this._pitch);
191
+ });
192
+ this.on('attr:pitchAngleMax', function (value, prev) {
193
+ this._pitch = this._clampPitchAngle(this._pitch);
194
+ });
195
+ this.on('attr:focusEntity', function (value, prev) {
196
+ if (this.frameOnStart) {
197
+ this.focus(value || this.app.root);
198
+ } else {
199
+ this.resetAndLookAtEntity(this.entity.getPosition(), value || this.app.root);
200
+ }
201
+ });
202
+ this.on('attr:frameOnStart', function (value, prev) {
203
+ if (value) {
204
+ this.focus(this.focusEntity || this.app.root);
205
+ }
206
+ });
207
+ this.on('destroy', function () {
208
+ window.removeEventListener('resize', onWindowResize, false);
209
+ });
210
+ };
211
+
212
+ OrbitCamera.prototype.update = function (dt) {
213
+ var t = this.inertiaFactor === 0 ? 1 : Math.min(dt / this.inertiaFactor, 1);
214
+ this._distance = pc.math.lerp(this._distance, this._targetDistance, t);
215
+ this._yaw = pc.math.lerp(this._yaw, this._targetYaw, t);
216
+ this._pitch = pc.math.lerp(this._pitch, this._targetPitch, t);
217
+ this._updatePosition();
218
+ };
219
 
220
  OrbitCamera.prototype._updatePosition = function () {
221
  this.entity.setLocalPosition(0, 0, 0);
222
  this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
223
+ var position = this.entity.getPosition();
224
+ position.copy(this.entity.forward);
225
+ position.mulScalar(-this._distance);
226
+ position.add(this.pivotPoint);
227
+ // NEW: Clamp the camera's Y position so it never goes below the specified minY value.
228
  position.y = Math.max(position.y, this.minY);
229
  this.entity.setPosition(position);
230
  };
231
 
232
+ OrbitCamera.prototype._removeInertia = function () {
233
+ this._yaw = this._targetYaw;
234
+ this._pitch = this._targetPitch;
235
+ this._distance = this._targetDistance;
236
+ };
237
+
238
+ OrbitCamera.prototype._checkAspectRatio = function () {
239
+ var height = this.app.graphicsDevice.height;
240
+ var width = this.app.graphicsDevice.width;
241
+ this.entity.camera.horizontalFov = (height > width);
242
+ };
243
+
244
+ OrbitCamera.prototype._buildAabb = function (entity) {
245
+ var i, m, meshInstances = [];
246
+ var renders = entity.findComponents('render');
247
+ for (i = 0; i < renders.length; i++) {
248
+ var render = renders[i];
249
+ for (m = 0; m < render.meshInstances.length; m++) {
250
+ meshInstances.push(render.meshInstances[m]);
251
+ }
252
+ }
253
+ var models = entity.findComponents('model');
254
+ for (i = 0; i < models.length; i++) {
255
+ var model = models[i];
256
+ for (m = 0; m < model.meshInstances.length; m++) {
257
+ meshInstances.push(model.meshInstances[m]);
258
+ }
259
+ }
260
+ var gsplats = entity.findComponents('gsplat');
261
+ for (i = 0; i < gsplats.length; i++) {
262
+ var gsplat = gsplats[i];
263
+ var instance = gsplat.instance;
264
+ if (instance?.meshInstance) {
265
+ meshInstances.push(instance.meshInstance);
266
+ }
267
+ }
268
+ for (i = 0; i < meshInstances.length; i++) {
269
+ if (i === 0) {
270
+ this._modelsAabb.copy(meshInstances[i].aabb);
271
+ } else {
272
+ this._modelsAabb.add(meshInstances[i].aabb);
273
+ }
274
+ }
275
+ };
276
+
277
+ OrbitCamera.prototype._calcYaw = function (quat) {
278
+ var transformedForward = new pc.Vec3();
279
+ quat.transformVector(pc.Vec3.FORWARD, transformedForward);
280
+ return Math.atan2(-transformedForward.x, -transformedForward.z) * pc.math.RAD_TO_DEG;
281
+ };
282
+
283
+ OrbitCamera.prototype._clampDistance = function (distance) {
284
+ if (this.distanceMax > 0) {
285
+ return pc.math.clamp(distance, this.distanceMin, this.distanceMax);
286
+ }
287
+ return Math.max(distance, this.distanceMin);
288
+ };
289
+
290
+
291
+ // ----- FIXED PITCH CLAMPING -----
292
+ // Clamp the pitch between pitchAngleMin and pitchAngleMax so that with your JSON values
293
+ // (minAngle: -90, maxAngle: 0) the allowed pitch is between -90 (overhead) and 0 (horizontal).
294
+ OrbitCamera.prototype._clampPitchAngle = function (pitch) {
295
+ return pc.math.clamp(pitch, this.pitchAngleMin, this.pitchAngleMax);
296
+ };
297
+
298
+ OrbitCamera.prototype._clampYawAngle = function (yaw) {
299
+ return pc.math.clamp(yaw, -this.yawAngleMax, -this.yawAngleMin);
300
+ };
301
+
302
+ OrbitCamera.quatWithoutYaw = new pc.Quat();
303
+ OrbitCamera.yawOffset = new pc.Quat();
304
+
305
+ // ----- REVISED PITCH CALCULATION -----
306
+ // Modify _calcPitch so that horizontal (looking straight ahead) returns 0,
307
+ // and looking overhead returns -90.
308
+ // This change ensures that with JSON (minAngle: -90, maxAngle: 0) the allowed pitch is strictly between -90 and 0.
309
+ OrbitCamera.prototype._calcPitch = function (quat, yaw) {
310
+ var quatWithoutYaw = OrbitCamera.quatWithoutYaw;
311
+ var yawOffset = OrbitCamera.yawOffset;
312
+ yawOffset.setFromEulerAngles(0, -yaw, 0);
313
+ quatWithoutYaw.mul2(yawOffset, quat);
314
+ var transformedForward = new pc.Vec3();
315
+ quatWithoutYaw.transformVector(pc.Vec3.FORWARD, transformedForward);
316
+ // Here we swap the sign of the computed angle so that:
317
+ // - When the camera is horizontal, transformedForward.y is 0 and the result is 0.
318
+ // - When the camera is overhead, transformedForward.y is negative and the result is -90.
319
+ return Math.atan2(-transformedForward.y, -transformedForward.z) * pc.math.RAD_TO_DEG;
320
+ };
321
 
322
 
323
  ////////////////////////////////////////////////////////////////////////////////
 
326
  var OrbitCameraInputMouse = pc.createScript('orbitCameraInputMouse');
327
 
328
  OrbitCameraInputMouse.attributes.add('orbitSensitivity', {
329
+ type: 'number',
330
+ default: 0.3,
331
+ title: 'Orbit Sensitivity',
332
+ description: 'How fast the camera moves around the orbit. Higher is faster'
333
  });
334
  OrbitCameraInputMouse.attributes.add('distanceSensitivity', {
335
+ type: 'number',
336
+ default: 0.4,
337
+ title: 'Distance Sensitivity',
338
+ description: 'How fast the camera moves in and out. Higher is faster'
339
  });
 
340
  OrbitCameraInputMouse.prototype.initialize = function () {
341
  this.orbitCamera = this.entity.script.orbitCamera;
342
+ if (this.orbitCamera) {
343
+ var self = this;
344
+ var onMouseOut = function (e) { self.onMouseOut(e); };
345
+ this.app.mouse.on(pc.EVENT_MOUSEDOWN, this.onMouseDown, this);
346
+ this.app.mouse.on(pc.EVENT_MOUSEUP, this.onMouseUp, this);
347
+ this.app.mouse.on(pc.EVENT_MOUSEMOVE, this.onMouseMove, this);
348
+ this.app.mouse.on(pc.EVENT_MOUSEWHEEL, this.onMouseWheel, this);
349
+ window.addEventListener('mouseout', onMouseOut, false);
350
+ this.on('destroy', function () {
351
+ this.app.mouse.off(pc.EVENT_MOUSEDOWN, this.onMouseDown, this);
352
+ this.app.mouse.off(pc.EVENT_MOUSEUP, this.onMouseUp, this);
353
+ this.app.mouse.off(pc.EVENT_MOUSEMOVE, this.onMouseMove, this);
354
+ this.app.mouse.off(pc.EVENT_MOUSEWHEEL, this.onMouseWheel, this);
355
+ window.removeEventListener('mouseout', onMouseOut, false);
356
+ });
357
+ }
358
+ this.app.mouse.disableContextMenu();
359
  this.lookButtonDown = false;
360
  this.panButtonDown = false;
361
  this.lastPoint = new pc.Vec2();
362
  };
363
+ OrbitCameraInputMouse.fromWorldPoint = new pc.Vec3();
364
+ OrbitCameraInputMouse.toWorldPoint = new pc.Vec3();
365
+ OrbitCameraInputMouse.worldDiff = new pc.Vec3();
366
+ OrbitCameraInputMouse.prototype.pan = function (screenPoint) {
367
+ var fromWorldPoint = OrbitCameraInputMouse.fromWorldPoint;
368
+ var toWorldPoint = OrbitCameraInputMouse.toWorldPoint;
369
+ var worldDiff = OrbitCameraInputMouse.worldDiff;
370
+ var camera = this.entity.camera;
371
+ var distance = this.orbitCamera.distance;
372
+ camera.screenToWorld(screenPoint.x, screenPoint.y, distance, fromWorldPoint);
373
+ camera.screenToWorld(this.lastPoint.x, this.lastPoint.y, distance, toWorldPoint);
374
+ worldDiff.sub2(toWorldPoint, fromWorldPoint);
375
+ this.orbitCamera.pivotPoint.add(worldDiff);
376
+ var pitchRadians = this.orbitCamera.pitch * pc.math.DEG_TO_RAD;
377
+ var minPivotY = this.orbitCamera.distance * Math.sin(pitchRadians);
378
+ if (this.orbitCamera.pivotPoint.y < minPivotY) {
379
+ this.orbitCamera.pivotPoint.y = minPivotY;
380
+ }
381
+ };
382
+ OrbitCameraInputMouse.prototype.onMouseDown = function (event) {
383
+ switch (event.button) {
384
+ case pc.MOUSEBUTTON_LEFT:
385
+ this.panButtonDown = true;
386
+ break;
387
+ case pc.MOUSEBUTTON_MIDDLE:
388
+ case pc.MOUSEBUTTON_RIGHT:
389
+ this.lookButtonDown = true;
390
+ break;
391
+ }
392
+ };
393
+ OrbitCameraInputMouse.prototype.onMouseUp = function (event) {
394
+ switch (event.button) {
395
+ case pc.MOUSEBUTTON_LEFT:
396
+ this.panButtonDown = false;
397
+ break;
398
+ case pc.MOUSEBUTTON_MIDDLE:
399
+ case pc.MOUSEBUTTON_RIGHT:
400
+ this.lookButtonDown = false;
401
+ break;
402
+ }
403
+ };
404
  OrbitCameraInputMouse.prototype.onMouseMove = function (event) {
405
  if (this.lookButtonDown) {
406
  var sens = this.orbitSensitivity;
407
  var currentPitch = this.orbitCamera.pitch;
408
  var currentYaw = this.orbitCamera.yaw;
409
+ var pitchMin = this.orbitCamera.pitchAngleMin;
 
410
 
411
+ // compute proposed new pitch
412
+ var proposedPitch = currentPitch - event.dy * sens;
413
+ var atMinPitch = currentPitch <= pitchMin + 1e-4;
414
+ var movingUpward = event.dy < 0; // drag up → event.dy negative
415
 
 
416
  console.log(
417
+ "[Mouse] currentPitch=", currentPitch.toFixed(2),
418
+ "proposedPitch=", proposedPitch.toFixed(2),
419
+ "pitchMin=", pitchMin,
420
+ "atMinPitch=", atMinPitch,
421
+ "movingUpward=", movingUpward
422
  );
423
 
424
+ if (atMinPitch && movingUpward) {
425
+ // suppress only upward pitch
 
426
  proposedPitch = currentPitch;
427
+ console.log("[Mouse] Upward pitch suppressed");
428
+ } else if (proposedPitch < pitchMin && movingUpward) {
429
+ // clamp if slightly past
430
+ proposedPitch = pitchMin;
431
+ console.log("[Mouse] Pitch clamped to min");
432
  }
433
 
434
+ // apply pitch and always apply yaw
435
  this.orbitCamera.pitch = proposedPitch;
436
+ this.orbitCamera.yaw = currentYaw - event.dx * sens;
437
+
438
  } else if (this.panButtonDown) {
439
  this.pan(new pc.Vec2(event.x, event.y));
440
  }
441
  this.lastPoint.set(event.x, event.y);
442
  };
443
+ OrbitCameraInputMouse.prototype.onMouseWheel = function (event) {
444
+ if (this.entity.camera.projection === pc.PROJECTION_PERSPECTIVE) {
445
+ this.orbitCamera.distance -= event.wheelDelta * this.distanceSensitivity * (this.orbitCamera.distance * 0.1);
446
+ } else {
447
+ this.orbitCamera.orthoHeight -= event.wheelDelta * this.distanceSensitivity * (this.orbitCamera.orthoHeight * 0.1);
448
+ }
449
+ event.event.preventDefault();
450
+ };
451
+ OrbitCameraInputMouse.prototype.onMouseOut = function (event) {
452
+ this.lookButtonDown = false;
453
+ this.panButtonDown = false;
454
+ };
455
 
456
 
457
  ////////////////////////////////////////////////////////////////////////////////
458
+ // Orbit Camera Touch Input Script //
459
  ////////////////////////////////////////////////////////////////////////////////
460
  var OrbitCameraInputTouch = pc.createScript('orbitCameraInputTouch');
461
 
462
  OrbitCameraInputTouch.attributes.add('orbitSensitivity', {
463
+ type: 'number',
464
+ default: 0.6,
465
+ title: 'Orbit Sensitivity',
466
+ description: 'How fast the camera moves around the orbit. Higher is faster'
467
  });
468
  OrbitCameraInputTouch.attributes.add('distanceSensitivity', {
469
+ type: 'number',
470
+ default: 0.5,
471
+ title: 'Distance Sensitivity',
472
+ description: 'How fast the camera moves in and out. Higher is faster'
473
  });
474
+ OrbitCameraInputTouch.prototype.initialize = function () {
475
+ this.orbitCamera = this.entity.script.orbitCamera;
476
+ this.lastTouchPoint = new pc.Vec2();
477
+ this.lastPinchMidPoint = new pc.Vec2();
478
+ this.lastPinchDistance = 0;
479
+ if (this.orbitCamera && this.app.touch) {
480
+ this.app.touch.on(pc.EVENT_TOUCHSTART, this.onTouchStartEndCancel, this);
481
+ this.app.touch.on(pc.EVENT_TOUCHEND, this.onTouchStartEndCancel, this);
482
+ this.app.touch.on(pc.EVENT_TOUCHCANCEL, this.onTouchStartEndCancel, this);
483
+ this.app.touch.on(pc.EVENT_TOUCHMOVE, this.onTouchMove, this);
484
+ this.on('destroy', function () {
485
+ this.app.touch.off(pc.EVENT_TOUCHSTART, this.onTouchStartEndCancel, this);
486
+ this.app.touch.off(pc.EVENT_TOUCHEND, this.onTouchStartEndCancel, this);
487
+ this.app.touch.off(pc.EVENT_TOUCHCANCEL, this.onTouchStartEndCancel, this);
488
+ this.app.touch.off(pc.EVENT_TOUCHMOVE, this.onTouchMove, this);
489
+ });
490
+ }
491
+ };
492
+ OrbitCameraInputTouch.prototype.getPinchDistance = function (pointA, pointB) {
493
+ var dx = pointA.x - pointB.x;
494
+ var dy = pointA.y - pointB.y;
495
+ return Math.sqrt((dx * dx) + (dy * dy));
496
+ };
497
+ OrbitCameraInputTouch.prototype.calcMidPoint = function (pointA, pointB, result) {
498
+ result.set(pointB.x - pointA.x, pointB.y - pointA.y);
499
+ result.mulScalar(0.5);
500
+ result.x += pointA.x;
501
+ result.y += pointA.y;
502
+ };
503
+ OrbitCameraInputTouch.prototype.onTouchStartEndCancel = function (event) {
504
+ var touches = event.touches;
505
+ if (touches.length === 1) {
506
+ this.lastTouchPoint.set(touches[0].x, touches[0].y);
507
+ } else if (touches.length === 2) {
508
+ this.lastPinchDistance = this.getPinchDistance(touches[0], touches[1]);
509
+ this.calcMidPoint(touches[0], touches[1], this.lastPinchMidPoint);
510
+ }
511
+ };
512
+ OrbitCameraInputTouch.fromWorldPoint = new pc.Vec3();
513
+ OrbitCameraInputTouch.toWorldPoint = new pc.Vec3();
514
+ OrbitCameraInputTouch.worldDiff = new pc.Vec3();
515
+ OrbitCameraInputTouch.prototype.pan = function (midPoint) {
516
+ var fromWorldPoint = OrbitCameraInputTouch.fromWorldPoint;
517
+ var toWorldPoint = OrbitCameraInputTouch.toWorldPoint;
518
+ var worldDiff = OrbitCameraInputTouch.worldDiff;
519
+ var camera = this.entity.camera;
520
+ var distance = this.orbitCamera.distance;
521
+ camera.screenToWorld(midPoint.x, midPoint.y, distance, fromWorldPoint);
522
+ camera.screenToWorld(this.lastPinchMidPoint.x, this.lastPinchMidPoint.y, distance, toWorldPoint);
523
+ worldDiff.sub2(toWorldPoint, fromWorldPoint);
524
+ this.orbitCamera.pivotPoint.add(worldDiff);
525
+ var pitchRadians = this.orbitCamera.pitch * pc.math.DEG_TO_RAD;
526
+ var minPivotY = this.orbitCamera.distance * Math.sin(pitchRadians);
527
+ if (this.orbitCamera.pivotPoint.y < minPivotY) {
528
+ this.orbitCamera.pivotPoint.y = minPivotY;
529
+ }
530
+ };
531
+ OrbitCameraInputTouch.pinchMidPoint = new pc.Vec2();
532
  OrbitCameraInputTouch.prototype.onTouchMove = function (event) {
533
  var touches = event.touches;
534
  if (touches.length === 1) {
 
536
  var sens = this.orbitSensitivity;
537
  var currentPitch = this.orbitCamera.pitch;
538
  var currentYaw = this.orbitCamera.yaw;
539
+ var pitchMin = this.orbitCamera.pitchAngleMin;
540
+
541
  var deltaY = touch.y - this.lastTouchPoint.y;
542
+ var proposedPitch = currentPitch - deltaY * sens;
543
+ var atMinPitch = currentPitch <= pitchMin + 1e-4;
544
+ var movingUpward = deltaY > 0; // moving finger up increases touch.y, so deltaY positive
 
545
 
 
546
  console.log(
547
+ "[Touch] currentPitch=", currentPitch.toFixed(2),
548
+ "proposedPitch=", proposedPitch.toFixed(2),
549
+ "pitchMin=", pitchMin,
550
+ "atMinPitch=", atMinPitch,
551
+ "movingUpward=", movingUpward
552
  );
553
 
554
+ if (atMinPitch && movingUpward) {
 
555
  proposedPitch = currentPitch;
556
+ console.log("[Touch] Upward pitch suppressed");
557
+ } else if (proposedPitch < pitchMin && movingUpward) {
558
+ proposedPitch = pitchMin;
559
+ console.log("[Touch] Pitch clamped to min");
560
  }
561
 
562
  this.orbitCamera.pitch = proposedPitch;
563
+ // always allow horizontal
564
+ var deltaX = touch.x - this.lastTouchPoint.x;
565
+ this.orbitCamera.yaw = currentYaw - deltaX * sens;
566
+
567
  this.lastTouchPoint.set(touch.x, touch.y);
568
+
569
  } else if (touches.length === 2) {
570
+ // ... existing pinch/zoom & pan logic unchanged ...
 
571
  }
572
  };