playcanvas
Version:
PlayCanvas WebGL game engine
424 lines (421 loc) • 15.3 kB
JavaScript
import { EventHandler } from '../../core/event-handler.js';
import { platform } from '../../core/platform.js';
import { XrAnchor } from './xr-anchor.js';
/**
* @import { Quat } from '../../core/math/quat.js'
* @import { Vec3 } from '../../core/math/vec3.js'
* @import { XrAnchorForgetCallback } from './xr-anchor.js'
* @import { XrManager } from './xr-manager.js'
*/ /**
* Callback used by {@link XrAnchors#create}.
*
* @callback XrAnchorCreateCallback
* @param {Error|null} err - The Error object if failed to create an anchor or null.
* @param {XrAnchor|null} anchor - The anchor that is tracked against real world geometry.
*/ /**
* Anchors provide an ability to specify a point in the world that needs to be updated to
* correctly reflect the evolving understanding of the world by the underlying AR system,
* such that the anchor remains aligned with the same place in the physical world.
* Anchors tend to persist better relative to the real world, especially during a longer
* session with lots of movement.
*
* ```javascript
* app.xr.start(camera, pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {
* anchors: true
* });
* ```
*
* @category XR
*/ class XrAnchors extends EventHandler {
/** @private */ _onSessionStart() {
var available = this.manager.session.enabledFeatures.indexOf('anchors') !== -1;
if (!available) return;
this._available = available;
this.fire('available');
}
/** @private */ _onSessionEnd() {
if (!this._available) return;
this._available = false;
// clear anchor creation queue
for(var i = 0; i < this._creationQueue.length; i++){
if (!this._creationQueue[i].callback) {
continue;
}
this._creationQueue[i].callback(new Error('session ended'), null);
}
this._creationQueue.length = 0;
this._index.clear();
this._indexByUuid.clear();
// destroy all anchors
var i1 = this._list.length;
while(i1--){
this._list[i1].destroy();
}
this._list.length = 0;
this.fire('unavailable');
}
/**
* @param {XRAnchor} xrAnchor - XRAnchor that has been added.
* @param {string|null} [uuid] - UUID string associated with persistent anchor.
* @returns {XrAnchor} new instance of XrAnchor.
* @private
*/ _createAnchor(xrAnchor, uuid) {
if (uuid === void 0) uuid = null;
var anchor = new XrAnchor(this, xrAnchor, uuid);
this._index.set(xrAnchor, anchor);
if (uuid) this._indexByUuid.set(uuid, anchor);
this._list.push(anchor);
anchor.once('destroy', this._onAnchorDestroy, this);
return anchor;
}
/**
* @param {XRAnchor} xrAnchor - XRAnchor that has been destroyed.
* @param {XrAnchor} anchor - Anchor that has been destroyed.
* @private
*/ _onAnchorDestroy(xrAnchor, anchor) {
this._index.delete(xrAnchor);
if (anchor.uuid) this._indexByUuid.delete(anchor.uuid);
var ind = this._list.indexOf(anchor);
if (ind !== -1) this._list.splice(ind, 1);
this.fire('destroy', anchor);
}
/**
* Create an anchor using position and rotation, or from hit test result.
*
* @param {Vec3|XRHitTestResult} position - Position for an anchor or a hit test result.
* @param {Quat|XrAnchorCreateCallback} [rotation] - Rotation for an anchor or a callback if
* creating from a hit test result.
* @param {XrAnchorCreateCallback} [callback] - Callback to fire when anchor was created or
* failed to be created.
* @example
* // create an anchor using a position and rotation
* app.xr.anchors.create(position, rotation, (err, anchor) => {
* if (!err) {
* // new anchor has been created
* }
* });
* @example
* // create an anchor from a hit test result
* hitTestSource.on('result', (position, rotation, inputSource, hitTestResult) => {
* app.xr.anchors.create(hitTestResult, function (err, anchor) {
* if (!err) {
* // new anchor has been created
* }
* });
* });
*/ create(position, rotation, callback) {
if (!this._available) {
callback == null ? void 0 : callback(new Error('Anchors API is not available'), null);
return;
}
if (window.XRHitTestResult && position instanceof XRHitTestResult) {
var hitResult = position;
callback = rotation;
if (!this._supported) {
callback == null ? void 0 : callback(new Error('Anchors API is not supported'), null);
return;
}
if (!hitResult.createAnchor) {
callback == null ? void 0 : callback(new Error('Creating Anchor from Hit Test is not supported'), null);
return;
}
hitResult.createAnchor().then((xrAnchor)=>{
var anchor = this._createAnchor(xrAnchor);
callback == null ? void 0 : callback(null, anchor);
this.fire('add', anchor);
}).catch((ex)=>{
callback == null ? void 0 : callback(ex, null);
this.fire('error', ex);
});
} else {
this._creationQueue.push({
transform: new XRRigidTransform(position, rotation),
callback: callback
});
}
}
/**
* Restore anchor using persistent UUID.
*
* @param {string} uuid - UUID string associated with persistent anchor.
* @param {XrAnchorCreateCallback} [callback] - Callback to fire when anchor was created or
* failed to be created.
* @example
* // restore an anchor using uuid string
* app.xr.anchors.restore(uuid, function (err, anchor) {
* if (!err) {
* // new anchor has been created
* }
* });
* @example
* // restore all available persistent anchors
* const uuids = app.xr.anchors.uuids;
* for(let i = 0; i < uuids.length; i++) {
* app.xr.anchors.restore(uuids[i]);
* }
*/ restore(uuid, callback) {
if (!this._available) {
callback == null ? void 0 : callback(new Error('Anchors API is not available'), null);
return;
}
if (!this._persistence) {
callback == null ? void 0 : callback(new Error('Anchor Persistence is not supported'), null);
return;
}
if (!this.manager.active) {
callback == null ? void 0 : callback(new Error('WebXR session is not active'), null);
return;
}
this.manager.session.restorePersistentAnchor(uuid).then((xrAnchor)=>{
var anchor = this._createAnchor(xrAnchor, uuid);
callback == null ? void 0 : callback(null, anchor);
this.fire('add', anchor);
}).catch((ex)=>{
callback == null ? void 0 : callback(ex, null);
this.fire('error', ex);
});
}
/**
* Forget an anchor by removing its UUID from underlying systems.
*
* @param {string} uuid - UUID string associated with persistent anchor.
* @param {XrAnchorForgetCallback} [callback] - Callback to fire when anchor persistent data
* was removed or error if failed.
* @example
* // forget all available anchors
* const uuids = app.xr.anchors.uuids;
* for (let i = 0; i < uuids.length; i++) {
* app.xr.anchors.forget(uuids[i]);
* }
*/ forget(uuid, callback) {
if (!this._available) {
callback == null ? void 0 : callback(new Error('Anchors API is not available'));
return;
}
if (!this._persistence) {
callback == null ? void 0 : callback(new Error('Anchor Persistence is not supported'));
return;
}
if (!this.manager.active) {
callback == null ? void 0 : callback(new Error('WebXR session is not active'));
return;
}
this.manager.session.deletePersistentAnchor(uuid).then(()=>{
callback == null ? void 0 : callback(null);
}).catch((ex)=>{
callback == null ? void 0 : callback(ex);
this.fire('error', ex);
});
}
/**
* @param {XRFrame} frame - XRFrame from requestAnimationFrame callback.
* @ignore
*/ update(frame) {
if (!this._available) {
// enabledFeatures - is not available, requires alternative way to check feature availability
if (!this.manager.session.enabledFeatures && !this._checkingAvailability) {
this._checkingAvailability = true;
frame.createAnchor(new XRRigidTransform(), this.manager._referenceSpace).then((xrAnchor)=>{
// successfully created an anchor - feature is available
xrAnchor.delete();
if (this.manager.active) {
this._available = true;
this.fire('available');
}
}).catch(()=>{}); // stay unavailable
}
return;
}
// check if need to create anchors
if (this._creationQueue.length) {
var _this, _loop = function(i) {
var request = _this._creationQueue[i];
frame.createAnchor(request.transform, _this.manager._referenceSpace).then((xrAnchor)=>{
if (request.callback) {
_this._callbacksAnchors.set(xrAnchor, request.callback);
}
}).catch((ex)=>{
if (request.callback) {
request.callback(ex, null);
}
_this.fire('error', ex);
});
};
for(var i = 0; i < this._creationQueue.length; i++)_this = this, _loop(i);
this._creationQueue.length = 0;
}
// check if destroyed
for (var [xrAnchor, anchor] of this._index){
if (frame.trackedAnchors.has(xrAnchor)) {
continue;
}
this._index.delete(xrAnchor);
anchor.destroy();
}
// update existing anchors
for(var i1 = 0; i1 < this._list.length; i1++){
this._list[i1].update(frame);
}
// check if added
for (var xrAnchor1 of frame.trackedAnchors){
if (this._index.has(xrAnchor1)) {
continue;
}
try {
var tmp = xrAnchor1.anchorSpace; // eslint-disable-line no-unused-vars
} catch (ex) {
continue;
}
var anchor1 = this._createAnchor(xrAnchor1);
anchor1.update(frame);
var callback = this._callbacksAnchors.get(xrAnchor1);
if (callback) {
this._callbacksAnchors.delete(xrAnchor1);
callback(null, anchor1);
}
this.fire('add', anchor1);
}
}
/**
* True if Anchors are supported.
*
* @type {boolean}
*/ get supported() {
return this._supported;
}
/**
* True if Anchors are available. This information is available only when session has started.
*
* @type {boolean}
*/ get available() {
return this._available;
}
/**
* True if Anchors support persistence.
*
* @type {boolean}
*/ get persistence() {
return this._persistence;
}
/**
* Array of UUID strings of persistent anchors, or null if not available.
*
* @type {null|string[]}
*/ get uuids() {
if (!this._available) {
return null;
}
if (!this._persistence) {
return null;
}
if (!this.manager.active) {
return null;
}
return this.manager.session.persistentAnchors;
}
/**
* List of available {@link XrAnchor}s.
*
* @type {XrAnchor[]}
*/ get list() {
return this._list;
}
/**
* Create a new XrAnchors instance.
*
* @param {XrManager} manager - WebXR Manager.
* @ignore
*/ constructor(manager){
var _window_XRSession, _window;
super(), /**
* @type {boolean}
* @private
*/ this._supported = platform.browser && !!window.XRAnchor, /**
* @type {boolean}
* @private
*/ this._available = false, /**
* @type {boolean}
* @private
*/ this._checkingAvailability = false, /**
* @type {boolean}
* @private
*/ this._persistence = platform.browser && !!((_window = window) == null ? void 0 : (_window_XRSession = _window.XRSession) == null ? void 0 : _window_XRSession.prototype.restorePersistentAnchor), /**
* List of anchor creation requests.
*
* @type {object[]}
* @private
*/ this._creationQueue = [], /**
* Index of XrAnchors, with XRAnchor (native handle) used as a key.
*
* @type {Map<XRAnchor,XrAnchor>}
* @private
*/ this._index = new Map(), /**
* Index of XrAnchors, with UUID (persistent string) used as a key.
*
* @type {Map<string,XrAnchor>}
* @private
*/ this._indexByUuid = new Map(), /**
* @type {XrAnchor[]}
* @private
*/ this._list = [], /**
* Map of callbacks to XRAnchors so that we can call its callback once an anchor is updated
* with a pose for the first time.
*
* @type {Map<XrAnchor, XrAnchorCreateCallback>}
* @private
*/ this._callbacksAnchors = new Map();
this.manager = manager;
if (this._supported) {
this.manager.on('start', this._onSessionStart, this);
this.manager.on('end', this._onSessionEnd, this);
}
}
}
/**
* Fired when anchors become available.
*
* @event
* @example
* app.xr.anchors.on('available', () => {
* console.log('Anchors are available');
* });
*/ XrAnchors.EVENT_AVAILABLE = 'available';
/**
* Fired when anchors become unavailable.
*
* @event
* @example
* app.xr.anchors.on('unavailable', () => {
* console.log('Anchors are unavailable');
* });
*/ XrAnchors.EVENT_UNAVAILABLE = 'unavailable';
/**
* Fired when an anchor failed to be created. The handler is passed an Error object.
*
* @event
* @example
* app.xr.anchors.on('error', (err) => {
* console.error(err.message);
* });
*/ XrAnchors.EVENT_ERROR = 'error';
/**
* Fired when a new {@link XrAnchor} is added. The handler is passed the {@link XrAnchor} that
* was added.
*
* @event
* @example
* app.xr.anchors.on('add', (anchor) => {
* console.log('Anchor added');
* });
*/ XrAnchors.EVENT_ADD = 'add';
/**
* Fired when an {@link XrAnchor} is destroyed. The handler is passed the {@link XrAnchor} that
* was destroyed.
*
* @event
* @example
* app.xr.anchors.on('destroy', (anchor) => {
* console.log('Anchor destroyed');
* });
*/ XrAnchors.EVENT_DESTROY = 'destroy';
export { XrAnchors };