869 lines
32 KiB
C#
869 lines
32 KiB
C#
|
|
/*===============================================================================
|
||
|
|
Copyright (C) 2024 Immersal - Part of Hexagon. All Rights Reserved.
|
||
|
|
|
||
|
|
This file is part of the Immersal SDK.
|
||
|
|
|
||
|
|
The Immersal SDK cannot be copied, distributed, or made available to
|
||
|
|
third-parties for commercial purposes without written permission of Immersal Ltd.
|
||
|
|
|
||
|
|
Contact sales@immersal.com for licensing requests.
|
||
|
|
===============================================================================*/
|
||
|
|
|
||
|
|
using System;
|
||
|
|
using System.Collections;
|
||
|
|
using System.Collections.Generic;
|
||
|
|
using System.IO;
|
||
|
|
using System.Linq;
|
||
|
|
using System.Threading.Tasks;
|
||
|
|
using Immersal.REST;
|
||
|
|
using Unity.XR.CoreUtils;
|
||
|
|
using UnityEngine;
|
||
|
|
using UnityEngine.Events;
|
||
|
|
using UnityEngine.Networking;
|
||
|
|
using UnityEngine.SceneManagement;
|
||
|
|
|
||
|
|
#if UNITY_EDITOR
|
||
|
|
using UnityEditor;
|
||
|
|
using UnityEditor.SceneManagement;
|
||
|
|
#endif
|
||
|
|
|
||
|
|
namespace Immersal.XR
|
||
|
|
{
|
||
|
|
public static class MapManager
|
||
|
|
{
|
||
|
|
public static UnityEvent<int> MapRegisteredAndLoaded;
|
||
|
|
|
||
|
|
private static Dictionary<int, MapEntry> m_MapEntries = new Dictionary<int, MapEntry>();
|
||
|
|
|
||
|
|
public static bool HasRegisteredMaps { get; private set; }
|
||
|
|
|
||
|
|
public static async Task RegisterAndLoadMap(XRMap map, ISceneUpdateable sceneParent)
|
||
|
|
{
|
||
|
|
if (!map.IsConfigured)
|
||
|
|
{
|
||
|
|
ImmersalLogger.LogError($"Trying to register and load unconfigured map: {map.name}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (map.LocalizationMethod == null)
|
||
|
|
{
|
||
|
|
ImmersalLogger.LogError($"Map {map.mapName} has null localization method.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!TryGetMapEntry(map.mapId, out MapEntry entry))
|
||
|
|
{
|
||
|
|
RegisterMap(map, sceneParent);
|
||
|
|
}
|
||
|
|
|
||
|
|
await map.LocalizationMethod.OnMapRegistered(map);
|
||
|
|
|
||
|
|
bool loadSuccess = await LoadMap(map);
|
||
|
|
|
||
|
|
if (loadSuccess)
|
||
|
|
{
|
||
|
|
MapRegisteredAndLoaded?.Invoke(map.mapId);
|
||
|
|
}
|
||
|
|
|
||
|
|
UpdateHasRegisteredMaps();
|
||
|
|
ImmersalLogger.Log($"RegisterAndLoad for map {map.mapId} complete.");
|
||
|
|
}
|
||
|
|
|
||
|
|
// The LoadMap method checks if the map has a MapLoadingOption and loads the map
|
||
|
|
// into the Immersal plugin based on the configuration.
|
||
|
|
// Note: not all maps require loading (ServerLocalization for example)
|
||
|
|
public static async Task<bool> LoadMap(XRMap map)
|
||
|
|
{
|
||
|
|
MapLoadingOption mlo = map.MapOptions.FirstOrDefault(option => option.Name == "MapLoading") as MapLoadingOption;
|
||
|
|
if (mlo == null)
|
||
|
|
{
|
||
|
|
ImmersalLogger.Log($"No MapLoadingOption on map {map.mapId}, skipping loading.");
|
||
|
|
// No MapLoadingOption -> assume map does not need to be loaded
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
ImmersalLogger.Log($"Loading map {map.mapId} according to MapLoadingOption.");
|
||
|
|
|
||
|
|
// Download visualization?
|
||
|
|
if (mlo.DownloadVisualizationAtRuntime)
|
||
|
|
{
|
||
|
|
ImmersalLogger.Log($"Downloading visualization for map {map.mapId}");
|
||
|
|
JobLoadMapSparseAsync j = new JobLoadMapSparseAsync();
|
||
|
|
j.id = map.mapId;
|
||
|
|
SDKSparseDownloadResult plyResult = await j.RunJobAsync();
|
||
|
|
ImmersalLogger.Log($"Adding visualization for map {map.mapId}");
|
||
|
|
map.CreateVisualization(XRMapVisualization.RenderMode.EditorAndRuntime, true);
|
||
|
|
map.Visualization.LoadPly(plyResult.data, map.mapName);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Only report failure if loading is attempted and failed
|
||
|
|
bool loadSuccess = true;
|
||
|
|
|
||
|
|
// Load raw bytes. This option is not exposed in the inspector, only for runtime configs.
|
||
|
|
if (mlo.Bytes is { Length: > 0 })
|
||
|
|
{
|
||
|
|
ImmersalLogger.Log($"Loading provided bytes for: {map.mapId}");
|
||
|
|
loadSuccess = await TryToLoadMap(mlo.Bytes, map.mapId);
|
||
|
|
}
|
||
|
|
// Download map data
|
||
|
|
else if (mlo.m_SerializedDataSource == (int)MapDataSource.Download)
|
||
|
|
{
|
||
|
|
ImmersalLogger.Log($"Map {map.mapId} configured to download data.");
|
||
|
|
|
||
|
|
ImmersalLogger.Log($"Downloading mapfile for map {map.mapId}");
|
||
|
|
JobLoadMapBinaryAsync j = new JobLoadMapBinaryAsync();
|
||
|
|
j.id = map.mapId;
|
||
|
|
|
||
|
|
SDKMapResult result = await j.RunJobAsync();
|
||
|
|
ImmersalLogger.Log($"Loading downloaded mapfile for map {map.mapId}");
|
||
|
|
loadSuccess = await TryToLoadMap(result.mapData, map.mapId);
|
||
|
|
}
|
||
|
|
// Use embedded mapfile
|
||
|
|
else if (mlo.m_SerializedDataSource == (int)MapDataSource.Embed)
|
||
|
|
{
|
||
|
|
if (map.mapFile == null)
|
||
|
|
{
|
||
|
|
ImmersalLogger.LogError($"Missing map file for: {map.mapId}.");
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
ImmersalLogger.Log($"Loading embedded mapfile for: {map.mapId}");
|
||
|
|
byte[] mapBytes = map.mapFile.bytes;
|
||
|
|
loadSuccess = await TryToLoadMap(mapBytes, map.mapId);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
ImmersalLogger.Log("MapLoadingOption not configured to load any map data.");
|
||
|
|
}
|
||
|
|
|
||
|
|
return loadSuccess;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static async Task<bool> TryToLoadMap(byte[] mapBytes, int mapId)
|
||
|
|
{
|
||
|
|
// Check if map is registered
|
||
|
|
if (TryGetMapEntry(mapId, out MapEntry entry))
|
||
|
|
{
|
||
|
|
if (mapBytes != null)
|
||
|
|
{
|
||
|
|
Task<int> t = Task.Run(() => { return Immersal.Core.LoadMap(mapId, mapBytes); });
|
||
|
|
await t;
|
||
|
|
return t.Result != -1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
ImmersalLogger.LogError($"Trying to load unregistered map ID: {mapId}");
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static async Task<MapCreationResult> TryCreateMap(MapCreationParameters parameters, bool unconfigured = false)
|
||
|
|
{
|
||
|
|
// Default to failure
|
||
|
|
MapCreationResult result = new MapCreationResult { Success = false };
|
||
|
|
|
||
|
|
// GameObject
|
||
|
|
parameters.Name = string.IsNullOrEmpty(parameters.Name) ? "Unnamed map" : parameters.Name;
|
||
|
|
GameObject go = new GameObject(parameters.Name);
|
||
|
|
|
||
|
|
ImmersalLogger.Log($"Creating a new map: '{parameters.Name}'");
|
||
|
|
|
||
|
|
// XRMap component
|
||
|
|
XRMap map = go.AddComponent<XRMap>();
|
||
|
|
result.Map = map;
|
||
|
|
|
||
|
|
// SceneParent
|
||
|
|
if (parameters.SceneParent == null)
|
||
|
|
{
|
||
|
|
GameObject newParent = new GameObject("New XR Space");
|
||
|
|
parameters.SceneParent = newParent.AddComponent<XRSpace>();
|
||
|
|
}
|
||
|
|
|
||
|
|
go.transform.SetParent(parameters.SceneParent.GetTransform());
|
||
|
|
result.SceneParent = parameters.SceneParent;
|
||
|
|
|
||
|
|
// Optionally return here and leave out configuration, registering and loading
|
||
|
|
if (unconfigured)
|
||
|
|
{
|
||
|
|
result.Success = true;
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Configure
|
||
|
|
if (parameters.MapId != null)
|
||
|
|
{
|
||
|
|
map.SetIdAndName(parameters.MapId.Value, parameters.Name, true);
|
||
|
|
map.Configure();
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
if (parameters.MetadataGetResult == null)
|
||
|
|
return result; // fail
|
||
|
|
map.Configure(parameters.MetadataGetResult.Value); // applies metadata
|
||
|
|
}
|
||
|
|
|
||
|
|
// Alignment
|
||
|
|
// Note: assumes alignment is already defined with metadata/custom data
|
||
|
|
if (parameters.ApplyMapAlignment)
|
||
|
|
{
|
||
|
|
map.ApplyAlignment();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Localization method
|
||
|
|
|
||
|
|
// If null, try to find based on Type
|
||
|
|
if (parameters.LocalizationMethod == null)
|
||
|
|
{
|
||
|
|
ILocalizationMethod[] availableMethods = AvailableLocalizationMethods;
|
||
|
|
ILocalizationMethod method =
|
||
|
|
availableMethods?.FirstOrDefault(m => m.GetType() == parameters.LocalizationMethodType);
|
||
|
|
if (method == null)
|
||
|
|
{
|
||
|
|
ImmersalLogger.LogError("Requested LocalizationMethod is not available");
|
||
|
|
return result; // fail
|
||
|
|
}
|
||
|
|
|
||
|
|
parameters.LocalizationMethod = method;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Try to configure the LocalizationMethod
|
||
|
|
DefaultLocalizerConfiguration config = new DefaultLocalizerConfiguration
|
||
|
|
{
|
||
|
|
ConfigurationsToAdd = new Dictionary<ILocalizationMethod, XRMap[]>
|
||
|
|
{
|
||
|
|
{ parameters.LocalizationMethod, new XRMap[] { map } }
|
||
|
|
}
|
||
|
|
};
|
||
|
|
ILocalizerConfigurationResult r = await ImmersalSDK.Instance.Localizer.ConfigureLocalizer(config);
|
||
|
|
|
||
|
|
//if (!await parameters.LocalizationMethod.Configure(new XRMap[] { map }))
|
||
|
|
if (!r.Success)
|
||
|
|
{
|
||
|
|
ImmersalLogger.LogError("Could not configure requested LocalizationMethod");
|
||
|
|
return result; // fail
|
||
|
|
}
|
||
|
|
|
||
|
|
map.LocalizationMethod = parameters.LocalizationMethod;
|
||
|
|
|
||
|
|
// Map options
|
||
|
|
if (parameters.MapOptions != null)
|
||
|
|
{
|
||
|
|
map.MapOptions = parameters.MapOptions.ToList();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Register and load
|
||
|
|
await RegisterAndLoadMap(map, parameters.SceneParent);
|
||
|
|
|
||
|
|
result.Success = true;
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static List<XRMap> GetRegisteredMaps()
|
||
|
|
{
|
||
|
|
List<XRMap> maps = new List<XRMap>();
|
||
|
|
foreach (KeyValuePair<int,MapEntry> keyValuePair in m_MapEntries)
|
||
|
|
{
|
||
|
|
maps.Add(keyValuePair.Value.Map);
|
||
|
|
}
|
||
|
|
|
||
|
|
return maps;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static int GetRegisteredMapCount()
|
||
|
|
{
|
||
|
|
return m_MapEntries.Count;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static List<ISceneUpdateable> GetSceneUpdateablesInUse()
|
||
|
|
{
|
||
|
|
List<ISceneUpdateable> updateables = new List<ISceneUpdateable>();
|
||
|
|
foreach (KeyValuePair<int,MapEntry> keyValuePair in m_MapEntries)
|
||
|
|
{
|
||
|
|
ISceneUpdateable sceneUpdateable = keyValuePair.Value.SceneParent;
|
||
|
|
if (!updateables.Contains(sceneUpdateable ))
|
||
|
|
updateables.Add(sceneUpdateable);
|
||
|
|
}
|
||
|
|
|
||
|
|
return updateables;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void UpdateHasRegisteredMaps()
|
||
|
|
{
|
||
|
|
HasRegisteredMaps = m_MapEntries.Count > 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
#region MapEntries
|
||
|
|
|
||
|
|
public static void RegisterMap(XRMap map, ISceneUpdateable sceneParent)
|
||
|
|
{
|
||
|
|
ImmersalLogger.Log($"Registering map: {map.mapId}");
|
||
|
|
|
||
|
|
if (m_MapEntries.ContainsKey(map.mapId))
|
||
|
|
{
|
||
|
|
ImmersalLogger.LogWarning("Map is already registered, aborting.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
Transform tr = map.transform;
|
||
|
|
MapToSpaceRelation mtsr = new MapToSpaceRelation
|
||
|
|
{
|
||
|
|
Position = tr.localPosition,
|
||
|
|
Rotation = tr.localRotation,
|
||
|
|
Scale = tr.localScale
|
||
|
|
};
|
||
|
|
|
||
|
|
MapEntry me = new MapEntry
|
||
|
|
{
|
||
|
|
Map = map,
|
||
|
|
SceneParent = sceneParent,
|
||
|
|
Relation = mtsr
|
||
|
|
};
|
||
|
|
|
||
|
|
m_MapEntries.Add(map.mapId, me);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static async void RemoveMap(int mapId, bool removeFromLocalizer = true, bool destroyObjects = false)
|
||
|
|
{
|
||
|
|
if (TryGetMapEntry(mapId, out MapEntry entry))
|
||
|
|
{
|
||
|
|
// Update localizer configuration
|
||
|
|
if (removeFromLocalizer)
|
||
|
|
{
|
||
|
|
ILocalizationMethod method = entry.Map.LocalizationMethod;
|
||
|
|
DefaultLocalizerConfiguration config = new DefaultLocalizerConfiguration
|
||
|
|
{
|
||
|
|
ConfigurationsToRemove = new Dictionary<ILocalizationMethod, XRMap[]>
|
||
|
|
{
|
||
|
|
{ method, new XRMap[] { entry.Map } }
|
||
|
|
}
|
||
|
|
};
|
||
|
|
await ImmersalSDK.Instance.Localizer.ConfigureLocalizer(config);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Free & remove mapping (if it's loaded)
|
||
|
|
Immersal.Core.FreeMap(mapId);
|
||
|
|
|
||
|
|
// Destroy
|
||
|
|
if (destroyObjects)
|
||
|
|
{
|
||
|
|
XRMap map = entry.Map;
|
||
|
|
|
||
|
|
if (map.Visualization != null)
|
||
|
|
map.RemoveVisualization();
|
||
|
|
|
||
|
|
if (map.gameObject)
|
||
|
|
GameObject.Destroy(map.gameObject);
|
||
|
|
|
||
|
|
// Check if parent only had this one child
|
||
|
|
// Note: the XRMap destruction above happens later when Unity wants it to
|
||
|
|
Transform parent = entry.SceneParent.GetTransform();
|
||
|
|
if (parent != null && parent.childCount == 1)
|
||
|
|
{
|
||
|
|
GameObject.Destroy(parent.gameObject);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Remove entry
|
||
|
|
m_MapEntries.Remove(mapId);
|
||
|
|
|
||
|
|
UpdateHasRegisteredMaps();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public static void RemoveAllMaps(bool removeFromLocalizer = true, bool destroyObjects = false)
|
||
|
|
{
|
||
|
|
if (removeFromLocalizer)
|
||
|
|
{
|
||
|
|
// Remove all from localizer configuration with one call
|
||
|
|
|
||
|
|
Dictionary<ILocalizationMethod, XRMap[]> allMaps = m_MapEntries.Values
|
||
|
|
.Select(entry => entry.Map)
|
||
|
|
.GroupBy(map => map.LocalizationMethod)
|
||
|
|
.ToDictionary(
|
||
|
|
group => group.Key,
|
||
|
|
group => group.ToArray());
|
||
|
|
|
||
|
|
DefaultLocalizerConfiguration config = new DefaultLocalizerConfiguration
|
||
|
|
{
|
||
|
|
ConfigurationsToRemove = allMaps
|
||
|
|
};
|
||
|
|
|
||
|
|
ImmersalSDK.Instance.Localizer.ConfigureLocalizer(config);
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (int id in m_MapEntries.Keys.ToList())
|
||
|
|
{
|
||
|
|
// removeFromLocalizer set to false since we already removed all
|
||
|
|
RemoveMap(id, false, destroyObjects);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public static bool TryGetMapEntry(int mapId, out MapEntry mapEntry)
|
||
|
|
{
|
||
|
|
return m_MapEntries.TryGetValue(mapId, out mapEntry);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static bool HasMapEntry(int mapId)
|
||
|
|
{
|
||
|
|
return m_MapEntries.ContainsKey(mapId);
|
||
|
|
}
|
||
|
|
|
||
|
|
#endregion
|
||
|
|
|
||
|
|
#region Localization methods and map options
|
||
|
|
|
||
|
|
/*
|
||
|
|
* This section takes care of managing a collection of available localization methods.
|
||
|
|
*/
|
||
|
|
|
||
|
|
private static ILocalizationMethod[] m_CachedLocalizationMethods;
|
||
|
|
|
||
|
|
public static ILocalizationMethod[] AvailableLocalizationMethods
|
||
|
|
{
|
||
|
|
get
|
||
|
|
{
|
||
|
|
if (m_CachedLocalizationMethods == null)
|
||
|
|
{
|
||
|
|
RefreshLocalizationMethods();
|
||
|
|
}
|
||
|
|
return m_CachedLocalizationMethods;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// To ensure our cached collections are up to date and pointing to the correct instances,
|
||
|
|
// we need to react to both assembly reloads and scene changes.
|
||
|
|
|
||
|
|
#if UNITY_EDITOR
|
||
|
|
static MapManager()
|
||
|
|
{
|
||
|
|
EditorSceneManager.sceneOpened += OnSceneOpened;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void OnSceneOpened(Scene scene, OpenSceneMode mode)
|
||
|
|
{
|
||
|
|
InvalidateCache();
|
||
|
|
}
|
||
|
|
|
||
|
|
[InitializeOnLoadMethod]
|
||
|
|
public static void ReactToAssemblyReload()
|
||
|
|
{
|
||
|
|
InvalidateCache();
|
||
|
|
}
|
||
|
|
#endif
|
||
|
|
|
||
|
|
private static void InvalidateCache()
|
||
|
|
{
|
||
|
|
m_CachedLocalizationMethods = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static void RefreshLocalizationMethods()
|
||
|
|
{
|
||
|
|
if (ImmersalSDK.Instance == null || ImmersalSDK.Instance.Localizer == null)
|
||
|
|
{
|
||
|
|
ImmersalLogger.LogError("Missing ImmersalSDK or Localizer, cant fetch localization methods.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
ILocalizationMethod[] methods = ImmersalSDK.Instance.Localizer.AvailableLocalizationMethods;
|
||
|
|
|
||
|
|
if (methods == null)
|
||
|
|
{
|
||
|
|
ImmersalLogger.LogError("No localization methods defined in Localizer");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for duplicates
|
||
|
|
HashSet<ILocalizationMethod> uniqueMethods = new HashSet<ILocalizationMethod>();
|
||
|
|
foreach (ILocalizationMethod method in methods)
|
||
|
|
{
|
||
|
|
if (!uniqueMethods.Add(method))
|
||
|
|
{
|
||
|
|
// Duplicate found
|
||
|
|
ImmersalLogger.LogWarning("Localizer has duplicate Localization Method references.");
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
m_CachedLocalizationMethods = uniqueMethods.ToArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
public static bool TryGetMapOptions(ILocalizationMethod localizationMethod, out List<IMapOption> options)
|
||
|
|
{
|
||
|
|
options = new List<IMapOption>();
|
||
|
|
|
||
|
|
if (localizationMethod.IsNullOrDead())
|
||
|
|
{
|
||
|
|
ImmersalLogger.LogError("Trying to fetch map options for null localization method.");
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
IMapOption[] mapOptionsFromMethod = localizationMethod.MapOptions;
|
||
|
|
if (mapOptionsFromMethod == null)
|
||
|
|
return false;
|
||
|
|
|
||
|
|
options = mapOptionsFromMethod.ToList();
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
#endregion
|
||
|
|
|
||
|
|
#region Downloading coroutines for edit time use
|
||
|
|
#if UNITY_EDITOR
|
||
|
|
public static string GetDirectoryPath(string inputPath = "", bool assetsRoot = false)
|
||
|
|
{
|
||
|
|
string result = "";
|
||
|
|
string root = assetsRoot ? "Assets/" : Application.dataPath;
|
||
|
|
string defaultPath = Path.Combine(root, ImmersalSDK.Instance.DownloadDirectory);
|
||
|
|
result = inputPath != "" ? Path.Combine(root, inputPath) : defaultPath;
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static bool CheckDirectory(string path)
|
||
|
|
{
|
||
|
|
if (!Directory.Exists(path))
|
||
|
|
{
|
||
|
|
ImmersalLogger.LogWarning("Requested directory does not exist, creating it now.");
|
||
|
|
Directory.CreateDirectory(path);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (string.IsNullOrEmpty(path))
|
||
|
|
{
|
||
|
|
ImmersalLogger.LogError("Requested path is null or empty.");
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
string fullPath = Path.GetFullPath(path);
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
ImmersalLogger.LogError($"Requested path is invalid: {path}");
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
public static IEnumerator DownloadMapMetadata(int mapId, Action<SDKMapMetadataGetResult> resultCallback, string jsonWritePath = "")
|
||
|
|
{
|
||
|
|
string targetFullPath = GetDirectoryPath(jsonWritePath);
|
||
|
|
if (!CheckDirectory(targetFullPath))
|
||
|
|
yield break;
|
||
|
|
|
||
|
|
// Load map metadata from Immersal Cloud Service
|
||
|
|
SDKMapMetadataGetRequest r = new SDKMapMetadataGetRequest();
|
||
|
|
r.token = ImmersalSDK.Instance.developerToken;
|
||
|
|
r.id = mapId;
|
||
|
|
|
||
|
|
if (r.token == "")
|
||
|
|
ImmersalLogger.LogWarning("Trying to download map data without developer token.");
|
||
|
|
|
||
|
|
string jsonString = JsonUtility.ToJson(r);
|
||
|
|
UnityWebRequest request =
|
||
|
|
UnityWebRequest.Put(
|
||
|
|
string.Format(ImmersalHttp.URL_FORMAT, ImmersalSDK.Instance.localizationServer,
|
||
|
|
SDKMapMetadataGetRequest.endpoint), jsonString);
|
||
|
|
request.method = UnityWebRequest.kHttpVerbPOST;
|
||
|
|
request.useHttpContinue = false;
|
||
|
|
request.SetRequestHeader("Content-Type", "application/json");
|
||
|
|
request.SetRequestHeader("Accept", "application/json");
|
||
|
|
request.SendWebRequest();
|
||
|
|
|
||
|
|
while (!request.isDone)
|
||
|
|
{
|
||
|
|
yield return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
#if UNITY_2020_1_OR_NEWER
|
||
|
|
if (request.result != UnityWebRequest.Result.Success)
|
||
|
|
#else
|
||
|
|
if (request.isNetworkError || request.isHttpError)
|
||
|
|
#endif
|
||
|
|
{
|
||
|
|
ImmersalLogger.LogError(request.error);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
SDKMapMetadataGetResult result =
|
||
|
|
JsonUtility.FromJson<SDKMapMetadataGetResult>(request.downloadHandler.text);
|
||
|
|
if (result.error == "none")
|
||
|
|
{
|
||
|
|
resultCallback(result);
|
||
|
|
|
||
|
|
string fileName = $"{result.id}-{result.name}-metadata.json";
|
||
|
|
string jsonFilePath = Path.Combine(targetFullPath, fileName);
|
||
|
|
WriteJson(jsonFilePath, request.downloadHandler.text);
|
||
|
|
string assetPath = Path.Combine(GetDirectoryPath(jsonWritePath, true), fileName);
|
||
|
|
AssetDatabase.Refresh();
|
||
|
|
AssetDatabase.ImportAsset(assetPath);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public static IEnumerator DownloadMapFile(int mapId, string mapName, Action<SDKMapDownloadResult, TextAsset> resultCallback, string bytesWritePath = "")
|
||
|
|
{
|
||
|
|
string targetFullPath = GetDirectoryPath(bytesWritePath);
|
||
|
|
string targetAssetPath = GetDirectoryPath(bytesWritePath, true);
|
||
|
|
if (!CheckDirectory(targetFullPath))
|
||
|
|
yield break;
|
||
|
|
|
||
|
|
// Load map file from Immersal Cloud Service
|
||
|
|
SDKMapDownloadRequest r = new SDKMapDownloadRequest();
|
||
|
|
r.token = ImmersalSDK.Instance.developerToken;
|
||
|
|
r.id = mapId;
|
||
|
|
|
||
|
|
if (r.token == "")
|
||
|
|
ImmersalLogger.LogWarning("Trying to download map data without developer token.");
|
||
|
|
|
||
|
|
string jsonString = JsonUtility.ToJson(r);
|
||
|
|
UnityWebRequest request = UnityWebRequest.Put(string.Format(ImmersalHttp.URL_FORMAT, ImmersalSDK.Instance.localizationServer, SDKMapDownloadRequest.endpoint), jsonString);
|
||
|
|
request.method = UnityWebRequest.kHttpVerbPOST;
|
||
|
|
request.useHttpContinue = false;
|
||
|
|
request.SetRequestHeader("Content-Type", "application/json");
|
||
|
|
request.SetRequestHeader("Accept", "application/json");
|
||
|
|
request.SendWebRequest();
|
||
|
|
|
||
|
|
while (!request.isDone)
|
||
|
|
{
|
||
|
|
yield return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
#if UNITY_2020_1_OR_NEWER
|
||
|
|
if (request.result != UnityWebRequest.Result.Success)
|
||
|
|
#else
|
||
|
|
if (request.isNetworkError || request.isHttpError)
|
||
|
|
#endif
|
||
|
|
{
|
||
|
|
ImmersalLogger.LogError(request.error);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
SDKMapDownloadResult mapDataResult = JsonUtility.FromJson<SDKMapDownloadResult>(request.downloadHandler.text);
|
||
|
|
if (mapDataResult.error == "none")
|
||
|
|
{
|
||
|
|
// Save map file on disk, overwrite existing file
|
||
|
|
string fileName = $"{mapId}-{mapName}.bytes";
|
||
|
|
string mapFileFullPath = Path.Combine(targetFullPath, fileName);
|
||
|
|
string mapFileAssetPath = Path.Combine(GetDirectoryPath(bytesWritePath, true), fileName);
|
||
|
|
WriteBytes(mapFileFullPath, mapDataResult.b64);
|
||
|
|
AssetDatabase.Refresh();
|
||
|
|
AssetDatabase.ImportAsset(mapFileAssetPath);
|
||
|
|
TextAsset mapFile =
|
||
|
|
(TextAsset)AssetDatabase.LoadAssetAtPath(mapFileAssetPath, typeof(TextAsset));
|
||
|
|
resultCallback(mapDataResult, mapFile);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public static IEnumerator DownloadSparseFile(int mapId, string mapName, Action<SDKSparseDownloadResult, string> resultCallback, string plyWritePath = "")
|
||
|
|
{
|
||
|
|
string targetFullPath = GetDirectoryPath(plyWritePath);
|
||
|
|
if (!CheckDirectory(targetFullPath))
|
||
|
|
yield break;
|
||
|
|
|
||
|
|
string uri =
|
||
|
|
$"{ImmersalSDK.Instance.localizationServer}/{SDKSparseDownloadRequest.endpoint}?token={ImmersalSDK.Instance.developerToken}&id={mapId}";
|
||
|
|
|
||
|
|
if (ImmersalSDK.Instance.developerToken == "")
|
||
|
|
ImmersalLogger.LogWarning("Trying to download map data without developer token.");
|
||
|
|
|
||
|
|
using (UnityWebRequest request = UnityWebRequest.Get(uri))
|
||
|
|
{
|
||
|
|
// Request and wait for completion
|
||
|
|
yield return request.SendWebRequest();
|
||
|
|
|
||
|
|
#if UNITY_2020_1_OR_NEWER
|
||
|
|
if (request.result != UnityWebRequest.Result.Success)
|
||
|
|
#else
|
||
|
|
if (request.isNetworkError || request.isHttpError)
|
||
|
|
#endif
|
||
|
|
{
|
||
|
|
ImmersalLogger.LogError(request.error);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
SDKSparseDownloadResult plyDataResult = new SDKSparseDownloadResult();
|
||
|
|
plyDataResult.data = request.downloadHandler.data;
|
||
|
|
plyDataResult.error = request.error;
|
||
|
|
if (plyDataResult.error == null)
|
||
|
|
{
|
||
|
|
// Save map file on disk, overwrite existing file
|
||
|
|
string fileName = $"{mapId}-{mapName}-sparse.ply";
|
||
|
|
string plyFilePath = Path.Combine(targetFullPath, fileName);
|
||
|
|
WritePly(plyFilePath, plyDataResult.data);
|
||
|
|
resultCallback(plyDataResult, plyFilePath);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public static void WriteJson(string jsonFilepath, string data, bool overwrite = false)
|
||
|
|
{
|
||
|
|
if (File.Exists(jsonFilepath))
|
||
|
|
{
|
||
|
|
if (!overwrite) return;
|
||
|
|
File.Delete(jsonFilepath);
|
||
|
|
}
|
||
|
|
|
||
|
|
File.WriteAllText(jsonFilepath, data);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static void WriteBytes(string mapFilepath, string b64, bool overwrite = false)
|
||
|
|
{
|
||
|
|
if (File.Exists(mapFilepath))
|
||
|
|
{
|
||
|
|
if (!overwrite) return;
|
||
|
|
File.Delete(mapFilepath);
|
||
|
|
}
|
||
|
|
byte[] data = Convert.FromBase64String(b64);
|
||
|
|
File.WriteAllBytes(mapFilepath, data);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static void WritePly(string plyFilepath, byte[] data, bool overwrite = false)
|
||
|
|
{
|
||
|
|
if (File.Exists(plyFilepath))
|
||
|
|
{
|
||
|
|
if (!overwrite) return;
|
||
|
|
File.Delete(plyFilepath);
|
||
|
|
}
|
||
|
|
File.WriteAllBytes(plyFilepath, data);
|
||
|
|
AssetDatabase.Refresh();
|
||
|
|
AssetDatabase.ImportAsset(plyFilepath);
|
||
|
|
}
|
||
|
|
|
||
|
|
#endif
|
||
|
|
#endregion
|
||
|
|
}
|
||
|
|
|
||
|
|
public enum MapDataSource
|
||
|
|
{
|
||
|
|
Embed = 0, // use embedded mapfile
|
||
|
|
Download = 1 // let MapManager download mapfile at runtime
|
||
|
|
}
|
||
|
|
|
||
|
|
[Serializable]
|
||
|
|
public class MapLoadingOption : IMapOption
|
||
|
|
{
|
||
|
|
public string Name => "MapLoading";
|
||
|
|
|
||
|
|
[SerializeField]
|
||
|
|
public int m_SerializedDataSource = 0;
|
||
|
|
|
||
|
|
private MapDataSource m_MapDataSource = MapDataSource.Embed;
|
||
|
|
|
||
|
|
[SerializeField]
|
||
|
|
public bool DownloadVisualizationAtRuntime = false;
|
||
|
|
|
||
|
|
private TextAsset currentMapFile = null;
|
||
|
|
private TextAsset prevMapFile = null;
|
||
|
|
|
||
|
|
public byte[] Bytes;
|
||
|
|
|
||
|
|
public void DrawEditorGUI(XRMap map)
|
||
|
|
{
|
||
|
|
#if UNITY_EDITOR
|
||
|
|
m_MapDataSource = (MapDataSource)m_SerializedDataSource;
|
||
|
|
EditorGUI.BeginChangeCheck();
|
||
|
|
m_MapDataSource = (MapDataSource)EditorGUILayout.EnumPopup("Map data source", m_MapDataSource);
|
||
|
|
if (EditorGUI.EndChangeCheck())
|
||
|
|
{
|
||
|
|
m_SerializedDataSource = (int)m_MapDataSource;
|
||
|
|
}
|
||
|
|
if (m_SerializedDataSource == 0)
|
||
|
|
{
|
||
|
|
currentMapFile = prevMapFile = map.mapFile;
|
||
|
|
EditorGUI.BeginChangeCheck();
|
||
|
|
currentMapFile =
|
||
|
|
(TextAsset)EditorGUILayout.ObjectField("Map file", currentMapFile, typeof(TextAsset), false);
|
||
|
|
|
||
|
|
if (EditorGUI.EndChangeCheck())
|
||
|
|
{
|
||
|
|
if (currentMapFile == null)
|
||
|
|
return;
|
||
|
|
|
||
|
|
if (prevMapFile == null || currentMapFile != prevMapFile)
|
||
|
|
{
|
||
|
|
string bytesPath = AssetDatabase.GetAssetPath(currentMapFile);
|
||
|
|
if (bytesPath.EndsWith(".bytes"))
|
||
|
|
{
|
||
|
|
// Switch mapfile
|
||
|
|
prevMapFile = currentMapFile;
|
||
|
|
map.Uninitialize();
|
||
|
|
map.Configure(currentMapFile);
|
||
|
|
EditorUtility.SetDirty(map);
|
||
|
|
UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
ImmersalLogger.LogError($"{AssetDatabase.GetAssetPath(currentMapFile)} is not a valid map file");
|
||
|
|
map.mapFile = prevMapFile;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
DownloadVisualizationAtRuntime =
|
||
|
|
EditorGUILayout.Toggle("Download visualization", DownloadVisualizationAtRuntime);
|
||
|
|
}
|
||
|
|
#endif
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public class MapEntry
|
||
|
|
{
|
||
|
|
public XRMap Map;
|
||
|
|
public ISceneUpdateable SceneParent;
|
||
|
|
public MapToSpaceRelation Relation;
|
||
|
|
}
|
||
|
|
|
||
|
|
public class MapToSpaceRelation
|
||
|
|
{
|
||
|
|
public Vector3 Position;
|
||
|
|
public Quaternion Rotation;
|
||
|
|
public Vector3 Scale;
|
||
|
|
|
||
|
|
public Matrix4x4 ApplyRelation(Matrix4x4 input)
|
||
|
|
{
|
||
|
|
Vector3 inPos = input.GetPosition();
|
||
|
|
Quaternion inRot = input.rotation;
|
||
|
|
Matrix4x4 offsetNoScale = Matrix4x4.TRS(Position, Rotation, Vector3.one);
|
||
|
|
Vector3 scaledPos = Vector3.Scale(inPos, Scale);
|
||
|
|
return offsetNoScale * Matrix4x4.TRS(scaledPos, inRot, Vector3.one);
|
||
|
|
}
|
||
|
|
|
||
|
|
public Matrix4x4 ApplyInverseRelation(Matrix4x4 input)
|
||
|
|
{
|
||
|
|
Matrix4x4 offsetNoScale = Matrix4x4.TRS(Position, Rotation, Vector3.one);
|
||
|
|
Matrix4x4 offsetNoScaleInverse = offsetNoScale.inverse;
|
||
|
|
Matrix4x4 intermediate = offsetNoScaleInverse * input;
|
||
|
|
Vector3 scaledPos = intermediate.GetPosition();
|
||
|
|
Quaternion rot = intermediate.rotation;
|
||
|
|
Vector3 unscaledPos = scaledPos.SafeDivide(Scale);
|
||
|
|
return Matrix4x4.TRS(unscaledPos, rot, Vector3.one);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public class MapCreationParameters
|
||
|
|
{
|
||
|
|
public int? MapId; // Either this
|
||
|
|
public SDKMapMetadataGetResult? MetadataGetResult; // or this is necessary
|
||
|
|
public String Name; // Optional
|
||
|
|
public ISceneUpdateable SceneParent; // Optional
|
||
|
|
public ILocalizationMethod LocalizationMethod; // Optional
|
||
|
|
public Type LocalizationMethodType = typeof(DeviceLocalization); // Necessary if above is null
|
||
|
|
public IMapOption[] MapOptions; // Optional
|
||
|
|
public bool ApplyMapAlignment;
|
||
|
|
}
|
||
|
|
|
||
|
|
public class MapCreationResult
|
||
|
|
{
|
||
|
|
public bool Success;
|
||
|
|
public XRMap Map;
|
||
|
|
public ISceneUpdateable SceneParent;
|
||
|
|
}
|
||
|
|
}
|