Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
e2c4e82bbThis file is pinned to the selected release. Download the original to open it in your design tool.
import {EventDispatcher, MathUtils, MOUSE, Quaternion, Vector2, Vector3} from "three"
const _changeEvent = {type: "change"}
const _startEvent = {type: "start"}
const _endEvent = {type: "end"}
class TrackballControls extends EventDispatcher {
constructor(object, domElement) {
super()
console.log("HELLO CAD CONTROLS")
const scope = this
const STATE = {NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4}
this.object = object
this.domElement = domElement
this.domElement.style.touchAction = "none" // disable touch scroll
// API
this.enabled = true
this.screen = {left: 0, top: 0, width: 0, height: 0}
this.rotateSpeed = 1.0
this.zoomSpeed = 1.2
this.panSpeed = 0.3
this.noRotate = false
this.noZoom = false
this.noPan = false
this.staticMoving = false
this.dynamicDampingFactor = 0.2
this.minDistance = 0
this.maxDistance = Infinity
this.minZoom = 0
this.maxZoom = Infinity
this.keys = ["KeyA" /*A*/, "KeyS" /*S*/, "KeyD" /*D*/]
this.mouseButtons = {LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN}
// internals
this.target = new Vector3()
const EPS = 0.000001
const lastPosition = new Vector3()
let lastZoom = 1
let _state = STATE.NONE,
_keyState = STATE.NONE,
_touchZoomDistanceStart = 0,
_touchZoomDistanceEnd = 0,
_lastAngle = 0
const _eye = new Vector3(),
_movePrev = new Vector2(),
_moveCurr = new Vector2(),
_lastAxis = new Vector3(),
_zoomStart = new Vector2(),
_zoomEnd = new Vector2(),
_panStart = new Vector2(),
_panEnd = new Vector2(),
_pointers = [],
_pointerPositions = {}
// for reset
this.target0 = this.target.clone()
this.position0 = this.object.position.clone()
this.up0 = this.object.up.clone()
this.zoom0 = this.object.zoom
// methods
this.handleResize = function () {
const box = scope.domElement.getBoundingClientRect()
// adjustments come from similar code in the jquery offset() function
const d = scope.domElement.ownerDocument.documentElement
scope.screen.left = box.left + window.pageXOffset - d.clientLeft
scope.screen.top = box.top + window.pageYOffset - d.clientTop
scope.screen.width = box.width
scope.screen.height = box.height
}
const getMouseOnScreen = (function () {
const vector = new Vector2()
return function getMouseOnScreen(pageX, pageY) {
vector.set((pageX - scope.screen.left) / scope.screen.width, (pageY - scope.screen.top) / scope.screen.height)
return vector
}
})()
const getMouseOnCircle = (function () {
const vector = new Vector2()
return function getMouseOnCircle(pageX, pageY) {
vector.set(
(pageX - scope.screen.width * 0.5 - scope.screen.left) / (scope.screen.width * 0.5),
(scope.screen.height + 2 * (scope.screen.top - pageY)) / scope.screen.width, // screen.width intentional
)
return vector
}
})()
this.rotateCamera = (function () {
const axis = new Vector3(),
quaternion = new Quaternion(),
eyeDirection = new Vector3(),
objectUpDirection = new Vector3(),
objectSidewaysDirection = new Vector3(),
moveDirection = new Vector3()
return function rotateCamera() {
moveDirection.set(_moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0)
let angle = moveDirection.length()
if (angle) {
_eye.copy(scope.object.position).sub(scope.target)
eyeDirection.copy(_eye).normalize()
objectUpDirection.copy(scope.object.up).normalize()
objectSidewaysDirection.crossVectors(objectUpDirection, eyeDirection).normalize()
objectUpDirection.setLength(_moveCurr.y - _movePrev.y)
objectSidewaysDirection.setLength(_moveCurr.x - _movePrev.x)
moveDirection.copy(objectUpDirection.add(objectSidewaysDirection))
axis.crossVectors(moveDirection, _eye).normalize()
angle *= scope.rotateSpeed
quaternion.setFromAxisAngle(axis, angle)
_eye.applyQuaternion(quaternion)
scope.object.up.applyQuaternion(quaternion)
_lastAxis.copy(axis)
_lastAngle = angle
} else if (!scope.staticMoving && _lastAngle) {
_lastAngle *= Math.sqrt(1.0 - scope.dynamicDampingFactor)
_eye.copy(scope.object.position).sub(scope.target)
quaternion.setFromAxisAngle(_lastAxis, _lastAngle)
_eye.applyQuaternion(quaternion)
scope.object.up.applyQuaternion(quaternion)
}
_movePrev.copy(_moveCurr)
}
})()
this.zoomCamera = function () {
let factor
if (_state === STATE.TOUCH_ZOOM_PAN) {
factor = _touchZoomDistanceStart / _touchZoomDistanceEnd
_touchZoomDistanceStart = _touchZoomDistanceEnd
if (scope.object.isPerspectiveCamera) {
_eye.multiplyScalar(factor)
} else if (scope.object.isOrthographicCamera) {
scope.object.zoom = MathUtils.clamp(scope.object.zoom / factor, scope.minZoom, scope.maxZoom)
if (lastZoom !== scope.object.zoom) {
scope.object.updateProjectionMatrix()
}
} else {
console.warn("THREE.TrackballControls: Unsupported camera type")
}
} else {
factor = 1.0 + (_zoomEnd.y - _zoomStart.y) * scope.zoomSpeed
if (factor !== 1.0 && factor > 0.0) {
if (scope.object.isPerspectiveCamera) {
_eye.multiplyScalar(factor)
} else if (scope.object.isOrthographicCamera) {
scope.object.zoom = MathUtils.clamp(scope.object.zoom / factor, scope.minZoom, scope.maxZoom)
if (lastZoom !== scope.object.zoom) {
scope.object.updateProjectionMatrix()
}
} else {
console.warn("THREE.TrackballControls: Unsupported camera type")
}
}
if (scope.staticMoving) {
_zoomStart.copy(_zoomEnd)
} else {
_zoomStart.y += (_zoomEnd.y - _zoomStart.y) * this.dynamicDampingFactor
}
}
}
this.panCamera = (function () {
const mouseChange = new Vector2(),
objectUp = new Vector3(),
pan = new Vector3()
return function panCamera() {
mouseChange.copy(_panEnd).sub(_panStart)
if (mouseChange.lengthSq()) {
if (scope.object.isOrthographicCamera) {
const scale_x = (scope.object.right - scope.object.left) / scope.object.zoom / scope.domElement.clientWidth
const scale_y = (scope.object.top - scope.object.bottom) / scope.object.zoom / scope.domElement.clientWidth
mouseChange.x *= scale_x
mouseChange.y *= scale_y
}
mouseChange.multiplyScalar(_eye.length() * scope.panSpeed)
pan.copy(_eye).cross(scope.object.up).setLength(mouseChange.x)
pan.add(objectUp.copy(scope.object.up).setLength(mouseChange.y))
scope.object.position.add(pan)
scope.target.add(pan)
if (scope.staticMoving) {
_panStart.copy(_panEnd)
} else {
_panStart.add(mouseChange.subVectors(_panEnd, _panStart).multiplyScalar(scope.dynamicDampingFactor))
}
}
}
})()
this.checkDistances = function () {
if (!scope.noZoom || !scope.noPan) {
if (_eye.lengthSq() > scope.maxDistance * scope.maxDistance) {
scope.object.position.addVectors(scope.target, _eye.setLength(scope.maxDistance))
_zoomStart.copy(_zoomEnd)
}
if (_eye.lengthSq() < scope.minDistance * scope.minDistance) {
scope.object.position.addVectors(scope.target, _eye.setLength(scope.minDistance))
_zoomStart.copy(_zoomEnd)
}
}
}
this.update = function () {
_eye.subVectors(scope.object.position, scope.target)
if (!scope.noRotate) {
scope.rotateCamera()
}
if (!scope.noZoom) {
scope.zoomCamera()
}
if (!scope.noPan) {
scope.panCamera()
}
scope.object.position.addVectors(scope.target, _eye)
if (scope.object.isPerspectiveCamera) {
scope.checkDistances()
scope.object.lookAt(scope.target)
if (lastPosition.distanceToSquared(scope.object.position) > EPS) {
scope.dispatchEvent(_changeEvent)
lastPosition.copy(scope.object.position)
}
} else if (scope.object.isOrthographicCamera) {
scope.object.lookAt(scope.target)
if (lastPosition.distanceToSquared(scope.object.position) > EPS || lastZoom !== scope.object.zoom) {
scope.dispatchEvent(_changeEvent)
lastPosition.copy(scope.object.position)
lastZoom = scope.object.zoom
}
} else {
console.warn("THREE.TrackballControls: Unsupported camera type")
}
}
this.reset = function () {
_state = STATE.NONE
_keyState = STATE.NONE
scope.target.copy(scope.target0)
scope.object.position.copy(scope.position0)
scope.object.up.copy(scope.up0)
scope.object.zoom = scope.zoom0
scope.object.updateProjectionMatrix()
_eye.subVectors(scope.object.position, scope.target)
scope.object.lookAt(scope.target)
scope.dispatchEvent(_changeEvent)
lastPosition.copy(scope.object.position)
lastZoom = scope.object.zoom
}
// listeners
function onPointerDown(event) {
if (scope.enabled === false) return
if (_pointers.length === 0) {
scope.domElement.setPointerCapture(event.pointerId)
scope.domElement.addEventListener("pointermove", onPointerMove)
scope.domElement.addEventListener("pointerup", onPointerUp)
}
//
addPointer(event)
if (event.pointerType === "touch") {
onTouchStart(event)
} else {
onMouseDown(event)
}
}
function onPointerMove(event) {
if (scope.enabled === false) return
if (event.pointerType === "touch") {
onTouchMove(event)
} else {
onMouseMove(event)
}
}
function onPointerUp(event) {
if (scope.enabled === false) return
if (event.pointerType === "touch") {
onTouchEnd(event)
} else {
onMouseUp()
}
//
removePointer(event)
if (_pointers.length === 0) {
scope.domElement.releasePointerCapture(event.pointerId)
scope.domElement.removeEventListener("pointermove", onPointerMove)
scope.domElement.removeEventListener("pointerup", onPointerUp)
}
}
function onPointerCancel(event) {
removePointer(event)
}
function keydown(event) {
if (scope.enabled === false) return
window.removeEventListener("keydown", keydown)
if (_keyState !== STATE.NONE) {
return
} else if (event.code === scope.keys[STATE.ROTATE] && !scope.noRotate) {
_keyState = STATE.ROTATE
} else if (event.code === scope.keys[STATE.ZOOM] && !scope.noZoom) {
_keyState = STATE.ZOOM
} else if (event.code === scope.keys[STATE.PAN] && !scope.noPan) {
_keyState = STATE.PAN
}
}
function keyup() {
if (scope.enabled === false) return
_keyState = STATE.NONE
window.addEventListener("keydown", keydown)
}
function onMouseDown(event) {
if (_state === STATE.NONE) {
switch (event.button) {
case scope.mouseButtons.LEFT:
_state = STATE.ROTATE
break
case scope.mouseButtons.MIDDLE:
_state = STATE.ZOOM
break
case scope.mouseButtons.RIGHT:
_state = STATE.PAN
break
}
}
const state = _keyState !== STATE.NONE ? _keyState : _state
if (state === STATE.ROTATE && !scope.noRotate) {
_moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY))
_movePrev.copy(_moveCurr)
} else if (state === STATE.ZOOM && !scope.noZoom) {
_zoomStart.copy(getMouseOnScreen(event.pageX, event.pageY))
_zoomEnd.copy(_zoomStart)
} else if (state === STATE.PAN && !scope.noPan) {
_panStart.copy(getMouseOnScreen(event.pageX, event.pageY))
_panEnd.copy(_panStart)
}
scope.dispatchEvent(_startEvent)
}
function onMouseMove(event) {
const state = _keyState !== STATE.NONE ? _keyState : _state
if (state === STATE.ROTATE && !scope.noRotate) {
_movePrev.copy(_moveCurr)
_moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY))
} else if (state === STATE.ZOOM && !scope.noZoom) {
_zoomEnd.copy(getMouseOnScreen(event.pageX, event.pageY))
} else if (state === STATE.PAN && !scope.noPan) {
_panEnd.copy(getMouseOnScreen(event.pageX, event.pageY))
}
}
function onMouseUp() {
_state = STATE.NONE
scope.dispatchEvent(_endEvent)
}
function onMouseWheel(event) {
if (scope.enabled === false) return
if (scope.noZoom === true) return
event.preventDefault()
switch (event.deltaMode) {
case 2:
// Zoom in pages
_zoomStart.y -= event.deltaY * 0.025
break
case 1:
// Zoom in lines
_zoomStart.y -= event.deltaY * 0.01
break
default:
// undefined, 0, assume pixels
_zoomStart.y -= event.deltaY * 0.00025
break
}
scope.dispatchEvent(_startEvent)
scope.dispatchEvent(_endEvent)
}
function onTouchStart(event) {
trackPointer(event)
switch (_pointers.length) {
case 1:
_state = STATE.TOUCH_ROTATE
_moveCurr.copy(getMouseOnCircle(_pointers[0].pageX, _pointers[0].pageY))
_movePrev.copy(_moveCurr)
break
default: // 2 or more
_state = STATE.TOUCH_ZOOM_PAN
const dx = _pointers[0].pageX - _pointers[1].pageX
const dy = _pointers[0].pageY - _pointers[1].pageY
_touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt(dx * dx + dy * dy)
const x = (_pointers[0].pageX + _pointers[1].pageX) / 2
const y = (_pointers[0].pageY + _pointers[1].pageY) / 2
_panStart.copy(getMouseOnScreen(x, y))
_panEnd.copy(_panStart)
break
}
scope.dispatchEvent(_startEvent)
}
function onTouchMove(event) {
trackPointer(event)
switch (_pointers.length) {
case 1:
_movePrev.copy(_moveCurr)
_moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY))
break
default: // 2 or more
const position = getSecondPointerPosition(event)
const dx = event.pageX - position.x
const dy = event.pageY - position.y
_touchZoomDistanceEnd = Math.sqrt(dx * dx + dy * dy)
const x = (event.pageX + position.x) / 2
const y = (event.pageY + position.y) / 2
_panEnd.copy(getMouseOnScreen(x, y))
break
}
}
function onTouchEnd(event) {
switch (_pointers.length) {
case 0:
_state = STATE.NONE
break
case 1:
_state = STATE.TOUCH_ROTATE
_moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY))
_movePrev.copy(_moveCurr)
break
case 2:
_state = STATE.TOUCH_ZOOM_PAN
for (let i = 0; i < _pointers.length; i++) {
if (_pointers[i].pointerId !== event.pointerId) {
const position = _pointerPositions[_pointers[i].pointerId]
_moveCurr.copy(getMouseOnCircle(position.x, position.y))
_movePrev.copy(_moveCurr)
break
}
}
break
}
scope.dispatchEvent(_endEvent)
}
function contextmenu(event) {
if (scope.enabled === false) return
event.preventDefault()
}
function addPointer(event) {
_pointers.push(event)
}
function removePointer(event) {
delete _pointerPositions[event.pointerId]
for (let i = 0; i < _pointers.length; i++) {
if (_pointers[i].pointerId == event.pointerId) {
_pointers.splice(i, 1)
return
}
}
}
function trackPointer(event) {
let position = _pointerPositions[event.pointerId]
if (position === undefined) {
position = new Vector2()
_pointerPositions[event.pointerId] = position
}
position.set(event.pageX, event.pageY)
}
function getSecondPointerPosition(event) {
const pointer = event.pointerId === _pointers[0].pointerId ? _pointers[1] : _pointers[0]
return _pointerPositions[pointer.pointerId]
}
this.dispose = function () {
scope.domElement.removeEventListener("contextmenu", contextmenu)
scope.domElement.removeEventListener("pointerdown", onPointerDown)
scope.domElement.removeEventListener("pointercancel", onPointerCancel)
scope.domElement.removeEventListener("wheel", onMouseWheel)
scope.domElement.removeEventListener("pointermove", onPointerMove)
scope.domElement.removeEventListener("pointerup", onPointerUp)
window.removeEventListener("keydown", keydown)
window.removeEventListener("keyup", keyup)
}
this.domElement.addEventListener("contextmenu", contextmenu)
this.domElement.addEventListener("pointerdown", onPointerDown)
this.domElement.addEventListener("pointercancel", onPointerCancel)
this.domElement.addEventListener("wheel", onMouseWheel, {passive: false})
window.addEventListener("keydown", keydown)
window.addEventListener("keyup", keyup)
this.handleResize()
// force an update at start
this.update()
}
}
export {TrackballControls}