Ejemplo n.º 1
0
            public void UpdatePositions()
            {
                Undo.RecordObjects(grabbedObjects, "Move");

                for (int i = 0; i < grabbedObjects.Length; i++)
                {
                    MathUtilsExt.SetTransformOffset(rayOrigin, grabbedObjects[i], positionOffsets[i], rotationOffsets[i]);
                }
            }
Ejemplo n.º 2
0
        void Update()
        {
            m_TooltipsToHide.Clear();
            foreach (var kvp in m_Tooltips)
            {
                var tooltip     = kvp.Key;
                var tooltipData = kvp.Value;
                var hoverTime   = Time.realtimeSinceStartup - tooltipData.startTime;
                if (hoverTime > k_Delay)
                {
                    var placement = tooltip as ITooltipPlacement;
                    var target    = GetTooltipTarget(tooltip);

                    var tooltipUI = tooltipData.tooltipUI;
                    if (!tooltipUI)
                    {
                        var tooltipObject = Instantiate(m_TooltipPrefab, m_TooltipCanvas);
                        tooltipUI                     = tooltipObject.GetComponent <TooltipUI>();
                        tooltipData.tooltipUI         = tooltipUI;
                        tooltipUI.highlight.material  = m_HighlightMaterial;
                        tooltipUI.background.material = m_TooltipBackgroundMaterial;
                        var tooltipTransform = tooltipObject.transform;
                        MathUtilsExt.SetTransformOffset(target, tooltipTransform, Vector3.zero, Quaternion.identity);
                        tooltipTransform.localScale = Vector3.zero;

                        if (placement == null)
                        {
                            ObjectUtils.Destroy(tooltipUI.dottedLine.gameObject);
                            foreach (var sphere in tooltipUI.spheres)
                            {
                                ObjectUtils.Destroy(sphere.gameObject);
                            }
                        }
                    }

                    var lerp = Mathf.Clamp01((hoverTime - k_Delay) / k_TransitionDuration);
                    UpdateVisuals(tooltip, tooltipUI, target, lerp);
                }

                if (!IsValidTooltip(tooltip))
                {
                    m_TooltipsToHide.Add(tooltip);
                }
            }

            foreach (var tooltip in m_TooltipsToHide)
            {
                HideTooltip(tooltip);
            }
        }
