Skip to content

Commit cbe3f0e

Browse files
✨ New Features & Improvements: (#5020)
* ✨ New Features & Improvements: 1. Single-controller rotation When a single controller intersects with the model (excluding the footprint) and moves horizontally, the model now rotates in the same direction as the controller movement. 2. Single-controller translation When a controller intersects with the footprint, the user can now translate/move the model across the surface. 3. Dual-controller scaling When both controllers intersect the model, the user can now scale the model using a pinch-like gesture (distance-based scaling). * Removed debugging notes * Removed extra lines * Fixed Indentation * Fix indentation
1 parent 4efaf8e commit cbe3f0e

File tree

1 file changed

+75
-52
lines changed

1 file changed

+75
-52
lines changed

packages/model-viewer/src/three-components/ARRenderer.ts

Lines changed: 75 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* limitations under the License.
1414
*/
1515

16-
import {BoxGeometry, BufferGeometry, Event as ThreeEvent, EventDispatcher, Line, Matrix4, Mesh, PerspectiveCamera, Quaternion, Vector3, WebGLRenderer, XRControllerEventType, XRTargetRaySpace} from 'three';
16+
import {Box3, BoxGeometry, BufferGeometry, Event as ThreeEvent, EventDispatcher, Line, Matrix4, Mesh, PerspectiveCamera, Quaternion, Vector3, WebGLRenderer, XRControllerEventType, XRTargetRaySpace, Object3D} from 'three';
1717
import {XREstimatedLight} from 'three/examples/jsm/webxr/XREstimatedLight.js';
1818

1919
import {CameraChangeDetails, ControlsInterface} from '../features/controls.js';
@@ -51,6 +51,10 @@ const DECAY = 150;
5151
const MAX_LINE_LENGTH = 5;
5252
// Maximum dimension of rotation indicator box on controller (meters).
5353
const BOX_SIZE = 0.1;
54+
// Axis Y in webxr.
55+
const AXIS_Y = new Vector3(0, 1, 0);
56+
// Webxr rotation sensitivity
57+
const ROTATION_SENSIVITY = 0.3;
5458

5559
export type ARStatus =
5660
'not-presenting'|'session-started'|'object-placed'|'failed';
@@ -78,7 +82,11 @@ export interface ARTrackingEvent extends ThreeEvent {
7882
}
7983

8084
interface UserData {
81-
turning: boolean, box: Mesh, line: Line
85+
turning: boolean
86+
box: Mesh
87+
line: Line
88+
isSelected: boolean
89+
initialX: number
8290
}
8391

8492
interface Controller extends XRTargetRaySpace {
@@ -131,7 +139,7 @@ export class ARRenderer extends EventDispatcher<
131139
private isRotating = false;
132140
private isTwoFingering = false;
133141
private lastDragPosition = new Vector3();
134-
private relativeOrientation = new Quaternion();
142+
private deltaRotation = new Quaternion();
135143
private scaleLine = new Line(lineGeometry);
136144
private firstRatio = 0;
137145
private lastAngle = 0;
@@ -370,33 +378,50 @@ export class ARRenderer extends EventDispatcher<
370378
private onControllerSelectStart = (event: XRControllerEvent) => {
371379
const scene = this.presentedScene!;
372380
const controller = event.target;
381+
382+
const intersection = this.placementBox!.controllerIntersection(scene,
383+
controller);
384+
if (intersection!=null){
385+
const bbox = new Box3().setFromObject(scene.pivot);
386+
const footprintY = bbox.min.y + 0.2; // Small threshold above base
387+
388+
// Check if the ray intersection is near the footprint
389+
const isFootprint = intersection.point.y <= footprintY;
390+
if (isFootprint) {
391+
if (this.selectedController != null) {
392+
this.selectedController.userData.line.visible = false;
393+
if (scene.canScale) {
394+
this.isTwoFingering = true;
395+
this.firstRatio = this.controllerSeparation() / scene.pivot.scale.x;
396+
this.scaleLine.visible = true;
397+
}
398+
} else {
399+
controller.attach(scene.pivot);
400+
}
401+
this.selectedController = controller;
402+
scene.setShadowIntensity(0.01);
403+
} else {
404+
if (controller == this.controller1) {
405+
this.controller1.userData.isSelected = true;
406+
} else if (controller == this.controller2) {
407+
this.controller2.userData.isSelected = true;
408+
}
373409

374-
if (this.placementBox!.controllerIntersection(scene, controller) != null) {
375-
if (this.selectedController != null) {
376-
this.selectedController.userData.line.visible = false;
377-
if (scene.canScale) {
378-
this.isTwoFingering = true;
379-
this.firstRatio = this.controllerSeparation() / scene.pivot.scale.x;
380-
this.scaleLine.visible = true;
410+
if (this.controller1?.userData.isSelected && this.controller2?.userData.isSelected) {
411+
if (scene.canScale) {
412+
this.isTwoFingering = true;
413+
this.firstRatio = this.controllerSeparation() / scene.pivot.scale.x;
414+
this.scaleLine.visible = true;
415+
}
416+
} else {
417+
const otherController = controller === this.controller1 ? this.controller2! :
418+
this.controller1!;
419+
controller.userData.initialX = controller.position.x;
420+
otherController.userData.turning = false;
421+
controller.userData.turning = true;
422+
controller.userData.line.visible = false;
381423
}
382424
}
383-
384-
controller.attach(scene.pivot);
385-
this.selectedController = controller;
386-
387-
scene.setShadowIntensity(0.01);
388-
} else {
389-
const otherController = controller === this.controller1 ?
390-
this.controller2! :
391-
this.controller1!;
392-
393-
this.relativeOrientation.copy(controller.quaternion)
394-
.invert()
395-
.multiply(scene.pivot.getWorldQuaternion(quaternion));
396-
397-
otherController.userData.turning = false;
398-
controller.userData.turning = true;
399-
controller.userData.line.visible = false;
400425
}
401426
};
402427

@@ -406,6 +431,13 @@ export class ARRenderer extends EventDispatcher<
406431
controller.userData.line.visible = true;
407432
this.isTwoFingering = false;
408433
this.scaleLine.visible = false;
434+
435+
if (controller == this.controller1) {
436+
this.controller1.userData.isSelected = false;
437+
} else if (controller == this.controller2) {
438+
this.controller2.userData.isSelected = false;
439+
}
440+
409441
if (this.selectedController != null &&
410442
this.selectedController != controller) {
411443
return;
@@ -889,41 +921,32 @@ export class ARRenderer extends EventDispatcher<
889921
}
890922
}
891923

924+
private applyControllerRotation(controller: Controller, pivot: Object3D) {
925+
if (!controller.userData.turning) {
926+
return;
927+
}
928+
const angle = (controller.position.x - controller.userData.initialX) * ROTATION_SENSIVITY;
929+
this.deltaRotation.setFromAxisAngle(AXIS_Y, angle);
930+
pivot.quaternion.multiplyQuaternions(this.deltaRotation, pivot.quaternion);
931+
}
892932
private moveScene(delta: number) {
893933
const scene = this.presentedScene!;
894934
const {pivot} = scene;
895935
const box = this.placementBox!;
896936
box.updateOpacity(delta);
897937

898-
if (this.controller1) {
899-
if (this.controller1.userData.turning) {
900-
pivot.quaternion.copy(this.controller1.quaternion)
901-
.multiply(this.relativeOrientation);
902-
if (this.selectedController &&
903-
this.selectedController === this.controller2) {
904-
pivot.quaternion.premultiply(
905-
quaternion.copy(this.controller2.quaternion).invert());
906-
}
907-
}
908-
this.controller1.userData.box.position.copy(this.controller1.position);
909-
pivot.getWorldQuaternion(this.controller1.userData.box.quaternion);
938+
939+
const bothSelected = this.controller1?.userData.isSelected && this.controller2?.userData.isSelected;
940+
if (bothSelected) {
941+
this.isTwoFingering = true;
910942
}
911943

912-
if (this.controller2) {
913-
if (this.controller2.userData.turning) {
914-
pivot.quaternion.copy(this.controller2.quaternion)
915-
.multiply(this.relativeOrientation);
916-
if (this.selectedController &&
917-
this.selectedController === this.controller1) {
918-
pivot.quaternion.premultiply(
919-
quaternion.copy(this.controller1.quaternion).invert());
920-
}
921-
}
922-
this.controller2.userData.box.position.copy(this.controller2.position);
923-
pivot.getWorldQuaternion(this.controller2.userData.box.quaternion);
944+
if (!bothSelected) {
945+
if (this.controller1) this.applyControllerRotation(this.controller1, pivot);
946+
if (this.controller2) this.applyControllerRotation(this.controller2, pivot);
924947
}
925948

926-
if (this.controller1 && this.controller2 && this.isTwoFingering) {
949+
if (this.controller1 && this.controller2 && bothSelected) {
927950
const dist = this.controllerSeparation();
928951
this.setScale(dist);
929952
this.scaleLine.scale.z = -dist;

0 commit comments

Comments
 (0)