private void ProcessPlacementResults()
    {
        // Check it
        if (queryStatus.State != QueryStates.Finished)
        {
            return;
        }
        if (!SpatialUnderstanding.Instance.AllowSpatialUnderstanding)
        {
            return;
        }

        // Clear results
        ClearGeometry();

        // We will reject any above or below the ceiling/floor
        SpatialUnderstandingDll.Imports.QueryPlayspaceAlignment(SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceAlignmentPtr());
        SpatialUnderstandingDll.Imports.PlayspaceAlignment alignment = SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceAlignment();

        // Copy over the results
        for (int i = 0; i < queryStatus.QueryResults.Count; ++i)
        {
            if ((queryStatus.QueryResults[i].Position.y < alignment.CeilingYValue) &&
                (queryStatus.QueryResults[i].Position.y > alignment.FloorYValue))
            {
                placementResults.Add(new PlacementResult(queryStatus.QueryResults[i].Clone() as SpatialUnderstandingDllObjectPlacement.ObjectPlacementResult, queryStatus.ObjectsToPlace[i]));
            }
        }

        // Text
        SpatialScanManager.Instance.ObjectPlacementDescription = queryStatus.Name + " (" + placementResults.Count + "/" + (queryStatus.CountSuccess + queryStatus.CountFail) + ")";

        // Mark done
        queryStatus.Reset();
    }
Exemple #2
0
    public void Query_PlayspaceAlignment()
    {
        // First clear all our geo
        ClearGeometry();

        // Only if we're enabled
        if (!SpatialUnderstanding.Instance.AllowSpatialUnderstanding)
        {
            return;
        }

        // Alignment information
        SpatialUnderstandingDll.Imports.QueryPlayspaceAlignment(SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceAlignmentPtr());
        SpatialUnderstandingDll.Imports.PlayspaceAlignment alignment = SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceAlignment();

        // Box for the space
        float timeDelay = (float)lineBoxList.Count * AnimatedBox.DelayPerItem;

        lineBoxList.Add(
            new AnimatedBox(
                timeDelay,
                new Vector3(alignment.Center.x, (alignment.CeilingYValue + alignment.FloorYValue) * 0.5f, alignment.Center.z),
                Quaternion.LookRotation(alignment.BasisZ, alignment.BasisY),
                Color.magenta,
                new Vector3(alignment.HalfDims.x, (alignment.CeilingYValue - alignment.FloorYValue) * 0.5f, alignment.HalfDims.z))
            );
        AppState.Instance.SpaceQueryDescription = "Playspace Alignment OBB";
    }
Exemple #3
0
    //if we create a reset feature, it's quite important to have this behaviour accessible from outside void Start()
    private void startIt()
    {
        sortBiggestGOFirst();
        for (int i = 0; i < mToPlace.Length; i++)
        {
            if (separatePlat(mPosition[i]))
            {
                mPlatformLeftToPlace.Add(i);
            }
            else
            {
                mLeftToPlace.Add(i);
            }
        }

        mPlaceFWCRThreshold = mLeftToPlace.Count;
        mTimesTried         = 0;
        SpatialUnderstandingDll.Imports.PlayspaceAlignment lPA = SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceAlignment();
        mYMinRandom = (lPA.CeilingYValue - lPA.FloorYValue) / 2 + lPA.FloorYValue;
        mYMaxRandom = lPA.CeilingYValue;

        if (mYMaxRandom == 0) //something went wrong when retrieving mYMaxRandom
        {
            mYMaxRandom = 4;
        }
    }