Ejemplo n.º 3
0
        void UpdateVisuals(ITooltip tooltip, TooltipData tooltipData, float lerp)
        {
            var target            = tooltipData.GetTooltipTarget(tooltip);
            var tooltipUI         = tooltipData.tooltipUI;
            var placement         = tooltipData.placement;
            var orientationWeight = tooltipData.orientationWeight;
            var tooltipTransform  = tooltipUI.transform;

            lerp = MathUtilsExt.SmoothInOutLerpFloat(lerp); // shape the lerp for better presentation
            var transitionLerp = MathUtilsExt.SmoothInOutLerpFloat(1.0f - Mathf.Clamp01((Time.time - tooltipData.transitionTime) / k_ChangeTransitionDuration));

            var viewerScale = this.GetViewerScale();

            tooltipTransform.localScale = m_TooltipScale * lerp * viewerScale;

            // Adjust for alignment
            var offset = GetTooltipOffset(tooltipUI, placement, tooltipData.transitionOffset * transitionLerp);

            // The rectTransform expansion is handled in the Tooltip dynamically, based on alignment & text length
            var rotationOffset = Quaternion.identity;
            var camTransform   = CameraUtils.GetMainCamera().transform;

            if (Vector3.Dot(camTransform.forward, target.forward) < 0)
            {
                rotationOffset *= k_FlipYRotation;
            }

            if (Vector3.Dot(camTransform.up, target.up) + orientationWeight < 0)
            {
                rotationOffset *= k_FlipZRotation;
                tooltipData.orientationWeight = -k_TextOrientationWeight;
            }
            else
            {
                tooltipData.orientationWeight = k_TextOrientationWeight;
            }

            MathUtilsExt.SetTransformOffset(target, tooltipTransform, offset * lerp, rotationOffset);

            if (placement != null)
            {
                //TODO: Figure out why rect gives us different height/width than GetWorldCorners
                tooltipUI.rectTransform.GetWorldCorners(k_Corners);
                var bottomLeft = k_Corners[0];
                var halfWidth  = (bottomLeft - k_Corners[2]).magnitude * 0.5f;
                var halfHeight = (bottomLeft - k_Corners[1]).magnitude * 0.5f;

                var source   = placement.tooltipSource;
                var toSource = tooltipTransform.InverseTransformPoint(source.position);

                // Position spheres: one at source, one on the closest edge of the tooltip
                var spheres = tooltipUI.spheres;
                spheres[0].position = source.position;

                var attachedSphere = spheres[1];
                var boxSlope       = halfHeight / halfWidth;
                var toSourceSlope  = Mathf.Abs(toSource.y / toSource.x);

                var parentScale = attachedSphere.parent.lossyScale;
                halfHeight *= Mathf.Sign(toSource.y) / parentScale.x;
                halfWidth  *= Mathf.Sign(toSource.x) / parentScale.y;
                attachedSphere.localPosition = toSourceSlope > boxSlope
                    ? new Vector3(0, halfHeight)
                    : new Vector3(halfWidth, 0);

                // Align dotted line
                var attachedSpherePosition = attachedSphere.position;
                toSource = source.position - attachedSpherePosition;
                var midPoint   = attachedSpherePosition + toSource * 0.5f;
                var dottedLine = tooltipUI.dottedLine;
                var length     = toSource.magnitude;
                var uvRect     = dottedLine.uvRect;
                var worldScale = 1 / viewerScale;
                uvRect.width      = length * k_UVScale * worldScale;
                uvRect.xMin      += k_UVScrollSpeed * Time.deltaTime;
                dottedLine.uvRect = uvRect;

                var dottedLineTransform = dottedLine.transform.parent.GetComponent <RectTransform>();
                dottedLineTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, length / tooltipTransform.lossyScale.x);
                dottedLineTransform.position = midPoint;
                dottedLineTransform.rotation = Quaternion.LookRotation(toSource, -tooltipTransform.forward);
            }
        }
