From 39c7b7c2a61c897507ed1dc90b43e00484c0300e Mon Sep 17 00:00:00 2001 From: mrmaxm Date: Wed, 1 Nov 2023 14:41:32 +0200 Subject: [PATCH 01/11] fix hit test providing multiple results --- examples/src/examples/xr/ar-hit-test.mjs | 29 ++++++++++++++++++++++++ src/framework/xr/xr-hit-test-source.js | 3 +++ src/framework/xr/xr-input-source.js | 8 +++++++ 3 files changed, 40 insertions(+) diff --git a/examples/src/examples/xr/ar-hit-test.mjs b/examples/src/examples/xr/ar-hit-test.mjs index 9589f5791f6..8c79e8a0920 100644 --- a/examples/src/examples/xr/ar-hit-test.mjs +++ b/examples/src/examples/xr/ar-hit-test.mjs @@ -140,6 +140,35 @@ async function example({ canvas }) { } }); + if (app.xr.hitTest.supported) { + app.xr.input.on('add', (inputSource) => { + inputSource.hitTestStart({ + entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE], + callback: (err, hitTestSource) => { + if (err) return; + + let target = new pc.Entity(); + target.addComponent("render", { + type: "cylinder" + }); + target.setLocalScale(0.5, 0.01, 0.5); + app.root.addChild(target); + + hitTestSource.on('result', (position, rotation, item) => { + target.setPosition(position); + target.setRotation(rotation); + console.log(inputSource.id, item.id); + }); + + hitTestSource.once('remove', () => { + target.destroy(); + target = null; + }); + } + }); + }); + } + if (!app.xr.isAvailable(pc.XRTYPE_AR)) { message("Immersive AR is not available"); } else if (!app.xr.hitTest.supported) { diff --git a/src/framework/xr/xr-hit-test-source.js b/src/framework/xr/xr-hit-test-source.js index 3cec300c314..a970596fa6a 100644 --- a/src/framework/xr/xr-hit-test-source.js +++ b/src/framework/xr/xr-hit-test-source.js @@ -132,6 +132,9 @@ class XrHitTestSource extends EventHandler { * @private */ updateHitResults(results, inputSource) { + if (inputSource && inputSource.hitTestSourcesSet.has(this)) + return; + for (let i = 0; i < results.length; i++) { const pose = results[i].getPose(this.manager._referenceSpace); diff --git a/src/framework/xr/xr-input-source.js b/src/framework/xr/xr-input-source.js index b3acc522abb..13cdab9a7f3 100644 --- a/src/framework/xr/xr-input-source.js +++ b/src/framework/xr/xr-input-source.js @@ -139,6 +139,12 @@ class XrInputSource extends EventHandler { */ _hitTestSources = []; + /** + * @type {Set} + * @ignore + */ + hitTestSourcesSet = new Set(); + /** * Create a new XrInputSource instance. * @@ -610,6 +616,7 @@ class XrInputSource extends EventHandler { * @private */ onHitTestSourceAdd(hitTestSource) { + this.hitTestSourcesSet.add(hitTestSource); this._hitTestSources.push(hitTestSource); this.fire('hittest:add', hitTestSource); @@ -632,6 +639,7 @@ class XrInputSource extends EventHandler { * @private */ onHitTestSourceRemove(hitTestSource) { + this.hitTestSourcesSet.delete(hitTestSource); const ind = this._hitTestSources.indexOf(hitTestSource); if (ind !== -1) this._hitTestSources.splice(ind, 1); } From 09cea5f73e74fe566074eb3087ec03b569eac606 Mon Sep 17 00:00:00 2001 From: mrmaxm Date: Wed, 1 Nov 2023 14:44:45 +0200 Subject: [PATCH 02/11] remove debug comment --- examples/src/examples/xr/ar-hit-test.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/src/examples/xr/ar-hit-test.mjs b/examples/src/examples/xr/ar-hit-test.mjs index 8c79e8a0920..1b58a907ad2 100644 --- a/examples/src/examples/xr/ar-hit-test.mjs +++ b/examples/src/examples/xr/ar-hit-test.mjs @@ -157,7 +157,6 @@ async function example({ canvas }) { hitTestSource.on('result', (position, rotation, item) => { target.setPosition(position); target.setRotation(rotation); - console.log(inputSource.id, item.id); }); hitTestSource.once('remove', () => { From a8383bc720cc780d62b0251abe467b30c871c494 Mon Sep 17 00:00:00 2001 From: mrmaxm Date: Wed, 1 Nov 2023 17:03:41 +0200 Subject: [PATCH 03/11] wip --- .../examples/xr/ar-anchors-persistence.mjs | 239 ++++++++++++++++ .../src/examples/xr/ar-hit-test-anchors.mjs | 258 ++++++++++++++++++ examples/src/examples/xr/index.mjs | 2 + src/framework/xr/xr-anchor.js | 72 ++++- src/framework/xr/xr-anchors.js | 139 +++++++++- src/framework/xr/xr-hit-test-source.js | 7 +- src/framework/xr/xr-manager.js | 7 + 7 files changed, 714 insertions(+), 10 deletions(-) create mode 100644 examples/src/examples/xr/ar-anchors-persistence.mjs create mode 100644 examples/src/examples/xr/ar-hit-test-anchors.mjs diff --git a/examples/src/examples/xr/ar-anchors-persistence.mjs b/examples/src/examples/xr/ar-anchors-persistence.mjs new file mode 100644 index 00000000000..6f4c0383954 --- /dev/null +++ b/examples/src/examples/xr/ar-anchors-persistence.mjs @@ -0,0 +1,239 @@ +import * as pc from 'playcanvas'; + +/** + * @typedef {import('../../options.mjs').ExampleOptions} ExampleOptions + * @param {import('../../options.mjs').ExampleOptions} options - The example options. + * @returns {Promise} The example application. + */ +async function example({ canvas }) { + /** + * @param {string} msg - The message. + */ + const message = function (msg) { + /** @type {HTMLDivElement} */ + let el = document.querySelector('.message'); + if (!el) { + el = document.createElement('div'); + el.classList.add('message'); + document.body.append(el); + } + el.textContent = msg; + }; + + const app = new pc.Application(canvas, { + mouse: new pc.Mouse(canvas), + touch: new pc.TouchDevice(canvas), + keyboard: new pc.Keyboard(window), + graphicsDeviceOptions: { alpha: true } + }); + app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW); + app.setCanvasResolution(pc.RESOLUTION_AUTO); + + // Ensure canvas is resized when window changes size + const resize = () => app.resizeCanvas(); + window.addEventListener('resize', resize); + app.on('destroy', () => { + window.removeEventListener('resize', resize); + }); + + // use device pixel ratio + app.graphicsDevice.maxPixelRatio = window.devicePixelRatio; + + app.start(); + + // create camera + const c = new pc.Entity(); + c.addComponent('camera', { + clearColor: new pc.Color(0, 0, 0, 0), + farClip: 10000 + }); + app.root.addChild(c); + + const l = new pc.Entity(); + l.addComponent("light", { + type: "spot", + range: 30 + }); + l.translate(0, 10, 0); + app.root.addChild(l); + + const cone = new pc.Entity(); + cone.addComponent('render', { + type: 'cone' + }); + cone.setLocalScale(0.1, 0.1, 0.1); + + const createAnchor = (hitTestResult) => { + app.xr.anchors.create(hitTestResult, (err, anchor) => { + if (err) return message("Failed creating Anchor"); + if (!anchor) return message("Anchor has not been created"); + + anchor.persist((err, uuid) => { + if (err) message('Anchor failed to persist'); + }); + }); + }; + + if (app.xr.supported) { + const activate = function () { + if (app.xr.isAvailable(pc.XRTYPE_AR)) { + c.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, { + anchors: true, + callback: function (err) { + if (err) message("WebXR Immersive AR failed to start: " + err.message); + } + }); + } else { + message("Immersive AR is not available"); + } + }; + + app.mouse.on("mousedown", function () { + if (!app.xr.active) + activate(); + }); + + if (app.touch) { + app.touch.on("touchend", function (evt) { + if (!app.xr.active) { + // if not in VR, activate + activate(); + } else { + // otherwise reset camera + c.camera.endXr(); + } + + evt.event.preventDefault(); + evt.event.stopPropagation(); + }); + } + + // end session by keyboard ESC + app.keyboard.on('keydown', function (evt) { + if (evt.key === pc.KEY_ESCAPE && app.xr.active) { + app.xr.end(); + } + }); + + app.xr.on('start', function () { + message("Immersive AR session has started"); + + // restore all persistent anchors + const uuids = app.xr.anchors.uuids; + for(let i = 0; i < uuids.length; i++) { + app.xr.anchors.restore(uuids[i]); + } + }); + app.xr.on('end', function () { + message("Immersive AR session has ended"); + }); + app.xr.on('available:' + pc.XRTYPE_AR, function (available) { + if (available) { + if (!app.xr.hitTest.supported) { + message("AR Hit Test is not supported"); + } else if (!app.xr.anchors.supported) { + message("AR Anchors are not supported"); + } else if (!app.xr.anchors.persistence) { + message("AR Anchors Persistence is not supported"); + } else { + message("Touch screen to start AR session and look at the floor or walls"); + } + } else { + message("Immersive AR is unavailable"); + } + }); + + // create hit test sources for all input sources + if (app.xr.hitTest.supported && app.xr.anchors.supported) { + app.xr.input.on('add', (inputSource) => { + inputSource.hitTestStart({ + entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE], + callback: (err, hitTestSource) => { + if (err) return; + + let target = new pc.Entity(); + target.addComponent("render", { + type: "cylinder" + }); + target.setLocalScale(0.1, 0.01, 0.1); + app.root.addChild(target); + + let lastHitTestResult = null; + + // persistent input sources + if (inputSource.targetRayMode === pc.XRTARGETRAY_POINTER) { + inputSource.on('select', () => { + if (lastHitTestResult) + createAnchor(lastHitTestResult); + }); + } + + hitTestSource.on('result', (position, rotation, inputSource, hitTestResult) => { + target.setPosition(position); + target.setRotation(rotation); + lastHitTestResult = hitTestResult; + }); + + hitTestSource.once('remove', () => { + target.destroy(); + target = null; + + // mobile screen input source + if (inputSource.targetRayMode === pc.XRTARGETRAY_SCREEN && lastHitTestResult) + createAnchor(lastHitTestResult); + + lastHitTestResult = null; + }); + } + }); + + if (inputSource.gamepad) { + inputSource.gamepad.buttons + } + }); + } + + // create entity for anchors + app.xr.anchors.on('add', (anchor) => { + let entity = cone.clone(); + app.root.addChild(entity); + entity.setPosition(anchor.getPosition()); + entity.setRotation(anchor.getRotation()); + entity.translateLocal(0, 0.05, 0); + + anchor.on('change', () => { + entity.setPosition(anchor.getPosition()); + entity.setRotation(anchor.getRotation()); + entity.translateLocal(0, 0.05, 0); + }); + + anchor.once('destroy', () => { + entity.destroy(); + entity = null; + }); + }); + + if (!app.xr.isAvailable(pc.XRTYPE_AR)) { + message("Immersive AR is not available"); + } else if (!app.xr.hitTest.supported) { + message("AR Hit Test is not supported"); + } else if (!app.xr.anchors.supported) { + message("AR Anchors are not supported"); + } else if (!app.xr.anchors.persistence) { + message("AR Anchors Persistence is not supported"); + } else { + message("Touch screen to start AR session and look at the floor or walls"); + } + } else { + message("WebXR is not supported"); + } + return app; +} + +class ArAnchorsPersistentExample { + static CATEGORY = 'XR'; + static NAME = 'AR Anchors Persistence'; + static example = example; +} + +export { ArAnchorsPersistentExample }; diff --git a/examples/src/examples/xr/ar-hit-test-anchors.mjs b/examples/src/examples/xr/ar-hit-test-anchors.mjs new file mode 100644 index 00000000000..c6a266b2de5 --- /dev/null +++ b/examples/src/examples/xr/ar-hit-test-anchors.mjs @@ -0,0 +1,258 @@ +import * as pc from 'playcanvas'; + +/** + * @typedef {import('../../options.mjs').ExampleOptions} ExampleOptions + * @param {import('../../options.mjs').ExampleOptions} options - The example options. + * @returns {Promise} The example application. + */ +async function example({ canvas }) { + /** + * @param {string} msg - The message. + */ + const message = function (msg) { + /** @type {HTMLDivElement} */ + let el = document.querySelector('.message'); + if (!el) { + el = document.createElement('div'); + el.classList.add('message'); + document.body.append(el); + } + el.textContent = msg; + }; + + const app = new pc.Application(canvas, { + mouse: new pc.Mouse(canvas), + touch: new pc.TouchDevice(canvas), + keyboard: new pc.Keyboard(window), + graphicsDeviceOptions: { alpha: true } + }); + app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW); + app.setCanvasResolution(pc.RESOLUTION_AUTO); + + // Ensure canvas is resized when window changes size + const resize = () => app.resizeCanvas(); + window.addEventListener('resize', resize); + app.on('destroy', () => { + window.removeEventListener('resize', resize); + }); + + // use device pixel ratio + app.graphicsDevice.maxPixelRatio = window.devicePixelRatio; + + app.start(); + + // create camera + const c = new pc.Entity(); + c.addComponent('camera', { + clearColor: new pc.Color(0, 0, 0, 0), + farClip: 10000 + }); + app.root.addChild(c); + + const l = new pc.Entity(); + l.addComponent("light", { + type: "spot", + range: 30 + }); + l.translate(0, 10, 0); + app.root.addChild(l); + + const target = new pc.Entity(); + target.addComponent('render', { + type: "cylinder" + }); + target.setLocalScale(0.1, 0.01, 0.1); + app.root.addChild(target); + + const cone = new pc.Entity(); + cone.addComponent('render', { + type: 'cone' + }); + cone.setLocalScale(0.1, 0.1, 0.1); + + const createAnchor = (hitTestResult) => { + app.xr.anchors.create(hitTestResult, (err, anchor) => { + if (err) return message("Failed creating Anchor"); + if (!anchor) return message("Anchor has not been created"); + + let entity = cone.clone(); + app.root.addChild(entity); + entity.setPosition(anchor.getPosition()); + entity.setRotation(anchor.getRotation()); + entity.translateLocal(0, 0.05, 0); + + anchor.on('change', () => { + entity.setPosition(anchor.getPosition()); + entity.setRotation(anchor.getRotation()); + entity.translateLocal(0, 0.05, 0); + }); + + anchor.once('destroy', () => { + entity.destroy(); + entity = null; + }); + }); + }; + + if (app.xr.supported) { + const activate = function () { + if (app.xr.isAvailable(pc.XRTYPE_AR)) { + c.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, { + anchors: true, + callback: function (err) { + if (err) message("WebXR Immersive AR failed to start: " + err.message); + } + }); + } else { + message("Immersive AR is not available"); + } + }; + + app.mouse.on("mousedown", function () { + if (!app.xr.active) + activate(); + }); + + if (app.touch) { + app.touch.on("touchend", function (evt) { + if (!app.xr.active) { + // if not in VR, activate + activate(); + } else { + // otherwise reset camera + c.camera.endXr(); + } + + evt.event.preventDefault(); + evt.event.stopPropagation(); + }); + } + + // end session by keyboard ESC + app.keyboard.on('keydown', function (evt) { + if (evt.key === pc.KEY_ESCAPE && app.xr.active) { + app.xr.end(); + } + }); + + app.xr.on('start', function () { + message("Immersive AR session has started"); + + if (!app.xr.hitTest.supported || !app.xr.anchors.supported) + return; + + // provide gaze-like way to create anchors + // best for mobile phones + let lastHitTestResult = null; + + app.xr.hitTest.start({ + entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE, pc.XRTRACKABLE_MESH], + callback: function (err, hitTestSource) { + if (err) { + message("Failed to start AR hit test"); + return; + } + + hitTestSource.on('result', function (position, rotation, inputSource, hitTestResult) { + target.setPosition(position); + target.setRotation(rotation); + lastHitTestResult = hitTestResult; + }); + } + }); + + app.xr.input.on('select', (inputSource) => { + if (inputSource.targetRayMode !== pc.XRTARGETRAY_SCREEN) + return; + + if (!lastHitTestResult) + return; + + createAnchor(lastHitTestResult); + }); + }); + app.xr.on('end', function () { + message("Immersive AR session has ended"); + }); + app.xr.on('available:' + pc.XRTYPE_AR, function (available) { + if (available) { + if (!app.xr.hitTest.supported) { + message("AR Hit Test is not supported"); + } else if (!app.xr.anchors.supported) { + message("AR Anchors are not supported"); + } else { + message("Touch screen to start AR session and look at the floor or walls"); + } + } else { + message("Immersive AR is unavailable"); + } + }); + + // create hit test sources for all input sources + if (app.xr.hitTest.supported && app.xr.anchors.supported) { + app.xr.input.on('add', (inputSource) => { + inputSource.hitTestStart({ + entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE], + callback: (err, hitTestSource) => { + if (err) return; + + let target = new pc.Entity(); + target.addComponent("render", { + type: "cylinder" + }); + target.setLocalScale(0.1, 0.01, 0.1); + app.root.addChild(target); + + let lastHitTestResult = null; + + // persistent input sources + if (inputSource.targetRayMode === pc.XRTARGETRAY_POINTER) { + inputSource.on('select', () => { + if (lastHitTestResult) + createAnchor(lastHitTestResult); + }); + } + + hitTestSource.on('result', (position, rotation, inputSource, hitTestResult) => { + target.setPosition(position); + target.setRotation(rotation); + lastHitTestResult = hitTestResult; + }); + + hitTestSource.once('remove', () => { + target.destroy(); + target = null; + + // mobile screen input source + if (inputSource.targetRayMode === pc.XRTARGETRAY_SCREEN && lastHitTestResult) + createAnchor(lastHitTestResult); + + lastHitTestResult = null; + }); + } + }); + }); + } + + if (!app.xr.isAvailable(pc.XRTYPE_AR)) { + message("Immersive AR is not available"); + } else if (!app.xr.hitTest.supported) { + message("AR Hit Test is not supported"); + } else if (!app.xr.anchors.supported) { + message("AR Anchors are not supported"); + } else { + message("Touch screen to start AR session and look at the floor or walls"); + } + } else { + message("WebXR is not supported"); + } + return app; +} + +class ArHitTestAnchorsExample { + static CATEGORY = 'XR'; + static NAME = 'AR Hit Test Anchors'; + static example = example; +} + +export { ArHitTestAnchorsExample }; diff --git a/examples/src/examples/xr/index.mjs b/examples/src/examples/xr/index.mjs index c40b96d7c91..f220f14a29b 100644 --- a/examples/src/examples/xr/index.mjs +++ b/examples/src/examples/xr/index.mjs @@ -1,5 +1,7 @@ export * from "./ar-basic.mjs"; export * from "./ar-hit-test.mjs"; +export * from "./ar-hit-test-anchors.mjs"; +export * from "./ar-anchors-persistence.mjs"; export * from "./vr-basic.mjs"; export * from './vr-controllers.mjs'; export * from "./vr-hands.mjs"; diff --git a/src/framework/xr/xr-anchor.js b/src/framework/xr/xr-anchor.js index 3ed6cdf9a1f..9fa6b651b67 100644 --- a/src/framework/xr/xr-anchor.js +++ b/src/framework/xr/xr-anchor.js @@ -25,16 +25,30 @@ class XrAnchor extends EventHandler { */ _rotation = new Quat(); + /** + * @type {string|null} + * @private + */ + _uuid = null; + + /** + * @type {string[]|null} + * @private + */ + _uuidRequests = null; + /** * @param {import('./xr-anchors.js').XrAnchors} anchors - Anchor manager. * @param {object} xrAnchor - native XRAnchor object that is provided by WebXR API + * @param {string|null} uuid - ID string associated with a persistent anchor * @hideconstructor */ - constructor(anchors, xrAnchor) { + constructor(anchors, xrAnchor, uuid = null) { super(); this._anchors = anchors; this._xrAnchor = xrAnchor; + this._uuid = uuid; } /** @@ -68,6 +82,9 @@ class XrAnchor extends EventHandler { if (!this._xrAnchor) return; this._anchors._index.delete(this._xrAnchor); + if (this._uuid) + this._anchors._indexByUuid.delete(this._uuid); + const ind = this._anchors._list.indexOf(this); if (ind !== -1) this._anchors._list.splice(ind, 1); @@ -114,6 +131,59 @@ class XrAnchor extends EventHandler { getRotation() { return this._rotation; } + + persist(callback) { + if (!this._anchors.persistence) + return callback(new Error('Persistent Anchors are not supported'), null); + + if (this._uuid) + return callback(null, this._uuid); + + if (this._uuidRequests) { + this._uuidRequests.push(callback); + return; + } + + this._uuidRequests = []; + + this._xrAnchor.requestPersistentHandle() + .then((uuid) => { + this._uuid = uuid; + this._anchors._indexByUuid.set(this._uuid, this); + callback(null, uuid); + for(let i = 0; i < this._uuidRequests.length; i++) { + this._uuidRequests[i](null, uuid); + } + this._uuidRequests = null; + }) + .catch((ex) => { + callback(ex); + for(let i = 0; i < this._uuidRequests.length; i++) { + this._uuidRequests[i](ex); + } + this._uuidRequests = null; + }); + } + + delete(callback) { + if (!this._uuid) { + if (callback) callback(new Error('Anchor is not persistent')); + return; + } + + this._anchors.delete(this._uuid, (ex) => { + this._uuid = null; + if (callback) callback(ex); + }); + } + + get uuid() { + return this._uuid; + } + + get persistent() { + return !!this._uuid; + } } export { XrAnchor }; diff --git a/src/framework/xr/xr-anchors.js b/src/framework/xr/xr-anchors.js index c273878a12d..a0fdfc793bc 100644 --- a/src/framework/xr/xr-anchors.js +++ b/src/framework/xr/xr-anchors.js @@ -32,6 +32,12 @@ class XrAnchors extends EventHandler { */ _supported = platform.browser && !!window.XRAnchor; + /** + * @type {boolean} + * @private + */ + _persistence = !!window?.XRSession?.prototype.restorePersistentAnchor; + /** * List of anchor creation requests. * @@ -48,6 +54,14 @@ class XrAnchors extends EventHandler { */ _index = new Map(); + /** + * Index of XrAnchors, with UUID (persistent string) used as a key. + * + * @type {Map} + * @ignore + */ + _indexByUuid = new Map(); + /** * @type {Array} * @ignore @@ -130,21 +144,110 @@ class XrAnchors extends EventHandler { /** * Create anchor with position, rotation and a callback. * - * @param {import('../../core/math/vec3.js').Vec3} position - Position for an anchor. - * @param {import('../../core/math/quat.js').Quat} [rotation] - Rotation for an anchor. + * @param {import('../../core/math/vec3.js').Vec3|XRHitTestResult} position - Position for an anchor. + * @param {import('../../core/math/quat.js').Quat|XrAnchorCreate} [rotation] - Rotation for an anchor. * @param {XrAnchorCreate} [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, function (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) { - this._creationQueue.push({ - transform: new XRRigidTransform(position, rotation), // eslint-disable-line no-undef - callback: callback - }); + if (window.XRHitTestResult && position instanceof XRHitTestResult) { + const hitResult = position; + callback = rotation; + + if (!this._supported) { + if (callback) callback(new Error('Anchors API is not supported'), null); + return; + } + + if (!hitResult.createAnchor) { + if (callback) callback(new Error('Creating Anchor from Hit Test is not supported'), null); + return; + } + + hitResult.createAnchor() + .then((xrAnchor) => { + const anchor = new XrAnchor(this, xrAnchor); + this._index.set(xrAnchor, anchor); + this._list.push(anchor); + + if (callback) + callback(null, anchor); + + this.fire('add', anchor); + }) + .catch((ex) => { + if (callback) + callback(ex, null); + + this.fire('error', ex); + }); + } else { + this._creationQueue.push({ + transform: new XRRigidTransform(position, rotation), // eslint-disable-line no-undef + callback: callback + }); + } + } + + restore(uuid, callback) { + if (!this._persistence) { + if (callback) callback(new Error('Anchor Persistence is not supported')); + return; + } + + if (!this.manager.active) { + if (callback) callback(new Error('WebXR session is not active')); + return; + } + + this.manager.session.restorePersistentAnchor(uuid) + .then((xrAnchor) => { + const anchor = new XrAnchor(this, xrAnchor, uuid); + this._index.set(xrAnchor, anchor); + this._indexByUuid.set(uuid, anchor); + this._list.push(anchor); + this.fire('add', anchor); + }) + .catch((ex) => { + if (callback) callback(ex, null); + this.fire('error', ex); + }); + } + + delete(uuid, callback) { + if (!this._persistence) { + if (callback) callback(new Error('Anchor Persistence is not supported')); + return; + } + + if (!this.manager.active) { + if (callback) callback(new Error('WebXR session is not active')); + return; + } + + this.manager.session.deletePersistentAnchor(uuid) + .then(() => { + if (callback) callback(null); + }) + .catch((ex) => { + if (callback) callback(ex); + this.fire('error', ex); + }); } /** @@ -223,6 +326,30 @@ class XrAnchors extends EventHandler { return this._supported; } + /** + * 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._persistence) + return null; + + if (!this.manager.active) + return null; + + return this.manager.session.persistentAnchors; + } + /** * List of available {@link XrAnchor}s. * diff --git a/src/framework/xr/xr-hit-test-source.js b/src/framework/xr/xr-hit-test-source.js index 3cec300c314..8e2c04db77f 100644 --- a/src/framework/xr/xr-hit-test-source.js +++ b/src/framework/xr/xr-hit-test-source.js @@ -75,8 +75,9 @@ class XrHitTestSource extends EventHandler { * @param {Quat} rotation - Rotation of hit test. * @param {import('./xr-input-source.js').XrInputSource|null} inputSource - If is transient hit * test source, then it will provide related input source. + * @param {XRHitTestResult} XRHitTestResult - object that is created by WebXR API. * @example - * hitTestSource.on('result', function (position, rotation, inputSource) { + * hitTestSource.on('result', function (position, rotation) { * target.setPosition(position); * target.setRotation(rotation); * }); @@ -143,8 +144,8 @@ class XrHitTestSource extends EventHandler { if (!rotation) rotation = new Quat(); rotation.copy(pose.transform.orientation); - this.fire('result', position, rotation, inputSource); - this.manager.hitTest.fire('result', this, position, rotation, inputSource); + this.fire('result', position, rotation, inputSource, results[i]); + this.manager.hitTest.fire('result', this, position, rotation, inputSource, results[i]); poolVec3.push(position); poolQuat.push(rotation); diff --git a/src/framework/xr/xr-manager.js b/src/framework/xr/xr-manager.js index 465b9ee8092..07cd22c3ae7 100644 --- a/src/framework/xr/xr-manager.js +++ b/src/framework/xr/xr-manager.js @@ -135,6 +135,13 @@ class XrManager extends EventHandler { */ lightEstimation; + /** + * Provides access to Anchors. + * + * @type {XrAnchors} + */ + anchors; + /** * @type {import('../components/camera/component.js').CameraComponent} * @private From b785cabad177a2d6f2e992395142cdb8dc3f3ea9 Mon Sep 17 00:00:00 2001 From: mrmaxm Date: Wed, 1 Nov 2023 17:34:58 +0200 Subject: [PATCH 04/11] fix condition --- examples/src/examples/xr/ar-hit-test.mjs | 6 ++-- src/framework/xr/xr-hit-test-source.js | 40 +++++++++++++++++------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/examples/src/examples/xr/ar-hit-test.mjs b/examples/src/examples/xr/ar-hit-test.mjs index 1b58a907ad2..d4ab73ef937 100644 --- a/examples/src/examples/xr/ar-hit-test.mjs +++ b/examples/src/examples/xr/ar-hit-test.mjs @@ -61,7 +61,7 @@ async function example({ canvas }) { target.addComponent("render", { type: "cylinder" }); - target.setLocalScale(0.5, 0.01, 0.5); + target.setLocalScale(0.1, 0.01, 0.1); app.root.addChild(target); if (app.xr.supported) { @@ -151,10 +151,10 @@ async function example({ canvas }) { target.addComponent("render", { type: "cylinder" }); - target.setLocalScale(0.5, 0.01, 0.5); + target.setLocalScale(0.1, 0.01, 0.1); app.root.addChild(target); - hitTestSource.on('result', (position, rotation, item) => { + hitTestSource.on('result', (position, rotation) => { target.setPosition(position); target.setRotation(rotation); }); diff --git a/src/framework/xr/xr-hit-test-source.js b/src/framework/xr/xr-hit-test-source.js index a970596fa6a..3077391a1fc 100644 --- a/src/framework/xr/xr-hit-test-source.js +++ b/src/framework/xr/xr-hit-test-source.js @@ -132,26 +132,44 @@ class XrHitTestSource extends EventHandler { * @private */ updateHitResults(results, inputSource) { - if (inputSource && inputSource.hitTestSourcesSet.has(this)) + if (inputSource && !inputSource.hitTestSourcesSet.has(this)) return; + let origin = poolVec3.pop(); + if (!origin) origin = new Vec3(); + + if (inputSource) { + origin.copy(inputSource.getOrigin()); + } else { + origin.copy(this.manager.camera.getPosition()); + } + + let candidateDistance = Infinity; + + let position = poolVec3.pop(); + if (!position) position = new Vec3(); + + let rotation = poolQuat.pop(); + if (!rotation) rotation = new Quat(); + for (let i = 0; i < results.length; i++) { const pose = results[i].getPose(this.manager._referenceSpace); - let position = poolVec3.pop(); - if (!position) position = new Vec3(); - position.copy(pose.transform.position); + const distance = origin.distance(pose.transform.position); + if (distance >= candidateDistance) + continue; - let rotation = poolQuat.pop(); - if (!rotation) rotation = new Quat(); + candidateDistance = distance; + position.copy(pose.transform.position); rotation.copy(pose.transform.orientation); + } - this.fire('result', position, rotation, inputSource); - this.manager.hitTest.fire('result', this, position, rotation, inputSource); + this.fire('result', position, rotation, inputSource); + this.manager.hitTest.fire('result', this, position, rotation, inputSource); - poolVec3.push(position); - poolQuat.push(rotation); - } + poolVec3.push(origin); + poolVec3.push(position); + poolQuat.push(rotation); } } From 7bd3dd7fabcbd9407a9afe5c234c961565bcfdd2 Mon Sep 17 00:00:00 2001 From: mrmaxm Date: Wed, 1 Nov 2023 18:14:51 +0200 Subject: [PATCH 05/11] persistent anchors --- .../examples/xr/ar-anchors-persistence.mjs | 326 ++++++++++-------- src/framework/xr/xr-anchor.js | 68 +++- src/framework/xr/xr-anchors.js | 34 +- 3 files changed, 274 insertions(+), 154 deletions(-) diff --git a/examples/src/examples/xr/ar-anchors-persistence.mjs b/examples/src/examples/xr/ar-anchors-persistence.mjs index 6f4c0383954..a86fc917981 100644 --- a/examples/src/examples/xr/ar-anchors-persistence.mjs +++ b/examples/src/examples/xr/ar-anchors-persistence.mjs @@ -20,6 +20,10 @@ async function example({ canvas }) { el.textContent = msg; }; + const assets = { + font: new pc.Asset('font', 'font', { url: assetPath + 'fonts/courier.json' }) + }; + const app = new pc.Application(canvas, { mouse: new pc.Mouse(canvas), touch: new pc.TouchDevice(canvas), @@ -39,194 +43,214 @@ async function example({ canvas }) { // use device pixel ratio app.graphicsDevice.maxPixelRatio = window.devicePixelRatio; - app.start(); - - // create camera - const c = new pc.Entity(); - c.addComponent('camera', { - clearColor: new pc.Color(0, 0, 0, 0), - farClip: 10000 - }); - app.root.addChild(c); + const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets); + assetListLoader.load(() => { - const l = new pc.Entity(); - l.addComponent("light", { - type: "spot", - range: 30 - }); - l.translate(0, 10, 0); - app.root.addChild(l); + app.start(); - const cone = new pc.Entity(); - cone.addComponent('render', { - type: 'cone' - }); - cone.setLocalScale(0.1, 0.1, 0.1); + // create camera + const c = new pc.Entity(); + c.addComponent('camera', { + clearColor: new pc.Color(0, 0, 0, 0), + farClip: 10000 + }); + app.root.addChild(c); - const createAnchor = (hitTestResult) => { - app.xr.anchors.create(hitTestResult, (err, anchor) => { - if (err) return message("Failed creating Anchor"); - if (!anchor) return message("Anchor has not been created"); + const l = new pc.Entity(); + l.addComponent("light", { + type: "spot", + range: 30 + }); + l.translate(0, 10, 0); + app.root.addChild(l); - anchor.persist((err, uuid) => { - if (err) message('Anchor failed to persist'); - }); + const cone = new pc.Entity(); + cone.addComponent('render', { + type: 'cone' }); - }; + cone.setLocalScale(0.1, 0.1, 0.1); + + const label = new pc.Entity('label'); + label.setLocalPosition(0, 0.1, 0); + label.addComponent("element", { + pivot: new pc.Vec2(0.5, 1), + anchor: new pc.Vec4(0.5, 0.5, 0.5, 0.5), + fontAsset: assets.font.id, + fontSize: 42, + text: "-", + type: pc.ELEMENTTYPE_TEXT + }); + cone.addChild(label); + + const createAnchor = (hitTestResult) => { + app.xr.anchors.create(hitTestResult, (err, anchor) => { + if (err) return message("Failed creating Anchor"); + if (!anchor) return message("Anchor has not been created"); - if (app.xr.supported) { - const activate = function () { - if (app.xr.isAvailable(pc.XRTYPE_AR)) { - c.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, { - anchors: true, - callback: function (err) { - if (err) message("WebXR Immersive AR failed to start: " + err.message); + anchor.persist((err, uuid) => { + if (err) { + message('Anchor failed to persist'); + console.log(err); } }); - } else { - message("Immersive AR is not available"); - } + }); }; - app.mouse.on("mousedown", function () { - if (!app.xr.active) - activate(); - }); - - if (app.touch) { - app.touch.on("touchend", function (evt) { - if (!app.xr.active) { - // if not in VR, activate - activate(); + if (app.xr.supported) { + const activate = function () { + if (app.xr.isAvailable(pc.XRTYPE_AR)) { + c.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, { + anchors: true, + callback: function (err) { + if (err) message("WebXR Immersive AR failed to start: " + err.message); + } + }); } else { - // otherwise reset camera - c.camera.endXr(); + message("Immersive AR is not available"); } + }; - evt.event.preventDefault(); - evt.event.stopPropagation(); + app.mouse.on("mousedown", function () { + if (!app.xr.active) + activate(); }); - } - // end session by keyboard ESC - app.keyboard.on('keydown', function (evt) { - if (evt.key === pc.KEY_ESCAPE && app.xr.active) { - app.xr.end(); + if (app.touch) { + app.touch.on("touchend", function (evt) { + if (!app.xr.active) { + // if not in VR, activate + activate(); + } else { + // otherwise reset camera + c.camera.endXr(); + } + + evt.event.preventDefault(); + evt.event.stopPropagation(); + }); } - }); - app.xr.on('start', function () { - message("Immersive AR session has started"); + // end session by keyboard ESC + app.keyboard.on('keydown', function (evt) { + if (evt.key === pc.KEY_ESCAPE && app.xr.active) { + app.xr.end(); + } + }); - // restore all persistent anchors - const uuids = app.xr.anchors.uuids; - for(let i = 0; i < uuids.length; i++) { - app.xr.anchors.restore(uuids[i]); - } - }); - app.xr.on('end', function () { - message("Immersive AR session has ended"); - }); - app.xr.on('available:' + pc.XRTYPE_AR, function (available) { - if (available) { - if (!app.xr.hitTest.supported) { - message("AR Hit Test is not supported"); - } else if (!app.xr.anchors.supported) { - message("AR Anchors are not supported"); - } else if (!app.xr.anchors.persistence) { - message("AR Anchors Persistence is not supported"); + app.xr.on('start', function () { + message("Immersive AR session has started"); + + // restore all persistent anchors + const uuids = app.xr.anchors.uuids; + for(let i = 0; i < uuids.length; i++) { + app.xr.anchors.restore(uuids[i]); + } + }); + app.xr.on('end', function () { + message("Immersive AR session has ended"); + }); + app.xr.on('available:' + pc.XRTYPE_AR, function (available) { + if (available) { + if (!app.xr.hitTest.supported) { + message("AR Hit Test is not supported"); + } else if (!app.xr.anchors.supported) { + message("AR Anchors are not supported"); + } else if (!app.xr.anchors.persistence) { + message("AR Anchors Persistence is not supported"); + } else { + message("Touch screen to start AR session and look at the floor or walls"); + } } else { - message("Touch screen to start AR session and look at the floor or walls"); + message("Immersive AR is unavailable"); } - } else { - message("Immersive AR is unavailable"); - } - }); + }); - // create hit test sources for all input sources - if (app.xr.hitTest.supported && app.xr.anchors.supported) { - app.xr.input.on('add', (inputSource) => { - inputSource.hitTestStart({ - entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE], - callback: (err, hitTestSource) => { - if (err) return; - - let target = new pc.Entity(); - target.addComponent("render", { - type: "cylinder" - }); - target.setLocalScale(0.1, 0.01, 0.1); - app.root.addChild(target); - - let lastHitTestResult = null; - - // persistent input sources - if (inputSource.targetRayMode === pc.XRTARGETRAY_POINTER) { - inputSource.on('select', () => { - if (lastHitTestResult) - createAnchor(lastHitTestResult); + // create hit test sources for all input sources + if (app.xr.hitTest.supported && app.xr.anchors.supported) { + app.xr.input.on('add', (inputSource) => { + inputSource.hitTestStart({ + entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE], + callback: (err, hitTestSource) => { + if (err) return; + + let target = new pc.Entity(); + target.addComponent("render", { + type: "cylinder" + }); + target.setLocalScale(0.1, 0.01, 0.1); + app.root.addChild(target); + + let lastHitTestResult = null; + + // persistent input sources + if (inputSource.targetRayMode === pc.XRTARGETRAY_POINTER) { + inputSource.on('select', () => { + if (lastHitTestResult) + createAnchor(lastHitTestResult); + }); + } + + hitTestSource.on('result', (position, rotation, inputSource, hitTestResult) => { + target.setPosition(position); + target.setRotation(rotation); + lastHitTestResult = hitTestResult; }); - } - hitTestSource.on('result', (position, rotation, inputSource, hitTestResult) => { - target.setPosition(position); - target.setRotation(rotation); - lastHitTestResult = hitTestResult; - }); + hitTestSource.once('remove', () => { + target.destroy(); + target = null; - hitTestSource.once('remove', () => { - target.destroy(); - target = null; + // mobile screen input source + if (inputSource.targetRayMode === pc.XRTARGETRAY_SCREEN && lastHitTestResult) + createAnchor(lastHitTestResult); - // mobile screen input source - if (inputSource.targetRayMode === pc.XRTARGETRAY_SCREEN && lastHitTestResult) - createAnchor(lastHitTestResult); + lastHitTestResult = null; + }); + } + }); - lastHitTestResult = null; - }); + if (inputSource.gamepad) { + inputSource.gamepad.buttons } }); + } - if (inputSource.gamepad) { - inputSource.gamepad.buttons - } - }); - } - - // create entity for anchors - app.xr.anchors.on('add', (anchor) => { - let entity = cone.clone(); - app.root.addChild(entity); - entity.setPosition(anchor.getPosition()); - entity.setRotation(anchor.getRotation()); - entity.translateLocal(0, 0.05, 0); - - anchor.on('change', () => { + // create entity for anchors + app.xr.anchors.on('add', (anchor) => { + let entity = cone.clone(); + app.root.addChild(entity); entity.setPosition(anchor.getPosition()); entity.setRotation(anchor.getRotation()); entity.translateLocal(0, 0.05, 0); - }); - anchor.once('destroy', () => { - entity.destroy(); - entity = null; + anchor.on('change', () => { + entity.setPosition(anchor.getPosition()); + entity.setRotation(anchor.getRotation()); + entity.translateLocal(0, 0.05, 0); + }); + + anchor.once('destroy', () => { + entity.destroy(); + entity = null; + }); }); - }); - if (!app.xr.isAvailable(pc.XRTYPE_AR)) { - message("Immersive AR is not available"); - } else if (!app.xr.hitTest.supported) { - message("AR Hit Test is not supported"); - } else if (!app.xr.anchors.supported) { - message("AR Anchors are not supported"); - } else if (!app.xr.anchors.persistence) { - message("AR Anchors Persistence is not supported"); + if (!app.xr.isAvailable(pc.XRTYPE_AR)) { + message("Immersive AR is not available"); + } else if (!app.xr.hitTest.supported) { + message("AR Hit Test is not supported"); + } else if (!app.xr.anchors.supported) { + message("AR Anchors are not supported"); + } else if (!app.xr.anchors.persistence) { + message("AR Anchors Persistence is not supported"); + } else { + message("Touch screen to start AR session and look at the floor or walls"); + } } else { - message("Touch screen to start AR session and look at the floor or walls"); + message("WebXR is not supported"); } - } else { - message("WebXR is not supported"); - } + }); + return app; } diff --git a/src/framework/xr/xr-anchor.js b/src/framework/xr/xr-anchor.js index 9fa6b651b67..fafc4007abb 100644 --- a/src/framework/xr/xr-anchor.js +++ b/src/framework/xr/xr-anchor.js @@ -3,6 +3,22 @@ import { EventHandler } from '../../core/event-handler.js'; import { Vec3 } from '../../core/math/vec3.js'; import { Quat } from '../../core/math/quat.js'; +/** + * Callback used by {@link XrAnchor#persist}. + * + * @callback XrAnchorPersistCallback + * @param {Error|null} err - The Error object if failed to persist an anchor or null. + * @param {string|null} uuid - unique string that can be used to restore {@link XRAnchor} + * in another session. + */ + +/** + * Callback used by {@link XrAnchor#persist}. + * + * @callback XrAnchorForgetCallback + * @param {Error|null} err - The Error object if failed to forget an anchor or null if succeeded. + */ + /** * An anchor keeps track of a position and rotation that is fixed relative to the real world. * This allows the application to adjust the location of the virtual objects placed in the @@ -75,6 +91,27 @@ class XrAnchor extends EventHandler { * }); */ + /** + * Fired when an {@link XrAnchor}'s has been persisted. + * + * @event XrAnchor#persist + * @param {string} uuid - Unique string that can be used to restore this anchor. + * @example + * anchor.on('persist', function (uuid) { + * // anchor has been persisted + * }); + */ + + /** + * Fired when an {@link XrAnchor}'s has been forgotten. + * + * @event XrAnchor#forget + * @example + * anchor.on('forget', function () { + * // anchor has been forgotten + * }); + */ + /** * Destroy an anchor. */ @@ -132,6 +169,15 @@ class XrAnchor extends EventHandler { return this._rotation; } + /** + * This method provides a way to persist anchor and get a string with UUID. + * UUID can be used later to restore anchor. + * Bear in mind that underlying systems might have a limit on number of anchors + * allowed to be persisted. + * + * @param {XrAnchorPersistCallback} [callback] - Callback to fire when anchor + * persistent UUID has been generated or error if failed. + */ persist(callback) { if (!this._anchors.persistence) return callback(new Error('Persistent Anchors are not supported'), null); @@ -155,6 +201,7 @@ class XrAnchor extends EventHandler { this._uuidRequests[i](null, uuid); } this._uuidRequests = null; + this.fire('persist', uuid); }) .catch((ex) => { callback(ex); @@ -165,22 +212,39 @@ class XrAnchor extends EventHandler { }); } - delete(callback) { + /** + * This method provides a way to remove persistent UUID of an anchor for underlying systems. + * + * @param {XrAnchorForgetCallback} [callback] - Callback to fire when anchor has been + * forgotten or error if failed. + */ + forget(callback) { if (!this._uuid) { if (callback) callback(new Error('Anchor is not persistent')); return; } - this._anchors.delete(this._uuid, (ex) => { + this._anchors.forget(this._uuid, (ex) => { this._uuid = null; if (callback) callback(ex); + this.fire('forget'); }); } + /** + * UUID string of a persistent anchor or null if not presisted. + * + * @type {null|string} + */ get uuid() { return this._uuid; } + /** + * True if an anchor is persistent. + * + * @type {boolean} + */ get persistent() { return !!this._uuid; } diff --git a/src/framework/xr/xr-anchors.js b/src/framework/xr/xr-anchors.js index a0fdfc793bc..d01b6054940 100644 --- a/src/framework/xr/xr-anchors.js +++ b/src/framework/xr/xr-anchors.js @@ -204,6 +204,25 @@ class XrAnchors extends EventHandler { } } + /** + * Restore anchor using persistent UUID. + * + * @param {string} uuid - UUID string associated with persistent anchor. + * @param {XrAnchorCreate} [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._persistence) { if (callback) callback(new Error('Anchor Persistence is not supported')); @@ -229,7 +248,20 @@ class XrAnchors extends EventHandler { }); } - delete(uuid, callback) { + /** + * Forget an anchor by removing its UUID from underlying systems. + * + * @param {string} uuid - UUID string associated with persistent anchor. + * @param {import('./xr-anchor.js').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._persistence) { if (callback) callback(new Error('Anchor Persistence is not supported')); return; From 2000362363e44568aa9525a022427b969f17f798 Mon Sep 17 00:00:00 2001 From: mrmaxm Date: Wed, 1 Nov 2023 19:36:10 +0200 Subject: [PATCH 06/11] persistent anchors example --- .../examples/xr/ar-anchors-persistence.mjs | 365 ++++++++++-------- 1 file changed, 196 insertions(+), 169 deletions(-) diff --git a/examples/src/examples/xr/ar-anchors-persistence.mjs b/examples/src/examples/xr/ar-anchors-persistence.mjs index a86fc917981..d6a6e524697 100644 --- a/examples/src/examples/xr/ar-anchors-persistence.mjs +++ b/examples/src/examples/xr/ar-anchors-persistence.mjs @@ -15,15 +15,18 @@ async function example({ canvas }) { if (!el) { el = document.createElement('div'); el.classList.add('message'); + el.style.position = 'absolute'; + el.style.bottom = '96px'; + el.style.right = '0'; + el.style.padding = '8px 16px'; + el.style.fontFamily = 'Helvetica, Arial, sans-serif'; + el.style.color = '#fff'; + el.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; document.body.append(el); } el.textContent = msg; }; - const assets = { - font: new pc.Asset('font', 'font', { url: assetPath + 'fonts/courier.json' }) - }; - const app = new pc.Application(canvas, { mouse: new pc.Mouse(canvas), touch: new pc.TouchDevice(canvas), @@ -43,213 +46,237 @@ async function example({ canvas }) { // use device pixel ratio app.graphicsDevice.maxPixelRatio = window.devicePixelRatio; - const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets); - assetListLoader.load(() => { + app.start(); - app.start(); + // create camera + const camera = new pc.Entity(); + camera.addComponent('camera', { + clearColor: new pc.Color(0, 0, 0, 0), + farClip: 10000 + }); + app.root.addChild(camera); - // create camera - const c = new pc.Entity(); - c.addComponent('camera', { - clearColor: new pc.Color(0, 0, 0, 0), - farClip: 10000 - }); - app.root.addChild(c); + const l = new pc.Entity(); + l.addComponent("light", { + type: "spot", + range: 30 + }); + l.translate(0, 10, 0); + app.root.addChild(l); - const l = new pc.Entity(); - l.addComponent("light", { - type: "spot", - range: 30 - }); - l.translate(0, 10, 0); - app.root.addChild(l); + const cone = new pc.Entity(); + cone.addComponent('render', { + type: 'cone' + }); + cone.setLocalScale(0.1, 0.1, 0.1); - const cone = new pc.Entity(); - cone.addComponent('render', { - type: 'cone' - }); - cone.setLocalScale(0.1, 0.1, 0.1); - - const label = new pc.Entity('label'); - label.setLocalPosition(0, 0.1, 0); - label.addComponent("element", { - pivot: new pc.Vec2(0.5, 1), - anchor: new pc.Vec4(0.5, 0.5, 0.5, 0.5), - fontAsset: assets.font.id, - fontSize: 42, - text: "-", - type: pc.ELEMENTTYPE_TEXT - }); - cone.addChild(label); + const materialStandard = new pc.StandardMaterial(); - const createAnchor = (hitTestResult) => { - app.xr.anchors.create(hitTestResult, (err, anchor) => { - if (err) return message("Failed creating Anchor"); - if (!anchor) return message("Anchor has not been created"); + const materialPersistent = new pc.StandardMaterial(); + materialPersistent.diffuse = new pc.Color(0.5, 1, 0.5); - anchor.persist((err, uuid) => { - if (err) { - message('Anchor failed to persist'); - console.log(err); - } - }); - }); - }; + const createAnchor = (hitTestResult) => { + app.xr.anchors.create(hitTestResult, (err, anchor) => { + if (err) return message("Failed creating Anchor"); + if (!anchor) return message("Anchor has not been created"); - if (app.xr.supported) { - const activate = function () { - if (app.xr.isAvailable(pc.XRTYPE_AR)) { - c.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, { - anchors: true, - callback: function (err) { - if (err) message("WebXR Immersive AR failed to start: " + err.message); - } - }); - } else { - message("Immersive AR is not available"); + anchor.persist((err, uuid) => { + if (err) { + message('Anchor failed to persist'); } - }; - - app.mouse.on("mousedown", function () { - if (!app.xr.active) - activate(); }); + }); + }; - if (app.touch) { - app.touch.on("touchend", function (evt) { - if (!app.xr.active) { - // if not in VR, activate - activate(); - } else { - // otherwise reset camera - c.camera.endXr(); + if (app.xr.supported) { + const activate = function () { + if (app.xr.isAvailable(pc.XRTYPE_AR)) { + camera.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, { + anchors: true, + callback: function (err) { + if (err) message("WebXR Immersive AR failed to start: " + err.message); } - - evt.event.preventDefault(); - evt.event.stopPropagation(); }); + } else { + message("Immersive AR is not available"); } + }; + + app.mouse.on("mousedown", function () { + if (!app.xr.active) + activate(); + }); - // end session by keyboard ESC - app.keyboard.on('keydown', function (evt) { - if (evt.key === pc.KEY_ESCAPE && app.xr.active) { - app.xr.end(); + if (app.touch) { + app.touch.on("touchend", function (evt) { + if (!app.xr.active) { + // if not in VR, activate + activate(); + } else { + // otherwise reset camera + camera.camera.endXr(); } + + evt.event.preventDefault(); + evt.event.stopPropagation(); }); + } - app.xr.on('start', function () { - message("Immersive AR session has started"); + // end session by keyboard ESC + app.keyboard.on('keydown', function (evt) { + if (evt.key === pc.KEY_ESCAPE && app.xr.active) { + app.xr.end(); + } + }); + + app.xr.on('start', function () { + message("Immersive AR session has started"); - // restore all persistent anchors + // restore all persistent anchors + if (app.xr.anchors.persistence) { const uuids = app.xr.anchors.uuids; for(let i = 0; i < uuids.length; i++) { app.xr.anchors.restore(uuids[i]); } - }); - app.xr.on('end', function () { - message("Immersive AR session has ended"); - }); - app.xr.on('available:' + pc.XRTYPE_AR, function (available) { - if (available) { - if (!app.xr.hitTest.supported) { - message("AR Hit Test is not supported"); - } else if (!app.xr.anchors.supported) { - message("AR Anchors are not supported"); - } else if (!app.xr.anchors.persistence) { - message("AR Anchors Persistence is not supported"); - } else { - message("Touch screen to start AR session and look at the floor or walls"); - } + } + }); + app.xr.on('end', function () { + message("Immersive AR session has ended"); + }); + app.xr.on('available:' + pc.XRTYPE_AR, function (available) { + if (available) { + if (!app.xr.hitTest.supported) { + message("AR Hit Test is not supported"); + } else if (!app.xr.anchors.supported) { + message("AR Anchors are not supported"); + } else if (!app.xr.anchors.persistence) { + message("AR Anchors Persistence is not supported"); } else { - message("Immersive AR is unavailable"); + message("Touch screen to start AR session and look at the floor or walls"); } - }); + } else { + message("Immersive AR is unavailable"); + } + }); - // create hit test sources for all input sources - if (app.xr.hitTest.supported && app.xr.anchors.supported) { - app.xr.input.on('add', (inputSource) => { - inputSource.hitTestStart({ - entityTypes: [pc.XRTRACKABLE_POINT, pc.XRTRACKABLE_PLANE], - callback: (err, hitTestSource) => { - if (err) return; - - let target = new pc.Entity(); - target.addComponent("render", { - type: "cylinder" - }); - target.setLocalScale(0.1, 0.01, 0.1); - app.root.addChild(target); - - let lastHitTestResult = null; - - // persistent input sources - if (inputSource.targetRayMode === pc.XRTARGETRAY_POINTER) { - inputSource.on('select', () => { - if (lastHitTestResult) - createAnchor(lastHitTestResult); - }); - } - - hitTestSource.on('result', (position, rotation, inputSource, hitTestResult) => { - target.setPosition(position); - target.setRotation(rotation); - lastHitTestResult = hitTestResult; + // create hit test sources for all input sources + if (app.xr.hitTest.supported && app.xr.anchors.supported) { + app.xr.input.on('add', (inputSource) => { + inputSource.hitTestStart({ + entityTypes: [pc.XRTRACKABLE_MESH], + callback: (err, hitTestSource) => { + if (err) return; + + let target = new pc.Entity(); + target.addComponent("render", { + type: "cylinder" + }); + target.setLocalScale(0.1, 0.01, 0.1); + app.root.addChild(target); + + let lastHitTestResult = null; + + // persistent input sources + if (inputSource.targetRayMode === pc.XRTARGETRAY_POINTER) { + inputSource.on('select', () => { + if (lastHitTestResult) + createAnchor(lastHitTestResult); }); + } - hitTestSource.once('remove', () => { - target.destroy(); - target = null; + hitTestSource.on('result', (position, rotation, inputSource, hitTestResult) => { + target.setPosition(position); + target.setRotation(rotation); + lastHitTestResult = hitTestResult; + }); - // mobile screen input source - if (inputSource.targetRayMode === pc.XRTARGETRAY_SCREEN && lastHitTestResult) - createAnchor(lastHitTestResult); + hitTestSource.once('remove', () => { + target.destroy(); + target = null; - lastHitTestResult = null; - }); - } - }); + // mobile screen input source + if (inputSource.targetRayMode === pc.XRTARGETRAY_SCREEN && lastHitTestResult) + createAnchor(lastHitTestResult); - if (inputSource.gamepad) { - inputSource.gamepad.buttons + lastHitTestResult = null; + }); } }); - } + }); + } + + if (app.xr.anchors.persistence) { + app.on('update', () => { + const inputSources = app.xr.input.inputSources; + for(let i = 0; i < inputSources.length; i++) { + const inputSource = inputSources[i]; + + if (!inputSource.gamepad) + continue; - // create entity for anchors - app.xr.anchors.on('add', (anchor) => { - let entity = cone.clone(); - app.root.addChild(entity); + for(let b = 0; b < inputSource.gamepad.buttons.length; b++) { + if (!inputSource.gamepad.buttons[b].pressed) + continue; + + if (b === 0) continue; + + // clear all persistent anchors + const uuids = app.xr.anchors.uuids; + for(let a = 0; a < uuids.length; a++) { + app.xr.anchors.forget(uuids[a]); + } + return; + } + } + }); + } + + // create entity for anchors + app.xr.anchors.on('add', (anchor) => { + let entity = cone.clone(); + app.root.addChild(entity); + entity.setPosition(anchor.getPosition()); + entity.setRotation(anchor.getRotation()); + entity.translateLocal(0, 0.05, 0); + + anchor.on('change', () => { entity.setPosition(anchor.getPosition()); entity.setRotation(anchor.getRotation()); entity.translateLocal(0, 0.05, 0); + }); - anchor.on('change', () => { - entity.setPosition(anchor.getPosition()); - entity.setRotation(anchor.getRotation()); - entity.translateLocal(0, 0.05, 0); - }); + if (anchor.persistent) { + entity.render.material = materialPersistent; + } - anchor.once('destroy', () => { - entity.destroy(); - entity = null; - }); + anchor.on('persist', () => { + entity.render.material = materialPersistent; }); - if (!app.xr.isAvailable(pc.XRTYPE_AR)) { - message("Immersive AR is not available"); - } else if (!app.xr.hitTest.supported) { - message("AR Hit Test is not supported"); - } else if (!app.xr.anchors.supported) { - message("AR Anchors are not supported"); - } else if (!app.xr.anchors.persistence) { - message("AR Anchors Persistence is not supported"); - } else { - message("Touch screen to start AR session and look at the floor or walls"); - } + anchor.on('forget', () => { + entity.render.material = materialStandard; + }); + + anchor.once('destroy', () => { + entity.destroy(); + entity = null; + }); + }); + + if (!app.xr.isAvailable(pc.XRTYPE_AR)) { + message("Immersive AR is not available"); + } else if (!app.xr.hitTest.supported) { + message("AR Hit Test is not supported"); + } else if (!app.xr.anchors.supported) { + message("AR Anchors are not supported"); + } else if (!app.xr.anchors.persistence) { + message("AR Anchors Persistence is not supported"); } else { - message("WebXR is not supported"); + message("Touch screen to start AR session and look at the floor or walls"); } - }); + } else { + message("WebXR is not supported"); + } return app; } From 3ccfbdccf6c4176704aa51f9fb01389e3d0417a4 Mon Sep 17 00:00:00 2001 From: mrmaxm Date: Wed, 1 Nov 2023 19:42:51 +0200 Subject: [PATCH 07/11] linter --- src/framework/xr/xr-anchor.js | 16 ++++++++++------ src/framework/xr/xr-anchors.js | 3 ++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/framework/xr/xr-anchor.js b/src/framework/xr/xr-anchor.js index fafc4007abb..ae40d54f772 100644 --- a/src/framework/xr/xr-anchor.js +++ b/src/framework/xr/xr-anchor.js @@ -179,11 +179,15 @@ class XrAnchor extends EventHandler { * persistent UUID has been generated or error if failed. */ persist(callback) { - if (!this._anchors.persistence) - return callback(new Error('Persistent Anchors are not supported'), null); + if (!this._anchors.persistence) { + callback(new Error('Persistent Anchors are not supported'), null); + return; + } - if (this._uuid) - return callback(null, this._uuid); + if (this._uuid) { + callback(null, this._uuid); + return; + } if (this._uuidRequests) { this._uuidRequests.push(callback); @@ -197,7 +201,7 @@ class XrAnchor extends EventHandler { this._uuid = uuid; this._anchors._indexByUuid.set(this._uuid, this); callback(null, uuid); - for(let i = 0; i < this._uuidRequests.length; i++) { + for (let i = 0; i < this._uuidRequests.length; i++) { this._uuidRequests[i](null, uuid); } this._uuidRequests = null; @@ -205,7 +209,7 @@ class XrAnchor extends EventHandler { }) .catch((ex) => { callback(ex); - for(let i = 0; i < this._uuidRequests.length; i++) { + for (let i = 0; i < this._uuidRequests.length; i++) { this._uuidRequests[i](ex); } this._uuidRequests = null; diff --git a/src/framework/xr/xr-anchors.js b/src/framework/xr/xr-anchors.js index d01b6054940..1177907cc4f 100644 --- a/src/framework/xr/xr-anchors.js +++ b/src/framework/xr/xr-anchors.js @@ -165,6 +165,7 @@ class XrAnchors extends EventHandler { * }); */ create(position, rotation, callback) { + // eslint-disable-next-line no-undef if (window.XRHitTestResult && position instanceof XRHitTestResult) { const hitResult = position; callback = rotation; @@ -233,7 +234,7 @@ class XrAnchors extends EventHandler { if (callback) callback(new Error('WebXR session is not active')); return; } - + this.manager.session.restorePersistentAnchor(uuid) .then((xrAnchor) => { const anchor = new XrAnchor(this, xrAnchor, uuid); From 82084315b597a18ee42b5146c94a05861171beb6 Mon Sep 17 00:00:00 2001 From: mrmaxm Date: Wed, 1 Nov 2023 19:50:38 +0200 Subject: [PATCH 08/11] anchor.persist callback is optional --- src/framework/xr/xr-anchor.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/framework/xr/xr-anchor.js b/src/framework/xr/xr-anchor.js index ae40d54f772..81865cad6ae 100644 --- a/src/framework/xr/xr-anchor.js +++ b/src/framework/xr/xr-anchor.js @@ -180,17 +180,17 @@ class XrAnchor extends EventHandler { */ persist(callback) { if (!this._anchors.persistence) { - callback(new Error('Persistent Anchors are not supported'), null); + if (callback) callback(new Error('Persistent Anchors are not supported'), null); return; } if (this._uuid) { - callback(null, this._uuid); + if (callback) callback(null, this._uuid); return; } if (this._uuidRequests) { - this._uuidRequests.push(callback); + if (callback) this._uuidRequests.push(callback); return; } @@ -200,7 +200,7 @@ class XrAnchor extends EventHandler { .then((uuid) => { this._uuid = uuid; this._anchors._indexByUuid.set(this._uuid, this); - callback(null, uuid); + if (callback) callback(null, uuid); for (let i = 0; i < this._uuidRequests.length; i++) { this._uuidRequests[i](null, uuid); } @@ -208,7 +208,7 @@ class XrAnchor extends EventHandler { this.fire('persist', uuid); }) .catch((ex) => { - callback(ex); + if (callback) callback(ex); for (let i = 0; i < this._uuidRequests.length; i++) { this._uuidRequests[i](ex); } From 4efade276f39e421b74c92a72a6e11fc8d909c25 Mon Sep 17 00:00:00 2001 From: mrmaxm Date: Wed, 1 Nov 2023 19:54:42 +0200 Subject: [PATCH 09/11] fix tests --- src/framework/xr/xr-anchors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/framework/xr/xr-anchors.js b/src/framework/xr/xr-anchors.js index 1177907cc4f..d82d0fda492 100644 --- a/src/framework/xr/xr-anchors.js +++ b/src/framework/xr/xr-anchors.js @@ -36,7 +36,7 @@ class XrAnchors extends EventHandler { * @type {boolean} * @private */ - _persistence = !!window?.XRSession?.prototype.restorePersistentAnchor; + _persistence = platform.browser && !!window?.XRSession?.prototype.restorePersistentAnchor; /** * List of anchor creation requests. From fd3ae126aa07760e6867aeb87bd3fb074d9f21e5 Mon Sep 17 00:00:00 2001 From: mrmaxm Date: Wed, 8 Nov 2023 14:52:45 +0200 Subject: [PATCH 10/11] small changes --- src/framework/xr/xr-anchor.js | 15 ++---- src/framework/xr/xr-anchors.js | 95 ++++++++++++++++++++-------------- 2 files changed, 60 insertions(+), 50 deletions(-) diff --git a/src/framework/xr/xr-anchor.js b/src/framework/xr/xr-anchor.js index 81865cad6ae..0cfb4d4a681 100644 --- a/src/framework/xr/xr-anchor.js +++ b/src/framework/xr/xr-anchor.js @@ -13,7 +13,7 @@ import { Quat } from '../../core/math/quat.js'; */ /** - * Callback used by {@link XrAnchor#persist}. + * Callback used by {@link XrAnchor#forget}. * * @callback XrAnchorForgetCallback * @param {Error|null} err - The Error object if failed to forget an anchor or null if succeeded. @@ -117,19 +117,10 @@ class XrAnchor extends EventHandler { */ destroy() { if (!this._xrAnchor) return; - this._anchors._index.delete(this._xrAnchor); - - if (this._uuid) - this._anchors._indexByUuid.delete(this._uuid); - - const ind = this._anchors._list.indexOf(this); - if (ind !== -1) this._anchors._list.splice(ind, 1); - + const xrAnchor = this._xrAnchor; this._xrAnchor.delete(); this._xrAnchor = null; - - this.fire('destroy'); - this._anchors.fire('destroy', this); + this.fire('destroy', xrAnchor, this); } /** diff --git a/src/framework/xr/xr-anchors.js b/src/framework/xr/xr-anchors.js index d82d0fda492..2b950392f7b 100644 --- a/src/framework/xr/xr-anchors.js +++ b/src/framework/xr/xr-anchors.js @@ -41,7 +41,7 @@ class XrAnchors extends EventHandler { /** * List of anchor creation requests. * - * @type {Array} + * @type {object[]} * @private */ _creationQueue = []; @@ -50,7 +50,7 @@ class XrAnchors extends EventHandler { * Index of XrAnchors, with XRAnchor (native handle) used as a key. * * @type {Map} - * @ignore + * @private */ _index = new Map(); @@ -58,13 +58,13 @@ class XrAnchors extends EventHandler { * Index of XrAnchors, with UUID (persistent string) used as a key. * * @type {Map} - * @ignore + * @private */ _indexByUuid = new Map(); /** - * @type {Array} - * @ignore + * @type {XrAnchor[]} + * @private */ _list = []; @@ -131,14 +131,43 @@ class XrAnchors extends EventHandler { } this._creationQueue.length = 0; + this._index.clear(); + this._indexByUuid.clear(); + // destroy all anchors - if (this._list) { - let i = this._list.length; - while (i--) { - this._list[i].destroy(); - } - this._list.length = 0; + let i = this._list.length; + while (i--) { + this._list[i].destroy(); } + this._list.length = 0; + } + + /** + * @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 = null) { + const 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); + const ind = this._list.indexOf(anchor); + if (ind !== -1) this._list.splice(ind, 1); + this.fire('destroy', anchor); } /** @@ -171,30 +200,23 @@ class XrAnchors extends EventHandler { callback = rotation; if (!this._supported) { - if (callback) callback(new Error('Anchors API is not supported'), null); + callback?.(new Error('Anchors API is not supported'), null); return; } if (!hitResult.createAnchor) { - if (callback) callback(new Error('Creating Anchor from Hit Test is not supported'), null); + callback?.(new Error('Creating Anchor from Hit Test is not supported'), null); return; } hitResult.createAnchor() .then((xrAnchor) => { - const anchor = new XrAnchor(this, xrAnchor); - this._index.set(xrAnchor, anchor); - this._list.push(anchor); - - if (callback) - callback(null, anchor); - + const anchor = this._createAnchor(xrAnchor); + callback?.(null, anchor); this.fire('add', anchor); }) .catch((ex) => { - if (callback) - callback(ex, null); - + callback?.(ex, null); this.fire('error', ex); }); } else { @@ -226,25 +248,23 @@ class XrAnchors extends EventHandler { */ restore(uuid, callback) { if (!this._persistence) { - if (callback) callback(new Error('Anchor Persistence is not supported')); + callback?.(new Error('Anchor Persistence is not supported'), null); return; } if (!this.manager.active) { - if (callback) callback(new Error('WebXR session is not active')); + callback?.(new Error('WebXR session is not active'), null); return; } this.manager.session.restorePersistentAnchor(uuid) .then((xrAnchor) => { - const anchor = new XrAnchor(this, xrAnchor, uuid); - this._index.set(xrAnchor, anchor); - this._indexByUuid.set(uuid, anchor); - this._list.push(anchor); + const anchor = this._createAnchor(xrAnchor, uuid); + callback?.(null, anchor); this.fire('add', anchor); }) .catch((ex) => { - if (callback) callback(ex, null); + callback?.(ex, null); this.fire('error', ex); }); } @@ -264,21 +284,21 @@ class XrAnchors extends EventHandler { */ forget(uuid, callback) { if (!this._persistence) { - if (callback) callback(new Error('Anchor Persistence is not supported')); + callback?.(new Error('Anchor Persistence is not supported')); return; } if (!this.manager.active) { - if (callback) callback(new Error('WebXR session is not active')); + callback?.(new Error('WebXR session is not active')); return; } this.manager.session.deletePersistentAnchor(uuid) .then(() => { - if (callback) callback(null); + callback?.(null); }) .catch((ex) => { - if (callback) callback(ex); + callback?.(ex); this.fire('error', ex); }); } @@ -314,6 +334,7 @@ class XrAnchors extends EventHandler { if (frame.trackedAnchors.has(xrAnchor)) continue; + this._index.delete(xrAnchor); anchor.destroy(); } @@ -335,9 +356,7 @@ class XrAnchors extends EventHandler { continue; } - const anchor = new XrAnchor(this, xrAnchor); - this._index.set(xrAnchor, anchor); - this._list.push(anchor); + const anchor = this._createAnchor(xrAnchor); anchor.update(frame); const callback = this._callbacksAnchors.get(xrAnchor); @@ -386,7 +405,7 @@ class XrAnchors extends EventHandler { /** * List of available {@link XrAnchor}s. * - * @type {Array} + * @type {XrAnchor[]} */ get list() { return this._list; From 82b5043c3a7efc01104e9f51541347d72f83d6d7 Mon Sep 17 00:00:00 2001 From: mrmaxm Date: Thu, 9 Nov 2023 11:19:39 +0200 Subject: [PATCH 11/11] callback?.( --- src/framework/xr/xr-anchor.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/framework/xr/xr-anchor.js b/src/framework/xr/xr-anchor.js index 0cfb4d4a681..fc1c109b9ee 100644 --- a/src/framework/xr/xr-anchor.js +++ b/src/framework/xr/xr-anchor.js @@ -171,12 +171,12 @@ class XrAnchor extends EventHandler { */ persist(callback) { if (!this._anchors.persistence) { - if (callback) callback(new Error('Persistent Anchors are not supported'), null); + callback?.(new Error('Persistent Anchors are not supported'), null); return; } if (this._uuid) { - if (callback) callback(null, this._uuid); + callback?.(null, this._uuid); return; } @@ -191,7 +191,7 @@ class XrAnchor extends EventHandler { .then((uuid) => { this._uuid = uuid; this._anchors._indexByUuid.set(this._uuid, this); - if (callback) callback(null, uuid); + callback?.(null, uuid); for (let i = 0; i < this._uuidRequests.length; i++) { this._uuidRequests[i](null, uuid); } @@ -199,7 +199,7 @@ class XrAnchor extends EventHandler { this.fire('persist', uuid); }) .catch((ex) => { - if (callback) callback(ex); + callback?.(ex, null); for (let i = 0; i < this._uuidRequests.length; i++) { this._uuidRequests[i](ex); } @@ -215,13 +215,13 @@ class XrAnchor extends EventHandler { */ forget(callback) { if (!this._uuid) { - if (callback) callback(new Error('Anchor is not persistent')); + callback?.(new Error('Anchor is not persistent')); return; } this._anchors.forget(this._uuid, (ex) => { this._uuid = null; - if (callback) callback(ex); + callback?.(ex); this.fire('forget'); }); }