13
13
* limitations under the License.
14
14
*/
15
15
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' ;
17
17
import { XREstimatedLight } from 'three/examples/jsm/webxr/XREstimatedLight.js' ;
18
18
19
19
import { CameraChangeDetails , ControlsInterface } from '../features/controls.js' ;
@@ -51,6 +51,10 @@ const DECAY = 150;
51
51
const MAX_LINE_LENGTH = 5 ;
52
52
// Maximum dimension of rotation indicator box on controller (meters).
53
53
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 ;
54
58
55
59
export type ARStatus =
56
60
'not-presenting' | 'session-started' | 'object-placed' | 'failed' ;
@@ -78,7 +82,11 @@ export interface ARTrackingEvent extends ThreeEvent {
78
82
}
79
83
80
84
interface UserData {
81
- turning : boolean , box : Mesh , line : Line
85
+ turning : boolean
86
+ box : Mesh
87
+ line : Line
88
+ isSelected : boolean
89
+ initialX : number
82
90
}
83
91
84
92
interface Controller extends XRTargetRaySpace {
@@ -131,7 +139,7 @@ export class ARRenderer extends EventDispatcher<
131
139
private isRotating = false ;
132
140
private isTwoFingering = false ;
133
141
private lastDragPosition = new Vector3 ( ) ;
134
- private relativeOrientation = new Quaternion ( ) ;
142
+ private deltaRotation = new Quaternion ( ) ;
135
143
private scaleLine = new Line ( lineGeometry ) ;
136
144
private firstRatio = 0 ;
137
145
private lastAngle = 0 ;
@@ -370,33 +378,50 @@ export class ARRenderer extends EventDispatcher<
370
378
private onControllerSelectStart = ( event : XRControllerEvent ) => {
371
379
const scene = this . presentedScene ! ;
372
380
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
+ }
373
409
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 ;
381
423
}
382
424
}
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 ;
400
425
}
401
426
} ;
402
427
@@ -406,6 +431,13 @@ export class ARRenderer extends EventDispatcher<
406
431
controller . userData . line . visible = true ;
407
432
this . isTwoFingering = false ;
408
433
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
+
409
441
if ( this . selectedController != null &&
410
442
this . selectedController != controller ) {
411
443
return ;
@@ -889,41 +921,32 @@ export class ARRenderer extends EventDispatcher<
889
921
}
890
922
}
891
923
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
+ }
892
932
private moveScene ( delta : number ) {
893
933
const scene = this . presentedScene ! ;
894
934
const { pivot} = scene ;
895
935
const box = this . placementBox ! ;
896
936
box . updateOpacity ( delta ) ;
897
937
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 ;
910
942
}
911
943
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 ) ;
924
947
}
925
948
926
- if ( this . controller1 && this . controller2 && this . isTwoFingering ) {
949
+ if ( this . controller1 && this . controller2 && bothSelected ) {
927
950
const dist = this . controllerSeparation ( ) ;
928
951
this . setScale ( dist ) ;
929
952
this . scaleLine . scale . z = - dist ;
0 commit comments