Ejemplo n.º 4
0
            public void OnDragging()
            {
                if (m_Resizing)
                {
                    var viewerScale     = m_WorkspaceUI.GetViewerScale();
                    var pointerPosition = m_WorkspaceUI.GetPointerPosition(rayOrigin);
                    var dragVector      = (pointerPosition - m_DragStart) / viewerScale;
                    var bounds          = m_WorkspaceUI.bounds;
                    var transform       = m_WorkspaceUI.transform;

                    var positionOffsetForward = Vector3.Dot(dragVector, transform.forward) * 0.5f;
                    var positionOffsetRight   = Vector3.Dot(dragVector, transform.right) * 0.5f;

                    switch (m_Direction)
                    {
                    default:
                        bounds.size         = m_BoundsSizeStart + Vector3.back * Vector3.Dot(dragVector, transform.forward);
                        positionOffsetRight = 0;
                        break;

                    case ResizeDirection.Back:
                        bounds.size         = m_BoundsSizeStart + Vector3.forward * Vector3.Dot(dragVector, transform.forward);
                        positionOffsetRight = 0;
                        break;

                    case ResizeDirection.Left:
                        bounds.size           = m_BoundsSizeStart + Vector3.left * Vector3.Dot(dragVector, transform.right);
                        positionOffsetForward = 0;
                        break;

                    case ResizeDirection.Right:
                        bounds.size           = m_BoundsSizeStart + Vector3.right * Vector3.Dot(dragVector, transform.right);
                        positionOffsetForward = 0;
                        break;

                    case ResizeDirection.Front | ResizeDirection.Left:
                        bounds.size = m_BoundsSizeStart + Vector3.left * Vector3.Dot(dragVector, transform.right)
                                      + Vector3.back * Vector3.Dot(dragVector, transform.forward);
                        break;

                    case ResizeDirection.Front | ResizeDirection.Right:
                        bounds.size = m_BoundsSizeStart + Vector3.right * Vector3.Dot(dragVector, transform.right)
                                      + Vector3.back * Vector3.Dot(dragVector, transform.forward);
                        break;

                    case ResizeDirection.Back | ResizeDirection.Left:
                        bounds.size = m_BoundsSizeStart + Vector3.left * Vector3.Dot(dragVector, transform.right)
                                      + Vector3.forward * Vector3.Dot(dragVector, transform.forward);
                        break;

                    case ResizeDirection.Back | ResizeDirection.Right:
                        bounds.size = m_BoundsSizeStart + Vector3.right * Vector3.Dot(dragVector, transform.right)
                                      + Vector3.forward * Vector3.Dot(dragVector, transform.forward);
                        break;
                    }

                    if (m_WorkspaceUI.resize != null)
                    {
                        m_WorkspaceUI.resize(bounds);
                    }

                    var currentExtents = m_WorkspaceUI.bounds.extents;
                    var extents        = bounds.extents;
                    var absRight       = Mathf.Abs(positionOffsetRight);
                    var absForward     = Mathf.Abs(positionOffsetForward);
                    var positionOffset = transform.right * (absRight - (currentExtents.x - extents.x)) * Mathf.Sign(positionOffsetRight)
                                         + transform.forward * (absForward - (currentExtents.z - extents.z)) * Mathf.Sign(positionOffsetForward);

                    m_WorkspaceUI.transform.parent.position = m_PositionStart + positionOffset * viewerScale;
                    m_WorkspaceUI.OnResizing(rayOrigin);
                }
                else
                {
                    MathUtilsExt.SetTransformOffset(rayOrigin, m_WorkspaceUI.transform.parent, m_PositionOffset, m_RotationOffset);
                    m_WorkspaceUI.OnMoving(rayOrigin);
                }
            }
Ejemplo n.º 5
0
        void UpdateVisuals(ITooltip tooltip, TooltipUI tooltipUI, Transform target, float lerp)
        {
            var tooltipTransform = tooltipUI.transform;

            var tooltipText = tooltipUI.text;

            if (tooltipText)
            {
                tooltipText.text = tooltip.tooltipText;
            }

            var viewerScale = getViewerScale();

            tooltipTransform.localScale = m_TooltipScale * lerp * viewerScale;

            var placement = tooltip as ITooltipPlacement;

            // Adjust for alignment
            var offset = Vector3.zero;

            if (placement != null)
            {
                switch (placement.tooltipAlignment)
                {
                case TextAlignment.Right:
                    offset = Vector3.left;
                    break;

                case TextAlignment.Left:
                    offset = Vector3.right;
                    break;
                }
            }

            var rectTransform = tooltipUI.GetComponent <RectTransform>();
            var rect          = rectTransform.rect;
            var halfWidth     = rect.width * 0.5f;
            var halfHeight    = rect.height * 0.5f;

            if (placement != null)
            {
                offset *= halfWidth * rectTransform.lossyScale.x;
            }
            else
            {
                offset = Vector3.back * k_Offset;
            }

            MathUtilsExt.SetTransformOffset(target, tooltipTransform, offset * lerp, Quaternion.identity);

            if (placement != null)
            {
                var source   = placement.tooltipSource;
                var toSource = tooltipTransform.InverseTransformPoint(source.position);

                // Position spheres: one at source, one on the closest edge of the tooltip
                var spheres = tooltipUI.spheres;
                spheres[0].position = source.position;

                var attachedSphere = spheres[1];
                var boxSlope       = halfHeight / halfWidth;
                var toSourceSlope  = Mathf.Abs(toSource.y / toSource.x);

                halfHeight *= Mathf.Sign(toSource.y);
                halfWidth  *= Mathf.Sign(toSource.x);
                attachedSphere.localPosition = toSourceSlope > boxSlope
                                        ? new Vector3(0, halfHeight)
                                        : new Vector3(halfWidth, 0);

                // Align dotted line
                var attachedSpherePosition = attachedSphere.position;
                toSource = source.position - attachedSpherePosition;
                var midPoint   = attachedSpherePosition + toSource * 0.5f;
                var dottedLine = tooltipUI.dottedLine;
                var length     = toSource.magnitude;
                var uvRect     = dottedLine.uvRect;
                var worldScale = 1 / viewerScale;
                uvRect.width      = length * k_UVScale * worldScale;
                uvRect.xMin      += k_UVScrollSpeed * Time.unscaledDeltaTime;
                dottedLine.uvRect = uvRect;

                var dottedLineTransform = dottedLine.transform.parent.GetComponent <RectTransform>();
                dottedLineTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, length / tooltipTransform.lossyScale.x);
                dottedLineTransform.position = midPoint;
                dottedLineTransform.rotation = Quaternion.LookRotation(toSource, -tooltipTransform.forward);
            }
        }
 public void Update(Transform parent)
 {
     MathUtilsExt.SetTransformOffset(parent, transform, m_LocalPositionOffset, m_RotationOffset);
 }
