playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
321 lines (320 loc) • 11.6 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
import { platform } from "../../core/platform.js";
import { EventHandler } from "../../core/event-handler.js";
import { XRSPACE_VIEWER } from "./constants.js";
import { XrHitTestSource } from "./xr-hit-test-source.js";
class XrHitTest extends EventHandler {
/**
* Create a new XrHitTest instance.
*
* @param {XrManager} manager - WebXR Manager.
* @ignore
*/
constructor(manager) {
super();
/**
* @type {XrManager}
* @private
*/
__publicField(this, "manager");
/**
* @type {boolean}
* @private
*/
__publicField(this, "_supported", platform.browser && !!(window.XRSession && window.XRSession.prototype.requestHitTestSource));
/** @private */
__publicField(this, "_available", false);
/** @private */
__publicField(this, "_checkingAvailability", false);
/**
* List of active {@link XrHitTestSource}.
*
* @type {XrHitTestSource[]}
*/
__publicField(this, "sources", []);
this.manager = manager;
if (this._supported) {
this.manager.on("start", this._onSessionStart, this);
this.manager.on("end", this._onSessionEnd, this);
}
}
/** @private */
_onSessionStart() {
if (this.manager.session.enabledFeatures) {
const available = this.manager.session.enabledFeatures.indexOf("hit-test") !== -1;
if (!available) return;
this._available = available;
this.fire("available");
} else if (!this._checkingAvailability) {
this._checkingAvailability = true;
this.manager.session.requestReferenceSpace(XRSPACE_VIEWER).then((referenceSpace) => {
this.manager.session.requestHitTestSource({
space: referenceSpace
}).then((hitTestSource) => {
hitTestSource.cancel();
if (this.manager.active) {
this._available = true;
this.fire("available");
}
}).catch(() => {
});
}).catch(() => {
});
}
}
/** @private */
_onSessionEnd() {
if (!this._available) return;
this._available = false;
for (let i = 0; i < this.sources.length; i++) {
this.sources[i].onStop();
}
this.sources = [];
this.fire("unavailable");
}
/**
* Attempts to start hit test with provided reference space.
*
* @param {object} [options] - Optional object for passing arguments.
* @param {string} [options.spaceType] - Reference space type. Defaults to
* {@link XRSPACE_VIEWER}. Can be one of the following:
*
* - {@link XRSPACE_VIEWER}: Viewer - hit test will be facing relative to viewers space.
* - {@link XRSPACE_LOCAL}: Local - represents a tracking space with a native origin near the
* viewer at the time of creation.
* - {@link XRSPACE_LOCALFLOOR}: Local Floor - represents a tracking space with a native origin
* at the floor in a safe position for the user to stand. The y axis equals 0 at floor level.
* Floor level value might be estimated by the underlying platform.
* - {@link XRSPACE_BOUNDEDFLOOR}: Bounded Floor - represents a tracking space with its native
* origin at the floor, where the user is expected to move within a pre-established boundary.
* - {@link XRSPACE_UNBOUNDED}: Unbounded - represents a tracking space where the user is
* expected to move freely around their environment, potentially long distances from their
* starting point.
*
* @param {string} [options.profile] - if hit test source meant to match input source instead
* of reference space, then name of profile of the {@link XrInputSource} should be provided.
* @param {string[]} [options.entityTypes] - Optional list of underlying entity types against
* which hit tests will be performed. Defaults to [ {@link XRTRACKABLE_PLANE} ]. Can be any
* combination of the following:
*
* - {@link XRTRACKABLE_POINT}: Point - indicates that the hit test results will be computed
* based on the feature points detected by the underlying Augmented Reality system.
* - {@link XRTRACKABLE_PLANE}: Plane - indicates that the hit test results will be computed
* based on the planes detected by the underlying Augmented Reality system.
* - {@link XRTRACKABLE_MESH}: Mesh - indicates that the hit test results will be computed
* based on the meshes detected by the underlying Augmented Reality system.
*
* @param {Ray} [options.offsetRay] - Optional ray by which
* hit test ray can be offset.
* @param {XrHitTestStartCallback} [options.callback] - Optional callback function called once
* hit test source is created or failed.
* @example
* // start hit testing from viewer position facing forwards
* app.xr.hitTest.start({
* spaceType: pc.XRSPACE_VIEWER,
* callback: (err, hitTestSource) => {
* if (err) return;
* hitTestSource.on('result', (position, rotation) => {
* // position and rotation of hit test result
* });
* }
* });
* @example
* // start hit testing using an arbitrary ray
* const ray = new pc.Ray(new pc.Vec3(0, 0, 0), new pc.Vec3(0, -1, 0));
* app.xr.hitTest.start({
* spaceType: pc.XRSPACE_LOCAL,
* offsetRay: ray,
* callback: (err, hitTestSource) => {
* // hit test source that will sample real world geometry straight down
* // from the position where AR session started
* }
* });
* @example
* // start hit testing for touch screen taps
* app.xr.hitTest.start({
* profile: 'generic-touchscreen',
* callback: (err, hitTestSource) => {
* if (err) return;
* hitTestSource.on('result', (position, rotation, inputSource) => {
* // position and rotation of hit test result
* // that will be created from touch on mobile devices
* });
* }
* });
*/
start(options = {}) {
if (!this._supported) {
options.callback?.(new Error("XR HitTest is not supported"), null);
return;
}
if (!this._available) {
options.callback?.(new Error("XR HitTest is not available"), null);
return;
}
if (!options.profile && !options.spaceType) {
options.spaceType = XRSPACE_VIEWER;
}
let xrRay;
const offsetRay = options.offsetRay;
if (offsetRay) {
const origin = new DOMPoint(offsetRay.origin.x, offsetRay.origin.y, offsetRay.origin.z, 1);
const direction = new DOMPoint(offsetRay.direction.x, offsetRay.direction.y, offsetRay.direction.z, 0);
xrRay = new XRRay(origin, direction);
}
const callback = options.callback;
if (options.spaceType) {
this.manager.session.requestReferenceSpace(options.spaceType).then((referenceSpace) => {
if (!this.manager.session) {
const err = new Error("XR Session is not started (2)");
if (callback) callback(err);
this.fire("error", err);
return;
}
this.manager.session.requestHitTestSource({
space: referenceSpace,
entityTypes: options.entityTypes || void 0,
offsetRay: xrRay
}).then((xrHitTestSource) => {
this._onHitTestSource(xrHitTestSource, false, options.inputSource, callback);
}).catch((ex) => {
if (callback) callback(ex);
this.fire("error", ex);
});
}).catch((ex) => {
if (callback) callback(ex);
this.fire("error", ex);
});
} else {
this.manager.session.requestHitTestSourceForTransientInput({
profile: options.profile,
entityTypes: options.entityTypes || void 0,
offsetRay: xrRay
}).then((xrHitTestSource) => {
this._onHitTestSource(xrHitTestSource, true, options.inputSource, callback);
}).catch((ex) => {
if (callback) callback(ex);
this.fire("error", ex);
});
}
}
/**
* @param {XRHitTestSource} xrHitTestSource - Hit test source.
* @param {boolean} transient - True if hit test source is created from transient input source.
* @param {XrInputSource|null} inputSource - Input Source with which hit test source is associated with.
* @param {Function} callback - Callback called once hit test source is created.
* @private
*/
_onHitTestSource(xrHitTestSource, transient, inputSource, callback) {
if (!this.manager.session) {
xrHitTestSource.cancel();
const err = new Error("XR Session is not started (3)");
if (callback) callback(err);
this.fire("error", err);
return;
}
const hitTestSource = new XrHitTestSource(this.manager, xrHitTestSource, transient, inputSource ?? null);
this.sources.push(hitTestSource);
if (callback) callback(null, hitTestSource);
this.fire("add", hitTestSource);
}
/**
* @param {XRFrame} frame - XRFrame from requestAnimationFrame callback.
* @ignore
*/
update(frame) {
if (!this._available) {
return;
}
for (let i = 0; i < this.sources.length; i++) {
this.sources[i].update(frame);
}
}
/**
* True if AR Hit Test is supported.
*
* @type {boolean}
*/
get supported() {
return this._supported;
}
/**
* True if Hit Test is available. This information is available only when the session has started.
*
* @type {boolean}
*/
get available() {
return this._available;
}
}
/**
* Fired when hit test becomes available.
*
* @event
* @example
* app.xr.hitTest.on('available', () => {
* console.log('Hit Testing is available');
* });
*/
__publicField(XrHitTest, "EVENT_AVAILABLE", "available");
/**
* Fired when hit test becomes unavailable.
*
* @event
* @example
* app.xr.hitTest.on('unavailable', () => {
* console.log('Hit Testing is unavailable');
* });
*/
__publicField(XrHitTest, "EVENT_UNAVAILABLE", "unavailable");
/**
* Fired when new {@link XrHitTestSource} is added to the list. The handler is passed the
* {@link XrHitTestSource} object that has been added.
*
* @event
* @example
* app.xr.hitTest.on('add', (hitTestSource) => {
* // new hit test source is added
* });
*/
__publicField(XrHitTest, "EVENT_ADD", "add");
/**
* Fired when {@link XrHitTestSource} is removed to the list. The handler is passed the
* {@link XrHitTestSource} object that has been removed.
*
* @event
* @example
* app.xr.hitTest.on('remove', (hitTestSource) => {
* // hit test source is removed
* });
*/
__publicField(XrHitTest, "EVENT_REMOVE", "remove");
/**
* Fired when hit test source receives new results. It provides transform information that
* tries to match real world picked geometry. The handler is passed the {@link XrHitTestSource}
* that produced the hit result, the {@link Vec3} position, the {@link Quat} rotation and the
* {@link XrInputSource} (if it is a transient hit test source).
*
* @event
* @example
* app.xr.hitTest.on('result', (hitTestSource, position, rotation, inputSource) => {
* target.setPosition(position);
* target.setRotation(rotation);
* });
*/
__publicField(XrHitTest, "EVENT_RESULT", "result");
/**
* Fired when failed create hit test source. The handler is passed the Error object.
*
* @event
* @example
* app.xr.hitTest.on('error', (err) => {
* console.error(err.message);
* });
*/
__publicField(XrHitTest, "EVENT_ERROR", "error");
export {
XrHitTest
};