Exemple #4
0
        public PlacementQuery OnFloorAndCeiling(Vector3 halfDims)
        {
            SpatialUnderstandingDll.Imports.QueryPlayspaceAlignment(SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceAlignmentPtr());
            SpatialUnderstandingDll.Imports.PlayspaceAlignment alignment = SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceAlignment();

            return
                (new PlacementQuery(
                     SpatialUnderstandingDllObjectPlacement.ObjectPlacementDefinition.Create_OnFloorAndCeiling(
                         new Vector3(halfDims.x, (alignment.CeilingYValue - alignment.FloorYValue) * 0.5f, halfDims.z),
                         new Vector3(halfDims.x, (alignment.CeilingYValue - alignment.FloorYValue) * 0.5f, halfDims.z)),
                     new List <SpatialUnderstandingDllObjectPlacement.ObjectPlacementRule>()
            {
                SpatialUnderstandingDllObjectPlacement.ObjectPlacementRule.Create_AwayFromOtherObjects(
                    Math.Max(halfDims.x, halfDims.z) * 3.0f),
            }));
        }
        /**
         * Handle the the placement query for artwork 2.
         *
         * @param success   indicates whether the placement query was successful
         * @param position  position result of the placement query
         * @param alignment user space alignment
         */
        private void HandleArtwork2(bool success, Vector3 position, SpatialUnderstandingDll.Imports.PlayspaceAlignment alignment)
        {
            if (success)
            {
                // Calculate the rotation of the 3D asset
                int       maxIndex       = 0;
                Vector3   artworkForward = this.transform.forward;
                Vector3[] vectors        = new Vector3[] {
                    alignment.BasisX,
                    alignment.BasisX *(-1),
                    alignment.BasisZ,
                    alignment.BasisZ *(-1)
                };
                float[] dots = new float[] {
                    Vector3.Dot(alignment.BasisX, artworkForward),
                    Vector3.Dot(alignment.BasisX * (-1), artworkForward),
                    Vector3.Dot(alignment.BasisZ, artworkForward),
                    Vector3.Dot(alignment.BasisZ * (-1), artworkForward)
                };
                for (int i = 1; i < dots.Length; i++)
                {
                    maxIndex = (dots[maxIndex] >= dots[i]) ? maxIndex : i;
                }

                // Instantiate the world anchor and the 3D asset
                this.assetAnchor = Instantiate(this.worldAnchorParent, position, Quaternion.LookRotation(vectors[maxIndex], alignment.BasisY));
                string anchorID = ANCHOR_ID_M2;
                this.anchorManager.RemoveAnchor(anchorID);
                this.anchorManager.AttachAnchor(this.assetAnchor, anchorID);
                this.instantiated3DAsset = Instantiate(this.artwork2Asset, this.assetAnchor.transform);
                this._3DButton.GetComponent <Button>().interactable = true;
                this._3DButton.GetComponent <Display3DAsset>().ActivateVisuals();
            }
            else
            {
                string output = "An adequate table surface couldn't be found.";
                this.logger.Log(output);
                this.infoCanvas.SetInfoText(output);
            }

            // Activate input again
            if (HoloToolkit.Unity.InputModule.InputManager.IsInitialized)
            {
                HoloToolkit.Unity.InputModule.InputManager.Instance.PopInputDisable();
            }
        }
Exemple #6
0
        public void Query_OnFloorAndCeiling()
        {
            SpatialUnderstandingDll.Imports.QueryPlayspaceAlignment(SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceAlignmentPtr());
            SpatialUnderstandingDll.Imports.PlayspaceAlignment alignment = SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceAlignment();
            List <PlacementQuery> placementQuery = new List <PlacementQuery>();

            for (int i = 0; i < 4; ++i)
            {
                float halfDimSize = UnityEngine.Random.Range(0.1f, 0.2f);
                placementQuery.Add(
                    new PlacementQuery(SpatialUnderstandingDllObjectPlacement.ObjectPlacementDefinition.Create_OnFloorAndCeiling(new Vector3(halfDimSize, (alignment.CeilingYValue - alignment.FloorYValue) * 0.5f, halfDimSize),
                                                                                                                                 new Vector3(halfDimSize, (alignment.CeilingYValue - alignment.FloorYValue) * 0.5f, halfDimSize)),
                                       new List <SpatialUnderstandingDllObjectPlacement.ObjectPlacementRule>()
                {
                    SpatialUnderstandingDllObjectPlacement.ObjectPlacementRule.Create_AwayFromOtherObjects(halfDimSize * 3.0f),
                }));
            }
            PlaceObjectAsync("OnFloorAndCeiling", placementQuery);
        }