Ejemplo n.º 7
0
            internal void UpdateMiniWorlds(ConsumeControlDelegate consumeControl)
            {
                if (m_MiniWorldIgnoreListDirty)
                {
                    UpdateMiniWorldIgnoreList();
                    m_MiniWorldIgnoreListDirty = false;
                }

                var objectsGrabber = evr.m_DirectSelection.objectsGrabber;

                foreach (var kvp in m_MiniWorldInputs)
                {
                    kvp.Key.ProcessInput(kvp.Value, consumeControl);
                }

                // Update MiniWorldRays
                foreach (var ray in m_Rays)
                {
                    var miniWorldRayOrigin = ray.Key;
                    var miniWorldRay       = ray.Value;

                    if (!miniWorldRay.proxy.active)
                    {
                        miniWorldRay.tester.active = false;
                        continue;
                    }

                    var miniWorld    = miniWorldRay.miniWorld;
                    var inverseScale = miniWorld.miniWorldTransform.lossyScale.Inverse();

                    if (float.IsInfinity(inverseScale.x) || float.IsNaN(inverseScale.x))                     // Extreme scales cause transform errors
                    {
                        continue;
                    }

                    // Transform into reference space
                    var originalRayOrigin  = miniWorldRay.originalRayOrigin;
                    var referenceTransform = miniWorld.referenceTransform;
                    miniWorldRayOrigin.position   = referenceTransform.position + Vector3.Scale(miniWorld.miniWorldTransform.InverseTransformPoint(originalRayOrigin.position), miniWorld.referenceTransform.localScale);
                    miniWorldRayOrigin.rotation   = referenceTransform.rotation * Quaternion.Inverse(miniWorld.miniWorldTransform.rotation) * originalRayOrigin.rotation;
                    miniWorldRayOrigin.localScale = Vector3.Scale(inverseScale, referenceTransform.localScale);

                    var directSelection = evr.m_DirectSelection;

                    // Set miniWorldRayOrigin active state based on whether controller is inside corresponding MiniWorld
                    var originalPointerPosition = originalRayOrigin.position + originalRayOrigin.forward * directSelection.GetPointerLength(originalRayOrigin);
                    var isContained             = miniWorld.Contains(originalPointerPosition);
                    miniWorldRay.tester.active = isContained;
                    miniWorldRayOrigin.gameObject.SetActive(isContained);

                    var directSelectInput = (DirectSelectInput)miniWorldRay.directSelectInput;
                    var dragObjects       = miniWorldRay.dragObjects;

                    if (dragObjects == null)
                    {
                        var heldObjects = objectsGrabber.GetHeldObjects(miniWorldRayOrigin);
                        if (heldObjects != null)
                        {
                            // Only one ray can grab an object, otherwise PlaceObject is called on each trigger release
                            // This does not prevent TransformTool from doing two-handed scaling
                            var otherRayHasObject = false;
                            foreach (var otherRay in m_Rays.Values)
                            {
                                if (otherRay != miniWorldRay && otherRay.dragObjects != null)
                                {
                                    otherRayHasObject = true;
                                }
                            }

                            if (!otherRayHasObject)
                            {
                                miniWorldRay.dragObjects = heldObjects;
                                var scales          = new Vector3[heldObjects.Length];
                                var dragGameObjects = new GameObject[heldObjects.Length];
                                for (var i = 0; i < heldObjects.Length; i++)
                                {
                                    var dragObject = heldObjects[i];
                                    scales[i]          = dragObject.transform.localScale;
                                    dragGameObjects[i] = dragObject.gameObject;
                                }

                                var totalBounds      = ObjectUtils.GetBounds(dragGameObjects);
                                var maxSizeComponent = totalBounds.size.MaxComponent();
                                if (!Mathf.Approximately(maxSizeComponent, 0f))
                                {
                                    miniWorldRay.previewScaleFactor = Vector3.one * (k_PreviewScale * Viewer.GetViewerScale() / maxSizeComponent);
                                }

                                miniWorldRay.originalScales = scales;
                            }
                        }
                    }

                    // Transfer objects to and from original ray and MiniWorld ray (e.g. outside to inside mini world)
                    if (directSelection != null && isContained != miniWorldRay.wasContained)
                    {
                        var pointerLengthDiff = directSelection.GetPointerLength(miniWorldRayOrigin) - directSelection.GetPointerLength(originalRayOrigin);
                        var from = isContained ? originalRayOrigin : miniWorldRayOrigin;
                        var to   = isContained ? miniWorldRayOrigin : originalRayOrigin;
                        if (isContained || miniWorldRay.dragObjects == null)
                        {
                            objectsGrabber.TransferHeldObjects(from, to, pointerLengthDiff * Vector3.forward);
                        }
                    }

                    // Transfer objects between MiniWorlds
                    if (dragObjects == null)
                    {
                        if (isContained)
                        {
                            foreach (var kvp in m_Rays)
                            {
                                var otherRayOrigin = kvp.Key;
                                var otherRay       = kvp.Value;
                                var otherObjects   = otherRay.dragObjects;
                                if (otherRay != miniWorldRay && !otherRay.wasContained && otherObjects != null)
                                {
                                    dragObjects = otherObjects;
                                    miniWorldRay.dragObjects        = otherObjects;
                                    miniWorldRay.originalScales     = otherRay.originalScales;
                                    miniWorldRay.previewScaleFactor = otherRay.previewScaleFactor;

                                    otherRay.dragObjects = null;

                                    if (directSelection != null)
                                    {
                                        var heldObjects = objectsGrabber.GetHeldObjects(otherRayOrigin);
                                        if (heldObjects != null)
                                        {
                                            objectsGrabber.TransferHeldObjects(otherRayOrigin, miniWorldRayOrigin,
                                                                               Vector3.zero);                  // Set the new offset to zero because the object will have moved (this could be improved by taking original offset into account)
                                        }
                                    }

                                    break;
                                }
                            }
                        }
                    }

                    if (isContained && !miniWorldRay.wasContained)
                    {
                        Rays.HideRay(originalRayOrigin, true);
                        Rays.LockRay(originalRayOrigin, this);
                    }

                    if (!isContained && miniWorldRay.wasContained)
                    {
                        Rays.UnlockRay(originalRayOrigin, this);
                        Rays.ShowRay(originalRayOrigin, true);
                    }

                    if (dragObjects == null)
                    {
                        miniWorldRay.wasContained = isContained;
                        continue;
                    }

                    var previewScaleFactor = miniWorldRay.previewScaleFactor;
                    var positionOffsets    = miniWorldRay.originalPositionOffsets;
                    var rotationOffsets    = miniWorldRay.originalRotationOffsets;
                    var originalScales     = miniWorldRay.originalScales;

                    if (directSelectInput.select.isHeld)
                    {
                        if (isContained)
                        {
                            // Scale the object back to its original scale when it re-enters the MiniWorld
                            if (!miniWorldRay.wasContained)
                            {
                                for (var i = 0; i < dragObjects.Length; i++)
                                {
                                    var dragObject = dragObjects[i];
                                    dragObject.localScale = originalScales[i];
                                    MathUtilsExt.SetTransformOffset(miniWorldRayOrigin, dragObject, positionOffsets[i], rotationOffsets[i]);
                                }

                                // Add the object (back) to TransformTool
                                if (directSelection != null)
                                {
                                    objectsGrabber.GrabObjects(miniWorldRay.node, miniWorldRayOrigin, directSelectInput, dragObjects);
                                }
                            }
                        }
                        else
                        {
                            // Check for player head
                            for (var i = 0; i < dragObjects.Length; i++)
                            {
                                var dragObject = dragObjects[i];
                                if (dragObject.CompareTag(k_VRPlayerTag))
                                {
                                    if (directSelection != null)
                                    {
                                        objectsGrabber.DropHeldObjects(miniWorldRayOrigin);
                                    }

                                    // Drop player at edge of MiniWorld
                                    miniWorldRay.dragObjects = null;
                                    dragObjects = null;
                                    break;
                                }
                            }

                            if (dragObjects == null)
                            {
                                continue;
                            }

                            if (miniWorldRay.wasContained)
                            {
                                var containedInOtherMiniWorld = false;
                                foreach (var world in m_Worlds)
                                {
                                    if (miniWorld != world && world.Contains(originalPointerPosition))
                                    {
                                        containedInOtherMiniWorld = true;
                                    }
                                }

                                // Don't switch to previewing the objects we are dragging if we are still in another mini world
                                if (!containedInOtherMiniWorld)
                                {
                                    for (var i = 0; i < dragObjects.Length; i++)
                                    {
                                        var dragObject = dragObjects[i];

                                        // Store the original scale in case the object re-enters the MiniWorld
                                        originalScales[i] = dragObject.localScale;

                                        dragObject.localScale = Vector3.Scale(dragObject.localScale, previewScaleFactor);
                                    }

                                    // Drop from TransformTool to take control of object
                                    if (directSelection != null)
                                    {
                                        objectsGrabber.DropHeldObjects(miniWorldRayOrigin, out positionOffsets, out rotationOffsets);
                                        miniWorldRay.originalPositionOffsets = positionOffsets;
                                        miniWorldRay.originalRotationOffsets = rotationOffsets;
                                        miniWorldRay.wasHeld = true;
                                    }
                                }
                            }

                            for (var i = 0; i < dragObjects.Length; i++)
                            {
                                var dragObject = dragObjects[i];
                                var rotation   = originalRayOrigin.rotation;
                                var position   = originalRayOrigin.position
                                                 + rotation * Vector3.Scale(previewScaleFactor, positionOffsets[i]);
                                MathUtilsExt.LerpTransform(dragObject, position, rotation * rotationOffsets[i]);
                            }
                        }
                    }

                    // Release the current object if the trigger is no longer held
                    if (directSelectInput.select.wasJustReleased)
                    {
                        var rayPosition = originalRayOrigin.position;
                        for (var i = 0; i < dragObjects.Length; i++)
                        {
                            var dragObject = dragObjects[i];

                            // If the user has pulled an object out of the MiniWorld, use PlaceObject to grow it back to its original scale
                            if (!isContained)
                            {
                                if (evr.m_Viewer.IsOverShoulder(originalRayOrigin))
                                {
                                    evr.m_SceneObjectModule.DeleteSceneObject(dragObject.gameObject);
                                }
                                else
                                {
                                    dragObject.localScale = originalScales[i];
                                    var rotation = originalRayOrigin.rotation;
                                    dragObject.position = rayPosition + rotation * positionOffsets[i];
                                    dragObject.rotation = rotation * rotationOffsets[i];
                                }
                            }
                        }

                        miniWorldRay.dragObjects = null;
                        miniWorldRay.wasHeld     = false;
                    }

                    miniWorldRay.wasContained = isContained;
                }
            }
