protected override void WhileHandTracked(Hand hand) { // Update pose with the position of the pinch, which is theoretical if there's no // pinch but absolute when a pinch is actually occuring. var pinchPosition = hand.GetPredictedPinchPosition(); var avgIndexThumbTip = ((hand.GetIndex().TipPosition + hand.GetThumb().TipPosition) / 2f).ToVector3(); pinchPosition = Vector3.Lerp(pinchPosition, avgIndexThumbTip, _latestPinchStrength); _lastPinchPose = new Pose() { position = pinchPosition, rotation = hand.Rotation.ToQuaternion() }; // Reset the "degenerate conditions" timer if we detect that we're looking down // the wrist of the hand; here fingers are usually occluded, so we want to ignore // pinch information in this case. var lookingDownWrist = Vector3.Angle(hand.DistalAxis(), hand.PalmPosition.ToVector3() - Camera.main.transform.position) < 25f; if (lookingDownWrist) { if (_drawDebug) { DebugPing.Ping(hand.WristPosition.ToVector3(), Color.black, 0.10f); } minReactivateSinceDegenerateConditionsTimer = 0; } }
public Pose GetPose() { var handleKinematicState = handleKinematicStateProvider.GetKinematicState(); var handlePose = handleKinematicState.pose; var handleToAttachedUIPose = handleAttachmentPoseProvider.GetHandleToAttachmentPose(); var layoutPos = LayoutUtils.LayoutThrownUIPosition2( Camera.main.transform.ToPose(), //handlePose.position, handlePose.Then(handleToAttachedUIPose).position, handleKinematicState.movement.velocity, optimalHeightFromHead: optimalHeightFromHead, optimalDistance: optimalDistance); if (drawDebug) { DebugPing.Ping(handlePose, LeapColor.red, 0.2f); DebugPing.PingCapsule(handlePose.position, layoutPos, LeapColor.purple, 0.2f); DebugPing.Ping(layoutPos, LeapColor.blue, 0.2f); } var solvedHandlePose = new Pose(layoutPos, Utils.FaceTargetWithoutTwist(layoutPos, Camera.main.transform.position, flip180)) .Then(handleToAttachedUIPose.inverse); return(solvedHandlePose); }
private void renderNewReceiverGained(Receiver receiver) { if (getControllerPointFunc == null) { getControllerPointFunc = getControllerPoint; } if (getReceiverPointFunc == null) { getReceiverPointFunc = getReceiverPoint; } DebugPing.PingCone(getControllerPointFunc, getReceiverPointFunc, renderColor, 0.3f, DebugPing.AnimType.Fade); DebugPing.Ping(getReceiverPointFunc, renderColor, 0.40f, DebugPing.AnimType.ExpandAndFade); DebugPing.Ping(getReceiverPointFunc, renderColor, 0.42f, DebugPing.AnimType.ExpandAndFade); DebugPing.Ping(getReceiverPointFunc, renderColor, 0.44f, DebugPing.AnimType.ExpandAndFade); DebugPing.Ping(getReceiverPointFunc, renderColor, 0.46f, DebugPing.AnimType.ExpandAndFade); DebugPing.Ping(getReceiverPointFunc, renderColor, 0.48f, DebugPing.AnimType.ExpandAndFade); }
protected override bool ShouldGestureActivate(Hand leftHand, Hand rightHand) { Vector3 positionOfInterest; bool isGesturePoseHeld = IsGesturePoseHeld(leftHand, rightHand, out positionOfInterest); _lastKnownPositionOfInterest = positionOfInterest; if (!isGesturePoseHeld && !_gestureReady) { _gestureReady = true; } bool isRelativeHandVelocityLow = updateIsRelativeHandVelocityLow(leftHand, rightHand); if (isRelativeHandVelocityLow && isGesturePoseHeld && _gestureReady) { if (drawHeldPoseDebug) { DebugPing.Ping(positionOfInterest, LeapColor.red, 0.10f); } return(true); } return(false); }
private void fireOnMoved(Pose movedToPose) { OnMoved(); OnMovedHandle(this, movedToPose); if (drawDebugGizmos) { DebugPing.Ping(intObj.transform.position, LeapColor.blue, 0.075f); } }
protected override void WhileGestureActive(Hand hand) { if (_drawDebugPath) { DebugPing.Ping(hand.GetPredictedPinchPosition(), LeapColor.amber, 0.05f); } // TODO: Make this a part of OneHandedGesture so this doesn't have to be explicit! OnSend(this.pose); }
private void fireOnPickedUp() { if (anchObj.isAttached) { OnPlaced(); } OnPickedUp(); OnPickedUpHandle(this); if (drawDebugGizmos) { DebugPing.Ping(intObj.transform.position, LeapColor.cyan, 0.5f); } }
private bool updateIsRelativeHandVelocityLow(Hand leftHand, Hand rightHand) { var leftHandPos = leftHand.PalmPosition.ToVector3(); var rightHandPos = rightHand.PalmPosition.ToVector3(); var leftMinusRightHandPosition = leftHandPos - rightHandPos; leftMinusRightHandPosBuffer.Add(leftMinusRightHandPosition, Time.time); if (leftMinusRightHandPosBuffer.IsFull) { var relativeHandVelocity = leftMinusRightHandPosBuffer.Delta(); if (drawHeldPoseDebug) { RuntimeGizmoDrawer drawer = null; if (RuntimeGizmoManager.TryGetGizmoDrawer(out drawer)) { drawer.DrawBar(relativeHandVelocity.magnitude, (leftHandPos + rightHandPos) / 2f, Vector3.up, 0.1f); } } if (relativeHandVelocity.sqrMagnitude < MAX_RELATIVE_HAND_VELOCITY_SQR) { return(true); } else { if (drawHeldPoseDebug) { DebugPing.Ping((leftHandPos + rightHandPos) / 2f, LeapColor.black, 0.2f); } return(false); } } if (drawHeldPoseDebug) { DebugPing.Ping((leftHandPos + rightHandPos) / 2f, LeapColor.gray, 0.1f); } return(false); }
protected override bool ShouldGestureDeactivate(Hand leftHand, Hand rightHand, out DeactivationReason?deactivationReason) { Vector3 positionOfInterest; bool isGesturePoseHeld = IsGesturePoseHeld(leftHand, rightHand, out positionOfInterest); _lastKnownPositionOfInterest = positionOfInterest; if (updateIsRelativeHandVelocityLow(leftHand, rightHand) && isGesturePoseHeld) { if (_gestureActiveTime > holdActivationDuration) { // Gesture finished successfully. if (drawHeldPoseDebug) { DebugPing.Ping(positionOfInterest, LeapColor.cerulean, 0.20f); } deactivationReason = DeactivationReason.FinishedGesture; _gestureReady = false; return(true); } else { // Continue gesture activation. deactivationReason = null; return(false); } } else { // Gesture was cancelled. deactivationReason = DeactivationReason.CancelledGesture; return(true); } }
private void onGraspEnd() { if (anchObj != null && anchObj.preferredAnchor != null) { OnPlacedInContainer(); OnPlacedHandleInContainer(this); } else if (intObj.rigidbody.velocity.magnitude > PhysicalInterfaceUtils.MIN_THROW_SPEED) { OnThrown(intObj.rigidbody.velocity); OnThrownHandle(this, intObj.rigidbody.velocity); } else { OnPlaced(); OnPlacedHandle(this); } if (drawDebugGizmos) { DebugPing.Ping(intObj.transform.position, LeapColor.orange, 0.5f); } }
protected override bool ShouldGestureDeactivate(Hand hand, out DeactivationReason? deactivationReason) { deactivationReason = DeactivationReason.FinishedGesture; bool shouldDeactivate = false; _latestPinchStrength = 1f; OnPinchStrengthEvent.Invoke(_latestPinchStrength); if (minDeactivateTimer > MIN_DEACTIVATE_TIME) { var pinchDistance = PinchSegment2SegmentDisplacement(hand).magnitude; if (pinchDistance > pinchDeactivateDistance) { shouldDeactivate = true; if (_drawDebug) { DebugPing.Ping(hand.GetPredictedPinchPosition(), Color.black, 0.20f); } } } else { minDeactivateTimer++; } if (shouldDeactivate) { minReactivateTimer = 0; } return(shouldDeactivate); }
public void UpdateCentroidMovement(Vector3?[] positions, float[] strengths = null, bool drawDebug = false) { if (strengths != null && positions.Length != strengths.Length) { throw new InvalidOperationException( "positions and strengths Indexables must have the same Count."); } bool[] useableIndices = new bool[_lastPositions.Length]; _didCentroidAppear = false; _didCentroidDisappear = false; int numLastValidPositions = CountValid(_lastPositions); int numCurValidPositions = CountValid(positions); if (numLastValidPositions == 0 && numCurValidPositions > 0) { _didCentroidAppear = true; } if (numLastValidPositions > 0 && numCurValidPositions == 0) { _didCentroidDisappear = true; } // Useable indices have valid positions in both the "last" and "current" arrays. for (int i = 0; i < _lastPositions.Length; i++) { if (i >= positions.Length) { break; } var lastV = _lastPositions[i]; var curV = positions[i]; if (lastV.HasValue && curV.HasValue) { useableIndices[i] = true; } else if (!lastV.HasValue && !curV.HasValue) { // One index has a value in one array and no value in the other; // this means the Centroid is going to teleport. _didCentroidTeleport = true; } } _isMoving = false; _avgDelta = Vector3.zero; int count = 0; for (int i = 0; i < useableIndices.Length; i++) { if (useableIndices[i]) { _isMoving = true; var addedDelta = (positions[i] - _lastPositions[i]).Value; if (strengths != null) { addedDelta *= strengths[i]; } _avgDelta += addedDelta; count++; } } if (count > 0) { _avgDelta /= count; } // Update centroid state. if (_didCentroidAppear) { _centroid = positions.Query() .Select(maybeV => maybeV.GetValueOrDefault()) .Fold((acc, v) => acc + v) / numCurValidPositions; if (drawDebug) { DebugPing.Ping(_centroid.Value, LeapColor.cyan, 0.20f); } } if (_centroid != null) { _centroid += _avgDelta; if (drawDebug) { DebugPing.Ping(_centroid.Value, LeapColor.green, 0.15f); } } if (_didCentroidDisappear) { if (drawDebug) { DebugPing.Ping(_centroid.Value, LeapColor.black, 0.20f); } _centroid = null; } // Set last positions with the current positions. for (int i = 0; i < _lastPositions.Length; i++) { if (i >= positions.Length) { _lastPositions[i] = null; } else { _lastPositions[i] = positions[i]; } } }
public Vector3 GetTargetPosition() { Vector3 layoutPos; if (!uiHandle.wasThrown) { layoutPos = uiHandle.pose.position; if (drawDebug) { DebugPing.Ping(layoutPos, Color.white); } } else { // When the UI is thrown, utilize the static thrown UI util to calculate a decent // final position relative to the user's head given the position and velocity of // the throw. layoutPos = PhysicalInterfaceUtils.LayoutThrownUIPosition2( Camera.main.transform.ToPose(), uiHandle.pose.position, uiHandle.movement.velocity, layoutDistanceMultiplier ); // However, UIs whose central "look" anchor is in a different position than their // grabbed/thrown anchor shouldn't be placed directly at the determined position. // Rather, we need to adjust this position so that the _look anchor,_ not the // thrown handle, winds up in the calculated position from the throw. // Start with the "final" pose as it would currently be calculated. // We need to know the target rotation of the UI based on the target position in // order to adjust the final position properly. Pose finalUIPose = new Pose(layoutPos, GetTargetRotationForPosition(layoutPos)); // We assume the uiAnchorHandle and the uiLookAnchor are rigidly connected. Vector3 curHandleToLookAnchorOffset = (uiLookPositionProvider.GetTargetWorldPosition() - uiHandle.pose.position); // We undo the current rotation of the UI handle and apply that rotation // on the current world-space offset between the handle and the look anchor. // Then we apply the final rotation of the UI to this unrotated offset vector, // giving us the expected final offset between the position that was calculated // by the layout function and the handle. Vector3 finalRotatedLookAnchorOffset = finalUIPose.rotation * (Quaternion.Inverse(uiHandle.pose.rotation) * curHandleToLookAnchorOffset); // We adjust the layout position by this offset, so now the UI should wind up // with its lookAnchor at the calculated location instead of the handle. layoutPos = layoutPos - finalRotatedLookAnchorOffset; // We also adjust any interface positions down a bit. layoutPos += (Camera.main.transform.parent != null ? -Camera.main.transform.parent.up : Vector3.down) * 0.19f; if (drawDebug) { DebugPing.Ping(layoutPos, Color.red); } } return(layoutPos); }
void Update() { _wasActivated = false; _wasDeactivated = false; _wasCancelled = false; _wasFinished = false; // Update the sequence. if (sequenceGraph.Length != 0) { var curGestureNode = sequenceGraph[_curSequenceIdx]; bool shouldActivate = false; bool shouldCancel = false; bool shouldFinish = false; // This gesture sequence as long as the current gesture in the sequence is // active. if (curGestureNode.gesture.isActive) { shouldActivate = true; // Check if the current gesture is an IPoseGesture, in which case, inherit its // pose. if (curGestureNode.gesture is IPoseGesture) { this._latestGesturePose = (curGestureNode.gesture as IPoseGesture).pose; } } if (curGestureNode.gesture.wasFinished) { // This gesture was completed, so next frame we'll look at the next // gesture in the sequence. _curSequenceIdx += 1; if (drawDebug) { DebugPing.Ping(Camera.main.transform.position + Camera.main.transform.forward * 0.5f, LeapColor.blue, 0.1f * _curSequenceIdx); } // Also reset the gesture timer. _nextGestureTimer = 0f; if (_curSequenceIdx == sequenceGraph.Length) { // We hit the end of the sequence successfully! shouldFinish = true; if (drawDebug) { DebugPing.Ping(Camera.main.transform.position + Camera.main.transform.forward * 0.5f, LeapColor.green, 0.11f * _curSequenceIdx); DebugPing.Ping(Camera.main.transform.position + Camera.main.transform.forward * 0.5f, LeapColor.yellow, 0.105f * _curSequenceIdx); } } } else { // Wait for the gesture to begin, or cancel this sequence if the // current gesture in the sequence was cancelled. if (curGestureNode.gesture.wasCancelled) { if (drawDebug) { DebugPing.Ping(Camera.main.transform.position + Camera.main.transform.forward * 0.5f, LeapColor.black, 0.11f * _curSequenceIdx); DebugPing.Ping(Camera.main.transform.position + Camera.main.transform.forward * 0.5f, LeapColor.red, 0.105f * _curSequenceIdx); } shouldCancel = true; } else if (!curGestureNode.gesture.isActive) { _nextGestureTimer += Time.deltaTime; if (_nextGestureTimer > curGestureNode.waitDuration) { if (drawDebug) { DebugPing.Ping(Camera.main.transform.position + Camera.main.transform.forward * 0.5f, LeapColor.black, 0.11f * _curSequenceIdx); DebugPing.Ping(Camera.main.transform.position + Camera.main.transform.forward * 0.5f, LeapColor.black, 0.105f * _curSequenceIdx); } shouldCancel = true; } } } // Set this gesture state appropriately. if (shouldActivate) { if (!_isActive) { _wasActivated = true; } _isActive = true; } else if (shouldCancel) { if (_isActive) { _isActive = false; _wasDeactivated = true; _wasCancelled = true; _wasFinished = false; } } else if (shouldFinish) { if (_isActive) { _isActive = false; _wasDeactivated = true; _wasCancelled = false; _wasFinished = true; } } if (_wasCancelled || _wasFinished) { _curSequenceIdx = 0; } } }
protected override bool ShouldGestureActivate(Hand hand) { bool shouldActivate = false; var wasEligibleLastCheck = _isGestureEligible; _isGestureEligible = false; // Update curl samples for each index and middle. updateIndexCurl(hand); updateMiddleCurl(hand); // Need to update the "pinch strength" during processing. _latestPinchStrength = 0f; // Can only activate a pinch if we haven't already activated a pinch very recently. if (minReactivateTimer > MIN_REACTIVATE_TIME) { // Can only activate a pinch if we're a certain number of frames past the last // frame where the hand was in a tracking-degenerate orientation (e.g. looking // down the wrist.) if (minReactivateSinceDegenerateConditionsTimer > MIN_REACTIVATE_TIME_SINCE_DEGENERATE_CONDITIONS) { // Update pinch and hand position samples. var latestPinchDistance = GetCustomPinchDistance(hand); OnPinchStrengthEvent.Invoke(latestPinchDistance); _handPositionBuffer.Add(hand.PalmPosition.ToVector3(), Time.time); // Full buffer == optimally stable hand velocity, also implicitly enforces // a hand lifetime. Hand velocity NOT CURRENTLY ACTUALLY USED. if (_handPositionBuffer.IsFull) { // Determine whether the hand meets the FOV heuristic -- result may be // ignored depending on public settings. var handFOVAngle = Vector3.Angle(Camera.main.transform.forward, hand.PalmPosition.ToVector3() - Camera.main.transform.position); var handWithinFOV = handFOVAngle < Camera.main.fieldOfView / 2.2f; // Heuristic: Higher hand velocity == more stringent pinch requirement. // Goal: Reduce accidental pinches when e.g. dropping the hands by requiring // a more "certain" pinch while the hand is moving. // CURRENTLY UNUSED. Comment left here as potential inspiration for // additional heuristics. TODO DELETEME. #pragma warning disable 0219 var handVelocity = _handPositionBuffer.Delta(); #pragma warning restore 0219 #region Middle Finger Safety var palmDir = hand.PalmarAxis(); var middleDir = hand.GetMiddle().bones[1].Direction.ToVector3(); var signedMiddlePalmAngle = Vector3.SignedAngle(palmDir, middleDir, hand.RadialAxis()); if (hand.IsLeft) { signedMiddlePalmAngle *= -1f; } #endregion #region Ring Finger Safety var ringDir = hand.GetRing().bones[1].Direction.ToVector3(); var signedRingPalmAngle = Vector3.SignedAngle(palmDir, ringDir, hand.RadialAxis()); if (hand.IsLeft) { signedRingPalmAngle *= -1f; } #endregion #region Index Angle (Eligibility Only) // Note: obviously pinching already requires the index finger to // close relative to the palm -- this check simply drives the // isEligible state for this pinch gesture so that the gesture isn't // "eligible" when the hand is fully open. var indexDir = hand.GetIndex().bones[1].Direction.ToVector3(); var indexPalmAngle = Vector3.Angle(indexDir, palmDir); #endregion #region Thumb Angle (Eligibility Only) // Note: obviously pinching already requires the thumb finger to // close to touch the index finger -- this check simply drives the // isEligible state for this pinch gesture so that the gesture isn't // "eligible" when the hand is fully open. var thumbDir = hand.GetThumb().bones[2].Direction.ToVector3(); var thumbPalmAngle = Vector3.Angle(thumbDir, palmDir); #endregion #region Check: Eligibility // Eligibility checks -- necessary, but not sufficient conditions to start // a pinch, suitable for e.g. visual feedback on whether the gesture is // "able to occur" or "about to occur." if ( ((!wasEligibleLastCheck && signedMiddlePalmAngle >= minPalmMiddleAngle) || (wasEligibleLastCheck && signedMiddlePalmAngle >= minPalmMiddleAngle * ringMiddleSafetyHysteresisMult) || !requireMiddleAndRingSafetyPinch) && ((!wasEligibleLastCheck && signedRingPalmAngle >= minPalmRingAngle) || (wasEligibleLastCheck && signedRingPalmAngle >= minPalmRingAngle * ringMiddleSafetyHysteresisMult) || !requireMiddleAndRingSafetyPinch) // Index angle (eligibility state only) && ((!wasEligibleLastCheck && indexPalmAngle < maxIndexAngleForEligibilityActivation) || (wasEligibleLastCheck && indexPalmAngle < maxIndexAngleForEligibilityDeactivation)) // Thumb angle (eligibility state only) && ((!wasEligibleLastCheck && thumbPalmAngle < maxThumbAngleForEligibilityActivation) || (wasEligibleLastCheck && thumbPalmAngle < maxThumbAngleForEligibilityDeactivation)) // FOV. && (handWithinFOV) // Must cross pinch threshold from a non-pinching / non-fist pose. && (!requiresRepinch) ) { // Conceptually, this should be true when all but the most essential // parameters for the gesture are satisfied, so the user can be notified // that the gesture is imminent. _isGestureEligible = true; } #endregion #region Update Pinch Strength // Update global "pinch strength". // If the gesture is eligible, we'll have a non-zero pinch strength. if (_isGestureEligible) { _latestPinchStrength = latestPinchDistance.Map(0f, pinchActivateDistance, 1f, 0f); } else { _latestPinchStrength = 0f; } #endregion #region Check: Pinch Distance if (_isGestureEligible // Absolute pinch strength. && (latestPinchDistance < pinchActivateDistance) ) { shouldActivate = true; if (_drawDebug) { DebugPing.Ping(hand.GetPredictedPinchPosition(), Color.red, 0.20f); } } #endregion #region Hysteresis for Failed Pinches // "requiresRepinch" prevents a closed-finger configuration from beginning // a pinch when the index and thumb never actually actively close from a // valid position -- think, closed-fist to safety-pinch, as opposed to // open-hand to safety-pinch -- without introducing any velocity-based // requirement. if (latestPinchDistance < pinchActivateDistance && !shouldActivate) { requiresRepinch = true; } if (requiresRepinch && latestPinchDistance > failedPinchResetDistance) { requiresRepinch = false; } #endregion } } else { minReactivateSinceDegenerateConditionsTimer += 1; } } else { minReactivateTimer += 1; } if (shouldActivate) { minDeactivateTimer = 0; } OnPinchStrengthEvent.Invoke(_latestPinchStrength); return(shouldActivate); }
private void Update() { GetComponentsInChildren <LODItem>(items); var camera = Camera.main; counter += 1; var pingThisFrame = false; if (counter % 10 == 0) { counter = 0; if (drawDebug) { pingThisFrame = true; } } var selector = GetComponent <PullTabSelector>(); var closestAngle = float.PositiveInfinity; LODItem closestItem = null; foreach (var item in items) { var testAngle = Vector3.Angle(camera.transform.forward, item.transform.position - camera.transform.position); var testDist = Vector3.Distance(item.transform.position, camera.transform.position); if (pingThisFrame) { DebugPing.Ping(item.transform.position, LeapColor.blue, 0.08f); Debug.Log(testAngle); } if (testAngle < closestAngle && testAngle <= maxCameraLookAngle && testDist <= maxDetailDistance) { closestAngle = testAngle; if (selector != null && selector.listOpenCloseAmount < 0.10f) { var activeMarbleItem = selector.activeMarbleParent.GetComponentInChildren <LODItem>(); if (item == activeMarbleItem) { closestItem = item; } } else { closestItem = item; } if (pingThisFrame) { DebugPing.Ping(item.transform.position, LeapColor.red, 0.09f); } } } if (viewMode == ViewMode.Full) { foreach (var item in items) { if (closestItem != null) { if (item.propertySwitch.GetIsOffOrTurningOff()) { item.propertySwitch.On(); } } else { if (item.propertySwitch.GetIsOnOrTurningOn()) { item.propertySwitch.Off(); } } } } else if (viewMode == ViewMode.Fade) { foreach (var item in items) { var detailActivation = 0f; if (closestItem != null) { float dist; switch (fadeDistanceMode) { case FadeDistanceMode.FastFalloff: dist = Mathf.Sqrt((item.transform.position - closestItem.transform.position).magnitude); detailActivation = dist.Map(0f, Mathf.Sqrt(fadeRadius), 1f, 0f); break; case FadeDistanceMode.SlowFalloff: dist = (item.transform.position - closestItem.transform.position).sqrMagnitude; detailActivation = dist.Map(0f, fadeRadius * fadeRadius, 1f, 0f); break; case FadeDistanceMode.Linear: default: dist = (item.transform.position - closestItem.transform.position).magnitude; detailActivation = dist.Map(0f, fadeRadius, 1f, 0f); break; } if (detailActivation < cutoffActivationPercent) { detailActivation = 0f; } } if (item.tweenSwitch != null) { item.tweenSwitch.SetTweenTarget(detailActivation); } else { Debug.LogError("Can't use Fade view mode for this item; it doesn't use a " + "TweenSwitch.", item); } } } else if (viewMode == ViewMode.Single) { foreach (var item in items) { if (item != closestItem && item.tweenSwitch != null && item.propertySwitch.GetIsOnOrTurningOn()) { item.propertySwitch.Off(); } } // Older: one at a time; if (closestItem != null && closestItem.tweenSwitch != null && closestItem.propertySwitch.GetIsOffOrTurningOff()) { closestItem.propertySwitch.On(); } } if (pingThisFrame && closestItem != null) { DebugPing.Ping(closestItem.transform.position, LeapColor.purple, 0.10f); } }
private void renderLossOfSignal() { // TODO: Currently unused, signal is never nullified. DebugPing.Ping(controllerPoint, Color.black); }