Exemple #7
0
    private void HandleResults_Shape(string visDesc, int shapeCount, Color color, Vector3 defaultHalfDims)
    {
        // First clear all our geo
        ClearGeometry();

        // Only if we're enabled
        if (!SpatialUnderstanding.Instance.AllowSpatialUnderstanding)
        {
            return;
        }

        // Alignment information
        SpatialUnderstandingDll.Imports.QueryPlayspaceAlignment(SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceAlignmentPtr());
        SpatialUnderstandingDll.Imports.PlayspaceAlignment alignment = SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceAlignment();

        // Add the line boxes (we may have more results than boxes - pick evenly across the results in that case)
        int lineInc        = Mathf.CeilToInt((float)shapeCount / (float)DisplayResultMaxCount);
        int boxesDisplayed = 0;

        for (int i = 0; i < shapeCount; i += lineInc)
        {
            float timeDelay = (float)lineBoxList.Count * AnimatedBox.DelayPerItem;
            lineBoxList.Add(
                new AnimatedBox(
                    timeDelay,
                    resultsShape[i].position,
                    Quaternion.LookRotation(alignment.BasisZ, alignment.BasisY),
                    Color.blue,
                    (resultsShape[i].halfDims.sqrMagnitude < 0.01f) ? defaultHalfDims : resultsShape[i].halfDims)
                );
            ++boxesDisplayed;
        }

        // Vis description
        if (shapeCount == boxesDisplayed)
        {
            AppState.Instance.SpaceQueryDescription = string.Format("{0} ({1})", visDesc, shapeCount);
        }
        else
        {
            AppState.Instance.SpaceQueryDescription = string.Format("{0} (found={1}, displayed={2})", visDesc, shapeCount, boxesDisplayed);
        }
    }
            private void SetupAnimation()
            {
                if (!SpatialUnderstandingManager.Instance.AllowSpatialUnderstanding)
                {
                    return;
                }

                // Calculate the forward distance for the animation start point
                Vector3 rayPos           = CameraCache.Main.transform.position;
                Vector3 rayVec           = CameraCache.Main.transform.forward * InitialPositionForwardMaxDistance;
                IntPtr  raycastResultPtr = SpatialUnderstandingManager.Instance.UnderstandingDLL.GetStaticRaycastResultPtr();

                SpatialUnderstandingDll.Imports.PlayspaceRaycast(
                    rayPos.x, rayPos.y, rayPos.z, rayVec.x, rayVec.y, rayVec.z,
                    raycastResultPtr);
                SpatialUnderstandingDll.Imports.RaycastResult rayCastResult = SpatialUnderstandingManager.Instance.UnderstandingDLL.GetStaticRaycastResult();
                Vector3 animOrigin = (rayCastResult.SurfaceType != SpatialUnderstandingDll.Imports.RaycastResult.SurfaceTypes.Invalid) ?
                                     rayPos + rayVec.normalized * Mathf.Max((rayCastResult.IntersectPoint - rayPos).magnitude - 0.3f, 0.0f) :
                                     rayPos + rayVec * InitialPositionForwardMaxDistance;

                // Create the animation (starting it on the ground in front of the camera
                SpatialUnderstandingDll.Imports.QueryPlayspaceAlignment(SpatialUnderstandingManager.Instance.UnderstandingDLL.GetStaticPlayspaceAlignmentPtr());
                SpatialUnderstandingDll.Imports.PlayspaceAlignment alignment = SpatialUnderstandingManager.Instance.UnderstandingDLL.GetStaticPlayspaceAlignment();
                AnimPosition.AddKey(TimeDelay + 0.0f, new Vector3(animOrigin.x, alignment.FloorYValue, animOrigin.z));
                AnimPosition.AddKey(TimeDelay + AnimationTime * 0.5f, new Vector3(animOrigin.x, alignment.FloorYValue + 1.25f, animOrigin.z));
                AnimPosition.AddKey(TimeDelay + AnimationTime * 0.6f, new Vector3(animOrigin.x, alignment.FloorYValue + 1.0f, animOrigin.z));
                AnimPosition.AddKey(TimeDelay + AnimationTime * 0.95f, Center);
                AnimPosition.AddKey(TimeDelay + AnimationTime * 1.0f, Center);

                AnimScale.AddKey(TimeDelay + 0.0f, 0.0f);
                AnimScale.AddKey(TimeDelay + AnimationTime * 0.5f, 0.5f);
                AnimScale.AddKey(TimeDelay + AnimationTime * 0.8f, 1.0f);
                AnimScale.AddKey(TimeDelay + AnimationTime * 1.0f, 1.0f);

                AnimRotation.AddKey(TimeDelay + 0.0f, -1.5f);
                AnimRotation.AddKey(TimeDelay + AnimationTime * 0.2f, -0.5f);
                AnimRotation.AddKey(TimeDelay + AnimationTime * 0.9f, 0.0f);
                AnimRotation.AddKey(TimeDelay + AnimationTime * 1.0f, 0.0f);

                IsAnimationSetup = true;
            }
