UNPKG

@pmndrs/pointer-events

Version:

framework agnostic pointer-events implementation for threejs

210 lines (209 loc) 8.88 kB
import { Matrix4, Plane, Quaternion, Raycaster, Vector3, Vector2, Mesh, } from 'three'; import { computeIntersectionWorldPlane, getDominantIntersectionIndex, pushTimes, voidObjectIntersectionFromRay, } from './utils.js'; import { getClosestUV, updateAndCheckWorldTransformation } from '../utils.js'; const invertedMatrixHelper = new Matrix4(); const scaleHelper = new Vector3(); const NegZAxis = new Vector3(0, 0, -1); const planeHelper = new Plane(); const point2Helper = new Vector2(); export class RayIntersector { space; options; raycaster = new Raycaster(); raycasterQuaternion = new Quaternion(); worldScale = 0; ready; intersects = []; pointerEventsOrders = []; constructor(space, options) { this.space = space; this.options = options; } isReady() { return this.ready ?? this.prepareTransformation(); } prepareTransformation() { const spaceObject = this.space.current; if (spaceObject == null) { return (this.ready = false); } this.ready = updateAndCheckWorldTransformation(spaceObject); if (!this.ready) { return false; } spaceObject.matrixWorld.decompose(this.raycaster.ray.origin, this.raycasterQuaternion, scaleHelper); this.worldScale = scaleHelper.x; this.raycaster.ray.direction.copy(this.options?.direction ?? NegZAxis).applyQuaternion(this.raycasterQuaternion); return true; } intersectPointerCapture({ intersection, object }) { if (intersection.details.type != 'ray') { throw new Error(`unable to process a pointer capture of type "${intersection.details.type}" with a ray intersector`); } if (!this.prepareTransformation()) { return intersection; } intersection.object.updateWorldMatrix(true, false); computeIntersectionWorldPlane(planeHelper, intersection, intersection.object.matrixWorld); const { ray } = this.raycaster; const pointOnFace = ray.intersectPlane(planeHelper, new Vector3()) ?? intersection.point; const point = ray.direction .clone() .multiplyScalar(intersection.pointerPosition.distanceTo(intersection.point)) .add(ray.origin); let uv = intersection.uv; if (intersection.object instanceof Mesh && getClosestUV(point2Helper, point, intersection.object)) { uv = point2Helper.clone(); } return { ...intersection, uv, object, pointOnFace, point, pointerPosition: ray.origin.clone(), pointerQuaternion: this.raycasterQuaternion.clone(), }; } startIntersection() { this.prepareTransformation(); } executeIntersection(object, objectPointerEventsOrder) { if (!this.isReady()) { return; } const start = this.intersects.length; object.raycast(this.raycaster, this.intersects); pushTimes(this.pointerEventsOrders, objectPointerEventsOrder, this.intersects.length - start); } finalizeIntersection(scene) { const pointerPosition = this.raycaster.ray.origin.clone(); const pointerQuaternion = this.raycasterQuaternion.clone(); let filter; if (this.options.minDistance != null) { const localMinDistance = this.options.minDistance / this.worldScale; filter = (intersection) => intersection.distance >= localMinDistance; } const index = getDominantIntersectionIndex(this.intersects, this.pointerEventsOrders, this.options, filter); const intersection = index == null ? undefined : this.intersects[index]; this.intersects.length = 0; this.pointerEventsOrders.length = 0; if (intersection == null) { return voidObjectIntersectionFromRay(scene, this.raycaster.ray, () => ({ type: 'ray' }), pointerPosition, pointerQuaternion); } intersection.object.updateWorldMatrix(true, false); return Object.assign(intersection, { details: { type: 'ray', }, pointerPosition, pointerQuaternion, pointOnFace: intersection.point, localPoint: intersection.point .clone() .applyMatrix4(invertedMatrixHelper.copy(intersection.object.matrixWorld).invert()), }); } } const directionHelper = new Vector3(); export class ScreenRayIntersector { prepareTransformation; options; raycaster = new Raycaster(); cameraQuaternion = new Quaternion(); fromPosition = new Vector3(); fromQuaternion = new Quaternion(); coords = new Vector2(); viewPlane = new Plane(); intersects = []; pointerEventsOrders = []; constructor(prepareTransformation, options) { this.prepareTransformation = prepareTransformation; this.options = options; } isReady() { return true; } intersectPointerCapture({ intersection, object }, nativeEvent) { const details = intersection.details; if (details.type != 'screen-ray') { throw new Error(`unable to process a pointer capture of type "${intersection.details.type}" with a camera ray intersector`); } if (!this.startIntersection(nativeEvent)) { return intersection; } this.viewPlane.constant -= details.distanceViewPlane; //find captured intersection point by intersecting the ray to the plane of the camera const point = this.raycaster.ray.intersectPlane(this.viewPlane, new Vector3()); if (point == null) { return intersection; } intersection.object.updateWorldMatrix(true, false); computeIntersectionWorldPlane(this.viewPlane, intersection, intersection.object.matrixWorld); let uv = intersection.uv; if (intersection.object instanceof Mesh && getClosestUV(point2Helper, point, intersection.object)) { uv = point2Helper.clone(); } return { ...intersection, details: { ...details, direction: this.raycaster.ray.direction.clone(), screenPoint: this.coords.clone(), }, uv, object, point, pointOnFace: point, pointerPosition: this.raycaster.ray.origin.clone(), pointerQuaternion: this.cameraQuaternion.clone(), }; } startIntersection(nativeEvent) { const from = this.prepareTransformation(nativeEvent, this.coords); if (from == null) { return false; } from.updateWorldMatrix(true, false); from.matrixWorld.decompose(this.fromPosition, this.fromQuaternion, scaleHelper); this.raycaster.setFromCamera(this.coords, from); this.viewPlane.setFromNormalAndCoplanarPoint(from.getWorldDirection(directionHelper), this.raycaster.ray.origin); return true; } executeIntersection(object, objectPointerEventsOrder) { const start = this.intersects.length; object.raycast(this.raycaster, this.intersects); pushTimes(this.pointerEventsOrders, objectPointerEventsOrder, this.intersects.length - start); } finalizeIntersection(scene) { const pointerPosition = this.fromPosition.clone(); const pointerQuaternion = this.cameraQuaternion.clone(); const pointerDirection = this.raycaster.ray.direction.clone(); const index = getDominantIntersectionIndex(this.intersects, this.pointerEventsOrders, this.options); const intersection = index == null ? undefined : this.intersects[index]; this.intersects.length = 0; this.pointerEventsOrders.length = 0; if (intersection == null) { return voidObjectIntersectionFromRay(scene, this.raycaster.ray, (_point, distance) => ({ type: 'screen-ray', distanceViewPlane: distance, screenPoint: this.coords.clone(), direction: pointerDirection, }), pointerPosition, pointerQuaternion); } intersection.object.updateWorldMatrix(true, false); invertedMatrixHelper.copy(intersection.object.matrixWorld).invert(); return Object.assign(intersection, { details: { type: 'screen-ray', distanceViewPlane: this.viewPlane.distanceToPoint(intersection.point), screenPoint: this.coords.clone(), direction: pointerDirection, }, pointOnFace: intersection.point, pointerPosition, pointerQuaternion, localPoint: intersection.point.clone().applyMatrix4(invertedMatrixHelper), }); } }