UNPKG

@pmndrs/pointer-events

Version:

framework agnostic pointer-events implementation for threejs

150 lines (149 loc) 6.82 kB
import { Line3, Matrix4, Plane, Quaternion, Ray, Raycaster, Vector3, Mesh, Vector2, } from 'three'; import { computeIntersectionWorldPlane, getDominantIntersectionIndex, pushTimes, voidObjectIntersectionFromRay, } from './utils.js'; import { getClosestUV, updateAndCheckWorldTransformation } from '../utils.js'; const invertedMatrixHelper = new Matrix4(); const lineHelper = new Line3(); const scaleHelper = new Vector3(); const planeHelper = new Plane(); const rayHelper = new Ray(); const point2Helper = new Vector2(); const defaultLinePoints = [new Vector3(0, 0, 0), new Vector3(0, 0, 1)]; export class LinesIntersector { space; options; raycasters = []; fromMatrixWorld = new Matrix4(); ready; intersects = []; pointerEventsOrders = []; raycasterIndices = []; 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; } this.fromMatrixWorld.copy(spaceObject.matrixWorld); return true; } intersectPointerCapture({ intersection, object }) { const details = intersection.details; if (details.type != 'lines') { throw new Error(`unable to process a pointer capture of type "${intersection.details.type}" with a lines intersector`); } if (!this.prepareTransformation()) { return intersection; } const linePoints = this.options.linePoints ?? defaultLinePoints; lineHelper.set(linePoints[details.lineIndex], linePoints[details.lineIndex + 1]).applyMatrix4(this.fromMatrixWorld); const point = lineHelper.at(details.distanceOnLine / lineHelper.distance(), new Vector3()); intersection.object.updateWorldMatrix(true, false); computeIntersectionWorldPlane(planeHelper, intersection, intersection.object.matrixWorld); const pointOnFace = rayHelper.intersectPlane(planeHelper, new Vector3()) ?? point; const pointerPosition = new Vector3(); const pointerQuaternion = new Quaternion(); this.fromMatrixWorld.decompose(pointerPosition, pointerQuaternion, scaleHelper); let uv = intersection.uv; if (intersection.object instanceof Mesh && getClosestUV(point2Helper, point, intersection.object)) { uv = point2Helper.clone(); } return { ...intersection, object, uv, pointOnFace, point, pointerPosition, pointerQuaternion, }; } startIntersection() { if (!this.prepareTransformation()) { return; } const linePoints = this.options.linePoints ?? defaultLinePoints; const length = linePoints.length - 1; for (let i = 0; i < length; i++) { const start = linePoints[i]; const end = linePoints[i + 1]; const raycaster = this.raycasters[i] ?? (this.raycasters[i] = new Raycaster()); //transform from local object to world raycaster.ray.origin.copy(start).applyMatrix4(this.fromMatrixWorld); raycaster.ray.direction.copy(end).applyMatrix4(this.fromMatrixWorld); //compute length & normalized direction raycaster.ray.direction.sub(raycaster.ray.origin); const lineLength = raycaster.ray.direction.length(); raycaster.ray.direction.divideScalar(lineLength); raycaster.far = lineLength; } this.raycasters.length = length; return; } executeIntersection(object, objectPointerEventsOrder) { if (!this.isReady()) { return; } const startOuter = this.intersects.length; const length = this.raycasters.length; for (let i = 0; i < length; i++) { const raycaster = this.raycasters[i]; const startInner = this.intersects.length; object.raycast(raycaster, this.intersects); pushTimes(this.raycasterIndices, i, this.intersects.length - startInner); } pushTimes(this.pointerEventsOrders, objectPointerEventsOrder, this.intersects.length - startOuter); } finalizeIntersection(scene) { const pointerPosition = new Vector3().setFromMatrixPosition(this.fromMatrixWorld); const pointerQuaternion = new Quaternion().setFromRotationMatrix(this.fromMatrixWorld); const index = getDominantIntersectionIndex(this.intersects, this.pointerEventsOrders, this.options); const intersection = index == null ? undefined : this.intersects[index]; const raycasterIndex = index == null ? undefined : this.raycasterIndices[index]; this.intersects.length = 0; this.raycasterIndices.length = 0; this.pointerEventsOrders.length = 0; if (intersection == null || raycasterIndex == null) { const lastRaycasterIndex = this.raycasters.length - 1; const prevDistance = this.raycasters.reduce((prev, caster, i) => (i === lastRaycasterIndex ? prev : prev + caster.far), 0); const lastRaycaster = this.raycasters[lastRaycasterIndex]; return voidObjectIntersectionFromRay(scene, lastRaycaster.ray, (point, distanceOnLine) => ({ line: new Line3(lastRaycaster.ray.origin.clone(), point), lineIndex: this.raycasters.length - 1, distanceOnLine, type: 'lines', }), pointerPosition, pointerQuaternion, prevDistance); } let distance = intersection.distance; for (let i = 0; i < raycasterIndex; i++) { distance += this.raycasters[i].far; } intersection.object.updateWorldMatrix(true, false); //TODO: consider maxLength const raycaster = this.raycasters[raycasterIndex]; return Object.assign(intersection, { details: { lineIndex: raycasterIndex, distanceOnLine: intersection.distance, type: 'lines', line: new Line3(raycaster.ray.origin.clone(), raycaster.ray.direction.clone().multiplyScalar(raycaster.far).add(raycaster.ray.origin)), }, distance, pointerPosition, pointerQuaternion, pointOnFace: intersection.point, localPoint: intersection.point .clone() .applyMatrix4(invertedMatrixHelper.copy(intersection.object.matrixWorld).invert()), }); } }