Exemple #9
0
    private void FinishSetup()
    {
        // use spatial understanding to find floor
        SpatialUnderstandingDll.Imports.QueryPlayspaceAlignment(SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceAlignmentPtr());
        SpatialUnderstandingDll.Imports.PlayspaceAlignment alignment = SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceAlignment();

        // find large floor area
        IntPtr resultsTopologyPtr = SpatialUnderstanding.Instance.UnderstandingDLL.PinObject(resultsTopology);
        int    locationCount      = SpatialUnderstandingDllTopology.QueryTopology_FindLargestPositionsOnFloor(
            resultsTopology.Length, resultsTopologyPtr);

        // hide mesh
        var customMesh = SpatialUnderstanding.Instance.GetComponent <SpatialUnderstandingCustomMesh>();

        customMesh.DrawProcessedMesh = false;

        // find some topology data
        Vector3 floorPosition;

        if (locationCount > 0)
        {
            // retrieve a large floor area from spatial understanding
            floorPosition  = resultsTopology[0].position;
            _floorPosition = floorPosition;
        }
        else
        {
            // just get floor in front of the player
            //var inFrontOfCamera = Camera.main.transform.position + Camera.main.transform.forward * 1.5f;
            //floorPosition = new Vector3(inFrontOfCamera.x, alignment.FloorYValue, 2.0f);
        }
        //if (_floorObject != null)
        {
            // just get floor in front of the player
            var inFrontOfCamera = Camera.main.transform.position + Camera.main.transform.forward * 1.5f;
            floorPosition = new Vector3(inFrontOfCamera.x, alignment.FloorYValue, 1.5f);
            InstantiateNPCs(floorPosition);
        }
    }
Exemple #10
0
        /**
         * Process the placement results asynchronously.
         */
        private void ProcessPlacementResults()
        {
            // Check it
            if (this.queryStatus.state != QueryStates.Finished)
            {
                return;
            }
            if (!SpatialUnderstanding.IsInitialized || !SpatialUnderstanding.Instance.AllowSpatialUnderstanding)
            {
                return;
            }

            // We will reject any above or below the ceiling/floor
            SpatialUnderstandingDll.Imports.QueryPlayspaceAlignment(SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceAlignmentPtr());
            SpatialUnderstandingDll.Imports.PlayspaceAlignment alignment = SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceAlignment();

            // Copy over the results
            //for (int i = 0; i < this.queryStatus.queryResult.Count; ++i) {
            //    if ((this.queryStatus.queryResult[i].Position.y < alignment.CeilingYValue) && (this.queryStatus.queryResult[i].Position.y > alignment.FloorYValue)) {
            //        float timeDelay = this.placementResults.Count * AnimatedBox.DelayPerItem;
            //        this.placementResults.Add(new PlacementResult(timeDelay, this.queryStatus.queryResult[i].Clone() as SUDLLOP.ObjectPlacementResult));
            //    }
            //}
            if ((this.queryStatus.queryResult.Count > 0) && (this.queryStatus.queryResult[0].Position.y < alignment.CeilingYValue) && (this.queryStatus.queryResult[0].Position.y > alignment.FloorYValue))
            {
                float timeDelay = this.placementResults.Count * AnimatedBox.DelayPerItem;
                this.placementResults.Add(new PlacementResult(timeDelay, this.queryStatus.queryResult[0].Clone() as SUDLLOP.ObjectPlacementResult, this.queryStatus.drawBox, this.queryStatus.callback));
            }
            else
            {
                this.queryStatus.callback(false, Vector3.zero);
            }

            // Text
            Debug.Log(this.queryStatus.name + " (" + this.placementResults.Count + "/" + (this.queryStatus.countSuccess + this.queryStatus.countFail) + ")");

            // Mark done
            this.queryStatus.Reset();
        }
