From b3c6b59721a4983d5f267fbafb54ed15b45be012 Mon Sep 17 00:00:00 2001 From: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com> Date: Wed, 14 Jun 2023 17:08:51 +0100 Subject: [PATCH] Cancellation fixes and editor progress --- .../{Editor.asmdef => EditorTests.asmdef} | 4 +- ...or.asmdef.meta => EditorTests.asmdef.meta} | 0 Assets/Tests/Editor/PerformanceTest.cs | 96 -------- Assets/Tests/PlayMode.meta | 8 + Assets/Tests/PlayMode/ComponentTest.cs | 18 ++ Assets/Tests/PlayMode/ComponentTest.cs.meta | 3 + Assets/Tests/PlayMode/ConvertToNativeTests.cs | 55 +++++ .../ConvertToNativeTests.cs.meta} | 2 +- Assets/Tests/PlayMode/PlayModeTests.asmdef | 25 ++ .../Tests/PlayMode/PlayModeTests.asmdef.meta | 7 + Assets/Tests/PlayMode/SpeckleReceiverTests.cs | 96 ++++++++ .../PlayMode/SpeckleReceiverTests.cs.meta | 11 + .../Components/SpeckleReceiverEditor.cs | 213 +++++++++++------- .../Editor/Components/SpeckleSendEditor.cs | 8 +- .../Components/RecursiveConverter.ToNative.cs | 8 +- .../Runtime/Components/SpeckleReceiver.cs | 54 +++-- .../Runtime/Utils/Utils.cs | 30 ++- .../Wrappers/Selection/OptionSelection.cs | 6 +- 18 files changed, 436 insertions(+), 208 deletions(-) rename Assets/Tests/Editor/{Editor.asmdef => EditorTests.asmdef} (86%) rename Assets/Tests/Editor/{Editor.asmdef.meta => EditorTests.asmdef.meta} (100%) delete mode 100644 Assets/Tests/Editor/PerformanceTest.cs create mode 100644 Assets/Tests/PlayMode.meta create mode 100644 Assets/Tests/PlayMode/ComponentTest.cs create mode 100644 Assets/Tests/PlayMode/ComponentTest.cs.meta create mode 100644 Assets/Tests/PlayMode/ConvertToNativeTests.cs rename Assets/Tests/{Editor/PerformanceTest.cs.meta => PlayMode/ConvertToNativeTests.cs.meta} (83%) create mode 100644 Assets/Tests/PlayMode/PlayModeTests.asmdef create mode 100644 Assets/Tests/PlayMode/PlayModeTests.asmdef.meta create mode 100644 Assets/Tests/PlayMode/SpeckleReceiverTests.cs create mode 100644 Assets/Tests/PlayMode/SpeckleReceiverTests.cs.meta diff --git a/Assets/Tests/Editor/Editor.asmdef b/Assets/Tests/Editor/EditorTests.asmdef similarity index 86% rename from Assets/Tests/Editor/Editor.asmdef rename to Assets/Tests/Editor/EditorTests.asmdef index 6e33215..01770e4 100644 --- a/Assets/Tests/Editor/Editor.asmdef +++ b/Assets/Tests/Editor/EditorTests.asmdef @@ -1,6 +1,6 @@ { - "name": "Editor", - "rootNamespace": "", + "name": "EditorTests", + "rootNamespace": "Speckle.ConnectorUnity.Tests", "references": [ "UnityEngine.TestRunner", "UnityEditor.TestRunner", diff --git a/Assets/Tests/Editor/Editor.asmdef.meta b/Assets/Tests/Editor/EditorTests.asmdef.meta similarity index 100% rename from Assets/Tests/Editor/Editor.asmdef.meta rename to Assets/Tests/Editor/EditorTests.asmdef.meta diff --git a/Assets/Tests/Editor/PerformanceTest.cs b/Assets/Tests/Editor/PerformanceTest.cs deleted file mode 100644 index cc6a0a9..0000000 --- a/Assets/Tests/Editor/PerformanceTest.cs +++ /dev/null @@ -1,96 +0,0 @@ - -using System; -using System.Collections; -using System.Diagnostics; -using System.Threading.Tasks; -using NUnit.Framework; -using Objects.Utils; -using Speckle.Core.Api; -using Speckle.Core.Models; -using Speckle.Core.Models.Extensions; -using UnityEngine; -using UnityEngine.TestTools; - - -public class PerformanceTest -{ - private static readonly string[] dataSource = new[] - { - "https://latest.speckle.dev/streams/24c3741255/commits/0925840e09" - }; - - - //This method is much faster - [Test, TestCaseSource(nameof(dataSource))] - public void Receive_GetAwaiterResult(string stream) - { - var stopwatch = Stopwatch.StartNew(); - - Helpers.Receive(stream).GetAwaiter().GetResult(); - - stopwatch.Stop(); - Console.WriteLine(stopwatch.ElapsedMilliseconds); - Assert.That(stopwatch.ElapsedMilliseconds, Is.Zero); - } - - - //This method takes around 46 seconds to complete - [Test, TestCaseSource(nameof(dataSource))] - public void Receive_TaskRunAsync(string stream) - { - var stopwatch = Stopwatch.StartNew(); - - Task.Run(async () => - { - await Helpers.Receive(stream); - }).GetAwaiter().GetResult(); - - stopwatch.Stop(); - Console.WriteLine(stopwatch.ElapsedMilliseconds); - Assert.That(stopwatch.ElapsedMilliseconds, Is.Zero); - } - - // [UnityTest, TestCaseSource(nameof(dataSource))] - // public IEnumerable Receive_Coroutine(string stream) - // { - // var stopwatch = Stopwatch.StartNew(); - // - // Task t = Helpers.Receive(stream); - // t.Start(); - // - // yield return new WaitUntil(() => !t.IsCompleted || stopwatch.ElapsedMilliseconds >= 100000); - // - // stopwatch.Stop(); - // Console.WriteLine(stopwatch.ElapsedMilliseconds); - // Assert.That(stopwatch.ElapsedMilliseconds, Is.Zero); - // Assert.True(t.IsCompletedSuccessfully); - // } - - - - - //This method takes around 46 seconds to complete - [Test] - public void TestTriangulate() - { - - - Base b = Task.Run(async () => - { - return await Helpers.Receive("https://speckle.xyz/streams/4a8fd0c6b6/commits/067bf723b1"); - }).GetAwaiter().GetResult(); - - - foreach (Base child in b.Traverse(b => b is Objects.Geometry.Mesh)) - { - if(child is not Objects.Geometry.Mesh m) continue; - - var stopwatch = Stopwatch.StartNew(); - - m.TriangulateMesh(); - - Console.WriteLine($"took {stopwatch.ElapsedMilliseconds:ms} to triangulate {child.id}"); - } - } - -} diff --git a/Assets/Tests/PlayMode.meta b/Assets/Tests/PlayMode.meta new file mode 100644 index 0000000..f872051 --- /dev/null +++ b/Assets/Tests/PlayMode.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5a4f4baa829261d438b740c7d3028756 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/PlayMode/ComponentTest.cs b/Assets/Tests/PlayMode/ComponentTest.cs new file mode 100644 index 0000000..978f21c --- /dev/null +++ b/Assets/Tests/PlayMode/ComponentTest.cs @@ -0,0 +1,18 @@ +using NUnit.Framework; +using UnityEngine; + +namespace Speckle.ConnectorUnity.Tests +{ + public abstract class ComponentTest where T : Component + { + protected T sut; + + [SetUp] + public void Setup() + { + GameObject go = new(); + sut = go.AddComponent(); + Assert.That(sut, Is.Not.Null); + } + } +} diff --git a/Assets/Tests/PlayMode/ComponentTest.cs.meta b/Assets/Tests/PlayMode/ComponentTest.cs.meta new file mode 100644 index 0000000..70603d7 --- /dev/null +++ b/Assets/Tests/PlayMode/ComponentTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4e2fe277dd9c47ad998138dcdbb024ae +timeCreated: 1686757093 \ No newline at end of file diff --git a/Assets/Tests/PlayMode/ConvertToNativeTests.cs b/Assets/Tests/PlayMode/ConvertToNativeTests.cs new file mode 100644 index 0000000..3963b13 --- /dev/null +++ b/Assets/Tests/PlayMode/ConvertToNativeTests.cs @@ -0,0 +1,55 @@ + +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; +using NUnit.Framework; +using Speckle.ConnectorUnity.Components; +using Speckle.Core.Api; +using Speckle.Core.Models; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Speckle.ConnectorUnity.Tests +{ + [TestFixture, TestOf(typeof(RecursiveConverter))] + public class ConvertToNativeTests : ComponentTest + { + private static IEnumerable TestCases() + { + yield return @"https://latest.speckle.dev/streams/c1faab5c62/commits/704984e22d"; + } + + private static Base Receive(string stream) + { + return Task.Run(async () => await Helpers.Receive(stream)).Result; + } + + [Test, TestCaseSource(nameof(TestCases))] + public void ToNative_Sync_Passes(string stream) + { + Base testCase = Receive(stream); + var resuts = sut.RecursivelyConvertToNative_Sync(testCase, null); + Assert.That(resuts, Has.Count.GreaterThan(0)); + Assert.That(resuts, Has.Some.Matches(x => x.WasSuccessful()))); + AssertChildren(); + } + + [UnityTest, TestCaseSource(nameof(TestCases))] + public IEnumerator ToNative_Coroutine_Passes(string stream) + { + Base testCase = Receive(stream); + GameObject parent = new("parent"); + + yield return sut.RecursivelyConvertToNative_Coroutine(testCase, parent); + AssertChildren(parent); + } + + private void AssertChildren(IEnumerable children) + { + foreach (var res in children) + { + res + } + } + } +} diff --git a/Assets/Tests/Editor/PerformanceTest.cs.meta b/Assets/Tests/PlayMode/ConvertToNativeTests.cs.meta similarity index 83% rename from Assets/Tests/Editor/PerformanceTest.cs.meta rename to Assets/Tests/PlayMode/ConvertToNativeTests.cs.meta index 0e99b1e..897febd 100644 --- a/Assets/Tests/Editor/PerformanceTest.cs.meta +++ b/Assets/Tests/PlayMode/ConvertToNativeTests.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 2dd598fed5008c44a815ba09e81a2d19 +guid: 3d9b0fc7baaf51a4a8e2bcefad8bd7b3 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Tests/PlayMode/PlayModeTests.asmdef b/Assets/Tests/PlayMode/PlayModeTests.asmdef new file mode 100644 index 0000000..884ba77 --- /dev/null +++ b/Assets/Tests/PlayMode/PlayModeTests.asmdef @@ -0,0 +1,25 @@ +{ + "name": "PlayModeTests", + "rootNamespace": "Speckle.ConnectorUnity.Tests", + "references": [ + "UnityEngine.TestRunner", + "UnityEditor.TestRunner", + "Speckle.ConnectorUnity.Components", + "Utils" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll", + "SpeckleCore2.dll", + "Objects.dll" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Tests/PlayMode/PlayModeTests.asmdef.meta b/Assets/Tests/PlayMode/PlayModeTests.asmdef.meta new file mode 100644 index 0000000..01bf67d --- /dev/null +++ b/Assets/Tests/PlayMode/PlayModeTests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 79301723eb79d2745ab1e1a9360f6f2d +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/PlayMode/SpeckleReceiverTests.cs b/Assets/Tests/PlayMode/SpeckleReceiverTests.cs new file mode 100644 index 0000000..bbc0dd3 --- /dev/null +++ b/Assets/Tests/PlayMode/SpeckleReceiverTests.cs @@ -0,0 +1,96 @@ +#nullable enable +using System; +using System.Collections; +using NUnit.Framework; +using Speckle.ConnectorUnity.Components; +using Speckle.Core.Models; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Speckle.ConnectorUnity.Tests +{ + [TestFixture, TestOf(typeof(SpeckleReceiver))] + public sealed class SpeckleReceiverTests : ComponentTest + { + + [UnityTest] + public IEnumerator ReceiveAsync_Succeeds() + { + yield return null; + + var task = new Utils.Utils.WaitForTask(async () => await sut.ReceiveAsync(default)); + yield return task; + Base myBase = task.Result; + Assert.That(myBase, Is.Not.Null); + } + + [UnityTest] + public IEnumerator ReceiveAndConvert_Async_Succeeds() + { + Transform expectedParent = new GameObject("parent").transform; + yield return null; + + bool wasSuccessful = false; + Transform? actualParent = null; + + sut.OnComplete.AddListener(t => + { + wasSuccessful = true; + actualParent = t; + }); + sut.OnErrorAction.AddListener((_, ex) => throw new Exception("Failed", ex)); + + sut.ReceiveAndConvert_Async(expectedParent); + + yield return new WaitUntil(() => wasSuccessful); + + Assert.That(actualParent, Is.EqualTo(expectedParent)); + } + + [UnityTest] + public IEnumerator ReceiveAndConvert_Routine_Succeeds() + { + Transform expectedParent = new GameObject("parent").transform; + yield return null; + + bool wasSuccessful = false; + Transform? actualParent = null; + + sut.OnComplete.AddListener(t => + { + wasSuccessful = true; + actualParent = t; + }); + sut.OnErrorAction.AddListener((_, ex) => throw new Exception("Failed", ex)); + + yield return sut.ReceiveAndConvert_Routine(expectedParent); + + yield return new WaitUntil(() => wasSuccessful); + + Assert.That(actualParent, Is.EqualTo(expectedParent)); + } + + [UnityTest] + public IEnumerator ReceiveAndConvert_Coroutine_Succeeds() + { + Transform expectedParent = new GameObject("parent").transform; + yield return null; + + bool wasSuccessful = false; + Transform? actualParent = null; + + sut.OnComplete.AddListener(t => + { + wasSuccessful = true; + actualParent = t; + }); + sut.OnErrorAction.AddListener((_, ex) => throw new Exception("Failed", ex)); + + yield return sut.StartCoroutine(sut.ReceiveAndConvert_Routine(expectedParent)); + + yield return new WaitUntil(() => wasSuccessful); + + Assert.That(actualParent, Is.EqualTo(expectedParent)); + } + } +} diff --git a/Assets/Tests/PlayMode/SpeckleReceiverTests.cs.meta b/Assets/Tests/PlayMode/SpeckleReceiverTests.cs.meta new file mode 100644 index 0000000..5664628 --- /dev/null +++ b/Assets/Tests/PlayMode/SpeckleReceiverTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1756c50dd28a4e341a70866daa68a8d5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/systems.speckle.speckle-unity/Editor/Components/SpeckleReceiverEditor.cs b/Packages/systems.speckle.speckle-unity/Editor/Components/SpeckleReceiverEditor.cs index 0aa2a52..c3a98b2 100644 --- a/Packages/systems.speckle.speckle-unity/Editor/Components/SpeckleReceiverEditor.cs +++ b/Packages/systems.speckle.speckle-unity/Editor/Components/SpeckleReceiverEditor.cs @@ -1,11 +1,6 @@ using System; using System.Collections.Concurrent; -using System.Linq; -using System.Threading; using System.Threading.Tasks; -using Speckle.ConnectorUnity.Utils; -using Speckle.Core.Api; -using Speckle.Core.Logging; using Speckle.Core.Models; using Speckle.Core.Models.GraphTraversal; using UnityEditor; @@ -21,51 +16,68 @@ namespace Speckle.ConnectorUnity.Components.Editor private static bool generateAssets = false; private bool foldOutStatus = true; private Texture2D? previewImage; - + public override async void OnInspectorGUI() { + var speckleReceiver = (SpeckleReceiver)target; + DrawDefaultInspector(); //Preview image - foldOutStatus = EditorGUILayout.Foldout(foldOutStatus, "Preview Image"); - if (foldOutStatus) { - Rect rect = GUILayoutUtility.GetAspectRect(7f / 4f); - if (previewImage != null) GUI.DrawTexture(rect, previewImage); + foldOutStatus = EditorGUILayout.Foldout(foldOutStatus, "Preview Image"); + if (foldOutStatus) + { + Rect rect = GUILayoutUtility.GetAspectRect(7f / 4f); + if (previewImage != null) GUI.DrawTexture(rect, previewImage); + } } - - //Receive button - bool receive = GUILayout.Button("Receive!"); - - bool selection = EditorGUILayout.ToggleLeft("Generate Assets", generateAssets); - if (generateAssets != selection) - { - generateAssets = selection; - UpdateGenerateAssets(); - } - - //TODO: Draw events in a collapsed region - - - if (receive) + + //Receive settings { - try + bool prev = GUI.enabled; + GUI.enabled = !speckleReceiver.IsReceiving; + //Receive button + bool receive = GUILayout.Button("Receive!"); + + bool selection = EditorGUILayout.ToggleLeft("Generate Assets", generateAssets); + if (generateAssets != selection) { - await ReceiveSelection(); + generateAssets = selection; + UpdateGenerateAssets(); } - catch (OperationCanceledException ex) + GUI.enabled = prev; + + + if (receive && !speckleReceiver.IsReceiving) { - Debug.Log($"Receive operation cancelled\n{ex}", this); - } - catch (Exception ex) - { - Debug.LogError($"Failed to receive selection {ex}", this); - } - finally - { - EditorUtility.ClearProgressBar(); + int id = Progress.Start( + "Receiving Speckle data", + "Fetching commit data", + Progress.Options.Indefinite | Progress.Options.Sticky); + Progress.SetPriority(id, Progress.Priority.High); + Progress.ShowDetails(); + try + { + await ReceiveSelection(id).ConfigureAwait(true); + Progress.Finish(id); + } + catch (OperationCanceledException ex) + { + Progress.Finish(id, Progress.Status.Canceled); + Debug.Log($"Receive operation cancelled\n{ex}", this); + } + catch (Exception ex) + { + Progress.Finish(id, Progress.Status.Failed); + Debug.LogError($"Receive operation failed {ex}", this); + } + finally + { + EditorUtility.ClearProgressBar(); + } } } } @@ -94,32 +106,57 @@ namespace Speckle.ConnectorUnity.Components.Editor previewImage = null; ((SpeckleReceiver)target).GetPreviewImage(t => previewImage = t); } - - private async Task ReceiveSelection() + + private async Task ReceiveSelection(int progressId) { var speckleReceiver = (SpeckleReceiver)target; - Base commitObject = await ReceiveCommit(); - - int childrenConverted = 0; - float totalChildren = commitObject.totalChildrenCount; - bool shouldCancel = false; + Progress.RegisterCancelCallback(progressId, () => + { + speckleReceiver.Cancel(); + shouldCancel = true; + return true; + }); + + Base commitObject; + try + { + var token = speckleReceiver.BeginOperation(); + commitObject = await Task.Run(async () => await ReceiveCommit(progressId).ConfigureAwait(false), + token + ) + .ConfigureAwait(true); + } + finally + { + speckleReceiver.FinishOperation(); + } + + int childrenConverted = 0; + int childrenFailed = 0; + + int totalChildren = (int) Math.Min(commitObject.totalChildrenCount, int.MaxValue); + float totalChildrenFloat = commitObject.totalChildrenCount; + + var convertProgress = Progress.Start("Converting To Native", "Preparing...", Progress.Options.Indefinite | Progress.Options.Sticky, progressId); + bool BeforeConvert(TraversalContext context) { Base b = context.current; //NOTE: progress wont reach 100% because not all objects are convertable - float progress = childrenConverted / totalChildren; - + float progress = (childrenConverted + childrenFailed) / totalChildrenFloat; + + if (shouldCancel) return false; + shouldCancel = EditorUtility.DisplayCancelableProgressBar( "Converting To Native...", $"{b.speckle_type} - {b.id}", progress); - - return true; + return !shouldCancel; } foreach (var conversionResult in speckleReceiver.Converter.RecursivelyConvertToNative_Enumerable( @@ -134,19 +171,27 @@ namespace Speckle.ConnectorUnity.Components.Editor } else { + childrenFailed++; Debug.LogWarning( $"Failed to convert Speckle object of type {speckleObject.speckle_type}\n{ex}", this); } + Progress.Report(progressId, childrenConverted + childrenFailed, totalChildren, "Receiving objects"); + if (shouldCancel) break; } - Debug.Log( - shouldCancel - ? $"Stopped converting to native. Created {childrenConverted} {nameof(GameObject)}s: Responding to cancel through editor" - : $"Finished converting to native. Created {childrenConverted} {nameof(GameObject)}s ", - this); + var resultString = $"{childrenConverted}{nameof(GameObject)}s created, {childrenFailed} objects failed to convert"; + + Debug.Log(shouldCancel + ? $"Stopped converting to native: The operation has been cancelled - {resultString}\n " + : $"Finished converting to native.\n{resultString}", + speckleReceiver); + + Progress.Finish(convertProgress); + + if (shouldCancel) throw new OperationCanceledException("Conversion operation canceled through editor dialogue"); } private void UpdateGenerateAssets() @@ -155,65 +200,73 @@ namespace Speckle.ConnectorUnity.Components.Editor speckleReceiver.Converter.AssetCache.nativeCaches = NativeCacheFactory.GetDefaultNativeCacheSetup(generateAssets); } - private async Task ReceiveCommit() + private async Task ReceiveCommit(int progressId) { var speckleReceiver = (SpeckleReceiver)target; - - speckleReceiver.BeginOperation(); - - string serverLogName = speckleReceiver.Account.Client?.ServerUrl ?? "Speckle"; - string message = $"Receiving data from {serverLogName}..."; - EditorUtility.DisplayProgressBar(message, "Fetching data", 0f); + string serverLogName = speckleReceiver.Account.Client?.ServerUrl ?? "Speckle"; + + int transport = Progress.Start($"Downloading data from {serverLogName}", "Waiting...", Progress.Options.Sticky, progressId); + int deserialize = Progress.Start("Deserializing data", "Waiting...", Progress.Options.Sticky, progressId); var totalObjectCount = 1; void OnTotalChildrenKnown(int count) { totalObjectCount = count; - EditorApplication.delayCall += () => EditorUtility.DisplayProgressBar(message, $"Fetching data {0}/{totalObjectCount}", 0f); + Progress.Report(progressId, 0, totalObjectCount, "Receiving objects"); } void OnProgress(ConcurrentDictionary dict) { - var currentProgress = dict.Values.Average(); - var progress = (float) currentProgress / totalObjectCount; - EditorApplication.delayCall += () => + bool r = dict.TryGetValue("RemoteTransport", out int rtProgress); + bool l = dict.TryGetValue("SQLite", out int ltProgress); + if (r || l) { - bool shouldCancel = EditorUtility.DisplayCancelableProgressBar(message, - $"Downloading data {currentProgress}/{totalObjectCount}", - progress); - - if (shouldCancel) - { - speckleReceiver.CancellationTokenSource!.Cancel(); - } - }; + var fetched = (rtProgress + ltProgress); + Progress.Report(transport, fetched, totalObjectCount, $"{fetched}/{totalObjectCount}"); + } + + if (dict.TryGetValue("DS", out int tsProgress)) + { + tsProgress--; //The root object isn't included, so we add an extra 1 + Progress.Report(deserialize,tsProgress, totalObjectCount, $"{tsProgress}/{totalObjectCount}"); + } } - + Base commitObject; try { speckleReceiver.OnTotalChildrenCountKnown.AddListener(OnTotalChildrenKnown); speckleReceiver.OnReceiveProgressAction.AddListener(OnProgress); - commitObject = await speckleReceiver.ReceiveAsync(speckleReceiver.CancellationToken).ConfigureAwait(false); + commitObject = await speckleReceiver.ReceiveAsync(speckleReceiver.CancellationToken) + .ConfigureAwait(false); + Progress.Finish(transport); + Progress.Finish(deserialize); } - catch(Exception ex) + catch(OperationCanceledException) { - throw new SpeckleException("Failed to receive commit", ex); + Progress.Finish(transport, Progress.Status.Canceled); + Progress.Finish(deserialize, Progress.Status.Canceled); + throw; + } + catch(Exception) + { + Progress.Finish(transport, Progress.Status.Failed); + Progress.Finish(deserialize, Progress.Status.Failed); + throw; } finally { speckleReceiver.OnTotalChildrenCountKnown.RemoveListener(OnTotalChildrenKnown); speckleReceiver.OnReceiveProgressAction.RemoveListener(OnProgress); - EditorApplication.delayCall += EditorUtility.ClearProgressBar; - speckleReceiver.FinishOperation(); } return commitObject; } [MenuItem("GameObject/Speckle/Speckle Connector", false, 10)] - static void CreateCustomGameObject(MenuCommand menuCommand) { + static void CreateCustomGameObject(MenuCommand menuCommand) + { // Create a custom game object GameObject go = new GameObject("Speckle Connector"); // Ensure it gets reparented if this was a context click (otherwise does nothing) diff --git a/Packages/systems.speckle.speckle-unity/Editor/Components/SpeckleSendEditor.cs b/Packages/systems.speckle.speckle-unity/Editor/Components/SpeckleSendEditor.cs index 1fb2af0..cec8754 100644 --- a/Packages/systems.speckle.speckle-unity/Editor/Components/SpeckleSendEditor.cs +++ b/Packages/systems.speckle.speckle-unity/Editor/Components/SpeckleSendEditor.cs @@ -106,12 +106,6 @@ namespace Speckle.ConnectorUnity.Components.Editor SceneManager.GetActiveScene().GetRootGameObjects(), go => go.activeInHierarchy); } - - private void CancelSend() - { - ((SpeckleReceiver)target).CancellationTokenSource?.Cancel(); - EditorApplication.delayCall += EditorUtility.ClearProgressBar; - } - + } } diff --git a/Packages/systems.speckle.speckle-unity/Runtime/Components/RecursiveConverter.ToNative.cs b/Packages/systems.speckle.speckle-unity/Runtime/Components/RecursiveConverter.ToNative.cs index a2b76fc..3e6454e 100644 --- a/Packages/systems.speckle.speckle-unity/Runtime/Components/RecursiveConverter.ToNative.cs +++ b/Packages/systems.speckle.speckle-unity/Runtime/Components/RecursiveConverter.ToNative.cs @@ -78,9 +78,11 @@ namespace Speckle.ConnectorUnity.Components { converted = this.converted; exception = this.exception; - return this.exception == null; + return WasSuccessful(); } + public bool WasSuccessful() => this.exception == null; + public Base SpeckleObject => traversalContext.current; } @@ -136,10 +138,10 @@ namespace Speckle.ConnectorUnity.Components Dictionary created = new(); foreach (var conversionResult in ConvertTree(objectsToConvert, parent, created)) { + if (!isActiveAndEnabled) throw new InvalidOperationException($"Cannot convert objects while {GetType()} is disabled"); + yield return conversionResult; } - - Debug.Log($"Finished converting {rootObject.id} to native. Created {created.Count} {nameof(GameObject)}s ",this); } /// diff --git a/Packages/systems.speckle.speckle-unity/Runtime/Components/SpeckleReceiver.cs b/Packages/systems.speckle.speckle-unity/Runtime/Components/SpeckleReceiver.cs index d5ee72c..85fd2b3 100644 --- a/Packages/systems.speckle.speckle-unity/Runtime/Components/SpeckleReceiver.cs +++ b/Packages/systems.speckle.speckle-unity/Runtime/Components/SpeckleReceiver.cs @@ -40,29 +40,45 @@ namespace Speckle.ConnectorUnity.Components public RecursiveConverter Converter { get; private set; } +#nullable enable [Header("Events")] [HideInInspector] - public CommitSelectionEvent OnCommitSelectionChange; + public CommitSelectionEvent OnCommitSelectionChange = new(); [HideInInspector] - public OperationProgressEvent OnReceiveProgressAction; + public OperationProgressEvent OnReceiveProgressAction = new(); [HideInInspector] - public ErrorActionEvent OnErrorAction; + public ErrorActionEvent OnErrorAction = new(); [HideInInspector] - public ChildrenCountHandler OnTotalChildrenCountKnown; + public ChildrenCountHandler OnTotalChildrenCountKnown = new(); [HideInInspector] - public ReceiveCompleteHandler OnComplete; + public ReceiveCompleteHandler OnComplete = new(); -#nullable enable - protected internal CancellationTokenSource? CancellationTokenSource { get; private set; } - protected internal CancellationToken CancellationToken => CancellationTokenSource?.Token ?? default; + protected CancellationTokenSource? CancellationTokenSource { get; private set; } + public CancellationToken CancellationToken => CancellationTokenSource?.Token ?? default; public bool IsReceiving => CancellationTokenSource != null; + + /// + /// Cancels any current receive operations + /// + /// + /// Note, this does not cancel any currently executing ConvertToNative, just the . + /// + /// if the cancellation request was made. if there was no pending operation to cancel (see ) + public bool Cancel() + { + if (CancellationTokenSource == null) return false; + CancellationTokenSource.Cancel(); + return true; + } /// /// Receive the selected object, and converts ToNative as children of /// - /// - /// - /// function does not throw, instead calls , and calls on complteion + /// Optional parent for the created root s + /// A filter function to allow for selectively excluding certain objects from being converted + /// function does not throw, instead calls , and calls upon completion + /// + /// public IEnumerator ReceiveAndConvert_Routine(Transform? parent, Predicate? predicate = null) { if (IsReceiving) @@ -172,18 +188,20 @@ namespace Speckle.ConnectorUnity.Components /// Starts a new receive operation with a /// /// already receiving - protected internal void BeginOperation() + protected internal CancellationToken BeginOperation() { if (IsReceiving) throw new InvalidOperationException("A pending receive operation has already started"); CancellationTokenSource?.Dispose(); CancellationTokenSource = new(); + + return CancellationTokenSource.Token; } protected internal void FinishOperation() { if (!IsReceiving) throw new InvalidOperationException("No pending operations to finish"); - + CancellationTokenSource!.Dispose(); CancellationTokenSource = null; } @@ -388,6 +406,8 @@ namespace Speckle.ConnectorUnity.Components Application.OpenURL(url); } #endif + + public string GetSelectedUrl() { string serverUrl = Account.Selected!.serverInfo.url; @@ -423,12 +443,16 @@ namespace Speckle.ConnectorUnity.Components Account.RefreshOptions(); } - public void OnDestroy() + public void OnDisable() { CancellationTokenSource?.Cancel(); + } + + public void OnDestroy() + { CancellationTokenSource?.Dispose(); } - + public void OnBeforeSerialize() { //pass diff --git a/Packages/systems.speckle.speckle-unity/Runtime/Utils/Utils.cs b/Packages/systems.speckle.speckle-unity/Runtime/Utils/Utils.cs index 6c22128..f587afb 100644 --- a/Packages/systems.speckle.speckle-unity/Runtime/Utils/Utils.cs +++ b/Packages/systems.speckle.speckle-unity/Runtime/Utils/Utils.cs @@ -1,7 +1,7 @@ #nullable enable using System; using System.Collections; -using Speckle.Core.Models; +using System.Threading.Tasks; using UnityEngine; using UnityEngine.Networking; @@ -94,5 +94,33 @@ namespace Speckle.ConnectorUnity.Utils Texture2D? texture = DownloadHandlerTexture.GetContent(www); callback.Invoke(texture); } + + /// + /// Coroutine that starts and waits for an async + /// to complete. + /// + /// Useful for running async tasks from coroutines + public class WaitForTask : CustomYieldInstruction + { + public readonly Task Task; + public override bool keepWaiting => !Task.IsCompleted; + + public WaitForTask(Func function) + { + Task = Task.Run(function); + } + } + + /// + public sealed class WaitForTask : CustomYieldInstruction + { + public readonly Task Task; + public TResult Result => Task.Result; + public override bool keepWaiting => !Task.IsCompleted; + public WaitForTask(Func> function) + { + this.Task = System.Threading.Tasks.Task.Run(function); + } + } } } diff --git a/Packages/systems.speckle.speckle-unity/Runtime/Wrappers/Selection/OptionSelection.cs b/Packages/systems.speckle.speckle-unity/Runtime/Wrappers/Selection/OptionSelection.cs index 2e4e273..f0e67ba 100644 --- a/Packages/systems.speckle.speckle-unity/Runtime/Wrappers/Selection/OptionSelection.cs +++ b/Packages/systems.speckle.speckle-unity/Runtime/Wrappers/Selection/OptionSelection.cs @@ -34,7 +34,7 @@ namespace Speckle.ConnectorUnity.Wrappers.Selection { get { - if (Options == null) return null; + if (Options is null) return null; if (SelectedIndex < 0 || SelectedIndex >= Options.Length) return null; return Options[SelectedIndex]; } @@ -52,7 +52,7 @@ namespace Speckle.ConnectorUnity.Wrappers.Selection protected void GenerateOptions(IList source, Func isDefault) { - List optionsToAdd = new List(source.Count); + List optionsToAdd = new (source.Count); int defaultOption = -1; int index = 0; foreach (TOption? a in source) @@ -77,4 +77,4 @@ namespace Speckle.ConnectorUnity.Wrappers.Selection OnSelectionChange?.Invoke(); } } -} \ No newline at end of file +}