Add ultimate xr
This commit is contained in:
8
Assets/UltimateXR/Runtime/Scripts/Extensions/System.meta
Normal file
8
Assets/UltimateXR/Runtime/Scripts/Extensions/System.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: caa3357c443f0d94b9baca41a8dd7018
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 438cf96278134136af485d7eff7f16aa
|
||||
timeCreated: 1620807425
|
||||
@@ -0,0 +1,130 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="CollectionExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UltimateXR.Extensions.System.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IReadOnlyCollection{T}" /> and <see cref="ICollection{T}" /> extensions.
|
||||
/// </summary>
|
||||
public static class CollectionExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Throws an exception if a given index is out of a <see cref="IReadOnlyCollection{T}" /> bounds.
|
||||
/// </summary>
|
||||
/// <param name="self">Collection</param>
|
||||
/// <param name="index">Index to check if it is out of bounds</param>
|
||||
/// <param name="paramName">Optional argument name</param>
|
||||
/// <typeparam name="T">Element type</typeparam>
|
||||
/// <exception cref="IndexOutOfRangeException">When index is out of range and no parameter name was specified</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">When index is out of range and a parameter name was specified</exception>
|
||||
public static void ThrowIfInvalidIndex<T>(this IReadOnlyCollection<T> self, int index, string paramName = null)
|
||||
{
|
||||
if (index >= 0 && index < self.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(paramName))
|
||||
{
|
||||
throw new IndexOutOfRangeException($"Index[{index}] out of range for collection of {typeof(T).Name}");
|
||||
}
|
||||
throw new ArgumentOutOfRangeException(paramName, index, $"Index[{index}] out of range for collection of {typeof(T).Name}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an exception if any of the given indexes is out of a <see cref="IReadOnlyCollection{T}" /> bounds.
|
||||
/// </summary>
|
||||
/// <param name="self">Collection</param>
|
||||
/// <param name="index1">Index 1 to check if it is out of bounds</param>
|
||||
/// <param name="index2">Index 2 to check if it is out of bounds</param>
|
||||
/// <typeparam name="T">Element type</typeparam>
|
||||
/// <seealso cref="ThrowIfInvalidIndex{T}" />
|
||||
public static void ThrowIfInvalidIndexes<T>(this IReadOnlyCollection<T> self, int index1, int index2)
|
||||
{
|
||||
self.ThrowIfInvalidIndex(index1);
|
||||
self.ThrowIfInvalidIndex(index2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an exception if any of the given indexes is out of a <see cref="IReadOnlyCollection{T}" /> bounds.
|
||||
/// </summary>
|
||||
/// <param name="self">Collection</param>
|
||||
/// <param name="index1">Index 1 to check if it is out of bounds</param>
|
||||
/// <param name="index2">Index 2 to check if it is out of bounds</param>
|
||||
/// <param name="index3">Index 3 to check if it is out of bounds</param>
|
||||
/// <typeparam name="T">Element type</typeparam>
|
||||
/// <seealso cref="ThrowIfInvalidIndex{T}" />
|
||||
public static void ThrowIfInvalidIndexes<T>(this IReadOnlyCollection<T> self, int index1, int index2, int index3)
|
||||
{
|
||||
self.ThrowIfInvalidIndex(index1);
|
||||
self.ThrowIfInvalidIndex(index2);
|
||||
self.ThrowIfInvalidIndex(index3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an exception if any of the given indexes is out of a <see cref="IReadOnlyCollection{T}" /> bounds.
|
||||
/// </summary>
|
||||
/// <param name="self">Collection</param>
|
||||
/// <param name="indexes">Indexes to check</param>
|
||||
/// <typeparam name="T">Element type</typeparam>
|
||||
public static void ThrowIfInvalidIndexes<T>(this IReadOnlyCollection<T> self, params int[] indexes)
|
||||
{
|
||||
foreach (int index in indexes)
|
||||
{
|
||||
self.ThrowIfInvalidIndex(index);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits a string using <see cref="string.Split(char[])" /> and adds the result to the collection.
|
||||
/// </summary>
|
||||
/// <param name="self">Collection to add the split result to</param>
|
||||
/// <param name="toSplit">String to split</param>
|
||||
/// <param name="separator">
|
||||
/// Separator to use for splitting. This will be used to call <see cref="string.Split(char[])" />
|
||||
/// on <paramref name="toSplit" />
|
||||
/// </param>
|
||||
/// <returns>The result collection</returns>
|
||||
public static ICollection<string> SplitAddRange(this ICollection<string> self, string toSplit, char separator)
|
||||
{
|
||||
self.ThrowIfNull(nameof(self));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(toSplit))
|
||||
{
|
||||
return self;
|
||||
}
|
||||
|
||||
foreach (string s in toSplit.Split(separator))
|
||||
{
|
||||
self.Add(s.Trim());
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits a string using <see cref="string.Split(char[])" /> and sets the result in the collection.
|
||||
/// </summary>
|
||||
/// <param name="self">Collection to set the split result in</param>
|
||||
/// <param name="toSplit">String to split</param>
|
||||
/// <param name="separator">
|
||||
/// Separator to use for splitting. This will be used to call <see cref="string.Split(char[])" />
|
||||
/// on <paramref name="toSplit" />
|
||||
/// </param>
|
||||
/// <returns>The result collection</returns>
|
||||
public static ICollection<string> SplitSetRange(this ICollection<string> self, string toSplit, char separator)
|
||||
{
|
||||
self.ThrowIfNull(nameof(self));
|
||||
self.Clear();
|
||||
return self.SplitAddRange(toSplit, separator);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2fbb5811e6ee84042ad95c9df2447cfd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,66 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="DictionaryExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UltimateXR.Extensions.System.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IDictionary{TKey,TValue}" /> and <see cref="Dictionary{TKey,TValue}" /> extensions.
|
||||
/// </summary>
|
||||
public static class DictionaryExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Adds all elements in another dictionary to the dictionary.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">Key type</typeparam>
|
||||
/// <typeparam name="TValue">Value type</typeparam>
|
||||
/// <param name="self">Destination dictionary</param>
|
||||
/// <param name="other">Source dictionary</param>
|
||||
/// <param name="overrideExistingKeys">Determines if duplicated keys must be overriden with other's values</param>
|
||||
public static void AddRange<TKey, TValue>(this IDictionary<TKey, TValue> self, IDictionary<TKey, TValue> other, bool overrideExistingKeys = false)
|
||||
{
|
||||
foreach (var kv in other)
|
||||
{
|
||||
if (!self.ContainsKey(kv.Key))
|
||||
{
|
||||
self.Add(kv.Key, kv.Value);
|
||||
}
|
||||
else if (overrideExistingKeys)
|
||||
{
|
||||
self[kv.Key] = kv.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a given value defined by a key in a dictionary. If the key is not found, it is added and the value is given
|
||||
/// the default value.
|
||||
/// </summary>
|
||||
/// <param name="self">Dictionary</param>
|
||||
/// <param name="key">Key to look for</param>
|
||||
/// <typeparam name="TKey">Key type</typeparam>
|
||||
/// <typeparam name="TValue">Value type</typeparam>
|
||||
/// <returns>
|
||||
/// Value in the dictionary. If the key doesn't exist it will be added and the return value will be the default
|
||||
/// value
|
||||
/// </returns>
|
||||
public static TValue GetOrAddValue<TKey, TValue>(this IDictionary<TKey, TValue> self, TKey key)
|
||||
where TValue : new()
|
||||
{
|
||||
if (!self.TryGetValue(key, out TValue value))
|
||||
{
|
||||
value = new TValue();
|
||||
self.Add(key, value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7d0cdca585a749e69c9d098ba98c006d
|
||||
timeCreated: 1620807425
|
||||
@@ -0,0 +1,318 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="EnumerableExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using UltimateXR.Core;
|
||||
using UltimateXR.Core.Settings;
|
||||
using UnityEngine;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
namespace UltimateXR.Extensions.System.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IEnumerable{T}" /> extensions.
|
||||
/// </summary>
|
||||
public static class EnumerableExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Compares two IEnumerable for equality, considering the order of elements.
|
||||
/// For dictionaries, compares key-value pairs regardless of their order.
|
||||
/// </summary>
|
||||
/// <param name="enumerableA">The first collection to compare</param>
|
||||
/// <param name="enumerableB">The second collection to compare</param>
|
||||
/// <returns>True if the collections are equal; otherwise, false</returns>
|
||||
public static bool ContentEqual(IEnumerable enumerableA, IEnumerable enumerableB)
|
||||
{
|
||||
return ContentEqual(enumerableA, enumerableB, (a, b) => a.ValuesEqual(b));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares two IEnumerable for equality, considering the order of elements.
|
||||
/// For dictionaries, compares key-value pairs regardless of their order.
|
||||
/// Values are compared using a floating point precision threshold used by
|
||||
/// <see cref="ObjectExt.ValuesEqual(object,object,float)" />.
|
||||
/// </summary>
|
||||
/// <param name="enumerableA">The first collection to compare</param>
|
||||
/// <param name="enumerableB">The second collection to compare</param>
|
||||
/// <param name="precisionThreshold">
|
||||
/// The precision threshold for float comparisons in types supported by
|
||||
/// <see cref="ObjectExt.ValuesEqual(object,object,float)" />.
|
||||
/// </param>
|
||||
/// <returns>True if the collections are equal; otherwise, false</returns>
|
||||
public static bool ContentEqual(IEnumerable enumerableA, IEnumerable enumerableB, float precisionThreshold)
|
||||
{
|
||||
return ContentEqual(enumerableA, enumerableB, (a, b) => a.ValuesEqual(b, precisionThreshold));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a random element from the collection.
|
||||
/// </summary>
|
||||
/// <param name="list">Collection to get the random element from</param>
|
||||
/// <typeparam name="TIn">Element type</typeparam>
|
||||
/// <returns>Random element from the collection</returns>
|
||||
/// <remarks>
|
||||
/// Uses Unity's random number generator (<see cref="UnityEngine.Random.Range(int,int)" />).
|
||||
/// </remarks>
|
||||
public static TIn RandomElement<TIn>(this IEnumerable<TIn> list)
|
||||
{
|
||||
return list.Any() ? list.ElementAt(Random.Range(0, list.Count())) : default(TIn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies an <see cref="Action" /> on all elements in a collection.
|
||||
/// </summary>
|
||||
/// <param name="list">Elements to apply the action on</param>
|
||||
/// <param name="action">Action to apply</param>
|
||||
/// <typeparam name="TIn">Element type</typeparam>
|
||||
/// <exception cref="ArgumentException">Any of the parameters was null</exception>
|
||||
public static void ForEach<TIn>(this IEnumerable<TIn> list, Action<TIn> action)
|
||||
{
|
||||
if (list == null)
|
||||
{
|
||||
throw new ArgumentException("Argument cannot be null.", nameof(list));
|
||||
}
|
||||
|
||||
if (action == null)
|
||||
{
|
||||
throw new ArgumentException("Argument cannot be null.", nameof(action));
|
||||
}
|
||||
|
||||
foreach (TIn value in list)
|
||||
{
|
||||
action(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously applies a function on all elements in a collection.
|
||||
/// </summary>
|
||||
/// <param name="list">Elements to apply the function on</param>
|
||||
/// <param name="function">Function to apply</param>
|
||||
/// <typeparam name="TIn">Element type</typeparam>
|
||||
/// <returns>An awaitable task wrapping the Task.WhenAll applying the function on all elements in a collection</returns>
|
||||
public static Task ForEachAsync<TIn>(this IEnumerable<TIn> list, Func<TIn, Task> function)
|
||||
{
|
||||
return Task.WhenAll(list.Select(function));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously applies a function to all elements in a collection.
|
||||
/// </summary>
|
||||
/// <param name="list">Elements to apply the function on</param>
|
||||
/// <param name="function">Function to apply</param>
|
||||
/// <typeparam name="TIn">Element type</typeparam>
|
||||
/// <typeparam name="TOut">Function return type</typeparam>
|
||||
/// <returns>An awaitable task wrapping the Task.WhenAll applying the function on all elements in a collection</returns>
|
||||
public static Task<TOut[]> ForEachAsync<TIn, TOut>(this IEnumerable<TIn> list, Func<TIn, Task<TOut>> function)
|
||||
{
|
||||
return Task.WhenAll(list.Select(function));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously applies an action on all elements in a collection.
|
||||
/// </summary>
|
||||
/// <param name="list">Elements to apply the action on</param>
|
||||
/// <param name="action">Action to apply</param>
|
||||
/// <typeparam name="TIn">Element type</typeparam>
|
||||
/// <returns>Task wrapping the Task.WhenAll applying the action on all elements in a collection</returns>
|
||||
public static Task ForEachThreaded<TIn>(this IEnumerable<TIn> list, Action<TIn> action)
|
||||
{
|
||||
void OnFaulted(Task runTask, int itemIndex)
|
||||
{
|
||||
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Warnings)
|
||||
{
|
||||
Debug.LogWarning($"{UxrConstants.CoreModule} ForEachThreaded::Item[{itemIndex}] faulted (see reason below):");
|
||||
}
|
||||
|
||||
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Errors)
|
||||
{
|
||||
Debug.LogException(runTask.Exception);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.WhenAll(list.Select((item, index) => Task.Run(() => action(item)).ContinueWith(runTask => OnFaulted(runTask, index), TaskContinuationOptions.OnlyOnFaulted)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously applies a function on all elements in a collection.
|
||||
/// </summary>
|
||||
/// <param name="list">Elements to apply the function on</param>
|
||||
/// <param name="function">Function to apply</param>
|
||||
/// <typeparam name="TIn">Element type</typeparam>
|
||||
/// <typeparam name="TOut">Function return type</typeparam>
|
||||
/// <returns>Task wrapping the Task.WhenAll applying the function on all elements in a collection</returns>
|
||||
public static Task<TOut[]> ForEachThreaded<TIn, TOut>(this IEnumerable<TIn> list, Func<TIn, TOut> function)
|
||||
{
|
||||
TOut OnFaulted(Task<TOut> t, int itemIndex)
|
||||
{
|
||||
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Warnings)
|
||||
{
|
||||
Debug.LogWarning($"{UxrConstants.CoreModule} ForEachThreaded::Item[{itemIndex}] faulted (see reason below):");
|
||||
}
|
||||
|
||||
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Errors)
|
||||
{
|
||||
Debug.LogException(t.Exception);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
return Task.WhenAll(list.Select((item, index) => Task.Run(() => function(item)).ContinueWith(t => OnFaulted(t, index), TaskContinuationOptions.OnlyOnFaulted)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the maximal element of the given sequence, based on the given projection.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This overload uses the default comparer for the projected type. This operator uses deferred execution. The results
|
||||
/// are evaluated and cached on first use to returned sequence.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TSource">Type of the source sequence</typeparam>
|
||||
/// <typeparam name="TKey">Type of the projected element</typeparam>
|
||||
/// <param name="source">Source sequence</param>
|
||||
/// <param name="selector">Selector to use to pick the results to compare</param>
|
||||
/// <returns>The maximal element, according to the projection.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="source" /> or <paramref name="selector" /> is null</exception>
|
||||
public static TSource MaxBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> selector)
|
||||
{
|
||||
return source.MaxBy(selector, null!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the maximal element of the given sequence, based on the given projection and the specified comparer for
|
||||
/// projected values.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This operator uses deferred execution. The results are evaluated and cached on first use to returned sequence.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TSource">Type of the source sequence</typeparam>
|
||||
/// <typeparam name="TKey">Type of the projected element</typeparam>
|
||||
/// <param name="source">Source sequence</param>
|
||||
/// <param name="selector">Selector to use to pick the results to compare</param>
|
||||
/// <param name="comparer">Comparer to use to compare projected values</param>
|
||||
/// <returns>The maximal element, according to the projection.</returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// <paramref name="source" />, <paramref name="selector" /> or <paramref name="comparer" /> is null
|
||||
/// </exception>
|
||||
/// <exception cref="T:System.Exception">A delegate callback throws an exception.</exception>
|
||||
public static TSource MaxBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> selector, IComparer<TKey> comparer)
|
||||
{
|
||||
source.ThrowIfNull(nameof(source));
|
||||
selector.ThrowIfNull(nameof(selector));
|
||||
|
||||
TSource result = default;
|
||||
TKey keyMax = default;
|
||||
comparer ??= Comparer<TKey>.Default;
|
||||
bool isFirst = true;
|
||||
|
||||
foreach (TSource s in source)
|
||||
{
|
||||
TKey key = selector(s);
|
||||
|
||||
if (isFirst)
|
||||
{
|
||||
result = s;
|
||||
keyMax = key;
|
||||
isFirst = false;
|
||||
}
|
||||
else if (comparer.Compare(key, keyMax) > 0)
|
||||
{
|
||||
result = s;
|
||||
keyMax = key;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits a list of strings using CamelCase.
|
||||
/// </summary>
|
||||
/// <param name="strings">List of strings</param>
|
||||
/// <returns>List of strings with added spacing</returns>
|
||||
public static IEnumerable<string> SplitCamelCase(this IEnumerable<string> strings)
|
||||
{
|
||||
foreach (string element in strings)
|
||||
{
|
||||
yield return element.SplitCamelCase();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
|
||||
/// <summary>
|
||||
/// Compares two IEnumerable for equality, considering the order of elements.
|
||||
/// For dictionaries, compares key-value pairs regardless of their order.
|
||||
/// </summary>
|
||||
/// <param name="enumerableA">The first collection to compare</param>
|
||||
/// <param name="enumerableB">The second collection to compare</param>
|
||||
/// <param name="comparer">Comparison function</param>
|
||||
/// <returns>True if the collections are equal; otherwise, false</returns>
|
||||
private static bool ContentEqual(IEnumerable enumerableA, IEnumerable enumerableB, Func<object, object, bool> comparer)
|
||||
{
|
||||
// If the collections are dictionaries, compare key-value pairs
|
||||
if (enumerableA is IDictionary dictionaryA && enumerableB is IDictionary dictionaryB)
|
||||
{
|
||||
// Ensure both dictionaries have the same number of elements
|
||||
if (dictionaryA.Count != dictionaryB.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare key-value pairs regardless of order
|
||||
foreach (DictionaryEntry entryA in dictionaryA)
|
||||
{
|
||||
if (!dictionaryB.Contains(entryA.Key) || !comparer(entryA.Value, dictionaryB[entryA.Key]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the collections are lists, do a quick test to check if they have different number of elements
|
||||
if (enumerableA is IList listA && enumerableB is IList listB)
|
||||
{
|
||||
if (listA.Count != listB.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If the collections are HashSets, compare elements using SetEquals
|
||||
if (enumerableA is HashSet<object> hashSetA && enumerableB is HashSet<object> hashSetB)
|
||||
{
|
||||
return hashSetA.SetEquals(hashSetB);
|
||||
}
|
||||
|
||||
// For non-dictionary, non-HashSet collections, compare elements
|
||||
IEnumerator enumeratorA = enumerableA.GetEnumerator();
|
||||
IEnumerator enumeratorB = enumerableB.GetEnumerator();
|
||||
|
||||
while (enumeratorA.MoveNext())
|
||||
{
|
||||
if (!enumeratorB.MoveNext() || !comparer(enumeratorA.Current, enumeratorB.Current))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure both collections have the same number of elements
|
||||
return !enumeratorB.MoveNext();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aded6c93d8bb43a799907d1f47eca9a4
|
||||
timeCreated: 1586946987
|
||||
@@ -0,0 +1,161 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="ListExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.System.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="List{T}" /> extensions.
|
||||
/// </summary>
|
||||
public static class ListExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index of a given item in a list.
|
||||
/// </summary>
|
||||
/// <param name="self">List where to look for the item</param>
|
||||
/// <param name="item">Item to look for</param>
|
||||
/// <typeparam name="T">Element type</typeparam>
|
||||
/// <returns>Element index or -1 if not found</returns>
|
||||
/// <remarks>Equals() is used for comparison</remarks>
|
||||
public static int IndexOf<T>(this IReadOnlyList<T> self, T item)
|
||||
{
|
||||
for (int i = 0; i < self.Count; ++i)
|
||||
{
|
||||
if (Equals(self[i], item))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the list in reverse order.
|
||||
/// </summary>
|
||||
/// <param name="self">List in reverse order</param>
|
||||
/// <typeparam name="T">Element type</typeparam>
|
||||
/// <returns>List in reverse order</returns>
|
||||
public static IEnumerable<T> Reversed<T>(this IList<T> self)
|
||||
{
|
||||
for (int i = self.Count - 1; i >= 0; --i)
|
||||
{
|
||||
yield return self[i];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a random element from the list.
|
||||
/// </summary>
|
||||
/// <param name="self">List to get the random element from</param>
|
||||
/// <typeparam name="T">Element type</typeparam>
|
||||
/// <returns>Random element from the list</returns>
|
||||
/// <remarks>
|
||||
/// Uses Unity's random number generator (<see cref="Random.Range(int,int)" />).
|
||||
/// </remarks>
|
||||
public static T RandomElement<T>(this IReadOnlyList<T> self)
|
||||
{
|
||||
return self.Count > 0 ? self[Random.Range(0, self.Count)] : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a list of random indices without repetition in a given range.
|
||||
/// </summary>
|
||||
/// <param name="minInclusive">The minimum index value (inclusive)</param>
|
||||
/// <param name="maxExclusive">The maximum value, which will not be included</param>
|
||||
/// <param name="count"></param>
|
||||
/// <returns></returns>
|
||||
public static List<int> GenerateRandomIndicesWithoutRepetition(int minInclusive, int maxExclusive, int count)
|
||||
{
|
||||
List<int> newList = new List<int>();
|
||||
|
||||
for (int i = minInclusive; i < maxExclusive; ++i)
|
||||
{
|
||||
newList.Add(i);
|
||||
}
|
||||
|
||||
return newList.RandomElementsWithoutRepetition(count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list with n random elements from a list without repetition.
|
||||
/// </summary>
|
||||
/// <param name="self">List to get the random elements from</param>
|
||||
/// <param name="count">Number of random elements to get</param>
|
||||
/// <typeparam name="T">Element type</typeparam>
|
||||
/// <returns>
|
||||
/// List with random elements. If <paramref name="count" /> is larger than the list, the result list will be as
|
||||
/// long as the input list.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// Uses Unity's random number generator (<see cref="Random.Range(int,int)" />).
|
||||
/// </remarks>
|
||||
public static List<T> RandomElementsWithoutRepetition<T>(this IReadOnlyList<T> self, int count)
|
||||
{
|
||||
List<T> candidates = new List<T>(self);
|
||||
List<T> randomElements = new List<T>();
|
||||
|
||||
for (int i = 0; i < count && candidates.Count > 0; ++i)
|
||||
{
|
||||
int randomIndex = Random.Range(0, candidates.Count);
|
||||
randomElements.Add(candidates[randomIndex]);
|
||||
candidates.RemoveAt(randomIndex);
|
||||
}
|
||||
|
||||
return randomElements;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list with n random elements from a list without repetition. An additional list can be provided to exclude
|
||||
/// elements from appearing in the results.
|
||||
/// </summary>
|
||||
/// <param name="self">List to get the random elements from</param>
|
||||
/// <param name="listToExclude">List of elements to exclude from the results</param>
|
||||
/// <param name="count">Number of random elements to get</param>
|
||||
/// <typeparam name="T">Element type</typeparam>
|
||||
/// <returns>
|
||||
/// List with random elements. If <paramref name="count" /> is larger than the list, the result list will be as
|
||||
/// long as the input list minus the excluded elements.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// Uses Unity's random number generator (<see cref="Random.Range(int,int)" />).
|
||||
/// </remarks>
|
||||
public static List<T> RandomElementsWithoutRepetitionExcept<T>(this IReadOnlyList<T> self, IReadOnlyList<T> listToExclude, int count)
|
||||
{
|
||||
List<T> candidates = new List<T>(self.Where(p => !listToExclude.Any(p2 => Equals(p2, p))));
|
||||
List<T> randomElements = new List<T>();
|
||||
|
||||
for (int i = 0; i < count && candidates.Count > 0; ++i)
|
||||
{
|
||||
int randomIndex = Random.Range(0, candidates.Count);
|
||||
randomElements.Add(candidates[randomIndex]);
|
||||
candidates.RemoveAt(randomIndex);
|
||||
}
|
||||
|
||||
return randomElements;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list with the input list elements shuffled.
|
||||
/// </summary>
|
||||
/// <param name="self">List to get the random elements from</param>
|
||||
/// <typeparam name="T">Element type</typeparam>
|
||||
/// <returns>List with shuffled elements.</returns>
|
||||
/// <remarks>
|
||||
/// Uses Unity's random number generator (<see cref="Random.Range(int,int)" />).
|
||||
/// </remarks>
|
||||
public static List<T> Shuffled<T>(this IReadOnlyList<T> self)
|
||||
{
|
||||
return self.RandomElementsWithoutRepetition(self.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0fe3095d1ef7a9c428f15cc9a190b4b2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,37 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="EnumExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UltimateXR.Extensions.System
|
||||
{
|
||||
/// <summary>
|
||||
/// Enum extensions.
|
||||
/// </summary>
|
||||
public static class EnumExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates all flags that are set in the enum value.
|
||||
/// </summary>
|
||||
/// <param name="self"></param>
|
||||
/// <param name="includeZero">Whether to include the 0 in the list</param>
|
||||
/// <returns>List of flags set in the enum value</returns>
|
||||
public static IEnumerable<T> GetFlags<T>(this T self, bool includeZero = false) where T : Enum
|
||||
{
|
||||
foreach (T value in Enum.GetValues(self.GetType()))
|
||||
{
|
||||
if (self.HasFlag(value) && !(!includeZero && Convert.ToInt32(value) == 0))
|
||||
{
|
||||
yield return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c46b7f127b45c614c9e7be73348c6e86
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,63 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="GuidExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace UltimateXR.Extensions.System
|
||||
{
|
||||
public static class GuidExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Throws an exception if the object has the default Guid.
|
||||
/// </summary>
|
||||
/// <param name="self">Guid to check</param>
|
||||
/// <param name="paramName">Parameter name, used as argument for the exception message or null to not use it</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown if the object is the default Guid (default or <see cref="Guid.Empty" />)</exception>
|
||||
public static void ThrowIfDefault(this Guid self, string paramName)
|
||||
{
|
||||
if (self == Guid.Empty)
|
||||
{
|
||||
if (paramName == null)
|
||||
{
|
||||
throw new Exception("Guid cannot be empty");
|
||||
}
|
||||
|
||||
throw new Exception($"Guid is empty for parameter {paramName}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines two Guid instances to create a new unique Guid.
|
||||
/// </summary>
|
||||
/// <param name="guid1">The first Guid to combine</param>
|
||||
/// <param name="guid2">The second Guid to combine</param>
|
||||
/// <returns>A new Guid that is a combination of the input Guids</returns>
|
||||
public static Guid Combine(Guid guid1, Guid guid2)
|
||||
{
|
||||
byte[] bytes1 = guid1.ToByteArray();
|
||||
byte[] bytes2 = guid2.ToByteArray();
|
||||
|
||||
// Concatenate the byte arrays
|
||||
byte[] combinedBytes = new byte[bytes1.Length + bytes2.Length];
|
||||
Buffer.BlockCopy(bytes1, 0, combinedBytes, 0, bytes1.Length);
|
||||
Buffer.BlockCopy(bytes2, 0, combinedBytes, bytes1.Length, bytes2.Length);
|
||||
|
||||
// Use SHA-256 hash function to generate a unique hash
|
||||
using SHA256 sha256 = SHA256.Create();
|
||||
byte[] hashBytes = sha256.ComputeHash(combinedBytes);
|
||||
|
||||
// Take the first 16 bytes of the hash to create a new Guid
|
||||
byte[] guidBytes = new byte[16];
|
||||
Buffer.BlockCopy(hashBytes, 0, guidBytes, 0, guidBytes.Length);
|
||||
|
||||
return new Guid(guidBytes);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f2a483ee55424a44b6ee467f1a919f9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1ccdc2da9fc4817855b0b71c1136de7
|
||||
timeCreated: 1620807376
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a9b4b01a8c9bab94e9b674feafc6b02e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a0855abdfb7ddf945ab79e51a5024864
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,309 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="FileExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UltimateXR.Exceptions;
|
||||
using UltimateXR.Extensions.Unity.IO;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
namespace UltimateXR.Extensions.System.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// File extensions.
|
||||
/// </summary>
|
||||
public static class FileExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Reads bytes from a file asynchronously.
|
||||
/// Multiple file locations are supported:
|
||||
/// <list type="bullet">
|
||||
/// <item>Files in <see cref="Application.streamingAssetsPath" /></item>
|
||||
/// <item>Files in an http:// location</item>
|
||||
/// <item>Files in a file:// location</item>
|
||||
/// </list>
|
||||
/// All other Uris will be considered file paths and the file:// location will be added.
|
||||
/// </summary>
|
||||
/// <param name="uri">File full path to be opened for reading</param>
|
||||
/// <param name="ct">
|
||||
/// Optional cancellation token, to be able to cancel the asynchronous operation
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Bytes read
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// <see cref="UnityWebRequest.Get(string)">UnityWebRequest.Get()</see> is used internally to perform the actual
|
||||
/// reading
|
||||
/// </remarks>
|
||||
/// <exception cref="OperationCanceledException">
|
||||
/// Task canceled using <paramref name="ct" />
|
||||
/// </exception>
|
||||
/// <exception cref="FileNotFoundException">
|
||||
/// The file specified in <paramref name="uri" /> was not found.
|
||||
/// </exception>
|
||||
/// <exception cref="NotSupportedException">
|
||||
/// <paramref name="uri" /> is in an invalid format.
|
||||
/// </exception>
|
||||
/// <exception cref="IOException">
|
||||
/// An I/O error occurred while opening the file.
|
||||
/// </exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// The stream is currently in use by a previous read operation.
|
||||
/// </exception>
|
||||
public static async Task<byte[]> Read(string uri, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
uri.ThrowIfNullOrWhitespace(nameof(uri));
|
||||
|
||||
if (UnityWebRequestExt.IsUwrUri(uri))
|
||||
{
|
||||
try
|
||||
{
|
||||
return await UnityWebRequestExt.ReadBytesAsync(uri, ct);
|
||||
}
|
||||
catch (UwrException e)
|
||||
{
|
||||
throw new FileNotFoundException(e.Message, uri, e);
|
||||
}
|
||||
}
|
||||
|
||||
List<byte> bytes = new List<byte>();
|
||||
byte[] buffer = new byte[0x1000];
|
||||
using (var fs = new FileStream(uri, FileMode.Open, FileAccess.Read, FileShare.Read, buffer.Length, true))
|
||||
{
|
||||
while (await fs.ReadAsync(buffer, 0, buffer.Length, ct).ConfigureAwait(false) != 0)
|
||||
{
|
||||
bytes.AddRange(buffer);
|
||||
}
|
||||
}
|
||||
return bytes.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads text from a file asynchronously.
|
||||
/// Multiple file locations are supported:
|
||||
/// <list type="bullet">
|
||||
/// <item>Files in <see cref="Application.streamingAssetsPath" /></item>
|
||||
/// <item>Files in an http:// location</item>
|
||||
/// <item>Files in a file:// location</item>
|
||||
/// </list>
|
||||
/// All other Uris will be considered file paths and the file:// location will be added.
|
||||
/// </summary>
|
||||
/// <param name="uri">File location</param>
|
||||
/// <param name="encoding">Optional file encoding</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the asynchronous operation</param>
|
||||
/// <returns>A pair describing a boolean success value and the text read</returns>
|
||||
/// <remarks>
|
||||
/// <see cref="UnityWebRequest.Get(string)">UnityWebRequest.Get()</see> is used internally to perform the actual
|
||||
/// reading
|
||||
/// </remarks>
|
||||
public static async Task<(bool success, string text)> TryReadText(string uri, Encoding encoding = default, CancellationToken ct = default)
|
||||
{
|
||||
(bool success, string text) result;
|
||||
try
|
||||
{
|
||||
result.text = await ReadText(uri, encoding, ct).ConfigureAwait(false);
|
||||
result.success = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
result.text = null;
|
||||
result.success = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads text from a file asynchronously.
|
||||
/// Multiple file locations are supported:
|
||||
/// <list type="bullet">
|
||||
/// <item>Files in <see cref="Application.streamingAssetsPath" /></item>
|
||||
/// <item>Files in an http:// location</item>
|
||||
/// <item>Files in a file:// location</item>
|
||||
/// </list>
|
||||
/// All other Uris will be considered file paths and the file:// location will be added.
|
||||
/// </summary>
|
||||
/// <param name="uri">File full path to be opened for reading</param>
|
||||
/// <param name="encoding">Optional file encoding</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the asynchronous operation</param>
|
||||
/// <returns>Text content of the file or <see langword="null" /> if not found.</returns>
|
||||
/// <remarks>
|
||||
/// <see cref="UnityWebRequest.Get(string)">UnityWebRequest.Get()</see> is used internally to perform the actual
|
||||
/// reading
|
||||
/// </remarks>
|
||||
/// <exception cref="OperationCanceledException">
|
||||
/// Task canceled using <paramref name="ct" />
|
||||
/// </exception>
|
||||
/// <exception cref="NotSupportedException">
|
||||
/// <paramref name="uri" /> is in an invalid format.
|
||||
/// </exception>
|
||||
/// <exception cref="IOException">
|
||||
/// An I/O error occurred while opening the file.
|
||||
/// </exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// The stream is currently in use by a previous read operation.
|
||||
/// </exception>
|
||||
public static async Task<string> ReadText(string uri, Encoding encoding = default, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
uri.ThrowIfNullOrWhitespace(nameof(uri));
|
||||
encoding ??= DefaultEncoding;
|
||||
|
||||
if (UnityWebRequestExt.IsUwrUri(uri))
|
||||
{
|
||||
try
|
||||
{
|
||||
return await UnityWebRequestExt.ReadTextAsync(uri, ct);
|
||||
}
|
||||
catch (UwrException e)
|
||||
{
|
||||
throw new FileNotFoundException(e.Message, uri, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!File.Exists(uri))
|
||||
{
|
||||
throw new FileNotFoundException("File does not exist", uri);
|
||||
}
|
||||
|
||||
using StreamReader sr = new StreamReader(uri, encoding, true);
|
||||
return await sr.ReadToEndAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously writes an <see cref="Array" /> of <see cref="byte" /> to a file at <paramref name="path" />.
|
||||
/// </summary>
|
||||
/// <param name="bytes">File content as <see cref="Array" /> of <see cref="byte" /></param>
|
||||
/// <param name="path">File full path to be opened for writing</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the asynchronous operation</param>
|
||||
/// <returns>An awaitable writing <see cref="Task" /></returns>
|
||||
/// <exception cref="IOException">
|
||||
/// An I/O error occurred while creating the file.
|
||||
/// </exception>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="path" /> is a zero-length string, contains only white space, or
|
||||
/// contains one or more invalid characters. You can query for invalid characters by using the
|
||||
/// <see cref="Path.GetInvalidPathChars" /> method.
|
||||
/// </exception>
|
||||
/// <exception cref="PathTooLongException">
|
||||
/// The <paramref name="path" /> parameter is longer than the system-defined maximum length.
|
||||
/// </exception>
|
||||
/// <exception cref="UnauthorizedAccessException">
|
||||
/// The caller does not have the required permission.
|
||||
/// </exception>
|
||||
/// <exception cref="OperationCanceledException">
|
||||
/// Task canceled using <paramref name="ct" />
|
||||
/// </exception>
|
||||
public static async Task Write(byte[] bytes, string path, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
path.ThrowIfNullOrWhitespace(nameof(path));
|
||||
|
||||
using var stream = new MemoryStream(bytes);
|
||||
await Write(path, stream, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously writes the content of an <paramref name="sourceStream" /> to a file at <paramref name="path" />.
|
||||
/// </summary>
|
||||
/// <param name="path">File full path to be opened for writing</param>
|
||||
/// <param name="sourceStream"><see cref="Stream" /> to be written into a file.</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the asynchronous operation</param>
|
||||
/// <returns>An awaitable writing <see cref="Task" /></returns>
|
||||
/// <exception cref="IOException">
|
||||
/// An I/O error occurred while creating the file.
|
||||
/// </exception>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="path" /> is a zero-length string, contains only white space, or
|
||||
/// contains one or more invalid characters. You can query for invalid characters by using the
|
||||
/// <see cref="Path.GetInvalidPathChars" /> method.
|
||||
/// </exception>
|
||||
/// <exception cref="PathTooLongException">
|
||||
/// The <paramref name="path" /> parameter is longer than the system-defined maximum length.
|
||||
/// </exception>
|
||||
/// <exception cref="NotSupportedException">
|
||||
/// <paramref name="sourceStream" /> does not support reading, or the destination stream does not support writing.
|
||||
/// </exception>
|
||||
/// <exception cref="UnauthorizedAccessException">
|
||||
/// The caller does not have the required permission.
|
||||
/// </exception>
|
||||
/// <exception cref="OperationCanceledException">
|
||||
/// Task canceled using <paramref name="ct" />
|
||||
/// </exception>
|
||||
public static Task Write(string path, Stream sourceStream, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
path.ThrowIfNullOrWhitespace(nameof(path));
|
||||
sourceStream.ThrowIfNull(nameof(sourceStream));
|
||||
|
||||
string dirName = Path.GetDirectoryName(path);
|
||||
const int bufferSize = 81920;
|
||||
|
||||
async Task WriteInternal()
|
||||
{
|
||||
if (dirName != null)
|
||||
{
|
||||
Directory.CreateDirectory(dirName);
|
||||
}
|
||||
|
||||
using FileStream outputStream = new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None, bufferSize, FileOptions.Asynchronous);
|
||||
await sourceStream.CopyToAsync(outputStream, bufferSize, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Creating the new file has a performance impact that requires a new thread.
|
||||
return Task.Run(WriteInternal, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously writes text to a file location.
|
||||
/// </summary>
|
||||
/// <param name="path">File full path to be opened for writing</param>
|
||||
/// <param name="text">Text to write</param>
|
||||
/// <param name="encoding">Optional file encoding</param>
|
||||
/// <param name="append">Optional boolean telling whether to append or override. Default behaviour is to override.</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the asynchronous operation</param>
|
||||
/// <returns>Awaitable task</returns>
|
||||
/// <exception cref="OperationCanceledException">
|
||||
/// Task canceled using <paramref name="ct" />
|
||||
/// </exception>
|
||||
public static Task WriteText(string path, string text, Encoding encoding = default, bool append = false, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
path.ThrowIfNullOrWhitespace(nameof(path));
|
||||
|
||||
encoding ??= DefaultEncoding;
|
||||
string dirName = Path.GetDirectoryName(path);
|
||||
|
||||
async Task WriteInternal()
|
||||
{
|
||||
if (dirName != null)
|
||||
{
|
||||
Directory.CreateDirectory(dirName);
|
||||
}
|
||||
|
||||
using StreamWriter sw = new StreamWriter(path, append, encoding);
|
||||
await sw.WriteAsync(text).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Creating the new file has a performance impact that requires a new thread.
|
||||
return Task.Run(WriteInternal, ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Types & Data
|
||||
|
||||
private static readonly Encoding DefaultEncoding = new UTF8Encoding(false);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f1327e2dd4ce0241b881966c84b67a6
|
||||
timeCreated: 1620127914
|
||||
@@ -0,0 +1,124 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="PathExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UltimateXR.Core;
|
||||
using UltimateXR.Core.Settings;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.System.IO
|
||||
{
|
||||
public static class PathExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Like .NET's <see cref="Path.Combine(string,string)" /> but addressing some issues discussed in
|
||||
/// https://www.davidboike.dev/2020/06/path-combine-isnt-as-cross-platform-as-you-think-it-is/
|
||||
/// </summary>
|
||||
/// <param name="basePath">Base path</param>
|
||||
/// <param name="additional">Additional segments or multi-segment paths</param>
|
||||
/// <returns>Path result of combining the base path and the additional segments or multi-segment paths</returns>
|
||||
public static string Combine(string basePath, params string[] additional)
|
||||
{
|
||||
string[][] splits = additional.Select(s => s.Split(PathSplitCharacters)).ToArray();
|
||||
int totalLength = splits.Sum(arr => arr.Length);
|
||||
string[] segments = new string[totalLength + 1];
|
||||
|
||||
segments[0] = basePath;
|
||||
var i = 0;
|
||||
|
||||
foreach (string[] split in splits)
|
||||
{
|
||||
foreach (string value in split)
|
||||
{
|
||||
i++;
|
||||
segments[i] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return Path.Combine(segments);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a path or sub-path so that any wrong directory separator char is fixed for the current platform.
|
||||
/// </summary>
|
||||
/// <param name="multiSegment">pathOrSubPath</param>
|
||||
/// <returns>Normalized path</returns>
|
||||
public static string Normalize(string pathOrSubPath)
|
||||
{
|
||||
if (Path.IsPathFullyQualified(pathOrSubPath))
|
||||
{
|
||||
return Path.GetFullPath(new Uri(pathOrSubPath).LocalPath).TrimEnd(PathSplitCharacters);
|
||||
}
|
||||
|
||||
foreach (char separator in PathSplitCharacters.Where(c => c != Path.DirectorySeparatorChar))
|
||||
{
|
||||
pathOrSubPath = pathOrSubPath.Replace(separator, Path.DirectorySeparatorChar);
|
||||
}
|
||||
|
||||
return pathOrSubPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a path is a child of another path.
|
||||
/// Adapted from https://stackoverflow.com/questions/8091829/how-to-check-if-one-path-is-a-child-of-another-path
|
||||
/// </summary>
|
||||
/// <param name="candidate">Path candidate</param>
|
||||
/// <param name="other">Path to check against</param>
|
||||
/// <param name="canBeSame">Whether to also consider the same directory as valid</param>
|
||||
/// <returns>Whether the path is child of the parent path</returns>
|
||||
public static bool IsSubDirectoryOf(string candidate, string other, bool canBeSame = true)
|
||||
{
|
||||
var isChild = false;
|
||||
try
|
||||
{
|
||||
// Some initial corrections to avoid false negatives:
|
||||
|
||||
var candidateInfo = new DirectoryInfo(candidate.Replace(@"\", @"/").TrimEnd('/'));
|
||||
var otherInfo = new DirectoryInfo(other.Replace(@"\", @"/").TrimEnd('/'));
|
||||
|
||||
// Check if same directory
|
||||
|
||||
if (canBeSame && string.Compare(candidateInfo.FullName, otherInfo.FullName, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Start traversing upwards
|
||||
|
||||
while (candidateInfo.Parent != null)
|
||||
{
|
||||
if (string.Equals(candidateInfo.Parent.FullName, otherInfo.FullName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
isChild = true;
|
||||
break;
|
||||
}
|
||||
|
||||
candidateInfo = candidateInfo.Parent;
|
||||
}
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Errors)
|
||||
{
|
||||
Debug.LogError($"{UxrConstants.CoreModule} Unable to check directories {candidate} and {other}: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
return isChild;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Types & Data
|
||||
|
||||
private static readonly char[] PathSplitCharacters = { '/', '\\' };
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1e021f51e7aad3b4889b7e7ff42776c8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2cc0d2d42fb0afe48b20d9f36bd35cbd
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,167 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="FloatExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using UltimateXR.Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.System.Math
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="float" /> extensions.
|
||||
/// </summary>
|
||||
public static class FloatExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Compares two <c>float</c> values for equality with a specified precision threshold.
|
||||
/// </summary>
|
||||
/// <param name="a">The first <c>float</c> to compare</param>
|
||||
/// <param name="b">The second <c>float</c> to compare</param>
|
||||
/// <param name="precisionThreshold">
|
||||
/// The precision threshold for <c>float</c> comparisons. Defaults to
|
||||
/// <see cref="UxrConstants.Math.DefaultPrecisionThreshold" />.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the <c>float</c> are equal; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool EqualsUsingPrecision(this float a, float b, float precisionThreshold = UxrConstants.Math.DefaultPrecisionThreshold)
|
||||
{
|
||||
return Mathf.Abs(a - b) <= precisionThreshold;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a float value representing time in seconds to a formatted string value.
|
||||
/// </summary>
|
||||
/// <param name="self">Seconds to convert</param>
|
||||
/// <param name="excludeHoursIfZero">Whether to exclude the hours from the string if they are 0</param>
|
||||
/// <param name="includeMilliseconds">Whether to include the milliseconds in the string</param>
|
||||
/// <returns>
|
||||
/// Formatted time hh:mm::ss:mmm applying <paramref name="excludeHoursIfZero" /> and
|
||||
/// <paramref name="includeMilliseconds" /> constraints.
|
||||
/// </returns>
|
||||
public static string SecondsToTimeString(this float self, bool excludeHoursIfZero = false, bool includeMilliseconds = false)
|
||||
{
|
||||
int hours = Mathf.FloorToInt(self / 3600.0f);
|
||||
int minutes = Mathf.FloorToInt((self - hours * 3600.0f) / 60.0f);
|
||||
int seconds = Mathf.FloorToInt(self - hours * 3600.0f - minutes * 60.0f);
|
||||
int milliseconds = (int)(self * 1000 % 1000);
|
||||
|
||||
if (hours >= 1)
|
||||
{
|
||||
return includeMilliseconds ? $"{hours:D2}:{minutes:D2}:{seconds:D2}:{milliseconds:D3}" : $"{hours:D2}:{minutes:D2}:{seconds:D2}";
|
||||
}
|
||||
|
||||
if (excludeHoursIfZero)
|
||||
{
|
||||
return includeMilliseconds ? $"{minutes:D2}:{seconds:D2}:{milliseconds:D3}" : $"{minutes:D2}:{seconds:D2}";
|
||||
}
|
||||
|
||||
return includeMilliseconds ? $"{hours:D2}:{minutes:D2}:{seconds:D2}:{milliseconds:D3}" : $"{hours:D2}:{minutes:D2}:{seconds:D2}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a float value is or is very close to zero.
|
||||
/// </summary>
|
||||
/// <param name="self">Value to check</param>
|
||||
/// <returns>Boolean telling whether the float value is or is very close to zero</returns>
|
||||
public static bool IsAlmostZero(this float self)
|
||||
{
|
||||
return Mathf.Approximately(self, 0.0f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a value in degrees, returns the same angle making sure it's in range [-180, 180]. For example, an
|
||||
/// input of -380.3 would return -20.3.
|
||||
/// </summary>
|
||||
/// <param name="self">Value to process</param>
|
||||
/// <returns>Degrees in range between [-180, 180]</returns>
|
||||
public static float ToEuler180(this float self)
|
||||
{
|
||||
float angle = self % 360.0f;
|
||||
|
||||
if (angle > 180.0f)
|
||||
{
|
||||
angle -= 360.0f;
|
||||
}
|
||||
else if (angle < -180.0f)
|
||||
{
|
||||
angle += 360.0f;
|
||||
}
|
||||
|
||||
return angle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps a value so that it doesn't go beyond a given range.
|
||||
/// </summary>
|
||||
/// <param name="self">Value to clamp</param>
|
||||
/// <param name="min">Minimum value</param>
|
||||
/// <param name="max">Maximum value</param>
|
||||
/// <returns>Clamped value between [min, max]</returns>
|
||||
public static float Clamp(this ref float self, float min, float max)
|
||||
{
|
||||
self = Mathf.Clamp(self, min, max);
|
||||
return self;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a clamped value.
|
||||
/// </summary>
|
||||
/// <param name="self">Value to clamp</param>
|
||||
/// <param name="min">Minimum value</param>
|
||||
/// <param name="max">Maximum value</param>
|
||||
/// <returns>Clamped value between [min, max]</returns>
|
||||
public static float Clamped(this float self, float min, float max)
|
||||
{
|
||||
return Mathf.Clamp(self, min, max);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps a value to [0.0, 1.0].
|
||||
/// </summary>
|
||||
/// <param name="self">Value to clamp</param>
|
||||
/// <returns>Clamped value between [0.0, 1.0]</returns>
|
||||
public static float Clamp(this ref float self)
|
||||
{
|
||||
self = Mathf.Clamp01(self);
|
||||
return self;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a clamped value in range [0.0, 1.0].
|
||||
/// </summary>
|
||||
/// <param name="self">Value to clamp</param>
|
||||
/// <returns>Clamped value between [0.0, 1.0]</returns>
|
||||
public static float Clamped(this float self)
|
||||
{
|
||||
return Mathf.Clamp01(self);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the value from the set with the maximum absolute value, but keeping the sign.
|
||||
/// </summary>
|
||||
/// <param name="values">Set of values</param>
|
||||
/// <returns>Value with the maximum absolute value keeping the sign</returns>
|
||||
public static float SignedAbsMax(params float[] values)
|
||||
{
|
||||
float signedAbsoluteMax = 0.0f;
|
||||
bool initialized = false;
|
||||
|
||||
foreach (float value in values)
|
||||
{
|
||||
if (!initialized || Mathf.Abs(value) > Mathf.Abs(signedAbsoluteMax))
|
||||
{
|
||||
initialized = true;
|
||||
signedAbsoluteMax = value;
|
||||
}
|
||||
}
|
||||
|
||||
return signedAbsoluteMax;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c0bd1e8805fb4c841bf65b2e3ceacb4d
|
||||
timeCreated: 1624962547
|
||||
@@ -0,0 +1,110 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="IntExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.System.Math
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="int" /> and <see cref="uint" /> extensions.
|
||||
/// </summary>
|
||||
public static class IntExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given int value has one or more flags set.
|
||||
/// </summary>
|
||||
/// <param name="self">int value</param>
|
||||
/// <param name="flags">Flag(s) to check for</param>
|
||||
/// <returns>Whether the int value has the given flag(s) set</returns>
|
||||
public static bool HasFlags(this int self, int flags)
|
||||
{
|
||||
return flags == (flags & self);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given uint value has one or more flags set.
|
||||
/// </summary>
|
||||
/// <param name="self">uint value</param>
|
||||
/// <param name="flags">Flag(s) to check for</param>
|
||||
/// <returns>Whether the uint value has the given flag(s) set</returns>
|
||||
public static bool HasFlags(this uint self, uint flags)
|
||||
{
|
||||
return flags == (flags & self);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an int value with one or more flags set if they weren't set already.
|
||||
/// </summary>
|
||||
/// <param name="self">int value</param>
|
||||
/// <param name="flags">Flag(s) to set when returned</param>
|
||||
/// <returns>int value with the given flag(s) set</returns>
|
||||
public static int WithFlags(this int self, int flags)
|
||||
{
|
||||
return self | flags;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an uint value with one or more flags set if they weren't set already.
|
||||
/// </summary>
|
||||
/// <param name="self">uint value</param>
|
||||
/// <param name="flags">Flag(s) to set when returned</param>
|
||||
/// <returns>uint value with the given flag(s) set</returns>
|
||||
public static uint WithFlags(this uint self, uint flags)
|
||||
{
|
||||
return self | flags;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an int value with one or more flags cleared if they were set.
|
||||
/// </summary>
|
||||
/// <param name="self">int value</param>
|
||||
/// <param name="flags">Flag(s) to clear when returned</param>
|
||||
/// <returns>int value with the given flag(s) cleared</returns>
|
||||
public static int WithoutFlags(this int self, int flags)
|
||||
{
|
||||
return self & ~flags;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an uint value with one or more flags cleared if they were set.
|
||||
/// </summary>
|
||||
/// <param name="self">uint value</param>
|
||||
/// <param name="flags">Flag(s) to clear when returned</param>
|
||||
/// <returns>uint value with the given flag(s) cleared</returns>
|
||||
public static uint WithoutFlags(this uint self, uint flags)
|
||||
{
|
||||
return self & ~flags;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps a value so that it doesn't go beyond a given range.
|
||||
/// </summary>
|
||||
/// <param name="self">Value to clamp</param>
|
||||
/// <param name="min">Minimum value</param>
|
||||
/// <param name="max">Maximum value</param>
|
||||
/// <returns>Clamped value between [min, max]</returns>
|
||||
public static int Clamp(this ref int self, int min, int max)
|
||||
{
|
||||
self = Mathf.Clamp(self, min, max);
|
||||
return self;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a clamped value.
|
||||
/// </summary>
|
||||
/// <param name="self">Value to clamp</param>
|
||||
/// <param name="min">Minimum value</param>
|
||||
/// <param name="max">Maximum value</param>
|
||||
/// <returns>Clamped value between [min, max]</returns>
|
||||
public static int Clamped(this int self, int min, int max)
|
||||
{
|
||||
return Mathf.Clamp(self, min, max);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c2c62d93e9b64c30af3654c58a2e2f10
|
||||
timeCreated: 1624962547
|
||||
273
Assets/UltimateXR/Runtime/Scripts/Extensions/System/ObjectExt.cs
Normal file
273
Assets/UltimateXR/Runtime/Scripts/Extensions/System/ObjectExt.cs
Normal file
@@ -0,0 +1,273 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="ObjectExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
using UltimateXR.Core;
|
||||
using UltimateXR.Extensions.System.Collections;
|
||||
using UltimateXR.Extensions.System.Math;
|
||||
using UltimateXR.Extensions.Unity.Math;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.System
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="object" /> extensions.
|
||||
/// </summary>
|
||||
public static class ObjectExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Compares two objects for equality, taking into account the content of collections for collection types.
|
||||
/// </summary>
|
||||
/// <param name="a">The first object to compare</param>
|
||||
/// <param name="b">The second object to compare</param>
|
||||
/// <returns><c>True</c> if the objects are equal; otherwise, <c>false</c></returns>
|
||||
public static bool ValuesEqual(this object a, object b)
|
||||
{
|
||||
return ValuesEqual(a, b, (ea, eb) => EnumerableExt.ContentEqual(ea, eb), (va, vb) => Equals(va, vb));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Same as <see cref="ValuesEqual(object, object)" /> but using a precision threshold for the following types:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <c>float</c>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <see cref="Vector2" />
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <see cref="Vector3" />
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <see cref="Vector4" />
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <see cref="Quaternion" />
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
/// <param name="a">The first object to compare</param>
|
||||
/// <param name="b">The second object to compare</param>
|
||||
/// <param name="precisionThreshold">
|
||||
/// The floating point precision threshold for the specific types listed above. The
|
||||
/// <see cref="UxrConstants.Math.DefaultPrecisionThreshold" /> constant can be used to provide a standard precision
|
||||
/// across all calls.
|
||||
/// </param>
|
||||
/// <returns><c>True</c> if the objects are equal; otherwise, <c>false</c></returns>
|
||||
public static bool ValuesEqual(this object a, object b, float precisionThreshold)
|
||||
{
|
||||
bool EqualsUsingPrecision(object a, object b, float precision)
|
||||
{
|
||||
// Check supported types with specific comparision using floating point precision
|
||||
|
||||
if (a is float fa && b is float fb)
|
||||
{
|
||||
return fa.EqualsUsingPrecision(fb, precision);
|
||||
}
|
||||
if (a is Vector3 v3a && b is Vector3 v3b)
|
||||
{
|
||||
return v3a.EqualsUsingPrecision(v3b, precision);
|
||||
}
|
||||
if (a is Quaternion qa && b is Quaternion qb)
|
||||
{
|
||||
return qa.EqualsUsingPrecision(qb, precision);
|
||||
}
|
||||
if (a is Vector2 v2a && b is Vector2 v2b)
|
||||
{
|
||||
return v2a.EqualsUsingPrecision(v2b, precision);
|
||||
}
|
||||
if (a is Vector4 v4a && b is Vector4 v4b)
|
||||
{
|
||||
return v4a.EqualsUsingPrecision(v4b, precision);
|
||||
}
|
||||
|
||||
// Default comparison fallback
|
||||
|
||||
return Equals(a, b);
|
||||
}
|
||||
|
||||
return ValuesEqual(a, b, (ea, eb) => EnumerableExt.ContentEqual(ea, eb, precisionThreshold), (va, vb) => EqualsUsingPrecision(va, vb, precisionThreshold));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a deep copy of the specified object, including support for arrays, List<T>, and Dictionary<TKey,
|
||||
/// TValue>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the object to be deep copied</typeparam>
|
||||
/// <param name="obj">The object to be deep copied</param>
|
||||
/// <returns>A deep copy of the original object</returns>
|
||||
/// <remarks>
|
||||
/// This method performs a deep copy, recursively copying all objects referenced by the original object.<br/>
|
||||
/// Types derived from <see cref="Component" /> are not supported, and a reference to the same object will be returned
|
||||
/// instead.<br/>
|
||||
/// If the type of the object is an array, List<T>, or Dictionary<TKey, TValue>, it is handled natively.
|
||||
/// If the type implements ICloneable, it uses the Clone method for copying.
|
||||
/// For value types (primitive types and structs), the method returns the original object as they are inherently
|
||||
/// deep-copied.
|
||||
/// For other types, binary serialization is used for deep copying.
|
||||
/// </remarks>
|
||||
public static T DeepCopy<T>(this T obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return default(T);
|
||||
}
|
||||
|
||||
if (obj is Component)
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
|
||||
Type type = obj.GetType();
|
||||
|
||||
// Check if the type is an array
|
||||
if (type.IsArray)
|
||||
{
|
||||
Type elementType = type.GetElementType();
|
||||
Array originalArray = obj as Array;
|
||||
Array copiedArray = Array.CreateInstance(elementType, originalArray.Length);
|
||||
for (int i = 0; i < originalArray.Length; i++)
|
||||
{
|
||||
copiedArray.SetValue(DeepCopy(originalArray.GetValue(i)), i);
|
||||
}
|
||||
return (T)(object)copiedArray;
|
||||
}
|
||||
|
||||
// Check if it's a List<T>
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
|
||||
{
|
||||
Type genericType = type.GetGenericArguments()[0];
|
||||
IList originalList = (IList)obj;
|
||||
IList copiedList = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(genericType));
|
||||
foreach (object item in originalList)
|
||||
{
|
||||
copiedList.Add(DeepCopy(item));
|
||||
}
|
||||
return (T)copiedList;
|
||||
}
|
||||
|
||||
// Check if it's a Dictionary<TKey, TValue>
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
{
|
||||
Type[] genericArguments = type.GetGenericArguments();
|
||||
Type keyType = genericArguments[0];
|
||||
Type valueType = genericArguments[1];
|
||||
IDictionary originalDict = (IDictionary)obj;
|
||||
IDictionary copiedDict = (IDictionary)Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(keyType, valueType));
|
||||
foreach (DictionaryEntry kvp in originalDict)
|
||||
{
|
||||
copiedDict.Add(DeepCopy(kvp.Key), DeepCopy(kvp.Value));
|
||||
}
|
||||
return (T)copiedDict;
|
||||
}
|
||||
|
||||
// Check if it's a HashSet<T>
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(HashSet<>))
|
||||
{
|
||||
IEnumerable originalSet = (IEnumerable)obj;
|
||||
|
||||
// Create a new HashSet<T> with the same element type
|
||||
HashSet<object> copiedSet = new HashSet<object>();
|
||||
|
||||
foreach (object item in originalSet)
|
||||
{
|
||||
copiedSet.Add(DeepCopy(item));
|
||||
}
|
||||
|
||||
return (T)(object)copiedSet;
|
||||
}
|
||||
|
||||
// Check if the type implements ICloneable
|
||||
if (typeof(ICloneable).IsAssignableFrom(type))
|
||||
{
|
||||
MethodInfo cloneMethod = type.GetMethod("Clone");
|
||||
if (cloneMethod != null)
|
||||
{
|
||||
return (T)cloneMethod.Invoke(obj, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's a primitive type or string
|
||||
if (type.IsValueType || type == typeof(string))
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Use serialization for other types
|
||||
return BinarySerializationCopy(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an exception if the object is null.
|
||||
/// </summary>
|
||||
/// <param name="self">Object to check</param>
|
||||
/// <param name="paramName">Parameter name, used as argument for the exceptions</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown if the object is null</exception>
|
||||
public static void ThrowIfNull(this object self, string paramName)
|
||||
{
|
||||
if (self is null)
|
||||
{
|
||||
throw new ArgumentNullException(paramName);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
|
||||
/// <summary>
|
||||
/// Compares two objects for equality, taking into account the content of collections for collection types.
|
||||
/// </summary>
|
||||
/// <param name="a">The first object to compare</param>
|
||||
/// <param name="b">The second object to compare</param>
|
||||
/// <returns>True if the objects are equal; otherwise, false</returns>
|
||||
private static bool ValuesEqual(this object a, object b, Func<IEnumerable, IEnumerable, bool> enumerableComparer, Func<object, object, bool> valueComparer)
|
||||
{
|
||||
if (ReferenceEquals(a, b))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a == null || b == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if both objects are collections
|
||||
if (a is IEnumerable enumerableA && b is IEnumerable enumerableB)
|
||||
{
|
||||
// Use enumerable comparer to compare the contents of collections
|
||||
return enumerableComparer(enumerableA, enumerableB);
|
||||
}
|
||||
|
||||
// Use value comparer for non-collection objects
|
||||
return valueComparer(a, b);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a deep copy of an object using serialization.
|
||||
/// </summary>
|
||||
/// <param name="obj">Object to get a deep copy of</param>
|
||||
/// <typeparam name="T">The object type</typeparam>
|
||||
/// <returns>A deep copy of the object</returns>
|
||||
private static T BinarySerializationCopy<T>(T obj)
|
||||
{
|
||||
using MemoryStream memoryStream = new MemoryStream();
|
||||
BinaryFormatter formatter = new BinaryFormatter();
|
||||
formatter.Serialize(memoryStream, obj);
|
||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||
return (T)formatter.Deserialize(memoryStream);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2cd622a10b541fd44b567959894a4019
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
237
Assets/UltimateXR/Runtime/Scripts/Extensions/System/StringExt.cs
Normal file
237
Assets/UltimateXR/Runtime/Scripts/Extensions/System/StringExt.cs
Normal file
@@ -0,0 +1,237 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="StringExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
namespace UltimateXR.Extensions.System
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="string" /> extensions.
|
||||
/// </summary>
|
||||
public static class StringExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of occurrences of a string in another string.
|
||||
/// </summary>
|
||||
/// <param name="self">The string where to perform the search</param>
|
||||
/// <param name="key">The string to find</param>
|
||||
/// <param name="caseSensitive">Whether the search should be case sensitive</param>
|
||||
/// <returns>Number of occurrences of <paramref name="key" /> in the source string</returns>
|
||||
public static int GetOccurrenceCount(this string self, string key, bool caseSensitive = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(self) || string.IsNullOrEmpty(key))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (caseSensitive)
|
||||
{
|
||||
return (self.Length - self.Replace(key, string.Empty).Length) / key.Length;
|
||||
}
|
||||
|
||||
return (self.Length - self.ToLower().Replace(key.ToLower(), string.Empty).Length) / key.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SHA-256 hash value of a string.
|
||||
/// </summary>
|
||||
/// <param name="self">String to get the SHA-256 hash value of</param>
|
||||
/// <returns>SHA-256 hash value of the string</returns>
|
||||
public static byte[] GetSha256(this string self)
|
||||
{
|
||||
using HashAlgorithm algorithm = SHA256.Create();
|
||||
return algorithm.ComputeHash(Encoding.ASCII.GetBytes(self));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the MD5 hash value of a string.
|
||||
/// </summary>
|
||||
/// <param name="self">String to get the MD5 hash value of</param>
|
||||
/// <returns>MD5 hash value of the string</returns>
|
||||
public static byte[] GetMd5(this string self)
|
||||
{
|
||||
using HashAlgorithm algorithm = MD5.Create();
|
||||
return algorithm.ComputeHash(Encoding.ASCII.GetBytes(self));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the double SHA-256 hash value of a string.
|
||||
/// </summary>
|
||||
/// <param name="self">String to get the double SHA-256 hash value of</param>
|
||||
/// <returns>Double SHA-256 hash value of the string</returns>
|
||||
public static string GetSha256x2(this string self)
|
||||
{
|
||||
byte[] sha256 = self.GetSha256();
|
||||
return sha256.Aggregate(new StringBuilder(sha256.Length * 2), (sb, b) => sb.AppendFormat("{0:x2}", b)).ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the double MD5 hash value of a string.
|
||||
/// </summary>
|
||||
/// <param name="self">String to get the double MD5 hash value of</param>
|
||||
/// <returns>Double MD5 hash value of the string</returns>
|
||||
public static string GetMd5x2(this string self)
|
||||
{
|
||||
byte[] md5 = self.GetMd5();
|
||||
return md5.Aggregate(new StringBuilder(md5.Length * 2), (sb, b) => sb.AppendFormat("{0:x2}", b)).ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a Guid result of hashing the string using SHA-256 and keeping the first 16 bytes.
|
||||
/// </summary>
|
||||
/// <param name="self">String to get the Guid hash value of</param>
|
||||
/// <returns>Guid hash value of the string</returns>
|
||||
public static Guid GetGuid(this string self)
|
||||
{
|
||||
// Take the first 16 bytes of the hash to create a Guid
|
||||
byte[] guidBytes = new byte[16];
|
||||
Buffer.BlockCopy(GetSha256(self), 0, guidBytes, 0, guidBytes.Length);
|
||||
|
||||
return new Guid(guidBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the invalid characters in a path with a given character.
|
||||
/// </summary>
|
||||
/// <param name="self">The path to process</param>
|
||||
/// <param name="fallbackChar">The valid character to use as replacement</param>
|
||||
/// <param name="invalidChars">The invalid characters to replace</param>
|
||||
/// <returns>New string with the replacements</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">The replacement character is part of the invalid characters</exception>
|
||||
public static string ReplaceInvalidPathChars(this string self, char fallbackChar = PathFallbackChar, params char[] invalidChars)
|
||||
{
|
||||
if (invalidChars.Length == 0)
|
||||
{
|
||||
return self.ReplaceInvalidDirPathChars(fallbackChar);
|
||||
}
|
||||
|
||||
if (invalidChars.Contains(fallbackChar))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(fallbackChar), fallbackChar, "Fallback should be a valid character");
|
||||
}
|
||||
|
||||
return string.Join(fallbackChar.ToString(), self.Split(invalidChars));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the invalid characters in a directory path with a given character.
|
||||
/// </summary>
|
||||
/// <param name="self">The directory path to process</param>
|
||||
/// <param name="fallbackChar">The valid character to use as replacement</param>
|
||||
/// <returns>New string with the replacements</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">The replacement character is part of the invalid characters</exception>
|
||||
public static string ReplaceInvalidDirPathChars(this string self, char fallbackChar = PathFallbackChar)
|
||||
{
|
||||
return self.ReplaceInvalidPathChars(fallbackChar, Path.GetInvalidPathChars());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the invalid characters in a file path with a given character.
|
||||
/// </summary>
|
||||
/// <param name="self">The file path to process</param>
|
||||
/// <param name="fallbackChar">The valid character to use as replacement</param>
|
||||
/// <returns>New string with the replacements</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">The replacement character is part of the invalid characters</exception>
|
||||
public static string ReplaceInvalidFilePathChars(this string self, char fallbackChar = PathFallbackChar)
|
||||
{
|
||||
return self.ReplaceInvalidPathChars(fallbackChar, Path.GetInvalidFileNameChars());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a random string.
|
||||
/// </summary>
|
||||
/// <param name="length">String length</param>
|
||||
/// <param name="includeLetters">Include letters in the string?</param>
|
||||
/// <param name="includeNumbers">Include numbers in the string?</param>
|
||||
/// <returns>Random string with given length or <see cref="string.Empty" /> if no letters and number were specified</returns>
|
||||
public static string RandomString(int length, bool includeLetters, bool includeNumbers)
|
||||
{
|
||||
const string lettersOnly = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const string numbersOnly = "0123456789";
|
||||
const string lettersAndNumbers = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
|
||||
if (includeLetters && !includeNumbers)
|
||||
{
|
||||
return new string(Enumerable.Repeat(lettersOnly, length).Select(s => s[Random.Range(0, s.Length)]).ToArray());
|
||||
}
|
||||
if (!includeLetters && includeNumbers)
|
||||
{
|
||||
return new string(Enumerable.Repeat(numbersOnly, length).Select(s => s[Random.Range(0, s.Length)]).ToArray());
|
||||
}
|
||||
if (includeLetters && includeNumbers)
|
||||
{
|
||||
return new string(Enumerable.Repeat(lettersAndNumbers, length).Select(s => s[Random.Range(0, s.Length)]).ToArray());
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits a string using CamelCase.
|
||||
/// </summary>
|
||||
/// <param name="self">Input string</param>
|
||||
/// <returns>Output string with added spaces</returns>
|
||||
public static string SplitCamelCase(this string self)
|
||||
{
|
||||
return Regex.Replace(self, "([A-Z])", " $1", RegexOptions.Compiled).Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an exception if the string is null or only contains whitespaces.
|
||||
/// </summary>
|
||||
/// <param name="self">The string to check</param>
|
||||
/// <param name="paramName">The parameter name, used as argument for the exceptions</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="self" /> is <see langword="null" /></exception>
|
||||
/// <exception cref="ArgumentException">Whitespace string is not allowed</exception>
|
||||
public static void ThrowIfNullOrWhitespace(this string self, string paramName)
|
||||
{
|
||||
if (self is null)
|
||||
{
|
||||
throw new ArgumentNullException(paramName);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(self))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(paramName, self, "Value cannot be whitespace");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an exception if the string is null or empty.
|
||||
/// </summary>
|
||||
/// <param name="self">The string</param>
|
||||
/// <param name="paramName">The parameter name, used as arguments for the exceptions</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="self" /> is <see langword="null" /></exception>
|
||||
/// <exception cref="ArgumentException">Empty string is not allowed</exception>
|
||||
public static void ThrowIfNullOrEmpty(this string self, string paramName)
|
||||
{
|
||||
if (self == null)
|
||||
{
|
||||
throw new ArgumentNullException(paramName);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(self))
|
||||
{
|
||||
throw new ArgumentException("Empty string is not allowed", paramName);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Types & Data
|
||||
|
||||
private const char PathFallbackChar = '_';
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5449ef1ab6eb4a27ab47977420ceecf5
|
||||
timeCreated: 1626264331
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 185edcb988fe5984c97a09ccc4946aa6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,137 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="ActionExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UltimateXR.Core.Threading.TaskControllers;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.System.Threading
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Action" /> extensions.
|
||||
/// </summary>
|
||||
public static class ActionExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Executes repeatedly this <see cref="Action" />, in the main thread, at <paramref name="rate" /> until cancellation
|
||||
/// is requested with <paramref name="ct" />.
|
||||
/// </summary>
|
||||
/// <param name="self"><see cref="Action" /> to loop at <paramref name="rate" /> Hz</param>
|
||||
/// <param name="rate">Loop frequency in Hz</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <seealso cref="LoopThreaded" />
|
||||
/// <seealso cref="ToLoop" />
|
||||
public static async void Loop(this Action self, float rate = 10f, CancellationToken ct = default)
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int deltaTimeMs = Mathf.RoundToInt(1000f / rate);
|
||||
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
// Start delay timer parallel to action execution
|
||||
Task delayTask = TaskExt.Delay(deltaTimeMs, ct);
|
||||
self();
|
||||
await delayTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes repeatedly this <see cref="Action" />, in a separated thread, at <paramref name="rate" /> Hz until
|
||||
/// cancellation is requested using <paramref name="ct" />.
|
||||
/// </summary>
|
||||
/// <param name="self"><see cref="Action" /> to loop at <paramref name="rate" /> Hz</param>
|
||||
/// <param name="rate">Loop frequency in Hz</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
public static async void LoopThreaded(this Action self, float rate = 10f, CancellationToken ct = default)
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int deltaTimeMs = Mathf.RoundToInt(1000f / rate);
|
||||
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
// We don't want to abort current thread (Task.Run) with ct
|
||||
// Instead, we wait for action to end, breaking the loop after that.
|
||||
Task delayTask = TaskExt.Delay(deltaTimeMs, ct);
|
||||
Task runTask = Task.Run(self, CancellationToken.None);
|
||||
await Task.WhenAll(delayTask, runTask);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="UxrLoopController" /> which wraps a cancellable loop executing this <see cref="Action" /> in
|
||||
/// the main thread.
|
||||
/// </summary>
|
||||
/// <param name="self"><see cref="Action" /> to loop at <paramref name="rate" /> Hz</param>
|
||||
/// <param name="rate">Loop frequency in Hz</param>
|
||||
/// <param name="autoStartDelay">
|
||||
/// Delay in milliseconds before loop executes its first iteration.
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// Equal or greater than zero: tells <see cref="UxrLoopController" /> to automatically start looping
|
||||
/// <paramref name="autoStartDelay" /> milliseconds after creation.
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// Negative (default) <see cref="UxrLoopController.Start()" /> needs to be called on returned
|
||||
/// <see cref="UxrLoopController" /> to start looping.
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// A <see cref="UxrLoopController" /> to handle (<see cref="UxrLoopController.Start()" />,
|
||||
/// <see cref="UxrLoopController.Stop" />) the loop execution.
|
||||
/// </returns>
|
||||
/// <seealso cref="UxrLoopController" />
|
||||
/// <seealso cref="Loop" />
|
||||
/// <seealso cref="ToThreadedLoop" />
|
||||
public static UxrLoopController ToLoop(this Action self, float rate = 10f, int autoStartDelay = -1)
|
||||
{
|
||||
return new UxrLoopController(ct => Loop(self, rate, ct), autoStartDelay);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="UxrLoopController" /> which wraps a cancellable loop executing this <see cref="Action" /> in a
|
||||
/// separate thread.
|
||||
/// </summary>
|
||||
/// <param name="self"><see cref="Action" /> to loop, in a separate thread, at <paramref name="rate" /> Hz</param>
|
||||
/// <param name="rate">Loop frequency in Hz</param>
|
||||
/// <param name="autoStartDelay">
|
||||
/// Delay in milliseconds before loop executes its first iteration.
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// Equal or greater than zero: tells <see cref="UxrLoopController" /> to automatically start looping
|
||||
/// <paramref name="autoStartDelay" /> milliseconds after creation.
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// Negative (default) <see cref="UxrLoopController.Start()" /> needs to be called on returned
|
||||
/// <see cref="UxrLoopController" /> to start looping.
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// A <see cref="UxrLoopController" /> to handle (<see cref="UxrLoopController.Start()" />,
|
||||
/// <see cref="UxrLoopController.Stop" />) the loop execution.
|
||||
/// </returns>
|
||||
/// <seealso cref="UxrLoopController" />
|
||||
/// <seealso cref="Loop" />
|
||||
public static UxrLoopController ToThreadedLoop(this Action self, float rate = 10f, int autoStartDelay = -1)
|
||||
{
|
||||
return new UxrLoopController(ct => LoopThreaded(self, rate, ct), autoStartDelay);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4688f45bbeea8e04090b2180af69567b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,314 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="TaskExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UltimateXR.Animation.Interpolation;
|
||||
using UltimateXR.Core;
|
||||
using UltimateXR.Core.Settings;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.System.Threading
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Task" /> extensions.
|
||||
/// </summary>
|
||||
public static class TaskExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Allows to run a task in "fire and forget" mode, when it is not required to await nor is it relevant whether it
|
||||
/// succeeds or not. There still needs to be a way to handle exceptions to avoid unhandled exceptions and process
|
||||
/// termination.
|
||||
/// </summary>
|
||||
/// <param name="self">Task to run in fire and forget mode</param>
|
||||
/// <exception cref="ArgumentNullException">The task is null</exception>
|
||||
public static async void FireAndForget(this Task self)
|
||||
{
|
||||
if (self == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(self));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Simply awaiting the task in an async void method already guarantees exception propagation
|
||||
await self;
|
||||
}
|
||||
catch (Exception e) // ...but default LogException behaviour only shows innerException.
|
||||
{
|
||||
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Errors)
|
||||
{
|
||||
Debug.LogError($"{UxrConstants.CoreModule} {nameof(TaskExt)}::{nameof(FireAndForget)} Exception missed (stack trace below):{e.Message}\n\n{e}");
|
||||
Debug.LogException(e); // Log and ignore exceptions, until playlists are empty.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an awaitable task that finishes the next frame.
|
||||
/// </summary>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the task</param>
|
||||
/// <returns>Awaitable task</returns>
|
||||
public static Task WaitForNextFrame(CancellationToken ct = default)
|
||||
{
|
||||
return SkipFrames(1, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an awaitable task that finishes after a given amount of frames.
|
||||
/// </summary>
|
||||
/// <param name="frameCount">Number of frames to wait</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the task</param>
|
||||
/// <returns>Awaitable task</returns>
|
||||
public static async Task SkipFrames(int frameCount, CancellationToken ct = default)
|
||||
{
|
||||
if (ct.IsCancellationRequested || frameCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint i = 0; i < frameCount && !ct.IsCancellationRequested; ++i)
|
||||
{
|
||||
await Task.Yield();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an awaitable task that finishes after a given amount of seconds.
|
||||
/// </summary>
|
||||
/// <param name="seconds">Number of seconds to wait</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the task</param>
|
||||
/// <returns>Awaitable task</returns>
|
||||
public static Task Delay(float seconds, CancellationToken ct = default)
|
||||
{
|
||||
return Delay(Mathf.RoundToInt(1000f * seconds), ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an awaitable task that finishes after a given amount of milliseconds.
|
||||
/// </summary>
|
||||
/// <param name="milliseconds">Number of milliseconds to wait</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the task</param>
|
||||
/// <returns>Awaitable task</returns>
|
||||
public static async Task Delay(int milliseconds, CancellationToken ct = default)
|
||||
{
|
||||
if (ct.IsCancellationRequested || milliseconds <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(milliseconds, ct);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignore: Task.Delay throws this exception when ct.IsCancellationRequested = true
|
||||
// In this case, we only want to stop polling and finish this async Task.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an awaitable task that blocks while a condition is true or the task is canceled.
|
||||
/// </summary>
|
||||
/// <param name="condition">The condition that will perpetuate the block</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the task</param>
|
||||
/// <returns>Awaitable <see cref="Task" /></returns>
|
||||
public static async Task WaitWhile(Func<bool> condition, CancellationToken ct = default)
|
||||
{
|
||||
while (!ct.IsCancellationRequested && condition())
|
||||
{
|
||||
await Task.Yield();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an awaitable task that blocks until a condition is true or the task is canceled.
|
||||
/// </summary>
|
||||
/// <param name="condition">The condition that will perpetuate the block</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the task</param>
|
||||
/// <returns>Awaitable <see cref="Task" /></returns>
|
||||
public static async Task WaitUntil(Func<bool> condition, CancellationToken ct = default)
|
||||
{
|
||||
while (!ct.IsCancellationRequested && !condition())
|
||||
{
|
||||
await Task.Yield();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an awaitable task that blocks while a condition is true, a timeout occurs or the task is canceled.
|
||||
/// </summary>
|
||||
/// <param name="condition">The condition that will perpetuate the block</param>
|
||||
/// <param name="timeout">Timeout, in milliseconds</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the task</param>
|
||||
/// <returns>Awaitable <see cref="Task" /></returns>
|
||||
/// <exception cref="TimeoutException">Thrown after <see cref="timeout" /> milliseconds</exception>
|
||||
public static async Task WaitWhile(Func<bool> condition, int timeout, CancellationToken ct = default)
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
Task waitTask = WaitWhile(condition, cts.Token);
|
||||
Task timeoutTask = Delay(timeout, cts.Token);
|
||||
Task finishedTask = await Task.WhenAny(waitTask, timeoutTask);
|
||||
|
||||
if (!finishedTask.IsCanceled)
|
||||
{
|
||||
cts.Cancel(); // Cancel unfinished task
|
||||
await finishedTask; // Propagate exceptions
|
||||
if (finishedTask == timeoutTask)
|
||||
{
|
||||
throw new TimeoutException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an awaitable task that blocks until a condition is true, a timeout occurs or the task is canceled.
|
||||
/// </summary>
|
||||
/// <param name="condition">The condition that will perpetuate the block</param>
|
||||
/// <param name="timeout">Timeout, in milliseconds</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the task</param>
|
||||
/// <returns>Awaitable <see cref="Task" /></returns>
|
||||
/// <exception cref="TimeoutException">Thrown after <see cref="timeout" /> milliseconds</exception>
|
||||
public static async Task WaitUntil(Func<bool> condition, int timeout, CancellationToken ct = default)
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
Task waitTask = WaitUntil(condition, cts.Token);
|
||||
Task timeoutTask = Delay(timeout, cts.Token);
|
||||
Task finishedTask = await Task.WhenAny(waitTask, timeoutTask);
|
||||
|
||||
if (!finishedTask.IsCanceled)
|
||||
{
|
||||
cts.Cancel(); // Cancel unfinished task
|
||||
await finishedTask; // Propagate exceptions
|
||||
if (finishedTask == timeoutTask)
|
||||
{
|
||||
throw new TimeoutException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an awaitable task that blocks while a condition is true, waiting a certain amount of seconds at maximum. An
|
||||
/// optional action can be called if the task was cancelled or it timed out.
|
||||
/// </summary>
|
||||
/// <param name="condition">The condition that will perpetuate the block</param>
|
||||
/// <param name="duration">The maximum amount of seconds to wait while the condition is true</param>
|
||||
/// <param name="cancelCallback">Optional action to execute if the task was canceled or it timed out</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the task</param>
|
||||
/// <returns>Awaitable <see cref="Task" /></returns>
|
||||
public static async Task WaitWhile(Func<bool> condition, float duration, Action cancelCallback = null, CancellationToken ct = default)
|
||||
{
|
||||
int timeout = Mathf.RoundToInt(duration * 1200f);
|
||||
bool mustCancel;
|
||||
try
|
||||
{
|
||||
await WaitWhile(condition, timeout, ct);
|
||||
mustCancel = ct.IsCancellationRequested;
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
mustCancel = true;
|
||||
}
|
||||
|
||||
if (mustCancel)
|
||||
{
|
||||
cancelCallback?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an awaitable task that blocks until a condition is true, waiting a certain amount of seconds at maximum. An
|
||||
/// optional action can be called if the task was cancelled or it timed out.
|
||||
/// </summary>
|
||||
/// <param name="condition">The condition that will perpetuate the block</param>
|
||||
/// <param name="duration">The maximum amount of seconds to wait while the condition is true</param>
|
||||
/// <param name="cancelCallback">Optional action to execute if the task was canceled or it timed out</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the task</param>
|
||||
/// <returns>Awaitable <see cref="Task" /></returns>
|
||||
public static async Task WaitUntil(Func<bool> condition, float duration, Action cancelCallback = null, CancellationToken ct = default)
|
||||
{
|
||||
int timeout = Mathf.RoundToInt(duration * 1200f);
|
||||
bool mustCancel;
|
||||
try
|
||||
{
|
||||
await WaitUntil(condition, timeout, ct);
|
||||
mustCancel = ct.IsCancellationRequested;
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
mustCancel = true;
|
||||
}
|
||||
|
||||
if (mustCancel)
|
||||
{
|
||||
cancelCallback?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides a one-liner method to await until a task is cancelled.
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns>Awaitable <see cref="Task" /></returns>
|
||||
public static async Task WaitUntilCancelled(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
await Task.Yield();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loops iterating once per frame during a specified amount of time, executing a user-defined action.
|
||||
/// </summary>
|
||||
/// <param name="ct">The cancellation token</param>
|
||||
/// <param name="durationSeconds">Loop duration in seconds</param>
|
||||
/// <param name="loopAction">
|
||||
/// The action performed each frame, which will receive the interpolation [0.0, 1.0] parameter as
|
||||
/// argument.
|
||||
/// </param>
|
||||
/// <param name="easing">The easing used to compute the interpolation parameter over time</param>
|
||||
/// <param name="forceLastT1">
|
||||
/// Will enforce a last iteration with 1.0 interpolation parameter. This will avoid
|
||||
/// having a last step with close than, but not 1.0, interpolation.
|
||||
/// </param>
|
||||
public static async Task Loop(CancellationToken ct,
|
||||
float durationSeconds,
|
||||
Action<float> loopAction,
|
||||
UxrEasing easing = UxrEasing.Linear,
|
||||
bool forceLastT1 = false)
|
||||
{
|
||||
float startTime = Time.time;
|
||||
|
||||
while (Time.time - startTime < durationSeconds)
|
||||
{
|
||||
float t = UxrInterpolator.Interpolate(0.0f, 1.0f, Time.time - startTime, new UxrInterpolationSettings(durationSeconds, 0.0f, easing));
|
||||
loopAction(t);
|
||||
await Task.Yield();
|
||||
}
|
||||
|
||||
if (forceLastT1)
|
||||
{
|
||||
loopAction(1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3fc135711d1b402b87818677827bf293
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
119
Assets/UltimateXR/Runtime/Scripts/Extensions/System/TypeExt.cs
Normal file
119
Assets/UltimateXR/Runtime/Scripts/Extensions/System/TypeExt.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="TypeExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
|
||||
namespace UltimateXR.Extensions.System
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Type" /> extensions.
|
||||
/// </summary>
|
||||
public static class TypeExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="InvalidCastException" /> if the type defined by <paramref name="from" /> can't be casted to
|
||||
/// the type defined by <see cref="to" />.
|
||||
/// </summary>
|
||||
/// <param name="from">Source type</param>
|
||||
/// <param name="to">Destination type</param>
|
||||
/// <exception cref="InvalidCastException">Thrown when the source type can't be casted to the destination type</exception>
|
||||
public static void ThrowIfInvalidCast(Type from, Type to)
|
||||
{
|
||||
if (!IsValidCast(from, to))
|
||||
{
|
||||
throw new InvalidCastException($"{from.Name} is not assignable to {to.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the type defined by <paramref name="from" /> can be casted to the type defined by <see cref="to" />.
|
||||
/// </summary>
|
||||
/// <param name="from">Source type</param>
|
||||
/// <param name="to">Destination type</param>
|
||||
/// <returns>Whether it can be casted</returns>
|
||||
public static bool IsValidCast(Type from, Type to)
|
||||
{
|
||||
return to.IsAssignableFrom(from);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="InvalidCastException" /> if the type defined by <paramref name="self" /> can't be casted to
|
||||
/// the type defined by <see cref="to" />.
|
||||
/// </summary>
|
||||
/// <param name="self">Source type</param>
|
||||
/// <param name="to">Destination type</param>
|
||||
/// <exception cref="InvalidCastException">Thrown when the source type can't be casted to the destination type</exception>
|
||||
public static void ThrowIfCannotCastTo(this Type self, Type to)
|
||||
{
|
||||
ThrowIfInvalidCast(self, to);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the type defined by <paramref name="self" /> can be casted to the type defined by <see cref="to" />.
|
||||
/// </summary>
|
||||
/// <param name="self">Source type</param>
|
||||
/// <param name="to">Destination type</param>
|
||||
/// <returns>Whether it can be casted</returns>
|
||||
public static bool CanCastTo(this Type self, Type to)
|
||||
{
|
||||
return IsValidCast(self, to);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a type given the assembly and the type name.
|
||||
/// </summary>
|
||||
/// <param name="typeName">Name of the type to get</param>
|
||||
/// <param name="assemblyName">Assembly name or null if the same assembly from the caller is used</param>
|
||||
/// <returns>Type or null if there was an error</returns>
|
||||
public static Type GetType(string typeName, string assemblyName)
|
||||
{
|
||||
string fullTypeName = string.IsNullOrEmpty(assemblyName) ? typeName : $"{typeName}, {assemblyName}";
|
||||
return Type.GetType(fullTypeName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string that describes the type given by the type name and the assembly where it is located. If the
|
||||
/// assembly is empty or null, it won't return any assembly information and the type is considered to be in the same
|
||||
/// assembly as UltimateXR.
|
||||
/// </summary>
|
||||
/// <param name="typeName">The name of the type</param>
|
||||
/// <param name="assemblyName">The name of the assembly. Null or empty to ignore assembly information</param>
|
||||
/// <returns>A string in the form of Type or Type, Assembly</returns>
|
||||
public static string GetTypeString(string typeName, string assemblyName)
|
||||
{
|
||||
string assemblyString = string.IsNullOrEmpty(assemblyName) ? string.Empty : $", {assemblyName}";
|
||||
return $"{typeName}{assemblyString}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates a given type which may be in the same or different assembly as the caller.
|
||||
/// </summary>
|
||||
/// <param name="typeName">Name of the type to instantiate</param>
|
||||
/// <param name="assemblyName">Assembly name or null/empty if the same assembly from the caller is used</param>
|
||||
/// <returns>New object or null if there was an error</returns>
|
||||
public static object CreateInstance(string typeName, string assemblyName)
|
||||
{
|
||||
Type type = GetType(typeName, assemblyName);
|
||||
return type == null ? null : Activator.CreateInstance(type, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates a given type which may be in the same or different assembly as the caller.
|
||||
/// </summary>
|
||||
/// <param name="typeName">Name of the type to instantiate</param>
|
||||
/// <param name="assemblyName">Assembly name or null/empty if the same assembly from the caller is used</param>
|
||||
/// <param name="parameters">Optional parameters to call a specific constructor</param>
|
||||
/// <returns>New object or null if there was an error</returns>
|
||||
public static object CreateInstance(string typeName, string assemblyName, params object[] parameters)
|
||||
{
|
||||
Type type = GetType(typeName, assemblyName);
|
||||
return type == null ? null : Activator.CreateInstance(type, parameters);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 00dc2a5d7a294de409ab56c576ee4774
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
3
Assets/UltimateXR/Runtime/Scripts/Extensions/Unity.meta
Normal file
3
Assets/UltimateXR/Runtime/Scripts/Extensions/Unity.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 215bdf8c02fd4e738cb48eba1d6102d0
|
||||
timeCreated: 1620807507
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bbf05f34d7400dd49b8f821cba8be7cb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,113 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="AudioClipExt.PcmData.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using UltimateXR.Extensions.System;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity.Audio
|
||||
{
|
||||
public static partial class AudioClipExt
|
||||
{
|
||||
#region Private Types & Data
|
||||
|
||||
/// <summary>
|
||||
/// Container of PCM audio data.
|
||||
/// </summary>
|
||||
private readonly struct PcmData
|
||||
{
|
||||
#region Public Types & Data
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sample data.
|
||||
/// </summary>
|
||||
public float[] Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sample count.
|
||||
/// </summary>
|
||||
public int Length { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of audio channels.
|
||||
/// </summary>
|
||||
public int Channels { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sample rate in Hz.
|
||||
/// </summary>
|
||||
public int SampleRate { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors & Finalizer
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
/// <param name="value">Sample data</param>
|
||||
/// <param name="channels">Audio channel count</param>
|
||||
/// <param name="sampleRate">Sample rate in Hz</param>
|
||||
private PcmData(float[] value, int channels, int sampleRate)
|
||||
{
|
||||
Value = value;
|
||||
Length = value.Length;
|
||||
Channels = channels;
|
||||
SampleRate = sampleRate;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="PcmData" /> object from a byte data array.
|
||||
/// </summary>
|
||||
/// <param name="bytes">Byte data array with the PCM header and sample data</param>
|
||||
/// <returns><see cref="PcmData" /> object with the audio data</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">The PCM header contains invalid data</exception>
|
||||
public static PcmData FromBytes(byte[] bytes)
|
||||
{
|
||||
bytes.ThrowIfNull(nameof(bytes));
|
||||
|
||||
PcmHeader pcmHeader = PcmHeader.FromBytes(bytes);
|
||||
if (pcmHeader.BitDepth != 16 && pcmHeader.BitDepth != 32 && pcmHeader.BitDepth != 8)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(pcmHeader.BitDepth), pcmHeader.BitDepth, "Supported values are: 8, 16, 32");
|
||||
}
|
||||
|
||||
float[] samples = new float[pcmHeader.AudioSampleCount];
|
||||
for (int i = 0; i < samples.Length; ++i)
|
||||
{
|
||||
int byteIndex = pcmHeader.AudioStartIndex + i * pcmHeader.AudioSampleSize;
|
||||
float rawSample;
|
||||
switch (pcmHeader.BitDepth)
|
||||
{
|
||||
case 8:
|
||||
rawSample = bytes[byteIndex];
|
||||
break;
|
||||
|
||||
case 16:
|
||||
rawSample = BitConverter.ToInt16(bytes, byteIndex);
|
||||
break;
|
||||
|
||||
case 32:
|
||||
rawSample = BitConverter.ToInt32(bytes, byteIndex);
|
||||
break;
|
||||
|
||||
default: throw new ArgumentOutOfRangeException(nameof(pcmHeader.BitDepth), pcmHeader.BitDepth, "Supported values are: 8, 16, 32");
|
||||
}
|
||||
|
||||
samples[i] = pcmHeader.NormalizeSample(rawSample); // normalize sample between [-1f, 1f]
|
||||
}
|
||||
|
||||
return new PcmData(samples, pcmHeader.Channels, pcmHeader.SampleRate);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bff70ee0e42241268ac7cd1479f8f5cc
|
||||
timeCreated: 1630064761
|
||||
@@ -0,0 +1,195 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="AudioClipExt.PcmHeader.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity.Audio
|
||||
{
|
||||
public static partial class AudioClipExt
|
||||
{
|
||||
#region Private Types & Data
|
||||
|
||||
/// <summary>
|
||||
/// Describes a PCM audio data header.
|
||||
/// </summary>
|
||||
private readonly struct PcmHeader
|
||||
{
|
||||
#region Public Types & Data
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bits per audio sample.
|
||||
/// </summary>
|
||||
public int BitDepth { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the audio total sample size in bytes.
|
||||
/// </summary>
|
||||
public int AudioSampleSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of audio samples.
|
||||
/// </summary>
|
||||
public int AudioSampleCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of audio channels.
|
||||
/// </summary>
|
||||
public ushort Channels { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sample rate in audio samples per second.
|
||||
/// </summary>
|
||||
public int SampleRate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data index where the audio data starts.
|
||||
/// </summary>
|
||||
public int AudioStartIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the audio data bytes per second.
|
||||
/// </summary>
|
||||
public int ByteRate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data block alignment.
|
||||
/// </summary>
|
||||
public ushort BlockAlign { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors & Finalizer
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
/// <param name="bitDepth">Sample bit size</param>
|
||||
/// <param name="audioSize">Total audio data size in bytes</param>
|
||||
/// <param name="audioStartIndex">Index where the audio sample data starts</param>
|
||||
/// <param name="channels">The number of audio channels</param>
|
||||
/// <param name="sampleRate">The number of samples per second</param>
|
||||
/// <param name="byteRate">The number of bytes per second</param>
|
||||
/// <param name="blockAlign">The block alignment</param>
|
||||
private PcmHeader(int bitDepth,
|
||||
int audioSize,
|
||||
int audioStartIndex,
|
||||
ushort channels,
|
||||
int sampleRate,
|
||||
int byteRate,
|
||||
ushort blockAlign)
|
||||
{
|
||||
BitDepth = bitDepth;
|
||||
_negativeDepth = Mathf.Pow(2f, BitDepth - 1f);
|
||||
_positiveDepth = _negativeDepth - 1f;
|
||||
|
||||
AudioSampleSize = bitDepth / 8;
|
||||
AudioSampleCount = Mathf.FloorToInt(audioSize / (float)AudioSampleSize);
|
||||
AudioStartIndex = audioStartIndex;
|
||||
|
||||
Channels = channels;
|
||||
SampleRate = sampleRate;
|
||||
ByteRate = byteRate;
|
||||
BlockAlign = blockAlign;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="PcmHeader" /> object reading from a byte array.
|
||||
/// </summary>
|
||||
/// <param name="pcmBytes">Source byte array</param>
|
||||
/// <returns><see cref="PcmHeader" /> object</returns>
|
||||
public static PcmHeader FromBytes(byte[] pcmBytes)
|
||||
{
|
||||
using var memoryStream = new MemoryStream(pcmBytes);
|
||||
return FromStream(memoryStream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="PcmHeader" /> object reading from a data stream.
|
||||
/// </summary>
|
||||
/// <param name="pcmStream">Source data</param>
|
||||
/// <returns><see cref="PcmHeader" /> object</returns>
|
||||
public static PcmHeader FromStream(Stream pcmStream)
|
||||
{
|
||||
pcmStream.Position = SizeIndex;
|
||||
using BinaryReader reader = new BinaryReader(pcmStream);
|
||||
|
||||
int headerSize = reader.ReadInt32(); // 16
|
||||
ushort audioFormatCode = reader.ReadUInt16(); // 20
|
||||
|
||||
string audioFormat = GetAudioFormatFromCode(audioFormatCode);
|
||||
if (audioFormatCode != 1 && audioFormatCode == 65534)
|
||||
{
|
||||
// Only uncompressed PCM wav files are supported.
|
||||
throw new ArgumentOutOfRangeException(nameof(pcmStream),
|
||||
$"Detected format code '{audioFormatCode}' {audioFormat}, but only PCM and WaveFormatExtensible uncompressed formats are currently supported.");
|
||||
}
|
||||
|
||||
ushort channelCount = reader.ReadUInt16(); // 22
|
||||
int sampleRate = reader.ReadInt32(); // 24
|
||||
int byteRate = reader.ReadInt32(); // 28
|
||||
ushort blockAlign = reader.ReadUInt16(); // 32
|
||||
ushort bitDepth = reader.ReadUInt16(); //34
|
||||
|
||||
pcmStream.Position = SizeIndex + headerSize + 2 * sizeof(int); // Header end index
|
||||
int audioSize = reader.ReadInt32(); // Audio size index
|
||||
|
||||
return new PcmHeader(bitDepth, audioSize, (int)pcmStream.Position, channelCount, sampleRate, byteRate, blockAlign); // audio start index
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a raw audio sample.
|
||||
/// </summary>
|
||||
/// <param name="rawSample">Audio sample to normalize</param>
|
||||
/// <returns>Normalized audio sample</returns>
|
||||
public float NormalizeSample(float rawSample)
|
||||
{
|
||||
float sampleDepth = rawSample < 0 ? _negativeDepth : _positiveDepth;
|
||||
return rawSample / sampleDepth;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
|
||||
/// <summary>
|
||||
/// Gets the audio format string from the numerical code.
|
||||
/// </summary>
|
||||
/// <param name="code">Numerical audio format code</param>
|
||||
/// <returns>Audio format string</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">The code is not valid</exception>
|
||||
private static string GetAudioFormatFromCode(ushort code)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case 1: return "PCM";
|
||||
case 2: return "ADPCM";
|
||||
case 3: return "IEEE";
|
||||
case 7: return "?-law";
|
||||
case 65534: return "WaveFormatExtensible";
|
||||
default: throw new ArgumentOutOfRangeException(nameof(code), code, "Unknown wav code format.");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Types & Data
|
||||
|
||||
private const int SizeIndex = 16;
|
||||
|
||||
private readonly float _positiveDepth;
|
||||
private readonly float _negativeDepth;
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2bfbb7d822e34d8f92097fa24960e1a6
|
||||
timeCreated: 1630065461
|
||||
@@ -0,0 +1,138 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="AudioClipExt.StreamedAudioClip.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using System.IO;
|
||||
using UltimateXR.Extensions.System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity.Audio
|
||||
{
|
||||
public static partial class AudioClipExt
|
||||
{
|
||||
#region Public Types & Data
|
||||
|
||||
/// <summary>
|
||||
/// Describes a PCM audio clip.
|
||||
/// </summary>
|
||||
public sealed class StreamedPcmClip : IDisposable
|
||||
{
|
||||
#region Public Types & Data
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="AudioClip" /> described by the object.
|
||||
/// </summary>
|
||||
public AudioClip InnerClip { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors & Finalizer
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
/// <param name="pcmStream">PCM data</param>
|
||||
/// <param name="clipName">Name assigned to the audio clip</param>
|
||||
/// <param name="header">PCM data header</param>
|
||||
private StreamedPcmClip(Stream pcmStream, string clipName, in PcmHeader header)
|
||||
{
|
||||
_pcmHeader = header;
|
||||
_pcmStream = pcmStream;
|
||||
_pcmReader = new BinaryReader(pcmStream);
|
||||
|
||||
InnerClip = AudioClip.Create(clipName, header.AudioSampleCount, header.Channels, header.SampleRate, true, OnPcmRead, OnPcmSetPosition);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implicit IDisposable
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_pcmReader.Dispose();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="StreamedPcmClip" /> object from a data stream.
|
||||
/// </summary>
|
||||
/// <param name="pcmStream">Source data stream</param>
|
||||
/// <param name="clipName">Name that will be assigned to the clip</param>
|
||||
/// <returns><see cref="StreamedPcmClip" /> describing the PCM audio clip</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">The bit depth is not supported</exception>
|
||||
public static StreamedPcmClip Create(Stream pcmStream, string clipName = "pcm")
|
||||
{
|
||||
pcmStream.ThrowIfNull(nameof(pcmStream));
|
||||
clipName.ThrowIfNullOrWhitespace(nameof(clipName));
|
||||
var pcmHeader = PcmHeader.FromStream(pcmStream);
|
||||
if (pcmHeader.BitDepth != 16 && pcmHeader.BitDepth != 32 && pcmHeader.BitDepth != 8)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(pcmHeader.BitDepth), pcmHeader.BitDepth, "Supported values are: 8, 16, 32");
|
||||
}
|
||||
|
||||
return new StreamedPcmClip(pcmStream, clipName, in pcmHeader);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Trigger Methods
|
||||
|
||||
/// <summary>
|
||||
/// PCM reader callback.
|
||||
/// </summary>
|
||||
/// <param name="data">Source data</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Unsupported audio bit depth</exception>
|
||||
private void OnPcmRead(float[] data)
|
||||
{
|
||||
for (int i = 0; i < data.Length && _pcmStream.Position < _pcmStream.Length; ++i)
|
||||
{
|
||||
float rawSample;
|
||||
switch (_pcmHeader.AudioSampleSize)
|
||||
{
|
||||
case 1:
|
||||
rawSample = _pcmReader.ReadByte();
|
||||
break;
|
||||
|
||||
case 2:
|
||||
rawSample = _pcmReader.ReadInt16();
|
||||
break;
|
||||
|
||||
case 3:
|
||||
rawSample = _pcmReader.ReadInt32();
|
||||
break;
|
||||
|
||||
default: throw new ArgumentOutOfRangeException(nameof(_pcmHeader.BitDepth), _pcmHeader.BitDepth, "Supported values are: 8, 16, 32");
|
||||
}
|
||||
data[i] = _pcmHeader.NormalizeSample(rawSample); // needs to be scaled to be within the range of - 1.0f to 1.0f.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PCM reader positioning callback.
|
||||
/// </summary>
|
||||
/// <param name="newPosition">New index where to position the read cursor</param>
|
||||
private void OnPcmSetPosition(int newPosition)
|
||||
{
|
||||
_pcmStream.Position = _pcmHeader.AudioStartIndex + newPosition * _pcmHeader.AudioSampleSize;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Types & Data
|
||||
|
||||
private readonly Stream _pcmStream;
|
||||
private readonly BinaryReader _pcmReader;
|
||||
private readonly PcmHeader _pcmHeader;
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f3e1a097a2b4481b486c9cc54c4a03c
|
||||
timeCreated: 1629966369
|
||||
@@ -0,0 +1,269 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="AudioClipExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UltimateXR.Exceptions;
|
||||
using UltimateXR.Extensions.System;
|
||||
using UltimateXR.Extensions.System.IO;
|
||||
using UltimateXR.Extensions.Unity.IO;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Audio extensions.
|
||||
/// </summary>
|
||||
public static partial class AudioClipExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Ubiquitously plays an <see cref="AudioClip" />.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This function creates an <see cref="AudioSource" /> but automatically disposes of it once the clip has finished
|
||||
/// playing.
|
||||
/// </remarks>
|
||||
/// <param name="self">Reference to the sound clip file that will be played.</param>
|
||||
/// <param name="volume">How loud the sound is at a distance of one world unit (one meter) [0.0, 1.0].</param>
|
||||
/// <param name="delay">Delay time specified in seconds.</param>
|
||||
/// <param name="pitch">
|
||||
/// Amount of change in pitch due to slowdown/speed up of the Audio Clip. Value 1 is normal playback speed.
|
||||
/// </param>
|
||||
/// <param name="offsetSeconds">Start offset in seconds</param>
|
||||
/// <returns>The just created temporal <see cref="AudioSource" />.</returns>
|
||||
/// <seealso cref="AudioSourceExt.PlayClip" />
|
||||
public static AudioSource PlayClip(AudioClip self,
|
||||
float volume = 1.0f,
|
||||
float delay = 0.0f,
|
||||
float pitch = 1.0f,
|
||||
float offsetSeconds = 0.0f)
|
||||
{
|
||||
return AudioSourceExt.PlayClip(self, volume, delay, pitch, offsetSeconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays an AudioClip at a given position in world space.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This function creates an <see cref="AudioSource" /> but automatically disposes of it once the clip has finished
|
||||
/// playing.
|
||||
/// </remarks>
|
||||
/// <param name="self">Reference to the sound clip file that will be played.</param>
|
||||
/// <param name="point">Position in world space from which sound originates.</param>
|
||||
/// <param name="volume">How loud the sound is at a distance of one world unit (one meter) [0.0, 1.0].</param>
|
||||
/// <param name="delay">Delay time specified in seconds.</param>
|
||||
/// <param name="pitch">
|
||||
/// Amount of change in pitch due to slowdown/speed up of the Audio Clip. Value 1 is normal playback
|
||||
/// speed.
|
||||
/// </param>
|
||||
/// <param name="spatialBlend">Sets how much the 3D engine has an effect on the audio source [0.0, 1.0].</param>
|
||||
/// <param name="offsetSeconds">Start offset in seconds</param>
|
||||
/// <returns>The just created temporal <see cref="AudioSource" />.</returns>
|
||||
/// <seealso cref="AudioSourceExt.PlayClipAtPoint" />
|
||||
public static AudioSource PlayClipAtPoint(AudioClip self,
|
||||
Vector3 point,
|
||||
float volume = 1.0f,
|
||||
float delay = 0.0f,
|
||||
float pitch = 1.0f,
|
||||
float spatialBlend = AudioSourceExt.SpatialBlend3D,
|
||||
float offsetSeconds = 0.0f)
|
||||
{
|
||||
return AudioSourceExt.PlayClipAtPoint(self, point, volume, delay, pitch, spatialBlend, offsetSeconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous and ubiquitously plays the <see cref="AudioClip" />.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This function creates an <see cref="AudioSource" /> but automatically disposes of it once the clip has finished
|
||||
/// playing.
|
||||
/// </remarks>
|
||||
/// <param name="self">Reference to the sound clip file that will be played.</param>
|
||||
/// <param name="volume">How loud the sound is at a distance of one world unit (one meter) [0.0, 1.0].</param>
|
||||
/// <param name="delay">Delay time specified in seconds.</param>
|
||||
/// <param name="pitch">
|
||||
/// Amount of change in pitch due to slowdown/speed up of the Audio Clip. Value 1 is normal playback
|
||||
/// speed.
|
||||
/// </param>
|
||||
/// <param name="offsetSeconds">Start offset in seconds</param>
|
||||
/// <param name="ct"><see cref="CancellationToken" /> to stop playing.</param>
|
||||
/// <returns>An awaitable <see cref="Task" />.</returns>
|
||||
/// <seealso cref="AudioSourceExt.PlayClipAsync" />
|
||||
public static Task PlayAsync(this AudioClip self,
|
||||
float volume = 1.0f,
|
||||
float delay = 0.0f,
|
||||
float pitch = 1.0f,
|
||||
float offsetSeconds = 0.0f,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return AudioSourceExt.PlayClipAsync(self, volume, delay, pitch, offsetSeconds, ct);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously plays the <see cref="AudioClip" /> at a given position in world space.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This function creates an <see cref="AudioSource" /> but automatically disposes of it once the clip has finished
|
||||
/// playing.
|
||||
/// </remarks>
|
||||
/// <param name="self">Reference to the sound clip file that will be played.</param>
|
||||
/// <param name="point">Position in world space from which sound originates.</param>
|
||||
/// <param name="volume">How loud the sound is at a distance of one world unit (one meter) [0.0, 1.0].</param>
|
||||
/// <param name="delay">Delay time specified in seconds.</param>
|
||||
/// <param name="pitch">
|
||||
/// Amount of change in pitch due to slowdown/speed up of the Audio Clip. Value 1 is normal playback
|
||||
/// speed.
|
||||
/// </param>
|
||||
/// <param name="spatialBlend">Sets how much the 3D engine has an effect on the audio source [0.0, 1.0].</param>
|
||||
/// <param name="offsetSeconds">Start offset in seconds</param>
|
||||
/// <param name="ct"><see cref="CancellationToken" /> to stop playing.</param>
|
||||
/// <returns>An awaitable <see cref="Task" />.</returns>
|
||||
/// <seealso cref="AudioSourceExt.PlayClipAtPointAsync" />
|
||||
public static Task PlayAtPointAsync(this AudioClip self,
|
||||
Vector3 point,
|
||||
float volume = 1.0f,
|
||||
float delay = 0.0f,
|
||||
float pitch = 1.0f,
|
||||
float spatialBlend = AudioSourceExt.SpatialBlend3D,
|
||||
float offsetSeconds = 0.0f,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return AudioSourceExt.PlayClipAtPointAsync(self, point, volume, delay, pitch, spatialBlend, offsetSeconds, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="AudioClip" /> from a PCM stream.
|
||||
/// </summary>
|
||||
/// <param name="sourceStream">The source stream</param>
|
||||
/// <param name="clipName">The name assigned to the clip</param>
|
||||
/// <returns>The <see cref="AudioClip" /> object</returns>
|
||||
public static AudioClip FromPcmStream(Stream sourceStream, string clipName = "pcm")
|
||||
{
|
||||
clipName.ThrowIfNullOrWhitespace(nameof(clipName));
|
||||
byte[] bytes = new byte[sourceStream.Length];
|
||||
sourceStream.Read(bytes, 0, bytes.Length);
|
||||
return FromPcmBytes(bytes, clipName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="AudioClip" /> from a PCM stream asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="sourceStream">The source stream</param>
|
||||
/// <param name="clipName">The name assigned to the clip</param>
|
||||
/// <param name="ct">The optional cancellation token, to cancel the task</param>
|
||||
/// <returns>An awaitable task that returns the <see cref="AudioClip" /> object</returns>
|
||||
public static async Task<AudioClip> FromPcmStreamAsync(Stream sourceStream, string clipName = "pcm", CancellationToken ct = default)
|
||||
{
|
||||
clipName.ThrowIfNullOrWhitespace(nameof(clipName));
|
||||
byte[] bytes = new byte[sourceStream.Length];
|
||||
await sourceStream.ReadAsync(bytes, 0, bytes.Length, ct);
|
||||
return await FromPcmBytesAsync(bytes, clipName, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="AudioClip" /> from a PCM byte array.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The source data</param>
|
||||
/// <param name="clipName">The name assigned to the clip</param>
|
||||
/// <returns>The <see cref="AudioClip" /> object</returns>
|
||||
public static AudioClip FromPcmBytes(byte[] bytes, string clipName = "pcm")
|
||||
{
|
||||
clipName.ThrowIfNullOrWhitespace(nameof(clipName));
|
||||
var pcmData = PcmData.FromBytes(bytes);
|
||||
var audioClip = AudioClip.Create(clipName, pcmData.Length, pcmData.Channels, pcmData.SampleRate, false);
|
||||
audioClip.SetData(pcmData.Value, 0);
|
||||
return audioClip;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="AudioClip" /> from a PCM byte array asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The source data</param>
|
||||
/// <param name="clipName">The name assigned to the clip</param>
|
||||
/// <param name="ct">The optional cancellation token, to cancel the task</param>
|
||||
/// <returns>An awaitable task that returns the <see cref="AudioClip" /> object</returns>
|
||||
public static async Task<AudioClip> FromPcmBytesAsync(byte[] bytes, string clipName = "pcm", CancellationToken ct = default)
|
||||
{
|
||||
clipName.ThrowIfNullOrWhitespace(nameof(clipName));
|
||||
var pcmData = await Task.Run(() => PcmData.FromBytes(bytes), ct);
|
||||
var audioClip = AudioClip.Create(clipName, pcmData.Length, pcmData.Channels, pcmData.SampleRate, false);
|
||||
audioClip.SetData(pcmData.Value, 0);
|
||||
return audioClip;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously reads and loads an <see cref="AudioClip" /> into memory from a given <paramref name="uri" />
|
||||
/// </summary>
|
||||
/// <param name="uri">Full path for <see cref="AudioClip" /> file</param>
|
||||
/// <param name="ct">The optional cancellation token, to cancel the task</param>
|
||||
/// <returns>Loaded <see cref="AudioClip" /></returns>
|
||||
/// <exception cref="HttpUwrException">
|
||||
/// HttpError flag is on
|
||||
/// </exception>
|
||||
/// <exception cref="NetUwrException">
|
||||
/// NetworkError flag is on
|
||||
/// </exception>
|
||||
/// <exception cref="OperationCanceledException">
|
||||
/// The task was canceled using <paramref name="ct" />
|
||||
/// </exception>
|
||||
public static Task<AudioClip> FromFile(string uri, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
uri.ThrowIfNullOrWhitespace(nameof(uri));
|
||||
|
||||
try
|
||||
{
|
||||
return UnityWebRequestExt.LoadAudioClipAsync(uri, ct);
|
||||
}
|
||||
catch (UwrException e)
|
||||
{
|
||||
throw new FileNotFoundException(e.Message, uri, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously reads and loads an <see cref="AudioClip" /> into memory from a given <paramref name="uri" />
|
||||
/// pointing to a file with PCM bytes.
|
||||
/// </summary>
|
||||
/// <param name="uri">Full path with the PCM bytes</param>
|
||||
/// <param name="ct">Optional cancellation token to cancel the task</param>
|
||||
/// <returns>Loaded <see cref="AudioClip" /></returns>
|
||||
/// <exception cref="HttpUwrException">
|
||||
/// HttpError flag is on
|
||||
/// </exception>
|
||||
/// <exception cref="NetUwrException">
|
||||
/// NetworkError flag is on
|
||||
/// </exception>
|
||||
/// <exception cref="OperationCanceledException">
|
||||
/// The task was canceled using <paramref name="ct" />
|
||||
/// </exception>
|
||||
public static async Task<AudioClip> FromPcmFile(string uri, CancellationToken ct = default)
|
||||
{
|
||||
string fileName = Path.GetFileNameWithoutExtension(uri);
|
||||
byte[] bytes = await FileExt.Read(uri, ct);
|
||||
return await FromPcmBytesAsync(bytes, fileName, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="StreamedPcmClip" /> object from a stream containing PCM data.
|
||||
/// </summary>
|
||||
/// <param name="pcmStream">PCM data</param>
|
||||
/// <param name="clipName">The name that will be assigned to the clip</param>
|
||||
/// <returns><see cref="StreamedPcmClip" /> object</returns>
|
||||
public static StreamedPcmClip CreatePcmStreamed(Stream pcmStream, string clipName = "pcm")
|
||||
{
|
||||
return StreamedPcmClip.Create(pcmStream, clipName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 543e6a1c3d51e984f8ef22cd13008d57
|
||||
timeCreated: 1620807376
|
||||
@@ -0,0 +1,277 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="AudioSourceExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UltimateXR.Extensions.System;
|
||||
using UltimateXR.Extensions.System.Threading;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="AudioSource" /> extensions.
|
||||
/// </summary>
|
||||
public static class AudioSourceExt
|
||||
{
|
||||
#region Public Types & Data
|
||||
|
||||
/// <summary>
|
||||
/// Default spatial blend for 3D positioned audio.
|
||||
/// </summary>
|
||||
public const float SpatialBlend3D = 0.9f;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Ubiquitously plays an <see cref="AudioClip" />.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This function creates an <see cref="AudioSource" /> but automatically disposes of it once the clip has finished
|
||||
/// playing.
|
||||
/// </remarks>
|
||||
/// <param name="clip">Reference to the sound clip file that will be played.</param>
|
||||
/// <param name="volume">How loud the sound is at a distance of one world unit (one meter) [0.0, 1.0].</param>
|
||||
/// <param name="delay">Delay time specified in seconds.</param>
|
||||
/// <param name="pitch">
|
||||
/// Amount of change in pitch due to slowdown/speed up of the Audio Clip. Value 1 is normal playback
|
||||
/// speed.
|
||||
/// </param>
|
||||
/// <param name="offsetSeconds">Start offset in seconds</param>
|
||||
/// <returns>The just created temporal <see cref="AudioSource" />.</returns>
|
||||
public static AudioSource PlayClip(AudioClip clip,
|
||||
float volume = 1.0f,
|
||||
float delay = 0.0f,
|
||||
float pitch = 1.0f,
|
||||
float offsetSeconds = 0.0f)
|
||||
{
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
throw new InvalidOperationException("Playback is only allowed while playing.");
|
||||
}
|
||||
clip.ThrowIfNull(nameof(clip));
|
||||
volume = Mathf.Clamp01(volume);
|
||||
pitch = Mathf.Clamp01(pitch);
|
||||
|
||||
var gameObject = new GameObject($"{nameof(AudioSourceExt)}_{nameof(PlayClip)}_{clip.name}");
|
||||
var audioSource = gameObject.AddComponent<AudioSource>();
|
||||
audioSource.clip = clip;
|
||||
audioSource.volume = volume;
|
||||
audioSource.pitch = pitch;
|
||||
audioSource.spatialBlend = SpatialBlendUbiquitous;
|
||||
|
||||
if (offsetSeconds - delay >= clip.length)
|
||||
{
|
||||
audioSource.Stop();
|
||||
Object.Destroy(gameObject, 1.0f);
|
||||
return audioSource;
|
||||
}
|
||||
|
||||
if (delay > offsetSeconds)
|
||||
{
|
||||
audioSource.PlayDelayed(delay - offsetSeconds);
|
||||
}
|
||||
else
|
||||
{
|
||||
audioSource.Play();
|
||||
audioSource.time = offsetSeconds - delay;
|
||||
}
|
||||
|
||||
float duration = (delay + clip.length - offsetSeconds) * (Time.timeScale < 0.00999999977648258 ? 0.01f : Time.timeScale);
|
||||
Object.Destroy(gameObject, duration);
|
||||
return audioSource;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays an AudioClip at a given position in world space.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This function creates an <see cref="AudioSource" /> but automatically disposes of it once the clip has finished
|
||||
/// playing.
|
||||
/// </remarks>
|
||||
/// <param name="clip">Reference to the sound clip file that will be played.</param>
|
||||
/// <param name="point">Position in world space from which sound originates.</param>
|
||||
/// <param name="volume">How loud the sound is at a distance of one world unit (one meter) [0.0, 1.0].</param>
|
||||
/// <param name="delay">Delay time specified in seconds.</param>
|
||||
/// <param name="pitch">
|
||||
/// Amount of change in pitch due to slowdown/speed up of the Audio Clip. Value 1 is normal playback
|
||||
/// speed.
|
||||
/// </param>
|
||||
/// <param name="spatialBlend">Sets how much the 3D engine has an effect on the audio source [0.0, 1.0].</param>
|
||||
/// <param name="offsetSeconds">Start offset in seconds</param>
|
||||
/// <returns>The just created temporal <see cref="AudioSource" />.</returns>
|
||||
/// <seealso cref="AudioSource.PlayClipAtPoint(AudioClip, Vector3, float)" />
|
||||
public static AudioSource PlayClipAtPoint(AudioClip clip,
|
||||
Vector3 point,
|
||||
float volume = 1.0f,
|
||||
float delay = 0.0f,
|
||||
float pitch = 1.0f,
|
||||
float spatialBlend = SpatialBlend3D,
|
||||
float offsetSeconds = 0.0f)
|
||||
{
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
throw new InvalidOperationException("Playback is only allowed while playing.");
|
||||
}
|
||||
|
||||
clip.ThrowIfNull(nameof(clip));
|
||||
volume = Mathf.Clamp01(volume);
|
||||
spatialBlend = Mathf.Clamp01(spatialBlend);
|
||||
|
||||
var gameObject = new GameObject($"{nameof(AudioSourceExt)}_{nameof(PlayClipAtPoint)}_{clip.name}") { transform = { position = point } };
|
||||
var audioSource = gameObject.AddComponent<AudioSource>();
|
||||
audioSource.clip = clip;
|
||||
audioSource.volume = volume;
|
||||
audioSource.pitch = pitch;
|
||||
audioSource.spatialBlend = spatialBlend;
|
||||
|
||||
if (offsetSeconds - delay >= clip.length)
|
||||
{
|
||||
audioSource.Stop();
|
||||
Object.Destroy(gameObject, 1.0f);
|
||||
return audioSource;
|
||||
}
|
||||
|
||||
audioSource.Play();
|
||||
offsetSeconds = Mathf.Max(offsetSeconds, 0.0f);
|
||||
|
||||
if (delay > offsetSeconds)
|
||||
{
|
||||
audioSource.PlayDelayed(delay - offsetSeconds);
|
||||
}
|
||||
else
|
||||
{
|
||||
audioSource.Play();
|
||||
audioSource.time = offsetSeconds - delay;
|
||||
}
|
||||
|
||||
float duration = (delay + clip.length - offsetSeconds) * (Time.timeScale < 0.00999999977648258 ? 0.01f : Time.timeScale);
|
||||
Object.Destroy(gameObject, duration);
|
||||
return audioSource;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous and ubiquitously plays an <see cref="AudioClip" />.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This function creates an <see cref="AudioSource" /> but automatically disposes of it once the clip has finished
|
||||
/// playing.
|
||||
/// </remarks>
|
||||
/// <param name="clip">Reference to the sound clip file that will be played.</param>
|
||||
/// <param name="volume">How loud the sound is at a distance of one world unit (one meter) [0.0, 1.0].</param>
|
||||
/// <param name="delay">Delay time specified in seconds.</param>
|
||||
/// <param name="pitch">
|
||||
/// Amount of change in pitch due to slowdown/speed up of the Audio Clip. Value 1 is normal playback
|
||||
/// speed.
|
||||
/// </param>
|
||||
/// <param name="offsetSeconds">Start offset in seconds</param>
|
||||
/// <param name="ct"><see cref="CancellationToken" /> to stop playing.</param>
|
||||
/// <returns>An awaitable <see cref="Task" />.</returns>
|
||||
public static async Task PlayClipAsync(AudioClip clip,
|
||||
float volume = 1.0f,
|
||||
float delay = 0.0f,
|
||||
float pitch = 1.0f,
|
||||
float offsetSeconds = 0.0f,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
throw new InvalidOperationException("Playback is only allowed while playing.");
|
||||
}
|
||||
if (offsetSeconds >= clip.length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
offsetSeconds = Mathf.Max(offsetSeconds, 0.0f);
|
||||
|
||||
float duration = (delay + clip.length - offsetSeconds) * (Time.timeScale < 0.00999999977648258 ? 0.01f : Time.timeScale);
|
||||
AudioSource audioSource = PlayClip(clip, volume, delay, pitch, offsetSeconds);
|
||||
await TaskExt.Delay(duration, ct);
|
||||
|
||||
if (ct.IsCancellationRequested && audioSource != null)
|
||||
{
|
||||
audioSource.Stop();
|
||||
Object.Destroy(audioSource.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously plays an <see cref="AudioClip" /> at a given position in world space.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This function creates an <see cref="AudioSource" /> but automatically disposes of it once the clip has finished
|
||||
/// playing.
|
||||
/// </remarks>
|
||||
/// <param name="clip">Reference to the sound clip file that will be played.</param>
|
||||
/// <param name="point">Position in world space from which sound originates.</param>
|
||||
/// <param name="volume">How loud the sound is at a distance of one world unit (one meter) [0.0, 1.0].</param>
|
||||
/// <param name="delay">Delay time specified in seconds.</param>
|
||||
/// <param name="pitch">
|
||||
/// Amount of change in pitch due to slowdown/speed up of the Audio Clip. Value 1 is normal playback
|
||||
/// speed.
|
||||
/// </param>
|
||||
/// <param name="spatialBlend">Sets how much the 3D engine has an effect on the audio source [0.0, 1.0].</param>
|
||||
/// <param name="offsetSeconds">Start offset in seconds</param>
|
||||
/// <param name="ct"><see cref="CancellationToken" /> to stop playing.</param>
|
||||
/// <returns>An awaitable <see cref="Task" />.</returns>
|
||||
public static async Task PlayClipAtPointAsync(AudioClip clip,
|
||||
Vector3 point,
|
||||
float volume = 1.0f,
|
||||
float delay = 0.0f,
|
||||
float pitch = 1.0f,
|
||||
float spatialBlend = SpatialBlend3D,
|
||||
float offsetSeconds = 0.0f,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
throw new InvalidOperationException("Playback is only allowed while playing.");
|
||||
}
|
||||
|
||||
if (offsetSeconds >= clip.length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
offsetSeconds = Mathf.Max(offsetSeconds, 0.0f);
|
||||
|
||||
float duration = (delay + clip.length - offsetSeconds) * (Time.timeScale < 0.00999999977648258 ? 0.01f : Time.timeScale);
|
||||
AudioSource audioSource = PlayClipAtPoint(clip, point, volume, delay, pitch, spatialBlend, offsetSeconds);
|
||||
|
||||
await TaskExt.Delay(duration, ct);
|
||||
|
||||
if (ct.IsCancellationRequested && audioSource != null)
|
||||
{
|
||||
audioSource.Stop();
|
||||
Object.Destroy(audioSource.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Types & Data
|
||||
|
||||
/// <summary>
|
||||
/// Spatial blend for ubiquitous playback.
|
||||
/// </summary>
|
||||
private const float SpatialBlendUbiquitous = 0f;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5c9177b2f45a03b43867e37e8ea7e869
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,44 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="BehaviourExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using UltimateXR.Core;
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace UltimateXR.Extensions.Unity
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Component" /> extensions.
|
||||
/// </summary>
|
||||
public static class BehaviourExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Controls the enabled state, using serialized properties when called from the Unity Editor to support Undo
|
||||
/// correctly.
|
||||
/// </summary>
|
||||
/// <param name="self">The behaviour to enable or disable</param>
|
||||
/// <param name="enabled">Whether to enable the behaviour or disable it</param>
|
||||
public static void SetEnabled(this Behaviour self, bool enabled)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (Application.isEditor && !Application.isPlaying)
|
||||
{
|
||||
SerializedObject so = new SerializedObject(self);
|
||||
so.FindProperty(UxrConstants.Editor.PropertyBehaviourEnabled).boolValue = enabled;
|
||||
so.ApplyModifiedProperties();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
self.enabled = enabled;
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 576dca578bb9cf4438d2736f2cfde08a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,316 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="ComponentExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UltimateXR.Core;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace UltimateXR.Extensions.Unity
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Component" /> extensions.
|
||||
/// </summary>
|
||||
public static class ComponentExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Controls whether to show a given component in the inspector.
|
||||
/// </summary>
|
||||
/// <param name="self">The component to show</param>
|
||||
/// <param name="show">Whether to show the component or now</param>
|
||||
public static void ShowInInspector(this Component self, bool show = true)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (Application.isEditor)
|
||||
{
|
||||
SerializedObject so = new SerializedObject(self);
|
||||
SerializedProperty prop = so.FindProperty(UxrConstants.Editor.PropertyObjectHideFlags);
|
||||
|
||||
if (show)
|
||||
{
|
||||
prop.intValue &= (int)~HideFlags.HideInInspector;
|
||||
}
|
||||
else
|
||||
{
|
||||
prop.intValue |= (int)HideFlags.HideInInspector;
|
||||
}
|
||||
|
||||
so.ApplyModifiedProperties();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (show)
|
||||
{
|
||||
self.hideFlags &= ~HideFlags.HideInInspector;
|
||||
}
|
||||
else
|
||||
{
|
||||
self.hideFlags |= HideFlags.HideInInspector;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Controls whether to show a given component in the inspector and whether it is editable.
|
||||
/// </summary>
|
||||
/// <param name="self">The object to set</param>
|
||||
/// <param name="show">Whether to show it in the inspector</param>
|
||||
/// <param name="editable">Whether it is editable</param>
|
||||
public static void ShowInInspector(this Component self, bool show, bool editable)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
SerializedObject so = new SerializedObject(self);
|
||||
SerializedProperty prop = so.FindProperty(UxrConstants.Editor.PropertyObjectHideFlags);
|
||||
|
||||
if (show)
|
||||
{
|
||||
prop.intValue &= (int)~HideFlags.HideInInspector;
|
||||
}
|
||||
else
|
||||
{
|
||||
prop.intValue |= (int)HideFlags.HideInInspector;
|
||||
}
|
||||
|
||||
if (editable)
|
||||
{
|
||||
prop.intValue &= (int)~HideFlags.NotEditable;
|
||||
}
|
||||
else
|
||||
{
|
||||
prop.intValue |= (int)HideFlags.NotEditable;
|
||||
}
|
||||
|
||||
so.ApplyModifiedProperties();
|
||||
#else
|
||||
if (show)
|
||||
{
|
||||
self.hideFlags &= ~HideFlags.HideInInspector;
|
||||
}
|
||||
else
|
||||
{
|
||||
self.hideFlags |= HideFlags.HideInInspector;
|
||||
}
|
||||
|
||||
if (editable)
|
||||
{
|
||||
self.hideFlags &= ~HideFlags.NotEditable;
|
||||
}
|
||||
else
|
||||
{
|
||||
self.hideFlags |= HideFlags.NotEditable;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the component is in a prefab.
|
||||
/// </summary>
|
||||
/// <param name="self">Component to check</param>
|
||||
/// <returns>Whether the component is in a prefab</returns>
|
||||
public static bool IsInPrefab(this Component self)
|
||||
{
|
||||
return self.gameObject.IsInPrefab();
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
/// <summary>
|
||||
/// Gets the GUID of the prefab the component is in, if it is in a prefab, or the GUID of the prefab the component was
|
||||
/// instantiated from, if it was instantiated from a prefab.
|
||||
/// If the component is not in a prefab and doesn't have a source prefab either, it will return string.Empty.
|
||||
/// </summary>
|
||||
/// <param name="prefabGuid">If the call was successful, returns the GUID or string.Empty</param>
|
||||
/// <returns>Whether the prefab GUID could be retrieved</returns>
|
||||
/// <remarks>
|
||||
/// The reason the call can be unsuccessful is because Unity for some reason will report
|
||||
/// a null/empty asset path even though PrefabUtility.IsPartOfPrefabAsset() returns true.
|
||||
/// This behaviour happens when in prefab isolation/context mode in the editor
|
||||
/// </remarks>
|
||||
public static bool GetPrefabGuid(this Component self, out string prefabGuid)
|
||||
{
|
||||
return self.gameObject.GetPrefabGuid(out prefabGuid, out string _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Same as <see cref="GetPrefabGuid(UnityEngine.Component)" /> but it also returns the asset path if it exists.
|
||||
/// </summary>
|
||||
/// <param name="prefabGuid">If the call was successful, returns the GUID or string.Empty</param>
|
||||
/// <param name="assetPath">If the call was successful, returns the asset path or string.Empty</param>
|
||||
/// <returns>Whether the prefab GUID could be retrieved</returns>
|
||||
/// <remarks>
|
||||
/// The reason the call can be unsuccessful is because Unity for some reason will report
|
||||
/// a null/empty asset path even though PrefabUtility.IsPartOfPrefabAsset() returns true.
|
||||
/// This behaviour happens when in prefab isolation/context mode in the editor
|
||||
/// </remarks>
|
||||
public static bool GetPrefabGuid(this Component self, out string prefabGuid, out string assetPath)
|
||||
{
|
||||
return self.gameObject.GetPrefabGuid(out prefabGuid, out assetPath);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Component of a given type. If it doesn't exist, it is added to the GameObject.
|
||||
/// </summary>
|
||||
/// <param name="self">Component whose GameObject will be used to retrieve or add the given component type to</param>
|
||||
/// <typeparam name="T">Component type to get or add</typeparam>
|
||||
/// <returns>Existing component or newly added if it didn't exist before</returns>
|
||||
public static T GetOrAddComponent<T>(this Component self) where T : Component
|
||||
{
|
||||
T component = self.GetComponent<T>();
|
||||
|
||||
if (component == null)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
component = self.gameObject.AddComponent<T>();
|
||||
}
|
||||
else
|
||||
{
|
||||
component = Undo.AddComponent<T>(self.gameObject);
|
||||
}
|
||||
#else
|
||||
component = self.gameObject.AddComponent<T>();
|
||||
#endif
|
||||
}
|
||||
|
||||
return component;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Component of a given type in the GameObject or any of its parents. It also works on prefabs, where regular
|
||||
/// <see cref="Component.GetComponentInParent" /> will not work:
|
||||
/// https://issuetracker.unity3d.com/issues/getcomponentinparent-is-returning-null-when-the-gameobject-is-a-prefab
|
||||
/// </summary>
|
||||
/// <typeparam name="T"><see cref="Component" /> type to get</typeparam>
|
||||
/// <returns>Component in same GameObject or any of its parents. Null if it wasn't found</returns>
|
||||
public static T SafeGetComponentInParent<T>(this Component self)
|
||||
{
|
||||
return self.GetComponentInParent<T>() ?? self.GetComponentsInParent<T>(true).FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full path under current scene, including all parents, but scene name, for the given component.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The path generated might not be unique. If that is the purpose, use <see cref="GetUniqueScenePath" /> instead.
|
||||
/// </remarks>
|
||||
/// <param name="self"><see cref="Component" /> to get the path for</param>
|
||||
/// <param name="relativeTo">
|
||||
/// Optional Transform to get the path relative to. If it's not the same Transform or a Transform up in the hierarchy
|
||||
/// it will return the full path
|
||||
/// </param>
|
||||
/// <returns>Component path string</returns>
|
||||
public static string GetPathUnderScene(this Component self, Transform relativeTo = null)
|
||||
{
|
||||
string path = self.transform.GetPathUnderScene(relativeTo);
|
||||
return self is Transform ? path : $"{path}/{self.GetType().Name}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an unique path in the scene for the given component. It will include scene name, sibling and component indices
|
||||
/// to make it unique.
|
||||
/// </summary>
|
||||
/// <param name="self"><see cref="Component" /> to get the unique path for</param>
|
||||
/// <param name="relativeTo">
|
||||
/// Optional Transform to get the path relative to. If it's not the same Transform or a Transform up in the hierarchy
|
||||
/// it will return the full path
|
||||
/// </param>
|
||||
/// <returns>Unique component path string</returns>
|
||||
public static string GetUniqueScenePath(this Component self, Transform relativeTo = null)
|
||||
{
|
||||
string path = self.transform.GetUniqueScenePath(relativeTo);
|
||||
return self is Transform ? path : $"{path}/{Array.IndexOf(self.GetComponents<Component>(), self):00}{self.GetType().Name}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an unique identifier string for the given component.
|
||||
/// </summary>
|
||||
/// <remarks>Generates an 8 characters hexadecimal hash code of <see cref="GetUniqueScenePath" />.</remarks>
|
||||
/// <param name="self"><see cref="Component" /> to get UID for.</param>
|
||||
/// <returns>8 characters hexadecimal unique identifier <see cref="string" /></returns>
|
||||
public static string GetSceneUid(this Component self)
|
||||
{
|
||||
return self.GetUniqueScenePath().GetHashCode().ToString("x8");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of all components of the given type in the open scenes
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of component to look for</typeparam>
|
||||
/// <param name="includeInactive">Whether to include inactive components or not</param>
|
||||
/// <returns>List of components</returns>
|
||||
public static List<T> GetAllComponentsInOpenScenes<T>(bool includeInactive) where T : Component
|
||||
{
|
||||
List<T> listResult = new List<T>();
|
||||
|
||||
for (int i = 0; i < SceneManager.sceneCount; i++)
|
||||
{
|
||||
Scene s = SceneManager.GetSceneAt(i);
|
||||
if (s.isLoaded)
|
||||
{
|
||||
GameObject[] rootGameObjects = s.GetRootGameObjects();
|
||||
foreach (GameObject go in rootGameObjects)
|
||||
{
|
||||
listResult.AddRange(go.GetComponentsInChildren<T>(includeInactive));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return listResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// From a set of components, returns which one of them has a transform that is a common root of all.
|
||||
/// The transform must be the transform of a component in the list.
|
||||
/// </summary>
|
||||
/// <param name="components">Components whose transforms to check</param>
|
||||
/// <returns>
|
||||
/// Returns which transform from all the components passed as parameters is a common root of all. If no component has a
|
||||
/// transform that is a common root it returns null.
|
||||
/// </returns>
|
||||
public static T GetCommonRootComponentFromSet<T>(params T[] components) where T : Component
|
||||
{
|
||||
T commonRoot = null;
|
||||
|
||||
for (int i = 0; i < components.Length; i++)
|
||||
{
|
||||
if (i == 0)
|
||||
{
|
||||
commonRoot = components[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
if (commonRoot == null || (components[i] != commonRoot && components[i].transform.HasParent(commonRoot.transform) == false))
|
||||
{
|
||||
bool found = true;
|
||||
|
||||
for (int j = 0; j < i - 1; j++)
|
||||
{
|
||||
if (components[i] != components[j] && components[j].transform.HasParent(components[i].transform) == false)
|
||||
{
|
||||
found = false;
|
||||
}
|
||||
}
|
||||
|
||||
commonRoot = found ? components[i] : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return commonRoot;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea44d5b576bdf9f4a95ac21684f8d567
|
||||
timeCreated: 1622456990
|
||||
@@ -0,0 +1,397 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="GameObjectExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UltimateXR.Avatar;
|
||||
using UltimateXR.Extensions.System;
|
||||
using UltimateXR.Extensions.Unity.Math;
|
||||
using UltimateXR.Manipulation;
|
||||
using UltimateXR.UI.UnityInputModule;
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
#endif
|
||||
|
||||
namespace UltimateXR.Extensions.Unity
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="GameObject" /> extensions.
|
||||
/// </summary>
|
||||
public static class GameObjectExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Component of a given type in the GameObject or any of its parents. It also works on prefabs, where regular
|
||||
/// <see cref="Component.GetComponentInParent" /> will not work:
|
||||
/// https://issuetracker.unity3d.com/issues/getcomponentinparent-is-returning-null-when-the-gameobject-is-a-prefab
|
||||
/// It also works on objects recently instantiated or on disabled objects.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Component type to get</typeparam>
|
||||
/// <returns>Component in same GameObject or any of its parents. Null if it wasn't found</returns>
|
||||
public static T SafeGetComponentInParent<T>(this GameObject self)
|
||||
{
|
||||
T parent = self.GetComponentInParent<T>();
|
||||
return parent ?? self.GetComponentsInParent<T>(true).FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activates/deactivates the object if it isn't active already.
|
||||
/// </summary>
|
||||
/// <param name="self">GameObject to activate</param>
|
||||
/// <param name="activate">Whether to activate or deactivate the object</param>
|
||||
public static void CheckSetActive(this GameObject self, bool activate)
|
||||
{
|
||||
if (self.activeSelf != activate)
|
||||
{
|
||||
self.SetActive(activate);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a unique path in the scene for the given GameObject. It will include sibling indices to make it unique.
|
||||
/// </summary>
|
||||
/// <param name="self">GameObject to get the unique path for</param>
|
||||
/// <returns>Unique GameObject path string</returns>
|
||||
/// <seealso cref="TransformExt.GetUniqueScenePath" />
|
||||
public static string GetUniqueScenePath(this GameObject self)
|
||||
{
|
||||
self.ThrowIfNull(nameof(self));
|
||||
return self.transform.GetUniqueScenePath();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path in the scene of the given GameObject.
|
||||
/// </summary>
|
||||
/// <param name="self">GameObject to get the scene path for</param>
|
||||
/// <returns>Path of the GameObject in the scene</returns>
|
||||
/// <seealso cref="TransformExt.GetPathUnderScene" />
|
||||
public static string GetPathUnderScene(this GameObject self)
|
||||
{
|
||||
self.ThrowIfNull(nameof(self));
|
||||
return self.transform.GetPathUnderScene();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given GameObject is the root GameObject inside a prefab.
|
||||
/// </summary>
|
||||
/// <param name="self">GameObject to check</param>
|
||||
/// <returns>Whether the GameObject is the root GameObject inside a prefab</returns>
|
||||
public static bool IsPrefabRoot(this GameObject self)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (PrefabStageUtility.GetCurrentPrefabStage() != null && PrefabStageUtility.GetCurrentPrefabStage().prefabContentsRoot == self)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
return self.scene.name == null && self.transform.parent == null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given GameObject is located inside a prefab.
|
||||
/// </summary>
|
||||
/// <param name="self">GameObject to check</param>
|
||||
/// <returns>Whether the GameObject is located inside a prefab</returns>
|
||||
public static bool IsInPrefab(this GameObject self)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
|
||||
if (PrefabStageUtility.GetCurrentPrefabStage() != null && PrefabStageUtility.GetCurrentPrefabStage().IsPartOfPrefabContents(self))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return PrefabUtility.IsPartOfPrefabAsset(self);
|
||||
#else
|
||||
return self.scene.name == null;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
/// <summary>
|
||||
/// Gets the GUID of the prefab the GameObject is in, if it is in a prefab, or the GUID of the prefab the GameObject
|
||||
/// was instantiated from, if it was instantiated from a prefab. If it is not
|
||||
/// </summary>
|
||||
/// <param name="self">The GameObject to retrieve the information of</param>
|
||||
/// <param name="prefabGuid">If the call was successful, returns the GUID or string.Empty</param>
|
||||
/// <param name="assetPath">If the call was successful, returns the asset path or string.Empty</param>
|
||||
/// <returns>Whether the call was successful</returns>
|
||||
/// <remarks>The reason the call can be unsuccessful is because Unity for some reason will report
|
||||
/// a null/empty asset path even though PrefabUtility.IsPartOfPrefabAsset() returns true.
|
||||
/// This behaviour happens when in prefab isolation/context mode in the editor</remarks>
|
||||
public static bool GetPrefabGuid(this GameObject self, out string prefabGuid, out string assetPath)
|
||||
{
|
||||
prefabGuid = string.Empty;
|
||||
assetPath = string.Empty;
|
||||
|
||||
if (PrefabStageUtility.GetCurrentPrefabStage() != null && PrefabStageUtility.GetCurrentPrefabStage().IsPartOfPrefabContents(self))
|
||||
{
|
||||
// Asset in prefab view
|
||||
assetPath = PrefabStageUtility.GetCurrentPrefabStage().assetPath;
|
||||
}
|
||||
else if (PrefabUtility.IsPartOfPrefabAsset(self))
|
||||
{
|
||||
// Prefab asset
|
||||
assetPath = AssetDatabase.GetAssetPath(self);
|
||||
|
||||
if (string.IsNullOrEmpty(assetPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (PrefabUtility.IsPartOfNonAssetPrefabInstance(self) && self.scene.name != null)
|
||||
{
|
||||
GameObject instanceRoot = PrefabUtility.GetOutermostPrefabInstanceRoot(self);
|
||||
GameObject rootPrefab = PrefabUtility.GetCorrespondingObjectFromSource(instanceRoot);
|
||||
assetPath = AssetDatabase.GetAssetPath(rootPrefab);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(assetPath))
|
||||
{
|
||||
// Try to get GUID from asset path, checking if path is valid. Otherwise return null.
|
||||
|
||||
// Check if all zeroes, which means invalid guid.
|
||||
|
||||
prefabGuid = AssetDatabase.GUIDFromAssetPath(assetPath).ToString();
|
||||
prefabGuid = string.IsNullOrEmpty(prefabGuid) || prefabGuid.All(c => c == '0') ? null : prefabGuid;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given GameObject is dynamic. Since <see cref="GameObject.isStatic" /> doesn't work at runtime
|
||||
/// due to the static flags being editor-only, a workaround is required to try to find out if an object is dynamic or
|
||||
/// not.
|
||||
/// </summary>
|
||||
/// <param name="self">GameObject to check</param>
|
||||
/// <returns>Whether the object appears to be dynamic</returns>
|
||||
public static bool IsDynamic(this GameObject self)
|
||||
{
|
||||
return self.GetComponentInParent<Rigidbody>() != null ||
|
||||
self.GetComponentInParent<SkinnedMeshRenderer>() != null ||
|
||||
self.GetComponentInParent<UxrAvatar>() != null ||
|
||||
self.GetComponentInParent<UxrGrabbableObject>() != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Component of a given type. If it doesn't exist, it is added to the GameObject.
|
||||
/// </summary>
|
||||
/// <param name="self">Target GameObject where the component will be looked for and added to if it doesn't exist</param>
|
||||
/// <typeparam name="T">Component type to get or add</typeparam>
|
||||
/// <returns>Existing component or newly added if it didn't exist before</returns>
|
||||
public static T GetOrAddComponent<T>(this GameObject self) where T : Component
|
||||
{
|
||||
T component = self.GetComponent<T>();
|
||||
|
||||
if (component == null)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
component = self.AddComponent<T>();
|
||||
}
|
||||
else
|
||||
{
|
||||
component = Undo.AddComponent<T>(self);
|
||||
}
|
||||
#else
|
||||
component = self.AddComponent<T>();
|
||||
#endif
|
||||
}
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new GameObject in the exact same position as the given one and parents it.
|
||||
/// </summary>
|
||||
/// <param name="parent">
|
||||
/// GameObject to parent the new object to and also place it at the same position
|
||||
/// and with the same orientation
|
||||
/// </param>
|
||||
/// <param name="newGameObjectName">Name for the new GameObject</param>
|
||||
/// <returns>New created GameObject</returns>
|
||||
public static GameObject CreateGameObjectAndParentSameTransform(GameObject parent, string newGameObjectName)
|
||||
{
|
||||
GameObject newGameObject = new GameObject(newGameObjectName);
|
||||
newGameObject.transform.parent = parent.transform;
|
||||
newGameObject.transform.localPosition = Vector3.zero;
|
||||
newGameObject.transform.localRotation = Quaternion.identity;
|
||||
return newGameObject;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the geometric center of the given GameObject based on all the MeshRenderers in the hierarchy.
|
||||
/// </summary>
|
||||
/// <param name="self">GameObject to compute the geometric center of</param>
|
||||
/// <returns>Geometric center</returns>
|
||||
public static Vector3 GetGeometricCenter(this GameObject self)
|
||||
{
|
||||
IEnumerable<MeshRenderer> meshRenderers = self.GetComponentsInChildren<MeshRenderer>().Where(r => !r.hideFlags.HasFlag(HideFlags.HideInHierarchy) && r.enabled);
|
||||
bool initialized = false;
|
||||
Vector3 min = Vector3.zero;
|
||||
Vector3 max = Vector3.zero;
|
||||
|
||||
if (!meshRenderers.Any())
|
||||
{
|
||||
return self.transform.position;
|
||||
}
|
||||
|
||||
foreach (MeshRenderer renderer in meshRenderers)
|
||||
{
|
||||
if (!initialized)
|
||||
{
|
||||
initialized = true;
|
||||
min = renderer.bounds.min;
|
||||
max = renderer.bounds.max;
|
||||
}
|
||||
else
|
||||
{
|
||||
min = Vector3.Min(min, renderer.bounds.min);
|
||||
max = Vector3.Max(max, renderer.bounds.max);
|
||||
}
|
||||
}
|
||||
|
||||
return (min + max) * 0.5f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the <see cref="GameObject" /> <see cref="Bounds" />. The bounds are the <see cref="Renderer" />'s bounds
|
||||
/// if there is one in the GameObject. Otherwise it will encapsulate all renderers found in the children.
|
||||
/// If <paramref name="forceRecurseIntoChildren" /> is true, it will also encapsulate all renderers found in
|
||||
/// the children no matter if the GameObject has a Renderer component or not.
|
||||
/// </summary>
|
||||
/// <param name="self">The GameObject whose <see cref="Bounds" /> to get</param>
|
||||
/// <param name="forceRecurseIntoChildren">
|
||||
/// Whether to also encapsulate all renderers found in the children no matter if the
|
||||
/// GameObject has a Renderer component or not
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <see cref="Bounds" /> in world-space.
|
||||
/// </returns>
|
||||
public static Bounds GetBounds(this GameObject self, bool forceRecurseIntoChildren)
|
||||
{
|
||||
Renderer renderer = self.GetComponent<Renderer>();
|
||||
|
||||
if (renderer != null && !forceRecurseIntoChildren)
|
||||
{
|
||||
if (renderer.enabled)
|
||||
{
|
||||
return renderer.bounds;
|
||||
}
|
||||
|
||||
return new Bounds(self.transform.position, Vector3.zero);
|
||||
}
|
||||
|
||||
IEnumerable<Renderer> renderers = self.GetComponentsInChildren<Renderer>().Where(r => !r.hideFlags.HasFlag(HideFlags.HideInHierarchy) && r.enabled);
|
||||
|
||||
if (!renderers.Any())
|
||||
{
|
||||
return new Bounds(self.transform.position, Vector3.zero);
|
||||
}
|
||||
|
||||
Vector3 min = Vector3Ext.Min(renderers.Select(r => r.bounds.min));
|
||||
Vector3 max = Vector3Ext.Max(renderers.Select(r => r.bounds.max));
|
||||
|
||||
return new Bounds((max + min) * 0.5f, max - min);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the <see cref="GameObject" /> <see cref="Bounds" /> in local space. The bounds are the
|
||||
/// <see cref="Renderer" />'s bounds if there is one in the GameObject. Otherwise it will encapsulate all renderers
|
||||
/// found in the children.
|
||||
/// If <paramref name="forceRecurseIntoChildren" /> is true, it will also encapsulate all renderers found in
|
||||
/// the children no matter if the GameObject has a Renderer component or not.
|
||||
/// </summary>
|
||||
/// <param name="self">The GameObject whose local <see cref="Bounds" /> to get</param>
|
||||
/// <param name="forceRecurseIntoChildren">
|
||||
/// Whether to also encapsulate all renderers found in the children no matter if the
|
||||
/// GameObject has a Renderer component or not
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Local <see cref="Bounds" />.
|
||||
/// </returns>
|
||||
public static Bounds GetLocalBounds(this GameObject self, bool forceRecurseIntoChildren)
|
||||
{
|
||||
Renderer renderer = self.GetComponent<Renderer>();
|
||||
|
||||
if (renderer != null && !forceRecurseIntoChildren)
|
||||
{
|
||||
if (renderer.enabled)
|
||||
{
|
||||
return renderer.localBounds;
|
||||
}
|
||||
|
||||
return new Bounds();
|
||||
}
|
||||
|
||||
IEnumerable<Renderer> renderers = self.GetComponentsInChildren<Renderer>().Where(r => !r.hideFlags.HasFlag(HideFlags.HideInHierarchy) && r.enabled);
|
||||
|
||||
if (!renderers.Any())
|
||||
{
|
||||
return new Bounds();
|
||||
}
|
||||
|
||||
IEnumerable<Vector3> allMinMaxToLocal = renderers.Select(r => self.transform.InverseTransformPoint(r.transform.TransformPoint(r.localBounds.min))).Concat(renderers.Select(r => self.transform.InverseTransformPoint(r.transform.TransformPoint(r.localBounds.max))));
|
||||
Vector3 min = Vector3Ext.Min(allMinMaxToLocal);
|
||||
Vector3 max = Vector3Ext.Max(allMinMaxToLocal);
|
||||
|
||||
return new Bounds((max + min) * 0.5f, max - min);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the layer of a GameObject and all its children.
|
||||
/// </summary>
|
||||
/// <param name="self">The root GameObject from where to start</param>
|
||||
/// <param name="layer">The layer value to assign</param>
|
||||
public static void SetLayerRecursively(this GameObject self, int layer)
|
||||
{
|
||||
if (self != null)
|
||||
{
|
||||
Transform selfTransform = self.transform;
|
||||
|
||||
self.gameObject.layer = layer;
|
||||
|
||||
for (int i = 0; i < selfTransform.childCount; ++i)
|
||||
{
|
||||
SetLayerRecursively(selfTransform.GetChild(i).gameObject, layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given GameObject's layer is present in a layer mask.
|
||||
/// </summary>
|
||||
/// <param name="self">The GameObject whose layer to check</param>
|
||||
/// <param name="layerMask">The layer mask to check against</param>
|
||||
/// <returns>Whether the GameObject's layer is present in the layer mask</returns>
|
||||
public static bool IsInLayerMask(this GameObject self, LayerMask layerMask)
|
||||
{
|
||||
return (1 << self.layer & layerMask.value) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the topmost <see cref="UxrCanvas" /> upwards in the hierarchy if it exists.
|
||||
/// </summary>
|
||||
/// <param name="self">The GameObject whose parents to look for</param>
|
||||
/// <returns>The topmost <see cref="UxrCanvas" /> component upwards in the hierarchy or null if it doesn't exists</returns>
|
||||
public static UxrCanvas GetTopmostCanvas(this GameObject self)
|
||||
{
|
||||
UxrCanvas[] canvases = self.GetComponentsInParent<UxrCanvas>();
|
||||
return ComponentExt.GetCommonRootComponentFromSet(canvases);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 07207d54b86c4fc49001159550f9d26c
|
||||
timeCreated: 1597311345
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 057ee23d309953c46b56e9e45348de51
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,34 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="AsyncOperationExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UltimateXR.Extensions.System.Threading;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="AsyncOperation" /> extensions.
|
||||
/// </summary>
|
||||
public static class AsyncOperationExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Creates an awaitable <see cref="Task" /> that finishes when the given <see cref="AsyncOperation" /> finished.
|
||||
/// </summary>
|
||||
/// <param name="self">Unity asynchronous operation object</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
|
||||
/// <returns>Awaitable <see cref="Task" /> returning the caller <see cref="AsyncOperation" /> object</returns>
|
||||
public static async Task<AsyncOperation> Wait(this AsyncOperation self, CancellationToken ct = default)
|
||||
{
|
||||
await TaskExt.WaitUntil(() => self.isDone, ct);
|
||||
return self;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d767412986bc9d24c8cea81a16a3a34d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,36 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="ResourcesExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Resources" /> extensions.
|
||||
/// </summary>
|
||||
public class ResourcesExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Loads a resource asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path relative to a Resources folder</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
|
||||
/// <typeparam name="T">Resource type to load</typeparam>
|
||||
/// <returns>Awaitable <see cref="Task" /> that returns the loaded resource</returns>
|
||||
public static async Task<T> Load<T>(string filePath, CancellationToken ct = default)
|
||||
where T : Object
|
||||
{
|
||||
ResourceRequest op = Resources.LoadAsync<T>(filePath);
|
||||
await op.Wait(ct);
|
||||
return (T)op.asset;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed867f5e61aa8644a94500ca8d1d8bb0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,244 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="UnityWebRequestExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UltimateXR.Exceptions;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="UnityWebRequest" /> extensions to read data synchronously and asynchronously.
|
||||
/// </summary>
|
||||
public static class UnityWebRequestExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a given URI can be read using a <see cref="UnityWebRequest" />.
|
||||
/// </summary>
|
||||
/// <param name="uri">URI to check</param>
|
||||
/// <returns>Whether the URI is compatible with <see cref="UnityWebRequest" /></returns>
|
||||
public static bool IsUwrUri(string uri)
|
||||
{
|
||||
return uri.Contains(Application.streamingAssetsPath)
|
||||
|| uri.StartsWith(FilePrefix)
|
||||
|| uri.StartsWith(HttpPrefix)
|
||||
|| uri.StartsWith(HttpsPrefix);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a <see cref="UnityWebRequest" />.
|
||||
/// </summary>
|
||||
/// <param name="self">Request to send</param>
|
||||
/// <exception cref="HttpUwrException">HttpError flag is on</exception>
|
||||
/// <exception cref="NetUwrException">NetworkError flag is on</exception>
|
||||
public static void Fetch(this UnityWebRequest self)
|
||||
{
|
||||
UnityWebRequestAsyncOperation request = self.SendWebRequest();
|
||||
|
||||
while (!request.isDone)
|
||||
{
|
||||
// Active wait
|
||||
Thread.Sleep(0);
|
||||
}
|
||||
|
||||
if (self.result == UnityWebRequest.Result.ConnectionError)
|
||||
{
|
||||
throw new NetUwrException(self.error);
|
||||
}
|
||||
|
||||
if (self.result == UnityWebRequest.Result.ProtocolError)
|
||||
{
|
||||
throw new HttpUwrException(self.error, self.responseCode);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a <see cref="UnityWebRequest" /> asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="self">Request to send</param>
|
||||
/// <param name="ct">Cancellation token, to cancel the operation</param>
|
||||
/// <returns>Awaitable task that will finish when the request was sent</returns>
|
||||
/// <exception cref="OperationCanceledException">The task was canceled using <paramref name="ct" /></exception>
|
||||
/// <exception cref="HttpUwrException">HttpError flag is on</exception>
|
||||
/// <exception cref="NetUwrException">NetworkError flag is on</exception>
|
||||
public static async Task FetchAsync(this UnityWebRequest self, CancellationToken ct = default)
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await self.SendWebRequest().Wait(ct);
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
self.Abort();
|
||||
}
|
||||
|
||||
if (self.result == UnityWebRequest.Result.ConnectionError)
|
||||
{
|
||||
throw new NetUwrException(self.error);
|
||||
}
|
||||
|
||||
if (self.result == UnityWebRequest.Result.ProtocolError)
|
||||
{
|
||||
throw new HttpUwrException(self.error, self.responseCode);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads an <see cref="AudioClip" /> asynchronously from an URI.
|
||||
/// </summary>
|
||||
/// <param name="uri">Location of the audio clip</param>
|
||||
/// <exception cref="HttpUwrException">HttpError flag is on</exception>
|
||||
/// <exception cref="NetUwrException">NetworkError flag is on</exception>
|
||||
public static AudioClip LoadAudioClip(string uri)
|
||||
{
|
||||
using UnityWebRequest req = UnityWebRequestMultimedia.GetAudioClip(FixUri(uri), AudioType.UNKNOWN);
|
||||
req.Fetch();
|
||||
|
||||
AudioClip result = DownloadHandlerAudioClip.GetContent(req);
|
||||
result.name = Path.GetFileNameWithoutExtension(uri);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads an <see cref="AudioClip" /> asynchronously from an URI.
|
||||
/// </summary>
|
||||
/// <param name="uri">Location of the audio clip</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
|
||||
/// <returns>Awaitable <see cref="Task" /> that returns the loaded audio clip</returns>
|
||||
/// <exception cref="HttpUwrException">HttpError flag is on</exception>
|
||||
/// <exception cref="NetUwrException">NetworkError flag is on</exception>
|
||||
/// <exception cref="OperationCanceledException">The task was canceled using <paramref name="ct" /></exception>
|
||||
public static async Task<AudioClip> LoadAudioClipAsync(string uri, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
using UnityWebRequest req = UnityWebRequestMultimedia.GetAudioClip(FixUri(uri), AudioType.UNKNOWN);
|
||||
await req.FetchAsync(ct);
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
AudioClip result = DownloadHandlerAudioClip.GetContent(req);
|
||||
result.name = Path.GetFileNameWithoutExtension(uri);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads bytes from an URI.
|
||||
/// </summary>
|
||||
/// <param name="uri">Location of the data</param>
|
||||
/// <exception cref="HttpUwrException">
|
||||
/// HttpError flag is on
|
||||
/// </exception>
|
||||
/// <exception cref="NetUwrException">
|
||||
/// NetworkError flag is on
|
||||
/// </exception>
|
||||
public static byte[] ReadBytes(string uri)
|
||||
{
|
||||
using UnityWebRequest req = UnityWebRequest.Get(FixUri(uri));
|
||||
req.Fetch();
|
||||
return req.downloadHandler.data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads bytes asynchronously from an URI.
|
||||
/// </summary>
|
||||
/// <param name="uri">Location of the data</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
|
||||
/// <returns>Awaitable task that returns the bytes read</returns>
|
||||
/// <exception cref="OperationCanceledException">The task was canceled using <paramref name="ct" /></exception>
|
||||
/// <exception cref="HttpUwrException">
|
||||
/// HttpError flag is on
|
||||
/// </exception>
|
||||
/// <exception cref="NetUwrException">
|
||||
/// NetworkError flag is on
|
||||
/// </exception>
|
||||
public static async Task<byte[]> ReadBytesAsync(string uri, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
using UnityWebRequest req = UnityWebRequest.Get(FixUri(uri));
|
||||
await req.FetchAsync(ct);
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
return req.downloadHandler.data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a string from an URI.
|
||||
/// </summary>
|
||||
/// <param name="uri">Text location</param>
|
||||
/// <exception cref="HttpUwrException">
|
||||
/// HttpError flag is on
|
||||
/// </exception>
|
||||
/// <exception cref="NetUwrException">
|
||||
/// NetworkError flag is on
|
||||
/// </exception>
|
||||
public static string ReadText(string uri)
|
||||
{
|
||||
using UnityWebRequest req = UnityWebRequest.Get(FixUri(uri));
|
||||
req.Fetch();
|
||||
return req.downloadHandler.text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a string asynchronously from an URI.
|
||||
/// </summary>
|
||||
/// <param name="uri">Text location</param>
|
||||
/// <param name="ct">Optional cancellation, to cancel the operation</param>
|
||||
/// <returns>Awaitable task that returns the string read</returns>
|
||||
/// <exception cref="OperationCanceledException">The task was canceled using <paramref name="ct" /></exception>
|
||||
/// <exception cref="HttpUwrException">
|
||||
/// HttpError flag is on
|
||||
/// </exception>
|
||||
/// <exception cref="NetUwrException">
|
||||
/// NetworkError flag is on
|
||||
/// </exception>
|
||||
public static async Task<string> ReadTextAsync(string uri, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
using UnityWebRequest req = UnityWebRequest.Get(FixUri(uri));
|
||||
await req.FetchAsync(ct);
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
return req.downloadHandler.text;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
|
||||
/// <summary>
|
||||
/// Fixes an URI string.
|
||||
/// </summary>
|
||||
/// <param name="uri">String to fix</param>
|
||||
/// <returns>Fixed URI string</returns>
|
||||
private static string FixUri(string uri)
|
||||
{
|
||||
string result = uri.Trim('\\', '/', ' ');
|
||||
if (!IsUwrUri(uri))
|
||||
{
|
||||
result = FilePrefix + uri;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Types & Data
|
||||
|
||||
private const string FilePrefix = "file://";
|
||||
private const string HttpPrefix = "http://";
|
||||
private const string HttpsPrefix = "https://";
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 881eec29ea48c504eaea7d53b6a9ab96
|
||||
timeCreated: 1598879590
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f7342ec28ef48144817db1724aa3b02
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,88 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="Matrix4x4Ext.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity.Math
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Matrix4x4" /> extensions.
|
||||
/// </summary>
|
||||
public static class Matrix4x4Ext
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Interpolates two matrices by decomposing the position, rotation and scale values and interpolating them separately.
|
||||
/// </summary>
|
||||
/// <param name="matrixA">Source matrix</param>
|
||||
/// <param name="matrixB">Destination matrix</param>
|
||||
/// <param name="blendValue">Interpolation value [0.0, 1.0]</param>
|
||||
/// <returns>Interpolated matrix</returns>
|
||||
public static Matrix4x4 Interpolate(Matrix4x4 matrixA, Matrix4x4 matrixB, float blendValue)
|
||||
{
|
||||
Vector3 position = Vector3.Lerp(matrixA.MultiplyPoint(Vector3.zero), matrixB.MultiplyPoint(Vector3.zero), blendValue);
|
||||
Quaternion rotation = Quaternion.Slerp(matrixA.rotation, matrixB.rotation, blendValue);
|
||||
Vector3 scale = Vector3.Lerp(matrixA.lossyScale, matrixB.lossyScale, blendValue);
|
||||
|
||||
return Matrix4x4.TRS(position, rotation, scale);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a projection matrix so that it has an oblique near clip plane.
|
||||
/// </summary>
|
||||
/// <param name="projection">Projection matrix</param>
|
||||
/// <param name="clipPlane">Clipping plane in camera space</param>
|
||||
/// <returns>Projection matrix with oblique clip plane</returns>
|
||||
public static Matrix4x4 GetObliqueMatrix(this Matrix4x4 projection, Vector4 clipPlane)
|
||||
{
|
||||
Matrix4x4 oblique = projection;
|
||||
Vector4 q = projection.inverse * new Vector4(Mathf.Sign(clipPlane.x), Mathf.Sign(clipPlane.y), 1.0f, 1.0f);
|
||||
Vector4 c = clipPlane * (2.0F / Vector4.Dot(clipPlane, q));
|
||||
|
||||
//third row = clip plane - fourth row
|
||||
oblique[2] = c.x - projection[3];
|
||||
oblique[6] = c.y - projection[7];
|
||||
oblique[10] = c.z - projection[11];
|
||||
oblique[14] = c.w - projection[15];
|
||||
|
||||
return oblique;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the reflection matrix around the given plane.
|
||||
/// </summary>
|
||||
/// <param name="plane">Reflection plane</param>
|
||||
/// <returns>Reflected matrix</returns>
|
||||
public static Matrix4x4 GetReflectionMatrix(Vector4 plane)
|
||||
{
|
||||
Matrix4x4 reflectionMat;
|
||||
|
||||
reflectionMat.m00 = 1F - 2F * plane[0] * plane[0];
|
||||
reflectionMat.m01 = -2F * plane[0] * plane[1];
|
||||
reflectionMat.m02 = -2F * plane[0] * plane[2];
|
||||
reflectionMat.m03 = -2F * plane[3] * plane[0];
|
||||
|
||||
reflectionMat.m10 = -2F * plane[1] * plane[0];
|
||||
reflectionMat.m11 = 1F - 2F * plane[1] * plane[1];
|
||||
reflectionMat.m12 = -2F * plane[1] * plane[2];
|
||||
reflectionMat.m13 = -2F * plane[3] * plane[1];
|
||||
|
||||
reflectionMat.m20 = -2F * plane[2] * plane[0];
|
||||
reflectionMat.m21 = -2F * plane[2] * plane[1];
|
||||
reflectionMat.m22 = 1F - 2F * plane[2] * plane[2];
|
||||
reflectionMat.m23 = -2F * plane[3] * plane[2];
|
||||
|
||||
reflectionMat.m30 = 0F;
|
||||
reflectionMat.m31 = 0F;
|
||||
reflectionMat.m32 = 0F;
|
||||
reflectionMat.m33 = 1F;
|
||||
|
||||
return reflectionMat;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f1ff835b97dd6f439498b64c5c9394d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,288 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="QuaternionExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UltimateXR.Core;
|
||||
using UltimateXR.Extensions.System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity.Math
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Quaternion" /> extensions.
|
||||
/// </summary>
|
||||
public static class QuaternionExt
|
||||
{
|
||||
#region Public Types & Data
|
||||
|
||||
/// <summary>
|
||||
/// Represents a NaN Quaternion.
|
||||
/// </summary>
|
||||
public static ref readonly Quaternion NaN => ref s_nan;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Compares two Unity Quaternion objects for equality with a specified precision threshold.
|
||||
/// </summary>
|
||||
/// <param name="a">The first Quaternion to compare</param>
|
||||
/// <param name="b">The second Quaternion to compare</param>
|
||||
/// <param name="precisionThreshold">
|
||||
/// The precision threshold for float comparisons. Defaults to
|
||||
/// <see cref="UxrConstants.Math.DefaultPrecisionThreshold" />.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the Quaternion objects are equal; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// This method performs a component-wise comparison between two Quaternion objects.
|
||||
/// Each component is compared using the specified precision threshold for float comparisons.
|
||||
/// </remarks>
|
||||
public static bool EqualsUsingPrecision(this Quaternion a, Quaternion b, float precisionThreshold = UxrConstants.Math.DefaultPrecisionThreshold)
|
||||
{
|
||||
return Mathf.Abs(a.x - b.x) <= precisionThreshold &&
|
||||
Mathf.Abs(a.y - b.y) <= precisionThreshold &&
|
||||
Mathf.Abs(a.z - b.z) <= precisionThreshold &&
|
||||
Mathf.Abs(a.w - b.w) <= precisionThreshold;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms a <see cref="Quaternion" /> to a <see cref="Vector4" /> component by component.
|
||||
/// </summary>
|
||||
/// <param name="self">Source quaternion</param>
|
||||
/// <returns><see cref="Vector4" /> with the components of the <see cref="Quaternion" /></returns>
|
||||
public static Vector4 ToVector4(this in Quaternion self)
|
||||
{
|
||||
return new Vector4(self.x, self.y, self.z, self.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given <see cref="Quaternion" /> has any NaN value.
|
||||
/// </summary>
|
||||
/// <param name="self">Source quaternion</param>
|
||||
/// <returns>Whether the quaternion has any NaN value</returns>
|
||||
public static bool IsNaN(this in Quaternion self)
|
||||
{
|
||||
return float.IsNaN(self.x) || float.IsNaN(self.y) || float.IsNaN(self.z) || float.IsNaN(self.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given <see cref="Quaternion" /> has any infinity value.
|
||||
/// </summary>
|
||||
/// <param name="self">Source quaternion</param>
|
||||
/// <returns>Whether the quaternion has any infinity value</returns>
|
||||
public static bool IsInfinity(this in Quaternion self)
|
||||
{
|
||||
return float.IsInfinity(self.x) || float.IsInfinity(self.y) || float.IsInfinity(self.z) || float.IsInfinity(self.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given <see cref="Quaternion" /> has any 0 value.
|
||||
/// </summary>
|
||||
/// <param name="self">Source quaternion</param>
|
||||
/// <returns>Whether the quaternion has any 0 value</returns>
|
||||
public static bool IsZero(this in Quaternion self)
|
||||
{
|
||||
return self.x == 0.0f && self.y == 0.0f && self.z == 0.0f && self.w == 0.0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given <see cref="Quaternion" /> contains valid data.
|
||||
/// </summary>
|
||||
/// <param name="self">Source quaternion</param>
|
||||
/// <returns>Whether the quaternion contains valid data</returns>
|
||||
public static bool IsValid(this in Quaternion self)
|
||||
{
|
||||
return !self.IsNaN() && !self.IsInfinity() && !self.IsZero();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies two quaternions component by component.
|
||||
/// </summary>
|
||||
/// <param name="self">Operand A</param>
|
||||
/// <param name="other">Operand B</param>
|
||||
/// <returns>Result quaternion</returns>
|
||||
public static Quaternion Multiply(this in Quaternion self, in Quaternion other)
|
||||
{
|
||||
return new Quaternion(self.x * other.x,
|
||||
self.y * other.y,
|
||||
self.z * other.z,
|
||||
self.w * other.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the inverse of a quaternion component by component (1 / value), checking for divisions by 0. Divisions by
|
||||
/// 0 have a result of 0.
|
||||
/// </summary>
|
||||
/// <param name="self">Source quaternion</param>
|
||||
/// <returns>Result quaternion</returns>
|
||||
public static Quaternion Inverse(this in Quaternion self)
|
||||
{
|
||||
return new Quaternion(Mathf.Approximately(self.x, 0f) ? 0f : 1f / self.x,
|
||||
Mathf.Approximately(self.y, 0f) ? 0f : 1f / self.y,
|
||||
Mathf.Approximately(self.z, 0f) ? 0f : 1f / self.z,
|
||||
Mathf.Approximately(self.w, 0f) ? 0f : 1f / self.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides two quaternions component by component, checking for divisions by 0. Divisions by 0 have a result of 0.
|
||||
/// </summary>
|
||||
/// <param name="self">Dividend</param>
|
||||
/// <param name="divisor">Divisor</param>
|
||||
/// <returns>Result quaternion</returns>
|
||||
public static Quaternion Divide(this in Quaternion self, in Quaternion divisor)
|
||||
{
|
||||
return self.Multiply(divisor.Inverse());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the average quaternion from a list.
|
||||
/// </summary>
|
||||
/// <param name="quaternions">List of quaternions</param>
|
||||
/// <param name="defaultIfEmpty">The default value to return if the list of quaternions is empty</param>
|
||||
/// <returns>Average quaternion</returns>
|
||||
/// <remarks>
|
||||
/// From
|
||||
/// https://gamedev.stackexchange.com/questions/119688/calculate-average-of-arbitrary-amount-of-quaternions-recursion
|
||||
/// </remarks>
|
||||
public static Quaternion Average(IEnumerable<Quaternion> quaternions, Quaternion defaultIfEmpty = default)
|
||||
{
|
||||
if (quaternions == null || !quaternions.Any())
|
||||
{
|
||||
return defaultIfEmpty;
|
||||
}
|
||||
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float z = 0.0f;
|
||||
float w = 0.0f;
|
||||
|
||||
foreach (Quaternion q in quaternions)
|
||||
{
|
||||
x += q.x;
|
||||
y += q.y;
|
||||
z += q.z;
|
||||
w += q.w;
|
||||
}
|
||||
|
||||
float k = 1.0f / Mathf.Sqrt(x * x + y * y + z * z + w * w);
|
||||
return new Quaternion(x * k, y * k, z * k, w * k);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the transformation to make a rotation defined by <paramref name="sourceRotation" /> rotate towards
|
||||
/// <paramref name="targetRotation" />.
|
||||
/// </summary>
|
||||
/// <param name="self">Quaternion to apply the rotation to</param>
|
||||
/// <param name="sourceRotation">Source rotation that will try to match <paramref name="targetRotation" /></param>
|
||||
/// <param name="targetRotation">Target rotation to match</param>
|
||||
/// <param name="t">Optional interpolation value [0.0, 1.0]</param>
|
||||
public static void ApplyAlignment(this Quaternion self, Quaternion sourceRotation, Quaternion targetRotation, float t = 1.0f)
|
||||
{
|
||||
Quaternion rotationTowards = Quaternion.RotateTowards(sourceRotation, targetRotation, 180.0f);
|
||||
Quaternion relative = Quaternion.Inverse(sourceRotation) * self;
|
||||
Quaternion result = Quaternion.Slerp(self, rotationTowards * relative, t);
|
||||
self.Set(result.x, result.y, result.z, result.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a <see cref="Quaternion" />.
|
||||
/// </summary>
|
||||
/// <param name="s">Source string</param>
|
||||
/// <returns>Result quaternion</returns>
|
||||
public static Quaternion Parse(string s)
|
||||
{
|
||||
s.ThrowIfNullOrWhitespace(nameof(s));
|
||||
|
||||
// Remove the parentheses
|
||||
s = s.TrimStart(' ', '(', '[');
|
||||
s = s.TrimEnd(' ', ')', ']');
|
||||
|
||||
// split the items
|
||||
string[] sArray = s.Split(s_cardinalSeparator, VectorLength);
|
||||
|
||||
// store as an array
|
||||
float[] result = new float[VectorLength];
|
||||
for (int i = 0; i < sArray.Length; ++i)
|
||||
{
|
||||
result[i] = float.TryParse(sArray[i],
|
||||
NumberStyles.Float,
|
||||
CultureInfo.InvariantCulture.NumberFormat,
|
||||
out float f)
|
||||
? f
|
||||
: float.NaN;
|
||||
}
|
||||
|
||||
return result.ToQuaternion();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a <see cref="Quaternion" />.
|
||||
/// </summary>
|
||||
/// <param name="s">Source string</param>
|
||||
/// <param name="result">Parsed quaternion</param>
|
||||
/// <returns>Whether the quaternion was successfully parsed</returns>
|
||||
public static bool TryParse(string s, out Quaternion result)
|
||||
{
|
||||
try
|
||||
{
|
||||
result = Parse(s);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
result = NaN;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Quaternion" /> from a float array. If the array does not contain enough elements, the missing
|
||||
/// components will contain NaN.
|
||||
/// </summary>
|
||||
/// <param name="data">Source data</param>
|
||||
/// <returns>Result quaternion</returns>
|
||||
public static Quaternion ToQuaternion(this float[] data)
|
||||
{
|
||||
return data.Length switch
|
||||
{
|
||||
0 => NaN,
|
||||
1 => new Quaternion(data[0], float.NaN, float.NaN, float.NaN),
|
||||
2 => new Quaternion(data[0], data[1], float.NaN, float.NaN),
|
||||
3 => new Quaternion(data[0], data[1], data[2], float.NaN),
|
||||
_ => new Quaternion(data[0], data[1], data[2], data[3])
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a <see cref="Quaternion" /> asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="s">Source string</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
|
||||
/// <returns>Awaitable <see cref="Task" /> that returns the parsed <see cref="Quaternion" /> or null if there was an error</returns>
|
||||
public static Task<Quaternion?> ParseAsync(string s, CancellationToken ct = default)
|
||||
{
|
||||
return Task.Run(() => TryParse(s, out Quaternion result) ? result : (Quaternion?)null, ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Types & Data
|
||||
|
||||
private const int VectorLength = 4;
|
||||
private const string CardinalSeparator = ",";
|
||||
|
||||
private static readonly char[] s_cardinalSeparator = CardinalSeparator.ToCharArray();
|
||||
private static readonly Quaternion s_nan = new Quaternion(float.NaN, float.NaN, float.NaN, float.NaN);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f4c05ae95c781bc4d933d3426cda4dd9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,253 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="Vector2Ext.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UltimateXR.Core;
|
||||
using UltimateXR.Extensions.System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity.Math
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Vector2" /> extensions.
|
||||
/// </summary>
|
||||
public static class Vector2Ext
|
||||
{
|
||||
#region Public Types & Data
|
||||
|
||||
/// <summary>
|
||||
/// Represents a NaN vector.
|
||||
/// </summary>
|
||||
public static ref readonly Vector2 NaN => ref s_nan;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Compares two Unity Vector2 objects for equality with a specified precision threshold.
|
||||
/// </summary>
|
||||
/// <param name="a">The first Vector2 to compare</param>
|
||||
/// <param name="b">The second Vector2 to compare</param>
|
||||
/// <param name="precisionThreshold">
|
||||
/// The precision threshold for float comparisons. Defaults to
|
||||
/// <see cref="UxrConstants.Math.DefaultPrecisionThreshold" />.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the Vector2 objects are equal; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// This method performs a component-wise comparison between two Vector2 objects.
|
||||
/// Each component is compared using the specified precision threshold for float comparisons.
|
||||
/// </remarks>
|
||||
public static bool EqualsUsingPrecision(this Vector2 a, Vector2 b, float precisionThreshold = UxrConstants.Math.DefaultPrecisionThreshold)
|
||||
{
|
||||
return Mathf.Abs(a.x - b.x) <= precisionThreshold &&
|
||||
Mathf.Abs(a.y - b.y) <= precisionThreshold;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given vector has any NaN component.
|
||||
/// </summary>
|
||||
/// <param name="self">Source vector</param>
|
||||
/// <returns>Whether any of the vector components has a NaN value</returns>
|
||||
public static bool IsNaN(this in Vector2 self)
|
||||
{
|
||||
return float.IsNaN(self.x) || float.IsNaN(self.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given vector has any infinity component.
|
||||
/// </summary>
|
||||
/// <param name="self">Source vector</param>
|
||||
/// <returns>Whether any of the vector components has an infinity value</returns>
|
||||
public static bool IsInfinity(this in Vector2 self)
|
||||
{
|
||||
return float.IsInfinity(self.x) || float.IsInfinity(self.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given vector contains valid data.
|
||||
/// </summary>
|
||||
/// <param name="self">Source vector</param>
|
||||
/// <returns>Whether the vector contains all valid values</returns>
|
||||
public static bool IsValid(this in Vector2 self)
|
||||
{
|
||||
return !self.IsNaN() && !self.IsInfinity();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces NaN component values with <paramref name="other" /> valid values.
|
||||
/// </summary>
|
||||
/// <param name="self">Vector whose NaN values to replace</param>
|
||||
/// <param name="other">Vector with valid values</param>
|
||||
/// <returns>Result vector</returns>
|
||||
public static Vector2 FillNanWith(this in Vector2 self, in Vector2 other)
|
||||
{
|
||||
float[] result = new float[VectorLength];
|
||||
for (int i = 0; i < VectorLength; ++i)
|
||||
{
|
||||
result[i] = float.IsNaN(self[i]) ? other[i] : self[i];
|
||||
}
|
||||
|
||||
return result.ToVector2();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the absolute value of each component in a vector.
|
||||
/// </summary>
|
||||
/// <param name="self">Source vector</param>
|
||||
/// <returns>Vector whose components are the absolute values</returns>
|
||||
public static Vector2 Abs(this in Vector2 self)
|
||||
{
|
||||
return new Vector2(Mathf.Abs(self.x), Mathf.Abs(self.y));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps <see cref="Vector2" /> values component by component.
|
||||
/// </summary>
|
||||
/// <param name="self">Vector whose components to clamp</param>
|
||||
/// <param name="min">Minimum values</param>
|
||||
/// <param name="max">Maximum values</param>
|
||||
/// <returns>Clamped vector</returns>
|
||||
public static Vector2 Clamp(this in Vector2 self, in Vector2 min, in Vector2 max)
|
||||
{
|
||||
float[] result = new float[VectorLength];
|
||||
for (int i = 0; i < VectorLength; ++i)
|
||||
{
|
||||
result[i] = Mathf.Clamp(self[i], min[i], max[i]);
|
||||
}
|
||||
|
||||
return result.ToVector2();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// returns a vector with all components containing 1/component, checking for divisions by 0. Divisions by 0 have a
|
||||
/// result of 0.
|
||||
/// </summary>
|
||||
/// <param name="self">Source vector</param>
|
||||
/// <returns>Result vector</returns>
|
||||
public static Vector2 Inverse(this in Vector2 self)
|
||||
{
|
||||
return new Vector2(Mathf.Approximately(self.x, 0f) ? 0f : 1f / self.x,
|
||||
Mathf.Approximately(self.y, 0f) ? 0f : 1f / self.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies two <see cref="Vector2" /> component by component.
|
||||
/// </summary>
|
||||
/// <param name="self">Operand A</param>
|
||||
/// <param name="other">Operand B</param>
|
||||
/// <returns>Result of multiplying both vectors component by component</returns>
|
||||
public static Vector2 Multiply(this in Vector2 self, in Vector2 other)
|
||||
{
|
||||
return new Vector2(self.x * other.x,
|
||||
self.y * other.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides a <see cref="Vector2" /> by another, checking for divisions by 0. Divisions by 0 have a result of 0.
|
||||
/// </summary>
|
||||
/// <param name="self">Dividend</param>
|
||||
/// <param name="divisor">Divisor</param>
|
||||
/// <returns>Result vector</returns>
|
||||
public static Vector2 Divide(this in Vector2 self, in Vector2 divisor)
|
||||
{
|
||||
return self.Multiply(divisor.Inverse());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms an array of floats to a <see cref="Vector2" /> component by component. If there are not enough values to
|
||||
/// read, the remaining values are set to NaN.
|
||||
/// </summary>
|
||||
/// <param name="data">Source data</param>
|
||||
/// <returns>Result vector</returns>
|
||||
public static Vector2 ToVector2(this float[] data)
|
||||
{
|
||||
return data.Length switch
|
||||
{
|
||||
0 => NaN,
|
||||
1 => new Vector2(data[0], float.NaN),
|
||||
_ => new Vector2(data[0], data[1])
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a <see cref="Vector2" /> from a string.
|
||||
/// </summary>
|
||||
/// <param name="s">Source string</param>
|
||||
/// <param name="result">Parsed vector or NaN if there was an error</param>
|
||||
/// <returns>Whether the vector was parsed successfully</returns>
|
||||
public static bool TryParse(string s, out Vector2 result)
|
||||
{
|
||||
try
|
||||
{
|
||||
result = Parse(s);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
result = NaN;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a <see cref="Vector2" /> from a string.
|
||||
/// </summary>
|
||||
/// <param name="s">Source string</param>
|
||||
/// <returns>Parsed vector</returns>
|
||||
public static Vector2 Parse(string s)
|
||||
{
|
||||
s.ThrowIfNullOrWhitespace(nameof(s));
|
||||
|
||||
// Remove the parentheses
|
||||
s = s.TrimStart(' ', '(', '[');
|
||||
s = s.TrimEnd(' ', ')', ']');
|
||||
|
||||
// split the items
|
||||
string[] sArray = s.Split(s_cardinalSeparator, VectorLength);
|
||||
|
||||
// store as an array
|
||||
float[] result = new float[VectorLength];
|
||||
for (int i = 0; i < sArray.Length; ++i)
|
||||
{
|
||||
result[i] = float.TryParse(sArray[i],
|
||||
NumberStyles.Float,
|
||||
CultureInfo.InvariantCulture.NumberFormat,
|
||||
out float f)
|
||||
? f
|
||||
: float.NaN;
|
||||
}
|
||||
|
||||
return result.ToVector2();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a <see cref="Vector2" /> from a string, asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="s">Source string</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
|
||||
/// <returns>Awaitable task returning the parsed vector or null if there was an error</returns>
|
||||
public static Task<Vector2?> ParseAsync(string s, CancellationToken ct = default)
|
||||
{
|
||||
return Task.Run(() => TryParse(s, out Vector2 result) ? result : (Vector2?)null, ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Types & Data
|
||||
|
||||
private const int VectorLength = 2;
|
||||
private const string CardinalSeparator = ",";
|
||||
|
||||
private static readonly char[] s_cardinalSeparator = CardinalSeparator.ToCharArray();
|
||||
private static readonly Vector2 s_nan = float.NaN * Vector2.one;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9d593b8ddf2258041a634b667c08a789
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,172 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="Vector2IntExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UltimateXR.Extensions.System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity.Math
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Vector2Int" /> extensions.
|
||||
/// </summary>
|
||||
public static class Vector2IntExt
|
||||
{
|
||||
#region Public Types & Data
|
||||
|
||||
/// <summary>
|
||||
/// Representation of the negative infinity vector.
|
||||
/// </summary>
|
||||
public static ref readonly Vector2Int NegativeInfinity => ref s_negativeInfinity;
|
||||
|
||||
/// <summary>
|
||||
/// Representation of the positive infinity vector.
|
||||
/// </summary>
|
||||
public static ref readonly Vector2Int PositiveInfinity => ref s_positiveInfinity;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether any vector component stores an infinity value.
|
||||
/// </summary>
|
||||
/// <param name="self">Vector to check</param>
|
||||
/// <returns>Whether any component has an infinity value</returns>
|
||||
public static bool IsInfinity(this in Vector2Int self)
|
||||
{
|
||||
return self.x == int.MinValue || self.x == int.MaxValue || self.y == int.MinValue || self.y == int.MaxValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the absolute values of each vector component.
|
||||
/// </summary>
|
||||
/// <param name="self">Input vector</param>
|
||||
/// <returns>Result vector where each component is the absolute value of the input value component</returns>
|
||||
public static Vector2Int Abs(this in Vector2Int self)
|
||||
{
|
||||
return new Vector2Int(Mathf.Abs(self.x), Mathf.Abs(self.y));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps the vector components between min and max values.
|
||||
/// </summary>
|
||||
/// <param name="self">Input vector whose values to clamp</param>
|
||||
/// <param name="min">Minimum component values</param>
|
||||
/// <param name="max">Maximum component values</param>
|
||||
/// <returns>Clamped vector</returns>
|
||||
public static Vector2Int Clamp(this in Vector2Int self, in Vector2Int min, in Vector2Int max)
|
||||
{
|
||||
int[] result = new int[VectorLength];
|
||||
for (int i = 0; i < VectorLength; ++i)
|
||||
{
|
||||
result[i] = Mathf.Clamp(self[i], min[i], max[i]);
|
||||
}
|
||||
|
||||
return result.ToVector2Int();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces NaN component values with <paramref name="other" /> valid values.
|
||||
/// </summary>
|
||||
/// <param name="self">Vector whose NaN values to replace</param>
|
||||
/// <param name="other">Vector with valid values</param>
|
||||
/// <returns>Result vector</returns>
|
||||
public static Vector2Int FillNanWith(this in Vector2Int self, in Vector2Int other)
|
||||
{
|
||||
int[] result = new int[VectorLength];
|
||||
for (int i = 0; i < VectorLength; ++i)
|
||||
{
|
||||
result[i] = self.x == int.MinValue || self.x == int.MaxValue ? other[i] : self[i];
|
||||
}
|
||||
|
||||
return result.ToVector2Int();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms an array of ints to a <see cref="Vector2Int" /> component by component.
|
||||
/// </summary>
|
||||
/// <param name="data">Source data</param>
|
||||
/// <returns>Result vector</returns>
|
||||
public static Vector2Int ToVector2Int(this int[] data)
|
||||
{
|
||||
Array.Resize(ref data, VectorLength);
|
||||
return new Vector2Int(data[0], data[1]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a <see cref="Vector2Int" /> from a string.
|
||||
/// </summary>
|
||||
/// <param name="s">Source string</param>
|
||||
/// <param name="result">Parsed vector or <see cref="PositiveInfinity" /> if there was an error</param>
|
||||
/// <returns>Whether the vector was parsed successfully</returns>
|
||||
public static bool TryParse(string s, out Vector2Int result)
|
||||
{
|
||||
try
|
||||
{
|
||||
result = Parse(s);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
result = PositiveInfinity;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a <see cref="Vector2Int" /> from a string.
|
||||
/// </summary>
|
||||
/// <param name="s">Source string</param>
|
||||
/// <returns>Parsed vector</returns>
|
||||
public static Vector2Int Parse(string s)
|
||||
{
|
||||
s.ThrowIfNullOrWhitespace(nameof(s));
|
||||
|
||||
// Remove the parentheses
|
||||
s = s.TrimStart(' ', '(', '[');
|
||||
s = s.TrimEnd(' ', ')', ']');
|
||||
|
||||
// split the items
|
||||
string[] sArray = s.Split(s_cardinalSeparator, VectorLength);
|
||||
|
||||
// store as an array
|
||||
int[] result = new int[VectorLength];
|
||||
for (int i = 0; i < sArray.Length; ++i)
|
||||
{
|
||||
result[i] = int.Parse(sArray[i], CultureInfo.InvariantCulture.NumberFormat);
|
||||
}
|
||||
|
||||
return result.ToVector2Int();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a <see cref="Vector2Int" /> from a string, asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="s">Source string</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
|
||||
/// <returns>Awaitable task returning the parsed vector or null if there was an error</returns>
|
||||
public static Task<Vector2Int?> ParseAsync(string s, CancellationToken ct = default)
|
||||
{
|
||||
return Task.Run(() => TryParse(s, out Vector2Int result) ? result : (Vector2Int?)null, ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Types & Data
|
||||
|
||||
private const int VectorLength = 2;
|
||||
private const string CardinalSeparator = ",";
|
||||
|
||||
private static readonly char[] s_cardinalSeparator = CardinalSeparator.ToCharArray();
|
||||
private static readonly Vector2Int s_negativeInfinity = int.MinValue * Vector2Int.one;
|
||||
private static readonly Vector2Int s_positiveInfinity = int.MaxValue * Vector2Int.one;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6951a64e8a7d31648bcde9c54e39eb06
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,727 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="Vector3Ext.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UltimateXR.Core;
|
||||
using UltimateXR.Extensions.System;
|
||||
using UltimateXR.Extensions.System.Math;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity.Math
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Vector3" /> extensions.
|
||||
/// </summary>
|
||||
public static class Vector3Ext
|
||||
{
|
||||
#region Public Types & Data
|
||||
|
||||
/// <summary>
|
||||
/// Represents the NaN vector, an invalid value.
|
||||
/// </summary>
|
||||
public static ref readonly Vector3 NaN => ref s_nan;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the Vector3 with minimum float values per component.
|
||||
/// </summary>
|
||||
public static ref readonly Vector3 MinValue => ref s_minValue;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the Vector3 with maximum float values per component.
|
||||
/// </summary>
|
||||
public static ref readonly Vector3 MaxValue => ref s_maxValue;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Compares two Unity Vector3 objects for equality with a specified precision threshold.
|
||||
/// </summary>
|
||||
/// <param name="a">The first Vector3 to compare</param>
|
||||
/// <param name="b">The second Vector3 to compare</param>
|
||||
/// <param name="precisionThreshold">
|
||||
/// The precision threshold for float comparisons. Defaults to
|
||||
/// <see cref="UxrConstants.Math.DefaultPrecisionThreshold" />.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the Vector3 objects are equal; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// This method performs a component-wise comparison between two Vector3 objects.
|
||||
/// Each component is compared using the specified precision threshold for float comparisons.
|
||||
/// </remarks>
|
||||
public static bool EqualsUsingPrecision(this Vector3 a, Vector3 b, float precisionThreshold = UxrConstants.Math.DefaultPrecisionThreshold)
|
||||
{
|
||||
return Mathf.Abs(a.x - b.x) <= precisionThreshold &&
|
||||
Mathf.Abs(a.y - b.y) <= precisionThreshold &&
|
||||
Mathf.Abs(a.z - b.z) <= precisionThreshold;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given vector has any NaN component.
|
||||
/// </summary>
|
||||
/// <param name="self">Source vector</param>
|
||||
/// <returns>Whether any of the vector components has a NaN value</returns>
|
||||
public static bool IsNaN(this in Vector3 self)
|
||||
{
|
||||
return float.IsNaN(self.x) || float.IsNaN(self.y) || float.IsNaN(self.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given vector has any infinity component.
|
||||
/// </summary>
|
||||
/// <param name="self">Source vector</param>
|
||||
/// <returns>Whether any of the vector components has an infinity value</returns>
|
||||
public static bool IsInfinity(this in Vector3 self)
|
||||
{
|
||||
return float.IsInfinity(self.x) || float.IsInfinity(self.y) || float.IsInfinity(self.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given vector contains valid data.
|
||||
/// </summary>
|
||||
/// <param name="self">Source vector</param>
|
||||
/// <returns>Whether the vector contains all valid values</returns>
|
||||
public static bool IsValid(this in Vector3 self)
|
||||
{
|
||||
return !self.IsNaN() && !self.IsInfinity();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces NaN component values with <paramref name="other" /> valid values.
|
||||
/// </summary>
|
||||
/// <param name="self">Vector whose NaN values to replace</param>
|
||||
/// <param name="other">Vector with valid values</param>
|
||||
/// <returns>Result vector</returns>
|
||||
public static Vector3 FillNanWith(this in Vector3 self, in Vector3 other)
|
||||
{
|
||||
float[] result = new float[VectorLength];
|
||||
for (int i = 0; i < VectorLength; ++i)
|
||||
{
|
||||
result[i] = float.IsNaN(self[i]) ? other[i] : self[i];
|
||||
}
|
||||
|
||||
return result.ToVector3();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the absolute value of each component in a vector.
|
||||
/// </summary>
|
||||
/// <param name="self">Source vector</param>
|
||||
/// <returns>Vector whose components are the absolute values</returns>
|
||||
public static Vector3 Abs(this in Vector3 self)
|
||||
{
|
||||
return new Vector3(Mathf.Abs(self.x), Mathf.Abs(self.y), Mathf.Abs(self.z));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps <see cref="Vector3" /> values component by component.
|
||||
/// </summary>
|
||||
/// <param name="self">Vector whose components to clamp</param>
|
||||
/// <param name="min">Minimum values</param>
|
||||
/// <param name="max">Maximum values</param>
|
||||
/// <returns>Clamped vector</returns>
|
||||
public static Vector3 Clamp(this in Vector3 self, in Vector3 min, in Vector3 max)
|
||||
{
|
||||
float[] result = new float[VectorLength];
|
||||
for (int i = 0; i < VectorLength; ++i)
|
||||
{
|
||||
result[i] = Mathf.Clamp(self[i], min[i], max[i]);
|
||||
}
|
||||
|
||||
return result.ToVector3();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fixes Euler angles so that they are always in the -180, 180 degrees range.
|
||||
/// </summary>
|
||||
/// <param name="self">Euler angles to fix</param>
|
||||
/// <returns>Euler angles in the -180, 180 degrees range</returns>
|
||||
public static Vector3 ToEuler180(this in Vector3 self)
|
||||
{
|
||||
float[] result = new float[VectorLength];
|
||||
|
||||
for (int i = 0; i < VectorLength; ++i)
|
||||
{
|
||||
result[i] = self[i].ToEuler180();
|
||||
}
|
||||
|
||||
return result.ToVector3();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the average of a set of vectors.
|
||||
/// </summary>
|
||||
/// <param name="vectors">Input vectors</param>
|
||||
/// <returns>Vector with components averaged</returns>
|
||||
public static Vector3 Average(params Vector3[] vectors)
|
||||
{
|
||||
return new Vector3(vectors.Average(v => v.x),
|
||||
vectors.Average(v => v.y),
|
||||
vectors.Average(v => v.z));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the average of a set of vectors.
|
||||
/// </summary>
|
||||
/// <param name="vectors">Input vectors</param>
|
||||
/// <param name="defaultIfEmpty">The default value to return if the list is empty</param>
|
||||
/// <returns>Vector with components averaged</returns>
|
||||
public static Vector3 Average(IEnumerable<Vector3> vectors, Vector3 defaultIfEmpty = default)
|
||||
{
|
||||
if (vectors == null || !vectors.Any())
|
||||
{
|
||||
return defaultIfEmpty;
|
||||
}
|
||||
|
||||
return new Vector3(vectors.Average(v => v.x),
|
||||
vectors.Average(v => v.y),
|
||||
vectors.Average(v => v.z));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the maximum values of a set of vectors.
|
||||
/// </summary>
|
||||
/// <param name="vectors">Input vectors</param>
|
||||
/// <returns>Vector with maximum component values</returns>
|
||||
public static Vector3 Max(params Vector3[] vectors)
|
||||
{
|
||||
return new Vector3(vectors.Max(v => v.x),
|
||||
vectors.Max(v => v.y),
|
||||
vectors.Max(v => v.z));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the maximum values of a set of vectors.
|
||||
/// </summary>
|
||||
/// <param name="vectors">Input vectors</param>
|
||||
/// <param name="defaultIfEmpty">The default value to return if the list is empty</param>
|
||||
/// <returns>Vector with maximum component values</returns>
|
||||
public static Vector3 Max(IEnumerable<Vector3> vectors, Vector3 defaultIfEmpty = default)
|
||||
{
|
||||
if (vectors == null || !vectors.Any())
|
||||
{
|
||||
return defaultIfEmpty;
|
||||
}
|
||||
|
||||
return new Vector3(vectors.Max(v => v.x),
|
||||
vectors.Max(v => v.y),
|
||||
vectors.Max(v => v.z));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the minimum values of a set of vectors.
|
||||
/// </summary>
|
||||
/// <param name="vectors">Input vectors</param>
|
||||
/// <returns>Vector with minimum component values</returns>
|
||||
public static Vector3 Min(params Vector3[] vectors)
|
||||
{
|
||||
return new Vector3(vectors.Min(v => v.x),
|
||||
vectors.Min(v => v.y),
|
||||
vectors.Min(v => v.z));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the minimum values of a set of vectors.
|
||||
/// </summary>
|
||||
/// <param name="vectors">Input vectors</param>
|
||||
/// <param name="defaultIfEmpty">The default value to return if the list is empty</param>
|
||||
/// <returns>Vector with minimum component values</returns>
|
||||
public static Vector3 Min(IEnumerable<Vector3> vectors, Vector3 defaultIfEmpty = default)
|
||||
{
|
||||
if (vectors == null || !vectors.Any())
|
||||
{
|
||||
return defaultIfEmpty;
|
||||
}
|
||||
|
||||
return new Vector3(vectors.Min(v => v.x),
|
||||
vectors.Min(v => v.y),
|
||||
vectors.Min(v => v.z));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// returns a vector with all components containing 1/component, checking for divisions by 0. Divisions by 0 have a
|
||||
/// result of 0.
|
||||
/// </summary>
|
||||
/// <param name="self">Source vector</param>
|
||||
/// <returns>Result vector</returns>
|
||||
public static Vector3 Inverse(this in Vector3 self)
|
||||
{
|
||||
return new Vector3(Mathf.Approximately(self.x, 0f) ? 0f : 1f / self.x,
|
||||
Mathf.Approximately(self.y, 0f) ? 0f : 1f / self.y,
|
||||
Mathf.Approximately(self.z, 0f) ? 0f : 1f / self.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of components that are different between two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">First vector</param>
|
||||
/// <param name="b">Second vector</param>
|
||||
/// <returns>The number of components [0, 3] that are different</returns>
|
||||
public static int DifferentComponentCount(Vector3 a, Vector3 b)
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
for (int axisIndex = 0; axisIndex < 3; ++axisIndex)
|
||||
{
|
||||
if (!Mathf.Approximately(a[axisIndex], b[axisIndex]))
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies two <see cref="Vector3" /> component by component.
|
||||
/// </summary>
|
||||
/// <param name="self">Operand A</param>
|
||||
/// <param name="other">Operand B</param>
|
||||
/// <returns>Result of multiplying both vectors component by component</returns>
|
||||
public static Vector3 Multiply(this in Vector3 self, in Vector3 other)
|
||||
{
|
||||
return new Vector3(self.x * other.x,
|
||||
self.y * other.y,
|
||||
self.z * other.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides a <see cref="Vector3" /> by another, checking for divisions by 0. Divisions by 0 have a result of 0.
|
||||
/// </summary>
|
||||
/// <param name="self">Dividend</param>
|
||||
/// <param name="divisor">Divisor</param>
|
||||
/// <returns>Result vector</returns>
|
||||
public static Vector3 Divide(this in Vector3 self, in Vector3 divisor)
|
||||
{
|
||||
return self.Multiply(divisor.Inverse());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms an array of floats to a <see cref="Vector3" /> component by component. If there are not enough values to
|
||||
/// read, the remaining values are set to NaN.
|
||||
/// </summary>
|
||||
/// <param name="data">Source data</param>
|
||||
/// <returns>Result vector</returns>
|
||||
public static Vector3 ToVector3(this float[] data)
|
||||
{
|
||||
return data.Length switch
|
||||
{
|
||||
0 => NaN,
|
||||
1 => new Vector3(data[0], float.NaN, float.NaN),
|
||||
2 => new Vector3(data[0], data[1], float.NaN),
|
||||
_ => new Vector3(data[0], data[1], data[2])
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a <see cref="Vector3" /> from a string.
|
||||
/// </summary>
|
||||
/// <param name="s">Source string</param>
|
||||
/// <param name="result">Parsed vector or <see cref="NaN" /> if there was an error</param>
|
||||
/// <returns>Whether the vector was parsed successfully</returns>
|
||||
public static bool TryParse(string s, out Vector3 result)
|
||||
{
|
||||
try
|
||||
{
|
||||
result = Parse(s);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
result = NaN;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a <see cref="Vector3" /> from a string.
|
||||
/// </summary>
|
||||
/// <param name="s">Source string</param>
|
||||
/// <returns>Parsed vector</returns>
|
||||
public static Vector3 Parse(string s)
|
||||
{
|
||||
s.ThrowIfNullOrWhitespace(nameof(s));
|
||||
|
||||
// Remove the parentheses
|
||||
s = s.TrimStart(' ', '(', '[');
|
||||
s = s.TrimEnd(' ', ')', ']');
|
||||
|
||||
// split the items
|
||||
string[] sArray = s.Split(s_cardinalSeparator, VectorLength);
|
||||
|
||||
// store as an array
|
||||
float[] result = new float[VectorLength];
|
||||
for (int i = 0; i < sArray.Length; ++i)
|
||||
{
|
||||
result[i] = float.TryParse(sArray[i],
|
||||
NumberStyles.Float,
|
||||
CultureInfo.InvariantCulture.NumberFormat,
|
||||
out float f)
|
||||
? f
|
||||
: float.NaN;
|
||||
}
|
||||
|
||||
return result.ToVector3();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a <see cref="Vector3" /> from a string, asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="s">Source string</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
|
||||
/// <returns>Awaitable task returning the parsed vector or null if there was an error</returns>
|
||||
public static Task<Vector3?> ParseAsync(string s, CancellationToken ct = default)
|
||||
{
|
||||
return Task.Run(() => TryParse(s, out Vector3 result) ? result : (Vector3?)null, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the vector which is the dominant negative or positive axis it is mostly pointing towards.
|
||||
/// </summary>
|
||||
/// <param name="vector">Vector to process</param>
|
||||
/// <returns>
|
||||
/// Can return <see cref="Vector3.right" />, <see cref="Vector3.up" />, <see cref="Vector3.forward" />, -
|
||||
/// <see cref="Vector3.right" />, -<see cref="Vector3.up" /> or -<see cref="Vector3.forward" />.
|
||||
/// </returns>
|
||||
public static Vector3 GetClosestAxis(this Vector3 vector)
|
||||
{
|
||||
float absX = Mathf.Abs(vector.x);
|
||||
float absY = Mathf.Abs(vector.y);
|
||||
float absZ = Mathf.Abs(vector.z);
|
||||
|
||||
if (absX > absY)
|
||||
{
|
||||
return absX > absZ ? Mathf.Sign(vector.x) * Vector3.right : Mathf.Sign(vector.z) * Vector3.forward;
|
||||
}
|
||||
|
||||
return absY > absZ ? Mathf.Sign(vector.y) * Vector3.up : Mathf.Sign(vector.z) * Vector3.forward;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a perpendicular vector.
|
||||
/// </summary>
|
||||
/// <param name="vector">Vector to compute another perpendicular to</param>
|
||||
/// <returns>Perpendicular vector in 3D space</returns>
|
||||
public static Vector3 GetPerpendicularVector(this Vector3 vector)
|
||||
{
|
||||
if (Mathf.Approximately(vector.x, 0.0f) == false)
|
||||
{
|
||||
return new Vector3(-vector.y, vector.x, 0.0f);
|
||||
}
|
||||
|
||||
if (Mathf.Approximately(vector.y, 0.0f) == false)
|
||||
{
|
||||
return new Vector3(0.0f, -vector.z, vector.y);
|
||||
}
|
||||
|
||||
return new Vector3(vector.z, 0.0f, -vector.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the signed distance from a point to a plane.
|
||||
/// </summary>
|
||||
/// <param name="point">The point to compute the distance from</param>
|
||||
/// <param name="planePoint">Point in a plane</param>
|
||||
/// <param name="planeNormal">Plane normal</param>
|
||||
/// <returns>Signed distance from a point to a plane</returns>
|
||||
public static float DistanceToPlane(this Vector3 point, Vector3 planePoint, Vector3 planeNormal)
|
||||
{
|
||||
return new Plane(planeNormal, planePoint).GetDistanceToPoint(point);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the distance from a point to a line.
|
||||
/// </summary>
|
||||
/// <param name="point">The point to compute the distance from</param>
|
||||
/// <param name="lineA">Point A in the line</param>
|
||||
/// <param name="lineB">Point B in the line</param>
|
||||
/// <returns>Distance from point to the line</returns>
|
||||
public static float DistanceToLine(this Vector3 point, Vector3 lineA, Vector3 lineB)
|
||||
{
|
||||
return Vector3.Cross(lineB - lineA, point - lineA).magnitude;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the distance from a point to a segment.
|
||||
/// </summary>
|
||||
/// <param name="point">The point to compute the distance from</param>
|
||||
/// <param name="segmentA">Segment start point</param>
|
||||
/// <param name="segmentB">Segment end point</param>
|
||||
/// <returns>Distance from point to the segment</returns>
|
||||
public static float DistanceToSegment(this Vector3 point, Vector3 segmentA, Vector3 segmentB)
|
||||
{
|
||||
Vector3 ab = segmentB - segmentA;
|
||||
Vector3 av = point - segmentA;
|
||||
|
||||
if (Vector3.Dot(av, ab) <= 0.0f)
|
||||
{
|
||||
return av.magnitude;
|
||||
}
|
||||
|
||||
Vector3 bv = point - segmentB;
|
||||
|
||||
if (Vector3.Dot(bv, ab) >= 0.0)
|
||||
{
|
||||
return bv.magnitude;
|
||||
}
|
||||
|
||||
return Vector3.Cross(ab, av).magnitude / ab.magnitude;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the closest point in a segment to another point.
|
||||
/// </summary>
|
||||
/// <param name="point">The point to project</param>
|
||||
/// <param name="segmentA">Segment start point</param>
|
||||
/// <param name="segmentB">Segment end point</param>
|
||||
/// <returns>Closest point in the segment</returns>
|
||||
public static Vector3 ProjectOnSegment(this Vector3 point, Vector3 segmentA, Vector3 segmentB)
|
||||
{
|
||||
Vector3 ab = segmentB - segmentA;
|
||||
Vector3 av = point - segmentA;
|
||||
|
||||
if (Vector3.Dot(av, ab) <= 0.0f)
|
||||
{
|
||||
return segmentA;
|
||||
}
|
||||
|
||||
Vector3 bv = point - segmentB;
|
||||
|
||||
if (Vector3.Dot(bv, ab) >= 0.0)
|
||||
{
|
||||
return segmentB;
|
||||
}
|
||||
|
||||
return segmentA + Vector3.Project(av, ab.normalized);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the closest point in a line to another point.
|
||||
/// </summary>
|
||||
/// <param name="point">The point to project</param>
|
||||
/// <param name="pointInLine">Point in the line</param>
|
||||
/// <param name="lineDirection">Line direction</param>
|
||||
/// <returns>Point projected on the line</returns>
|
||||
public static Vector3 ProjectOnLine(this Vector3 point, Vector3 pointInLine, Vector3 lineDirection)
|
||||
{
|
||||
return pointInLine + Vector3.Project(point - pointInLine, lineDirection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a point is inside a sphere. Supports spheres without uniform scaling.
|
||||
/// </summary>
|
||||
/// <param name="point">Point to check</param>
|
||||
/// <param name="sphere">Sphere collider to test against</param>
|
||||
/// <returns>Boolean telling whether the point is inside</returns>
|
||||
public static bool IsInsideSphere(this Vector3 point, SphereCollider sphere)
|
||||
{
|
||||
Vector3 localPos = sphere.transform.InverseTransformPoint(point);
|
||||
return localPos.magnitude <= sphere.radius;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a point is inside of a BoxCollider.
|
||||
/// </summary>
|
||||
/// <param name="point">Point in world coordinates</param>
|
||||
/// <param name="box">Box collider to test against</param>
|
||||
/// <param name="margin">Optional margin to be added to the each of the box sides</param>
|
||||
/// <param name="marginIsWorld">Whether the margin is specified in world coordinates or local</param>
|
||||
/// <returns>Whether point is inside</returns>
|
||||
public static bool IsInsideBox(this Vector3 point, BoxCollider box, Vector3 margin = default, bool marginIsWorld = true)
|
||||
{
|
||||
if (box == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector3 localPos = box.transform.InverseTransformPoint(point);
|
||||
|
||||
if (marginIsWorld && box.transform.lossyScale != Vector3.one)
|
||||
{
|
||||
Vector3 pointPlusX = box.transform.InverseTransformPoint(point + box.transform.right);
|
||||
Vector3 pointPlusY = box.transform.InverseTransformPoint(point + box.transform.up);
|
||||
Vector3 pointPlusZ = box.transform.InverseTransformPoint(point + box.transform.forward);
|
||||
|
||||
margin.x *= Vector3.Distance(localPos, pointPlusX);
|
||||
margin.y *= Vector3.Distance(localPos, pointPlusY);
|
||||
margin.z *= Vector3.Distance(localPos, pointPlusZ);
|
||||
}
|
||||
|
||||
if (localPos.x - box.center.x >= -box.size.x * 0.5f - margin.x && localPos.x - box.center.x <= box.size.x * 0.5f + margin.x)
|
||||
{
|
||||
if (localPos.y - box.center.y >= -box.size.y * 0.5f - margin.y && localPos.y - box.center.y <= box.size.y * 0.5f + margin.y)
|
||||
{
|
||||
if (localPos.z - box.center.z >= -box.size.z * 0.5f - margin.z && localPos.z - box.center.z <= box.size.z * 0.5f + margin.z)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a point is inside of a box.
|
||||
/// </summary>
|
||||
/// <param name="point">Point in world coordinates</param>
|
||||
/// <param name="boxPosition">The box position in world space</param>
|
||||
/// <param name="boxRotation">The box rotation in world space</param>
|
||||
/// <param name="boxScale">The box scale</param>
|
||||
/// <param name="boxCenter">The box center in local box coordinates</param>
|
||||
/// <param name="boxSize">The box size in local box coordinates</param>
|
||||
/// <param name="margin">Optional margin to be added to the each of the box sides</param>
|
||||
/// <returns>True if it is inside, false if not</returns>
|
||||
public static bool IsInsideBox(this Vector3 point,
|
||||
Vector3 boxPosition,
|
||||
Quaternion boxRotation,
|
||||
Vector3 boxScale,
|
||||
Vector3 boxCenter,
|
||||
Vector3 boxSize,
|
||||
Vector3 margin = default)
|
||||
{
|
||||
Matrix4x4 boxMatrix = Matrix4x4.TRS(boxPosition, boxRotation, boxScale);
|
||||
Matrix4x4 inverseBoxMatrix = boxMatrix.inverse;
|
||||
Vector3 localPos = inverseBoxMatrix.MultiplyPoint(point);
|
||||
|
||||
if (localPos.x - boxCenter.x >= -boxSize.x * 0.5f - margin.x && localPos.x - boxCenter.x <= boxSize.x * 0.5f + margin.x)
|
||||
{
|
||||
if (localPos.y - boxCenter.y >= -boxSize.y * 0.5f - margin.y && localPos.y - boxCenter.y <= boxSize.y * 0.5f + margin.y)
|
||||
{
|
||||
if (localPos.z - boxCenter.z >= -boxSize.z * 0.5f - margin.z && localPos.z - boxCenter.z <= boxSize.z * 0.5f + margin.z)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a point is inside of a BoxCollider. If it is outside, it is clamped to remain inside.
|
||||
/// </summary>
|
||||
/// <param name="point">Point in world coordinates</param>
|
||||
/// <param name="box">Box collider to test against</param>
|
||||
/// <returns>Point clamped inside given box volume</returns>
|
||||
public static Vector3 ClampToBox(this Vector3 point, BoxCollider box)
|
||||
{
|
||||
if (box == null)
|
||||
{
|
||||
return point;
|
||||
}
|
||||
|
||||
Vector3 pos = box.transform.InverseTransformPoint(point);
|
||||
Vector3 center = box.center;
|
||||
Vector3 halfBoxSize = box.size * 0.5f;
|
||||
|
||||
if (pos.x < center.x - halfBoxSize.x)
|
||||
{
|
||||
pos.x = center.x - halfBoxSize.x;
|
||||
}
|
||||
|
||||
if (pos.x > center.x + halfBoxSize.x)
|
||||
{
|
||||
pos.x = center.x + halfBoxSize.x;
|
||||
}
|
||||
|
||||
if (pos.y < center.y - halfBoxSize.y)
|
||||
{
|
||||
pos.y = center.y - halfBoxSize.y;
|
||||
}
|
||||
|
||||
if (pos.y > center.y + halfBoxSize.y)
|
||||
{
|
||||
pos.y = center.y + halfBoxSize.y;
|
||||
}
|
||||
|
||||
if (pos.z < center.z - halfBoxSize.z)
|
||||
{
|
||||
pos.z = center.z - halfBoxSize.z;
|
||||
}
|
||||
|
||||
if (pos.z > center.z + halfBoxSize.z)
|
||||
{
|
||||
pos.z = center.z + halfBoxSize.z;
|
||||
}
|
||||
|
||||
return box.transform.TransformPoint(pos);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a point is inside of a SphereCollider. If it is outside, it is clamped to remain inside.
|
||||
/// </summary>
|
||||
/// <param name="point">Point in world coordinates</param>
|
||||
/// <param name="sphere">Sphere collider to test against</param>
|
||||
/// <returns>Point restricted to the given sphere volume</returns>
|
||||
public static Vector3 ClampToSphere(this Vector3 point, SphereCollider sphere)
|
||||
{
|
||||
if (sphere == null)
|
||||
{
|
||||
return point;
|
||||
}
|
||||
|
||||
Vector3 pos = sphere.transform.InverseTransformPoint(point);
|
||||
Vector3 center = sphere.center;
|
||||
|
||||
float distance = Vector3.Distance(center, pos);
|
||||
|
||||
if (distance > sphere.radius)
|
||||
{
|
||||
pos = center + (pos - center).normalized * sphere.radius;
|
||||
return sphere.transform.TransformPoint(pos);
|
||||
}
|
||||
|
||||
return point;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the rotation of a direction around an axis.
|
||||
/// </summary>
|
||||
/// <param name="direction">Direction to rotate</param>
|
||||
/// <param name="axis">The rotation axis to use for the rotation</param>
|
||||
/// <param name="degrees">Rotation angle</param>
|
||||
/// <returns>Rotated direction</returns>
|
||||
public static Vector3 GetRotationAround(this Vector3 direction, Vector3 axis, float degrees)
|
||||
{
|
||||
return Quaternion.AngleAxis(degrees, axis) * direction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the rotation of a point around a pivot and an axis.
|
||||
/// </summary>
|
||||
/// <param name="point">Point to rotate</param>
|
||||
/// <param name="pivot">Pivot to rotate it around to</param>
|
||||
/// <param name="axis">The rotation axis to use for the rotation</param>
|
||||
/// <param name="degrees">Rotation angle</param>
|
||||
/// <returns>Rotated point</returns>
|
||||
public static Vector3 GetRotationAround(this Vector3 point, Vector3 pivot, Vector3 axis, float degrees)
|
||||
{
|
||||
Vector3 dir = point - pivot;
|
||||
dir = Quaternion.AngleAxis(degrees, axis) * dir;
|
||||
return dir + pivot;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Types & Data
|
||||
|
||||
private const int VectorLength = 3;
|
||||
private const string CardinalSeparator = ",";
|
||||
|
||||
private static readonly char[] s_cardinalSeparator = CardinalSeparator.ToCharArray();
|
||||
private static readonly Vector3 s_nan = float.NaN * Vector3.one;
|
||||
private static readonly Vector3 s_minValue = new Vector3(float.MinValue, float.MinValue, float.MinValue);
|
||||
private static readonly Vector3 s_maxValue = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7392a554436d3d248aa19a303cdc65e5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,174 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="Vector3IntExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UltimateXR.Extensions.System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity.Math
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Vector3Int" /> extensions.
|
||||
/// </summary>
|
||||
public static class Vector3IntExt
|
||||
{
|
||||
#region Public Types & Data
|
||||
|
||||
/// <summary>
|
||||
/// Representation of the minimum int values per component.
|
||||
/// </summary>
|
||||
public static ref readonly Vector3Int MinValue => ref s_minValue;
|
||||
|
||||
/// <summary>
|
||||
/// Representation of the maximum int values per component.
|
||||
/// </summary>
|
||||
public static ref readonly Vector3Int MaxValue => ref s_maxValue;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether any vector component stores an infinity value.
|
||||
/// </summary>
|
||||
/// <param name="self">Vector to check</param>
|
||||
/// <returns>Whether any component has an infinity value</returns>
|
||||
public static bool IsInfinity(this in Vector3Int self)
|
||||
{
|
||||
return self.x == int.MinValue || self.x == int.MaxValue ||
|
||||
self.y == int.MinValue || self.y == int.MaxValue ||
|
||||
self.z == int.MinValue || self.z == int.MaxValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the absolute values of each vector component.
|
||||
/// </summary>
|
||||
/// <param name="self">Input vector</param>
|
||||
/// <returns>Result vector where each component is the absolute value of the input value component</returns>
|
||||
public static Vector3Int Abs(this in Vector3Int self)
|
||||
{
|
||||
return new Vector3Int(Mathf.Abs(self.x), Mathf.Abs(self.y), Mathf.Abs(self.z));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps the vector components between min and max values.
|
||||
/// </summary>
|
||||
/// <param name="self">Input vector whose values to clamp</param>
|
||||
/// <param name="min">Minimum component values</param>
|
||||
/// <param name="max">Maximum component values</param>
|
||||
/// <returns>Clamped vector</returns>
|
||||
public static Vector3Int Clamp(this in Vector3Int self, in Vector3Int min, in Vector3Int max)
|
||||
{
|
||||
int[] result = new int[VectorLength];
|
||||
for (int i = 0; i < VectorLength; ++i)
|
||||
{
|
||||
result[i] = Mathf.Clamp(self[i], min[i], max[i]);
|
||||
}
|
||||
|
||||
return result.ToVector3Int();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces NaN component values with <paramref name="other" /> valid values.
|
||||
/// </summary>
|
||||
/// <param name="self">Vector whose NaN values to replace</param>
|
||||
/// <param name="other">Vector with valid values</param>
|
||||
/// <returns>Result vector</returns>
|
||||
public static Vector3Int FillNaNWith(this in Vector3Int self, in Vector3Int other)
|
||||
{
|
||||
int[] result = new int[VectorLength];
|
||||
for (int i = 0; i < VectorLength; ++i)
|
||||
{
|
||||
result[i] = self.x == int.MinValue || self.x == int.MaxValue ? other[i] : self[i];
|
||||
}
|
||||
|
||||
return result.ToVector3Int();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms an array of ints to a <see cref="Vector3Int" /> component by component.
|
||||
/// </summary>
|
||||
/// <param name="data">Source data</param>
|
||||
/// <returns>Result vector</returns>
|
||||
public static Vector3Int ToVector3Int(this int[] data)
|
||||
{
|
||||
Array.Resize(ref data, VectorLength);
|
||||
return new Vector3Int(data[0], data[1], data[2]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a <see cref="Vector3Int" /> from a string.
|
||||
/// </summary>
|
||||
/// <param name="s">Source string</param>
|
||||
/// <param name="result">Parsed vector or <see cref="MaxValue" /> if there was an error</param>
|
||||
/// <returns>Whether the vector was parsed successfully</returns>
|
||||
public static bool TryParse(string s, out Vector3Int result)
|
||||
{
|
||||
try
|
||||
{
|
||||
result = Parse(s);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
result = MaxValue;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a <see cref="Vector3Int" /> from a string.
|
||||
/// </summary>
|
||||
/// <param name="s">Source string</param>
|
||||
/// <returns>Parsed vector</returns>
|
||||
public static Vector3Int Parse(string s)
|
||||
{
|
||||
s.ThrowIfNullOrWhitespace(nameof(s));
|
||||
|
||||
// Remove the parentheses
|
||||
s = s.TrimStart(' ', '(', '[');
|
||||
s = s.TrimEnd(' ', ')', ']');
|
||||
|
||||
// split the items
|
||||
string[] sArray = s.Split(s_cardinalSeparator, VectorLength);
|
||||
|
||||
// store as an array
|
||||
int[] result = new int[VectorLength];
|
||||
for (int i = 0; i < sArray.Length; ++i)
|
||||
{
|
||||
result[i] = int.Parse(sArray[i], CultureInfo.InvariantCulture.NumberFormat);
|
||||
}
|
||||
|
||||
return result.ToVector3Int();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a <see cref="Vector3Int" /> from a string, asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="s">Source string</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
|
||||
/// <returns>Awaitable task returning the parsed vector or null if there was an error</returns>
|
||||
public static Task<Vector3Int?> ParseAsync(string s, CancellationToken ct = default)
|
||||
{
|
||||
return Task.Run(() => TryParse(s, out Vector3Int result) ? result : (Vector3Int?)null, ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Types & Data
|
||||
|
||||
private const int VectorLength = 3;
|
||||
private const string CardinalSeparator = ",";
|
||||
|
||||
private static readonly char[] s_cardinalSeparator = CardinalSeparator.ToCharArray();
|
||||
private static readonly Vector3Int s_minValue = int.MinValue * Vector3Int.one;
|
||||
private static readonly Vector3Int s_maxValue = int.MaxValue * Vector3Int.one;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dec4dbd92ff86464196213d4637743e8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,271 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="Vector4Ext.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UltimateXR.Core;
|
||||
using UltimateXR.Extensions.System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity.Math
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Vector4" /> extensions.
|
||||
/// </summary>
|
||||
public static class Vector4Ext
|
||||
{
|
||||
#region Public Types & Data
|
||||
|
||||
/// <summary>
|
||||
/// Represents a NaN vector.
|
||||
/// </summary>
|
||||
public static ref readonly Vector4 NaN => ref s_nan;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Compares two Unity Vector4 objects for equality with a specified precision threshold.
|
||||
/// </summary>
|
||||
/// <param name="a">The first Vector4 to compare</param>
|
||||
/// <param name="b">The second Vector4 to compare</param>
|
||||
/// <param name="precisionThreshold">
|
||||
/// The precision threshold for float comparisons. Defaults to
|
||||
/// <see cref="UxrConstants.Math.DefaultPrecisionThreshold" />.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the Vector4 objects are equal; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// This method performs a component-wise comparison between two Vector4 objects.
|
||||
/// Each component is compared using the specified precision threshold for float comparisons.
|
||||
/// </remarks>
|
||||
public static bool EqualsUsingPrecision(this Vector4 a, Vector4 b, float precisionThreshold = UxrConstants.Math.DefaultPrecisionThreshold)
|
||||
{
|
||||
return Mathf.Abs(a.x - b.x) <= precisionThreshold &&
|
||||
Mathf.Abs(a.y - b.y) <= precisionThreshold &&
|
||||
Mathf.Abs(a.z - b.z) <= precisionThreshold &&
|
||||
Mathf.Abs(a.w - b.w) <= precisionThreshold;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given vector has any NaN component.
|
||||
/// </summary>
|
||||
/// <param name="self">Source vector</param>
|
||||
/// <returns>Whether any of the vector components has a NaN value</returns>
|
||||
public static bool IsNaN(this in Vector4 self)
|
||||
{
|
||||
return float.IsNaN(self.x) || float.IsNaN(self.y) || float.IsNaN(self.z) || float.IsNaN(self.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given vector has any infinity component.
|
||||
/// </summary>
|
||||
/// <param name="self">Source vector</param>
|
||||
/// <returns>Whether any of the vector components has an infinity value</returns>
|
||||
public static bool IsInfinity(this in Vector4 self)
|
||||
{
|
||||
return float.IsInfinity(self.x) || float.IsInfinity(self.y) || float.IsInfinity(self.z) || float.IsInfinity(self.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given vector contains valid data.
|
||||
/// </summary>
|
||||
/// <param name="self">Source vector</param>
|
||||
/// <returns>Whether the vector contains all valid values</returns>
|
||||
public static bool IsValid(this in Vector4 self)
|
||||
{
|
||||
return !self.IsNaN() && !self.IsInfinity();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces NaN component values with <paramref name="other" /> valid values.
|
||||
/// </summary>
|
||||
/// <param name="self">Vector whose NaN values to replace</param>
|
||||
/// <param name="other">Vector with valid values</param>
|
||||
/// <returns>Result vector</returns>
|
||||
public static Vector4 FillNanWith(this in Vector4 self, in Vector4 other)
|
||||
{
|
||||
float[] result = new float[VectorLength];
|
||||
for (int i = 0; i < VectorLength; ++i)
|
||||
{
|
||||
result[i] = float.IsNaN(self[i]) ? other[i] : self[i];
|
||||
}
|
||||
|
||||
return result.ToVector4();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the absolute value of each component in a vector.
|
||||
/// </summary>
|
||||
/// <param name="self">Source vector</param>
|
||||
/// <returns>Vector whose components are the absolute values</returns>
|
||||
public static Vector4 Abs(this in Vector4 self)
|
||||
{
|
||||
return new Vector4(Mathf.Abs(self.x), Mathf.Abs(self.y), Mathf.Abs(self.z), Mathf.Abs(self.w));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps <see cref="Vector4" /> values component by component.
|
||||
/// </summary>
|
||||
/// <param name="self">Vector whose components to clamp</param>
|
||||
/// <param name="min">Minimum values</param>
|
||||
/// <param name="max">Maximum values</param>
|
||||
/// <returns>Clamped vector</returns>
|
||||
public static Vector4 Clamp(this in Vector4 self, in Vector4 min, in Vector4 max)
|
||||
{
|
||||
float[] result = new float[VectorLength];
|
||||
for (int i = 0; i < VectorLength; ++i)
|
||||
{
|
||||
result[i] = Mathf.Clamp(self[i], min[i], max[i]);
|
||||
}
|
||||
|
||||
return result.ToVector4();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// returns a vector with all components containing 1/component, checking for divisions by 0. Divisions by 0 have a
|
||||
/// result of 0.
|
||||
/// </summary>
|
||||
/// <param name="self">Source vector</param>
|
||||
/// <returns>Result vector</returns>
|
||||
public static Vector4 Inverse(this in Vector4 self)
|
||||
{
|
||||
return new Vector4(Mathf.Approximately(self.x, 0f) ? 0f : 1f / self.x,
|
||||
Mathf.Approximately(self.y, 0f) ? 0f : 1f / self.y,
|
||||
Mathf.Approximately(self.z, 0f) ? 0f : 1f / self.z,
|
||||
Mathf.Approximately(self.w, 0f) ? 0f : 1f / self.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies two <see cref="Vector4" /> component by component.
|
||||
/// </summary>
|
||||
/// <param name="self">Operand A</param>
|
||||
/// <param name="other">Operand B</param>
|
||||
/// <returns>Result of multiplying both vectors component by component</returns>
|
||||
public static Vector4 Multiply(this in Vector4 self, in Vector4 other)
|
||||
{
|
||||
return new Vector4(self.x * other.x,
|
||||
self.y * other.y,
|
||||
self.z * other.z,
|
||||
self.w * other.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides a <see cref="Vector4" /> by another, checking for divisions by 0. Divisions by 0 have a result of 0.
|
||||
/// </summary>
|
||||
/// <param name="self">Dividend</param>
|
||||
/// <param name="divisor">Divisor</param>
|
||||
/// <returns>Result vector</returns>
|
||||
public static Vector4 Divide(this in Vector4 self, in Vector4 divisor)
|
||||
{
|
||||
return self.Multiply(divisor.Inverse());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Vector4 to a Quaternion component by component.
|
||||
/// </summary>
|
||||
/// <param name="self">Source vector</param>
|
||||
/// <returns>Quaternion result</returns>
|
||||
public static Quaternion ToQuaternion(this in Vector4 self)
|
||||
{
|
||||
return new Quaternion(self.x, self.y, self.z, self.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms an array of floats to a <see cref="Vector4" /> component by component. If there are not enough values to
|
||||
/// read, the remaining values are set to NaN.
|
||||
/// </summary>
|
||||
/// <param name="data">Source data</param>
|
||||
/// <returns>Result vector</returns>
|
||||
public static Vector4 ToVector4(this float[] data)
|
||||
{
|
||||
return data.Length switch
|
||||
{
|
||||
0 => NaN,
|
||||
1 => new Vector4(data[0], float.NaN, float.NaN, float.NaN),
|
||||
2 => new Vector4(data[0], data[1], float.NaN, float.NaN),
|
||||
3 => new Vector4(data[0], data[1], data[2], float.NaN),
|
||||
_ => new Vector4(data[0], data[1], data[2], data[3])
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a <see cref="Vector4" /> from a string.
|
||||
/// </summary>
|
||||
/// <param name="s">Source string</param>
|
||||
/// <param name="result">Parsed vector or NaN if there was an error</param>
|
||||
/// <returns>Whether the vector was parsed successfully</returns>
|
||||
public static bool TryParse(string s, out Vector4 result)
|
||||
{
|
||||
try
|
||||
{
|
||||
result = Parse(s);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
result = NaN;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a <see cref="Vector4" /> from a string.
|
||||
/// </summary>
|
||||
/// <param name="s">Source string</param>
|
||||
/// <returns>Parsed vector</returns>
|
||||
public static Vector4 Parse(string s)
|
||||
{
|
||||
s.ThrowIfNullOrWhitespace(nameof(s));
|
||||
|
||||
// Remove the parentheses
|
||||
s = s.TrimStart(' ', '(', '[');
|
||||
s = s.TrimEnd(' ', ')', ']');
|
||||
|
||||
// split the items
|
||||
string[] sArray = s.Split(s_cardinalSeparator, VectorLength);
|
||||
|
||||
// store as an array
|
||||
float[] result = new float[VectorLength];
|
||||
for (int i = 0; i < sArray.Length; ++i)
|
||||
{
|
||||
result[i] = float.TryParse(sArray[i],
|
||||
NumberStyles.Float,
|
||||
CultureInfo.InvariantCulture.NumberFormat,
|
||||
out float f)
|
||||
? f
|
||||
: float.NaN;
|
||||
}
|
||||
|
||||
return result.ToVector4();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a <see cref="Vector4" /> from a string, asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="s">Source string</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
|
||||
/// <returns>Awaitable task returning the parsed vector or null if there was an error</returns>
|
||||
public static Task<Vector4?> ParseAsync(string s, CancellationToken ct = default)
|
||||
{
|
||||
return Task.Run(() => TryParse(s, out Vector4 result) ? result : (Vector4?)null, ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Types & Data
|
||||
|
||||
private const int VectorLength = 4;
|
||||
private const string CardinalSeparator = ",";
|
||||
|
||||
private static readonly char[] s_cardinalSeparator = CardinalSeparator.ToCharArray();
|
||||
private static readonly Vector4 s_nan = float.NaN * Vector4.one;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4fd65efcccc44c04db1fb0623a460cf3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,72 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="MonoBehaviourExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UltimateXR.Animation.Interpolation;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="MonoBehaviour" /> extensions.
|
||||
/// </summary>
|
||||
public static class MonoBehaviourExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Enables/disabled the component if it isn't enabled already.
|
||||
/// </summary>
|
||||
/// <param name="self">Component to enable/disable</param>
|
||||
/// <param name="enable">Whether to enable or disable the component</param>
|
||||
public static void CheckSetEnabled(this MonoBehaviour self, bool enable)
|
||||
{
|
||||
if (self.enabled != enable)
|
||||
{
|
||||
self.enabled = enable;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Coroutines
|
||||
|
||||
/// <summary>
|
||||
/// Creates a coroutine that simplifies executing a loop during a certain amount of time.
|
||||
/// </summary>
|
||||
/// <param name="monoBehaviour">Caller</param>
|
||||
/// <param name="durationSeconds">Time in seconds of the interpolation</param>
|
||||
/// <param name="loopAction">
|
||||
/// The action to perform on each loop step. The action receives
|
||||
/// the interpolation value t [0.0, 1.0] as parameter.
|
||||
/// </param>
|
||||
/// <param name="easing">Easing to use in the interpolation (linear by default)</param>
|
||||
/// <param name="forceLastT1">Forces a last loop step with t = 1.0f exactly.</param>
|
||||
/// <returns>Coroutine enumerator</returns>
|
||||
public static IEnumerator LoopCoroutine(this MonoBehaviour monoBehaviour,
|
||||
float durationSeconds,
|
||||
Action<float> loopAction,
|
||||
UxrEasing easing = UxrEasing.Linear,
|
||||
bool forceLastT1 = false)
|
||||
{
|
||||
float startTime = Time.time;
|
||||
|
||||
while (Time.time - startTime < durationSeconds)
|
||||
{
|
||||
float t = UxrInterpolator.Interpolate(0.0f, 1.0f, Time.time - startTime, new UxrInterpolationSettings(durationSeconds, 0.0f, easing));
|
||||
loopAction(t);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (forceLastT1)
|
||||
{
|
||||
loopAction(1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e84a6c50aec209a44b24370d2a16fe30
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,96 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="ObjectExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace UltimateXR.Extensions.Unity
|
||||
{
|
||||
/// <summary>
|
||||
/// Unity <see cref="UnityEngine.Object" /> extensions
|
||||
/// </summary>
|
||||
public static class ObjectExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// Assigns a serialized property value if code that only executes in the editor.
|
||||
/// </summary>
|
||||
/// <param name="self">The object (GameObject or component) with the serialized property</param>
|
||||
/// <param name="propertyName">The property name</param>
|
||||
/// <param name="assigner">Action that gets the serialized property as argument and enables to assign any value</param>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// component.AssignSerializedProperty("_myBoolVar", p => p.boolValue = true);
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static void AssignSerializedProperty(this Object self, string propertyName, Action<SerializedProperty> assigner)
|
||||
{
|
||||
SerializedObject serializedObject = new SerializedObject(self);
|
||||
SerializedProperty serializedProperty = serializedObject.FindProperty(propertyName);
|
||||
|
||||
if (serializedProperty == null)
|
||||
{
|
||||
Debug.LogError($"{nameof(AssignSerializedProperty)}(): Cannot find property {propertyName}");
|
||||
return;
|
||||
}
|
||||
|
||||
assigner.Invoke(serializedProperty);
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Controls whether to show a given object in the inspector.
|
||||
/// </summary>
|
||||
/// <param name="self">The object to show</param>
|
||||
/// <param name="show">Whether to show the object or now</param>
|
||||
public static void ShowInInspector(this Object self, bool show = true)
|
||||
{
|
||||
if (show)
|
||||
{
|
||||
self.hideFlags &= ~HideFlags.HideInInspector;
|
||||
}
|
||||
else
|
||||
{
|
||||
self.hideFlags |= HideFlags.HideInInspector;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Controls whether to show a given object in the inspector and whether it is editable.
|
||||
/// </summary>
|
||||
/// <param name="self">The object to set</param>
|
||||
/// <param name="show">Whether to show it in the inspector</param>
|
||||
/// <param name="editable">Whether it is editable</param>
|
||||
public static void ShowInInspector(this Object self, bool show, bool editable)
|
||||
{
|
||||
if (show)
|
||||
{
|
||||
self.hideFlags &= ~HideFlags.HideInInspector;
|
||||
}
|
||||
else
|
||||
{
|
||||
self.hideFlags |= HideFlags.HideInInspector;
|
||||
}
|
||||
|
||||
if (editable)
|
||||
{
|
||||
self.hideFlags &= ~HideFlags.NotEditable;
|
||||
}
|
||||
else
|
||||
{
|
||||
self.hideFlags |= HideFlags.NotEditable;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f6ff9cfc3eed1a45ae6c4cf96c6e100
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a266ccd01a014964a81614013cb803b2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,182 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="Color32Ext.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UltimateXR.Extensions.System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Color32" /> extensions.
|
||||
/// </summary>
|
||||
public static class Color32Ext
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Transforms an array of bytes to a <see cref="Color32" /> component by component. If there are not enough values to
|
||||
/// read, the remaining values are set to <see cref="byte.MinValue" /> (0) for RGB and <see cref="byte.MaxValue" />
|
||||
/// (255) for A.
|
||||
/// </summary>
|
||||
/// <param name="data">Source data</param>
|
||||
/// <returns>Result color</returns>
|
||||
public static Color32 ToColor32(this byte[] data)
|
||||
{
|
||||
switch (data.Length)
|
||||
{
|
||||
case 0: return default;
|
||||
case 1: return new Color32(data[0], byte.MinValue, byte.MinValue, byte.MaxValue);
|
||||
case 2: return new Color32(data[0], data[1], byte.MinValue, byte.MaxValue);
|
||||
case 3: return new Color32(data[0], data[1], data[2], byte.MaxValue);
|
||||
default: return new Color32(data[0], data[1], data[2], data[3]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms a <see cref="Color32" /> value into the int value it encodes the color in.
|
||||
/// </summary>
|
||||
/// <param name="self">Color</param>
|
||||
/// <returns>Int value</returns>
|
||||
public static int ToInt(this in Color32 self)
|
||||
{
|
||||
return self.r << 24 | self.g << 16 | self.b << 8 | self.a;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps <see cref="Color32" /> values component by component.
|
||||
/// </summary>
|
||||
/// <param name="self">Color whose components to clamp</param>
|
||||
/// <param name="min">Minimum RGB values</param>
|
||||
/// <param name="max">Maximum RGB values</param>
|
||||
/// <returns>Clamped color</returns>
|
||||
public static Color32 Clamp(this in Color32 self, in Color32 min, in Color32 max)
|
||||
{
|
||||
byte[] result = new byte[VectorLength];
|
||||
for (int i = 0; i < VectorLength; ++i)
|
||||
{
|
||||
result[i] = (byte)Mathf.Clamp(self[i], min[i], max[i]);
|
||||
}
|
||||
|
||||
return result.ToColor32();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies two colors by multiplying each component.
|
||||
/// </summary>
|
||||
/// <param name="self">Operand A</param>
|
||||
/// <param name="other">Operand B</param>
|
||||
/// <returns>Result color</returns>
|
||||
public static Color32 Multiply(this in Color32 self, in Color32 other)
|
||||
{
|
||||
return new Color32((byte)(self.r * other.r),
|
||||
(byte)(self.g * other.g),
|
||||
(byte)(self.b * other.b),
|
||||
(byte)(self.a * other.a));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares two colors.
|
||||
/// </summary>
|
||||
/// <param name="self">First color to compare</param>
|
||||
/// <param name="other">Second color to compare</param>
|
||||
/// <returns>Whether the two colors are the same</returns>
|
||||
public static bool IsSameColor(this in Color32 self, in Color32 other)
|
||||
{
|
||||
return self.r == other.r && self.g == other.g && self.b == other.b && self.a == other.a;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the color to a HTML color value (#RRGGBB or #RRGGBBAA).
|
||||
/// </summary>
|
||||
/// <param name="self">Color to convert</param>
|
||||
/// <returns>HTML color string</returns>
|
||||
public static string ToHtml(this in Color32 self)
|
||||
{
|
||||
return self.a == 255 ? string.Format(StringFormatRGB, self.r, self.g, self.b) : string.Format(StringFormatRGBA, self.r, self.g, self.b, self.a);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a <see cref="Color32" /> from an HTML string (#RRGGBB or #RRGGBBAA).
|
||||
/// </summary>
|
||||
/// <param name="html">Source HTML string</param>
|
||||
/// <param name="result">Parsed color or the default color value if there was an error</param>
|
||||
/// <returns>Whether the color was parsed successfully</returns>
|
||||
public static bool TryParse(string html, out Color32 result)
|
||||
{
|
||||
try
|
||||
{
|
||||
result = Parse(html);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a <see cref="Color32" /> from an HTML string (#RRGGBB or #RRGGBBAA).
|
||||
/// </summary>
|
||||
/// <param name="html">Source HTML string</param>
|
||||
/// <returns>The parsed color</returns>
|
||||
/// <exception cref="FormatException">The string had an incorrect format</exception>
|
||||
public static Color32 Parse(string html)
|
||||
{
|
||||
html.ThrowIfNull(nameof(html));
|
||||
|
||||
Match match = _regex.Match(html);
|
||||
if (!match.Success)
|
||||
{
|
||||
throw new FormatException($"Input string [{html}] does not have the right format: #RRGGBB or #RRGGBBAA");
|
||||
}
|
||||
|
||||
byte[] colorBytes = new byte[VectorLength];
|
||||
|
||||
for (int i = 0; i < VectorLength - 1; ++i)
|
||||
{
|
||||
string hex = match.Groups[i + 1].Value;
|
||||
colorBytes[i] = byte.Parse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture.NumberFormat);
|
||||
}
|
||||
|
||||
string aa = match.Groups[VectorLength].Value;
|
||||
colorBytes[VectorLength - 1] = aa == string.Empty
|
||||
? byte.MaxValue
|
||||
: byte.Parse(aa, NumberStyles.HexNumber, CultureInfo.InvariantCulture.NumberFormat);
|
||||
|
||||
return colorBytes.ToColor32();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses asynchronously a <see cref="Color32" /> from an HTML string (#RRGGBB or #RRGGBBAA).
|
||||
/// </summary>
|
||||
/// <param name="html">Source HTML string</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
|
||||
/// <returns>An awaitable <see cref="Task" /> that returns the parsed color</returns>
|
||||
/// <exception cref="FormatException">The string had an incorrect format</exception>
|
||||
public static Task<Color32?> ParseAsync(string html, CancellationToken ct = default)
|
||||
{
|
||||
return Task.Run(() => TryParse(html, out Color32 result) ? result : (Color32?)null, ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Types & Data
|
||||
|
||||
private const int VectorLength = 4;
|
||||
private const string StringFormatRGBA = "#{0:X2}{1:X2}{2:X2}{3:X2}";
|
||||
private const string StringFormatRGB = "#{0:X2}{1:X2}{2:X2}";
|
||||
private const string RegexPattern = "^#?([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})?$";
|
||||
|
||||
private static readonly Regex _regex = new Regex(RegexPattern);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f69c5ccb17b9474c88c59e005eb072a
|
||||
timeCreated: 1621500585
|
||||
@@ -0,0 +1,205 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="ColorExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UltimateXR.Extensions.System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Color" /> extensions.
|
||||
/// </summary>
|
||||
public static class ColorExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Transforms an array of floats to a <see cref="Color" /> component by component. If there are not enough values to
|
||||
/// read, the remaining values are set to 0.0 for RGB and 1.0 for A.
|
||||
/// </summary>
|
||||
/// <param name="data">Source data</param>
|
||||
/// <returns>Result color</returns>
|
||||
public static Color ToColor(this float[] data)
|
||||
{
|
||||
return data.Length switch
|
||||
{
|
||||
0 => default,
|
||||
1 => new Color(data[0], 0f, 0f, 1f),
|
||||
2 => new Color(data[0], data[1], 0f, 1f),
|
||||
3 => new Color(data[0], data[1], data[2], 1f),
|
||||
_ => new Color(data[0], data[1], data[2], data[3])
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps <see cref="Color" /> values component by component.
|
||||
/// </summary>
|
||||
/// <param name="self">Color whose components to clamp</param>
|
||||
/// <param name="min">Minimum RGB values</param>
|
||||
/// <param name="max">Maximum RGB values</param>
|
||||
/// <returns>Clamped color</returns>
|
||||
public static Color Clamp(this in Color self, in Color min, in Color max)
|
||||
{
|
||||
float[] result = new float[VectorLength];
|
||||
for (int i = 0; i < VectorLength; ++i)
|
||||
{
|
||||
result[i] = Mathf.Clamp(self[i], min[i], max[i]);
|
||||
}
|
||||
|
||||
return result.ToColor();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies two colors by multiplying each component.
|
||||
/// </summary>
|
||||
/// <param name="self">Operand A</param>
|
||||
/// <param name="other">Operand B</param>
|
||||
/// <returns>Result color</returns>
|
||||
public static Color Multiply(this in Color self, in Color other)
|
||||
{
|
||||
return new Color(self.r * other.r,
|
||||
self.g * other.g,
|
||||
self.b * other.b,
|
||||
self.a * other.a);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a color based on an already existing color and an alpha value.
|
||||
/// </summary>
|
||||
/// <param name="color">Color value</param>
|
||||
/// <param name="alpha">Alpha value</param>
|
||||
/// <returns>
|
||||
/// Result of combining the RGB of the <paramref name="color" /> value and alpha of <paramref name="alpha" />
|
||||
/// </returns>
|
||||
public static Color ColorAlpha(in Color color, float alpha)
|
||||
{
|
||||
return new Color(color.r, color.g, color.b, alpha);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a color based on an already existing color and an alpha value.
|
||||
/// </summary>
|
||||
/// <param name="self">Color value</param>
|
||||
/// <param name="alpha">Alpha value</param>
|
||||
/// <returns>
|
||||
/// Result of combining the RGBA of the color value and <paramref name="alpha" />
|
||||
/// </returns>
|
||||
public static Color WithAlpha(this in Color self, float alpha)
|
||||
{
|
||||
return ColorAlpha(self, alpha);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a color based on an already existing color and a brightness scale value.
|
||||
/// </summary>
|
||||
/// <param name="color">Color value</param>
|
||||
/// <param name="brightnessScale">The brightness scale factor</param>
|
||||
/// <returns>Color with adjusted brightness</returns>
|
||||
public static Color ScaleColorBrightness(this in Color color, float brightnessScale)
|
||||
{
|
||||
Color.RGBToHSV(color, out float h, out float s, out float v);
|
||||
v *= brightnessScale;
|
||||
return Color.HSVToRGB(h, s, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the color to a HTML color value (#RRGGBB or #RRGGBBAA).
|
||||
/// </summary>
|
||||
/// <param name="self">Color to convert</param>
|
||||
/// <returns>HTML color string</returns>
|
||||
public static string ToHtml(this in Color self)
|
||||
{
|
||||
return Mathf.Approximately(self.a, 1f)
|
||||
? string.Format(StringFormatRGB,
|
||||
(byte)Mathf.Round(Mathf.Clamp01(self.r) * byte.MaxValue),
|
||||
(byte)Mathf.Round(Mathf.Clamp01(self.g) * byte.MaxValue),
|
||||
(byte)Mathf.Round(Mathf.Clamp01(self.b) * byte.MaxValue))
|
||||
: string.Format(StringFormatRGBA,
|
||||
(byte)Mathf.Round(Mathf.Clamp01(self.r) * byte.MaxValue),
|
||||
(byte)Mathf.Round(Mathf.Clamp01(self.g) * byte.MaxValue),
|
||||
(byte)Mathf.Round(Mathf.Clamp01(self.b) * byte.MaxValue),
|
||||
(byte)Mathf.Round(Mathf.Clamp01(self.a) * byte.MaxValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a <see cref="Color" /> from an HTML string (#RRGGBB or #RRGGBBAA).
|
||||
/// </summary>
|
||||
/// <param name="html">Source HTML string</param>
|
||||
/// <param name="result">Parsed color or the default color value if there was an error</param>
|
||||
/// <returns>Whether the color was parsed successfully</returns>
|
||||
public static bool TryParse(string html, out Color result)
|
||||
{
|
||||
try
|
||||
{
|
||||
result = Parse(html);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a <see cref="Color" /> from an HTML string (#RRGGBB or #RRGGBBAA).
|
||||
/// </summary>
|
||||
/// <param name="html">Source HTML string</param>
|
||||
/// <returns>The parsed color</returns>
|
||||
/// <exception cref="FormatException">The string had an incorrect format</exception>
|
||||
public static Color Parse(string html)
|
||||
{
|
||||
html.ThrowIfNull(nameof(html));
|
||||
|
||||
Match match = _regex.Match(html);
|
||||
if (!match.Success)
|
||||
{
|
||||
throw new FormatException($"Input string [{html}] does not have the right format: #RRGGBB or #RRGGBBAA");
|
||||
}
|
||||
|
||||
float[] result = new float[VectorLength];
|
||||
for (int i = 0; i < VectorLength - 1; ++i)
|
||||
{
|
||||
string hex = match.Groups[i + 1].Value;
|
||||
result[i] = byte.Parse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture.NumberFormat) / (float)byte.MaxValue;
|
||||
}
|
||||
|
||||
string aa = match.Groups[VectorLength].Value;
|
||||
result[VectorLength - 1] = aa != string.Empty ? byte.Parse(aa, NumberStyles.HexNumber, CultureInfo.InvariantCulture.NumberFormat) / (float)byte.MaxValue : 1f;
|
||||
|
||||
return result.ToColor();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses asynchronously a <see cref="Color" /> from an HTML string (#RRGGBB or #RRGGBBAA).
|
||||
/// </summary>
|
||||
/// <param name="html">Source HTML string</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
|
||||
/// <returns>An awaitable <see cref="Task" /> that returns the parsed color</returns>
|
||||
/// <exception cref="FormatException">The string had an incorrect format</exception>
|
||||
public static Task<Color?> ParseAsync(string html, CancellationToken ct = default)
|
||||
{
|
||||
return Task.Run(() => TryParse(html, out Color result) ? result : (Color?)null, ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Types & Data
|
||||
|
||||
private const int VectorLength = 4;
|
||||
private const string StringFormatRGBA = "#{0:X2}{1:X2}{2:X2}{3:X2}";
|
||||
private const string StringFormatRGB = "#{0:X2}{1:X2}{2:X2}";
|
||||
private const string RegexPattern = "^#?([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})?$";
|
||||
|
||||
private static readonly Regex _regex = new Regex(RegexPattern);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 29cad001faae79346949b7a9251a414a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,112 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="ImageExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UltimateXR.Extensions.System;
|
||||
using UltimateXR.Extensions.System.IO;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Image" /> extensions.
|
||||
/// </summary>
|
||||
public static class ImageExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Loads a sprite asynchronously from a base64 encoded string and assigns it to the
|
||||
/// <see cref="Image.overrideSprite" /> property of an <see cref="Image" />.
|
||||
/// </summary>
|
||||
/// <param name="self">Target <see cref="Image" /></param>
|
||||
/// <param name="base64">Base64 encoded string. See <see cref="SpriteExt.ReadSpriteBase64Async" /></param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="base64" /> is null or empty</exception>
|
||||
/// <exception cref="OperationCanceledException">Task canceled using <paramref name="ct" /></exception>
|
||||
/// <exception cref="FormatException">
|
||||
/// The length of <paramref name="base64" />, ignoring white-space characters, is not
|
||||
/// zero or a multiple of 4
|
||||
/// </exception>
|
||||
public static async Task OverrideSpriteFromBase64Async(this Image self, string base64, CancellationToken ct = default)
|
||||
{
|
||||
self.ThrowIfNull(nameof(self));
|
||||
self.overrideSprite = await SpriteExt.ReadSpriteBase64Async(self, base64, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a sprite asynchronously from an URI and assigns it to the <see cref="Image.overrideSprite" /> property of an
|
||||
/// <see cref="Image" />.
|
||||
/// </summary>
|
||||
/// <param name="self">Target image</param>
|
||||
/// <param name="uri">File location. See <see cref="FileExt.Read" /></param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="uri" /> is null or empty</exception>
|
||||
/// <exception cref="OperationCanceledException">Task canceled using <paramref name="ct" /></exception>
|
||||
/// <exception cref="FileNotFoundException">The file specified in <paramref name="uri" /> was not found</exception>
|
||||
/// <exception cref="NotSupportedException"><paramref name="uri" /> is in an invalid format</exception>
|
||||
/// <exception cref="IOException">An I/O error occurred while opening the file</exception>
|
||||
/// <exception cref="InvalidOperationException">The stream is currently in use by a previous read operation</exception>
|
||||
public static async Task OverrideSpriteFromUriAsync(this Image self, string uri, CancellationToken ct = default)
|
||||
{
|
||||
self.ThrowIfNull(nameof(self));
|
||||
self.overrideSprite = await SpriteExt.ReadSpriteFileAsync(self, uri, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to load a sprite asynchronously from an URI and assign it to the <see cref="Image.overrideSprite" /> property
|
||||
/// of an <see cref="Image" />.
|
||||
/// </summary>
|
||||
/// <param name="self">Target image</param>
|
||||
/// <param name="uri">File location. See <see cref="FileExt.Read" /></param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
|
||||
/// <returns>
|
||||
/// Whether the sprite was correctly load and the <see cref="Image" /> had its <see cref="Image.overrideSprite" />
|
||||
/// assigned
|
||||
/// </returns>
|
||||
public static async Task<bool> TryOverrideSpriteFromUriAsync(this Image self, string uri, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await self.OverrideSpriteFromUriAsync(uri, ct);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to load a sprite asynchronously from a base64 encoded string and assign it to the
|
||||
/// <see cref="Image.overrideSprite" /> property of an <see cref="Image" />.
|
||||
/// </summary>
|
||||
/// <param name="self">Target image</param>
|
||||
/// <param name="base64">Base64 encoded string with the image file content</param>
|
||||
/// <param name="ct">Optional cancellation token, to cancel the operation</param>
|
||||
/// <returns>
|
||||
/// Whether the sprite was correctly load and the <see cref="Image" /> had its <see cref="Image.overrideSprite" />
|
||||
/// assigned
|
||||
/// </returns>
|
||||
public static async Task<bool> TryOverrideSpriteFromBase64Async(this Image self, string base64, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
await self.OverrideSpriteFromBase64Async(base64, ct);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a054e74bfe056a14bbbad631f4310243
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,155 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="LODGroupExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="LODGroup" /> extensions.
|
||||
/// Most functionality has been copied from:
|
||||
/// https://github.com/JulienHeijmans/EditorScripts/blob/master/Scripts/Utility/Editor/LODExtendedUtility.cs, which
|
||||
/// in turn copied functionality from:
|
||||
/// https://github.com/Unity-Technologies/AutoLOD/blob/master/Scripts/Extensions/LODGroupExtensions.cs
|
||||
/// </summary>
|
||||
public static class LODGroupExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Gets the LOD level index that should be enabled from a specific view.
|
||||
/// </summary>
|
||||
/// <param name="lodGroup">Component to check</param>
|
||||
/// <param name="camera">Camera to use as point of view</param>
|
||||
/// <returns>LOD level index that should be enabled</returns>
|
||||
public static int GetVisibleLevel(this LODGroup lodGroup, Camera camera)
|
||||
{
|
||||
if (camera == null)
|
||||
{
|
||||
return lodGroup.lodCount - 1;
|
||||
}
|
||||
|
||||
var lods = lodGroup.GetLODs();
|
||||
var relativeHeight = GetRelativeHeight(lodGroup, camera);
|
||||
|
||||
int lodIndex = lodGroup.lodCount - 1;
|
||||
|
||||
for (int i = 0; i < lods.Length; i++)
|
||||
{
|
||||
var lod = lods[i];
|
||||
|
||||
if (relativeHeight >= lod.screenRelativeTransitionHeight)
|
||||
{
|
||||
lodIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return lodIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually enables all renderers belonging to a LOD level.
|
||||
/// </summary>
|
||||
/// <param name="lodGroup">Component to process</param>
|
||||
/// <param name="level">Level whose renderers to enable</param>
|
||||
public static void EnableLevelRenderers(this LODGroup lodGroup, int level)
|
||||
{
|
||||
var lods = lodGroup.GetLODs();
|
||||
|
||||
for (int i = 0; i < lods.Length; i++)
|
||||
{
|
||||
foreach (Renderer renderer in lods[i].renderers)
|
||||
{
|
||||
if (renderer != null)
|
||||
{
|
||||
renderer.enabled = i == level;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually enables the renderers from all LOD levels.
|
||||
/// </summary>
|
||||
/// <param name="lodGroup">Component to process</param>
|
||||
public static void EnableAllLevelRenderers(this LODGroup lodGroup)
|
||||
{
|
||||
var lods = lodGroup.GetLODs();
|
||||
|
||||
for (int i = 0; i < lods.Length; i++)
|
||||
{
|
||||
foreach (Renderer renderer in lods[i].renderers)
|
||||
{
|
||||
if (renderer != null)
|
||||
{
|
||||
renderer.enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
|
||||
/// <summary>
|
||||
/// Computes the relative height in the camera view.
|
||||
/// </summary>
|
||||
/// <param name="lodGroup">Component to check</param>
|
||||
/// <param name="camera">Camera to use as point of view</param>
|
||||
/// <returns>Relative height</returns>
|
||||
private static float GetRelativeHeight(LODGroup lodGroup, Camera camera)
|
||||
{
|
||||
var distance = (lodGroup.transform.TransformPoint(lodGroup.localReferencePoint) - camera.transform.position).magnitude;
|
||||
return DistanceToRelativeHeight(camera, distance / QualitySettings.lodBias, GetWorldSpaceSize(lodGroup));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the relative height in the camera view.
|
||||
/// </summary>
|
||||
/// <param name="camera">Camera to use as point of view</param>
|
||||
/// <param name="distance">Distance to the camera</param>
|
||||
/// <param name="size">Largest axis in world-space</param>
|
||||
/// <returns>Relative height</returns>
|
||||
private static float DistanceToRelativeHeight(Camera camera, float distance, float size)
|
||||
{
|
||||
if (camera.orthographic)
|
||||
{
|
||||
return size * 0.5F / camera.orthographicSize;
|
||||
}
|
||||
|
||||
var halfAngle = Mathf.Tan(Mathf.Deg2Rad * camera.fieldOfView * 0.5F);
|
||||
var relativeHeight = size * 0.5F / (distance * halfAngle);
|
||||
return relativeHeight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the largest axis of the <see cref="LODGroup" /> in world-space.
|
||||
/// </summary>
|
||||
/// <param name="lodGroup">Component to process</param>
|
||||
/// <returns>World space size</returns>
|
||||
private static float GetWorldSpaceSize(LODGroup lodGroup)
|
||||
{
|
||||
return GetWorldSpaceScale(lodGroup.transform) * lodGroup.size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the largest scale axis.
|
||||
/// </summary>
|
||||
/// <param name="transform">Transform to get the largest scale of</param>
|
||||
/// <returns>Largest scale axis</returns>
|
||||
private static float GetWorldSpaceScale(Transform transform)
|
||||
{
|
||||
var scale = transform.lossyScale;
|
||||
float largestAxis = Mathf.Abs(scale.x);
|
||||
largestAxis = Mathf.Max(largestAxis, Mathf.Abs(scale.y));
|
||||
largestAxis = Mathf.Max(largestAxis, Mathf.Abs(scale.z));
|
||||
return largestAxis;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5258a0f4700615643a99e29dd5f01350
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,30 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="MeshExt.ExtractSubMeshOperation.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
namespace UltimateXR.Extensions.Unity.Render
|
||||
{
|
||||
public static partial class MeshExt
|
||||
{
|
||||
#region Public Types & Data
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates possible mesh extraction algorithms.
|
||||
/// </summary>
|
||||
public enum ExtractSubMeshOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new mesh copying all the mesh that is influenced by the bone or any of its children.
|
||||
/// </summary>
|
||||
BoneAndChildren,
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new mesh copying all the mesh that is not influenced by the reference bone or any of its children.
|
||||
/// </summary>
|
||||
NotFromBoneOrChildren
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a64f8ffb3bd4e51b0f99a55e6a90f80
|
||||
timeCreated: 1667922130
|
||||
@@ -0,0 +1,399 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="MeshExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UltimateXR.Animation.Splines;
|
||||
using UltimateXR.Core;
|
||||
using UltimateXR.Extensions.System.Collections;
|
||||
using UltimateXR.Extensions.Unity.Math;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Mesh" /> extensions.
|
||||
/// </summary>
|
||||
public static partial class MeshExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Creates a quad mesh <see cref="Mesh" />.
|
||||
/// </summary>
|
||||
/// <param name="size">Quad size (total width and height)</param>
|
||||
/// <returns>Quad <see cref="Mesh" /></returns>
|
||||
public static Mesh CreateQuad(float size)
|
||||
{
|
||||
Mesh quadMesh = new Mesh();
|
||||
float halfSize = size * 0.5f;
|
||||
|
||||
quadMesh.vertices = new[] { new Vector3(halfSize, halfSize, 0.0f), new Vector3(halfSize, -halfSize, 0.0f), new Vector3(-halfSize, -halfSize, 0.0f), new Vector3(-halfSize, halfSize, 0.0f) };
|
||||
quadMesh.uv = new[] { new Vector2(1.0f, 1.0f), new Vector2(1.0f, 0.0f), new Vector2(0.0f, 0.0f), new Vector2(0.0f, 1.0f) };
|
||||
quadMesh.triangles = new[] { 0, 2, 1, 0, 3, 2 };
|
||||
|
||||
return quadMesh;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Mesh" /> tessellating a <see cref="UxrSpline" />
|
||||
/// </summary>
|
||||
/// <param name="spline">Spline to evaluate</param>
|
||||
/// <param name="subdivisions">Number of subdivisions along the spline axis</param>
|
||||
/// <param name="sides">Number of subdivisions in the section</param>
|
||||
/// <param name="radius">Section radius</param>
|
||||
/// <returns><see cref="Mesh" /> with the tessellated <see cref="UxrSpline" /></returns>
|
||||
public static Mesh CreateSpline(UxrSpline spline, int subdivisions, int sides, float radius)
|
||||
{
|
||||
subdivisions = Mathf.Max(subdivisions, 2);
|
||||
sides = Mathf.Max(sides, 3);
|
||||
|
||||
Mesh splineMesh = new Mesh();
|
||||
|
||||
// Create mesh
|
||||
|
||||
Vector3[] vertices = new Vector3[subdivisions * (sides + 1)];
|
||||
Vector3[] normals = new Vector3[vertices.Length];
|
||||
Vector2[] mapping = new Vector2[vertices.Length];
|
||||
int[] indices = new int[(subdivisions - 1) * sides * 2 * 3];
|
||||
|
||||
for (int sub = 0; sub < subdivisions; ++sub)
|
||||
{
|
||||
float arcLength = sub / (subdivisions - 1.0f) * spline.ArcLength;
|
||||
float normalizedArcLength = arcLength / spline.ArcLength;
|
||||
|
||||
spline.EvaluateUsingArcLength(arcLength, out Vector3 splinePosition, out Vector3 splineDirection);
|
||||
|
||||
Vector3 perpendicular = splineDirection.GetPerpendicularVector().normalized * radius;
|
||||
Vector3 vertexStart = splinePosition + perpendicular;
|
||||
|
||||
for (int side = 0; side < sides + 1; ++side)
|
||||
{
|
||||
int vertexIndex = sub * (sides + 1) + side;
|
||||
int faceBase = sub * sides * 2 * 3 + side * 2 * 3;
|
||||
|
||||
float rotation = side / (float)sides;
|
||||
float degrees = 360.0f * rotation;
|
||||
|
||||
vertices[vertexIndex] = vertexStart.GetRotationAround(splinePosition, splineDirection, degrees);
|
||||
mapping[vertexIndex] = new Vector2(rotation, normalizedArcLength);
|
||||
normals[vertexIndex] = (vertices[vertexIndex] - splinePosition).normalized;
|
||||
|
||||
if (side < sides && sub < subdivisions - 1)
|
||||
{
|
||||
indices[faceBase + 0] = vertexIndex;
|
||||
indices[faceBase + 1] = vertexIndex + 1;
|
||||
indices[faceBase + 2] = vertexIndex + sides + 1;
|
||||
indices[faceBase + 3] = vertexIndex + 1;
|
||||
indices[faceBase + 4] = vertexIndex + sides + 2;
|
||||
indices[faceBase + 5] = vertexIndex + sides + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
splineMesh.vertices = vertices;
|
||||
splineMesh.uv = mapping;
|
||||
splineMesh.normals = normals;
|
||||
splineMesh.triangles = indices;
|
||||
|
||||
return splineMesh;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new mesh from a skinned mesh renderer based on a reference bone and an extract operation.
|
||||
/// </summary>
|
||||
/// <param name="skin">Skin to process</param>
|
||||
/// <param name="bone">Reference bone</param>
|
||||
/// <param name="extractOperation">Which part of the skinned mesh to extract</param>
|
||||
/// <param name="weightThreshold">Bone weight threshold above which the vertices will be extracted</param>
|
||||
/// <returns>New mesh</returns>
|
||||
public static Mesh ExtractSubMesh(SkinnedMeshRenderer skin, Transform bone, ExtractSubMeshOperation extractOperation, float weightThreshold = UxrConstants.Geometry.SignificantBoneWeight)
|
||||
{
|
||||
Mesh newMesh = new Mesh();
|
||||
|
||||
// Create dictionary to check which bones belong to the hierarchy
|
||||
|
||||
Dictionary<int, bool> areHierarchyBones = new Dictionary<int, bool>();
|
||||
|
||||
Vector3[] vertices = skin.sharedMesh.vertices;
|
||||
Vector3[] normals = skin.sharedMesh.normals;
|
||||
Vector2[] uv = skin.sharedMesh.uv;
|
||||
BoneWeight[] boneWeights = skin.sharedMesh.boneWeights;
|
||||
Transform[] bones = skin.bones;
|
||||
|
||||
for (int i = 0; i < bones.Length; ++i)
|
||||
{
|
||||
areHierarchyBones.Add(i, bones[i].HasParent(bone));
|
||||
}
|
||||
|
||||
// Create filtered mesh
|
||||
|
||||
List<List<int>> newTriangles = new List<List<int>>();
|
||||
Dictionary<int, int> old2New = new Dictionary<int, int>();
|
||||
List<Vector3> newVertices = new List<Vector3>();
|
||||
List<Vector3> newNormals = new List<Vector3>();
|
||||
List<Vector2> newUV = new List<Vector2>();
|
||||
List<BoneWeight> newBoneWeights = new List<BoneWeight>();
|
||||
|
||||
bool VertexMeetsRequirement(bool isFromHierarchy)
|
||||
{
|
||||
switch (extractOperation)
|
||||
{
|
||||
case ExtractSubMeshOperation.BoneAndChildren: return isFromHierarchy;
|
||||
case ExtractSubMeshOperation.NotFromBoneOrChildren: return !isFromHierarchy;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int submesh = 0; submesh < skin.sharedMesh.subMeshCount; ++submesh)
|
||||
{
|
||||
int[] submeshIndices = skin.sharedMesh.GetTriangles(submesh);
|
||||
List<int> newSubmeshIndices = new List<int>();
|
||||
|
||||
for (int t = 0; t < submeshIndices.Length / 3; t++)
|
||||
{
|
||||
float totalWeight = 0.0f;
|
||||
|
||||
for (int v = 0; v < 3; v++)
|
||||
{
|
||||
BoneWeight boneWeight = boneWeights[submeshIndices[t * 3 + v]];
|
||||
|
||||
if (areHierarchyBones.TryGetValue(boneWeight.boneIndex0, out bool isFromHierarchy) && VertexMeetsRequirement(isFromHierarchy))
|
||||
{
|
||||
totalWeight += boneWeight.weight0;
|
||||
}
|
||||
|
||||
if (areHierarchyBones.TryGetValue(boneWeight.boneIndex1, out isFromHierarchy) && VertexMeetsRequirement(isFromHierarchy))
|
||||
{
|
||||
totalWeight += boneWeight.weight1;
|
||||
}
|
||||
|
||||
if (areHierarchyBones.TryGetValue(boneWeight.boneIndex2, out isFromHierarchy) && VertexMeetsRequirement(isFromHierarchy))
|
||||
{
|
||||
totalWeight += boneWeight.weight2;
|
||||
}
|
||||
|
||||
if (areHierarchyBones.TryGetValue(boneWeight.boneIndex3, out isFromHierarchy) && VertexMeetsRequirement(isFromHierarchy))
|
||||
{
|
||||
totalWeight += boneWeight.weight3;
|
||||
}
|
||||
}
|
||||
|
||||
if (totalWeight > weightThreshold)
|
||||
{
|
||||
for (int v = 0; v < 3; v++)
|
||||
{
|
||||
int oldIndex = submeshIndices[t * 3 + v];
|
||||
|
||||
if (!old2New.ContainsKey(oldIndex))
|
||||
{
|
||||
old2New.Add(oldIndex, old2New.Count);
|
||||
|
||||
newVertices.Add(vertices[oldIndex]);
|
||||
newNormals.Add(normals[oldIndex]);
|
||||
newUV.Add(uv[oldIndex]);
|
||||
newBoneWeights.Add(boneWeights[oldIndex]);
|
||||
}
|
||||
|
||||
newSubmeshIndices.Add(old2New[oldIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newTriangles.Add(newSubmeshIndices);
|
||||
}
|
||||
|
||||
// Create new mesh
|
||||
|
||||
newMesh.vertices = newVertices.ToArray();
|
||||
newMesh.normals = newNormals.ToArray();
|
||||
newMesh.uv = newUV.ToArray();
|
||||
newMesh.boneWeights = newBoneWeights.ToArray();
|
||||
|
||||
// Create and assign new triangle list
|
||||
|
||||
newMesh.subMeshCount = newTriangles.Count;
|
||||
|
||||
for (int submesh = 0; submesh < newTriangles.Count; ++submesh)
|
||||
{
|
||||
newMesh.SetTriangles(newTriangles[submesh].ToArray(), submesh);
|
||||
}
|
||||
|
||||
return newMesh;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the number of vertices that a bone influences in a skinned mesh.
|
||||
/// </summary>
|
||||
/// <param name="skin">Skinned mesh</param>
|
||||
/// <param name="bone">Bone to check</param>
|
||||
/// <param name="weightThreshold">Weight above which will be considered significant influence</param>
|
||||
/// <returns>
|
||||
/// Number of vertices influenced by <paramref name="bone" /> with a weight above
|
||||
/// <paramref name="weightThreshold" />.
|
||||
/// </returns>
|
||||
public static int GetBoneInfluenceVertexCount(SkinnedMeshRenderer skin, Transform bone, float weightThreshold = UxrConstants.Geometry.SignificantBoneWeight)
|
||||
{
|
||||
Transform[] skinBones = skin.bones;
|
||||
int boneIndex = skinBones.IndexOf(bone);
|
||||
|
||||
if (boneIndex == -1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
BoneWeight[] boneWeights = skin.sharedMesh.boneWeights;
|
||||
|
||||
return boneWeights.Count(w => HasBoneInfluence(w, boneIndex, weightThreshold));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the number of vertices that a bone influences in a skinned mesh.
|
||||
/// </summary>
|
||||
/// <param name="skin">Skinned mesh</param>
|
||||
/// <param name="bone">Bone to check</param>
|
||||
/// <param name="weightThreshold">Weight above which to consider significant influence</param>
|
||||
/// <returns>
|
||||
/// Number of vertices influenced by <paramref name="bone" /> with a weight above
|
||||
/// <paramref name="weightThreshold" />
|
||||
/// </returns>
|
||||
public static bool HasBoneInfluence(SkinnedMeshRenderer skin, Transform bone, float weightThreshold = UxrConstants.Geometry.SignificantBoneWeight)
|
||||
{
|
||||
Transform[] skinBones = skin.bones;
|
||||
int boneIndex = skinBones.IndexOf(bone);
|
||||
|
||||
if (boneIndex == -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
BoneWeight[] boneWeights = skin.sharedMesh.boneWeights;
|
||||
|
||||
return boneWeights.Any(w => HasBoneInfluence(w, boneIndex, weightThreshold));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a given bone index has influence on a skinned mesh vertex.
|
||||
/// </summary>
|
||||
/// <param name="boneWeight">Vertex's bone weight information</param>
|
||||
/// <param name="boneIndex">Bone index</param>
|
||||
/// <param name="weightThreshold">Weight above which will be considered significant influence</param>
|
||||
/// <returns>Whether the bone influences the vertex in a significant amount</returns>
|
||||
public static bool HasBoneInfluence(in BoneWeight boneWeight, int boneIndex, float weightThreshold = UxrConstants.Geometry.SignificantBoneWeight)
|
||||
{
|
||||
if (boneWeight.boneIndex0 == boneIndex && boneWeight.weight0 > weightThreshold)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (boneWeight.boneIndex1 == boneIndex && boneWeight.weight1 > weightThreshold)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (boneWeight.boneIndex2 == boneIndex && boneWeight.weight2 > weightThreshold)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (boneWeight.boneIndex3 == boneIndex && boneWeight.weight3 > weightThreshold)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the bounding box that contains all the vertices that a bone has influence on in a skinned mesh. The
|
||||
/// bounding box is computed in local bone space.
|
||||
/// </summary>
|
||||
/// <param name="skin">Skinned mesh</param>
|
||||
/// <param name="bone">Bone to check</param>
|
||||
/// <param name="weightThreshold">Weight above which to consider significant influence</param>
|
||||
/// <returns>
|
||||
/// Bounding box in local <paramref name="bone" /> coordinates.
|
||||
/// </returns>
|
||||
public static Bounds GetBoneInfluenceBounds(SkinnedMeshRenderer skin, Transform bone, float weightThreshold = UxrConstants.Geometry.SignificantBoneWeight)
|
||||
{
|
||||
Transform[] skinBones = skin.bones;
|
||||
int boneIndex = skinBones.IndexOf(bone);
|
||||
|
||||
if (boneIndex == -1)
|
||||
{
|
||||
return new Bounds();
|
||||
}
|
||||
|
||||
Vector3[] vertices = skin.sharedMesh.vertices;
|
||||
BoneWeight[] boneWeights = skin.sharedMesh.boneWeights;
|
||||
Transform[] bones = skin.bones;
|
||||
Matrix4x4[] boneBindPoses = skin.sharedMesh.bindposes;
|
||||
Vector3 min = Vector3.zero;
|
||||
Vector3 max = Vector3.zero;
|
||||
bool initialized = false;
|
||||
|
||||
for (int i = 0; i < boneWeights.Length; ++i)
|
||||
{
|
||||
if (HasBoneInfluence(boneWeights[i], boneIndex, weightThreshold))
|
||||
{
|
||||
Vector3 localVertex = bones[boneIndex].InverseTransformPoint(GetSkinnedWorldVertex(skin, boneWeights[i], vertices[i], bones, boneBindPoses));
|
||||
|
||||
if (!initialized)
|
||||
{
|
||||
initialized = true;
|
||||
min = localVertex;
|
||||
max = localVertex;
|
||||
}
|
||||
else
|
||||
{
|
||||
min = Vector3Ext.Min(localVertex, min);
|
||||
max = Vector3Ext.Max(localVertex, max);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Bounds((min + max) * 0.5f, max - min);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a skinned vertex in world coordinates.
|
||||
/// </summary>
|
||||
/// <param name="skin">Skin</param>
|
||||
/// <param name="boneWeight">Vertex bone weights info</param>
|
||||
/// <param name="vertex">Vertex in local skin coordinates when the skin is in the bind pose</param>
|
||||
/// <param name="bones">Bone list</param>
|
||||
/// <param name="boneBindPoses">Bone bind poses</param>
|
||||
/// <returns>Vertex in world coordinates</returns>
|
||||
public static Vector3 GetSkinnedWorldVertex(SkinnedMeshRenderer skin, BoneWeight boneWeight, Vector3 vertex, Transform[] bones, Matrix4x4[] boneBindPoses)
|
||||
{
|
||||
Vector3 result = Vector3.zero;
|
||||
|
||||
if (boneWeight.weight0 > UxrConstants.Geometry.SmallestBoneWeight)
|
||||
{
|
||||
result += bones[boneWeight.boneIndex0].localToWorldMatrix.MultiplyPoint(boneBindPoses[boneWeight.boneIndex0].MultiplyPoint(vertex)) * boneWeight.weight0;
|
||||
}
|
||||
|
||||
if (boneWeight.weight1 > UxrConstants.Geometry.SmallestBoneWeight)
|
||||
{
|
||||
result += bones[boneWeight.boneIndex1].localToWorldMatrix.MultiplyPoint(boneBindPoses[boneWeight.boneIndex1].MultiplyPoint(vertex)) * boneWeight.weight1;
|
||||
}
|
||||
|
||||
if (boneWeight.weight2 > UxrConstants.Geometry.SmallestBoneWeight)
|
||||
{
|
||||
result += bones[boneWeight.boneIndex2].localToWorldMatrix.MultiplyPoint(boneBindPoses[boneWeight.boneIndex2].MultiplyPoint(vertex)) * boneWeight.weight2;
|
||||
}
|
||||
|
||||
if (boneWeight.weight3 > UxrConstants.Geometry.SmallestBoneWeight)
|
||||
{
|
||||
result += bones[boneWeight.boneIndex3].localToWorldMatrix.MultiplyPoint(boneBindPoses[boneWeight.boneIndex3].MultiplyPoint(vertex)) * boneWeight.weight3;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e562213b88d80624a867d53e83926632
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,50 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="RendererExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using System.Collections.Generic;
|
||||
using UltimateXR.Extensions.System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Renderer" /> extensions.
|
||||
/// </summary>
|
||||
public static class RendererExt
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the <see cref="Bounds" /> encapsulating a set of renderers.
|
||||
/// </summary>
|
||||
/// <param name="renderers">Renderers to compute the bounds for</param>
|
||||
/// <returns><see cref="Bounds" /> encapsulating all renderers</returns>
|
||||
public static Bounds CalculateBounds(this IEnumerable<Renderer> renderers)
|
||||
{
|
||||
renderers.ThrowIfNull(nameof(renderers));
|
||||
|
||||
Bounds bounds = default;
|
||||
bool isFirst = true;
|
||||
|
||||
foreach (Renderer r in renderers)
|
||||
{
|
||||
Bounds b = r.bounds;
|
||||
if (isFirst)
|
||||
{
|
||||
bounds = r.bounds;
|
||||
isFirst = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
bounds.Encapsulate(b);
|
||||
}
|
||||
}
|
||||
|
||||
return bounds;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5aa895f60ddf8374f801421c8b5b8c45
|
||||
timeCreated: 1604664051
|
||||
@@ -0,0 +1,26 @@
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// <copyright file="ShaderExt.cs" company="VRMADA">
|
||||
// Copyright (c) VRMADA, All rights reserved.
|
||||
// </copyright>
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
using UnityEngine;
|
||||
|
||||
namespace UltimateXR.Extensions.Unity.Render
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Shader" /> extensions.
|
||||
/// </summary>
|
||||
public static class ShaderExt
|
||||
{
|
||||
#region Public Types & Data
|
||||
|
||||
public const string ShaderBase = "UltimateXR/";
|
||||
|
||||
public static Shader UnlitAdditiveColor => Shader.Find($"{ShaderBase}Basic Unlit/Unlit Additive Color");
|
||||
public static Shader UnlitTransparentColor => Shader.Find($"{ShaderBase}Basic Unlit/Unlit Transparent Color");
|
||||
public static Shader UnlitTransparentColorNoDepthTest => Shader.Find($"{ShaderBase}Basic Unlit/Unlit Transparent Color (No Depth Test)");
|
||||
public static Shader UnlitOverlayFade => Shader.Find($"{ShaderBase}Basic Unlit/Overlay Fade");
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 109b8a7bffaf6ca4195de7a878d7ecef
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user