@threlte/xr
Version:
Tools to more easily create VR and AR experiences with Threlte
92 lines (91 loc) • 3.55 kB
JavaScript
class InputSourcesState {
current = $state.raw([]);
}
export const inputSources = new InputSourcesState();
const subscribers = new Set();
/**
* Registers callbacks with the module-level XR input-source dispatcher.
*
* This does not subscribe to a specific `XRInputSource`, `XRSession`, or
* Three.js object. Instead, the subscriber is stored in the internal
* `subscribers` set and receives events for whichever current input-source
* state matches its `type` and `handedness`.
*
* For example, a `{ type: 'controller', handedness: 'left' }` subscriber will
* receive forwarded events for the current left controller, even if the
* underlying `XRInputSource` instance disconnects and reconnects.
*
* Returns a cleanup function that removes the subscriber from the dispatcher.
*/
export const addSubscriber = (sub) => {
subscribers.add(sub);
return () => {
subscribers.delete(sub);
};
};
export const dispatchEvent = (state, eventType, event) => {
const key = `on${eventType}`;
for (const sub of subscribers) {
if (sub.type !== state.type)
continue;
if (sub.handedness !== state.handedness)
continue;
const cb = sub.callbacks[key];
cb?.(event);
}
};
const dispatchSpaceEvent = (state, event) => {
state.targetRay.dispatchEvent(event);
if (state.type === 'controller') {
state.grip.dispatchEvent(event);
}
if (state.type === 'hand') {
state.hand.dispatchEvent(event);
}
};
export const createInputSourceEvent = (state, type, extra = {}) => {
return {
type,
data: state.inputSource,
inputSource: state.inputSource,
target: state.type === 'hand' ? state.hand : state.targetRay,
...extra
};
};
export const dispatchInputSourceStateEvent = (state, eventType, event, options = {}) => {
if (options.dispatchSpaces !== false) {
dispatchSpaceEvent(state, event);
}
dispatchEvent(state, eventType, event);
};
const getPreferredState = (predicate, options) => {
if (options?.isPrimary !== undefined) {
return inputSources.current.find((state) => state.isPrimary === options.isPrimary && predicate(state));
}
return (inputSources.current.find((state) => state.isPrimary && predicate(state)) ??
inputSources.current.find(predicate));
};
export const getInputSourceState = (inputSource, options) => {
if (options?.isPrimary !== undefined) {
return inputSources.current.find((state) => state.inputSource === inputSource && state.isPrimary === options.isPrimary);
}
return inputSources.current.find((state) => state.inputSource === inputSource);
};
export const getControllerState = (handedness, options) => {
return getPreferredState((state) => state.type === 'controller' && state.handedness === handedness, options);
};
export const getHandState = (handedness, options) => {
return getPreferredState((state) => state.type === 'hand' && state.handedness === handedness, options);
};
export const dispatchInputSourceEvent = (inputSource, eventType, event) => {
const state = getInputSourceState(inputSource);
if (state === undefined)
return;
dispatchInputSourceStateEvent(state, eventType, event);
};
export const dispatchXRInputSourceEvent = (event) => {
const state = getInputSourceState(event.inputSource);
if (state === undefined)
return;
dispatchInputSourceStateEvent(state, event.type, createInputSourceEvent(state, event.type, { frame: event.frame }));
};