Ejemplo n.º 8
0
        void UpdateVisuals(ITooltip tooltip, TooltipUI tooltipUI, Transform target, float lerp)
        {
            var tooltipTransform = tooltipUI.transform;

            lerp = MathUtilsExt.SmoothInOutLerpFloat(lerp);             // shape the lerp for better presentation

            var tooltipText = tooltipUI.text;

            if (tooltipText)
            {
                tooltipText.text  = tooltip.tooltipText;
                tooltipText.color = Color.Lerp(Color.clear, Color.white, lerp);
            }

            var viewerScale = this.GetViewerScale();

            tooltipTransform.localScale = m_TooltipScale * lerp * viewerScale;

            TooltipData toolTipData;

            m_Tooltips.TryGetValue(tooltip, out toolTipData);
            var highlightMaterial = toolTipData != null ? toolTipData.customHighlightMaterial : m_HighlightMaterial;

            tooltipUI.highlight.material = highlightMaterial;

            m_TooltipBackgroundMaterial.SetColor("_Color", Color.Lerp(UnityBrandColorScheme.darker, m_OriginalBackgroundColor, lerp));

            var placement = tooltip as ITooltipPlacement;

            // Adjust for alignment
            var offset = Vector3.zero;

            if (placement != null)
            {
                switch (placement.tooltipAlignment)
                {
                case TextAlignment.Right:
                    offset = Vector3.left;
                    break;

                case TextAlignment.Left:
                    offset = Vector3.right;
                    break;
                }
            }

            var rectTransform = tooltipUI.GetComponent <RectTransform>();
            var rect          = rectTransform.rect;
            var halfWidth     = rect.width * 0.5f;
            var halfHeight    = rect.height * 0.5f;

            if (placement != null)
            {
                offset *= halfWidth * rectTransform.lossyScale.x;
            }
            else
            {
                offset = Vector3.back * k_Offset * this.GetViewerScale();
            }

            MathUtilsExt.SetTransformOffset(target, tooltipTransform, offset * lerp, Quaternion.identity);

            if (placement != null)
            {
                var source   = placement.tooltipSource;
                var toSource = tooltipTransform.InverseTransformPoint(source.position);

                // Position spheres: one at source, one on the closest edge of the tooltip
                var spheres = tooltipUI.spheres;
                spheres[0].position = source.position;

                var attachedSphere = spheres[1];
                var boxSlope       = halfHeight / halfWidth;
                var toSourceSlope  = Mathf.Abs(toSource.y / toSource.x);

                halfHeight *= Mathf.Sign(toSource.y);
                halfWidth  *= Mathf.Sign(toSource.x);
                attachedSphere.localPosition = toSourceSlope > boxSlope
                                        ? new Vector3(0, halfHeight)
                                        : new Vector3(halfWidth, 0);

                // Align dotted line
                var attachedSpherePosition = attachedSphere.position;
                toSource = source.position - attachedSpherePosition;
                var midPoint   = attachedSpherePosition + toSource * 0.5f;
                var dottedLine = tooltipUI.dottedLine;
                var length     = toSource.magnitude;
                var uvRect     = dottedLine.uvRect;
                var worldScale = 1 / viewerScale;
                uvRect.width      = length * k_UVScale * worldScale;
                uvRect.xMin      += k_UVScrollSpeed * Time.deltaTime;
                dottedLine.uvRect = uvRect;

                var dottedLineTransform = dottedLine.transform.parent.GetComponent <RectTransform>();
                dottedLineTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, length / tooltipTransform.lossyScale.x);
                dottedLineTransform.position = midPoint;
                dottedLineTransform.rotation = Quaternion.LookRotation(toSource, -tooltipTransform.forward);
            }
        }