Skip to content

Commit 926bd8a

Browse files
authored
feat: refactor orbit controls to pointer events (#71)
Brings orbit controls up to date after mrdoob/three.js#21972
1 parent 3265982 commit 926bd8a

File tree

1 file changed

+117
-82
lines changed

1 file changed

+117
-82
lines changed

src/controls/OrbitControls.ts

Lines changed: 117 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ class OrbitControls extends EventDispatcher {
9696

9797
this.object = object
9898
this.domElement = domElement
99+
// disables touch scroll
100+
// touch-action needs to be defined for pointer events to work on mobile
101+
// https://stackoverflow.com/a/48254578
102+
this.domElement && (this.domElement.style.touchAction = 'none')
103+
99104
// for reset
100105
this.target0 = this.target.clone()
101106
this.position0 = this.object.position.clone()
@@ -301,19 +306,15 @@ class OrbitControls extends EventDispatcher {
301306
scope.domElement = domElement
302307
scope.domElement.addEventListener('contextmenu', onContextMenu)
303308
scope.domElement.addEventListener('pointerdown', onPointerDown)
309+
scope.domElement.addEventListener('pointercancel', onPointerCancel)
304310
scope.domElement.addEventListener('wheel', onMouseWheel)
305-
scope.domElement.addEventListener('touchstart', onTouchStart)
306-
scope.domElement.addEventListener('touchend', onTouchEnd)
307-
scope.domElement.addEventListener('touchmove', onTouchMove)
308311
}
309312

310313
this.dispose = (): void => {
311314
scope.domElement?.removeEventListener('contextmenu', onContextMenu)
312315
scope.domElement?.removeEventListener('pointerdown', onPointerDown)
316+
scope.domElement?.removeEventListener('pointercancel', onPointerCancel)
313317
scope.domElement?.removeEventListener('wheel', onMouseWheel)
314-
scope.domElement?.removeEventListener('touchstart', onTouchStart)
315-
scope.domElement?.removeEventListener('touchend', onTouchEnd)
316-
scope.domElement?.removeEventListener('touchmove', onTouchMove)
317318
scope.domElement?.ownerDocument.removeEventListener('pointermove', onPointerMove)
318319
scope.domElement?.ownerDocument.removeEventListener('pointerup', onPointerUp)
319320
if (scope._domElementKeyEvents !== null) {
@@ -367,6 +368,9 @@ class OrbitControls extends EventDispatcher {
367368
const dollyEnd = new Vector2()
368369
const dollyDelta = new Vector2()
369370

371+
const pointers: PointerEvent[] = []
372+
const pointerPositions: { [key: string]: Vector2 } = {}
373+
370374
function getAutoRotationAngle(): number {
371375
return ((2 * Math.PI) / 60 / 60) * scope.autoRotateSpeed
372376
}
@@ -572,53 +576,53 @@ class OrbitControls extends EventDispatcher {
572576
}
573577
}
574578

575-
function handleTouchStartRotate(event: TouchEvent) {
576-
if (event.touches.length == 1) {
577-
rotateStart.set(event.touches[0].pageX, event.touches[0].pageY)
579+
function handleTouchStartRotate() {
580+
if (pointers.length == 1) {
581+
rotateStart.set(pointers[0].pageX, pointers[0].pageY)
578582
} else {
579-
const x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX)
580-
const y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY)
583+
const x = 0.5 * (pointers[0].pageX + pointers[1].pageX)
584+
const y = 0.5 * (pointers[0].pageY + pointers[1].pageY)
581585

582586
rotateStart.set(x, y)
583587
}
584588
}
585589

586-
function handleTouchStartPan(event: TouchEvent) {
587-
if (event.touches.length == 1) {
588-
panStart.set(event.touches[0].pageX, event.touches[0].pageY)
590+
function handleTouchStartPan() {
591+
if (pointers.length == 1) {
592+
panStart.set(pointers[0].pageX, pointers[0].pageY)
589593
} else {
590-
const x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX)
591-
const y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY)
594+
const x = 0.5 * (pointers[0].pageX + pointers[1].pageX)
595+
const y = 0.5 * (pointers[0].pageY + pointers[1].pageY)
592596

593597
panStart.set(x, y)
594598
}
595599
}
596600

597-
function handleTouchStartDolly(event: TouchEvent) {
598-
const dx = event.touches[0].pageX - event.touches[1].pageX
599-
const dy = event.touches[0].pageY - event.touches[1].pageY
601+
function handleTouchStartDolly() {
602+
const dx = pointers[0].pageX - pointers[1].pageX
603+
const dy = pointers[0].pageY - pointers[1].pageY
600604
const distance = Math.sqrt(dx * dx + dy * dy)
601605

602606
dollyStart.set(0, distance)
603607
}
604608

605-
function handleTouchStartDollyPan(event: TouchEvent) {
606-
if (scope.enableZoom) handleTouchStartDolly(event)
607-
if (scope.enablePan) handleTouchStartPan(event)
609+
function handleTouchStartDollyPan() {
610+
if (scope.enableZoom) handleTouchStartDolly()
611+
if (scope.enablePan) handleTouchStartPan()
608612
}
609613

610-
function handleTouchStartDollyRotate(event: TouchEvent) {
611-
if (scope.enableZoom) handleTouchStartDolly(event)
612-
if (scope.enableRotate) handleTouchStartRotate(event)
614+
function handleTouchStartDollyRotate() {
615+
if (scope.enableZoom) handleTouchStartDolly()
616+
if (scope.enableRotate) handleTouchStartRotate()
613617
}
614618

615-
function handleTouchMoveRotate(event: TouchEvent) {
616-
if (event.touches.length == 1) {
617-
rotateEnd.set(event.touches[0].pageX, event.touches[0].pageY)
619+
function handleTouchMoveRotate(event: PointerEvent) {
620+
if (pointers.length == 1) {
621+
rotateEnd.set(event.pageX, event.pageY)
618622
} else {
619-
const x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX)
620-
const y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY)
621-
623+
const position = getSecondPointerPosition(event)
624+
const x = 0.5 * (event.pageX + position.x)
625+
const y = 0.5 * (event.pageY + position.y)
622626
rotateEnd.set(x, y)
623627
}
624628

@@ -633,13 +637,13 @@ class OrbitControls extends EventDispatcher {
633637
rotateStart.copy(rotateEnd)
634638
}
635639

636-
function handleTouchMovePan(event: TouchEvent) {
637-
if (event.touches.length == 1) {
638-
panEnd.set(event.touches[0].pageX, event.touches[0].pageY)
640+
function handleTouchMovePan(event: PointerEvent) {
641+
if (pointers.length == 1) {
642+
panEnd.set(event.pageX, event.pageY)
639643
} else {
640-
const x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX)
641-
const y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY)
642-
644+
const position = getSecondPointerPosition(event)
645+
const x = 0.5 * (event.pageX + position.x)
646+
const y = 0.5 * (event.pageY + position.y)
643647
panEnd.set(x, y)
644648
}
645649

@@ -648,9 +652,10 @@ class OrbitControls extends EventDispatcher {
648652
panStart.copy(panEnd)
649653
}
650654

651-
function handleTouchMoveDolly(event: TouchEvent) {
652-
const dx = event.touches[0].pageX - event.touches[1].pageX
653-
const dy = event.touches[0].pageY - event.touches[1].pageY
655+
function handleTouchMoveDolly(event: PointerEvent) {
656+
const position = getSecondPointerPosition(event)
657+
const dx = event.pageX - position.x
658+
const dy = event.pageY - position.y
654659
const distance = Math.sqrt(dx * dx + dy * dy)
655660

656661
dollyEnd.set(0, distance)
@@ -659,12 +664,12 @@ class OrbitControls extends EventDispatcher {
659664
dollyStart.copy(dollyEnd)
660665
}
661666

662-
function handleTouchMoveDollyPan(event: TouchEvent) {
667+
function handleTouchMoveDollyPan(event: PointerEvent) {
663668
if (scope.enableZoom) handleTouchMoveDolly(event)
664669
if (scope.enablePan) handleTouchMovePan(event)
665670
}
666671

667-
function handleTouchMoveDollyRotate(event: TouchEvent) {
672+
function handleTouchMoveDollyRotate(event: PointerEvent) {
668673
if (scope.enableZoom) handleTouchMoveDolly(event)
669674
if (scope.enableRotate) handleTouchMoveRotate(event)
670675
}
@@ -680,40 +685,51 @@ class OrbitControls extends EventDispatcher {
680685
function onPointerDown(event: PointerEvent) {
681686
if (scope.enabled === false) return
682687

683-
switch (event.pointerType) {
684-
case 'mouse':
685-
case 'pen':
686-
onMouseDown(event)
687-
break
688+
if (pointers.length === 0) {
689+
scope.domElement?.ownerDocument.addEventListener('pointermove', onPointerMove)
690+
scope.domElement?.ownerDocument.addEventListener('pointerup', onPointerUp)
691+
}
692+
693+
addPointer(event)
688694

689-
// TODO touch
695+
if (event.pointerType === 'touch') {
696+
onTouchStart(event)
697+
} else {
698+
onMouseDown(event)
690699
}
691700
}
692701

693702
function onPointerMove(event: PointerEvent) {
694703
if (scope.enabled === false) return
695704

696-
switch (event.pointerType) {
697-
case 'mouse':
698-
case 'pen':
699-
onMouseMove(event)
700-
break
701-
702-
// TODO touch
705+
if (event.pointerType === 'touch') {
706+
onTouchMove(event)
707+
} else {
708+
onMouseMove(event)
703709
}
704710
}
705711

706712
function onPointerUp(event: PointerEvent) {
707-
switch (event.pointerType) {
708-
case 'mouse':
709-
case 'pen':
710-
onMouseUp()
711-
break
713+
if (scope.enabled === false) return
714+
715+
if (event.pointerType === 'touch') {
716+
onTouchEnd()
717+
} else {
718+
onMouseUp()
719+
}
712720

713-
// TODO touch
721+
removePointer(event)
722+
723+
if (pointers.length === 0) {
724+
scope.domElement?.ownerDocument.removeEventListener('pointermove', onPointerMove)
725+
scope.domElement?.ownerDocument.removeEventListener('pointerup', onPointerUp)
714726
}
715727
}
716728

729+
function onPointerCancel(event: PointerEvent) {
730+
removePointer(event)
731+
}
732+
717733
function onMouseDown(event: MouseEvent) {
718734
let mouseAction
719735

@@ -770,8 +786,6 @@ class OrbitControls extends EventDispatcher {
770786
}
771787

772788
if (state !== STATE.NONE) {
773-
scope.domElement?.ownerDocument.addEventListener('pointermove', onPointerMove)
774-
scope.domElement?.ownerDocument.addEventListener('pointerup', onPointerUp)
775789
scope.dispatchEvent(startEvent)
776790
}
777791
}
@@ -798,10 +812,6 @@ class OrbitControls extends EventDispatcher {
798812
}
799813

800814
function onMouseUp() {
801-
scope.domElement?.ownerDocument.removeEventListener('pointermove', onPointerMove)
802-
scope.domElement?.ownerDocument.removeEventListener('pointerup', onPointerUp)
803-
804-
if (scope.enabled === false) return
805815
handleMouseUp()
806816
scope.dispatchEvent(endEvent)
807817
state = STATE.NONE
@@ -826,23 +836,21 @@ class OrbitControls extends EventDispatcher {
826836
handleKeyDown(event)
827837
}
828838

829-
function onTouchStart(event: TouchEvent) {
830-
if (scope.enabled === false) return
831-
832-
event.preventDefault() // prevent scrolling
839+
function onTouchStart(event: PointerEvent) {
840+
trackPointer(event)
833841

834-
switch (event.touches.length) {
842+
switch (pointers.length) {
835843
case 1:
836844
switch (scope.touches.ONE) {
837845
case TOUCH.ROTATE:
838846
if (scope.enableRotate === false) return
839-
handleTouchStartRotate(event)
847+
handleTouchStartRotate()
840848
state = STATE.TOUCH_ROTATE
841849
break
842850

843851
case TOUCH.PAN:
844852
if (scope.enablePan === false) return
845-
handleTouchStartPan(event)
853+
handleTouchStartPan()
846854
state = STATE.TOUCH_PAN
847855
break
848856

@@ -856,13 +864,13 @@ class OrbitControls extends EventDispatcher {
856864
switch (scope.touches.TWO) {
857865
case TOUCH.DOLLY_PAN:
858866
if (scope.enableZoom === false && scope.enablePan === false) return
859-
handleTouchStartDollyPan(event)
867+
handleTouchStartDollyPan()
860868
state = STATE.TOUCH_DOLLY_PAN
861869
break
862870

863871
case TOUCH.DOLLY_ROTATE:
864872
if (scope.enableZoom === false && scope.enableRotate === false) return
865-
handleTouchStartDollyRotate(event)
873+
handleTouchStartDollyRotate()
866874
state = STATE.TOUCH_DOLLY_ROTATE
867875
break
868876

@@ -881,10 +889,8 @@ class OrbitControls extends EventDispatcher {
881889
}
882890
}
883891

884-
function onTouchMove(event: TouchEvent) {
885-
if (scope.enabled === false) return
886-
887-
event.preventDefault() // prevent scrolling
892+
function onTouchMove(event: PointerEvent) {
893+
trackPointer(event)
888894

889895
switch (state) {
890896
case STATE.TOUCH_ROTATE:
@@ -917,8 +923,6 @@ class OrbitControls extends EventDispatcher {
917923
}
918924

919925
function onTouchEnd() {
920-
if (scope.enabled === false) return
921-
922926
handleTouchEnd()
923927
scope.dispatchEvent(endEvent)
924928
state = STATE.NONE
@@ -929,6 +933,37 @@ class OrbitControls extends EventDispatcher {
929933
event.preventDefault()
930934
}
931935

936+
function addPointer(event: PointerEvent) {
937+
pointers.push(event)
938+
}
939+
940+
function removePointer(event: PointerEvent) {
941+
delete pointerPositions[event.pointerId]
942+
943+
for (let i = 0; i < pointers.length; i++) {
944+
if (pointers[i].pointerId == event.pointerId) {
945+
pointers.splice(i, 1)
946+
return
947+
}
948+
}
949+
}
950+
951+
function trackPointer(event: PointerEvent) {
952+
let position = pointerPositions[event.pointerId]
953+
954+
if (position === undefined) {
955+
position = new Vector2()
956+
pointerPositions[event.pointerId] = position
957+
}
958+
959+
position.set(event.pageX, event.pageY)
960+
}
961+
962+
function getSecondPointerPosition(event: PointerEvent) {
963+
const pointer = event.pointerId === pointers[0].pointerId ? pointers[1] : pointers[0]
964+
return pointerPositions[pointer.pointerId]
965+
}
966+
932967
// connect events
933968
if (domElement !== undefined) this.connect(domElement)
934969
// force an update at start

0 commit comments

Comments
 (0)