UNPKG

@pmndrs/handle

Version:

framework agnostic expandable handle implementation for threejs

254 lines (253 loc) 10.1 kB
import { Euler, Object3D, Quaternion, Vector3 } from 'three'; import { computeTranslateAsHandleTransformState, } from './computations/index.js'; import { computeOnePointerHandleTransformState, } from './computations/one-pointer.js'; import { computeTwoPointerHandleTransformState, } from './computations/two-pointer.js'; import { HandleStateImpl } from './state.js'; import { getWorldDirection } from './utils.js'; const vectorHelper = new Vector3(); export class HandleStore { target; getOptions; //internal out state (will be used to output the state) outputState; latestMoveEvent; //internal in state (will be written on save) inputState = new Map(); capturedObjects = new Map(); initialTargetPosition = new Vector3(); initialTargetQuaternion = new Quaternion(); initialTargetRotation = new Euler(); initialTargetScale = new Vector3(); initialTargetParentWorldMatrix; //prev state prevTwoPointerDeltaRotation; prevTranslateAsDeltaRotation; prevAngle; handlers = { onPointerDown: this.onPointerDown.bind(this), onPointerMove: this.onPointerMove.bind(this), onPointerUp: this.onPointerUp.bind(this), }; constructor(target, getOptions = () => ({})) { this.target = target; this.getOptions = getOptions; this.outputState = new HandleStateImpl(this.cancel.bind(this)); } /** * @requires that the pointerId is in this.capturedSet */ firstOnPointer(event) { const target = this.getTarget(); if (target == null) { return; } const pointerWorldDirection = getWorldDirection(event, vectorHelper) ? vectorHelper.clone() : undefined; event.intersection.details.type; this.inputState.set(event.pointerId, { pointerWorldDirection, pointerWorldPoint: event.point, pointerWorldOrigin: event.pointerPosition, pointerWorldQuaternion: event.pointerQuaternion, initialPointerWorldPoint: event.point.clone(), initialPointerWorldDirection: pointerWorldDirection?.clone(), initialPointerWorldQuaternion: event.pointerQuaternion.clone(), prevPointerWorldQuaternion: event.pointerQuaternion, }); this.save(); if (this.inputState.size === 1) { this.outputState.start(event, { pointerAmount: 1, time: event.timeStamp, position: this.initialTargetPosition.clone(), quaternion: this.initialTargetQuaternion.clone(), rotation: this.initialTargetRotation.clone(), scale: this.initialTargetScale.clone(), }); } this.outputState.memo = this.apply(target); } onPointerDown(event) { if (this.getOptions().filter?.(event) === false) { return; } this.stopPropagation(event); if (!this.capturePointer(event.pointerId, event.object)) { return; } this.firstOnPointer(event); } onPointerMove(event) { if (!this.capturedObjects.has(event.pointerId)) { return; } this.stopPropagation(event); const entry = this.inputState.get(event.pointerId); if (entry == null) { this.firstOnPointer(event); return; } this.latestMoveEvent = event; entry.pointerWorldPoint = event.point; entry.prevPointerWorldQuaternion = entry.pointerWorldQuaternion; entry.pointerWorldQuaternion = event.pointerQuaternion; entry.pointerWorldOrigin = event.pointerPosition; if (entry.pointerWorldDirection != null) { getWorldDirection(event, entry.pointerWorldDirection); } } cancel() { if (this.capturedObjects.size === 0) { return; } for (const [pointerId, object] of this.capturedObjects) { object.releasePointerCapture(pointerId); } this.capturedObjects.clear(); this.inputState.clear(); this.outputState.end(undefined); const target = this.getTarget(); if (target != null) { this.apply(target); } } onPointerUp(event) { if (!this.capturedObjects.has(event.pointerId)) { return; } this.stopPropagation(event); this.releasePointer(event.pointerId, event.object, event); } update(time) { const target = this.getTarget(); if (target == null || this.inputState.size === 0 || (this.latestMoveEvent == null && (this.getOptions().alwaysUpdate ?? false) === false)) { return; } const options = this.getOptions(); let transformState; if (options.translate === 'as-rotate' || options.translate === 'as-rotate-and-scale' || options.translate === 'as-scale') { options.translate; this.prevTwoPointerDeltaRotation = undefined; this.prevAngle = undefined; const [p1] = this.inputState.values(); const matrixWorld = target.matrixWorld; const parentMatrixWorld = target.parent?.matrixWorld; transformState = computeTranslateAsHandleTransformState(time, p1, this, matrixWorld, parentMatrixWorld, options); } else if (this.inputState.size === 1) { this.prevTwoPointerDeltaRotation = undefined; this.prevAngle = undefined; this.prevTranslateAsDeltaRotation = undefined; const [p1] = this.inputState.values(); transformState = computeOnePointerHandleTransformState(time, p1, this, target.parent?.matrixWorld, options); } else { this.prevTranslateAsDeltaRotation = undefined; const [p1, p2] = this.inputState.values(); transformState = computeTwoPointerHandleTransformState(time, p1, p2, this, target.parent?.matrixWorld, options); } this.outputState.update(this.latestMoveEvent, transformState); this.outputState.memo = this.apply(target); this.latestMoveEvent = undefined; } getTarget() { return this.target instanceof Object3D ? this.target : this.target?.current; } capturePointer(pointerId, object) { if (this.capturedObjects.has(pointerId)) { return false; } const { multitouch, translate } = this.getOptions(); if (((multitouch ?? true) === false || typeof translate === 'string') && this.capturedObjects.size === 1) { return false; } this.capturedObjects.set(pointerId, object); object.setPointerCapture(pointerId); return true; } releasePointer(pointerId, object, event) { const target = this.getTarget(); if (target == null || !this.capturedObjects.delete(pointerId)) { return; } this.inputState.delete(pointerId); object.releasePointerCapture(pointerId); if (this.inputState.size > 0) { this.save(); return; } this.outputState.end(event); this.apply(target); } stopPropagation(event) { if (event == null || !(this.getOptions()?.stopPropagation ?? true)) { return; } event.stopPropagation(); } apply(target) { const apply = this.getOptions().apply ?? defaultApply; return apply(this.outputState, target); } getState() { return this.inputState.size === 0 ? undefined : this.outputState; } save() { const target = this.getTarget(); if (target == null) { return; } target.updateWorldMatrix(true, false); //reset prev this.prevAngle = undefined; this.prevTwoPointerDeltaRotation = undefined; this.prevTranslateAsDeltaRotation = undefined; //update initial this.initialTargetParentWorldMatrix = target.parent?.matrixWorld.clone(); if (target.matrixAutoUpdate) { this.initialTargetPosition.copy(target.position); this.initialTargetQuaternion.copy(target.quaternion); this.initialTargetRotation.copy(target.rotation); this.initialTargetScale.copy(target.scale); } else { target.matrix.decompose(this.initialTargetPosition, this.initialTargetQuaternion, this.initialTargetScale); this.initialTargetRotation.setFromQuaternion(this.initialTargetQuaternion, target.rotation.order); } for (const data of this.inputState.values()) { if (data.pointerWorldDirection != null) { data.initialPointerWorldDirection?.copy(data.pointerWorldDirection); } data.initialPointerWorldPoint.copy(data.pointerWorldPoint); data.initialPointerWorldQuaternion.copy(data.pointerWorldQuaternion); } } bind(handle) { const { onPointerDown, onPointerMove, onPointerUp } = this.handlers; handle.addEventListener('pointerdown', onPointerDown); handle.addEventListener('pointermove', onPointerMove); handle.addEventListener('pointerup', onPointerUp); return () => { handle.removeEventListener('pointerdown', onPointerDown); handle.removeEventListener('pointermove', onPointerMove); handle.removeEventListener('pointerup', onPointerUp); this.cancel(); }; } capture(pointerId, object) { if (!this.capturePointer(pointerId, object)) { return noop; } return () => this.releasePointer(pointerId, object, undefined); } } function noop() { } export function defaultApply(state, target) { target.position.copy(state.current.position); target.rotation.order = state.current.rotation.order; target.quaternion.copy(state.current.quaternion); target.scale.copy(state.current.scale); }