// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. using HoloToolkit.Unity; using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using UnityEngine; using UnityEngine.UI; namespace HoloToolkit.Examples.SpatialUnderstandingFeatureOverview { public class UI : LineDrawer { // Consts public const float MenuWidth = 1.5f; public const float MenuHeight = 1.0f; public const float MenuMinDepth = 2.0f; // Enums public enum Panels { Topology, Shapes, LevelSolver, PANEL_COUNT } [Serializable] public class TabPanel { public Button Button; public Image ButtonImage; public Image Background; public GridLayoutGroup ButtonGrid; public List<Button> GridButtons = new List<Button>(); } // Config public Canvas ParentCanvas; public TabPanel[] ButtonPanels = new TabPanel[(int)Panels.PANEL_COUNT]; public Button PrefabButton; public LayerMask UILayerMask; // Properties public bool HasPlacedMenu { get; private set; } public AnimatedBox MenuAnimatedBox { get; private set; } public Panels ActivePanel { get; private set; } // Privates private DateTime timeLastQuery = DateTime.MinValue; private bool placedMenuNeedsBillboard = false; // Functions private void Start() { // Turn menu off until we're placed ParentCanvas.gameObject.SetActive(false); // Events SpatialUnderstanding.Instance.ScanStateChanged += OnScanStateChanged; } protected override void OnDestroy() { if (SpatialUnderstanding.Instance != null) { SpatialUnderstanding.Instance.ScanStateChanged -= OnScanStateChanged; } base.OnDestroy(); } private void OnScanStateChanged() { // If we are leaving the None state, go ahead and register shapes now if ((SpatialUnderstanding.Instance.ScanState == SpatialUnderstanding.ScanStates.Done) && SpatialUnderstanding.Instance.AllowSpatialUnderstanding) { // Make sure we've created our shapes ShapeDefinition.Instance.CreateShapes(); // Make sure our solver is initialized LevelSolver.Instance.InitializeSolver(); // Setup the menu StartCoroutine(SetupMenu()); } } private IEnumerator SetupMenu() { // Setup for queries SpatialUnderstandingDllTopology.TopologyResult[] resultsTopology = new SpatialUnderstandingDllTopology.TopologyResult[1]; IntPtr resultsTopologyPtr = SpatialUnderstanding.Instance.UnderstandingDLL.PinObject(resultsTopology); // Place on a wall (do it in a thread, as it can take a little while) SpatialUnderstandingDllObjectPlacement.ObjectPlacementDefinition placeOnWallDef = SpatialUnderstandingDllObjectPlacement.ObjectPlacementDefinition.Create_OnWall(new Vector3(MenuWidth * 0.5f, MenuHeight * 0.5f, MenuMinDepth * 0.5f), 0.5f, 3.0f); SpatialUnderstandingDllObjectPlacement.ObjectPlacementResult placementResult = SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticObjectPlacementResult(); var thread = #if UNITY_EDITOR || !UNITY_WSA new System.Threading.Thread #else System.Threading.Tasks.Task.Run #endif (() => { if (SpatialUnderstandingDllObjectPlacement.Solver_PlaceObject( "UIPlacement", SpatialUnderstanding.Instance.UnderstandingDLL.PinObject(placeOnWallDef), 0, IntPtr.Zero, 0, IntPtr.Zero, SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticObjectPlacementResultPtr()) == 0) { placementResult = null; } }); #if UNITY_EDITOR || !UNITY_WSA thread.Start(); #endif while ( #if UNITY_EDITOR || !UNITY_WSA !thread.Join(TimeSpan.Zero) #else !thread.IsCompleted #endif ) { yield return null; } if (placementResult != null) { Debug.Log("PlaceMenu - ObjectSolver-OnWall"); Vector3 posOnWall = placementResult.Position - placementResult.Forward * MenuMinDepth * 0.5f; PlaceMenu(posOnWall, -placementResult.Forward); yield break; } // Wait a frame yield return null; Transform cameraTransform = CameraCache.Main.transform; // Fallback, place floor (add a facing, if so) int locationCount = SpatialUnderstandingDllTopology.QueryTopology_FindLargestPositionsOnFloor( resultsTopology.Length, resultsTopologyPtr); if (locationCount > 0) { Debug.Log("PlaceMenu - LargestPositionsOnFloor"); SpatialUnderstandingDllTopology.TopologyResult menuLocation = resultsTopology[0]; Vector3 menuPosition = menuLocation.position + Vector3.up * MenuHeight; Vector3 menuLookVector = cameraTransform.position - menuPosition; PlaceMenu(menuPosition, (new Vector3(menuLookVector.x, 0.0f, menuLookVector.z)).normalized, true); yield break; } // Final fallback just in front of the user SpatialUnderstandingDll.Imports.QueryPlayspaceAlignment(SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceAlignmentPtr()); SpatialUnderstandingDll.Imports.PlayspaceAlignment alignment = SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceAlignment(); Vector3 defaultPosition = cameraTransform.position + cameraTransform.forward * 2.0f; PlaceMenu(new Vector3(defaultPosition.x, Math.Max(defaultPosition.y, alignment.FloorYValue + 1.5f), defaultPosition.z), (new Vector3(cameraTransform.forward.x, 0.0f, cameraTransform.forward.z)).normalized, true); Debug.Log("PlaceMenu - InFrontOfUser"); } private void SetActiveTab(Panels panel) { // Set it ActivePanel = panel; timeLastQuery = DateTime.MinValue; // Colors Update_Colors(); } private void Update_Colors() { const float TimeToFadeAfterQuery = 3.0f; // Time since query (fade for a bit after a query) float timeSinceQuery = (float)(DateTime.Now - timeLastQuery).TotalSeconds; float alphaScale = Mathf.SmoothStep(0.0f, 1.0f, Mathf.Clamp01(timeSinceQuery - TimeToFadeAfterQuery)) * 0.8f + 0.2f; // Colors Color colorButtonActive = new Color(1.0f, 1.0f, 1.0f, 0.8f * alphaScale); Color colorButtonInactive = new Color(1.0f, 1.0f, 1.0f, 0.25f * alphaScale); Color colorPanelActive = new Color(1.0f, 1.0f, 1.0f, 0.6f * alphaScale); Color colorPanelInactive = new Color(1.0f, 1.0f, 1.0f, 0.15f * alphaScale); // Colors on buttons for (int i = 0; i < (int)Panels.PANEL_COUNT; ++i) { bool isEnabled = (i == (int)ActivePanel); ButtonPanels[i].ButtonImage.color = isEnabled ? colorButtonActive : colorButtonInactive; ButtonPanels[i].Background.enabled = isEnabled; ButtonPanels[i].Background.color = isEnabled ? colorPanelActive : colorPanelInactive; ButtonPanels[i].ButtonGrid.enabled = isEnabled; for (int j = 0; j < ButtonPanels[i].GridButtons.Count; ++j) { ButtonPanels[i].GridButtons[j].gameObject.SetActive(isEnabled); } } } private void SetupMenus() { // Topology queries ButtonPanels[(int)Panels.Topology].Button.GetComponentInChildren<Text>().text = "Topology Queries"; ButtonPanels[(int)Panels.Topology].Button.onClick.AddListener(() => { SetActiveTab(Panels.Topology); }); AddButton("Position on wall", Panels.Topology, () => { SpaceVisualizer.Instance.Query_Topology_FindPositionOnWall(); timeLastQuery = DateTime.MinValue; }); AddButton("Large positions on wall", Panels.Topology, () => { SpaceVisualizer.Instance.Query_Topology_FindLargePositionsOnWalls(); timeLastQuery = DateTime.MinValue; }); AddButton("Largest wall", Panels.Topology, () => { SpaceVisualizer.Instance.Query_Topology_FindLargeWall(); timeLastQuery = DateTime.MinValue; }); AddButton("Positions on floor", Panels.Topology, () => { SpaceVisualizer.Instance.Query_Topology_FindPositionsOnFloor(); timeLastQuery = DateTime.MinValue; }); AddButton("Large positions on floor", Panels.Topology, () => { SpaceVisualizer.Instance.Query_Topology_FindLargestPositionsOnFloor(); timeLastQuery = DateTime.MinValue; }); AddButton("Place objects positions", Panels.Topology, () => { SpaceVisualizer.Instance.Query_Topology_FindPositionsPlaceable(); timeLastQuery = DateTime.MinValue; }); AddButton("Large positions sittable", Panels.Topology, () => { SpaceVisualizer.Instance.Query_Topology_FindLargePositionsSittable(); timeLastQuery = DateTime.MinValue; }); // Shape queries ButtonPanels[(int)Panels.Shapes].Button.GetComponentInChildren<Text>().text = "Shape Queries"; ButtonPanels[(int)Panels.Shapes].Button.onClick.AddListener(() => { SetActiveTab(Panels.Shapes); }); ReadOnlyCollection<string> customShapes = ShapeDefinition.Instance.CustomShapeDefinitions; for (int i = 0; i < customShapes.Count; ++i) { string shapeName = customShapes[i]; AddButton(shapeName, Panels.Shapes, () => { SpaceVisualizer.Instance.Query_Shape_FindShapeHalfDims(shapeName); timeLastQuery = DateTime.MinValue; }); } // Level solver ButtonPanels[(int)Panels.LevelSolver].Button.GetComponentInChildren<Text>().text = "Object Placement"; ButtonPanels[(int)Panels.LevelSolver].Button.onClick.AddListener(() => { SetActiveTab(Panels.LevelSolver); timeLastQuery = DateTime.MinValue; }); AddButton("On Floor", Panels.LevelSolver, () => { LevelSolver.Instance.Query_OnFloor(); timeLastQuery = DateTime.MinValue; }); AddButton("On Wall", Panels.LevelSolver, () => { LevelSolver.Instance.Query_OnWall(); timeLastQuery = DateTime.MinValue; }); AddButton("On Ceiling", Panels.LevelSolver, () => { LevelSolver.Instance.Query_OnCeiling(); timeLastQuery = DateTime.MinValue; }); AddButton("On SurfaceEdge", Panels.LevelSolver, () => { LevelSolver.Instance.Query_OnEdge(); timeLastQuery = DateTime.MinValue; }); AddButton("On FloorAndCeiling", Panels.LevelSolver, () => { LevelSolver.Instance.Query_OnFloorAndCeiling(); timeLastQuery = DateTime.MinValue; }); AddButton("RandomInAir AwayFromMe", Panels.LevelSolver, () => { LevelSolver.Instance.Query_RandomInAir_AwayFromMe(); timeLastQuery = DateTime.MinValue; }); AddButton("OnEdge NearCenter", Panels.LevelSolver, () => { LevelSolver.Instance.Query_OnEdge_NearCenter(); timeLastQuery = DateTime.MinValue; }); AddButton("OnFloor AwayFromMe", Panels.LevelSolver, () => { LevelSolver.Instance.Query_OnFloor_AwayFromMe(); timeLastQuery = DateTime.MinValue; }); AddButton("OnFloor NearMe", Panels.LevelSolver, () => { LevelSolver.Instance.Query_OnFloor_NearMe(); timeLastQuery = DateTime.MinValue; }); // Default one of them active SetActiveTab(Panels.Topology); } private void AddButton(string text, Panels panel, UnityEngine.Events.UnityAction action) { Button button = Instantiate(PrefabButton); button.GetComponentInChildren<Text>().text = text; button.transform.SetParent(ButtonPanels[(int)panel].ButtonGrid.transform, false); button.transform.localScale = Vector3.one; button.onClick.AddListener(action); ButtonPanels[(int)panel].GridButtons.Add(button); } private void PlaceMenu(Vector3 position, Vector3 normal, bool needsBillboarding = false) { // Offset in a bit position -= normal * 0.05f; Quaternion rotation = Quaternion.LookRotation(normal, Vector3.up); // Place it transform.position = position; transform.rotation = rotation; // Setup the menu SetupMenus(); // Enable it ParentCanvas.gameObject.SetActive(true); // Create up a box MenuAnimatedBox = new AnimatedBox(0.0f, position, rotation, new Color(1.0f, 1.0f, 1.0f, 0.25f), new Vector3(MenuWidth * 0.5f, MenuHeight * 0.5f, 0.025f), LineDrawer.DefaultLineWidth * 0.5f); // Initial position transform.position = MenuAnimatedBox.AnimPosition.Evaluate(MenuAnimatedBox.Time); transform.rotation = MenuAnimatedBox.Rotation * Quaternion.AngleAxis(360.0f * MenuAnimatedBox.AnimRotation.Evaluate(MenuAnimatedBox.Time), Vector3.up); // Billboarding (note that because of the transition animation we need to place this late) placedMenuNeedsBillboard = needsBillboarding; // And mark that we've done it HasPlacedMenu = true; } private void Update() { Update_Colors(); // Animated box if (MenuAnimatedBox != null) { // We're using the animated box for the animation only MenuAnimatedBox.Update(Time.deltaTime); // Billboarding if (MenuAnimatedBox.IsAnimationComplete && placedMenuNeedsBillboard) { // Rotate to face the user transform.position = MenuAnimatedBox.AnimPosition.Evaluate(MenuAnimatedBox.Time); Vector3 lookDirTarget = CameraCache.Main.transform.position - transform.position; lookDirTarget = (new Vector3(lookDirTarget.x, 0.0f, lookDirTarget.z)).normalized; transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(-lookDirTarget), Time.deltaTime * 10.0f); } else { // Keep the UI locked to the animated box transform.position = MenuAnimatedBox.AnimPosition.Evaluate(MenuAnimatedBox.Time); transform.rotation = MenuAnimatedBox.Rotation * Quaternion.AngleAxis(360.0f * MenuAnimatedBox.AnimRotation.Evaluate(MenuAnimatedBox.Time), Vector3.up); } } } } }