Files
2025-07-21 09:11:14 +02:00

1670 lines
67 KiB
C#

/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* Licensed under the Oculus SDK License Agreement (the "License");
* you may not use the Oculus SDK except in compliance with the License,
* which is provided at the time of installation or download, or which
* otherwise accompanies this software in either electronic or hard copy form.
*
* You may obtain a copy of the License at
*
* https://developer.oculus.com/licenses/oculussdk/
*
* Unless required by applicable law or agreed to in writing, the Oculus SDK
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
#if DEVELOPMENT_BUILD
using System.Linq;
#endif
using Unity.Collections;
using UnityEngine;
using Debug = UnityEngine.Debug;
/// <summary>
/// Represents a spatial anchor.
/// </summary>
/// <remarks>
/// This component can be used in two ways: to create a new spatial anchor or to bind to an existing spatial anchor.
///
/// To create a new spatial anchor, simply add this component to any GameObject. The transform of the GameObject is used
/// to create a new spatial anchor in the Oculus Runtime. Afterwards, the GameObject's transform will be updated
/// automatically. The creation operation is asynchronous, and, if it fails, this component will be destroyed.
///
/// To load previously saved anchors and bind them to an <see cref="OVRSpatialAnchor"/>, see
/// <see cref="LoadUnboundAnchors"/>.
/// </remarks>
[DisallowMultipleComponent]
[HelpURL("https://developer.oculus.com/reference/unity/latest/class_o_v_r_spatial_anchor")]
public class OVRSpatialAnchor : MonoBehaviour
{
private bool _startCalled;
private ulong _requestId;
private readonly SaveOptions _defaultSaveOptions = new SaveOptions
{
Storage = OVRSpace.StorageLocation.Local,
};
private readonly EraseOptions _defaultEraseOptions = new EraseOptions
{
Storage = OVRSpace.StorageLocation.Local,
};
/// <summary>
/// Event that is dispatched when the localization process finishes.
/// </summary>
public event Action<OperationResult> OnLocalize;
/// <summary>
/// The space associated with the spatial anchor.
/// </summary>
/// <remarks>
/// The <see cref="OVRSpace"/> represents the runtime instance of the spatial anchor and will change across
/// different sessions.
/// </remarks>
public OVRSpace Space { get; private set; }
/// <summary>
/// The UUID associated with the spatial anchor.
/// </summary>
/// <remarks>
/// UUIDs persist across sessions. If you load a persisted anchor, you can use the UUID to identify
/// it.
/// </remarks>
public Guid Uuid { get; private set; }
/// <summary>
/// Checks whether the spatial anchor is created.
/// </summary>
/// <remarks>
/// Creation is asynchronous and may take several frames. If creation fails, the component is destroyed.
/// </remarks>
public bool Created => Space.Valid;
/// <summary>
/// Checks whether the spatial anchor is pending creation.
/// </summary>
public bool PendingCreation => _requestId != 0;
/// <summary>
/// Checks whether the spatial anchor is localized.
/// </summary>
/// <remarks>
/// When you create a new spatial anchor, it may take a few frames before it is localized. Once localized,
/// its transform updates automatically.
/// </remarks>
public bool Localized => Space.Valid &&
OVRPlugin.GetSpaceComponentStatus(Space, OVRPlugin.SpaceComponentType.Locatable,
out var isEnabled, out _) && isEnabled;
/// <summary>
/// Initializes this component from an existing space handle and uuid, e.g., the result of a call to
/// <see cref="OVRPlugin.QuerySpaces"/>.
/// </summary>
/// <remarks>
/// This method associates the component with an existing spatial anchor, for example, the one that was saved in
/// a previous session. Do not call this method to create a new spatial anchor.
///
/// If you call this method, you must do so prior to the component's `Start` method. You cannot change the spatial
/// anchor associated with this component after that.
/// </remarks>
/// <param name="space">The existing <see cref="OVRSpace"/> to associate with this spatial anchor.</param>
/// <param name="uuid">The universally unique identifier to associate with this spatial anchor.</param>
/// <exception cref="InvalidOperationException">Thrown if `Start` has already been called on this component.</exception>
/// <exception cref="ArgumentException">Thrown if <paramref name="space"/> is not <see cref="OVRSpace.Valid"/>.</exception>
public void InitializeFromExisting(OVRSpace space, Guid uuid)
{
if (_startCalled)
throw new InvalidOperationException(
$"Cannot call {nameof(InitializeFromExisting)} after {nameof(Start)}. This must be set once upon creation.");
try
{
if (!space.Valid)
throw new ArgumentException($"Invalid space {space}.", nameof(space));
ThrowIfBound(uuid);
}
catch
{
Destroy(this);
throw;
}
InitializeUnchecked(space, uuid);
}
/// <summary>
/// Saves the <see cref="OVRSpatialAnchor"/> to local persistent storage.
/// </summary>
/// <remarks>
/// This method is asynchronous. Use <paramref name="onComplete"/> to be notified of completion.
///
/// When saved, an <see cref="OVRSpatialAnchor"/> can be loaded by a different session. Use the
/// <see cref="Uuid"/> to identify the same <see cref="OVRSpatialAnchor"/> at a future time.
///
/// This operation fully succeeds or fails, which means, either all anchors are successfully saved,
/// or the operation fails.
/// </remarks>
/// <param name="onComplete">
/// Invoked when the save operation completes. May be null. Parameters are
/// - <see cref="OVRSpatialAnchor"/>: The anchor being saved.
/// - `bool`: A value indicating whether the save operation succeeded.
/// </param>
public void Save(Action<OVRSpatialAnchor, bool> onComplete = null)
{
Save(_defaultSaveOptions, onComplete);
}
private static NativeArray<ulong> ToNativeArray(ICollection<OVRSpatialAnchor> anchors)
{
var count = anchors.Count;
var spaces = new NativeArray<ulong>(count, Allocator.Temp);
var i = 0;
foreach (var anchor in anchors.ToNonAlloc())
{
spaces[i++] = anchor ? anchor.Space : 0;
}
return spaces;
}
/// <summary>
/// Saves the <see cref="OVRSpatialAnchor"/> with specified <see cref="SaveOptions"/>.
/// </summary>
/// <remarks>
/// This method is asynchronous. Use <paramref name="onComplete"/> to be notified of completion.
/// When saved, the <see cref="OVRSpatialAnchor"/> can be loaded by a different session. Use the
/// <see cref="Uuid"/> to identify the same <see cref="OVRSpatialAnchor"/> at a future time.
///
/// This operation fully succeeds or fails; that is, either all anchors are successfully saved,
/// or the operation fails.
/// </remarks>
/// <param name="saveOptions">Options how the anchor is saved. whether local or cloud.</param>
/// <param name="onComplete">
/// Invoked when the save operation completes. May be null. Parameters are
/// - <see cref="OVRSpatialAnchor"/>: The anchor being saved.
/// - `bool`: A value indicating whether the save operation succeeded.
/// </param>
public void Save(SaveOptions saveOptions, Action<OVRSpatialAnchor, bool> onComplete = null)
{
var task = SaveAsync(saveOptions);
if (onComplete != null)
{
InvertedCapture<bool, OVRSpatialAnchor>.ContinueTaskWith(task, onComplete, this);
}
}
/// <summary>
/// Saves the <see cref="OVRSpatialAnchor"/> with specified <see cref="SaveOptions"/>.
/// </summary>
/// <remarks>
/// This method is asynchronous; use the returned <see cref="OVRTask{TResult}"/> to be notified of completion.
/// When saved, the <see cref="OVRSpatialAnchor"/> can be loaded by a different session. Use the
/// <see cref="Uuid"/> to identify the same <see cref="OVRSpatialAnchor"/> at a future time.
///
/// This operation fully succeeds or fails; that is, either all anchors are successfully saved,
/// or the operation fails.
/// </remarks>
/// <returns>
/// An <see cref="OVRTask{TResult}"/> with a boolean type parameter indicating the success of the save operation.
/// </returns>
public OVRTask<bool> SaveAsync() => SaveAsync(_defaultSaveOptions);
/// <summary>
/// Saves the <see cref="OVRSpatialAnchor"/> with specified <see cref="SaveOptions"/>.
/// </summary>
/// <remarks>
/// This method is asynchronous; use the returned <see cref="OVRTask{TResult}"/> to be notified of completion.
/// When saved, the <see cref="OVRSpatialAnchor"/> can be loaded by a different session. Use the
/// <see cref="Uuid"/> to identify the same <see cref="OVRSpatialAnchor"/> at a future time.
///
/// This operation fully succeeds or fails; that is, either all anchors are successfully saved,
/// or the operation fails.
/// </remarks>
/// <param name="saveOptions">Options for how the anchor will be saved.</param>
/// <returns>
/// An <see cref="OVRTask{TResult}"/> with a boolean type parameter indicating the success of the save operation.
/// </returns>
public OVRTask<bool> SaveAsync(SaveOptions saveOptions)
{
var requestId = Guid.NewGuid();
SaveRequests[saveOptions.Storage].Add(this);
AsyncRequestTaskIds[this] = requestId;
return OVRTask.FromGuid<bool>(requestId);
}
/// <summary>
/// Saves a collection of <see cref="OVRSpatialAnchor"/> to specified storage.
/// </summary>
/// <remarks>
/// This method is asynchronous. Use <paramref name="onComplete"/> to be notified of completion.
/// When saved, an <see cref="OVRSpatialAnchor"/> can be loaded by a different session. Use the
/// <see cref="Uuid"/> to identify the same <see cref="OVRSpatialAnchor"/> at a future time.
/// </remarks>
/// <param name="anchors">Collection of anchors</param>
/// <param name="saveOptions">Options how the anchors are saved whether local or cloud.</param>
/// <param name="onComplete">
/// Invoked when the save operation completes. May be null. <paramref name="onComplete"/> receives two parameters:
/// - `ICollection&lt;OVRSpatialAnchor&gt;`: The same collection as in <paramref name="anchors"/> parameter
/// - `OperationResult`: An error code indicating whether the save operation succeeded or not.
/// </param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="anchors"/> is `null`.</exception>
public static void Save(ICollection<OVRSpatialAnchor> anchors, SaveOptions saveOptions,
Action<ICollection<OVRSpatialAnchor>, OperationResult> onComplete = null)
{
if (anchors == null)
throw new ArgumentNullException(nameof(anchors));
using var spaces = ToNativeArray(anchors);
var saveResult =
OVRPlugin.SaveSpaceList(spaces, saveOptions.Storage.ToSpaceStorageLocation(), out var requestId);
if (saveResult.IsSuccess())
{
Development.LogRequest(requestId, $"Saving spatial anchors...");
MultiAnchorCompletionDelegates[requestId] = new MultiAnchorDelegatePair
{
Anchors = CopyAnchorListIntoListFromPool(anchors),
Delegate = onComplete
};
if (onComplete != null)
{
OVRTelemetry.Client.MarkerStart(OVRTelemetryConstants.Scene.MarkerId.SpatialAnchorSave,
requestId.GetHashCode());
}
}
else
{
Development.LogError(
$"{nameof(OVRPlugin)}.{nameof(OVRPlugin.SaveSpaceList)} failed with error {saveResult}.");
onComplete?.Invoke(anchors, (OperationResult)saveResult);
}
}
private static List<OVRSpatialAnchor> CopyAnchorListIntoListFromPool(
IEnumerable<OVRSpatialAnchor> anchorList)
{
var poolList = OVRObjectPool.List<OVRSpatialAnchor>();
poolList.AddRange(anchorList);
return poolList;
}
/// <summary>
/// Shares the anchor to an <see cref="OVRSpaceUser"/>.
/// The specified user will be able to download, track, and share specified anchors.
/// </summary>
/// <remarks>
/// This method is asynchronous. Use <paramref name="onComplete"/> to be notified of completion.
/// </remarks>
/// <param name="user">An Oculus user to share the anchor with.</param>
/// <param name="onComplete">
/// Invoked when the share operation completes. May be null. Delegate parameter is
/// - `OperationResult`: An error code that indicates whether the share operation succeeded or not.
/// </param>
public void Share(OVRSpaceUser user, Action<OperationResult> onComplete = null)
{
var task = ShareAsync(user);
if (onComplete != null)
{
task.ContinueWith(onComplete);
}
}
/// <summary>
/// Shares the anchor to an <see cref="OVRSpaceUser"/>.
/// The specified user will be able to download, track, and share specified anchors.
/// </summary>
/// <remarks>
/// This method is asynchronous; use the returned <see cref="OVRTask{TResult}"/> to be notified of completion.
/// </remarks>
/// <param name="user">An Oculus user to share the anchor with.</param>
/// <returns>
/// An <see cref="OVRTask{TResult}"/> with a <see cref="OperationResult"/> type parameter indicating the success of the share operation.
/// </returns>
public OVRTask<OperationResult> ShareAsync(OVRSpaceUser user)
{
var userList = OVRObjectPool.List<OVRSpaceUser>();
userList.Add(user);
return ShareAsyncInternal(userList);
}
/// <summary>
/// Shares the anchor with two <see cref="OVRSpaceUser"/>.
/// Specified users will be able to download, track, and share specified anchors.
/// </summary>
/// <remarks>
/// This method is asynchronous. Use <paramref name="onComplete"/> to be notified of completion.
/// </remarks>
/// <param name="user1">An Oculus user to share the anchor with.</param>
/// <param name="user2">An Oculus user to share the anchor with.</param>
/// <param name="onComplete">
/// Invoked when the share operation completes. May be null. Delegate parameter is
/// - `OperationResult`: An error code that indicates whether the share operation succeeded or not.
/// </param>
public void Share(OVRSpaceUser user1, OVRSpaceUser user2, Action<OperationResult> onComplete = null)
{
var task = ShareAsync(user1, user2);
if (onComplete != null)
{
task.ContinueWith(onComplete);
}
}
/// <summary>
/// Shares the anchor to an <see cref="OVRSpaceUser"/>.
/// The specified user will be able to download, track, and share specified anchors.
/// </summary>
/// <remarks>
/// This method is asynchronous; use the returned <see cref="OVRTask{TResult}"/> to be notified of completion.
/// </remarks>
/// <param name="user1">An Oculus user to share the anchor with.</param>
/// <param name="user2">An Oculus user to share the anchor with.</param>
/// <returns>
/// An <see cref="OVRTask{TResult}"/> with a <see cref="OperationResult"/> type parameter indicating the success of the share operation.
/// </returns>
public OVRTask<OperationResult> ShareAsync(OVRSpaceUser user1, OVRSpaceUser user2)
{
var userList = OVRObjectPool.List<OVRSpaceUser>();
userList.Add(user1);
userList.Add(user2);
return ShareAsyncInternal(userList);
}
/// <summary>
/// Shares the anchor with three <see cref="OVRSpaceUser"/>.
/// Specified users will be able to download, track, and share specified anchors.
/// </summary>
/// <remarks>
/// This method is asynchronous. Use <paramref name="onComplete"/> to be notified of completion.
/// </remarks>
/// <param name="user1">An Oculus user to share the anchor with.</param>
/// <param name="user2">An Oculus user to share the anchor with.</param>
/// <param name="user3">An Oculus user to share the anchor with.</param>
/// <param name="onComplete">
/// Invoked when the share operation completes. May be null. Delegate parameter is
/// - `OperationResult`: An error code that indicates whether the share operation succeeded or not.
/// </param>
public void Share(OVRSpaceUser user1, OVRSpaceUser user2, OVRSpaceUser user3,
Action<OperationResult> onComplete = null)
{
var task = ShareAsync(user1, user2, user3);
if (onComplete != null)
{
task.ContinueWith(onComplete);
}
}
/// <summary>
/// Shares the anchor to an <see cref="OVRSpaceUser"/>.
/// The specified user will be able to download, track, and share specified anchors.
/// </summary>
/// <remarks>
/// This method is asynchronous; use the returned <see cref="OVRTask{TResult}"/> to be notified of completion.
/// </remarks>
/// <param name="user1">An Oculus user to share the anchor with.</param>
/// <param name="user2">An Oculus user to share the anchor with.</param>
/// <param name="user3">An Oculus user to share the anchor with.</param>
/// <returns>
/// An <see cref="OVRTask{TResult}"/> with a <see cref="OperationResult"/> type parameter indicating the success of the share operation.
/// </returns>
public OVRTask<OperationResult> ShareAsync(OVRSpaceUser user1, OVRSpaceUser user2, OVRSpaceUser user3)
{
var userList = OVRObjectPool.List<OVRSpaceUser>();
userList.Add(user1);
userList.Add(user2);
userList.Add(user3);
return ShareAsyncInternal(userList);
}
/// <summary>
/// Shares the anchor with four <see cref="OVRSpaceUser"/>.
/// Specified users will be able to download, track, and share specified anchors.
/// </summary>
/// <remarks>
/// This method is asynchronous. Use <paramref name="onComplete"/> to be notified of completion.
/// </remarks>
/// <param name="user1">An Oculus user to share the anchor with.</param>
/// <param name="user2">An Oculus user to share the anchor with.</param>
/// <param name="user3">An Oculus user to share the anchor with.</param>
/// <param name="user4">An Oculus user to share the anchor with.</param>
/// <param name="onComplete">
/// Invoked when the share operation completes. May be null. Delegate parameter is
/// - `OperationResult`: An error code that indicates whether the share operation succeeded or not.
/// </param>
public void Share(OVRSpaceUser user1, OVRSpaceUser user2, OVRSpaceUser user3, OVRSpaceUser user4,
Action<OperationResult> onComplete = null)
{
var task = ShareAsync(user1, user2, user3, user4);
if (onComplete != null)
{
task.ContinueWith(onComplete);
}
}
/// <summary>
/// Shares the anchor to an <see cref="OVRSpaceUser"/>.
/// The specified user will be able to download, track, and share specified anchors.
/// </summary>
/// <remarks>
/// This method is asynchronous; use the returned <see cref="OVRTask{TResult}"/> to be notified of completion.
/// </remarks>
/// <param name="user1">An Oculus user to share the anchor with.</param>
/// <param name="user2">An Oculus user to share the anchor with.</param>
/// <param name="user3">An Oculus user to share the anchor with.</param>
/// <param name="user4">An Oculus user to share the anchor with.</param>
/// <returns>
/// An <see cref="OVRTask{TResult}"/> with a <see cref="OperationResult"/> type parameter indicating the success of the share operation.
/// </returns>
public OVRTask<OperationResult> ShareAsync(OVRSpaceUser user1, OVRSpaceUser user2, OVRSpaceUser user3,
OVRSpaceUser user4)
{
var userList = OVRObjectPool.List<OVRSpaceUser>();
userList.Add(user1);
userList.Add(user2);
userList.Add(user3);
userList.Add(user4);
return ShareAsyncInternal(userList);
}
/// <summary>
/// Shares the anchor to a collection of <see cref="OVRSpaceUser"/>.
/// Specified users will be able to download, track, and share specified anchors.
/// </summary>
/// <remarks>
/// This method is asynchronous. Use <paramref name="onComplete"/> to be notified of completion.
/// </remarks>
/// <param name="users">A collection of Oculus users to share the anchor with.</param>
/// <param name="onComplete">
/// Invoked when the share operation completes. May be null. Delegate parameter is
/// - `OperationResult`: An error code that indicates whether the share operation succeeded or not.
/// </param>
public void Share(IEnumerable<OVRSpaceUser> users, Action<OperationResult> onComplete = null)
{
var task = ShareAsync(users);
if (onComplete != null)
{
task.ContinueWith(onComplete);
}
}
/// <summary>
/// Shares the anchor to an <see cref="OVRSpaceUser"/>.
/// The specified user will be able to download, track, and share specified anchors.
/// </summary>
/// <remarks>
/// This method is asynchronous; use the returned <see cref="OVRTask{TResult}"/> to be notified of completion.
/// </remarks>
/// <param name="users">A collection of Oculus users to share the anchor with.</param>
/// <returns>
/// An <see cref="OVRTask{TResult}"/> with a <see cref="OperationResult"/> type parameter indicating the success of the share operation.
/// </returns>
public OVRTask<OperationResult> ShareAsync(IEnumerable<OVRSpaceUser> users)
{
var userList = OVRObjectPool.List<OVRSpaceUser>();
userList.AddRange(users);
return ShareAsyncInternal(userList);
}
/// <summary>
/// Shares a collection of <see cref="OVRSpatialAnchor"/> to specified users.
/// Specified users will be able to download, track, and share specified anchors.
/// </summary>
/// <remarks>
/// This method is asynchronous. Use <paramref name="onComplete"/> to be notified of completion.
///
/// This operation fully succeeds or fails, which means, either all anchors are successfully shared
/// or the operation fails.
/// </remarks>
/// <param name="anchors">The collection of anchors to share.</param>
/// <param name="users">An array of Oculus users to share these anchors with.</param>
/// <param name="onComplete">
/// Invoked when the share operation completes. May be null. Delegate parameter is
/// - `ICollection&lt;OVRSpatialAnchor&gt;`: The collection of anchors being shared.
/// - `OperationResult`: An error code that indicates whether the share operation succeeded or not.
/// </param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="anchors"/> is `null`.</exception>
public static void Share(ICollection<OVRSpatialAnchor> anchors, ICollection<OVRSpaceUser> users,
Action<ICollection<OVRSpatialAnchor>, OperationResult> onComplete = null)
{
if (anchors == null)
throw new ArgumentNullException(nameof(anchors));
using var spaces = ToNativeArray(anchors);
var handles = new NativeArray<ulong>(users.Count, Allocator.Temp);
using var disposer = handles;
int i = 0;
foreach (var user in users)
{
handles[i++] = user._handle;
}
var shareResult = OVRPlugin.ShareSpaces(spaces, handles, out var requestId);
if (shareResult.IsSuccess())
{
Development.LogRequest(requestId, $"Sharing {(uint)spaces.Length} spatial anchors...");
MultiAnchorCompletionDelegates[requestId] = new MultiAnchorDelegatePair
{
Anchors = CopyAnchorListIntoListFromPool(anchors),
Delegate = onComplete
};
}
else
{
Development.LogError(
$"{nameof(OVRPlugin)}.{nameof(OVRPlugin.ShareSpaces)} failed with error {shareResult}.");
onComplete?.Invoke(anchors, (OperationResult)shareResult);
}
}
private OVRTask<OperationResult> ShareAsyncInternal(List<OVRSpaceUser> users)
{
var shareRequestAnchors = GetListToStoreTheShareRequest(users);
shareRequestAnchors.Add(this);
var requestId = Guid.NewGuid();
AsyncRequestTaskIds[this] = requestId;
return OVRTask.FromGuid<OperationResult>(requestId);
}
private List<OVRSpatialAnchor> GetListToStoreTheShareRequest(List<OVRSpaceUser> users)
{
users.Sort((x, y) => x.Id.CompareTo(y.Id));
foreach (var (shareRequestUsers, shareRequestAnchors) in ShareRequests)
{
if (!AreSortedUserListsEqual(users, shareRequestUsers))
{
continue;
}
// reuse the current request
return shareRequestAnchors;
}
// add a new request
var anchorList = OVRObjectPool.List<OVRSpatialAnchor>();
ShareRequests.Add((users, anchorList));
return anchorList;
}
private static bool AreSortedUserListsEqual(IReadOnlyList<OVRSpaceUser> sortedList1,
IReadOnlyList<OVRSpaceUser> sortedList2)
{
if (sortedList1.Count != sortedList2.Count)
{
return false;
}
for (var i = 0; i < sortedList1.Count; i++)
{
if (sortedList1[i].Id != sortedList2[i].Id)
{
return false;
}
}
return true;
}
/// <summary>
/// Erases the <see cref="OVRSpatialAnchor"/> from persistent storage.
/// </summary>
/// <remarks>
/// This method is asynchronous. Use <paramref name="onComplete"/> to be notified of completion.
/// Erasing an <see cref="OVRSpatialAnchor"/> does not destroy the anchor.
/// </remarks>
/// <param name="onComplete">
/// Invoked when the erase operation completes. May be null. Parameters are
/// - <see cref="OVRSpatialAnchor"/>: The anchor being erased.
/// - `bool`: A value indicating whether the erase operation succeeded.
/// </param>
public void Erase(Action<OVRSpatialAnchor, bool> onComplete = null)
{
Erase(_defaultEraseOptions, onComplete);
}
/// <summary>
/// Erases the <see cref="OVRSpatialAnchor"/> from specified storage.
/// </summary>
/// <remarks>
/// This method is asynchronous. Use <paramref name="onComplete"/> to be notified of completion.
/// Erasing an <see cref="OVRSpatialAnchor"/> does not destroy the anchor.
/// </remarks>
/// <param name="eraseOptions">Options how the anchor should be erased.</param>
/// <param name="onComplete">
/// Invoked when the erase operation completes. May be null. Parameters are
/// - <see cref="OVRSpatialAnchor"/>: The anchor being erased.
/// - `bool`: A value indicating whether the erase operation succeeded.
/// </param>
public void Erase(EraseOptions eraseOptions, Action<OVRSpatialAnchor, bool> onComplete = null)
{
var task = EraseAsync(eraseOptions);
if (onComplete != null)
{
InvertedCapture<bool, OVRSpatialAnchor>.ContinueTaskWith(task, onComplete, this);
}
}
/// <summary>
/// Erases the <see cref="OVRSpatialAnchor"/> from specified storage.
/// </summary>
/// <remarks>
/// This method is asynchronous; use the returned <see cref="OVRTask{TResult}"/> to be notified of completion.
/// Erasing an <see cref="OVRSpatialAnchor"/> does not destroy the anchor.
/// </remarks>
/// <returns>
/// An <see cref="OVRTask{TResult}"/> with a boolean type parameter indicating the success of the erase operation.
/// </returns>
public OVRTask<bool> EraseAsync() => EraseAsync(_defaultEraseOptions);
/// <summary>
/// Erases the <see cref="OVRSpatialAnchor"/> from specified storage.
/// </summary>
/// <remarks>
/// This method is asynchronous; use the returned <see cref="OVRTask{TResult}"/> to be notified of completion.
/// Erasing an <see cref="OVRSpatialAnchor"/> does not destroy the anchor.
/// </remarks>
/// <param name="eraseOptions">Options for how the anchor should be erased.</param>
/// <returns>
/// An <see cref="OVRTask{TResult}"/> with a boolean type parameter indicating the success of the erase operation.
/// </returns>
public OVRTask<bool> EraseAsync(EraseOptions eraseOptions) =>
OVRPlugin.EraseSpace(Space, eraseOptions.Storage.ToSpaceStorageLocation(), out var requestId)
? OVRTask.FromRequest<bool>(requestId)
: OVRTask.FromResult(false);
private static void ThrowIfBound(Guid uuid)
{
if (SpatialAnchors.ContainsKey(uuid))
throw new InvalidOperationException(
$"Spatial anchor with uuid {uuid} is already bound to an {nameof(OVRSpatialAnchor)}.");
}
// Initializes this component without checking preconditions
private void InitializeUnchecked(OVRSpace space, Guid uuid)
{
SpatialAnchors.Add(uuid, this);
_requestId = 0;
Space = space;
Uuid = uuid;
OVRPlugin.SetSpaceComponentStatus(Space, OVRPlugin.SpaceComponentType.Locatable, true, 0, out _);
OVRPlugin.SetSpaceComponentStatus(Space, OVRPlugin.SpaceComponentType.Storable, true, 0, out _);
OVRPlugin.SetSpaceComponentStatus(Space, OVRPlugin.SpaceComponentType.Sharable, true, 0, out _);
// Try to update the pose as soon as we can.
UpdateTransform();
}
private void Start()
{
_startCalled = true;
if (Space.Valid)
{
Development.Log($"[{Uuid}] Created spatial anchor from existing an existing space.");
}
else
{
CreateSpatialAnchor();
}
}
private void Update()
{
if (Space.Valid)
{
UpdateTransform();
}
}
private void LateUpdate()
{
SaveBatchAnchors();
ShareBatchAnchors();
}
private static void SaveBatchAnchors()
{
foreach (var pair in SaveRequests)
{
if (pair.Value.Count == 0)
{
continue;
}
Save(pair.Value, new SaveOptions { Storage = pair.Key });
pair.Value.Clear();
}
}
private static void ShareBatchAnchors()
{
foreach (var (userList, anchorList) in ShareRequests)
{
if (userList.Count > 0 && anchorList.Count > 0)
{
Share(anchorList, userList);
}
OVRObjectPool.Return(userList);
OVRObjectPool.Return(anchorList);
}
ShareRequests.Clear();
}
private void OnDestroy()
{
if (Space.Valid)
{
OVRPlugin.DestroySpace(Space);
}
SpatialAnchors.Remove(Uuid);
}
private OVRPose GetTrackingSpacePose()
{
var mainCamera = Camera.main;
if (mainCamera)
{
return transform.ToTrackingSpacePose(mainCamera);
}
Development.LogWarning($"No main camera found. Using world-space pose.");
return transform.ToOVRPose(isLocal: false);
}
private void CreateSpatialAnchor()
{
var created = OVRPlugin.CreateSpatialAnchor(new OVRPlugin.SpatialAnchorCreateInfo
{
BaseTracking = OVRPlugin.GetTrackingOriginType(),
PoseInSpace = GetTrackingSpacePose().ToPosef(),
Time = OVRPlugin.GetTimeInSeconds(),
}, out _requestId);
OVRTelemetry.Client.MarkerStart(OVRTelemetryConstants.Scene.MarkerId.SpatialAnchorCreate,
_requestId.GetHashCode());
if (created)
{
Development.LogRequest(_requestId, $"Creating spatial anchor...");
CreationRequests[_requestId] = this;
}
else
{
OVRTelemetry.Client.MarkerEnd(OVRTelemetryConstants.Scene.MarkerId.SpatialAnchorCreate,
OVRPlugin.Qpl.ResultType.Fail, _requestId.GetHashCode());
Development.LogError(
$"{nameof(OVRPlugin)}.{nameof(OVRPlugin.CreateSpatialAnchor)} failed. Destroying {nameof(OVRSpatialAnchor)} component.");
Destroy(this);
}
}
internal static bool TryGetPose(OVRSpace space, out OVRPose pose)
{
var tryLocateSpace = OVRPlugin.TryLocateSpace(space, OVRPlugin.GetTrackingOriginType(), out var posef, out var locationFlags);
if (!tryLocateSpace || !locationFlags.IsOrientationValid() || !locationFlags.IsPositionValid())
{
pose = OVRPose.identity;
return false;
}
pose = posef.ToOVRPose();
var mainCamera = Camera.main;
if (mainCamera)
{
pose = pose.ToWorldSpacePose(mainCamera);
}
return true;
}
private void UpdateTransform()
{
if (TryGetPose(Space, out var pose))
{
transform.SetPositionAndRotation(pose.position, pose.orientation);
}
}
private static bool TryExtractValue<TKey, TValue>(Dictionary<TKey, TValue> dict, TKey key, out TValue value) =>
dict.TryGetValue(key, out value) && dict.Remove(key);
private struct MultiAnchorDelegatePair
{
public List<OVRSpatialAnchor> Anchors;
public Action<ICollection<OVRSpatialAnchor>, OperationResult> Delegate;
}
internal static readonly Dictionary<Guid, OVRSpatialAnchor> SpatialAnchors =
new Dictionary<Guid, OVRSpatialAnchor>();
private static readonly Dictionary<ulong, OVRSpatialAnchor> CreationRequests =
new Dictionary<ulong, OVRSpatialAnchor>();
private static readonly Dictionary<OVRSpace.StorageLocation, List<OVRSpatialAnchor>> SaveRequests =
new Dictionary<OVRSpace.StorageLocation, List<OVRSpatialAnchor>>
{
{ OVRSpace.StorageLocation.Cloud, new List<OVRSpatialAnchor>() },
{ OVRSpace.StorageLocation.Local, new List<OVRSpatialAnchor>() },
};
private static readonly Dictionary<OVRSpatialAnchor, Guid> AsyncRequestTaskIds =
new Dictionary<OVRSpatialAnchor, Guid>();
private static readonly List<(List<OVRSpaceUser>, List<OVRSpatialAnchor>)> ShareRequests =
new List<(List<OVRSpaceUser>, List<OVRSpatialAnchor>)>();
private static readonly Dictionary<ulong, MultiAnchorDelegatePair> MultiAnchorCompletionDelegates =
new Dictionary<ulong, MultiAnchorDelegatePair>();
private static readonly List<UnboundAnchor> UnboundAnchorBuffer = new List<UnboundAnchor>();
private static readonly OVRPlugin.SpaceComponentType[] ComponentTypeBuffer = new OVRPlugin.SpaceComponentType[32];
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void InitializeOnLoad()
{
CreationRequests.Clear();
MultiAnchorCompletionDelegates.Clear();
UnboundAnchorBuffer.Clear();
SpatialAnchors.Clear();
}
static OVRSpatialAnchor()
{
OVRManager.SpatialAnchorCreateComplete += OnSpatialAnchorCreateComplete;
OVRManager.SpaceSaveComplete += OnSpaceSaveComplete;
OVRManager.SpaceListSaveComplete += OnSpaceListSaveComplete;
OVRManager.ShareSpacesComplete += OnShareSpacesComplete;
OVRManager.SpaceEraseComplete += OnSpaceEraseComplete;
OVRManager.SpaceQueryComplete += OnSpaceQueryComplete;
OVRManager.SpaceSetComponentStatusComplete += OnSpaceSetComponentStatusComplete;
}
private static void InvokeMultiAnchorDelegate(ulong requestId, OperationResult result,
MultiAnchorActionType actionType)
{
if (!TryExtractValue(MultiAnchorCompletionDelegates, requestId, out var value))
{
return;
}
if (actionType == MultiAnchorActionType.Save)
{
OVRTelemetry.Client.MarkerEnd(OVRTelemetryConstants.Scene.MarkerId.SpatialAnchorSave,
result == OperationResult.Success ? OVRPlugin.Qpl.ResultType.Success : OVRPlugin.Qpl.ResultType.Fail,
requestId.GetHashCode());
}
value.Delegate?.Invoke(value.Anchors, result);
try
{
foreach (var anchor in value.Anchors)
{
switch (actionType)
{
case MultiAnchorActionType.Save:
{
if (result != OperationResult.Success)
{
Development.LogError(
$"[{anchor.Uuid}] {nameof(OVRPlugin)}.{nameof(OVRPlugin.SaveSpaceList)} failed with result: {result}.");
}
if (AsyncRequestTaskIds.TryGetValue(anchor, out var taskId))
{
AsyncRequestTaskIds.Remove(anchor);
OVRTask.GetExisting<bool>(taskId).SetResult(result == OperationResult.Success);
}
break;
}
case MultiAnchorActionType.Share:
{
if (result != OperationResult.Success)
{
Development.LogError(
$"[{anchor.Uuid}] {nameof(OVRPlugin)}.{nameof(OVRPlugin.ShareSpaces)} failed with result: {result}.");
}
if (AsyncRequestTaskIds.TryGetValue(anchor, out var taskId))
{
AsyncRequestTaskIds.Remove(anchor);
OVRTask.GetExisting<OperationResult>(taskId).SetResult(result);
}
break;
}
default:
throw new ArgumentOutOfRangeException(nameof(actionType), actionType, null);
}
}
}
finally
{
OVRObjectPool.Return(value.Anchors);
}
}
private static void OnSpatialAnchorCreateComplete(ulong requestId, bool success, OVRSpace space, Guid uuid)
{
Development.LogRequestResult(requestId, success,
$"[{uuid}] Spatial anchor created.",
$"Failed to create spatial anchor. Destroying {nameof(OVRSpatialAnchor)} component.");
if (!TryExtractValue(CreationRequests, requestId, out var anchor)) return;
OVRTelemetry.Client.MarkerEnd(OVRTelemetryConstants.Scene.MarkerId.SpatialAnchorCreate,
success ? OVRPlugin.Qpl.ResultType.Success : OVRPlugin.Qpl.ResultType.Fail,
requestId.GetHashCode());
if (success && anchor)
{
// All good; complete setup of OVRSpatialAnchor component.
anchor.InitializeUnchecked(space, uuid);
return;
}
if (success && !anchor)
{
// Creation succeeded, but the OVRSpatialAnchor component was destroyed before the callback completed.
OVRPlugin.DestroySpace(space);
}
else if (!success && anchor)
{
// The OVRSpatialAnchor component exists but creation failed.
Destroy(anchor);
}
// else if creation failed and the OVRSpatialAnchor component was destroyed, nothing to do.
}
private static void OnSpaceSaveComplete(ulong requestId, OVRSpace space, bool result, Guid uuid)
{
Development.LogRequestResult(requestId, result,
$"[{uuid}] Saved.",
$"[{uuid}] Save failed.");
}
private static void OnSpaceEraseComplete(ulong requestId, bool result, Guid uuid,
OVRPlugin.SpaceStorageLocation location)
{
Development.LogRequestResult(requestId, result,
$"[{uuid}] Erased.",
$"[{uuid}] Erase failed.");
}
/// <summary>
/// Options for loading unbound spatial anchors used by <see cref="OVRSpatialAnchor.LoadUnboundAnchors"/>.
/// </summary>
/// <example>
/// This example shows how to create LoadOptions for loading anchors when given a set of UUIDs.
/// <code>
/// OVRSpatialAnchor.LoadOptions options = new OVRSpatialAnchor.LoadOptions
/// {
/// StorageLocation = OVRSpace.StorageLocation.Local,
/// Timeout = 0,
/// Uuids = savedAnchorUuids
/// };
/// </code>
/// </example>
public struct LoadOptions
{
/// <summary>
/// The maximum number of uuids that may be present in the <see cref="Uuids"/> collection.
/// </summary>
public const int MaxSupported = OVRSpaceQuery.Options.MaxUuidCount;
/// <summary>
/// The storage location from which to query spatial anchors.
/// </summary>
public OVRSpace.StorageLocation StorageLocation { get; set; }
/// <summary>
/// (Obsolete) The maximum number of anchors to query.
/// </summary>
/// <remarks>
/// In prior SDK versions, it was mandatory to set this property to receive any
/// results. However, this property is now obsolete. If <see cref="MaxAnchorCount"/> is zero,
/// i.e., the default initialized value, it will automatically be set to the count of
/// <see cref="Uuids"/>.
///
/// If non-zero, the number of anchors in the result will be limited to
/// <see cref="MaxAnchorCount"/>, preserving the previous behavior.
/// </remarks>
[Obsolete(
"This property is no longer required. MaxAnchorCount will be automatically set to the number of uuids to load.")]
public int MaxAnchorCount { get; set; }
/// <summary>
/// The timeout, in seconds, for the query operation.
/// </summary>
/// <remarks>
/// A value of zero indicates no timeout.
/// </remarks>
public double Timeout { get; set; }
/// <summary>
/// The set of spatial anchors to query, identified by their UUIDs.
/// </summary>
/// <remarks>
/// The UUIDs are copied by the <see cref="OVRSpatialAnchor.LoadUnboundAnchors"/> method and no longer
/// referenced internally afterwards.
///
/// You must supply a list of UUIDs. <see cref="OVRSpatialAnchor.LoadUnboundAnchors"/> will throw if this
/// property is null.
/// </remarks>
/// <exception cref="System.ArgumentException">Thrown if <see cref="Uuids"/> contains more
/// than <see cref="MaxSupported"/> elements.</exception>
public IReadOnlyList<Guid> Uuids
{
get => _uuids;
set
{
if (value?.Count > OVRSpaceQuery.Options.MaxUuidCount)
throw new ArgumentException(
$"There must not be more than {MaxSupported} UUIDs (new value contains {value.Count} UUIDs).",
nameof(value));
_uuids = value;
}
}
private IReadOnlyList<Guid> _uuids;
internal OVRSpaceQuery.Options ToQueryOptions() => new OVRSpaceQuery.Options
{
Location = StorageLocation,
#pragma warning disable CS0618
MaxResults = MaxAnchorCount == 0 ? Uuids?.Count ?? 0 : MaxAnchorCount,
#pragma warning restore CS0618
Timeout = Timeout,
UuidFilter = Uuids,
QueryType = OVRPlugin.SpaceQueryType.Action,
ActionType = OVRPlugin.SpaceQueryActionType.Load,
};
}
/// <summary>
/// A spatial anchor that has not been bound to an <see cref="OVRSpatialAnchor"/>.
/// </summary>
/// <remarks>
/// Use this object to bind an unbound spatial anchor to an <see cref="OVRSpatialAnchor"/>.
/// </remarks>
public readonly struct UnboundAnchor
{
internal readonly OVRSpace _space;
/// <summary>
/// The universally unique identifier associated with this anchor.
/// </summary>
public Guid Uuid { get; }
/// <summary>
/// Whether the anchor has been localized.
/// </summary>
/// <remarks>
/// Prior to localization, the anchor's <see cref="Pose"/> cannot be determined.
/// </remarks>
/// <seealso cref="Localized"/>
/// <seealso cref="Localizing"/>
public bool Localized => OVRPlugin.GetSpaceComponentStatus(_space, OVRPlugin.SpaceComponentType.Locatable,
out var enabled, out _) && enabled;
/// <summary>
/// Whether the anchor is in the process of being localized.
/// </summary>
/// <seealso cref="Localized"/>
/// <seealso cref="Localize"/>
public bool Localizing => OVRPlugin.GetSpaceComponentStatus(_space, OVRPlugin.SpaceComponentType.Locatable,
out var enabled, out var pending) && !enabled && pending;
/// <summary>
/// The world space pose of the spatial anchor.
/// </summary>
public Pose Pose
{
get
{
if (!TryGetPose(_space, out var pose))
throw new InvalidOperationException(
$"[{Uuid}] Anchor must be localized before obtaining its pose.");
return new Pose(pose.position, pose.orientation);
}
}
/// <summary>
/// Localizes an anchor.
/// </summary>
/// <remarks>
/// The delegate supplied to <see cref="OVRSpatialAnchor.LoadUnboundAnchors"/> receives an array of unbound
/// spatial anchors. You can choose whether to localize each one and be notified when localization completes.
///
/// The <paramref name="onComplete"/> delegate receives two arguments:
/// - `bool`: Whether localization was successful
/// - <see cref="UnboundAnchor"/>: The anchor to bind
///
/// Upon successful localization, your delegate should instantiate an <see cref="OVRSpatialAnchor"/>, then bind
/// the <see cref="UnboundAnchor"/> to the <see cref="OVRSpatialAnchor"/> by calling
/// <see cref="UnboundAnchor.BindTo"/>. Once an <see cref="UnboundAnchor"/> is bound to an
/// <see cref="OVRSpatialAnchor"/>, it cannot be used again; that is, it cannot be bound to multiple
/// <see cref="OVRSpatialAnchor"/> components.
/// </remarks>
/// <param name="onComplete">A delegate invoked when localization completes (which may fail). The delegate
/// receives two arguments:
/// - <see cref="UnboundAnchor"/>: The anchor to bind
/// - `bool`: Whether localization was successful
/// </param>
/// <param name="timeout">The timeout, in seconds, to attempt localization, or zero to indicate no timeout.</param>
/// <exception cref="InvalidOperationException">Thrown if
/// - The anchor does not support localization, e.g., because it is invalid.
/// - The anchor has already been localized.
/// - The anchor is being localized, e.g., because <see cref="Localize"/> was previously called.
/// </exception>
public void Localize(Action<UnboundAnchor, bool> onComplete = null, double timeout = 0)
{
var task = LocalizeAsync(timeout);
if (onComplete != null)
{
InvertedCapture<bool, UnboundAnchor>.ContinueTaskWith(task, onComplete, this);
}
}
private void ValidateLocalization()
{
if (!OVRPlugin.GetSpaceComponentStatus(_space, OVRPlugin.SpaceComponentType.Locatable, out var enabled,
out var changePending))
throw new InvalidOperationException($"[{Uuid}] {nameof(UnboundAnchor)} does not support localization.");
if (enabled)
throw new InvalidOperationException($"[{Uuid}] Anchor has already been localized.");
if (changePending)
throw new InvalidOperationException($"[{Uuid}] Anchor is currently being localized.");
}
/// <summary>
/// Localizes an anchor.
/// </summary>
/// <remarks>
/// The delegate supplied to <see cref="OVRSpatialAnchor.LoadUnboundAnchors"/> receives an array of unbound
/// spatial anchors. You can choose whether to localize each one and be notified when localization completes.
///
/// Upon successful localization, your delegate should instantiate an <see cref="OVRSpatialAnchor"/>, then bind
/// the <see cref="UnboundAnchor"/> to the <see cref="OVRSpatialAnchor"/> by calling
/// <see cref="UnboundAnchor.BindTo"/>. Once an <see cref="UnboundAnchor"/> is bound to an
/// <see cref="OVRSpatialAnchor"/>, it cannot be used again; that is, it cannot be bound to multiple
/// <see cref="OVRSpatialAnchor"/> components.
/// </remarks>
/// <param name="timeout">The timeout, in seconds, to attempt localization, or zero to indicate no timeout.</param>
/// <returns>
/// An <see cref="OVRTask{TResult}"/> with a boolean type parameter indicating the success of the localization.
/// </returns>
/// <exception cref="InvalidOperationException">Thrown if
/// - The anchor does not support localization, e.g., because it is invalid.
/// - The anchor has already been localized.
/// - The anchor is being localized, e.g., because <see cref="Localize"/> was previously called.
/// </exception>
public OVRTask<bool> LocalizeAsync(double timeout = 0)
{
ValidateLocalization();
var setStatus = OVRPlugin.SetSpaceComponentStatus(_space, OVRPlugin.SpaceComponentType.Locatable, true,
timeout, out var requestId);
if (!setStatus)
{
Development.LogError($"[{Uuid}] {nameof(OVRPlugin.SetSpaceComponentStatus)} failed.");
return OVRTask.FromResult(false);
}
Development.LogRequest(requestId,
$"[{Uuid}] {nameof(OVRPlugin.SetSpaceComponentStatus)} enable {nameof(OVRPlugin.SpaceComponentType.Locatable)}.");
AddStorableAndShareableComponents();
return OVRTask.FromRequest<bool>(requestId);
}
private void AddStorableAndShareableComponents()
{
OVRPlugin.SetSpaceComponentStatus(_space, OVRPlugin.SpaceComponentType.Storable, true, 0, out _);
OVRPlugin.SetSpaceComponentStatus(_space, OVRPlugin.SpaceComponentType.Sharable, true, 0, out _);
}
/// <summary>
/// Binds an unbound anchor to an <see cref="OVRSpatialAnchor"/> component.
/// </summary>
/// <remarks>
/// Use this to bind an unbound anchor to an <see cref="OVRSpatialAnchor"/>. After <see cref="BindTo"/> is used
/// to bind an <see cref="UnboundAnchor"/> to an <see cref="OVRSpatialAnchor"/>, the
/// <see cref="UnboundAnchor"/> is no longer valid; that is, it cannot be bound to another
/// <see cref="OVRSpatialAnchor"/>.
/// </remarks>
/// <param name="spatialAnchor">The component to which this unbound anchor should be bound.</param>
/// <exception cref="InvalidOperationException">Thrown if this <see cref="UnboundAnchor"/> does not refer to a valid anchor.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="spatialAnchor"/> is `null`.</exception>
/// <exception cref="ArgumentException">Thrown if an anchor is already bound to <paramref name="spatialAnchor"/>.</exception>
/// <exception cref="ArgumentException">Thrown if <paramref name="spatialAnchor"/> is pending creation (see <see cref="OVRSpatialAnchor.PendingCreation"/>).</exception>
/// <exception cref="InvalidOperationException">Thrown if this <see cref="UnboundAnchor"/> is already bound to an <see cref="OVRSpatialAnchor"/>.</exception>
public void BindTo(OVRSpatialAnchor spatialAnchor)
{
if (!_space.Valid)
throw new InvalidOperationException($"{nameof(UnboundAnchor)} does not refer to a valid anchor.");
if (spatialAnchor == null)
throw new ArgumentNullException(nameof(spatialAnchor));
if (spatialAnchor.Created)
throw new ArgumentException(
$"Cannot bind {Uuid} to {nameof(spatialAnchor)} because {nameof(spatialAnchor)} is already bound to {spatialAnchor.Uuid}.",
nameof(spatialAnchor));
if (spatialAnchor.PendingCreation)
throw new ArgumentException(
$"Cannot bind {Uuid} to {nameof(spatialAnchor)} because {nameof(spatialAnchor)} is being used to create a new spatial anchor.",
nameof(spatialAnchor));
ThrowIfBound(Uuid);
spatialAnchor.InitializeUnchecked(_space, Uuid);
}
internal UnboundAnchor(OVRSpace space, Guid uuid)
{
_space = space;
Uuid = uuid;
}
}
/// <summary>
/// Performs a query for anchors with the specified <paramref name="options"/>.
/// </summary>
/// <remarks>
/// Use this method to find anchors that were previously persisted with
/// <see cref="Save(Action{OVRSpatialAnchor, bool}"/>. The query is asynchronous; when the query completes,
/// <paramref name="onComplete"/> is invoked with an array of <see cref="UnboundAnchor"/>s for which tracking
/// may be requested.
/// </remarks>
/// <param name="options">Options that affect the query.</param>
/// <param name="onComplete">A delegate invoked when the query completes. The delegate accepts one argument:
/// - `UnboundAnchor[]`: An array of unbound anchors.
///
/// If the operation fails, <paramref name="onComplete"/> is invoked with `null`.</param>
/// <returns>Returns `true` if the operation could be initiated; otherwise `false`.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="onComplete"/> is `null`.</exception>
/// <exception cref="InvalidOperationException">Thrown if <see cref="LoadOptions.Uuids"/> of <paramref name="options"/> is `null`.</exception>
public static bool LoadUnboundAnchors(LoadOptions options, Action<UnboundAnchor[]> onComplete)
{
var task = LoadUnboundAnchorsAsync(options);
task.ContinueWith(onComplete);
return task.IsPending;
}
/// <summary>
/// Performs a query for anchors with the specified <paramref name="options"/>.
/// </summary>
/// <remarks>
/// Use this method to find anchors that were previously persisted with
/// <see cref="Save(Action{OVRSpatialAnchor, bool}"/>. The query is asynchronous; when the query completes,
/// the returned <see cref="OVRTask{TResult}"/> will contain an array of <see cref="UnboundAnchor"/>s for which tracking
/// may be requested.
/// </remarks>
/// <param name="options">Options that affect the query.</param>
/// <returns>
/// An <see cref="OVRTask{TResult}"/> with a <see cref="T:UnboundAnchor[]"/> type parameter containing the loaded unbound anchors.
/// </returns>
/// <exception cref="InvalidOperationException">Thrown if <see cref="LoadOptions.Uuids"/> of <paramref name="options"/> is `null`.</exception>
public static OVRTask<UnboundAnchor[]> LoadUnboundAnchorsAsync(LoadOptions options)
{
if (options.Uuids == null)
{
throw new InvalidOperationException($"{nameof(LoadOptions)}.{nameof(LoadOptions.Uuids)} must not be null.");
}
if (!options.ToQueryOptions().TryQuerySpaces(out var requestId))
{
Development.LogError($"{nameof(OVRPlugin.QuerySpaces)} failed.");
return OVRTask.FromResult<UnboundAnchor[]>(null);
}
Development.LogRequest(requestId, $"{nameof(OVRPlugin.QuerySpaces)}: Query created.");
return OVRTask.FromRequest<UnboundAnchor[]>(requestId);
}
private static void OnSpaceQueryComplete(ulong requestId, bool queryResult)
{
Development.LogRequestResult(requestId, queryResult,
$"{nameof(OVRPlugin.QuerySpaces)}: Query succeeded.",
$"{nameof(OVRPlugin.QuerySpaces)}: Query failed.");
var hasPendingTask = OVRTask.GetExisting<UnboundAnchor[]>(requestId).IsPending;
if (!hasPendingTask)
{
return;
}
OVRTelemetry.Client.MarkerEnd(OVRTelemetryConstants.Scene.MarkerId.SpatialAnchorQuery,
queryResult ? OVRPlugin.Qpl.ResultType.Success : OVRPlugin.Qpl.ResultType.Fail,
requestId.GetHashCode());
if (!queryResult)
{
OVRTask.GetExisting<UnboundAnchor[]>(requestId).SetResult(null);
return;
}
if (OVRPlugin.RetrieveSpaceQueryResults(requestId, out var results, Allocator.Temp))
{
Development.Log(
$"{nameof(OVRPlugin.RetrieveSpaceQueryResults)}({requestId}): Retrieved {results.Length} results.");
}
else
{
Development.LogError(
$"{nameof(OVRPlugin.RetrieveSpaceQueryResults)}({requestId}): Failed to retrieve results.");
OVRTask.GetExisting<UnboundAnchor[]>(requestId).SetResult(null);
return;
}
using var disposer = results;
UnboundAnchorBuffer.Clear();
foreach (var result in results)
{
PopulateUnbound(result.uuid, result.space);
}
Development.Log(
$"Invoking callback with {UnboundAnchorBuffer.Count} unbound anchor{(UnboundAnchorBuffer.Count == 1 ? "" : "s")}.");
var unboundAnchors = UnboundAnchorBuffer.Count == 0
? Array.Empty<UnboundAnchor>()
: UnboundAnchorBuffer.ToArray();
OVRTask.GetExisting<UnboundAnchor[]>(requestId).SetResult(unboundAnchors);
}
private static void PopulateUnbound(Guid uuid, UInt64 space)
{
if (SpatialAnchors.ContainsKey(uuid))
{
Development.Log($"[{uuid}] Anchor is already bound to an {nameof(OVRSpatialAnchor)}. Ignoring.");
return;
}
// See if it supports localization
if (!OVRPlugin.EnumerateSpaceSupportedComponents(space, out var numSupportedComponents,
ComponentTypeBuffer))
{
Development.LogWarning($"[{uuid}] Unable to enumerate supported component types. Ignoring.");
return;
}
var supportsLocatable = false;
for (var i = 0; i < numSupportedComponents; i++)
{
supportsLocatable |= ComponentTypeBuffer[i] == OVRPlugin.SpaceComponentType.Locatable;
}
#if DEVELOPMENT_BUILD
var supportedComponentTypesMsg =
$"[{uuid}] Supports {numSupportedComponents} component type(s): {(numSupportedComponents == 0 ? "(none)" : string.Join(", ", ComponentTypeBuffer.Take((int)numSupportedComponents).Select(c => c.ToString())))}";
#endif
if (!supportsLocatable)
{
#if DEVELOPMENT_BUILD
Development.Log($"{supportedComponentTypesMsg} -- ignoring because it does not support localization.");
#endif
return;
}
if (!OVRPlugin.GetSpaceComponentStatus(space, OVRPlugin.SpaceComponentType.Locatable, out var enabled,
out var changePending))
{
#if DEVELOPMENT_BUILD
Development.Log($"{supportedComponentTypesMsg} -- ignoring because failed to get localization.");
#endif
return;
}
#if DEVELOPMENT_BUILD
Development.Log($"{supportedComponentTypesMsg}.");
#endif
Debug.Log($"{uuid}: locatable enabled? {enabled} changePending? {changePending}");
UnboundAnchorBuffer.Add(new UnboundAnchor(space, uuid));
}
private static void OnSpaceSetComponentStatusComplete(ulong requestId, bool result, OVRSpace space, Guid uuid,
OVRPlugin.SpaceComponentType componentType, bool enabled)
{
Development.LogRequestResult(requestId, result,
$"[{uuid}] {componentType} {(enabled ? "enabled" : "disabled")}.",
$"[{uuid}] Failed to set {componentType} status.");
if (componentType == OVRPlugin.SpaceComponentType.Locatable && SpatialAnchors.TryGetValue(uuid, out var anchor))
{
anchor.OnLocalize?.Invoke(enabled ? OperationResult.Success : OperationResult.Failure);
}
}
private enum MultiAnchorActionType
{
Save,
Share
}
private static void OnSpaceListSaveComplete(ulong requestId, OperationResult result)
{
Development.LogRequestResult(requestId, result >= 0,
$"Spaces saved.",
$"Spaces save failed with error {result}.");
InvokeMultiAnchorDelegate(requestId, result, MultiAnchorActionType.Save);
}
private static void OnShareSpacesComplete(ulong requestId, OperationResult result)
{
Development.LogRequestResult(requestId, result >= 0,
$"Spaces shared.",
$"Spaces share failed with error {result}.");
InvokeMultiAnchorDelegate(requestId, result, MultiAnchorActionType.Share);
}
private static class Development
{
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
public static void Log(string message) => Debug.Log($"[{nameof(OVRSpatialAnchor)}] {message}");
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
public static void LogWarning(string message) => Debug.LogWarning($"[{nameof(OVRSpatialAnchor)}] {message}");
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
public static void LogError(string message) => Debug.LogError($"[{nameof(OVRSpatialAnchor)}] {message}");
#if DEVELOPMENT_BUILD
private static readonly HashSet<ulong> _requests = new HashSet<ulong>();
#endif // DEVELOPMENT_BUILD
[Conditional("DEVELOPMENT_BUILD")]
public static void LogRequest(ulong requestId, string message)
{
#if DEVELOPMENT_BUILD
_requests.Add(requestId);
#endif // DEVELOPMENT_BUILD
Log($"({requestId}) {message}");
}
[Conditional("DEVELOPMENT_BUILD")]
public static void LogRequestResult(ulong requestId, bool result, string successMessage, string failureMessage)
{
#if DEVELOPMENT_BUILD
// Not a request we're tracking
if (!_requests.Remove(requestId)) return;
#endif // DEVELOPMENT_BUILD
if (result)
{
Log($"({requestId}) {successMessage}");
}
else
{
LogError($"({requestId}) {failureMessage}");
}
}
}
/// <summary>
/// Represents options for saving <see cref="OVRSpatialAnchor"/>.
/// </summary>
public struct SaveOptions
{
/// <summary>
/// Location where <see cref="OVRSpatialAnchor"/> will be saved.
/// </summary>
public OVRSpace.StorageLocation Storage;
}
/// <summary>
/// Represents options for erasing <see cref="OVRSpatialAnchor"/>.
/// </summary>
public struct EraseOptions
{
/// <summary>
/// Location from where <see cref="OVRSpatialAnchor"/> will be erased.
/// </summary>
public OVRSpace.StorageLocation Storage;
}
public enum OperationResult
{
/// <summary>Operation succeeded.</summary>
Success = 0,
/// <summary>Operation failed.</summary>
Failure = -1000,
/// <summary>Saving anchors to cloud storage is not permitted by the user.</summary>
Failure_SpaceCloudStorageDisabled = -2000,
/// <summary>
/// The user was able to download the anchors, but the device was unable to localize
/// itself in the spatial data received from the sharing device.
/// </summary>
Failure_SpaceMappingInsufficient = -2001,
/// <summary>
/// The user was able to download the anchors, but the device was unable to localize them.
/// </summary>
Failure_SpaceLocalizationFailed = -2002,
/// <summary>Network operation timed out.</summary>
Failure_SpaceNetworkTimeout = -2003,
/// <summary>Network operation failed.</summary>
Failure_SpaceNetworkRequestFailed = -2004,
}
/// <summary>
/// This struct helped inverting callback signature
/// when using OVRTasks. OVRTasks expect <c>Action{TResult, TCapture}</c> signature
/// but public API requires <c>Action{TCapture, TResult}</c> signature.
/// </summary>
private readonly struct InvertedCapture<TResult, TCapture>
{
private static readonly Action<TResult, InvertedCapture<TResult, TCapture>> Delegate = Invoke;
private readonly TCapture _capture;
private readonly Action<TCapture, TResult> _callback;
private InvertedCapture(Action<TCapture, TResult> callback, TCapture capture)
{
_callback = callback;
_capture = capture;
}
private static void Invoke(TResult result, InvertedCapture<TResult, TCapture> invertedCapture)
{
invertedCapture._callback?.Invoke(invertedCapture._capture, result);
}
public static void ContinueTaskWith(OVRTask<TResult> task, Action<TCapture, TResult> onCompleted,
TCapture state)
{
task.ContinueWith(Delegate, new InvertedCapture<TResult, TCapture>(onCompleted, state));
}
}
}
public static class OperationResultExtensions
{
public static bool IsSuccess(this OVRSpatialAnchor.OperationResult res) => res == OVRSpatialAnchor.OperationResult.Success;
public static bool IsError(this OVRSpatialAnchor.OperationResult res) => res < 0;
public static bool IsWarning(this OVRSpatialAnchor.OperationResult res) => res > 0;
}
/// <summary>
/// Represents a user for purposes of sharing scene anchors
/// </summary>
public struct OVRSpaceUser : IDisposable
{
internal ulong _handle;
/// <summary>
/// Checks if the user is valid
/// </summary>
public bool Valid => _handle != 0 && Id != 0;
/// <summary>
/// Creates a space user handle for given Facebook user ID
/// </summary>
/// <param name="spaceUserId">The Facebook user ID obtained from the other party over the network</param>
public OVRSpaceUser(ulong spaceUserId)
{
OVRPlugin.CreateSpaceUser(spaceUserId, out _handle);
}
/// <summary>
/// The user ID associated with this <see cref="OVRSpaceUser"/>.
/// </summary>
public ulong Id => OVRPlugin.GetSpaceUserId(_handle, out var userId) ? userId : 0;
/// <summary>
/// Disposes of the <see cref="OVRSpaceUser"/>.
/// </summary>
/// <remarks>
/// This method does not destroy the user account. It disposes the handle used to reference it.
/// </remarks>
public void Dispose()
{
OVRPlugin.DestroySpaceUser(_handle);
_handle = 0;
}
}