@coconut-xr/xinteraction
Advanced tools
Comparing version 0.0.2 to 0.0.3
@@ -0,1 +1,2 @@ | ||
import { Camera } from "@react-three/fiber"; | ||
import { Intersection, Object3D, Quaternion, Vector3 } from "three"; | ||
@@ -9,2 +10,10 @@ export type ObjectEventTypes = "press" | "release" | "cancel" | "select" | "move" | "enter" | "leave" | "wheel" | "losteventcapture"; | ||
export declare function isXIntersection(val: Intersection): val is XIntersection; | ||
/** | ||
* | ||
* @param p1 point 1 in world coordinates | ||
* @param p2 point 2 in world coordinates | ||
* @param camera | ||
*/ | ||
export declare function getDistanceSquaredInNDC(camera: Camera, p1: Vector3, p2: Vector3): number; | ||
export declare function isDragDefault(camera: Camera, i1: XIntersection, i2: XIntersection): boolean; | ||
export type EventDispatcher<E, I extends XIntersection> = { | ||
@@ -16,2 +25,3 @@ [Key in ObjectEventTypes]: (object: Object3D, intersection: I, inputDeviceElementId?: number) => void; | ||
}; | ||
export declare const voidObject: Object3D<import("three").Event>; | ||
export declare class EventTranslator<E = Event, I extends XIntersection = XIntersection> { | ||
@@ -23,11 +33,11 @@ readonly inputDeviceId: number; | ||
protected getPressedElementIds: (intersection?: I) => Iterable<number>; | ||
protected onPressMissed?: ((event: E) => void) | undefined; | ||
protected onReleaseMissed?: ((event: E) => void) | undefined; | ||
protected onSelectMissed?: ((event: E) => void) | undefined; | ||
protected isDrag: (pressIntersection: I, currentIntersection: I) => boolean; | ||
protected getInputDeviceTransformation: (position: Vector3, rotation: Quaternion) => void; | ||
onIntersections?: ((intersections: ReadonlyArray<I>) => void) | undefined; | ||
filterIntersections?: ((intersections: Array<I>) => Array<I>) | undefined; | ||
intersections: Array<I>; | ||
private lastPositionChangeTime; | ||
private capturedEvents; | ||
private objectInteractionDataMap; | ||
private voidInteractionData; | ||
constructor(inputDeviceId: number, dispatchPressAlways: boolean, eventDispatcher: EventDispatcher<E, I>, computeIntersections: (event: E, capturedEvents?: Map<Object3D, I>) => Array<I>, getPressedElementIds: (intersection?: I) => Iterable<number>, onPressMissed?: ((event: E) => void) | undefined, onReleaseMissed?: ((event: E) => void) | undefined, onSelectMissed?: ((event: E) => void) | undefined); | ||
private objectInteractionStateMap; | ||
constructor(inputDeviceId: number, dispatchPressAlways: boolean, eventDispatcher: EventDispatcher<E, I>, computeIntersections: (event: E, capturedEvents?: Map<Object3D, I>) => Array<I>, getPressedElementIds: (intersection?: I) => Iterable<number>, isDrag: (pressIntersection: I, currentIntersection: I) => boolean, getInputDeviceTransformation: (position: Vector3, rotation: Quaternion) => void, onIntersections?: ((intersections: ReadonlyArray<I>) => void) | undefined, filterIntersections?: ((intersections: Array<I>) => Array<I>) | undefined); | ||
/** | ||
@@ -43,7 +53,10 @@ * called when the input device receives a press, release, or move event | ||
leave(event: E): void; | ||
private dispatchPressAndRelease; | ||
private updateElementStateMap; | ||
private checkDrag; | ||
private dispatchPress; | ||
private dispatchRelease; | ||
/** | ||
* @returns if the object was entered | ||
*/ | ||
private dispatchEnterAndMove; | ||
private dispatchEnterOrMove; | ||
addEventCapture(eventObject: Object3D, intersection: I): void; | ||
@@ -57,3 +70,3 @@ removeEventCapture(eventObject: Object3D): void; | ||
blockFollowingIntersections(eventObject: Object3D): void; | ||
private getInteractionData; | ||
private getInteractionState; | ||
} |
@@ -0,6 +1,29 @@ | ||
import { Object3D, Quaternion, Vector3 } from "three"; | ||
export function isXIntersection(val) { | ||
return "inputDevicePosition" in val; | ||
} | ||
const p1Helper = new Vector3(); | ||
const p2Helper = new Vector3(); | ||
const inputSourcePositionHelper = new Vector3(); | ||
const inputSourceRotationHelper = new Quaternion(); | ||
/** | ||
* | ||
* @param p1 point 1 in world coordinates | ||
* @param p2 point 2 in world coordinates | ||
* @param camera | ||
*/ | ||
export function getDistanceSquaredInNDC(camera, p1, p2) { | ||
return p1Helper | ||
.copy(p1) | ||
.project(camera) | ||
.distanceToSquared(p2Helper.copy(p2).project(camera)); | ||
} | ||
const defaultDragDistanceSquared = 0.0001; //0.01 | ||
export function isDragDefault(camera, i1, i2) { | ||
return (getDistanceSquaredInNDC(camera, i1.point, i2.point) > | ||
defaultDragDistanceSquared); | ||
} | ||
const traversalIdSymbol = Symbol("traversal-id"); | ||
const emptySet = new Set(); | ||
export const voidObject = new Object3D(); | ||
export class EventTranslator { | ||
@@ -12,5 +35,6 @@ inputDeviceId; | ||
getPressedElementIds; | ||
onPressMissed; | ||
onReleaseMissed; | ||
onSelectMissed; | ||
isDrag; | ||
getInputDeviceTransformation; | ||
onIntersections; | ||
filterIntersections; | ||
//state | ||
@@ -20,8 +44,4 @@ intersections = []; | ||
capturedEvents; | ||
objectInteractionDataMap = new Map(); | ||
voidInteractionData = { | ||
lastPressedElementIds: emptySet, | ||
lastPressedElementEventTimeMap: new Map(), | ||
}; | ||
constructor(inputDeviceId, dispatchPressAlways, eventDispatcher, computeIntersections, getPressedElementIds, onPressMissed, onReleaseMissed, onSelectMissed) { | ||
objectInteractionStateMap = new Map(); | ||
constructor(inputDeviceId, dispatchPressAlways, eventDispatcher, computeIntersections, getPressedElementIds, isDrag, getInputDeviceTransformation, onIntersections, filterIntersections) { | ||
this.inputDeviceId = inputDeviceId; | ||
@@ -32,5 +52,6 @@ this.dispatchPressAlways = dispatchPressAlways; | ||
this.getPressedElementIds = getPressedElementIds; | ||
this.onPressMissed = onPressMissed; | ||
this.onReleaseMissed = onReleaseMissed; | ||
this.onSelectMissed = onSelectMissed; | ||
this.isDrag = isDrag; | ||
this.getInputDeviceTransformation = getInputDeviceTransformation; | ||
this.onIntersections = onIntersections; | ||
this.filterIntersections = filterIntersections; | ||
} | ||
@@ -50,63 +71,34 @@ /** | ||
this.intersections = this.computeIntersections(event, this.capturedEvents); | ||
if (this.intersections.length > 0) { | ||
//leave void | ||
this.voidInteractionData.lastLeftTime = currentTime; | ||
this.voidInteractionData.lastPressedElementIds = emptySet; | ||
//filter insections when not events captured | ||
if (this.capturedEvents == null && this.filterIntersections != null) { | ||
this.intersections = this.filterIntersections(this.intersections); | ||
} | ||
} | ||
//TODO: refactor (the following code is the same for "objects") | ||
//onPressMissed, onReleaseMissed, onSelectMissed | ||
if (pressChanged) { | ||
const pressedElementIds = new Set(this.getPressedElementIds()); | ||
//dispatch onPressMissed if intersected with nothing | ||
this.onIntersections?.(this.intersections); | ||
if (this.intersections.length === 0) { | ||
const lastPressedElementIds = new Set(this.voidInteractionData.lastPressedElementIds); | ||
for (const pressedElementId of pressedElementIds) { | ||
lastPressedElementIds.delete(pressedElementId); | ||
if (dispatchPressFor.includes(pressedElementId) || | ||
this.dispatchPressAlways) { | ||
this.onPressMissed?.(event); | ||
} | ||
} | ||
for (const releasedElementId of lastPressedElementIds) { | ||
this.onReleaseMissed?.(event); | ||
const lastPressedElementEventTime = this.voidInteractionData.lastPressedElementEventTimeMap.get(releasedElementId); | ||
if (lastPressedElementEventTime != null && | ||
(this.voidInteractionData.lastLeftTime == null || | ||
this.voidInteractionData.lastLeftTime < | ||
lastPressedElementEventTime)) { | ||
this.onSelectMissed?.(event); | ||
} | ||
} | ||
//update lastPressedElementIds | ||
this.voidInteractionData.lastPressedElementIds = pressedElementIds; | ||
//update lastPressedElementTimeMap | ||
for (const pressedElementId of pressedElementIds) { | ||
if (dispatchPressFor.includes(pressedElementId) || | ||
this.dispatchPressAlways) { | ||
this.voidInteractionData.lastPressedElementEventTimeMap.set(pressedElementId, currentTime); | ||
} | ||
} | ||
this.getInputDeviceTransformation(inputSourcePositionHelper, inputSourceRotationHelper); | ||
this.intersections = [ | ||
{ | ||
distance: Infinity, | ||
inputDevicePosition: inputSourcePositionHelper.clone(), | ||
inputDeviceRotation: inputSourceRotationHelper.clone(), | ||
object: voidObject, | ||
point: inputSourcePositionHelper.clone(), | ||
}, | ||
]; | ||
} | ||
} | ||
//enter, move, press, release, click, losteventcapture events | ||
this.traverseIntersections(this.intersections, (eventObject, interactionData, intersection, intersectionIndex, pressedElementIds) => { | ||
this.traverseIntersections(this.intersections, (eventObject, interactionState, intersection, intersectionIndex, pressedElementIds) => { | ||
if (positionChanged) { | ||
this.dispatchEnterAndMove(eventObject, interactionData, intersection); | ||
this.dispatchEnterOrMove(eventObject, interactionState, intersection); | ||
//update last intersection time | ||
interactionData.lastIntersectedTime = currentTime; | ||
interactionState.lastIntersectedTime = currentTime; | ||
} | ||
if (pressChanged) { | ||
this.dispatchPressAndRelease(eventObject, interactionData, intersection, pressedElementIds, dispatchPressFor); | ||
//update lastPressedElementIds | ||
interactionData.lastPressedElementIds = pressedElementIds; | ||
//update lastPressedElementTimeMap | ||
for (const pressedElementId of pressedElementIds) { | ||
if (dispatchPressFor.includes(pressedElementId) || | ||
this.dispatchPressAlways) { | ||
interactionData.lastPressedElementEventTimeMap.set(pressedElementId, currentTime); | ||
} | ||
} | ||
this.dispatchPress(eventObject, intersection, pressedElementIds, dispatchPressFor); | ||
this.dispatchRelease(eventObject, intersection, interactionState, pressedElementIds, currentTime); | ||
this.updateElementStateMap(intersection, interactionState, pressedElementIds, dispatchPressFor, currentTime); | ||
} | ||
if (interactionData.blockFollowingIntersections) { | ||
interactionState.lastPressedElementIds = pressedElementIds; | ||
if (interactionState.blockFollowingIntersections) { | ||
//we remove the intersections that happen after | ||
@@ -118,11 +110,13 @@ this.intersections.length = intersectionIndex + 1; | ||
if (positionChanged) { | ||
const pressedElementIds = new Set(this.getPressedElementIds()); | ||
//leave events | ||
this.traverseIntersections(prevIntersections, (eventObject, interactionData, intersection) => { | ||
if (interactionData.lastIntersectedTime === currentTime) { | ||
this.traverseIntersections(prevIntersections, (eventObject, interactionState, intersection) => { | ||
if (interactionState.lastIntersectedTime === currentTime) { | ||
//object was intersected this time –> therefore also all the ancestors –> can stop bubbeling up here | ||
return false; | ||
} | ||
this.dispatchRelease(eventObject, intersection, interactionState, pressedElementIds, currentTime); | ||
this.eventDispatcher.leave(eventObject, intersection); | ||
interactionData.lastLeftTime = currentTime; | ||
interactionData.lastPressedElementIds = emptySet; | ||
interactionState.lastLeftTime = currentTime; | ||
interactionState.lastPressedElementIds = emptySet; | ||
return true; | ||
@@ -135,3 +129,3 @@ }); | ||
this.eventDispatcher.bind(event, this); | ||
this.traverseIntersections(this.intersections, (eventObject, interactionData, intersection) => { | ||
this.traverseIntersections(this.intersections, (eventObject, interactionState, intersection) => { | ||
this.eventDispatcher.cancel(eventObject, intersection); | ||
@@ -143,3 +137,3 @@ return true; | ||
this.eventDispatcher.bind(event, this); | ||
this.traverseIntersections(this.intersections, (eventObject, interactionData, intersection) => { | ||
this.traverseIntersections(this.intersections, (eventObject, interactionState, intersection) => { | ||
this.eventDispatcher.wheel(eventObject, intersection); | ||
@@ -151,3 +145,3 @@ return true; | ||
this.eventDispatcher.bind(event, this); | ||
this.traverseIntersections(this.intersections, (eventObject, interactionData, intersection) => { | ||
this.traverseIntersections(this.intersections, (eventObject, interactionState, intersection) => { | ||
this.eventDispatcher.leave(eventObject, intersection); | ||
@@ -161,10 +155,25 @@ return true; | ||
} | ||
dispatchPressAndRelease(eventObject, interactionData, intersection, pressedElementIds, dispatchPressFor) { | ||
const lastPressedElementIds = new Set(interactionData.lastPressedElementIds); | ||
updateElementStateMap(intersection, interactionState, pressedElementIds, dispatchPressFor, currentTime) { | ||
for (const pressedElementId of pressedElementIds) { | ||
if (lastPressedElementIds.delete(pressedElementId)) { | ||
//was pressed last time | ||
continue; | ||
if (dispatchPressFor.includes(pressedElementId) || | ||
this.dispatchPressAlways) { | ||
interactionState.elementStateMap.set(pressedElementId, { | ||
lastPressEventTime: currentTime, | ||
lastPressEventIntersection: intersection, | ||
}); | ||
} | ||
//pressedElementId was not pressed last time | ||
else { | ||
this.checkDrag(intersection, interactionState, pressedElementId, currentTime); | ||
} | ||
} | ||
} | ||
checkDrag(intersection, interactionState, pressedElementId, currentTime) { | ||
const elementState = interactionState.elementStateMap.get(pressedElementId); | ||
if (elementState != null && | ||
this.isDrag(elementState.lastPressEventIntersection, intersection)) { | ||
elementState.lastDragTime = currentTime; | ||
} | ||
} | ||
dispatchPress(eventObject, intersection, pressedElementIds, dispatchPressFor) { | ||
for (const pressedElementId of pressedElementIds) { | ||
if (this.dispatchPressAlways || | ||
@@ -175,13 +184,21 @@ dispatchPressFor.includes(pressedElementId)) { | ||
} | ||
for (const releasedElementId of lastPressedElementIds) { | ||
} | ||
dispatchRelease(eventObject, intersection, interactionState, pressedElementIds, currentTime) { | ||
for (const releasedElementId of interactionState.lastPressedElementIds) { | ||
if (pressedElementIds.has(releasedElementId)) { | ||
continue; | ||
} | ||
this.checkDrag(intersection, interactionState, releasedElementId, currentTime); | ||
//pressedElementId was not pressed this time | ||
this.eventDispatcher.release(eventObject, intersection, releasedElementId); | ||
const lastPressedElementEventTime = interactionData.lastPressedElementEventTimeMap.get(releasedElementId); | ||
if (lastPressedElementEventTime != null && | ||
(interactionData.lastLeftTime == null || | ||
interactionData.lastLeftTime < lastPressedElementEventTime)) { | ||
//the object wasn't left since it was pressed last | ||
this.removeEventCapture(eventObject); | ||
const elementState = interactionState.elementStateMap.get(releasedElementId); | ||
if (elementState != null && | ||
(interactionState.lastLeftTime == null || | ||
interactionState.lastLeftTime < elementState.lastPressEventTime) && | ||
(elementState.lastDragTime == null || | ||
elementState.lastDragTime < elementState.lastPressEventTime)) { | ||
//=> the object wasn't left and dragged since it was pressed last | ||
this.eventDispatcher.select(eventObject, intersection, releasedElementId); | ||
} | ||
this.removeEventCapture(eventObject); | ||
} | ||
@@ -192,5 +209,5 @@ } | ||
*/ | ||
dispatchEnterAndMove(eventObject, interactionData, intersection) { | ||
if (interactionData.lastIntersectedTime != null && | ||
interactionData.lastIntersectedTime === this.lastPositionChangeTime) { | ||
dispatchEnterOrMove(eventObject, interactionState, intersection) { | ||
if (interactionState.lastIntersectedTime != null && | ||
interactionState.lastIntersectedTime === this.lastPositionChangeTime) { | ||
//object was intersected last time | ||
@@ -201,3 +218,3 @@ this.eventDispatcher.move(eventObject, intersection); | ||
//reset to not block the following intersections | ||
interactionData.blockFollowingIntersections = false; | ||
interactionState.blockFollowingIntersections = false; | ||
//object was not intersected last time | ||
@@ -242,4 +259,4 @@ this.eventDispatcher.enter(eventObject, intersection); | ||
if (this.eventDispatcher.hasEventHandlers(eventObject)) { | ||
const interactionData = this.getInteractionData(eventObject); | ||
const continueUpwards = callback(eventObject, interactionData, intersection, intersectionIndex, info); | ||
const interactionState = this.getInteractionState(eventObject); | ||
const continueUpwards = callback(eventObject, interactionState, intersection, intersectionIndex, info); | ||
if (!continueUpwards) { | ||
@@ -254,10 +271,10 @@ continue outer; | ||
blockFollowingIntersections(eventObject) { | ||
const interactionData = this.getInteractionData(eventObject); | ||
interactionData.blockFollowingIntersections = true; | ||
const interactionState = this.getInteractionState(eventObject); | ||
interactionState.blockFollowingIntersections = true; | ||
} | ||
getInteractionData(eventObject) { | ||
let data = this.objectInteractionDataMap.get(eventObject); | ||
if (data == null) { | ||
this.objectInteractionDataMap.set(eventObject, (data = { | ||
lastPressedElementEventTimeMap: new Map(), | ||
getInteractionState(eventObject) { | ||
let interactionState = this.objectInteractionStateMap.get(eventObject); | ||
if (interactionState == null) { | ||
this.objectInteractionStateMap.set(eventObject, (interactionState = { | ||
elementStateMap: new Map(), | ||
lastPressedElementIds: emptySet, | ||
@@ -267,3 +284,3 @@ blockFollowingIntersections: false, | ||
} | ||
return data; | ||
return interactionState; | ||
} | ||
@@ -270,0 +287,0 @@ } |
@@ -1,2 +0,3 @@ | ||
import { Object3D } from "three"; | ||
import { Intersection, Object3D } from "three"; | ||
export declare function traverseUntilInteractable<T, R>(object: Object3D, isInteractable: (object: Object3D) => boolean, callback: (object: Object3D) => T, reduce: (prev: R, value: T) => R, initial: R): R; | ||
export declare function isIntersectionNotClipped(intersection: Intersection): boolean; |
@@ -0,1 +1,2 @@ | ||
import { Mesh } from "three"; | ||
export function traverseUntilInteractable(object, isInteractable, callback, reduce, initial) { | ||
@@ -11,1 +12,14 @@ if (isInteractable(object)) { | ||
} | ||
export function isIntersectionNotClipped(intersection) { | ||
if (!(intersection.object instanceof Mesh) || | ||
intersection.object.material.clippingPlanes == null) { | ||
return true; | ||
} | ||
const planes = intersection.object.material.clippingPlanes; | ||
for (const plane of planes) { | ||
if (plane.distanceToPoint(intersection.point) < 0) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} |
@@ -8,2 +8,2 @@ import { Object3D, Quaternion, Vector3 } from "three"; | ||
export declare function intersectLinesFromCapturedEvents(from: Object3D, fromPosition: Vector3, fromRotation: Quaternion, linePoints: Array<Vector3>, capturedEvents: Map<Object3D, XLinesIntersection>): Array<XLinesIntersection>; | ||
export declare function intersectLinesFromObject(from: Object3D, fromPosition: Vector3, fromRotation: Quaternion, linePoints: Array<Vector3>, on: Object3D, dispatcher: EventDispatcher<Event, XLinesIntersection>, filterIntersections?: (intersections: Array<XLinesIntersection>) => Array<XLinesIntersection>): Array<XLinesIntersection>; | ||
export declare function intersectLinesFromObject(from: Object3D, fromPosition: Vector3, fromRotation: Quaternion, linePoints: Array<Vector3>, on: Object3D, dispatcher: EventDispatcher<Event, XLinesIntersection>, filterClipped: boolean): Array<XLinesIntersection>; |
import { Line3, Raycaster, Vector3 } from "three"; | ||
import { traverseUntilInteractable } from "./index.js"; | ||
import { isIntersectionNotClipped, traverseUntilInteractable, } from "./index.js"; | ||
const raycaster = new Raycaster(); | ||
@@ -22,3 +22,3 @@ const directionHelper = new Vector3(); | ||
} | ||
export function intersectLinesFromObject(from, fromPosition, fromRotation, linePoints, on, dispatcher, filterIntersections) { | ||
export function intersectLinesFromObject(from, fromPosition, fromRotation, linePoints, on, dispatcher, filterClipped) { | ||
let intersections = traverseUntilInteractable(on, dispatcher.hasEventHandlers.bind(dispatcher), (object) => { | ||
@@ -58,5 +58,7 @@ const intersections = []; | ||
}, (prev, cur) => prev.concat(cur), []); | ||
intersections = filterIntersections?.(intersections) ?? intersections; | ||
if (filterClipped) { | ||
intersections = intersections.filter(isIntersectionNotClipped); | ||
} | ||
//sort smallest distance first | ||
return intersections.sort((a, b) => a.distance - b.distance); | ||
} |
@@ -7,4 +7,4 @@ import { Camera, Object3D, Quaternion, Vector2, Vector3 } from "three"; | ||
export declare function intersectRayFromCapturedEvents(fromPosition: Vector3, fromRotation: Quaternion, capturedEvents: Map<Object3D, XIntersection>): Array<XIntersection>; | ||
export declare function intersectRayFromCameraCapturedEvents(camera: Camera, coords: Vector2, capturedEvents: Map<Object3D, XCameraRayIntersection>): Array<XCameraRayIntersection>; | ||
export declare function intersectRayFromObject(fromPosition: Vector3, fromRotation: Quaternion, on: Object3D, dispatcher: EventDispatcher<Event, XIntersection>, filterIntersections?: (intersections: Array<XIntersection>) => Array<XIntersection>): Array<XIntersection>; | ||
export declare function intersectRayFromCamera(from: Camera, coords: Vector2, on: Object3D, dispatcher: EventDispatcher<Event, XCameraRayIntersection>, filterIntersections?: (intersections: Array<XCameraRayIntersection>) => Array<XCameraRayIntersection>): Array<XCameraRayIntersection>; | ||
export declare function intersectRayFromCameraCapturedEvents(camera: Camera, coords: Vector2, capturedEvents: Map<Object3D, XCameraRayIntersection>, worldPositionTarget: Vector3, worldQuaternionTarget: Quaternion): Array<XCameraRayIntersection>; | ||
export declare function intersectRayFromObject(fromPosition: Vector3, fromRotation: Quaternion, on: Object3D, dispatcher: EventDispatcher<Event, XIntersection>, filterClipped: boolean): Array<XIntersection>; | ||
export declare function intersectRayFromCamera(from: Camera, coords: Vector2, on: Object3D, dispatcher: EventDispatcher<Event, XCameraRayIntersection>, filterClipped: boolean, worldPositionTarget: Vector3, worldQuaternionTarget: Quaternion): Array<XCameraRayIntersection>; |
@@ -1,3 +0,3 @@ | ||
import { Plane, Quaternion, Raycaster, Vector3, } from "three"; | ||
import { traverseUntilInteractable } from "./index.js"; | ||
import { Plane, Raycaster, Vector3, } from "three"; | ||
import { isIntersectionNotClipped, traverseUntilInteractable, } from "./index.js"; | ||
const raycaster = new Raycaster(); | ||
@@ -21,5 +21,6 @@ const directionHelper = new Vector3(); | ||
} | ||
export function intersectRayFromCameraCapturedEvents(camera, coords, capturedEvents) { | ||
export function intersectRayFromCameraCapturedEvents(camera, coords, capturedEvents, worldPositionTarget, worldQuaternionTarget) { | ||
raycaster.setFromCamera(coords, camera); | ||
rayQuaternion.setFromUnitVectors(ZAXIS, raycaster.ray.direction); | ||
worldPositionTarget.copy(raycaster.ray.origin); | ||
worldQuaternionTarget.setFromUnitVectors(ZAXIS, raycaster.ray.direction); | ||
camera.getWorldDirection(directionHelper); | ||
@@ -36,4 +37,4 @@ return Array.from(capturedEvents).map(([capturedObject, intersection]) => { | ||
point, | ||
inputDevicePosition: raycaster.ray.origin.clone(), | ||
inputDeviceRotation: rayQuaternion.clone(), | ||
inputDevicePosition: worldPositionTarget.clone(), | ||
inputDeviceRotation: worldQuaternionTarget.clone(), | ||
capturedObject, | ||
@@ -43,3 +44,3 @@ }; | ||
} | ||
export function intersectRayFromObject(fromPosition, fromRotation, on, dispatcher, filterIntersections) { | ||
export function intersectRayFromObject(fromPosition, fromRotation, on, dispatcher, filterClipped) { | ||
raycaster.ray.origin.copy(fromPosition); | ||
@@ -51,20 +52,24 @@ raycaster.ray.direction.set(0, 0, 1).applyQuaternion(fromRotation); | ||
})), (prev, cur) => prev.concat(cur), []); | ||
intersections = filterIntersections?.(intersections) ?? intersections; | ||
if (filterClipped) { | ||
intersections = intersections.filter(isIntersectionNotClipped); | ||
} | ||
//sort smallest distance first | ||
return intersections.sort((a, b) => a.distance - b.distance); | ||
} | ||
const rayQuaternion = new Quaternion(); | ||
const ZAXIS = new Vector3(); | ||
export function intersectRayFromCamera(from, coords, on, dispatcher, filterIntersections) { | ||
export function intersectRayFromCamera(from, coords, on, dispatcher, filterClipped, worldPositionTarget, worldQuaternionTarget) { | ||
raycaster.setFromCamera(coords, from); | ||
rayQuaternion.setFromUnitVectors(ZAXIS, raycaster.ray.direction); | ||
worldPositionTarget.copy(raycaster.ray.origin); | ||
worldQuaternionTarget.setFromUnitVectors(ZAXIS, raycaster.ray.direction); | ||
planeHelper.setFromNormalAndCoplanarPoint(from.getWorldDirection(directionHelper), raycaster.ray.origin); | ||
let intersections = traverseUntilInteractable(on, dispatcher.hasEventHandlers.bind(dispatcher), (object) => raycaster.intersectObject(object, true).map((intersection) => Object.assign(intersection, { | ||
inputDevicePosition: raycaster.ray.origin.clone(), | ||
inputDeviceRotation: rayQuaternion.clone(), | ||
inputDevicePosition: worldPositionTarget.clone(), | ||
inputDeviceRotation: worldQuaternionTarget.clone(), | ||
distanceViewPlane: planeHelper.distanceToPoint(intersection.point), | ||
})), (prev, cur) => prev.concat(cur), []); | ||
intersections = filterIntersections?.(intersections) ?? intersections; | ||
if (filterClipped) { | ||
intersections = intersections.filter(isIntersectionNotClipped); | ||
} | ||
//sort smallest distance first | ||
return intersections.sort((a, b) => a.distance - b.distance); | ||
} |
import { Object3D, Vector3, Quaternion } from "three"; | ||
import { EventDispatcher, XIntersection } from "../index.js"; | ||
export declare function intersectSphereFromCapturedEvents(fromPosition: Vector3, fromRotation: Quaternion, capturedEvents: Map<Object3D, XIntersection>): Array<XIntersection>; | ||
export declare function intersectSphereFromObject(fromPosition: Vector3, fromQuaternion: Quaternion, radius: number, on: Object3D, dispatcher: EventDispatcher<Event, XIntersection>, filterIntersections?: (intersections: Array<XIntersection>) => Array<XIntersection>): Array<XIntersection>; | ||
export type XSphereIntersection = XIntersection & { | ||
/** | ||
* set when the event is captured because the "distance" property is only the distance to a "expected intersection" | ||
*/ | ||
actualDistance?: number; | ||
}; | ||
export declare function intersectSphereFromCapturedEvents(fromPosition: Vector3, fromRotation: Quaternion, capturedEvents: Map<Object3D, XSphereIntersection>): Array<XSphereIntersection>; | ||
export declare function intersectSphereFromObject(fromPosition: Vector3, fromQuaternion: Quaternion, radius: number, on: Object3D, dispatcher: EventDispatcher<Event, XSphereIntersection>, filterClipped: boolean): Array<XSphereIntersection>; |
import { InstancedMesh, Matrix4, Mesh, Vector3, Sphere, Quaternion, } from "three"; | ||
import { traverseUntilInteractable } from "./index.js"; | ||
import { isIntersectionNotClipped, traverseUntilInteractable, } from "./index.js"; | ||
const oldInputDevicePointOffset = new Vector3(); | ||
@@ -30,11 +30,41 @@ const inputDeviceQuaternionOffset = new Quaternion(); | ||
capturedObject, | ||
actualDistance: computeActualDistance(fromPosition, intersection), | ||
}; | ||
}); | ||
} | ||
function computeActualDistance(fromPosition, intersection) { | ||
const object = intersection.object; | ||
if (intersection.instanceId != null && object instanceof InstancedMesh) { | ||
if (object.geometry.boundingBox == null) { | ||
object.geometry.computeBoundingBox(); | ||
} | ||
object.getMatrixAt(intersection.instanceId, matrixHelper); | ||
matrixHelper.premultiply(object.matrixWorld); | ||
invertedMatrixHelper.copy(matrixHelper).invert(); | ||
vectorHelper.copy(fromPosition).applyMatrix4(invertedMatrixHelper); | ||
object.geometry.boundingBox.clampPoint(vectorHelper, vectorHelper); | ||
vectorHelper.applyMatrix4(matrixHelper); | ||
return vectorHelper.distanceTo(fromPosition); | ||
} | ||
if (object instanceof Mesh) { | ||
if (object.geometry.boundingBox == null) { | ||
object.geometry.computeBoundingBox(); | ||
} | ||
invertedMatrixHelper.copy(object.matrixWorld).invert(); | ||
vectorHelper.copy(fromPosition).applyMatrix4(invertedMatrixHelper); | ||
object.geometry.boundingBox.clampPoint(vectorHelper, vectorHelper); | ||
vectorHelper.applyMatrix4(object.matrixWorld); | ||
return vectorHelper.distanceTo(fromPosition); | ||
} | ||
//not lösung - emergency solution | ||
return object.getWorldPosition(vectorHelper).distanceTo(fromPosition); | ||
} | ||
const collisionSphere = new Sphere(); | ||
export function intersectSphereFromObject(fromPosition, fromQuaternion, radius, on, dispatcher, filterIntersections) { | ||
export function intersectSphereFromObject(fromPosition, fromQuaternion, radius, on, dispatcher, filterClipped) { | ||
collisionSphere.center.copy(fromPosition); | ||
collisionSphere.radius = radius; | ||
let intersections = traverseUntilInteractable(on, dispatcher.hasEventHandlers.bind(dispatcher), (object) => intersectSphereRecursive(object, fromQuaternion), (prev, cur) => prev.concat(cur), []); | ||
intersections = filterIntersections?.(intersections) ?? intersections; | ||
if (filterClipped) { | ||
intersections = intersections.filter(isIntersectionNotClipped); | ||
} | ||
//sort smallest distance first | ||
@@ -82,8 +112,7 @@ return intersections.sort((a, b) => a.distance - b.distance); | ||
object.getMatrixAt(i, matrixHelper); | ||
invertedMatrixHelper.copy(matrixHelper); | ||
invertedMatrixHelper.premultiply(object.matrixWorld); | ||
if (!intersectSphereSphere(invertedMatrixHelper, object.geometry)) { | ||
matrixHelper.premultiply(object.matrixWorld); | ||
if (!intersectSphereSphere(matrixHelper, object.geometry)) { | ||
continue; | ||
} | ||
invertedMatrixHelper.invert(); | ||
invertedMatrixHelper.copy(matrixHelper).invert(); | ||
const intersection = intersectSphereBox(object, collisionSphere.center, inputDeviceRotation, matrixHelper, invertedMatrixHelper, object.geometry, i); | ||
@@ -90,0 +119,0 @@ if (intersection != null) { |
import React from "react"; | ||
import { Vector3 } from "three"; | ||
import { Vector3, Event } from "three"; | ||
import { XIntersection } from "../index.js"; | ||
import { XLinesIntersection } from "../intersections/lines.js"; | ||
import { InputDeviceFunctions } from "./index.js"; | ||
import { ThreeEvent } from "@react-three/fiber"; | ||
export declare const XCurvedPointer: React.ForwardRefExoticComponent<{ | ||
@@ -10,2 +12,7 @@ id: number; | ||
filterIntersections?: ((intersections: Array<XLinesIntersection>) => Array<XLinesIntersection>) | undefined; | ||
onPointerDownMissed?: ((event: ThreeEvent<Event>) => void) | undefined; | ||
onPointerUpMissed?: ((event: ThreeEvent<Event>) => void) | undefined; | ||
onClickMissed?: ((event: ThreeEvent<Event>) => void) | undefined; | ||
isDrag?: ((i1: XIntersection, i2: XIntersection) => boolean) | undefined; | ||
filterClipped?: boolean | undefined; | ||
} & React.RefAttributes<InputDeviceFunctions>>; |
/* eslint-disable react/display-name */ | ||
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, } from "react"; | ||
import { Quaternion, Vector3 } from "three"; | ||
import { EventTranslator } from "../index.js"; | ||
import { EventTranslator, isDragDefault } from "../index.js"; | ||
import { intersectLinesFromCapturedEvents, intersectLinesFromObject, } from "../intersections/lines.js"; | ||
import { R3FEventDispatcher } from "./index.js"; | ||
import { useFrame, useThree } from "@react-three/fiber"; | ||
import { useFrame, useStore } from "@react-three/fiber"; | ||
const emptyIntersections = []; | ||
const worldPositionHelper = new Vector3(); | ||
const worldRotationHelper = new Quaternion(); | ||
export const XCurvedPointer = forwardRef(({ id, points, onIntersections, filterIntersections }, ref) => { | ||
export const XCurvedPointer = forwardRef(({ id, points, onIntersections, filterIntersections, onClickMissed, onPointerDownMissed, onPointerUpMissed, isDrag: customIsDrag, filterClipped = true, }, ref) => { | ||
const objectRef = useRef(null); | ||
const scene = useThree(({ scene }) => scene); | ||
const store = useStore(); | ||
const dispatcher = useMemo(() => new R3FEventDispatcher(), []); | ||
dispatcher.onPointerDownMissed = onPointerDownMissed; | ||
dispatcher.onPointerUpMissed = onPointerUpMissed; | ||
dispatcher.onClickMissed = onClickMissed; | ||
const pressedElementIds = useMemo(() => new Set(), []); | ||
const translator = useMemo(() => { | ||
const dispatcher = new R3FEventDispatcher(); | ||
return new EventTranslator(id, false, dispatcher, (_, capturedEvents) => { | ||
if (objectRef.current == null) { | ||
return emptyIntersections; | ||
} | ||
objectRef.current.getWorldPosition(worldPositionHelper); | ||
objectRef.current.getWorldQuaternion(worldRotationHelper); | ||
if (capturedEvents == null) { | ||
return intersectLinesFromObject(objectRef.current, worldPositionHelper, worldRotationHelper, points, scene, dispatcher, filterIntersections); | ||
} | ||
return intersectLinesFromCapturedEvents(objectRef.current, worldPositionHelper, worldRotationHelper, points, capturedEvents); | ||
}, () => pressedElementIds); | ||
}, [id, filterIntersections, points, scene]); | ||
const properties = useMemo(() => ({ points, customIsDrag, filterClipped }), []); | ||
properties.points = points; | ||
properties.customIsDrag = customIsDrag; | ||
properties.filterClipped = filterClipped; | ||
const translator = useMemo(() => new EventTranslator(id, false, dispatcher, (_, capturedEvents) => { | ||
if (objectRef.current == null) { | ||
return emptyIntersections; | ||
} | ||
objectRef.current.getWorldPosition(worldPositionHelper); | ||
objectRef.current.getWorldQuaternion(worldRotationHelper); | ||
return capturedEvents == null | ||
? //events not captured -> compute normally | ||
intersectLinesFromObject(objectRef.current, worldPositionHelper, worldRotationHelper, properties.points, store.getState().scene, dispatcher, properties.filterClipped) | ||
: intersectLinesFromCapturedEvents(objectRef.current, worldPositionHelper, worldRotationHelper, properties.points, capturedEvents); | ||
}, () => pressedElementIds, (i1, i2) => properties.customIsDrag == null | ||
? isDragDefault(store.getState().camera, i1, i2) | ||
: properties.customIsDrag(i1, i2), (position, rotation) => { | ||
if (objectRef.current == null) { | ||
return; | ||
} | ||
objectRef.current.getWorldPosition(position); | ||
objectRef.current.getWorldQuaternion(rotation); | ||
}), [id, store]); | ||
translator.onIntersections = onIntersections; | ||
translator.filterIntersections = filterIntersections; | ||
useImperativeHandle(ref, () => ({ | ||
@@ -44,9 +59,8 @@ press: (id, event) => { | ||
//cleanup translator | ||
useEffect(() => () => translator.leave({}), [translator]); | ||
useEffect(() => translator.leave.bind(translator, {}), [translator]); | ||
//update translator every frame | ||
useFrame(() => { | ||
translator.update({}, true, false); | ||
onIntersections?.(translator.intersections); | ||
}); | ||
return React.createElement("object3D", { ref: objectRef }); | ||
}); |
import { Object3D, Event } from "three"; | ||
import { EventDispatcher, EventTranslator, XIntersection } from "../index.js"; | ||
import { ThreeEvent } from "@react-three/fiber"; | ||
import type { EventManager } from "@react-three/fiber/dist/declarations/src/core/events.js"; | ||
export declare const noEvents: () => EventManager<HTMLElement>; | ||
export declare class R3FEventDispatcher<I extends XIntersection> implements EventDispatcher<Event, I> { | ||
onPointerDownMissed?: ((event: ThreeEvent<Event>) => void) | undefined; | ||
onPointerUpMissed?: ((event: ThreeEvent<Event>) => void) | undefined; | ||
onClickMissed?: ((event: ThreeEvent<Event>) => void) | undefined; | ||
private stoppedEventTypeSet; | ||
private event; | ||
private translator; | ||
constructor(onPointerDownMissed?: ((event: ThreeEvent<Event>) => void) | undefined, onPointerUpMissed?: ((event: ThreeEvent<Event>) => void) | undefined, onClickMissed?: ((event: ThreeEvent<Event>) => void) | undefined); | ||
press: (eventObject: Object3D<Event>, intersection: I, inputDeviceElementId?: number | undefined) => void; | ||
@@ -8,0 +15,0 @@ release: (eventObject: Object3D<Event>, intersection: I, inputDeviceElementId?: number | undefined) => void; |
@@ -0,5 +1,18 @@ | ||
import { voidObject, } from "../index.js"; | ||
export const noEvents = () => ({ | ||
enabled: false, | ||
priority: 0, | ||
}); | ||
export class R3FEventDispatcher { | ||
onPointerDownMissed; | ||
onPointerUpMissed; | ||
onClickMissed; | ||
stoppedEventTypeSet; | ||
event; | ||
translator; | ||
constructor(onPointerDownMissed, onPointerUpMissed, onClickMissed) { | ||
this.onPointerDownMissed = onPointerDownMissed; | ||
this.onPointerUpMissed = onPointerUpMissed; | ||
this.onClickMissed = onClickMissed; | ||
} | ||
press = this.dispatch.bind(this, ["onPointerDown"]); | ||
@@ -21,2 +34,16 @@ release = this.dispatch.bind(this, ["onPointerUp"]); | ||
} | ||
if (eventObject == voidObject) { | ||
switch (name) { | ||
case "onClick": | ||
case "onPointerDown": | ||
case "onPointerUp": { | ||
const handler = this[`${name}Missed`]; | ||
if (handler == null) { | ||
return; | ||
} | ||
handler(this.createEvent(name, eventObject, intersection, inputDeviceElementId)); | ||
} | ||
} | ||
return; | ||
} | ||
const instance = eventObject.__r3f; | ||
@@ -75,2 +102,5 @@ instance.handlers[name]?.(this.createEvent(name, eventObject, intersection, inputDeviceElementId)); | ||
hasEventHandlers(object) { | ||
if (object === voidObject) { | ||
return true; | ||
} | ||
const instance = object.__r3f; | ||
@@ -77,0 +107,0 @@ return instance != null && instance.eventCount > 0; |
@@ -0,2 +1,4 @@ | ||
import { ThreeEvent } from "@react-three/fiber"; | ||
import React from "react"; | ||
import { Event } from "three"; | ||
import { XIntersection } from "../index.js"; | ||
@@ -11,4 +13,9 @@ import { InputDeviceFunctions } from "./index.js"; | ||
} | undefined; | ||
onIntersections?: ((intersections: Array<XIntersection>) => void) | undefined; | ||
onIntersections?: ((intersections: ReadonlyArray<XIntersection>) => void) | undefined; | ||
filterIntersections?: ((intersections: Array<XIntersection>) => Array<XIntersection>) | undefined; | ||
onPointerDownMissed?: ((event: ThreeEvent<Event>) => void) | undefined; | ||
onPointerUpMissed?: ((event: ThreeEvent<Event>) => void) | undefined; | ||
onClickMissed?: ((event: ThreeEvent<Event>) => void) | undefined; | ||
isDrag?: ((i1: XIntersection, i2: XIntersection) => boolean) | undefined; | ||
filterClipped?: boolean | undefined; | ||
} & React.RefAttributes<InputDeviceFunctions>>; |
/* eslint-disable react/display-name */ | ||
import { useThree, useFrame } from "@react-three/fiber"; | ||
import { useFrame, useStore } from "@react-three/fiber"; | ||
import React, { useRef, useMemo, useEffect, forwardRef, useImperativeHandle, } from "react"; | ||
import { Quaternion, Vector3 } from "three"; | ||
import { EventTranslator } from "../index.js"; | ||
import { EventTranslator, isDragDefault } from "../index.js"; | ||
import { R3FEventDispatcher } from "./index.js"; | ||
@@ -11,32 +11,52 @@ import { intersectSphereFromCapturedEvents, intersectSphereFromObject, } from "../intersections/sphere.js"; | ||
const worldRotationHelper = new Quaternion(); | ||
export const XSphereCollider = forwardRef(({ id, distanceElement, radius, onIntersections, filterIntersections }, ref) => { | ||
export const XSphereCollider = forwardRef(({ id, distanceElement, radius, onIntersections, filterIntersections, onClickMissed, onPointerDownMissed, onPointerUpMissed, isDrag: customIsDrag, filterClipped = true, }, ref) => { | ||
const objectRef = useRef(null); | ||
const scene = useThree(({ scene }) => scene); | ||
const store = useStore(); | ||
const pressedElementIds = useMemo(() => new Set(), []); | ||
const translator = useMemo(() => { | ||
const dispatcher = new R3FEventDispatcher(); | ||
return new EventTranslator(id, true, dispatcher, (_, capturedEvents) => { | ||
if (objectRef.current == null) { | ||
return emptyIntersections; | ||
} | ||
objectRef.current.getWorldPosition(worldPositionHelper); | ||
objectRef.current.getWorldQuaternion(worldRotationHelper); | ||
if (capturedEvents == null) { | ||
//events not captured -> compute intersections normally | ||
return intersectSphereFromObject(worldPositionHelper, worldRotationHelper, radius, scene, dispatcher, filterIntersections); | ||
} | ||
return intersectSphereFromCapturedEvents(worldPositionHelper, worldRotationHelper, capturedEvents); | ||
}, (intersection) => { | ||
if (distanceElement == null || intersection == null) { | ||
return pressedElementIds; | ||
} | ||
if (intersection.distance <= distanceElement.downRadius) { | ||
pressedElementIds.add(distanceElement.id); | ||
} | ||
else { | ||
pressedElementIds.delete(distanceElement.id); | ||
} | ||
const dispatcher = useMemo(() => new R3FEventDispatcher(), []); | ||
dispatcher.onPointerDownMissed = onPointerDownMissed; | ||
dispatcher.onPointerUpMissed = onPointerUpMissed; | ||
dispatcher.onClickMissed = onClickMissed; | ||
const properties = useMemo(() => ({ distanceElement, radius, customIsDrag, filterClipped }), []); | ||
properties.distanceElement = distanceElement; | ||
properties.radius = radius; | ||
properties.customIsDrag = customIsDrag; | ||
properties.filterClipped = filterClipped; | ||
const translator = useMemo(() => new EventTranslator(id, true, dispatcher, (_, capturedEvents) => { | ||
if (objectRef.current == null) { | ||
return emptyIntersections; | ||
} | ||
objectRef.current.getWorldPosition(worldPositionHelper); | ||
objectRef.current.getWorldQuaternion(worldRotationHelper); | ||
return capturedEvents == null | ||
? //events not captured -> compute intersections normally | ||
intersectSphereFromObject(worldPositionHelper, worldRotationHelper, properties.radius, store.getState().scene, dispatcher, properties.filterClipped) | ||
: //event captured | ||
intersectSphereFromCapturedEvents(worldPositionHelper, worldRotationHelper, capturedEvents); | ||
}, (intersection) => { | ||
if (properties.distanceElement == null || intersection == null) { | ||
return pressedElementIds; | ||
}); | ||
}, [id, filterIntersections, radius, distanceElement, scene]); | ||
} | ||
if (intersection.distance <= properties.distanceElement.downRadius && | ||
// either the intersection is not captured (=> actualDistance == null) OR the actual distance to the object is smaller then 2x downRadius => if not we release the capture | ||
(intersection.actualDistance == null || | ||
intersection.actualDistance < | ||
2 * properties.distanceElement.downRadius * 2)) { | ||
pressedElementIds.add(properties.distanceElement.id); | ||
} | ||
else { | ||
pressedElementIds.delete(properties.distanceElement.id); | ||
} | ||
return pressedElementIds; | ||
}, (i1, i2) => properties.customIsDrag == null | ||
? isDragDefault(store.getState().camera, i1, i2) | ||
: properties.customIsDrag(i1, i2), (position, rotation) => { | ||
if (objectRef.current == null) { | ||
return; | ||
} | ||
objectRef.current.getWorldPosition(position); | ||
objectRef.current.getWorldQuaternion(rotation); | ||
}), [id, store]); | ||
translator.onIntersections = onIntersections; | ||
translator.filterIntersections = filterIntersections; | ||
useEffect(() => () => { | ||
@@ -63,8 +83,7 @@ if (distanceElement == null) { | ||
//cleanup translator | ||
useEffect(() => () => translator.leave({}), [translator]); | ||
useEffect(() => translator.leave.bind(translator, {}), [translator]); | ||
useFrame(() => { | ||
translator.update({}, true, distanceElement != null); | ||
onIntersections?.(translator.intersections); | ||
}); | ||
return React.createElement("object3D", { ref: objectRef }); | ||
}); |
import React from "react"; | ||
import { Event } from "three"; | ||
import { XIntersection } from "../index.js"; | ||
import { InputDeviceFunctions } from "./index.js"; | ||
import { ThreeEvent } from "@react-three/fiber"; | ||
export declare const XStraightPointer: React.ForwardRefExoticComponent<{ | ||
id: number; | ||
onIntersections?: ((intersections: Array<XIntersection>) => void) | undefined; | ||
onIntersections?: ((intersections: ReadonlyArray<XIntersection>) => void) | undefined; | ||
filterIntersections?: ((intersections: Array<XIntersection>) => Array<XIntersection>) | undefined; | ||
onPointerDownMissed?: ((event: ThreeEvent<Event>) => void) | undefined; | ||
onPointerUpMissed?: ((event: ThreeEvent<Event>) => void) | undefined; | ||
onClickMissed?: ((event: ThreeEvent<Event>) => void) | undefined; | ||
isDrag?: ((i1: XIntersection, i2: XIntersection) => boolean) | undefined; | ||
filterClipped?: boolean | undefined; | ||
} & React.RefAttributes<InputDeviceFunctions>>; |
/* eslint-disable react/display-name */ | ||
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, } from "react"; | ||
import { Quaternion, Vector3 } from "three"; | ||
import { EventTranslator } from "../index.js"; | ||
import { EventTranslator, isDragDefault } from "../index.js"; | ||
import { intersectRayFromCapturedEvents, intersectRayFromObject, } from "../intersections/ray.js"; | ||
import { R3FEventDispatcher } from "./index.js"; | ||
import { useFrame, useThree } from "@react-three/fiber"; | ||
import { useFrame, useStore } from "@react-three/fiber"; | ||
const emptyIntersections = []; | ||
const worldPositionHelper = new Vector3(); | ||
const worldRotationHelper = new Quaternion(); | ||
export const XStraightPointer = forwardRef(({ id, onIntersections, filterIntersections }, ref) => { | ||
export const XStraightPointer = forwardRef(({ id, onIntersections, filterIntersections, onClickMissed, onPointerDownMissed, onPointerUpMissed, isDrag: customIsDrag, filterClipped = true, }, ref) => { | ||
const store = useStore(); | ||
const objectRef = useRef(null); | ||
const scene = useThree(({ scene }) => scene); | ||
const dispatcher = useMemo(() => new R3FEventDispatcher(), []); | ||
dispatcher.onPointerDownMissed = onPointerDownMissed; | ||
dispatcher.onPointerUpMissed = onPointerUpMissed; | ||
dispatcher.onClickMissed = onClickMissed; | ||
const pressedElementIds = useMemo(() => new Set(), []); | ||
const translator = useMemo(() => { | ||
const dispatcher = new R3FEventDispatcher(); | ||
return new EventTranslator(id, false, dispatcher, (_, capturedEvents) => { | ||
if (objectRef.current == null) { | ||
return emptyIntersections; | ||
} | ||
objectRef.current.getWorldPosition(worldPositionHelper); | ||
objectRef.current.getWorldQuaternion(worldRotationHelper); | ||
if (capturedEvents != null) { | ||
return intersectRayFromCapturedEvents(worldPositionHelper, worldRotationHelper, capturedEvents); | ||
} | ||
return intersectRayFromObject(worldPositionHelper, worldRotationHelper, scene, dispatcher, filterIntersections); | ||
}, () => pressedElementIds); | ||
}, [id, filterIntersections, scene]); | ||
const properties = useMemo(() => ({ customIsDrag, filterClipped }), []); | ||
properties.customIsDrag = customIsDrag; | ||
properties.filterClipped = filterClipped; | ||
const translator = useMemo(() => new EventTranslator(id, false, dispatcher, (events, capturedEvents) => { | ||
if (objectRef.current == null) { | ||
return emptyIntersections; | ||
} | ||
objectRef.current.getWorldPosition(worldPositionHelper); | ||
objectRef.current.getWorldQuaternion(worldRotationHelper); | ||
return capturedEvents == null | ||
? //no events captured -> compute intersections normally | ||
intersectRayFromObject(worldPositionHelper, worldRotationHelper, store.getState().scene, dispatcher, properties.filterClipped) | ||
: //events captured | ||
intersectRayFromCapturedEvents(worldPositionHelper, worldRotationHelper, capturedEvents); | ||
}, () => pressedElementIds, (i1, i2) => properties.customIsDrag == null | ||
? isDragDefault(store.getState().camera, i1, i2) | ||
: properties.customIsDrag(i1, i2), (position, rotation) => { | ||
if (objectRef.current == null) { | ||
return; | ||
} | ||
objectRef.current.getWorldPosition(position); | ||
objectRef.current.getWorldQuaternion(rotation); | ||
}), [id, store]); | ||
translator.onIntersections = onIntersections; | ||
translator.filterIntersections = filterIntersections; | ||
useImperativeHandle(ref, () => ({ | ||
@@ -44,9 +59,8 @@ press: (id, event) => { | ||
//cleanup translator | ||
useEffect(() => () => translator.leave({}), [translator]); | ||
useEffect(() => translator.leave.bind(translator, {}), [translator]); | ||
//update translator every frame | ||
useFrame(() => { | ||
translator.update({}, true, false); | ||
onIntersections?.(translator.intersections); | ||
}); | ||
return React.createElement("object3D", { ref: objectRef }); | ||
}); |
@@ -0,5 +1,13 @@ | ||
import { ThreeEvent } from "@react-three/fiber"; | ||
import { XIntersection } from "../index.js"; | ||
import { Event } from "three"; | ||
import { XCameraRayIntersection } from "../intersections/ray.js"; | ||
export declare function XWebPointers({ onIntersections, filterIntersections, }: { | ||
onIntersections?: (id: number, intersections: Array<XCameraRayIntersection>) => void; | ||
export declare function XWebPointers({ onIntersections, filterIntersections, onClickMissed, onPointerDownMissed, onPointerUpMissed, isDrag: customIsDrag, filterClipped, }: { | ||
onIntersections?: (id: number, intersections: ReadonlyArray<XCameraRayIntersection>) => void; | ||
filterIntersections?: (intersections: Array<XCameraRayIntersection>) => Array<XCameraRayIntersection>; | ||
onPointerDownMissed?: (event: ThreeEvent<Event>) => void; | ||
onPointerUpMissed?: (event: ThreeEvent<Event>) => void; | ||
onClickMissed?: (event: ThreeEvent<Event>) => void; | ||
isDrag?: (i1: XIntersection, i2: XIntersection) => boolean; | ||
filterClipped?: boolean; | ||
}): null; |
import { useStore, useThree } from "@react-three/fiber"; | ||
import { useEffect, useMemo } from "react"; | ||
import { EventTranslator } from "../index.js"; | ||
import { EventTranslator, isDragDefault } from "../index.js"; | ||
import { R3FEventDispatcher } from "./index.js"; | ||
import { Vector2 } from "three"; | ||
import { Vector2, Vector3, Quaternion } from "three"; | ||
import { intersectRayFromCamera, intersectRayFromCameraCapturedEvents, } from "../intersections/ray.js"; | ||
export function XWebPointers({ onIntersections, filterIntersections, }) { | ||
const canvas = useThree(({ gl }) => gl.domElement); | ||
export function XWebPointers({ onIntersections, filterIntersections, onClickMissed, onPointerDownMissed, onPointerUpMissed, isDrag: customIsDrag, filterClipped = true, }) { | ||
const pointerMap = useMemo(() => new Map(), []); | ||
const store = useStore(); | ||
const dispatcher = useMemo(() => new R3FEventDispatcher(), []); | ||
dispatcher.onPointerDownMissed = onPointerDownMissed; | ||
dispatcher.onPointerUpMissed = onPointerUpMissed; | ||
dispatcher.onClickMissed = onClickMissed; | ||
//update properties for all pointers | ||
for (const [pointerId, entry] of pointerMap) { | ||
entry.translator.onIntersections = onIntersections?.bind(null, pointerId); | ||
entry.translator.filterIntersections = filterIntersections; | ||
entry.customIsDrag = customIsDrag; | ||
entry.filterClipped = filterClipped; | ||
} | ||
const canvas = useThree(({ gl }) => gl.domElement); | ||
useEffect(() => { | ||
const getOrCreate = getOrCreatePointerMapEntry.bind(null, pointerMap, () => store.getState()); | ||
const getOrCreate = (id) => getOrCreatePointerMapEntry(pointerMap, store, dispatcher, id); | ||
const pointercancel = (event) => { | ||
const { translator } = getOrCreate(event.pointerId, filterIntersections); | ||
const { translator } = getOrCreate(event.pointerId); | ||
translator.cancel(event); | ||
}; | ||
const pointerdown = (event) => { | ||
const { pressedInputDeviceElements, translator } = getOrCreate(event.pointerId, filterIntersections); | ||
const { pressedInputDeviceElements, translator } = getOrCreate(event.pointerId); | ||
updatePressedButtons(event.buttons, pressedInputDeviceElements); | ||
@@ -23,3 +34,3 @@ translator.update(event, false, true, event.button); | ||
const pointerup = (event) => { | ||
const { pressedInputDeviceElements, translator } = getOrCreate(event.pointerId, filterIntersections); | ||
const { pressedInputDeviceElements, translator } = getOrCreate(event.pointerId); | ||
updatePressedButtons(event.buttons, pressedInputDeviceElements); | ||
@@ -29,11 +40,9 @@ translator.update(event, false, true); | ||
const pointerover = (event) => { | ||
const { translator, pressedInputDeviceElements } = getOrCreate(event.pointerId, filterIntersections); | ||
const { translator, pressedInputDeviceElements } = getOrCreate(event.pointerId); | ||
updatePressedButtons(event.buttons, pressedInputDeviceElements); | ||
translator.update(event, true, true, event.button); | ||
onIntersections?.(event.pointerId, translator.intersections); | ||
}; | ||
const pointermove = (event) => { | ||
const { translator } = getOrCreate(event.pointerId, filterIntersections); | ||
const { translator } = getOrCreate(event.pointerId); | ||
translator.update(event, true, false); | ||
onIntersections?.(event.pointerId, translator.intersections); | ||
}; | ||
@@ -46,6 +55,5 @@ const wheel = (event) => { | ||
const pointerout = (event) => { | ||
const { translator } = getOrCreate(event.pointerId, filterIntersections); | ||
const { translator } = getOrCreate(event.pointerId); | ||
translator.leave(event); | ||
pointerMap.delete(event.pointerId); | ||
onIntersections?.(event.pointerId, emptyIntersection); | ||
}; | ||
@@ -76,3 +84,3 @@ const blur = (event) => { | ||
}; | ||
}, [canvas, filterIntersections, store]); | ||
}, [canvas, store]); | ||
return null; | ||
@@ -94,6 +102,6 @@ } | ||
} | ||
function getOrCreatePointerMapEntry(pointerMap, getState, pointerId, filterIntersections) { | ||
function getOrCreatePointerMapEntry(pointerMap, store, dispatcher, pointerId) { | ||
let entry = pointerMap.get(pointerId); | ||
if (entry == null) { | ||
pointerMap.set(pointerId, (entry = createPointerMapEntry(pointerId, getState, filterIntersections))); | ||
pointerMap.set(pointerId, (entry = createPointerMapEntry(pointerId, store, dispatcher))); | ||
} | ||
@@ -103,20 +111,25 @@ return entry; | ||
const emptyIntersection = []; | ||
function createPointerMapEntry(pointerId, getState, filterIntersections) { | ||
const pressedInputDeviceElements = new Set(); | ||
const dispatcher = new R3FEventDispatcher(); | ||
const translator = new EventTranslator(pointerId, false, dispatcher, (event, capturedEvents) => { | ||
if (!(event.target instanceof HTMLCanvasElement)) { | ||
return emptyIntersection; | ||
} | ||
const { camera, scene, size } = getState(); | ||
const coords = new Vector2((event.offsetX / size.width) * 2 - 1, -(event.offsetY / size.height) * 2 + 1); | ||
if (capturedEvents == null) { | ||
return intersectRayFromCamera(camera, coords, scene, dispatcher, filterIntersections); | ||
} | ||
return intersectRayFromCameraCapturedEvents(camera, coords, capturedEvents); | ||
}, () => pressedInputDeviceElements); | ||
return { | ||
pressedInputDeviceElements, | ||
translator, | ||
function createPointerMapEntry(pointerId, store, dispatcher) { | ||
const lastWorldPosition = new Vector3(); | ||
const lastWorldRotation = new Quaternion(); | ||
const pointerMapEntry = { | ||
filterClipped: true, | ||
pressedInputDeviceElements: new Set(), | ||
translator: new EventTranslator(pointerId, false, dispatcher, (event, capturedEvents) => { | ||
if (!(event.target instanceof HTMLCanvasElement)) { | ||
return emptyIntersection; | ||
} | ||
const { camera, scene, size } = store.getState(); | ||
const coords = new Vector2((event.offsetX / size.width) * 2 - 1, -(event.offsetY / size.height) * 2 + 1); | ||
return capturedEvents == null | ||
? intersectRayFromCamera(camera, coords, scene, dispatcher, pointerMapEntry.filterClipped, lastWorldPosition, lastWorldRotation) | ||
: intersectRayFromCameraCapturedEvents(camera, coords, capturedEvents, lastWorldPosition, lastWorldRotation); | ||
}, () => pointerMapEntry.pressedInputDeviceElements, (i1, i2) => pointerMapEntry.customIsDrag == null | ||
? isDragDefault(store.getState().camera, i1, i2) | ||
: pointerMapEntry.customIsDrag(i1, i2), (position, rotation) => { | ||
position.copy(lastWorldPosition); | ||
rotation.copy(lastWorldRotation); | ||
}), | ||
}; | ||
return pointerMapEntry; | ||
} |
{ | ||
"name": "@coconut-xr/xinteraction", | ||
"version": "0.0.2", | ||
"version": "0.0.3", | ||
"homepage": "https://coconut-xr.github.io/xinteraction", | ||
@@ -5,0 +5,0 @@ "license": "SEE LICENSE IN LICENSE", |
@@ -12,3 +12,3 @@ ![header image](./images/header.jpg) | ||
**xinteraction** translates events from input devices (e.g. Mouse, 6DOF Controller, Hand) into events on 3D Objects in a Three.js Scene. | ||
**xinteraction** translates events from input devices (e.g., Mouse, 6DOF Controller, Hand) into events on 3D Objects in a Three.js Scene. | ||
@@ -28,2 +28,2 @@ | ||
* [Distance Based Input Device](https://coconut-xr.github.io/xinteraction/#/distance.md) Explains interactions like touching or grabbing | ||
* [Event Capture](https://coconut-xr.github.io/xinteraction/#/event-capture.md) Explains event capture for dragging |
67928
1285
27