@pmndrs/handle
Version:
framework agnostic expandable handle implementation for threejs
254 lines (253 loc) • 10.1 kB
JavaScript
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);
}