@pmndrs/pointer-events
Advanced tools
Comparing version 6.2.2 to 6.2.3
import { Object3D } from 'three'; | ||
import { NativeEvent } from './event.js'; | ||
import { Pointer } from './pointer.js'; | ||
import { Pointer, PointerCapture } from './pointer.js'; | ||
import { Intersection } from './index.js'; | ||
export declare class CombinedPointer { | ||
private pointers; | ||
private isDefaults; | ||
private readonly enableMultiplePointers; | ||
private readonly pointers; | ||
private readonly isDefaults; | ||
private enabled; | ||
register(pointer: Pointer, isDefault: boolean): () => void; | ||
private activePointer; | ||
private readonly nonCapturedPointers; | ||
constructor(enableMultiplePointers: boolean); | ||
register(pointer: Pointer | CombinedPointer, isDefault?: boolean): () => void; | ||
private unregister; | ||
private startIntersection; | ||
/** | ||
* only for internal use | ||
*/ | ||
getIntersection(): Intersection | undefined; | ||
/** | ||
* only for internal use | ||
*/ | ||
getPointerCapture(): PointerCapture | undefined; | ||
private computeActivePointer; | ||
/** | ||
* only for internal use | ||
*/ | ||
commit(nativeEvent: NativeEvent, computeActivePointer?: boolean): void; | ||
move(scene: Object3D, nativeEvent: NativeEvent): void; | ||
setEnabled(enabled: boolean, nativeEvent: NativeEvent): void; | ||
} |
@@ -0,6 +1,13 @@ | ||
import { intersectPointerEventTargets } from './intersections/utils.js'; | ||
export class CombinedPointer { | ||
enableMultiplePointers; | ||
pointers = []; | ||
isDefaults = []; | ||
enabled = true; | ||
register(pointer, isDefault) { | ||
activePointer; | ||
nonCapturedPointers = []; | ||
constructor(enableMultiplePointers) { | ||
this.enableMultiplePointers = enableMultiplePointers; | ||
} | ||
register(pointer, isDefault = false) { | ||
this.pointers.push(pointer); | ||
@@ -18,29 +25,94 @@ this.isDefaults.push(isDefault); | ||
} | ||
move(scene, nativeEvent) { | ||
if (!this.enabled) { | ||
return; | ||
startIntersection(nonCapturedPointers, nativeEvent) { | ||
const length = this.pointers.length; | ||
for (let i = 0; i < length; i++) { | ||
const pointer = this.pointers[i]; | ||
if (pointer instanceof CombinedPointer) { | ||
pointer.startIntersection(nonCapturedPointers, nativeEvent); | ||
continue; | ||
} | ||
const pointerCapture = pointer.getPointerCapture(); | ||
if (pointerCapture != null) { | ||
pointer.setIntersection(pointer.intersector.intersectPointerCapture(pointerCapture, nativeEvent)); | ||
continue; | ||
} | ||
nonCapturedPointers.push(pointer); | ||
pointer.intersector.startIntersection(nativeEvent); | ||
} | ||
} | ||
/** | ||
* only for internal use | ||
*/ | ||
getIntersection() { | ||
return this.activePointer?.getIntersection(); | ||
} | ||
/** | ||
* only for internal use | ||
*/ | ||
getPointerCapture() { | ||
return this.activePointer?.getPointerCapture(); | ||
} | ||
computeActivePointer() { | ||
let smallestDistance; | ||
this.activePointer = undefined; | ||
const length = this.pointers.length; | ||
if (length === 0) { | ||
return; | ||
} | ||
for (let i = 0; i < length; i++) { | ||
this.pointers[i].computeMove(scene, nativeEvent); | ||
} | ||
let smallestIndex = 0; | ||
let smallestDistance = this.pointers[0].getIntersection()?.distance ?? Infinity; | ||
for (let i = 1; i < length; i++) { | ||
const distance = this.pointers[i].getIntersection()?.distance ?? Infinity; | ||
const pointer = this.pointers[i]; | ||
if (pointer instanceof CombinedPointer) { | ||
pointer.computeActivePointer(); | ||
} | ||
const intersection = pointer.getIntersection(); | ||
const distance = pointer.getPointerCapture() != null ? -Infinity : (intersection?.distance ?? Infinity); | ||
const isDefault = this.isDefaults[i]; | ||
if ((isDefault && distance === smallestDistance) || distance < smallestDistance) { | ||
smallestIndex = i; | ||
if (smallestDistance == null || (isDefault && distance === smallestDistance) || distance < smallestDistance) { | ||
this.activePointer = pointer; | ||
smallestDistance = distance; | ||
} | ||
} | ||
} | ||
/** | ||
* only for internal use | ||
*/ | ||
commit(nativeEvent, computeActivePointer = true) { | ||
if (this.enableMultiplePointers) { | ||
const length = this.pointers.length; | ||
for (let i = 0; i < length; i++) { | ||
this.pointers[i].commit(nativeEvent); | ||
} | ||
return; | ||
} | ||
if (computeActivePointer) { | ||
this.computeActivePointer(); | ||
} | ||
//commit all pointers, enable the active pointer, and disable all other pointers | ||
const length = this.pointers.length; | ||
for (let i = 0; i < length; i++) { | ||
const pointer = this.pointers[i]; | ||
pointer.setEnabled(i === smallestIndex, nativeEvent, false); | ||
pointer.commit(nativeEvent); | ||
pointer.setEnabled(pointer === this.activePointer, nativeEvent, false); | ||
pointer.commit(nativeEvent, false); | ||
} | ||
} | ||
move(scene, nativeEvent) { | ||
if (!this.enabled) { | ||
return; | ||
} | ||
/* | ||
slow version that stays in here for benchmarking | ||
for (let i = 0; i < this.pointers.length; i++) { | ||
this.pointers[i].move(scene, nativeEvent) | ||
}*/ | ||
//start intersection, build nonCapturedPointers list, and compute the intersection for all captured pointers | ||
this.nonCapturedPointers.length = 0; | ||
this.startIntersection(this.nonCapturedPointers, nativeEvent); | ||
//intersect scene using the non captured pointers | ||
intersectPointerEventTargets(scene, this.nonCapturedPointers); | ||
//finalize the intersection for the non captured pointers | ||
const nonCapturedPointerLength = this.nonCapturedPointers.length; | ||
for (let i = 0; i < nonCapturedPointerLength; i++) { | ||
const pointer = this.nonCapturedPointers[i]; | ||
pointer.setIntersection(pointer.intersector.finalizeIntersection()); | ||
} | ||
//commit the intersection, compute active pointers, and enabling/disabling pointers | ||
this.commit(nativeEvent); | ||
} | ||
setEnabled(enabled, nativeEvent) { | ||
@@ -50,5 +122,6 @@ this.enabled = enabled; | ||
for (let i = 0; i < length; i++) { | ||
this.pointers[i].setEnabled(enabled, nativeEvent); | ||
const pointer = this.pointers[i]; | ||
pointer.setEnabled(enabled && (this.enableMultiplePointers || pointer == this.activePointer), nativeEvent); | ||
} | ||
} | ||
} |
@@ -1,9 +0,5 @@ | ||
import { Quaternion, Vector2, Vector3 } from 'three'; | ||
import { Pointer } from './pointer.js'; | ||
import { PointerEvent } from './event.js'; | ||
import { intersectRayFromCamera } from './intersections/ray.js'; | ||
import { CameraRayIntersector } from './intersections/ray.js'; | ||
import { generateUniquePointerId } from './pointer/index.js'; | ||
const vectorHelper = new Vector3(); | ||
const vector2Helper = new Vector2(); | ||
const quaternionHelper = new Quaternion(); | ||
function htmlEventToCoords(element, e, target) { | ||
@@ -52,5 +48,6 @@ if (!(e instanceof globalThis.MouseEvent)) { | ||
} | ||
pointerType = `${pointerTypePrefix}${pointerType}`; | ||
const computeIntersection = (scene, nativeEvent, pointerCapture) => intersectRayFromCamera(toCamera, toCoords(nativeEvent, vector2Helper), toCamera.getWorldPosition(vectorHelper), toCamera.getWorldQuaternion(quaternionHelper), scene, pointerId, pointerType, pointerState, pointerCapture, options); | ||
pointerMap.set(pointerId, (innerPointer = new Pointer(generateUniquePointerId(), pointerType, pointerState, computeIntersection, undefined, forwardPointerCapture ? setPointerCapture.bind(null, pointerId) : undefined, forwardPointerCapture ? releasePointerCapture.bind(null, pointerId) : undefined, options))); | ||
pointerMap.set(pointerId, (innerPointer = new Pointer(generateUniquePointerId(), `${pointerTypePrefix}${pointerType}`, pointerState, new CameraRayIntersector((nativeEvent, coords) => { | ||
toCoords(nativeEvent, coords); | ||
return toCamera; | ||
}, options), undefined, forwardPointerCapture ? setPointerCapture.bind(null, pointerId) : undefined, forwardPointerCapture ? releasePointerCapture.bind(null, pointerId) : undefined, options))); | ||
return innerPointer; | ||
@@ -57,0 +54,0 @@ }; |
@@ -13,4 +13,4 @@ import { Intersection as ThreeIntersection, Quaternion, Vector3 } from 'three'; | ||
type: 'lines'; | ||
distanceOnLine: number; | ||
lineIndex: number; | ||
distanceOnLine: number; | ||
} | { | ||
@@ -17,0 +17,0 @@ type: 'camera-ray'; |
import { Matrix4, Vector3, Object3D } from 'three'; | ||
import { Intersection, IntersectionOptions } from './index.js'; | ||
import type { PointerCapture } from '../pointer.js'; | ||
export declare function intersectLines(fromMatrixWorld: Matrix4, linePoints: Array<Vector3>, scene: Object3D, pointerId: number, pointerType: string, pointerState: unknown, pointerCapture: PointerCapture | undefined, options: IntersectionOptions | undefined): Intersection | undefined; | ||
import { Intersector } from './intersector.js'; | ||
import { Intersection, IntersectionOptions } from '../index.js'; | ||
export declare class LinesIntersector extends Intersector { | ||
private readonly prepareTransformation; | ||
private readonly options; | ||
private raycasters; | ||
private fromMatrixWorld; | ||
private intersectionLineIndex; | ||
private intersectionDistanceOnLine; | ||
constructor(prepareTransformation: (nativeEvent: unknown, fromMatrixWorld: Matrix4) => boolean, options: IntersectionOptions & { | ||
linePoints?: Array<Vector3>; | ||
minDistance?: number; | ||
}); | ||
intersectPointerCapture({ intersection, object }: PointerCapture, nativeEvent: unknown): Intersection | undefined; | ||
protected prepareIntersection(nativeEvent: unknown): boolean; | ||
executeIntersection(object: Object3D, objectPointerEventsOrder: number | undefined): void; | ||
finalizeIntersection(): Intersection | undefined; | ||
} |
import { Line3, Matrix4, Plane, Quaternion, Ray, Raycaster, Vector3, } from 'three'; | ||
import { computeIntersectionWorldPlane, getDominantIntersectionIndex, traversePointerEventTargets } from './utils.js'; | ||
const raycaster = new Raycaster(); | ||
import { computeIntersectionWorldPlane, getDominantIntersectionIndex } from './utils.js'; | ||
import { Intersector } from './intersector.js'; | ||
const invertedMatrixHelper = new Matrix4(); | ||
const intersectsHelper = []; | ||
export function intersectLines(fromMatrixWorld, linePoints, scene, pointerId, pointerType, pointerState, pointerCapture, options) { | ||
if (pointerCapture != null) { | ||
return intersectLinesPointerCapture(fromMatrixWorld, linePoints, pointerCapture); | ||
const lineHelper = new Line3(); | ||
const planeHelper = new Plane(); | ||
const rayHelper = new Ray(); | ||
const defaultLinePoints = [new Vector3(0, 0, 0), new Vector3(0, 0, 1)]; | ||
export class LinesIntersector extends Intersector { | ||
prepareTransformation; | ||
options; | ||
raycasters = []; | ||
fromMatrixWorld = new Matrix4(); | ||
intersectionLineIndex = 0; | ||
intersectionDistanceOnLine = 0; | ||
constructor(prepareTransformation, options) { | ||
super(); | ||
this.prepareTransformation = prepareTransformation; | ||
this.options = options; | ||
} | ||
let intersection; | ||
let pointerEventsOrder; | ||
traversePointerEventTargets(scene, pointerId, pointerType, pointerState, (object, objectPointerEventsOrder) => { | ||
let prevAccLineLength = 0; | ||
const length = (intersection?.details.lineIndex ?? linePoints.length - 2) + 2; | ||
for (let i = 1; i < length; i++) { | ||
const start = linePoints[i - 1]; | ||
const end = linePoints[i]; | ||
intersectPointerCapture({ intersection, object }, nativeEvent) { | ||
const details = intersection.details; | ||
if (details.type != 'lines') { | ||
return undefined; | ||
} | ||
if (!this.prepareTransformation(nativeEvent, this.fromMatrixWorld)) { | ||
return undefined; | ||
} | ||
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()); | ||
computeIntersectionWorldPlane(planeHelper, intersection, object); | ||
const pointOnFace = rayHelper.intersectPlane(planeHelper, new Vector3()) ?? point; | ||
return { | ||
...intersection, | ||
pointOnFace, | ||
point, | ||
pointerPosition: new Vector3().setFromMatrixPosition(this.fromMatrixWorld), | ||
pointerQuaternion: new Quaternion().setFromRotationMatrix(this.fromMatrixWorld), | ||
}; | ||
} | ||
prepareIntersection(nativeEvent) { | ||
if (!this.prepareTransformation(nativeEvent, this.fromMatrixWorld)) { | ||
return false; | ||
} | ||
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(fromMatrixWorld); | ||
raycaster.ray.direction.copy(end).applyMatrix4(fromMatrixWorld); | ||
raycaster.ray.origin.copy(start).applyMatrix4(this.fromMatrixWorld); | ||
raycaster.ray.direction.copy(end).applyMatrix4(this.fromMatrixWorld); | ||
//compute length & normalized direction | ||
@@ -26,74 +61,46 @@ raycaster.ray.direction.sub(raycaster.ray.origin); | ||
raycaster.far = lineLength; | ||
} | ||
this.raycasters.length = length; | ||
return true; | ||
} | ||
executeIntersection(object, objectPointerEventsOrder) { | ||
let lineLengthSum = 0; | ||
const length = this.raycasters.length; | ||
//TODO: optimize - we only need to intersect with raycasters before or equal to the raycaster that did the current intersection | ||
for (let i = 0; i < length; i++) { | ||
const raycaster = this.raycasters[i]; | ||
object.raycast(raycaster, intersectsHelper); | ||
//we're adding the details and the prev acc line length so that the intersections are correctly sorted | ||
const length = intersectsHelper.length; | ||
for (let intersectionIndex = 0; intersectionIndex < length; intersectionIndex++) { | ||
const int = intersectsHelper[intersectionIndex]; | ||
const distanceOnLine = int.distance; | ||
int.distance += prevAccLineLength; | ||
Object.assign(int, { | ||
details: { | ||
lineIndex: i - 1, | ||
distanceOnLine, | ||
}, | ||
}); | ||
for (const intersection of intersectsHelper) { | ||
intersection.distance += lineLengthSum; | ||
} | ||
const index = getDominantIntersectionIndex(intersection, pointerEventsOrder, intersectsHelper, objectPointerEventsOrder, options); | ||
const index = getDominantIntersectionIndex(this.intersection, this.pointerEventsOrder, intersectsHelper, objectPointerEventsOrder, this.options); | ||
if (index != null) { | ||
intersection = intersectsHelper[index]; | ||
pointerEventsOrder = objectPointerEventsOrder; | ||
this.intersection = intersectsHelper[index]; | ||
this.intersectionLineIndex = i; | ||
this.intersectionDistanceOnLine = this.intersection.distance - raycaster.far; | ||
this.pointerEventsOrder = objectPointerEventsOrder; | ||
} | ||
intersectsHelper.length = 0; | ||
prevAccLineLength += lineLength; | ||
lineLengthSum += raycaster.far; | ||
} | ||
}); | ||
if (intersection == null) { | ||
return undefined; | ||
} | ||
return Object.assign(intersection, { | ||
details: { | ||
...intersection.details, | ||
type: 'lines', | ||
}, | ||
pointerPosition: new Vector3().setFromMatrixPosition(fromMatrixWorld), | ||
pointerQuaternion: new Quaternion().setFromRotationMatrix(fromMatrixWorld), | ||
pointOnFace: intersection.point, | ||
localPoint: intersection.point | ||
.clone() | ||
.applyMatrix4(invertedMatrixHelper.copy(intersection.object.matrixWorld).invert()), | ||
}); | ||
} | ||
const lineHelper = new Line3(); | ||
const planeHelper = new Plane(); | ||
function intersectLinesPointerCapture(fromMatrixWorld, linePoints, { intersection, object }) { | ||
const details = intersection.details; | ||
if (details.type != 'lines') { | ||
return undefined; | ||
} | ||
lineHelper.set(linePoints[details.lineIndex], linePoints[details.lineIndex + 1]).applyMatrix4(fromMatrixWorld); | ||
const point = lineHelper.at(details.distanceOnLine / lineHelper.distance(), new Vector3()); | ||
computeIntersectionWorldPlane(planeHelper, intersection, object); | ||
const pointOnFace = backwardsIntersectionLinesWithPlane(fromMatrixWorld, linePoints, planeHelper) ?? point; | ||
return { | ||
...intersection, | ||
pointOnFace, | ||
point, | ||
pointerPosition: new Vector3().setFromMatrixPosition(fromMatrixWorld), | ||
pointerQuaternion: new Quaternion().setFromRotationMatrix(fromMatrixWorld), | ||
}; | ||
} | ||
const vectorHelper = new Vector3(); | ||
const rayHelper = new Ray(); | ||
function backwardsIntersectionLinesWithPlane(fromMatrixWorld, linePoints, plane) { | ||
for (let i = linePoints.length - 1; i > 0; i--) { | ||
const start = linePoints[i - 1]; | ||
const end = linePoints[i]; | ||
rayHelper.origin.copy(start).applyMatrix4(fromMatrixWorld); | ||
rayHelper.direction.copy(end).applyMatrix4(fromMatrixWorld).sub(raycaster.ray.origin).normalize(); | ||
const point = rayHelper.intersectPlane(plane, vectorHelper); | ||
if (point != null) { | ||
return vectorHelper.clone(); | ||
finalizeIntersection() { | ||
if (this.intersection == null) { | ||
return undefined; | ||
} | ||
//TODO: consider maxLength | ||
return Object.assign(this.intersection, { | ||
details: { | ||
lineIndex: this.intersectionLineIndex, | ||
distanceOnLine: this.intersectionDistanceOnLine, | ||
type: 'lines', | ||
}, | ||
pointerPosition: new Vector3().setFromMatrixPosition(this.fromMatrixWorld), | ||
pointerQuaternion: new Quaternion().setFromRotationMatrix(this.fromMatrixWorld), | ||
pointOnFace: this.intersection.point, | ||
localPoint: this.intersection.point | ||
.clone() | ||
.applyMatrix4(invertedMatrixHelper.copy(this.intersection.object.matrixWorld).invert()), | ||
}); | ||
} | ||
return undefined; | ||
} |
@@ -1,5 +0,33 @@ | ||
import { Camera, Quaternion, Vector2, Vector3, Object3D } from 'three'; | ||
import { Matrix4, Vector3, Object3D, Camera, Vector2 } from 'three'; | ||
import { Intersection, IntersectionOptions } from './index.js'; | ||
import type { PointerCapture } from '../pointer.js'; | ||
export declare function intersectRay(fromPosition: Vector3, fromQuaternion: Quaternion, direction: Vector3, scene: Object3D, pointerId: number, pointerType: string, pointerState: unknown, pointerCapture: PointerCapture | undefined, options: IntersectionOptions | undefined): Intersection | undefined; | ||
export declare function intersectRayFromCamera(from: Camera, coords: Vector2, fromPosition: Vector3, fromQuaternion: Quaternion, scene: Object3D, pointerId: number, pointerType: string, pointerState: unknown, pointerCapture: PointerCapture | undefined, options: IntersectionOptions | undefined): Intersection | undefined; | ||
import { type PointerCapture } from '../pointer.js'; | ||
import { Intersector } from './intersector.js'; | ||
export declare class RayIntersector extends Intersector { | ||
private readonly prepareTransformation; | ||
private readonly options; | ||
private readonly raycaster; | ||
private readonly raycasterQuaternion; | ||
private worldScale; | ||
constructor(prepareTransformation: (nativeEvent: unknown, matrixWorld: Matrix4) => boolean, options: IntersectionOptions & { | ||
minDistance?: number; | ||
direction?: Vector3; | ||
}); | ||
intersectPointerCapture({ intersection, object }: PointerCapture, nativeEvent: unknown): Intersection | undefined; | ||
protected prepareIntersection(nativeEvent: unknown): boolean; | ||
executeIntersection(object: Object3D, objectPointerEventsOrder: number | undefined): void; | ||
finalizeIntersection(): Intersection | undefined; | ||
} | ||
export declare class CameraRayIntersector extends Intersector { | ||
private readonly prepareTransformation; | ||
private readonly options; | ||
private readonly raycaster; | ||
private readonly fromPosition; | ||
private readonly fromQuaternion; | ||
private readonly coords; | ||
private viewPlane; | ||
constructor(prepareTransformation: (nativeEvent: unknown, coords: Vector2) => Camera | undefined, options: IntersectionOptions); | ||
intersectPointerCapture({ intersection, object }: PointerCapture, nativeEvent: unknown): Intersection | undefined; | ||
protected prepareIntersection(nativeEvent: unknown): boolean; | ||
executeIntersection(object: Object3D, objectPointerEventsOrder: number | undefined): void; | ||
finalizeIntersection(): Intersection | undefined; | ||
} |
@@ -1,115 +0,153 @@ | ||
import { Matrix4, Plane, Ray, Raycaster, Vector3, } from 'three'; | ||
import { computeIntersectionWorldPlane, getDominantIntersectionIndex, traversePointerEventTargets } from './utils.js'; | ||
const raycaster = new Raycaster(); | ||
import { Matrix4, Plane, Quaternion, Raycaster, Vector3, Vector2, } from 'three'; | ||
import { computeIntersectionWorldPlane, getDominantIntersectionIndex } from './utils.js'; | ||
import { Intersector } from './intersector.js'; | ||
const invertedMatrixHelper = new Matrix4(); | ||
const intersectsHelper = []; | ||
const matrixHelper = new Matrix4(); | ||
const scaleHelper = new Vector3(); | ||
const NegZAxis = new Vector3(0, 0, -1); | ||
const directionHelper = new Vector3(); | ||
const planeHelper = new Plane(); | ||
const invertedMatrixHelper = new Matrix4(); | ||
const intersectsHelper = []; | ||
export function intersectRay(fromPosition, fromQuaternion, direction, scene, pointerId, pointerType, pointerState, pointerCapture, options) { | ||
if (pointerCapture != null) { | ||
return intersectRayPointerCapture(fromPosition, fromQuaternion, direction, pointerCapture); | ||
export class RayIntersector extends Intersector { | ||
prepareTransformation; | ||
options; | ||
raycaster = new Raycaster(); | ||
raycasterQuaternion = new Quaternion(); | ||
worldScale = 0; | ||
constructor(prepareTransformation, options) { | ||
super(); | ||
this.prepareTransformation = prepareTransformation; | ||
this.options = options; | ||
} | ||
let intersection; | ||
let pointerEventsOrder; | ||
raycaster.ray.origin.copy(fromPosition); | ||
raycaster.ray.direction.copy(direction).applyQuaternion(fromQuaternion); | ||
traversePointerEventTargets(scene, pointerId, pointerType, pointerState, (object, objectPointerEventsOrder) => { | ||
object.raycast(raycaster, intersectsHelper); | ||
const index = getDominantIntersectionIndex(intersection, pointerEventsOrder, intersectsHelper, objectPointerEventsOrder, options); | ||
intersectPointerCapture({ intersection, object }, nativeEvent) { | ||
if (intersection.details.type != 'ray') { | ||
return undefined; | ||
} | ||
if (!this.prepareIntersection(nativeEvent)) { | ||
return undefined; | ||
} | ||
computeIntersectionWorldPlane(planeHelper, intersection, object); | ||
const { ray } = this.raycaster; | ||
const pointOnFace = ray.intersectPlane(planeHelper, new Vector3()) ?? intersection.point; | ||
return { | ||
...intersection, | ||
object, | ||
pointOnFace, | ||
point: ray.direction.clone().multiplyScalar(intersection.distance).add(ray.origin), | ||
pointerPosition: ray.origin.clone(), | ||
pointerQuaternion: this.raycasterQuaternion.clone(), | ||
}; | ||
} | ||
prepareIntersection(nativeEvent) { | ||
if (!this.prepareTransformation(nativeEvent, matrixHelper)) { | ||
return false; | ||
} | ||
matrixHelper.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; | ||
} | ||
executeIntersection(object, objectPointerEventsOrder) { | ||
object.raycast(this.raycaster, intersectsHelper); | ||
const index = getDominantIntersectionIndex(this.intersection, this.pointerEventsOrder, intersectsHelper, objectPointerEventsOrder, this.options); | ||
if (index != null) { | ||
intersection = intersectsHelper[index]; | ||
pointerEventsOrder = objectPointerEventsOrder; | ||
this.intersection = intersectsHelper[index]; | ||
this.pointerEventsOrder = objectPointerEventsOrder; | ||
} | ||
intersectsHelper.length = 0; | ||
}); | ||
if (intersection == null) { | ||
return undefined; | ||
} | ||
return Object.assign(intersection, { | ||
details: { | ||
type: 'ray', | ||
}, | ||
pointerPosition: fromPosition.clone(), | ||
pointerQuaternion: fromQuaternion.clone(), | ||
pointOnFace: intersection.point, | ||
localPoint: intersection.point | ||
.clone() | ||
.applyMatrix4(invertedMatrixHelper.copy(intersection.object.matrixWorld).invert()), | ||
}); | ||
} | ||
const rayHelper = new Ray(); | ||
function intersectRayPointerCapture(fromPosition, fromQuaternion, direction, { intersection, object }) { | ||
if (intersection.details.type != 'ray') { | ||
return undefined; | ||
finalizeIntersection() { | ||
if (this.intersection == null) { | ||
return undefined; | ||
} | ||
if (this.options.minDistance != null && this.intersection.distance * this.worldScale < this.options.minDistance) { | ||
return undefined; | ||
} | ||
return Object.assign(this.intersection, { | ||
details: { | ||
type: 'ray', | ||
}, | ||
pointerPosition: this.raycaster.ray.origin.clone(), | ||
pointerQuaternion: this.raycasterQuaternion.clone(), | ||
pointOnFace: this.intersection.point, | ||
localPoint: this.intersection.point | ||
.clone() | ||
.applyMatrix4(invertedMatrixHelper.copy(this.intersection.object.matrixWorld).invert()), | ||
}); | ||
} | ||
directionHelper.copy(direction).applyQuaternion(fromQuaternion); | ||
rayHelper.set(fromPosition, directionHelper); | ||
computeIntersectionWorldPlane(planeHelper, intersection, object); | ||
const pointOnFace = rayHelper.intersectPlane(planeHelper, new Vector3()) ?? intersection.point; | ||
return { | ||
...intersection, | ||
object, | ||
pointOnFace, | ||
point: directionHelper.clone().multiplyScalar(intersection.distance).add(fromPosition), | ||
pointerPosition: fromPosition.clone(), | ||
pointerQuaternion: fromQuaternion.clone(), | ||
}; | ||
} | ||
export function intersectRayFromCamera(from, coords, fromPosition, fromQuaternion, scene, pointerId, pointerType, pointerState, pointerCapture, options) { | ||
if (pointerCapture != null) { | ||
return intersectRayFromCameraPointerCapture(from, coords, fromPosition, fromQuaternion, pointerCapture); | ||
export class CameraRayIntersector extends Intersector { | ||
prepareTransformation; | ||
options; | ||
raycaster = new Raycaster(); | ||
fromPosition = new Vector3(); | ||
fromQuaternion = new Quaternion(); | ||
coords = new Vector2(); | ||
viewPlane = new Plane(); | ||
constructor(prepareTransformation, options) { | ||
super(); | ||
this.prepareTransformation = prepareTransformation; | ||
this.options = options; | ||
} | ||
let intersection; | ||
let pointerEventsOrder; | ||
raycaster.setFromCamera(coords, from); | ||
planeHelper.setFromNormalAndCoplanarPoint(from.getWorldDirection(directionHelper), raycaster.ray.origin); | ||
traversePointerEventTargets(scene, pointerId, pointerType, pointerState, (object, objectPointerEventsOrder) => { | ||
object.raycast(raycaster, intersectsHelper); | ||
const index = getDominantIntersectionIndex(intersection, pointerEventsOrder, intersectsHelper, objectPointerEventsOrder, options); | ||
intersectPointerCapture({ intersection, object }, nativeEvent) { | ||
const details = intersection.details; | ||
if (details.type != 'camera-ray') { | ||
return undefined; | ||
} | ||
if (!this.prepareIntersection(nativeEvent)) { | ||
return undefined; | ||
} | ||
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 undefined; | ||
} | ||
computeIntersectionWorldPlane(this.viewPlane, intersection, object); | ||
const pointOnFace = this.raycaster.ray.intersectPlane(this.viewPlane, new Vector3()) ?? point; | ||
return { | ||
...intersection, | ||
object, | ||
point, | ||
pointOnFace, | ||
pointerPosition: this.fromPosition.clone(), | ||
pointerQuaternion: this.fromQuaternion.clone(), | ||
}; | ||
} | ||
prepareIntersection(nativeEvent) { | ||
const from = this.prepareTransformation(nativeEvent, this.coords); | ||
if (from == null) { | ||
return false; | ||
} | ||
from.matrixWorld.decompose(this.fromPosition, this.fromQuaternion, scaleHelper); | ||
from.updateWorldMatrix(true, false); | ||
this.raycaster.setFromCamera(this.coords, from); | ||
this.viewPlane.setFromNormalAndCoplanarPoint(from.getWorldDirection(directionHelper), this.raycaster.ray.origin); | ||
return true; | ||
} | ||
executeIntersection(object, objectPointerEventsOrder) { | ||
object.raycast(this.raycaster, intersectsHelper); | ||
const index = getDominantIntersectionIndex(this.intersection, this.pointerEventsOrder, intersectsHelper, objectPointerEventsOrder, this.options); | ||
if (index != null) { | ||
intersection = intersectsHelper[index]; | ||
pointerEventsOrder = objectPointerEventsOrder; | ||
this.intersection = intersectsHelper[index]; | ||
this.pointerEventsOrder = objectPointerEventsOrder; | ||
} | ||
intersectsHelper.length = 0; | ||
}); | ||
if (intersection == null) { | ||
return undefined; | ||
} | ||
invertedMatrixHelper.copy(intersection.object.matrixWorld).invert(); | ||
return Object.assign(intersection, { | ||
details: { | ||
type: 'camera-ray', | ||
distanceViewPlane: planeHelper.distanceToPoint(intersection.point), | ||
}, | ||
pointOnFace: intersection.point, | ||
pointerPosition: fromPosition.clone(), | ||
pointerQuaternion: fromQuaternion.clone(), | ||
localPoint: intersection.point.clone().applyMatrix4(invertedMatrixHelper), | ||
}); | ||
} | ||
function intersectRayFromCameraPointerCapture(from, coords, fromPosition, fromQuaternion, { intersection, object }) { | ||
const details = intersection.details; | ||
if (details.type != 'camera-ray') { | ||
return undefined; | ||
finalizeIntersection() { | ||
if (this.intersection == null) { | ||
return undefined; | ||
} | ||
invertedMatrixHelper.copy(this.intersection.object.matrixWorld).invert(); | ||
return Object.assign(this.intersection, { | ||
details: { | ||
type: 'camera-ray', | ||
distanceViewPlane: this.viewPlane.distanceToPoint(this.intersection.point), | ||
}, | ||
pointOnFace: this.intersection.point, | ||
pointerPosition: this.fromPosition.clone(), | ||
pointerQuaternion: this.fromQuaternion.clone(), | ||
localPoint: this.intersection.point.clone().applyMatrix4(invertedMatrixHelper), | ||
}); | ||
} | ||
raycaster.setFromCamera(coords, from); | ||
from.getWorldDirection(directionHelper); | ||
//set the plane to the viewPlane + the distance of the prev intersection in the camera distance | ||
planeHelper.setFromNormalAndCoplanarPoint(directionHelper, raycaster.ray.origin); | ||
planeHelper.constant -= details.distanceViewPlane; | ||
//find captured intersection point by intersecting the ray to the plane of the camera | ||
const point = raycaster.ray.intersectPlane(planeHelper, new Vector3()); | ||
if (point == null) { | ||
return undefined; | ||
} | ||
computeIntersectionWorldPlane(planeHelper, intersection, object); | ||
const pointOnFace = raycaster.ray.intersectPlane(planeHelper, new Vector3()) ?? point; | ||
return { | ||
...intersection, | ||
object, | ||
point, | ||
pointOnFace, | ||
pointerPosition: fromPosition.clone(), | ||
pointerQuaternion: fromQuaternion.clone(), | ||
}; | ||
} |
import { Object3D, Vector3, Quaternion } from 'three'; | ||
import { Intersection, IntersectionOptions } from './index.js'; | ||
import type { PointerCapture } from '../pointer.js'; | ||
export declare function intersectSphere(fromPosition: Vector3, fromQuaternion: Quaternion, radius: number, scene: Object3D, pointerId: number, pointerType: string, pointerState: unknown, pointerCapture: PointerCapture | undefined, options: IntersectionOptions | undefined): Intersection | undefined; | ||
import { Intersector } from './intersector.js'; | ||
import { Intersection, IntersectionOptions } from '../index.js'; | ||
export declare class SphereIntersector extends Intersector { | ||
/** | ||
* @returns the sphere radius | ||
*/ | ||
private readonly prepareTransformation; | ||
private readonly options; | ||
private readonly fromPosition; | ||
private readonly fromQuaternion; | ||
constructor( | ||
/** | ||
* @returns the sphere radius | ||
*/ | ||
prepareTransformation: (nativeEvent: unknown, fromPosition: Vector3, fromQuaternion: Quaternion) => number | undefined, options: IntersectionOptions); | ||
intersectPointerCapture({ intersection, object }: PointerCapture, nativeEvent: unknown): Intersection | undefined; | ||
protected prepareIntersection(nativeEvent: unknown): boolean; | ||
executeIntersection(object: Object3D, objectPointerEventsOrder: number | undefined): void; | ||
finalizeIntersection(): Intersection | undefined; | ||
} |
import { InstancedMesh, Matrix4, Mesh, Vector3, Sphere, Quaternion, Plane, } from 'three'; | ||
import { computeIntersectionWorldPlane, getDominantIntersectionIndex, traversePointerEventTargets } from './utils.js'; | ||
import { computeIntersectionWorldPlane, getDominantIntersectionIndex } from './utils.js'; | ||
import { Intersector } from './intersector.js'; | ||
const collisionSphere = new Sphere(); | ||
const intersectsHelper = []; | ||
export function intersectSphere(fromPosition, fromQuaternion, radius, scene, pointerId, pointerType, pointerState, pointerCapture, options) { | ||
if (pointerCapture != null) { | ||
return intersectSpherePointerCapture(fromPosition, fromQuaternion, pointerCapture); | ||
export class SphereIntersector extends Intersector { | ||
prepareTransformation; | ||
options; | ||
fromPosition = new Vector3(); | ||
fromQuaternion = new Quaternion(); | ||
constructor( | ||
/** | ||
* @returns the sphere radius | ||
*/ | ||
prepareTransformation, options) { | ||
super(); | ||
this.prepareTransformation = prepareTransformation; | ||
this.options = options; | ||
} | ||
let intersection; | ||
let pointerEventsOrder; | ||
collisionSphere.center.copy(fromPosition); | ||
collisionSphere.radius = radius; | ||
traversePointerEventTargets(scene, pointerId, pointerType, pointerState, (object, objectPointerEventsOrder) => { | ||
intersectPointerCapture({ intersection, object }, nativeEvent) { | ||
if (intersection.details.type != 'sphere') { | ||
return undefined; | ||
} | ||
if (this.prepareTransformation(nativeEvent, this.fromPosition, this.fromQuaternion) == null) { | ||
return undefined; | ||
} | ||
//compute old inputDevicePosition-point offset | ||
oldInputDevicePointOffset.copy(intersection.point).sub(intersection.pointerPosition); | ||
//compute oldInputDeviceQuaternion-newInputDeviceQuaternion offset | ||
inputDeviceQuaternionOffset.copy(intersection.pointerQuaternion).invert().multiply(this.fromQuaternion); | ||
//apply quaternion offset to old inputDevicePosition-point offset and add to new inputDevicePosition | ||
const point = oldInputDevicePointOffset.clone().applyQuaternion(inputDeviceQuaternionOffset).add(this.fromPosition); | ||
computeIntersectionWorldPlane(planeHelper, intersection, object); | ||
const pointOnFace = planeHelper.projectPoint(this.fromPosition, new Vector3()); | ||
return { | ||
details: { | ||
type: 'sphere', | ||
}, | ||
distance: intersection.distance, | ||
pointerPosition: this.fromPosition.clone(), | ||
pointerQuaternion: this.fromQuaternion.clone(), | ||
object, | ||
point, | ||
pointOnFace, | ||
face: intersection.face, | ||
localPoint: intersection.localPoint, | ||
}; | ||
} | ||
prepareIntersection(nativeEvent) { | ||
const radius = this.prepareTransformation(nativeEvent, this.fromPosition, this.fromQuaternion); | ||
if (radius == null) { | ||
return false; | ||
} | ||
collisionSphere.center.copy(this.fromPosition); | ||
collisionSphere.radius = radius; | ||
return true; | ||
} | ||
executeIntersection(object, objectPointerEventsOrder) { | ||
intersectSphereWithObject(collisionSphere, object, intersectsHelper); | ||
const index = getDominantIntersectionIndex(intersection, pointerEventsOrder, intersectsHelper, objectPointerEventsOrder, options); | ||
const index = getDominantIntersectionIndex(this.intersection, this.pointerEventsOrder, intersectsHelper, objectPointerEventsOrder, this.options); | ||
if (index != null) { | ||
intersection = intersectsHelper[index]; | ||
pointerEventsOrder = objectPointerEventsOrder; | ||
this.intersection = intersectsHelper[index]; | ||
this.pointerEventsOrder = objectPointerEventsOrder; | ||
} | ||
intersectsHelper.length = 0; | ||
}); | ||
if (intersection == null) { | ||
return undefined; | ||
} | ||
return Object.assign(intersection, { | ||
details: { | ||
type: 'sphere', | ||
}, | ||
pointOnFace: intersection.point, | ||
pointerPosition: fromPosition.clone(), | ||
pointerQuaternion: fromQuaternion.clone(), | ||
localPoint: intersection.point | ||
.clone() | ||
.applyMatrix4(invertedMatrixHelper.copy(intersection.object.matrixWorld).invert()), | ||
}); | ||
finalizeIntersection() { | ||
if (this.intersection == null) { | ||
return undefined; | ||
} | ||
return Object.assign(this.intersection, { | ||
details: { | ||
type: 'sphere', | ||
}, | ||
pointOnFace: this.intersection.point, | ||
pointerPosition: this.fromPosition.clone(), | ||
pointerQuaternion: this.fromQuaternion.clone(), | ||
localPoint: this.intersection.point | ||
.clone() | ||
.applyMatrix4(invertedMatrixHelper.copy(this.intersection.object.matrixWorld).invert()), | ||
}); | ||
} | ||
} | ||
@@ -83,28 +130,2 @@ const matrixHelper = new Matrix4(); | ||
const planeHelper = new Plane(); | ||
function intersectSpherePointerCapture(fromPosition, fromQuaterion, { intersection, object }) { | ||
if (intersection.details.type != 'sphere') { | ||
return undefined; | ||
} | ||
//compute old inputDevicePosition-point offset | ||
oldInputDevicePointOffset.copy(intersection.point).sub(intersection.pointerPosition); | ||
//compute oldInputDeviceQuaternion-newInputDeviceQuaternion offset | ||
inputDeviceQuaternionOffset.copy(intersection.pointerQuaternion).invert().multiply(fromQuaterion); | ||
//apply quaternion offset to old inputDevicePosition-point offset and add to new inputDevicePosition | ||
const point = oldInputDevicePointOffset.clone().applyQuaternion(inputDeviceQuaternionOffset).add(fromPosition); | ||
computeIntersectionWorldPlane(planeHelper, intersection, object); | ||
const pointOnFace = planeHelper.projectPoint(fromPosition, new Vector3()); | ||
return { | ||
details: { | ||
type: 'sphere', | ||
}, | ||
distance: intersection.distance, | ||
pointerPosition: fromPosition.clone(), | ||
pointerQuaternion: fromQuaterion.clone(), | ||
object, | ||
point, | ||
pointOnFace, | ||
face: intersection.face, | ||
localPoint: intersection.localPoint, | ||
}; | ||
} | ||
const helperSphere = new Sphere(); | ||
@@ -111,0 +132,0 @@ function isSphereIntersectingMesh(pointerSphere, { geometry }, meshMatrixWorld) { |
import { Plane, Intersection as ThreeIntersection, Object3D } from 'three'; | ||
import { Intersection, IntersectionOptions } from './index.js'; | ||
import { AllowedPointerEventsType, type AllowedPointerEvents } from '../pointer.js'; | ||
import { AllowedPointerEventsType, Pointer, type AllowedPointerEvents } from '../pointer.js'; | ||
export declare function computeIntersectionWorldPlane(target: Plane, intersection: Intersection, object: Object3D): boolean; | ||
export declare function traversePointerEventTargets(object: Object3D, pointerId: number, pointerType: string, pointerState: unknown, callback: (object: Object3D, pointerEventsOrder: number | undefined) => void, parentHasListener?: boolean, parentPointerEvents?: AllowedPointerEvents, parentPointerEventsType?: AllowedPointerEventsType, parentPointerEventsOrder?: number): void; | ||
export declare function intersectPointerEventTargets(object: Object3D, pointers: Array<Pointer>, parentHasListener?: boolean, parentPointerEvents?: AllowedPointerEvents, parentPointerEventsType?: AllowedPointerEventsType, parentPointerEventsOrder?: number): void; | ||
/** | ||
* @returns undefined if `i1` is the dominant intersection | ||
* @param i2DistanceOffset modifies i2 and adds the i2DistanceOffset to the current distance | ||
*/ | ||
export declare function getDominantIntersectionIndex<T extends ThreeIntersection>(i1: T | undefined, pointerEventsOrder1: number | undefined, i2: Array<T>, pointerEventsOrder2: number | undefined, { customFilter, customSort: compare }?: IntersectionOptions): number | undefined; |
@@ -10,3 +10,3 @@ import { hasObjectListeners } from '../utils.js'; | ||
} | ||
function isPointerEventsAllowed(hasListener, pointerEvents, pointerEventsType, pointerId, pointerType, pointerState) { | ||
function isPointerEventsAllowed(hasListener, pointerEvents, pointerEventsType) { | ||
if (pointerEvents === 'none') { | ||
@@ -22,3 +22,3 @@ return false; | ||
if (typeof pointerEventsType === 'function') { | ||
return pointerEventsType(pointerId, pointerType, pointerState); | ||
return ({ id, type, state }) => pointerEventsType(id, type, state); | ||
} | ||
@@ -35,27 +35,39 @@ let value; | ||
} | ||
let result; | ||
if (Array.isArray(value)) { | ||
result = value.includes(pointerType); | ||
return (pointer) => invertIf(value.includes(pointer.type), invert); | ||
} | ||
else { | ||
result = value === pointerType; | ||
} | ||
return invert ? !result : result; | ||
return (pointer) => invertIf(value === pointer.type, invert); | ||
} | ||
export function traversePointerEventTargets(object, pointerId, pointerType, pointerState, callback, parentHasListener = false, parentPointerEvents, parentPointerEventsType, parentPointerEventsOrder) { | ||
function invertIf(toInvert, ifIsTrue) { | ||
return ifIsTrue ? !toInvert : toInvert; | ||
} | ||
export function intersectPointerEventTargets(object, pointers, parentHasListener = false, parentPointerEvents, parentPointerEventsType, parentPointerEventsOrder) { | ||
const hasListener = parentHasListener || hasObjectListeners(object); | ||
const pointerEvents = object.pointerEvents ?? parentPointerEvents; | ||
const pointerEventsType = object.pointerEventsType ?? parentPointerEventsType; | ||
const pointerEvents = object.pointerEvents ?? parentPointerEvents ?? 'listener'; | ||
const pointerEventsType = object.pointerEventsType ?? parentPointerEventsType ?? 'all'; | ||
const pointerEventsOrder = object.pointerEventsOrder ?? parentPointerEventsOrder; | ||
const isAllowed = isPointerEventsAllowed(hasListener, pointerEvents ?? 'listener', pointerEventsType ?? 'all', pointerId, pointerType, pointerState); | ||
if (isAllowed) { | ||
callback(object, pointerEventsOrder); | ||
const isAllowed = isPointerEventsAllowed(hasListener, pointerEvents, pointerEventsType); | ||
const length = pointers.length; | ||
if (isAllowed === true) { | ||
for (let i = 0; i < length; i++) { | ||
pointers[i].intersector.executeIntersection(object, pointerEventsOrder); | ||
} | ||
} | ||
const length = object.children.length; | ||
for (let i = 0; i < length; i++) { | ||
traversePointerEventTargets(object.children[i], pointerId, pointerType, pointerState, callback, hasListener, pointerEvents, pointerEventsType, pointerEventsOrder); | ||
else if (typeof isAllowed === 'function') { | ||
for (let i = 0; i < length; i++) { | ||
const pointer = pointers[i]; | ||
if (!isAllowed(pointer)) { | ||
continue; | ||
} | ||
pointers[i].intersector.executeIntersection(object, pointerEventsOrder); | ||
} | ||
} | ||
const childrenLength = object.children.length; | ||
for (let i = 0; i < childrenLength; i++) { | ||
intersectPointerEventTargets(object.children[i], pointers, hasListener, pointerEvents, pointerEventsType, pointerEventsOrder); | ||
} | ||
} | ||
/** | ||
* @returns undefined if `i1` is the dominant intersection | ||
* @param i2DistanceOffset modifies i2 and adds the i2DistanceOffset to the current distance | ||
*/ | ||
@@ -62,0 +74,0 @@ export function getDominantIntersectionIndex(i1, pointerEventsOrder1, i2, pointerEventsOrder2, { customFilter, customSort: compare = defaultSort } = {}) { |
import { Object3D } from 'three'; | ||
import { Intersection } from './intersections/index.js'; | ||
import { NativeEvent, NativeWheelEvent } from './event.js'; | ||
import { Intersector } from './intersections/intersector.js'; | ||
declare const buttonsDownTimeKey: unique symbol; | ||
@@ -64,3 +65,3 @@ declare const buttonsClickTimeKey: unique symbol; | ||
readonly state: any; | ||
private readonly computeIntersection; | ||
readonly intersector: Intersector; | ||
private readonly onMoveCommited?; | ||
@@ -84,3 +85,4 @@ private readonly parentSetPointerCapture?; | ||
private onFirstMove; | ||
constructor(id: number, type: string, state: any, computeIntersection: (scene: Object3D, nativeEvent: unknown, pointerCapture: PointerCapture | undefined) => Intersection | undefined, onMoveCommited?: ((pointer: Pointer) => void) | undefined, parentSetPointerCapture?: (() => void) | undefined, parentReleasePointerCapture?: (() => void) | undefined, options?: PointerOptions); | ||
constructor(id: number, type: string, state: any, intersector: Intersector, onMoveCommited?: ((pointer: Pointer) => void) | undefined, parentSetPointerCapture?: (() => void) | undefined, parentReleasePointerCapture?: (() => void) | undefined, options?: PointerOptions); | ||
getPointerCapture(): PointerCapture | undefined; | ||
hasCaptured(object: Object3D): boolean; | ||
@@ -92,2 +94,4 @@ setCapture(object: Object3D | undefined): void; | ||
setEnabled(enabled: boolean, nativeEvent: NativeEvent, commit?: boolean): void; | ||
private computeIntersection; | ||
setIntersection(intersection: Intersection | undefined): void; | ||
/** | ||
@@ -94,0 +98,0 @@ * allows to separately compute and afterwards commit a move |
import { Object3D } from 'three'; | ||
import { PointerEvent, WheelEvent, emitPointerEvent } from './event.js'; | ||
import { intersectPointerEventTargets } from './intersections/utils.js'; | ||
const buttonsDownTimeKey = Symbol('buttonsDownTime'); | ||
@@ -26,3 +27,3 @@ const buttonsClickTimeKey = Symbol('buttonsClickTime'); | ||
state; | ||
computeIntersection; | ||
intersector; | ||
onMoveCommited; | ||
@@ -49,7 +50,7 @@ parentSetPointerCapture; | ||
onFirstMove = []; | ||
constructor(id, type, state, computeIntersection, onMoveCommited, parentSetPointerCapture, parentReleasePointerCapture, options = {}) { | ||
constructor(id, type, state, intersector, onMoveCommited, parentSetPointerCapture, parentReleasePointerCapture, options = {}) { | ||
this.id = id; | ||
this.type = type; | ||
this.state = state; | ||
this.computeIntersection = computeIntersection; | ||
this.intersector = intersector; | ||
this.onMoveCommited = onMoveCommited; | ||
@@ -61,2 +62,5 @@ this.parentSetPointerCapture = parentSetPointerCapture; | ||
} | ||
getPointerCapture() { | ||
return this.pointerCapture; | ||
} | ||
hasCaptured(object) { | ||
@@ -100,2 +104,13 @@ return this.pointerCapture?.object === object; | ||
} | ||
computeIntersection(scene, nativeEvent) { | ||
if (this.pointerCapture != null) { | ||
return this.intersector.intersectPointerCapture(this.pointerCapture, nativeEvent); | ||
} | ||
this.intersector.startIntersection(nativeEvent); | ||
intersectPointerEventTargets(scene, [this]); | ||
return this.intersector.finalizeIntersection(); | ||
} | ||
setIntersection(intersection) { | ||
this.intersection = intersection; | ||
} | ||
/** | ||
@@ -107,3 +122,3 @@ * allows to separately compute and afterwards commit a move | ||
computeMove(scene, nativeEvent) { | ||
this.intersection = this.computeIntersection(scene, nativeEvent, this.pointerCapture); | ||
this.intersection = this.computeIntersection(scene, nativeEvent); | ||
} | ||
@@ -237,3 +252,3 @@ commit(nativeEvent) { | ||
if (!useCurrentIntersection) { | ||
intersection = this.computeIntersection(scene, nativeEvent, this.pointerCapture); | ||
intersection = this.computeIntersection(scene, nativeEvent); | ||
} | ||
@@ -240,0 +255,0 @@ if (!this.wasMoved && useCurrentIntersection) { |
@@ -10,7 +10,4 @@ import { Object3D } from 'three'; | ||
} & PointerOptions & IntersectionOptions; | ||
export declare const defaultGrabPointerOptions: { | ||
radius: number; | ||
}; | ||
export declare function createGrabPointer(space: { | ||
current?: Object3D | null; | ||
}, pointerState: any, options?: GrabPointerOptions, pointerType?: string): Pointer; |
@@ -1,13 +0,6 @@ | ||
import { Quaternion, Vector3 } from 'three'; | ||
import { Pointer } from '../pointer.js'; | ||
import { intersectSphere } from '../intersections/sphere.js'; | ||
import { SphereIntersector } from '../intersections/sphere.js'; | ||
import { generateUniquePointerId } from './index.js'; | ||
export const defaultGrabPointerOptions = { | ||
radius: 0.07, | ||
}; | ||
export function createGrabPointer(space, pointerState, options = defaultGrabPointerOptions, pointerType = 'grab') { | ||
const fromPosition = new Vector3(); | ||
const fromQuaternion = new Quaternion(); | ||
const poinerId = generateUniquePointerId(); | ||
return new Pointer(poinerId, pointerType, pointerState, (scene, _, pointerCapture) => { | ||
export function createGrabPointer(space, pointerState, options = {}, pointerType = 'grab') { | ||
return new Pointer(generateUniquePointerId(), pointerType, pointerState, new SphereIntersector((_nativeEvent, fromPosition, fromQuaternion) => { | ||
const spaceObject = space.current; | ||
@@ -20,4 +13,4 @@ if (spaceObject == null) { | ||
fromQuaternion.setFromRotationMatrix(spaceObject.matrixWorld); | ||
return intersectSphere(fromPosition, fromQuaternion, options.radius ?? defaultGrabPointerOptions.radius, scene, poinerId, pointerType, pointerState, pointerCapture, options); | ||
}, undefined, undefined, undefined, options); | ||
return options.radius ?? 0.07; | ||
}, options), undefined, undefined, undefined, options); | ||
} |
export declare function generateUniquePointerId(): number; | ||
export * from './grab.js'; | ||
export * from './ray.js'; | ||
export * from './lines.js'; | ||
export * from './touch.js'; |
@@ -7,2 +7,3 @@ let pointerIdCounter = 23412; | ||
export * from './ray.js'; | ||
export * from './lines.js'; | ||
export * from './touch.js'; |
@@ -11,6 +11,2 @@ import { Object3D, Vector3 } from 'three'; | ||
/** | ||
* @default null | ||
*/ | ||
linePoints?: Array<Vector3> | null; | ||
/** | ||
* @default NegZAxis | ||
@@ -20,9 +16,4 @@ */ | ||
} & PointerOptions & IntersectionOptions; | ||
export declare const defaultRayPointerOptions: { | ||
direction: Vector3; | ||
minDistance: number; | ||
linePoints: null; | ||
}; | ||
export declare function createRayPointer(space: { | ||
current?: Object3D | null; | ||
}, pointerState: any, options?: RayPointerOptions, pointerType?: string): Pointer; |
@@ -1,41 +0,12 @@ | ||
import { Quaternion, Vector3 } from 'three'; | ||
import { Pointer } from '../pointer.js'; | ||
import { intersectLines, intersectRay } from '../intersections/index.js'; | ||
import { RayIntersector } from '../intersections/index.js'; | ||
import { generateUniquePointerId } from './index.js'; | ||
const NegZAxis = new Vector3(0, 0, -1); | ||
const vectorHelper = new Vector3(); | ||
export const defaultRayPointerOptions = { | ||
direction: NegZAxis, | ||
minDistance: 0, | ||
linePoints: null, | ||
}; | ||
export function createRayPointer(space, pointerState, options = defaultRayPointerOptions, pointerType = 'ray') { | ||
const fromPosition = new Vector3(); | ||
const fromQuaternion = new Quaternion(); | ||
const pointerId = generateUniquePointerId(); | ||
return new Pointer(pointerId, pointerType, pointerState, (scene, _, pointerCapture) => { | ||
const spaceObject = space.current; | ||
if (spaceObject == null) { | ||
return undefined; | ||
export function createRayPointer(space, pointerState, options = {}, pointerType = 'ray') { | ||
return new Pointer(generateUniquePointerId(), pointerType, pointerState, new RayIntersector((_nativeEvent, matrixWorld) => { | ||
if (space.current == null) { | ||
return false; | ||
} | ||
spaceObject.updateWorldMatrix(true, false); | ||
let intersection; | ||
const linePoints = options.linePoints ?? defaultRayPointerOptions.linePoints; | ||
if (linePoints == null) { | ||
fromPosition.setFromMatrixPosition(spaceObject.matrixWorld); | ||
fromQuaternion.setFromRotationMatrix(spaceObject.matrixWorld); | ||
intersection = intersectRay(fromPosition, fromQuaternion, options.direction ?? defaultRayPointerOptions.direction, scene, pointerId, pointerType, pointerState, pointerCapture, options); | ||
} | ||
else { | ||
intersection = intersectLines(spaceObject.matrixWorld, linePoints, scene, pointerId, pointerType, pointerState, pointerCapture, options); | ||
} | ||
if (intersection == null) { | ||
return undefined; | ||
} | ||
const localDistance = intersection.distance * spaceObject.getWorldScale(vectorHelper).x; | ||
if (localDistance < (options.minDistance ?? defaultRayPointerOptions.minDistance)) { | ||
return undefined; | ||
} | ||
return intersection; | ||
}, undefined, undefined, undefined, options); | ||
matrixWorld.copy(space.current.matrixWorld); | ||
return true; | ||
}, options), undefined, undefined, undefined, options); | ||
} |
@@ -18,9 +18,4 @@ import { Object3D } from 'three'; | ||
} & PointerOptions & IntersectionOptions; | ||
export declare const defaultTouchPointerOptions: { | ||
button: number; | ||
downRadius: number; | ||
hoverRadius: number; | ||
}; | ||
export declare function createTouchPointer(space: { | ||
current?: Object3D | null; | ||
}, pointerState: any, options?: TouchPointerOptions, pointerType?: string): Pointer; |
@@ -1,15 +0,6 @@ | ||
import { Quaternion, Vector3 } from 'three'; | ||
import { Pointer } from '../pointer.js'; | ||
import { intersectSphere } from '../intersections/index.js'; | ||
import { SphereIntersector } from '../intersections/index.js'; | ||
import { generateUniquePointerId } from './index.js'; | ||
export const defaultTouchPointerOptions = { | ||
button: 0, | ||
downRadius: 0.03, | ||
hoverRadius: 0.1, | ||
}; | ||
export function createTouchPointer(space, pointerState, options = defaultTouchPointerOptions, pointerType = 'touch') { | ||
const fromPosition = new Vector3(); | ||
const fromQuaternion = new Quaternion(); | ||
const pointerId = generateUniquePointerId(); | ||
return new Pointer(pointerId, pointerType, pointerState, (scene, _, pointerCapture) => { | ||
export function createTouchPointer(space, pointerState, options = {}, pointerType = 'touch') { | ||
return new Pointer(generateUniquePointerId(), pointerType, pointerState, new SphereIntersector((_nativeEvent, fromPosition, fromQuaternion) => { | ||
const spaceObject = space.current; | ||
@@ -22,6 +13,6 @@ if (spaceObject == null) { | ||
fromQuaternion.setFromRotationMatrix(spaceObject.matrixWorld); | ||
return intersectSphere(fromPosition, fromQuaternion, options.hoverRadius ?? defaultTouchPointerOptions.hoverRadius, scene, pointerId, pointerType, pointerState, pointerCapture, options); | ||
}, createUpdateTouchPointer(options), undefined, undefined, options); | ||
return options.hoverRadius ?? 0.1; | ||
}, options), createUpdateTouchPointer(options), undefined, undefined, options); | ||
} | ||
function createUpdateTouchPointer(options = defaultTouchPointerOptions) { | ||
function createUpdateTouchPointer(options) { | ||
let wasPointerDown = false; | ||
@@ -33,7 +24,7 @@ return (pointer) => { | ||
const intersection = pointer.getIntersection(); | ||
const isPointerDown = computeIsPointerDown(intersection, options.downRadius ?? defaultTouchPointerOptions.downRadius); | ||
const isPointerDown = computeIsPointerDown(intersection, options.downRadius ?? 0.03); | ||
if (isPointerDown === wasPointerDown) { | ||
return; | ||
} | ||
const nativeEvent = { timeStamp: performance.now(), button: options.button ?? defaultTouchPointerOptions.button }; | ||
const nativeEvent = { timeStamp: performance.now(), button: options.button ?? 0 }; | ||
if (isPointerDown) { | ||
@@ -40,0 +31,0 @@ pointer.down(nativeEvent); |
@@ -5,3 +5,3 @@ { | ||
"license": "SEE LICENSE IN LICENSE", | ||
"version": "6.2.2", | ||
"version": "6.2.3", | ||
"homepage": "https://github.com/pmndrs/xr", | ||
@@ -32,8 +32,9 @@ "author": "Bela Bohlender", | ||
"vite": "^5.2.11", | ||
"vitest": "^1.6.0" | ||
"vitest": "^2.0.5" | ||
}, | ||
"scripts": { | ||
"build": "tsc -p build.tsconfig.json", | ||
"test": "vitest run", | ||
"bench": "vitest bench", | ||
"test": "vitest run --printConsoleTrace", | ||
"bench-write": "vitest bench --output-json bench.json --run", | ||
"bench-compare": "vitest bench --compare bench.json --run", | ||
"example": "vite example --host", | ||
@@ -40,0 +41,0 @@ "example:build": "vite build example" |
# pointer-events | ||
*framework agnostic pointer-events implementation for three.js* | ||
_framework agnostic pointer-events implementation for three.js_ | ||
@@ -10,24 +10,25 @@ based on [🎯 Designing Pointer-events for 3D & XR](https://polar.sh/bbohlender/posts/designing-pointer-events-for-3d) | ||
```js | ||
import * as THREE from 'three'; | ||
import { forwardHtmlEvents } from '@pmndrs/pointer-events'; | ||
import * as THREE from 'three' | ||
import { forwardHtmlEvents } from '@pmndrs/pointer-events' | ||
const canvas = document.getElementById("canvas") | ||
const scene = new THREE.Scene(); | ||
const camera = new THREE.PerspectiveCamera( 70, width / height, 0.01, 10 ); | ||
camera.position.z = 1; | ||
const canvas = document.getElementById('canvas') | ||
const scene = new THREE.Scene() | ||
const camera = new THREE.PerspectiveCamera(70, width / height, 0.01, 10) | ||
camera.position.z = 1 | ||
forwardHtmlEvents(canvas, camera, scene) | ||
const width = window.innerWidth, height = window.innerHeight; | ||
const width = window.innerWidth, | ||
height = window.innerHeight | ||
const geometry = new THREE.BoxGeometry( 0.2, 0.2, 0.2 ); | ||
const material = new THREE.MeshBasicMaterial({ color: new THREE.Color("red") }); | ||
const mesh = new THREE.Mesh( geometry, material ); | ||
scene.add( mesh ); | ||
const geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2) | ||
const material = new THREE.MeshBasicMaterial({ color: new THREE.Color('red') }) | ||
const mesh = new THREE.Mesh(geometry, material) | ||
scene.add(mesh) | ||
mesh.addEventListener("pointerover", () => material.color.set("blue")) | ||
mesh.addEventListener("pointerout", () => material.color.set("red")) | ||
mesh.addEventListener('pointerover', () => material.color.set('blue')) | ||
mesh.addEventListener('pointerout', () => material.color.set('red')) | ||
const renderer = new THREE.WebGLRenderer( { antialias: true } ); | ||
renderer.setSize( width, height ); | ||
renderer.setAnimationLoop(() => renderer.render( scene, camera )); | ||
const renderer = new THREE.WebGLRenderer({ antialias: true }) | ||
renderer.setSize(width, height) | ||
renderer.setAnimationLoop(() => renderer.render(scene, camera)) | ||
``` | ||
@@ -40,6 +41,6 @@ | ||
```js | ||
object.pointerEvents = "none"; | ||
object.pointerEvents = 'none' | ||
``` | ||
The values `none` and `auto` correspond to the css properties, where `none` means that an object is not directly targetted and `auto` means the object is always targetted for events. The additional value `listener`, which is the default value, expresses that the object is only targetted by events if the object has any listeners. In 3D scenes this default is more reasonable than `auto`, which is the default in the web, because 3D scenes often contain semi-transparent content, such as particles, that should not catch pointer events by default. | ||
The values `none` and `auto` correspond to the css properties, where `none` means that an object is not directly targetted and `auto` means the object is always targetted for events. The additional value `listener`, which is the default value, expresses that the object is only targetted by events if the object has any listeners. In 3D scenes this default is more reasonable than `auto`, which is the default in the web, because 3D scenes often contain semi-transparent content, such as particles, that should not catch pointer events by default. | ||
@@ -50,2 +51,33 @@ In addition to the `pointerEvents` property, each 3D object can also filter events based on the `pointerType` with the `pointerEventsType` property. This property defaults to the value `all`, which expresses that pointer events from pointers of all types should be accepted. To filter specific pointer types, such as `screen-mouse`, which represents a normal mouse used through a 2D screen, `pointerEventsType` can be set to `{ allow: "screen-mouse" }` or `{ deny: "screen-touch" }`. `pointerEventsType`'s `allow` and `deny` accept strings and array of strings. In case more custom logic is needed, `pointerEventsType` also accepts a function. In general the pointer types `screen-touch`, `screen-pen`, `ray`, `grab`, and `touch` are used by default. For pointer events that were forwarded through a portal using `forwardObjectEvents`, their `pointerType` is prefixed with `forward-`, while events forwarded from the dom to the scene are prefixed with `screen-`. | ||
Create your own `Pointer` that can represent a WebXR controller or something else. These `Pointer` can use a normal `Ray` for intersection, or a set of `Lines`, or even a `Sphere`, for grab and touch events. | ||
Create your own `Pointer` that can represent a WebXR controller or something else. These `Pointer` can use a normal `Ray` for intersection, or a set of `Lines`, or even a `Sphere`, for grab and touch events. | ||
## Performance | ||
In some cases multi-modal interactivity requires multiple pointers at the same time. Executing `pointer.move`, such as in the following example, can lead to performance issues because the scene graph will be traversed several times. | ||
```ts | ||
leftGrabPointer.move() | ||
leftTouchPointer.move() | ||
leftRayPointer.move() | ||
rightGrabPointer.move() | ||
rightTouchPointer.move() | ||
rightRayPointer.move() | ||
``` | ||
In this case, performance can be improved by combining the pointer using `CombinedPointer`, which will traverse the scene graph once per combined pointer, calculating the intersections for each pointer on each object. | ||
```ts | ||
const leftPointer = new CombinedPointer() | ||
const rightPointer = new CombinedPointer() | ||
leftPointer.register(leftGrabPointer) | ||
leftPointer.register(leftTouchPointer) | ||
leftPointer.register(leftRayPointer) | ||
rightPointer.register(rightGrabPointer) | ||
rightPointer.register(rightTouchPointer) | ||
rightPointer.register(rightRayPointer) | ||
leftPointer.move() | ||
rightPointer.move() | ||
``` | ||
80418
37
1854
81