Exemple #11
0
    private IEnumerator SetupMenu()
    {
        // Setup for queries
        SpatialUnderstandingDllTopology.TopologyResult[] resultsTopology = new SpatialUnderstandingDllTopology.TopologyResult[1];
        IntPtr resultsTopologyPtr = SpatialUnderstanding.Instance.UnderstandingDLL.PinObject(resultsTopology);

#if UNITY_WSA && !UNITY_EDITOR
        // 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();
        System.Threading.Tasks.Task thread = System.Threading.Tasks.Task.Run(() =>
        {
            if (SpatialUnderstandingDllObjectPlacement.Solver_PlaceObject(
                    "UIPlacement",
                    SpatialUnderstanding.Instance.UnderstandingDLL.PinObject(placeOnWallDef),
                    0,
                    IntPtr.Zero,
                    0,
                    IntPtr.Zero,
                    SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticObjectPlacementResultPtr()) == 0)
            {
                placementResult = null;
            }
        });
        while (!thread.IsCompleted)
        {
            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;
        }
#endif

        // Wait a frame
        yield return(null);

        // 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 = Camera.main.transform.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 = Camera.main.transform.position + Camera.main.transform.forward * 2.0f;
        PlaceMenu(new Vector3(defaultPosition.x, Math.Max(defaultPosition.y, alignment.FloorYValue + 1.5f), defaultPosition.z), (new Vector3(Camera.main.transform.forward.x, 0.0f, Camera.main.transform.forward.z)).normalized, true);
        Debug.Log("PlaceMenu - InFrontOfUser");
    }
        /**
         * Handle the shape query results.
         *
         * @param visDesc           visual description
         * @param shapeCount        number of detected shapes
         * @param color             color of the animated box
         * @param defaultHalfDims   default shape dimensions
         * @param nearPos           position near which the shape should be detected
         * @param drawBox           indicates whether an animated box should be drawn around the shape
         * @param callback          callback method
         */
        private void HandleResults(string visDesc, int shapeCount, Color color, Vector3 defaultHalfDims, Vector3?nearPos, bool drawBox, ResponseDelegate callback)
        {
            // Check query permission
            if (!SpatialUnderstanding.IsInitialized || !SpatialUnderstanding.Instance.AllowSpatialUnderstanding)
            {
                return;
            }

            // Alignment information
            SpatialUnderstandingDll.Imports.QueryPlayspaceAlignment(SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceAlignmentPtr());
            SpatialUnderstandingDll.Imports.PlayspaceAlignment alignment = SpatialUnderstanding.Instance.UnderstandingDLL.GetStaticPlayspaceAlignment();

            // Process results
            float   foundDistance = NULL_DISTANCE;
            Vector3 foundPosition = Vector3.zero;

            for (int i = 0; i < shapeCount; i++)
            {
                bool    valid    = true;
                Vector3 halfSize = (this.resultsShape[i].halfDims.sqrMagnitude < 0.01f) ? defaultHalfDims : this.resultsShape[i].halfDims;
                Vector3 position = new Vector3(this.resultsShape[i].position.x, this.resultsShape[i].position.y - halfSize.y, this.resultsShape[i].position.z);

                // Check distance
                if ((nearPos != null) && (callback != null))
                {
                    // Check MIN_DISTANCE, MIN_HEIGHT and MAX_HEIGHT
                    float distance = Vector3.Distance(nearPos.Value, position);
                    valid = (distance <= MAX_DISTANCE) && (position.y >= (alignment.FloorYValue + MIN_HEIGHT)) && (position.y <= (alignment.FloorYValue + MAX_HEIGHT));

                    // Ceck MIN_WIDTH and MIN_DEPTH
                    float width = (this.resultsShape[i].halfDims.x >= this.resultsShape[i].halfDims.z) ? this.resultsShape[i].halfDims.x : this.resultsShape[i].halfDims.z;
                    float depth = (this.resultsShape[i].halfDims.x < this.resultsShape[i].halfDims.z) ? this.resultsShape[i].halfDims.x : this.resultsShape[i].halfDims.z;
                    valid = valid && (width >= (MIN_WIDTH * 0.5)) && (depth >= (MIN_DEPTH * 0.5));

                    // Save position and distance if it is optimal
                    if (valid)
                    {
                        foundDistance = Mathf.Min(foundDistance, distance);
                        foundPosition = (foundDistance == distance) ? position : foundPosition;
                    }
                }

                // Draw an animated box
                if (drawBox && valid)
                {
                    float timeDelay = this.lineBoxList.Count * AnimatedBox.DelayPerItem;
                    this.lineBoxList.Add(new AnimatedBox(timeDelay, this.resultsShape[i].position, Quaternion.LookRotation(alignment.BasisZ, alignment.BasisY), color, halfSize));
                }
            }

            // Send the position and rotation information to the callback function
            if ((nearPos != null) && (callback != null) && (shapeCount != 0) && (foundDistance < NULL_DISTANCE))
            {
                Debug.Log("Shape distance: " + foundDistance);
                callback(true, foundPosition, alignment);
            }
            else if ((nearPos != null) && (callback != null))
            {
                callback(false, Vector3.zero, alignment);
            }

            Debug.Log(string.Format("{0} ({1})", visDesc, shapeCount));
        }